yams/tests/unit/compression/framed_payload_catch2_test.cpp
trvon 73e5d3831c
fix(cli): decode compressed content in yams get
Since v0.7.3 'yams get' has requested compressed transfers
(acceptCompressed), and the daemon has answered with a framed payload: a
CompressionHeader followed by a Zstandard body. Only the MCP server
decoded it; 'yams get' wrote the frame itself to stdout or -o, so users
saw the 'CNRK' header and a zstd frame instead of the document. 'yams cat'
was unaffected because it does not request compression.

- Move the frame format into compression/framed_payload
  (encodeFramedPayload/decodeFramedPayload). DocumentService encodes with
  it; 'yams get' and MCP decode with it.
- The decoder checks the header, body size and CRC, and the decoded size
  and CRC. A payload that fails is an error; MCP used to fall back to
  returning the framed bytes as content.

Verified end to end: 'yams get' output (stdout and -o) is byte-identical
to the added file while the transfer stays compressed (51.75x).

Signed-off-by: trvon <me@trevon.dev>
2026-09-26 21:10:00 -06:00

88 lines
3 KiB
C++

// Framed payloads: CompressionHeader followed by the (possibly compressed) body.
//
// The daemon sends these to clients that set acceptCompressed. Only the MCP server decoded
// them; `yams get` printed the header and the zstd frame as the document from v0.7.3 on.
#include <catch2/catch_test_macros.hpp>
#include <cstddef>
#include <cstring>
#include <string>
#include <vector>
#include <yams/compression/compression_header.h>
#include <yams/compression/framed_payload.h>
using namespace yams::compression;
namespace {
std::vector<std::byte> bytesOf(const std::string& text) {
std::vector<std::byte> out(text.size());
std::memcpy(out.data(), text.data(), text.size());
return out;
}
std::string textOf(const std::vector<std::byte>& bytes) {
return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
}
std::string sampleDocument() {
std::string text;
for (int i = 0; i < 2000; ++i) {
text += "line " + std::to_string(i) + ": the quick brown fox jumps over the lazy dog\n";
}
return text;
}
} // namespace
TEST_CASE("A framed payload round-trips to the original bytes", "[compression][framed]") {
const auto original = bytesOf(sampleDocument());
auto framed = encodeFramedPayload(original);
REQUIRE(framed.has_value());
REQUIRE((framed.value().size() >= CompressionHeader::SIZE));
CHECK((std::memcmp(framed.value().data(), "CNRK", 4) == 0)); // what `yams get` printed
auto decoded = decodeFramedPayload(framed.value());
REQUIRE(decoded.has_value());
CHECK((textOf(decoded.value()) == sampleDocument()));
}
TEST_CASE("Tiny and empty framed payloads round-trip", "[compression][framed]") {
const auto original = bytesOf("hi");
auto framed = encodeFramedPayload(original);
REQUIRE(framed.has_value());
auto decoded = decodeFramedPayload(framed.value());
REQUIRE(decoded.has_value());
CHECK((textOf(decoded.value()) == "hi"));
auto empty = encodeFramedPayload({});
REQUIRE(empty.has_value());
auto decodedEmpty = decodeFramedPayload(empty.value());
REQUIRE(decodedEmpty.has_value());
CHECK(decodedEmpty.value().empty());
}
TEST_CASE("A damaged framed payload is an error, never raw bytes", "[compression][framed]") {
auto framed = encodeFramedPayload(bytesOf(sampleDocument()));
REQUIRE(framed.has_value());
const auto& good = framed.value();
SECTION("corrupted body") {
auto bad = good;
bad[CompressionHeader::SIZE + 8] ^= std::byte{0x5a};
CHECK_FALSE(decodeFramedPayload(bad).has_value());
}
SECTION("truncated body") {
std::vector<std::byte> bad(good.begin(), good.end() - 16);
CHECK_FALSE(decodeFramedPayload(bad).has_value());
}
SECTION("shorter than a header") {
std::vector<std::byte> bad(good.begin(), good.begin() + 10);
CHECK_FALSE(decodeFramedPayload(bad).has_value());
}
SECTION("not framed at all") {
CHECK_FALSE(decodeFramedPayload(bytesOf(sampleDocument())).has_value());
}
}