mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 01:31:10 +00:00
* refactor: the LIDL codec exists once
The cdylib generator emitted its own copy of the codec — ~186 lines of
C++-emitting-C++ mirroring logos-protocol's logos_codec.h by hand. Every codec
fix had to be written twice or it silently only half-applied, which happened
twice in a row recently (routing scalars through the codec + signedness; then
accepting 3.0 while still rejecting 3.7).
It was worse than duplication. The two copies had DRIFTED — the emitted integer
decode gated on is_number() where the canonical one checked is_number_integer()
|| is_number_unsigned() — and logos_json.h's byte helpers were the same mangled
symbols with weak linkage and DIFFERENT bodies as logos_codec.h's, both reaching
one program (module TUs compiled one; liblogos_protocol.a carries TUs that
included the other). Which body won was down to link order.
logos_json.h goes back to its documented charter — "LogosMap/LogosList aliases
for impl classes", per its own CMakeLists — and loses 77 lines. jsonToBytes moves
beside its sibling jsonToStringVec in logos_lp_client.h, rebuilt on the canonical
isTaggedBytes/b64UrlDecode; it keeps its own narrow spelling because every lp
decoder is documented to yield the default-constructed value on a mismatch,
which neither bytesFromJson (throws) nor bytesFromJsonLenient (accepts more) does.
Emptying it rather than making it include logos_codec.h is deliberate: some
thirty alias-only include sites across the module repos get ZERO new includes,
and logos-cpp-sdkConfig's "only dependency is nlohmann_json" stays true.
With the clash gone the generic half is deletable. emitGeneratedCodec becomes
emitRecordCodecs: one logos::detail::Codec<::Rec, void> per declared record, and
nothing else. That residue is irreducible — a LIDL `type` is a per-contract
struct whose fields exist only in that module's header, and C++17 has no field
reflection. Nesting composes for free: Codec<std::vector<Blob>> and deeper come
from the shared half once Codec<::Blob> exists.
One asymmetry dies with it. The scalar bstr decode and the [bstr] element decode
were different functions with different strictness, so echoBytes("hi") succeeded
while echoBytesList(["hi"]) threw — inside one module, for the same type. They
are one function now.
Build wiring: ONE line, in this repo's own test CMake, using a variable
nix/tests.nix already supplies. Nothing in logos-module-builder, logos-qt-sdk, or
any module repo.
verified: cpp-sdk + protocol suites green; test_fullapi_cpp, test_fullapi_ext_cpp
and test_basic_module_cpp build; test-modules 176/176. Conformance delta is
exactly one cell, baselined first in logos-test-modules#31.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: bump logos-protocol to the path-threaded bstr decoder
logos-protocol 4359557 (#33). Required by this branch, not incidental: deleting
the emitted codec swaps its path-carrying bstr decode for the canonical one, and
without #33 the canonical one reported "at value" instead of "[0].payload" —
losing the diagnostic exactly where a malformed bstr is hardest to find.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
120 lines
4.8 KiB
C++
120 lines
4.8 KiB
C++
// Value-level tests for the canonical tagged-bytes codec.
|
|
//
|
|
// 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>
|
|
|
|
// logos_codec.h owns the canonical encode; logos_lp_client.h owns the lenient
|
|
// `lp`-path decode. They used to be second copies in logos_json.h, which is now
|
|
// aliases-only — see the note at the top of that header.
|
|
#include <logos_codec.h>
|
|
#include <logos_lp_client.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"));
|
|
}
|