Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/hot-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ Targets: zero allocation, zero lock, bounded latency, low jitter. Data path: pro

## Determinism and proof
- Loops bounded by input or constant; recursion depth-limited. No hidden syscalls, locale or `errno` side effects. `JOIN_UNLIKELY` on error branches, `JOIN_LIKELY` on the expected path, `JOIN_ALWAYS_INLINE` only on measured inner functions.
- Claim a gain only after measuring in `release` (skill `benchmark`); never trade correctness for speed without an equivalence test (`roundtrip_test.cpp`, `parse_test.cpp`, stress tests). Report what, how, before/after.
- Claim a gain only after measuring in `release` (skill `benchmark`); never trade correctness for speed without an equivalence test (`*_roundtrip_test.cpp`, `parse_test.cpp`, stress tests). Report what, how, before/after.
7 changes: 6 additions & 1 deletion data/include/join/pack.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,18 @@
#include <optional>
#include <sstream>
#include <fstream>
#include <limits>
#include <span>
#include <bit>

// C.
#include <cstring>

namespace join
{
static_assert (std::numeric_limits<float>::is_iec559 && std::numeric_limits<double>::is_iec559,
"MessagePack requires IEEE 754 floating point numbers");

/**
* @brief message pack writer class.
*/
Expand Down Expand Up @@ -296,7 +301,7 @@ namespace join
int setDouble (double value)
{
float f = static_cast<float> (value);
if (std::isfinite (value) && static_cast<double> (f) == value)
if (std::bit_cast<uint64_t> (static_cast<double> (f)) == std::bit_cast<uint64_t> (value))
{
append (0xca);
pack (f);
Expand Down
13 changes: 9 additions & 4 deletions data/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ target_link_libraries(pack_writer.gtest ${JOIN_DATA} GTest::gtest_main)
add_test(NAME pack_writer.gtest COMMAND pack_writer.gtest)
install(TARGETS pack_writer.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)

add_executable(pack_roundtrip.gtest pack_roundtrip_test.cpp)
target_link_libraries(pack_roundtrip.gtest ${JOIN_DATA} GTest::gtest_main)
add_test(NAME pack_roundtrip.gtest COMMAND pack_roundtrip.gtest)
install(TARGETS pack_roundtrip.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)

add_executable(json_error.gtest json_error_test.cpp)
target_link_libraries(json_error.gtest ${JOIN_DATA} GTest::gtest_main)
add_test(NAME json_error.gtest COMMAND json_error.gtest)
Expand All @@ -62,10 +67,10 @@ target_link_libraries(json_canonicalizer.gtest ${JOIN_DATA} GTest::gtest_main)
add_test(NAME json_canonicalizer.gtest COMMAND json_canonicalizer.gtest)
install(TARGETS json_canonicalizer.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)

add_executable(roundtrip.gtest roundtrip_test.cpp)
target_link_libraries(roundtrip.gtest ${JOIN_DATA} GTest::gtest_main)
add_test(NAME roundtrip.gtest COMMAND roundtrip.gtest)
install(TARGETS roundtrip.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)
add_executable(json_roundtrip.gtest json_roundtrip_test.cpp)
target_link_libraries(json_roundtrip.gtest ${JOIN_DATA} GTest::gtest_main)
add_test(NAME json_roundtrip.gtest COMMAND json_roundtrip.gtest)
install(TARGETS json_roundtrip.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)

add_executable(parse.gtest parse_test.cpp)
target_link_libraries(parse.gtest ${JOIN_DATA} GTest::gtest_main)
Expand Down
99 changes: 79 additions & 20 deletions data/tests/json_reader_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1585,27 +1585,86 @@ TEST (JsonReader, encoding)
ASSERT_TRUE (value.isObject ());
EXPECT_EQ (value["\xC3\xB6\xC3\xA9\xC3\xB6"].getString (), "\xE2\x82\xAC\n\xE2\x82\xAC");

for (auto const& doc : {"[\"\xC0\xAF\"]", "[\"\xED\xA0\x80\"]", "[\"\xF5\x80\x80\x80\"]", "[\"\xE2\x28\xA1\"]",
"[\"\xE2\x82\"]", "[\"\xE2\x82\\n\"]", "{\"\xFF\":1}"})
{
stream.clear ();
stream.str (doc);
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);

stream.clear ();
stream.str (doc);
ASSERT_EQ (JsonReader ().deserialize (stream, value), 0) << join::lastError.message ();
}
stream.clear ();
stream.str ("[\"\xC0\xAF\"]");
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);
stream.clear ();
stream.str ("[\"\xC0\xAF\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), 0) << join::lastError.message ();

for (auto const& doc :
{"[\"\\uZZZZ\"]", "[\"\\uDC00\"]", "[\"\\uD800x\"]", "[\"\\uD800\\uZZZZ\"]", "[\"\\uD800\\u0041\"]"})
{
stream.clear ();
stream.str (doc);
ASSERT_EQ (JsonReader ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);
}
stream.clear ();
stream.str ("[\"\xED\xA0\x80\"]");
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);
stream.clear ();
stream.str ("[\"\xED\xA0\x80\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), 0) << join::lastError.message ();

stream.clear ();
stream.str ("[\"\xF5\x80\x80\x80\"]");
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);
stream.clear ();
stream.str ("[\"\xF5\x80\x80\x80\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), 0) << join::lastError.message ();

stream.clear ();
stream.str ("[\"\xE2\x28\xA1\"]");
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);
stream.clear ();
stream.str ("[\"\xE2\x28\xA1\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), 0) << join::lastError.message ();

stream.clear ();
stream.str ("[\"\xE2\x82\"]");
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);
stream.clear ();
stream.str ("[\"\xE2\x82\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), 0) << join::lastError.message ();

stream.clear ();
stream.str ("[\"\xE2\x82\\n\"]");
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);
stream.clear ();
stream.str ("[\"\xE2\x82\\n\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), 0) << join::lastError.message ();

stream.clear ();
stream.str ("{\"\xFF\":1}");
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);
stream.clear ();
stream.str ("{\"\xFF\":1}");
ASSERT_EQ (JsonReader ().deserialize (stream, value), 0) << join::lastError.message ();

stream.clear ();
stream.str ("[\"\\uZZZZ\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);

stream.clear ();
stream.str ("[\"\\uDC00\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);

stream.clear ();
stream.str ("[\"\\uD800x\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);

stream.clear ();
stream.str ("[\"\\uD800\\uZZZZ\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);

stream.clear ();
stream.str ("[\"\\uD800\\u0041\"]");
ASSERT_EQ (JsonReader ().deserialize (stream, value), -1);
EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding);

std::string str ("[\"caf\xC3\xA9\", \"\xE2\x82\"]");
ASSERT_EQ (BasicJsonReader<JsonReadMode::ValidateEncoding> ().deserialize (str, value), -1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ using join::JsonReader;
/**
* @brief Test JSON round trip.
*/
TEST (JsonReader, roundtrip)
TEST (Json, roundtrip)
{
Value value;
JsonReader reader;
Expand Down Expand Up @@ -264,6 +264,38 @@ TEST (JsonReader, roundtrip)
stream.clear ();
ASSERT_NE (value.serialize (writer), -1) << join::lastError.message ();
EXPECT_EQ (stream.str (), "[1.7976931348623157e308]");

stream.clear ();
stream.str ("[Inf]");
ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message ();
stream.str ("");
stream.clear ();
ASSERT_NE (value.serialize (writer), -1) << join::lastError.message ();
EXPECT_EQ (stream.str (), "[Inf]");

stream.clear ();
stream.str ("[-Inf]");
ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message ();
stream.str ("");
stream.clear ();
ASSERT_NE (value.serialize (writer), -1) << join::lastError.message ();
EXPECT_EQ (stream.str (), "[-Inf]");

stream.clear ();
stream.str ("[NaN]");
ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message ();
stream.str ("");
stream.clear ();
ASSERT_NE (value.serialize (writer), -1) << join::lastError.message ();
EXPECT_EQ (stream.str (), "[NaN]");

stream.clear ();
stream.str ("[-NaN]");
ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message ();
stream.str ("");
stream.clear ();
ASSERT_NE (value.serialize (writer), -1) << join::lastError.message ();
EXPECT_EQ (stream.str (), "[-NaN]");
}

/**
Expand Down
89 changes: 67 additions & 22 deletions data/tests/pack_reader_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,46 @@ TEST (PackReader, deserialize)
ASSERT_EQ (PackReader ().deserialize (std::string ({'\xDA', '\x00'}), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);

for (char head : {'\xDD', '\xDC', '\xDF', '\xDE', '\xDB', '\xDA', '\xD9', '\xC6', '\xC5', '\xC4',
'\xD3', '\xCF', '\xCB', '\xD2', '\xCE', '\xCA', '\xD1', '\xCD', '\xD0', '\xCC'})
{
ASSERT_EQ (PackReader ().deserialize (std::string (1, head), value), -1) << int (head & 0xFF);
ASSERT_EQ (join::lastError, SaxErrc::Truncated) << int (head & 0xFF);
}
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xDD'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xDC'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xDF'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xDE'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xDB'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xDA'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xD9'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xC6'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xC5'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xC4'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xD3'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xCF'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xCB'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xD2'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xCE'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xCA'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xD1'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xCD'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xD0'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);
ASSERT_EQ (PackReader ().deserialize (std::string (1, '\xCC'), value), -1);
ASSERT_EQ (join::lastError, SaxErrc::Truncated);

Throwing throwing;
ASSERT_THROW (PackReader ().deserialize (std::string ({'\x91', '\xA1', 'a'}), throwing), std::range_error);
Expand Down Expand Up @@ -1596,23 +1630,34 @@ TEST (PackReader, encoding)
{
using Validator = join::BasicPackReader<join::PackReadMode::ValidateEncoding>;

for (const std::string& document : {std::string ({'\xa2', '\xc3', '\xa9'}), std::string ({'\xc4', '\x01', '\xff'})})
{
std::stringstream stream (document);
join::SaxHandler handler;
ASSERT_EQ (Validator ().deserialize (document, handler), 0) << join::lastError.message ();
ASSERT_EQ (Validator ().deserialize (stream, handler), 0) << join::lastError.message ();
}
join::SaxHandler handler;
std::stringstream stream;

for (const std::string& document : {std::string ({'\xa1', '\xff'}), std::string ({'\x81', '\xa1', '\xff', '\x01'})})
{
std::stringstream stream (document);
join::SaxHandler handler;
ASSERT_EQ (Validator ().deserialize (document, handler), -1);
ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding);
ASSERT_EQ (Validator ().deserialize (stream, handler), -1);
ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding);
}
ASSERT_EQ (Validator ().deserialize (std::string ({'\xa2', '\xc3', '\xa9'}), handler), 0)
<< join::lastError.message ();
stream.clear ();
stream.str (std::string ({'\xa2', '\xc3', '\xa9'}));
ASSERT_EQ (Validator ().deserialize (stream, handler), 0) << join::lastError.message ();

ASSERT_EQ (Validator ().deserialize (std::string ({'\xc4', '\x01', '\xff'}), handler), 0)
<< join::lastError.message ();
stream.clear ();
stream.str (std::string ({'\xc4', '\x01', '\xff'}));
ASSERT_EQ (Validator ().deserialize (stream, handler), 0) << join::lastError.message ();

ASSERT_EQ (Validator ().deserialize (std::string ({'\xa1', '\xff'}), handler), -1);
ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding);
stream.clear ();
stream.str (std::string ({'\xa1', '\xff'}));
ASSERT_EQ (Validator ().deserialize (stream, handler), -1);
ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding);

ASSERT_EQ (Validator ().deserialize (std::string ({'\x81', '\xa1', '\xff', '\x01'}), handler), -1);
ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding);
stream.clear ();
stream.str (std::string ({'\x81', '\xa1', '\xff', '\x01'}));
ASSERT_EQ (Validator ().deserialize (stream, handler), -1);
ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding);

Value value;
ASSERT_EQ (PackReader ().deserialize (std::string ({'\xa1', '\xff'}), value), 0) << join::lastError.message ();
Expand Down
Loading
Loading