summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYongDo-Hyun <froster12@naver.com>2025-11-26 20:10:42 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-03-27 19:57:09 +0300
commitedbbe8dcfd30fcfe84f6b62240e22dbf9138677c (patch)
tree8b9c8edb939d573d76d6390535d3eacf4342e9c4
parent687e43031df0dc641984b4256bcca50d5b3f7de3 (diff)
downloadProject-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--.gitignore1
-rw-r--r--CMakeLists.txt2
-rw-r--r--README.md67
-rw-r--r--src/io/ozlibstream.cpp17
-rw-r--r--src/text/json_formatter.cpp29
-rw-r--r--src/value.cpp180
-rw-r--r--test/CMakeLists.txt9
-rw-r--r--test/format_test.cpp21
-rw-r--r--test/test_value.cpp34
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(
diff --git a/README.md b/README.md
index 975ef3c1ec..fdc96df533 100644
--- a/README.md
+++ b/README.md
@@ -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;
+}