Files
DarkflameServer/dCommon/Sd0.cpp
Aaron Kimbrell ceec638e52 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>
2026-09-28 22:31:13 -05:00

193 lines
5.8 KiB
C++

#include "Sd0.h"
#include <array>
#include <ranges>
#include "BinaryIO.h"
#include "Game.h"
#include "Logger.h"
#include "ZCompression.h"
// Insert header if on first buffer
void WriteHeader(Sd0::BinaryBuffer& chunk) {
chunk.push_back(Sd0::SD0_HEADER[0]);
chunk.push_back(Sd0::SD0_HEADER[1]);
chunk.push_back(Sd0::SD0_HEADER[2]);
chunk.push_back(Sd0::SD0_HEADER[3]);
chunk.push_back(Sd0::SD0_HEADER[4]);
}
// Write the size of the buffer to a chunk
void WriteSize(Sd0::BinaryBuffer& chunk, uint32_t chunkSize) {
for (int i = 0; i < 4; i++) {
char toPush = chunkSize & 0xff;
chunkSize = chunkSize >> 8;
chunk.push_back(toPush);
}
}
int32_t GetDataOffset(bool firstBuffer) {
return firstBuffer ? 9 : 4;
}
Sd0::Sd0(std::istream& buffer) {
char header[5]{};
// Check if this is an sd0 buffer. It's possible we may be handed a zlib buffer directly due to old code so check for that too.
if (!BinaryIO::BinaryRead(buffer, header) || memcmp(header, SD0_HEADER, sizeof(header)) != 0) {
LOG("Failed to read SD0 header %i %i %i %i %i %i %i", buffer.good(), buffer.tellg(), header[0], header[1], header[2], header[3], header[4]);
LOG_DEBUG("This may be a zlib buffer directly? Trying again assuming its a zlib buffer.");
auto& firstChunk = m_Chunks.emplace_back();
WriteHeader(firstChunk);
buffer.seekg(0, std::ios::end);
uint32_t bufferSize = buffer.tellg();
buffer.seekg(0, std::ios::beg);
WriteSize(firstChunk, bufferSize);
// its expected that if we got here, we got an old sd0 buffer where we ignored the sd0 part
// that means this can be at most the compressed chunk limit.
if (bufferSize > MAX_UNCOMPRESSED_CHUNK_SIZE) {
LOG("Possible bad chunk size of %i specified, rejecting.", bufferSize);
return;
}
firstChunk.resize(firstChunk.size() + bufferSize);
auto* dataStart = reinterpret_cast<char*>(firstChunk.data() + GetDataOffset(true));
if (!buffer.read(dataStart, bufferSize)) {
m_Chunks.pop_back();
LOG("Failed to read %u bytes from chunk %i", bufferSize, m_Chunks.size() - 1);
}
return;
}
while (buffer && buffer.peek() != std::istream::traits_type::eof()) {
uint32_t chunkSize{};
if (!BinaryIO::BinaryRead(buffer, chunkSize)) {
LOG("Failed to read chunk size from stream %lld %zu", buffer.tellg(), m_Chunks.size());
break;
}
auto& chunk = m_Chunks.emplace_back();
bool firstBuffer = m_Chunks.size() == 1;
auto dataOffset = GetDataOffset(firstBuffer);
// Insert header if on first buffer
if (firstBuffer) {
WriteHeader(chunk);
}
WriteSize(chunk, chunkSize);
// Assuming a good buffer that is large enough to take up 2 zlib buffers
// any buffer should be compressed enough to take up less size than its uncompressed counterpart
if (chunkSize > MAX_UNCOMPRESSED_CHUNK_SIZE) {
LOG("Possible bad chunk size of %i specified, rejecting.", chunkSize);
break;
}
chunk.resize(chunkSize + dataOffset);
auto* dataStart = reinterpret_cast<char*>(chunk.data() + dataOffset);
if (!buffer.read(dataStart, chunkSize)) {
m_Chunks.pop_back();
LOG("Failed to read %u bytes from chunk %i", chunkSize, m_Chunks.size() - 1);
break;
}
}
}
void Sd0::FromData(const uint8_t* data, size_t bufferSize) {
const auto originalBufferSize = bufferSize;
if (bufferSize == 0) return;
m_Chunks.clear();
// Room for a chunk that doesn't compress (zlib makes it slightly bigger), on the heap: this runs on worker threads
std::vector<uint8_t> compressedChunk(ZCompression::GetMaxCompressedLength(MAX_UNCOMPRESSED_CHUNK_SIZE));
while (bufferSize > 0) {
const auto numToCopy = std::min(MAX_UNCOMPRESSED_CHUNK_SIZE, bufferSize);
const auto* startOffset = data + originalBufferSize - bufferSize;
bufferSize -= numToCopy;
const auto compressedSize = ZCompression::Compress(
startOffset, numToCopy,
compressedChunk.data(), compressedChunk.size());
if (compressedSize == -1) {
LOG("Failed to compress chunk, aborting");
// Part of the data isn't a usable buffer
m_Chunks.clear();
break;
}
auto& chunk = m_Chunks.emplace_back();
bool firstBuffer = m_Chunks.size() == 1;
auto dataOffset = GetDataOffset(firstBuffer);
if (firstBuffer) {
WriteHeader(chunk);
}
WriteSize(chunk, compressedSize);
chunk.resize(compressedSize + dataOffset);
memcpy(chunk.data() + dataOffset, compressedChunk.data(), compressedSize);
}
}
std::string Sd0::Compress(std::string_view data) {
Sd0 sd0;
if (data.empty()) return std::string(SD0_HEADER, 5);
sd0.FromData(reinterpret_cast<const uint8_t*>(data.data()), data.size());
std::string out;
for (const auto& chunk : sd0.GetAsVector()) out.append(reinterpret_cast<const char*>(chunk.data()), chunk.size());
return out;
}
std::string Sd0::GetAsStringUncompressed() const {
std::string toReturn;
bool first = true;
uint32_t totalSize{};
for (const auto& chunk : m_Chunks) {
auto dataOffset = GetDataOffset(first);
first = false;
const auto chunkSize = chunk.size();
if (chunkSize <= static_cast<size_t>(dataOffset)) {
LOG("Bad chunkSize for data, aborting");
toReturn = "";
totalSize = 0;
break;
}
auto oldSize = toReturn.size();
toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE);
int32_t error{};
const auto uncompressedSize = ZCompression::Decompress(
chunk.data() + dataOffset, chunkSize - dataOffset,
reinterpret_cast<uint8_t*>(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE,
error);
if (uncompressedSize == -1) {
LOG("Failed to decompress chunk, aborting");
toReturn = "";
totalSize = 0;
break;
}
totalSize += uncompressedSize;
}
toReturn.resize(totalSize);
return toReturn;
}
std::stringstream Sd0::GetAsStream() const {
std::stringstream toReturn;
for (const auto& chunk : m_Chunks) {
toReturn.write(reinterpret_cast<const char*>(chunk.data()), chunk.size());
}
return toReturn;
}
const std::vector<Sd0::BinaryBuffer>& Sd0::GetAsVector() const {
return m_Chunks;
}