diff options
| author | YongDo-Hyun <froster12@naver.com> | 2025-11-26 20:10:42 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-03-27 19:57:09 +0300 |
| commit | edbbe8dcfd30fcfe84f6b62240e22dbf9138677c (patch) | |
| tree | 8b9c8edb939d573d76d6390535d3eacf4342e9c4 | |
| parent | 687e43031df0dc641984b4256bcca50d5b3f7de3 (diff) | |
| download | Project-Tick-edbbe8dcfd30fcfe84f6b62240e22dbf9138677c.tar.gz Project-Tick-edbbe8dcfd30fcfe84f6b62240e22dbf9138677c.zip | |
feat: add local test executable and improve JSON string escaping - Added option to build a local test executable for value assignments. - Enhanced JSON string formatting by escaping special characters. - Updated README with build instructions and prerequisites. - Modified .gitignore to include .vscode directory. - Added file read/write tests in format_test.cpp. - Refactored value assignment logic to reduce code duplication.
Signed-off-by: YongDo-Hyun <froster12@naver.com>
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | README.md | 67 | ||||
| -rw-r--r-- | src/io/ozlibstream.cpp | 17 | ||||
| -rw-r--r-- | src/text/json_formatter.cpp | 29 | ||||
| -rw-r--r-- | src/value.cpp | 180 | ||||
| -rw-r--r-- | test/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | test/format_test.cpp | 21 | ||||
| -rw-r--r-- | test/test_value.cpp | 34 |
9 files changed, 243 insertions, 117 deletions
diff --git a/.gitignore b/.gitignore index bbd5e827cb..a5e8264be3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /obj /build /doxygen +.vscode
\ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e3486c400..cb3bff5baa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,8 @@ endif() add_library(${NBT_NAME} ${NBT_SOURCES}) target_include_directories(${NBT_NAME} PUBLIC include ${CMAKE_CURRENT_BINARY_DIR}) +# Local developer tests are built in the test subdirectory now + # Install it if(DEFINED NBT_DEST_DIR) install( @@ -16,3 +16,70 @@ libnbt++2 is a remake of the old libnbt++ library with the goal of making it more easily usable and fixing some problems. The old libnbt++ especially suffered from a very convoluted syntax and boilerplate code needed to work with NBT data. + +## Building + +This project uses CMake for building. Ensure you have CMake installed. + +### Prerequisites +- C++11 compatible compiler +- CMake 3.15 or later +- ZLIB (optional, for compressed NBT support) + +### Build Steps +1. Clone the repository: + ``` + git clone https://github.com/PrismLauncher/libnbtplusplus.git + cd libnbtplusplus + ``` + +2. Create a build directory: + ``` + mkdir build + cd build + ``` + +3. Configure with CMake: + ``` + cmake .. + ``` + Options: + - `NBT_BUILD_SHARED=OFF` (default): Build static library + - `NBT_USE_ZLIB=ON` (default): Enable zlib support + - `NBT_BUILD_TESTS=ON` (default): Build tests + +4. Build: + ``` + cmake --build . + ``` + +5. Install (optional): + ``` + cmake --install . + ``` + +## Usage + +Include the headers and link against the library. + +### Example +```cpp +#include <nbt_tags.h> +#include <fstream> +#include <iostream> + +int main() { + // Read an NBT file + std::ifstream file("example.nbt", std::ios::binary); + nbt::tag_compound root = nbt::io::read_compound(file).first; + + // Access data + std::cout << root["some_key"].as<nbt::tag_string>() << std::endl; + + return 0; +} +``` + +## License + +This project is licensed under the GNU General Public License v3.0. See the [COPYING](COPYING) file for details. diff --git a/src/io/ozlibstream.cpp b/src/io/ozlibstream.cpp index cf0f505394..b936219787 100644 --- a/src/io/ozlibstream.cpp +++ b/src/io/ozlibstream.cpp @@ -101,9 +101,20 @@ void ozlibstream::close() } catch(...) { - setstate(badbit); //FIXME: This will throw the wrong type of exception - //but there's no good way of setting the badbit - //without causing an exception when exceptions is set + // Setting the stream state while exceptions are enabled may cause + // `setstate` to throw an `ios_base::failure` which would replace + // the original exception. Temporarily disable exceptions on this + // stream, set the `badbit`, then restore the exception mask. + std::ios_base::iostate old_ex = exceptions(); + try { + exceptions(std::ios_base::goodbit); + setstate(std::ios_base::badbit); + } + catch(...) { + // If anything unexpected happens while setting state, swallow + // it — we don't want this to throw here. + } + exceptions(old_ex); } } diff --git a/src/text/json_formatter.cpp b/src/text/json_formatter.cpp index efa807f58c..bd882c15d2 100644 --- a/src/text/json_formatter.cpp +++ b/src/text/json_formatter.cpp @@ -71,7 +71,11 @@ namespace //anonymous { os << "[" << ba.size() << " bytes]"; } void visit(const tag_string& s) override - { os << '"' << s.get() << '"'; } //TODO: escape special characters + { + os << '"'; + write_escaped_string(s.get()); + os << '"'; + } void visit(const tag_list& l) override { @@ -198,6 +202,29 @@ namespace //anonymous { os << "null"; } + + void write_escaped_string(const std::string& str) + { + for (char c : str) { + switch (c) { + case '"': os << "\\\""; break; + case '\\': os << "\\\\"; break; + case '\b': os << "\\b"; break; + case '\f': os << "\\f"; break; + case '\n': os << "\\n"; break; + case '\r': os << "\\r"; break; + case '\t': os << "\\t"; break; + default: + if (static_cast<unsigned char>(c) < 32 || c == 127) { + // Control characters, escape as \u00XX + os << "\\u00" << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(static_cast<unsigned char>(c)); + } else { + os << c; + } + break; + } + } + } }; } diff --git a/src/value.cpp b/src/value.cpp index 56c44053a1..8c16da6e23 100644 --- a/src/value.cpp +++ b/src/value.cpp @@ -24,6 +24,10 @@ #include "value.h" #include "nbt_tags.h" #include <typeinfo> +#include <algorithm> +#include <initializer_list> +#include <cstdint> +#include <memory> namespace nbt { @@ -60,145 +64,99 @@ void value::set(tag&& t) } //Primitive assignment -//FIXME: Make this less copypaste! -value& value::operator=(int8_t val) +namespace // helper functions local to this translation unit { - if(!tag_) - set(tag_byte(val)); - else switch(tag_->get_type()) + template<typename T> + void assign_numeric_impl(std::unique_ptr<tag>& tag_ptr, T val, + tag_type default_type) { - case tag_type::Byte: - static_cast<tag_byte&>(*tag_).set(val); - break; - case tag_type::Short: - static_cast<tag_short&>(*tag_).set(val); - break; - case tag_type::Int: - static_cast<tag_int&>(*tag_).set(val); - break; - case tag_type::Long: - static_cast<tag_long&>(*tag_).set(val); - break; - case tag_type::Float: - static_cast<tag_float&>(*tag_).set(val); - break; - case tag_type::Double: - static_cast<tag_double&>(*tag_).set(val); - break; - - default: - throw std::bad_cast(); + using nbt::tag_type; + if(!tag_ptr) + { + switch(default_type) + { + case tag_type::Byte: tag_ptr.reset(new tag_byte(static_cast<int8_t>(val))); break; + case tag_type::Short: tag_ptr.reset(new tag_short(static_cast<int16_t>(val))); break; + case tag_type::Int: tag_ptr.reset(new tag_int(static_cast<int32_t>(val))); break; + case tag_type::Long: tag_ptr.reset(new tag_long(static_cast<int64_t>(val))); break; + case tag_type::Float: tag_ptr.reset(new tag_float(static_cast<float>(val))); break; + case tag_type::Double: tag_ptr.reset(new tag_double(static_cast<double>(val))); break; + default: throw std::invalid_argument("Invalid default_type"); + } + return; + } + + // Determine the incoming tag type for T + auto incoming_type = detail::get_primitive_type<T>::value; + + // If the existing tag is of a narrower type than the incoming type, + // replace it with a new tag of the incoming type so the value is not + // truncated. Otherwise set the existing tag (possibly narrowing). + auto existing_type = tag_ptr->get_type(); + + if(static_cast<int>(existing_type) < static_cast<int>(incoming_type)) + { + // replace with a new, wider tag that preserves the value + switch(incoming_type) + { + case tag_type::Byte: tag_ptr.reset(new tag_byte(static_cast<int8_t>(val))); break; + case tag_type::Short: tag_ptr.reset(new tag_short(static_cast<int16_t>(val))); break; + case tag_type::Int: tag_ptr.reset(new tag_int(static_cast<int32_t>(val))); break; + case tag_type::Long: tag_ptr.reset(new tag_long(static_cast<int64_t>(val))); break; + case tag_type::Float: tag_ptr.reset(new tag_float(static_cast<float>(val))); break; + case tag_type::Double: tag_ptr.reset(new tag_double(static_cast<double>(val))); break; + default: throw std::bad_cast(); + } + return; + } + + // Existing type is same or wider: write into the existing tag (may narrow) + switch(existing_type) + { + case tag_type::Byte: static_cast<tag_byte&>(*tag_ptr).set(static_cast<int8_t>(val)); break; + case tag_type::Short: static_cast<tag_short&>(*tag_ptr).set(static_cast<int16_t>(val)); break; + case tag_type::Int: static_cast<tag_int&>(*tag_ptr).set(static_cast<int32_t>(val)); break; + case tag_type::Long: static_cast<tag_long&>(*tag_ptr).set(static_cast<int64_t>(val)); break; + case tag_type::Float: static_cast<tag_float&>(*tag_ptr).set(static_cast<float>(val)); break; + case tag_type::Double: static_cast<tag_double&>(*tag_ptr).set(static_cast<double>(val)); break; + default: throw std::bad_cast(); + } } +} + +value& value::operator=(int8_t val) +{ + assign_numeric_impl(tag_, val, tag_type::Byte); return *this; } value& value::operator=(int16_t val) { - if(!tag_) - set(tag_short(val)); - else switch(tag_->get_type()) - { - case tag_type::Short: - static_cast<tag_short&>(*tag_).set(val); - break; - case tag_type::Int: - static_cast<tag_int&>(*tag_).set(val); - break; - case tag_type::Long: - static_cast<tag_long&>(*tag_).set(val); - break; - case tag_type::Float: - static_cast<tag_float&>(*tag_).set(val); - break; - case tag_type::Double: - static_cast<tag_double&>(*tag_).set(val); - break; - - default: - throw std::bad_cast(); - } + assign_numeric_impl(tag_, val, tag_type::Short); return *this; } value& value::operator=(int32_t val) { - if(!tag_) - set(tag_int(val)); - else switch(tag_->get_type()) - { - case tag_type::Int: - static_cast<tag_int&>(*tag_).set(val); - break; - case tag_type::Long: - static_cast<tag_long&>(*tag_).set(val); - break; - case tag_type::Float: - static_cast<tag_float&>(*tag_).set(val); - break; - case tag_type::Double: - static_cast<tag_double&>(*tag_).set(val); - break; - - default: - throw std::bad_cast(); - } + assign_numeric_impl(tag_, val, tag_type::Int); return *this; } value& value::operator=(int64_t val) { - if(!tag_) - set(tag_long(val)); - else switch(tag_->get_type()) - { - case tag_type::Long: - static_cast<tag_long&>(*tag_).set(val); - break; - case tag_type::Float: - static_cast<tag_float&>(*tag_).set(val); - break; - case tag_type::Double: - static_cast<tag_double&>(*tag_).set(val); - break; - - default: - throw std::bad_cast(); - } + assign_numeric_impl(tag_, val, tag_type::Long); return *this; } value& value::operator=(float val) { - if(!tag_) - set(tag_float(val)); - else switch(tag_->get_type()) - { - case tag_type::Float: - static_cast<tag_float&>(*tag_).set(val); - break; - case tag_type::Double: - static_cast<tag_double&>(*tag_).set(val); - break; - - default: - throw std::bad_cast(); - } + assign_numeric_impl(tag_, val, tag_type::Float); return *this; } value& value::operator=(double val) { - if(!tag_) - set(tag_double(val)); - else switch(tag_->get_type()) - { - case tag_type::Double: - static_cast<tag_double&>(*tag_).set(val); - break; - - default: - throw std::bad_cast(); - } + assign_numeric_impl(tag_, val, tag_type::Double); return *this; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 234b0cf9d2..41549842cb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -107,3 +107,12 @@ add_executable(format_test format_test.cpp) target_link_libraries(format_test ${NBT_NAME}) add_test(format_test format_test) stop_warnings(format_test) + +# Optional local test executable to verify value assignments (developer helper) +option(NBT_BUILD_LOCAL_TEST "Build a small local test executable for value assignments" ON) +if(NBT_BUILD_LOCAL_TEST) + add_executable(test_value test_value.cpp) + target_link_libraries(test_value ${NBT_NAME}) + add_test(test_value test_value) + stop_warnings(test_value) +endif() diff --git a/test/format_test.cpp b/test/format_test.cpp index 559af11299..1a689ed3f2 100644 --- a/test/format_test.cpp +++ b/test/format_test.cpp @@ -22,7 +22,8 @@ * along with libnbt++. If not, see <http://www.gnu.org/licenses/>. */ //#include "text/json_formatter.h" -//#include "io/stream_reader.h" +#include "io/stream_reader.h" +#include "io/stream_writer.h" #include <fstream> #include <iostream> #include <limits> @@ -32,7 +33,7 @@ using namespace nbt; int main() { - //TODO: Write that into a file + // Write that into a file and read back for testing tag_compound comp{ {"byte", tag_byte(-128)}, {"short", tag_short(-32768)}, @@ -83,4 +84,20 @@ int main() std::cout << "----- default operator<<:\n"; std::cout << comp; std::cout << "\n-----" << std::endl; + + // Write to file and read back + { + std::ofstream out("test_output.nbt", std::ios::binary); + nbt::io::write_compound(out, comp); + } + + { + std::ifstream in("test_output.nbt", std::ios::binary); + auto [read_comp, name] = nbt::io::read_compound(in); + std::cout << "----- read back from file:\n"; + std::cout << read_comp; + std::cout << "\n-----" << std::endl; + } + + return 0; } diff --git a/test/test_value.cpp b/test/test_value.cpp new file mode 100644 index 0000000000..009cefe8ef --- /dev/null +++ b/test/test_value.cpp @@ -0,0 +1,34 @@ +#include <iostream> +#include "nbt_tags.h" +#include "value.h" + +using namespace nbt; + +int main() +{ + try { + value v; + + v = int8_t(-5); + std::cout << "assigned int8_t(-5): as int32=" << int32_t(v) << ", as double=" << double(v) << "\n"; + + v = int16_t(12345); + std::cout << "assigned int16_t(12345): as int32=" << int32_t(v) << ", as double=" << double(v) << "\n"; + + v = int32_t(100000); + std::cout << "assigned int32_t(100000): as int64=" << int64_t(v) << ", as double=" << double(v) << "\n"; + + v = float(3.14f); + std::cout << "assigned float(3.14): as double=" << double(v) << "\n"; + + v = double(2.718281828); + std::cout << "assigned double(2.71828): as double=" << double(v) << "\n"; + + std::cout << "Test finished OK\n"; + } + catch(const std::exception& e) { + std::cerr << "Exception: " << e.what() << "\n"; + return 1; + } + return 0; +} |
