mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 17:51:07 +00:00
* cdylib events: Qt-free types, and drop the unused bytes encoder Three follow-ups to the bstr event fix, all in the cdylib events sidecar -- a Qt-FREE translation unit: - An `any`/map event parameter was emitted as a bare QVariant/QVariantMap, which does not compile there. Spell those as their nlohmann aliases (LogosMap / LogosList) and pull in <logos_json.h> when they appear. - std::vector<std::vector<uint8_t>> fell through the impl-header parser's unknown-type fallback to `any`, so the cdylib gate admitted it and the generator then emitted QVariant. Parse it as `[bstr]` so the gate rejects it with a message naming the offending parameter. - The bytes encoder was emitted into every module's sidecar, leaving an unused static function (-Wunused-function) wherever no event carries binary data. Emit it only when a bstr event parameter exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * lp consumer: decode bstr into std::vector<uint8_t> The Qt-free (`lp`) consumer wrappers -- what every universal C++ module gets for its dependencies -- had no QByteArray in their type tables, so a `bstr` event parameter, method argument, or return degraded to QVariant and then to LogosMap. A consumer subscribing to a binary event was handed the raw tagged JSON object {"_bytes": "<base64url>"} instead of the bytes, with no generated decode. Teach the tables about QByteArray (-> std::vector<uint8_t>) and marshal it through the canonical tagged form in both directions: logos::bytesToJson on the way out, logos::jsonToBytes on the way in. Those live in logos_json.h -- Qt-free and protocol-free, so the generated wrappers and module code can share them. The Qt apiStyle already did this via QByteArray::toBase64/fromBase64. Without this, a subscriber written the obvious way -- onBinaryReady([](const std::string&, const std::vector<uint8_t>& payload) {...}) -- compiles (nlohmann::json has an implicit conversion operator) and then throws at runtime on every event, so the callback body silently never runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * tests: cover binary event payloads by value, not just by source text The regression test for #99 asserts on generated source text, so it stays green against an encoder that emits the wrong bytes. Add the value-level half: - tests/sdk/test_logos_json_bytes.cpp exercises the canonical tagged-bytes codec against the RFC 4648 vectors, the URL-safe alphabet, every len%3 tail group, embedded NULs and high bytes, a 109,447-byte payload (the size from #99), and the lenient/padded decode paths. - tests/experimental/test_lidl_gen_cdylib.cpp additionally pins the Qt-free spelling of JSON event payloads, the rejection of [bstr], and the omission of the bytes encoder from modules whose events carry no binary data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * doctests: prove a binary event payload survives the round trip Neither doc-test covered bytes-in-an-event -- the gap #99 fell through. The generator round-trip carried `bstr` only as a method argument and return, and the composition doc-test, which is the one that actually runs two modules under logoscore and subscribes to an event, carried only a string. So a generator that dropped every bstr event argument kept both of them green. - cpp-sdk-module-composition: greeter_module gains a `blobReady(label, payload)` event and an `emitBlob(size)` method; orchestrator_module subscribes and reports the length AND a checksum of what it received. Length alone would not catch a corrupted payload -- a wrong alphabet round-trips to the same size. - cpp-sdk-generator-roundtrip: sensor_module gains a `capture(id, frame: bstr)` event, and a new step shows the generated event body encoding it through lidlBytesToJson rather than pushing it raw. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * logos_json.h: include <cstddef> for size_t The tagged-bytes codec uses size_t but relied on it arriving transitively through the other includes. Include <cstddef> directly so the header is self-contained. (Copilot review, PR #102.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
4.5 KiB
C++
116 lines
4.5 KiB
C++
// Value-level tests for the canonical tagged-bytes codec (logos_json.h).
|
|
//
|
|
// Binary payloads cross every module boundary as {"_bytes": "<base64url>"}.
|
|
// The generated cdylib event sidecar encodes into that form and the generated
|
|
// `lp` consumer wrappers decode out of it, so a wrong alphabet, a stray '=',
|
|
// or a botched tail group silently corrupts every binary event in the system.
|
|
// The code-generation tests assert on generated *source text* and cannot catch
|
|
// that; these assert on actual bytes.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <logos_json.h>
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
std::vector<uint8_t> bytesOf(const std::string& s)
|
|
{
|
|
return std::vector<uint8_t>(s.begin(), s.end());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// RFC 4648 §10 test vectors, in the URL-safe alphabet without padding — the
|
|
// form logos-protocol emits (Base64UrlEncoding | OmitTrailingEquals).
|
|
TEST(LogosJsonBytes, EncodesRfc4648VectorsUnpadded)
|
|
{
|
|
EXPECT_EQ(logos::b64UrlEncode(bytesOf("")), "");
|
|
EXPECT_EQ(logos::b64UrlEncode(bytesOf("f")), "Zg");
|
|
EXPECT_EQ(logos::b64UrlEncode(bytesOf("fo")), "Zm8");
|
|
EXPECT_EQ(logos::b64UrlEncode(bytesOf("foo")), "Zm9v");
|
|
EXPECT_EQ(logos::b64UrlEncode(bytesOf("foob")), "Zm9vYg");
|
|
EXPECT_EQ(logos::b64UrlEncode(bytesOf("fooba")), "Zm9vYmE");
|
|
EXPECT_EQ(logos::b64UrlEncode(bytesOf("foobar")), "Zm9vYmFy");
|
|
}
|
|
|
|
// The URL-safe alphabet uses '-' and '_' where standard base64 uses '+' and '/'.
|
|
// Getting this wrong still round-trips through our own decoder but corrupts
|
|
// every payload crossing to the Qt side, which decodes with Base64UrlEncoding.
|
|
TEST(LogosJsonBytes, UsesTheUrlSafeAlphabet)
|
|
{
|
|
const std::vector<uint8_t> bytes = {0xfb, 0xef, 0xbe}; // four sextets of 62
|
|
const std::string enc = logos::b64UrlEncode(bytes); // "++++" in standard b64
|
|
EXPECT_EQ(enc, "----");
|
|
EXPECT_EQ(enc.find('+'), std::string::npos);
|
|
EXPECT_EQ(enc.find('/'), std::string::npos);
|
|
EXPECT_EQ(enc.find('='), std::string::npos);
|
|
}
|
|
|
|
TEST(LogosJsonBytes, RoundTripsEveryTailLength)
|
|
{
|
|
// Every length 0..64 covers all three len%3 tail groups repeatedly.
|
|
for (size_t n = 0; n <= 64; ++n) {
|
|
std::vector<uint8_t> in(n);
|
|
for (size_t i = 0; i < n; ++i)
|
|
in[i] = static_cast<uint8_t>((i * 7 + 11) & 0xff);
|
|
|
|
const nlohmann::json tagged = logos::bytesToJson(in);
|
|
ASSERT_TRUE(tagged.is_object());
|
|
ASSERT_TRUE(tagged.contains("_bytes"));
|
|
EXPECT_EQ(logos::jsonToBytes(tagged), in) << "length " << n;
|
|
}
|
|
}
|
|
|
|
// The reason the tagged form exists at all: a plain JSON string would truncate
|
|
// at the first NUL and mangle anything >= 0x80.
|
|
TEST(LogosJsonBytes, SurvivesEmbeddedNulsAndHighBytes)
|
|
{
|
|
const std::vector<uint8_t> in = {0x00, 0xff, 0x00, 0x80, 0x7f, 0x00, 0xfe, 0xc3, 0x28};
|
|
EXPECT_EQ(logos::jsonToBytes(logos::bytesToJson(in)), in);
|
|
}
|
|
|
|
// A large payload — the shape of the proof blobs that surfaced this bug
|
|
// (logos-cpp-sdk#99 reported a 109,447-byte payload arriving empty).
|
|
TEST(LogosJsonBytes, RoundTripsALargePayload)
|
|
{
|
|
std::vector<uint8_t> in(109447);
|
|
for (size_t i = 0; i < in.size(); ++i)
|
|
in[i] = static_cast<uint8_t>((i * 31 + 7) & 0xff);
|
|
|
|
const std::vector<uint8_t> out = logos::jsonToBytes(logos::bytesToJson(in));
|
|
ASSERT_EQ(out.size(), in.size());
|
|
EXPECT_EQ(out, in);
|
|
}
|
|
|
|
// The empty payload must round-trip as empty — and, critically, must be
|
|
// distinguishable from the bug it masked: an event that dropped its bytes used
|
|
// to arrive as exactly this value.
|
|
TEST(LogosJsonBytes, EmptyPayloadRoundTrips)
|
|
{
|
|
const nlohmann::json tagged = logos::bytesToJson({});
|
|
EXPECT_EQ(tagged, nlohmann::json({{"_bytes", ""}}));
|
|
EXPECT_TRUE(logos::jsonToBytes(tagged).empty());
|
|
}
|
|
|
|
// Decoding is lenient in the same way the rest of the `lp` decode path is:
|
|
// a malformed value yields empty rather than throwing across the C ABI.
|
|
TEST(LogosJsonBytes, DecodeIsLenientOnMalformedInput)
|
|
{
|
|
EXPECT_TRUE(logos::jsonToBytes(nlohmann::json()).empty());
|
|
EXPECT_TRUE(logos::jsonToBytes(nlohmann::json("plain string")).empty());
|
|
EXPECT_TRUE(logos::jsonToBytes(nlohmann::json::array({1, 2, 3})).empty());
|
|
EXPECT_TRUE(logos::jsonToBytes(nlohmann::json{{"other", "key"}}).empty());
|
|
EXPECT_TRUE(logos::jsonToBytes(nlohmann::json{{"_bytes", 42}}).empty());
|
|
}
|
|
|
|
// Padded input is not what we emit, but a peer that pads must still decode:
|
|
// '=' is skipped rather than treated as data.
|
|
TEST(LogosJsonBytes, DecodeAcceptsPaddedInput)
|
|
{
|
|
EXPECT_EQ(logos::jsonToBytes(nlohmann::json{{"_bytes", "Zm9vYg=="}}), bytesOf("foob"));
|
|
}
|