feat(common): Sd0::Compress, a default Sd0 and room for chunks that don't shrink

Sd0 gets a default constructor and Sd0::Compress(data), the raw sd0 bytes of
some data (what the client reads for UGC files it downloads without 3D
services). FromData compresses into a heap buffer big enough for a chunk that
grows when deflated (it used a 256 KiB stack buffer, so random data failed and
workers carried a large stack frame), and drops a half-written result.

Tests: chunks as the 1.10.64 client inflates them, incompressible data, empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 17:46:34 -05:00
parent 36cefc1ede
commit ceec638e52
4 changed files with 90 additions and 1 deletions

View File

@@ -29,6 +29,7 @@ set(DCOMMONTEST_SOURCES
"PropertyReputationRulesTests.cpp"
"BindAddressTests.cpp"
"TrafficStatsTests.cpp"
"Sd0Tests.cpp"
)
add_subdirectory(dEnumsTests)

View File

@@ -0,0 +1,67 @@
#include <gtest/gtest.h>
#include <cstring>
#include <random>
#include <sstream>
#include <string>
#include "Sd0.h"
#include "ZCompression.h"
namespace {
// The chunks as the 1.10.64 client reads them (Sd0Decompress): the header, then a u32 size and zlib data per chunk,
// each inflating to at most 256 KiB
std::string InflateLikeTheClient(const std::string& sd0, size_t& chunks) {
EXPECT_TRUE(sd0.starts_with(std::string(Sd0::SD0_HEADER, 5)));
size_t offset = 5;
std::string inflated;
chunks = 0;
while (offset < sd0.size()) {
if (offset + 4 > sd0.size()) return {};
uint32_t length{};
std::memcpy(&length, sd0.data() + offset, 4);
offset += 4;
if (offset + length > sd0.size()) return {};
std::string chunk(Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, '\0');
int32_t error{};
const auto size = ZCompression::Decompress(reinterpret_cast<const uint8_t*>(sd0.data() + offset), length,
reinterpret_cast<uint8_t*>(chunk.data()), static_cast<uint32_t>(chunk.size()), error);
if (size < 0) return {};
inflated.append(chunk.data(), static_cast<size_t>(size));
offset += length;
chunks++;
}
return inflated;
}
}
TEST(Sd0, CompressesInChunksTheClientReads) {
// 600 KiB: chunks of 256, 256 and 88 KiB
std::string data(600 * 1024, '\0');
for (size_t i = 0; i < data.size(); i++) data[i] = static_cast<char>((i * 7) % 251);
const auto sd0 = Sd0::Compress(data);
size_t chunks{};
EXPECT_EQ(InflateLikeTheClient(sd0, chunks), data);
EXPECT_EQ(chunks, 3u);
// And back through the reader
std::istringstream stream(sd0);
EXPECT_EQ(Sd0(stream).GetAsStringUncompressed(), data);
}
TEST(Sd0, CompressesDataThatDoesNotShrink) {
// Random bytes grow a little when deflated: a full chunk must still fit
std::string data(Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE + 1000, '\0');
std::mt19937 random(1234);
for (auto& c : data) c = static_cast<char>(random());
const auto sd0 = Sd0::Compress(data);
size_t chunks{};
EXPECT_EQ(InflateLikeTheClient(sd0, chunks), data);
EXPECT_EQ(chunks, 2u);
}
TEST(Sd0, EmptyDataIsTheHeaderOnly) {
EXPECT_EQ(Sd0::Compress(""), std::string(Sd0::SD0_HEADER, 5));
Sd0 empty;
EXPECT_TRUE(empty.GetAsVector().empty());
}