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>
88 lines
3 KiB
C++
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());
|
|
}
|
|
}
|