summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Meadows <leviathan@lindenlab.com>2026-08-19 08:57:03 -0700
committerGitHub <noreply@github.com>2026-08-19 08:57:03 -0700
commit63eb2da05a6211ce4e038f1bf1aef5dda3c6ac77 (patch)
tree946e521cf56d4b2f6080b47bbec2c8d8314da3fd
parent1c1e8e07d33b9c02d2e312d76c3b9ea5c5ce2442 (diff)
move zerocode logic to util, add unit-tests (#6152)
-rw-r--r--indra/llmessage/CMakeLists.txt2
-rw-r--r--indra/llmessage/lltemplatemessagebuilder.cpp93
-rw-r--r--indra/llmessage/llzerocode.h211
-rw-r--r--indra/llmessage/message.cpp152
-rw-r--r--indra/llmessage/message.h1
-rw-r--r--indra/llmessage/tests/llzerocode_test.cpp253
6 files changed, 490 insertions, 222 deletions
diff --git a/indra/llmessage/CMakeLists.txt b/indra/llmessage/CMakeLists.txt
index f143ee18cf..2306c377ba 100644
--- a/indra/llmessage/CMakeLists.txt
+++ b/indra/llmessage/CMakeLists.txt
@@ -165,6 +165,7 @@ set(llmessage_HEADER_FILES
llxfer_mem.h
llxfer_vfile.h
llxorcipher.h
+ llzerocode.h
machine.h
mean_collision_data.h
message.h
@@ -228,6 +229,7 @@ if (LL_TESTS)
LL_ADD_INTEGRATION_TEST(llservicebuilder "" "${test_libs}")
LL_ADD_INTEGRATION_TEST(lltemplatemessagebuilder "" "${test_libs}")
LL_ADD_INTEGRATION_TEST(llxfer_file "" "${test_libs}")
+ LL_ADD_INTEGRATION_TEST(llzerocode "" "${test_libs}")
LL_ADD_INTEGRATION_TEST(llxorcipher "" "${test_libs}")
LL_ADD_INTEGRATION_TEST(message "" "${test_libs}")
endif (LL_TESTS)
diff --git a/indra/llmessage/lltemplatemessagebuilder.cpp b/indra/llmessage/lltemplatemessagebuilder.cpp
index 758d6d343f..c4ff63bea4 100644
--- a/indra/llmessage/lltemplatemessagebuilder.cpp
+++ b/indra/llmessage/lltemplatemessagebuilder.cpp
@@ -29,6 +29,7 @@
#include "lltemplatemessagebuilder.h"
#include "llmessagetemplate.h"
+#include "llzerocode.h"
#include "llmath.h"
#include "llquaternion.h"
#include "u64.h"
@@ -492,93 +493,29 @@ void LLTemplateMessageBuilder::addUUID(const char *varname, const LLUUID& uuid)
addData(varname, uuid.mData, MVT_LLUUID, sizeof(uuid.mData));
}
-static S32 zero_code(U8 **data, U32 *data_size)
+void LLTemplateMessageBuilder::compressMessage(U8*& buf_ptr, U32& buffer_length)
{
- // Encoded send buffer needs to be slightly larger since the zero
- // coding can potentially increase the size of the send data.
- static U8 encodedSendBuffer[2 * MAX_BUFFER_SIZE];
-
- S32 count = *data_size;
-
- S32 net_gain = 0;
- U8 num_zeroes = 0;
-
- U8 *inptr = (U8 *)*data;
- U8 *outptr = (U8 *)encodedSendBuffer;
-
-// skip the packet id field
-
- for (U32 ii = 0; ii < LL_PACKET_ID_SIZE ; ++ii)
- {
- count--;
- *outptr++ = *inptr++;
- }
-
-// build encoded packet, keeping track of net size gain
-
-// sequential zero bytes are encoded as 0 [U8 count]
-// with 0 0 [count] representing wrap (>256 zeroes)
-
- while (count--)
+ if(ME_ZEROCODED != mCurrentSMessageTemplate->getEncoding())
{
- if (!(*inptr)) // in a zero count
- {
- if (num_zeroes)
- {
- if (++num_zeroes > 254)
- {
- *outptr++ = num_zeroes;
- num_zeroes = 0;
- }
- net_gain--; // subseqent zeroes save one
- }
- else
- {
- *outptr++ = 0;
- net_gain++; // starting a zero count adds one
- num_zeroes = 1;
- }
- inptr++;
- }
- else
- {
- if (num_zeroes)
- {
- *outptr++ = num_zeroes;
- num_zeroes = 0;
- }
- *outptr++ = *inptr++;
- }
+ return;
}
- if (num_zeroes)
- {
- *outptr++ = num_zeroes;
- }
+ // Encoded send buffer needs to be slightly larger since the zero
+ // coding can potentially increase the size of the send data.
+ static U8 encodedSendBuffer[2 * MAX_BUFFER_SIZE];
- if (net_gain < 0)
+ S32 encoded_size = LLZeroCode::encode(buf_ptr, buffer_length,
+ encodedSendBuffer, sizeof(encodedSendBuffer),
+ LL_PACKET_ID_SIZE);
+ if (encoded_size >= 0)
{
// TODO: babbage: reinstate stat collecting...
//mCompressedPacketsOut++;
- //mUncompressedBytesOut += *data_size;
-
- *data = encodedSendBuffer;
- *data_size += net_gain;
- encodedSendBuffer[0] |= LL_ZERO_CODE_FLAG; // set the head bit to indicate zero coding
-
- //mCompressedBytesOut += *data_size;
-
- }
- //mTotalBytesOut += *data_size;
+ //mUncompressedBytesOut += buffer_length;
+ //mCompressedBytesOut += encoded_size;
- return(net_gain);
-}
-
-void LLTemplateMessageBuilder::compressMessage(U8*& buf_ptr, U32& buffer_length)
-{
- if(ME_ZEROCODED == mCurrentSMessageTemplate->getEncoding())
- {
- zero_code(&buf_ptr, &buffer_length);
+ buf_ptr = encodedSendBuffer;
+ buffer_length = (U32)encoded_size;
}
}
diff --git a/indra/llmessage/llzerocode.h b/indra/llmessage/llzerocode.h
new file mode 100644
index 0000000000..7246c9d946
--- /dev/null
+++ b/indra/llmessage/llzerocode.h
@@ -0,0 +1,211 @@
+/**
+ * @file llzerocode.h
+ * @brief Zero-code run-length compression used by the LLMessageSystem UDP protocol.
+ *
+ * $LicenseInfo:firstyear=2001&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLZEROCODE_H
+#define LL_LLZEROCODE_H
+
+#include <cstring>
+
+#include "stdtypes.h"
+
+// Zero-coding compresses runs of zero bytes in a packet body, leaving the
+// first header_size bytes of the buffer (the packet header - flags,
+// sequence number, offset, etc.) untouched aside from the flag bit below.
+//
+// Runs of zero bytes in the body are replaced by a two-byte token:
+// 0x00 N - represents N zero bytes, for N in 1..254
+// 0x00 0x00 N - represents (255 + N) zero bytes (wrap/overflow case,
+// produced by decode()'s wire format but never emitted
+// by encode(), which instead starts a fresh 0x00 token
+// every 255 zero bytes)
+namespace LLZeroCode
+{
+ // High bit of the first header byte: set by encode() and cleared by
+ // decode() to indicate whether the body that follows is zero-coded.
+ const U8 FLAG = 0x80;
+
+ // Zero-codes src (src_size bytes, the first header_size of which are the
+ // packet header and are copied verbatim) into dst.
+ //
+ // dst_capacity must be at least 2 * src_size: a pathological body of
+ // isolated zero bytes can nearly double in size when encoded.
+ //
+ // Returns the encoded size (with FLAG set in dst[0]) if doing so made the
+ // packet smaller. Returns -1 if compression would not help (or the
+ // arguments are invalid), in which case dst is left untouched and the
+ // caller should keep using the original, uncompressed buffer.
+ inline S32 encode(const U8* src, U32 src_size, U8* dst, U32 dst_capacity, U32 header_size)
+ {
+ if (src_size < header_size || dst_capacity < 2 * src_size)
+ {
+ return -1;
+ }
+
+ S32 count = (S32)(src_size - header_size);
+ S32 net_gain = 0;
+ U8 num_zeroes = 0;
+
+ const U8* inptr = src;
+ U8* outptr = dst;
+
+ // copy the header verbatim
+ for (U32 ii = 0; ii < header_size; ++ii)
+ {
+ *outptr++ = *inptr++;
+ }
+
+ // sequential zero bytes are encoded as 0 [U8 count]; a run longer
+ // than 254 bytes is split into consecutive 0 [U8 count] tokens.
+ while (count--)
+ {
+ if (!(*inptr)) // in a zero count
+ {
+ if (num_zeroes)
+ {
+ if (++num_zeroes > 254)
+ {
+ *outptr++ = num_zeroes;
+ num_zeroes = 0;
+ }
+ net_gain--; // subsequent zeroes save one
+ }
+ else
+ {
+ *outptr++ = 0;
+ net_gain++; // starting a zero count adds one
+ num_zeroes = 1;
+ }
+ inptr++;
+ }
+ else
+ {
+ if (num_zeroes)
+ {
+ *outptr++ = num_zeroes;
+ num_zeroes = 0;
+ }
+ *outptr++ = *inptr++;
+ }
+ }
+
+ if (num_zeroes)
+ {
+ *outptr++ = num_zeroes;
+ }
+
+ if (net_gain >= 0)
+ {
+ // compression did not shrink the packet; caller should keep the original
+ return -1;
+ }
+
+ dst[0] |= FLAG;
+ return (S32)src_size + net_gain;
+ }
+
+ // Expands a zero-coded src (src_size bytes) into dst.
+ //
+ // If FLAG is not set in src[0], the body is not zero-coded: no work is
+ // done and the function returns 0.
+ //
+ // On success, returns the number of bytes written to dst (always includes
+ // the header_size header bytes, copied verbatim except for FLAG being
+ // cleared from dst[0]).
+ //
+ // If expansion would write past dst_capacity - which only a malformed or
+ // malicious packet should cause - decoding is aborted, *overflow is set
+ // true, and the returned size reflects however much (if anything) was
+ // salvaged; the caller should treat the packet as invalid.
+ inline U32 decode(const U8* src, U32 src_size, U8* dst, U32 dst_capacity, U32 header_size, bool& overflow)
+ {
+ overflow = false;
+
+ if (src_size < header_size || !(src[0] & FLAG))
+ {
+ return 0;
+ }
+
+ S32 count = (S32)(src_size - header_size);
+
+ const U8* inptr = src;
+ U8* outptr = dst;
+
+ for (U32 ii = 0; ii < header_size; ++ii)
+ {
+ *outptr++ = *inptr++;
+ }
+ dst[0] &= ~FLAG;
+
+ // reconstruct the body: a 0x00 byte starts a run; the byte(s) that
+ // follow give its length (see the wire format described above).
+ while (count--)
+ {
+ if (outptr > &dst[dst_capacity - 1])
+ {
+ overflow = true;
+ outptr = dst;
+ break;
+ }
+
+ if (!((*outptr++ = *inptr++)))
+ {
+ while ((count--) && (!(*inptr)))
+ {
+ if (outptr > &dst[dst_capacity - 256])
+ {
+ overflow = true;
+ outptr = dst;
+ count = -1;
+ break;
+ }
+ *outptr++ = *inptr++;
+ memset(outptr, 0, 255);
+ outptr += 255;
+ }
+
+ if (count < 0)
+ {
+ break;
+ }
+ else
+ {
+ if (outptr > &dst[dst_capacity - (*inptr)])
+ {
+ overflow = true;
+ outptr = dst;
+ }
+ memset(outptr, 0, (*inptr) - 1);
+ outptr += ((*inptr) - 1);
+ inptr++;
+ }
+ }
+ }
+
+ return (U32)(outptr - dst);
+ }
+}
+
+#endif // LL_LLZEROCODE_H
diff --git a/indra/llmessage/message.cpp b/indra/llmessage/message.cpp
index db7405f93f..71210f7535 100644
--- a/indra/llmessage/message.cpp
+++ b/indra/llmessage/message.cpp
@@ -67,6 +67,7 @@
#include "llsdmessagereader.h"
#include "llsdserialize.h"
#include "llstring.h"
+#include "llzerocode.h"
#include "lltransfermanager.h"
#include "lluuid.h"
#include "llxfermanager.h"
@@ -3060,85 +3061,6 @@ U32 LLMessageSystem::getListenPort( void ) const
return mPort;
}
-// TODO: babbage: remove this horror!
-S32 LLMessageSystem::zeroCodeAdjustCurrentSendTotal()
-{
- if(mMessageBuilder == mLLSDMessageBuilder)
- {
- // babbage: don't compress LLSD messages, so delta is 0
- return 0;
- }
-
- if (! mMessageBuilder->isBuilt())
- {
- mSendSize = mMessageBuilder->buildMessage(
- mSendBuffer,
- MAX_BUFFER_SIZE,
- 0);
- }
- // TODO: babbage: remove this horror
- mMessageBuilder->setBuilt(false);
-
- S32 count = mSendSize;
-
- S32 net_gain = 0;
- U8 num_zeroes = 0;
-
- U8 *inptr = (U8 *)mSendBuffer;
-
-// skip the packet id field
-
- for (U32 ii = 0; ii < LL_PACKET_ID_SIZE; ++ii)
- {
- count--;
- inptr++;
- }
-
-// don't actually build, just test
-
-// sequential zero bytes are encoded as 0 [U8 count]
-// with 0 0 [count] representing wrap (>256 zeroes)
-
- while (count--)
- {
- if (!(*inptr)) // in a zero count
- {
- if (num_zeroes)
- {
- if (++num_zeroes > 254)
- {
- num_zeroes = 0;
- }
- net_gain--; // subseqent zeroes save one
- }
- else
- {
- net_gain++; // starting a zero count adds one
- num_zeroes = 1;
- }
- inptr++;
- }
- else
- {
- if (num_zeroes)
- {
- num_zeroes = 0;
- }
- inptr++;
- }
- }
- if (net_gain < 0)
- {
- return net_gain;
- }
- else
- {
- return 0;
- }
-}
-
-
-
S32 LLMessageSystem::zeroCodeExpand(U8** data, S32* data_size)
{
if ((*data_size ) < LL_MINIMUM_VALID_PACKET_SIZE)
@@ -3159,74 +3081,18 @@ S32 LLMessageSystem::zeroCodeExpand(U8** data, S32* data_size)
mCompressedPacketsIn++;
mCompressedBytesIn += *data_size;
- *data[0] &= (~LL_ZERO_CODE_FLAG);
-
- S32 count = (*data_size);
-
- U8 *inptr = (U8 *)*data;
- U8 *outptr = (U8 *)mEncodedRecvBuffer;
-
-// skip the packet id field
-
- for (U32 ii = 0; ii < LL_PACKET_ID_SIZE; ++ii)
- {
- count--;
- *outptr++ = *inptr++;
- }
-
-// reconstruct encoded packet, keeping track of net size gain
-
-// sequential zero bytes are encoded as 0 [U8 count]
-// with 0 0 [count] representing wrap (>256 zeroes)
-
- while (count--)
+ bool overflow = false;
+ U32 decoded_size = LLZeroCode::decode(*data, (U32)*data_size,
+ mEncodedRecvBuffer, sizeof(mEncodedRecvBuffer),
+ LL_PACKET_ID_SIZE, overflow);
+ if (overflow)
{
- if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-1]))
- {
- LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 1" << LL_ENDL;
- callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE);
- outptr = mEncodedRecvBuffer;
- break;
- }
- if (!((*outptr++ = *inptr++)))
- {
- while (((count--)) && (!(*inptr)))
- {
- *outptr++ = *inptr++;
- if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-256]))
- {
- LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 2" << LL_ENDL;
- callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE);
- outptr = mEncodedRecvBuffer;
- count = -1;
- break;
- }
- memset(outptr,0,255);
- outptr += 255;
- }
-
- if (count < 0)
- {
- break;
- }
-
- else
- {
- if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-(*inptr)]))
- {
- LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 3" << LL_ENDL;
- callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE);
- outptr = mEncodedRecvBuffer;
- }
- memset(outptr,0,(*inptr) - 1);
- outptr += ((*inptr) - 1);
- inptr++;
- }
- }
+ LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size" << LL_ENDL;
+ callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE);
}
*data = mEncodedRecvBuffer;
- *data_size = (S32)(outptr - mEncodedRecvBuffer);
+ *data_size = (S32)decoded_size;
mUncompressedBytesIn += *data_size;
return(in_size);
diff --git a/indra/llmessage/message.h b/indra/llmessage/message.h
index e23a67c04a..2e19e567e6 100644
--- a/indra/llmessage/message.h
+++ b/indra/llmessage/message.h
@@ -604,7 +604,6 @@ public:
//void buildMessage();
S32 zeroCodeExpand(U8 **data, S32 *data_size);
- S32 zeroCodeAdjustCurrentSendTotal();
// Uses ping-based retry
S32 sendReliable(const LLHost &host);
diff --git a/indra/llmessage/tests/llzerocode_test.cpp b/indra/llmessage/tests/llzerocode_test.cpp
new file mode 100644
index 0000000000..1aba46149d
--- /dev/null
+++ b/indra/llmessage/tests/llzerocode_test.cpp
@@ -0,0 +1,253 @@
+/**
+ * @file llzerocode_test.cpp
+ * @brief LLZeroCode test cases.
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "../llzerocode.h"
+
+#include "../test/lltut.h"
+
+#include <vector>
+
+namespace tut
+{
+ struct zerocode_data
+ {
+ };
+ typedef test_group<zerocode_data> zerocode_test;
+ typedef zerocode_test::object zerocode_object;
+ tut::zerocode_test zerocode_testcase("LLZeroCode");
+
+ // Builds header_size bytes of header (with header[0] == header0) followed by body.
+ static std::vector<U8> makeBuffer(U32 header_size, U8 header0, const std::vector<U8>& body)
+ {
+ std::vector<U8> buf(header_size, 0);
+ if (header_size)
+ {
+ buf[0] = header0;
+ }
+ buf.insert(buf.end(), body.begin(), body.end());
+ return buf;
+ }
+
+ // Runs src through encode() then decode(), and (when compression is expected to help)
+ // asserts the round trip reproduces src exactly, including untouched header bits other
+ // than the zero-code flag. When compression is not expected to help, asserts encode()
+ // refuses it.
+ static void ensureRoundTrip(const char* msg, U32 header_size, U8 header0,
+ const std::vector<U8>& body, bool expect_compressed)
+ {
+ std::vector<U8> src = makeBuffer(header_size, header0, body);
+ std::vector<U8> enc(2 * src.size() + 16, 0xAA);
+
+ S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(),
+ enc.data(), (U32)enc.size(), header_size);
+
+ if (!expect_compressed)
+ {
+ ensure(std::string(msg) + ": encode should refuse (no benefit)", enc_size < 0);
+ return;
+ }
+
+ ensure(std::string(msg) + ": encode should succeed", enc_size >= 0);
+ ensure(std::string(msg) + ": encoded size should be smaller", (U32)enc_size < src.size());
+ ensure(std::string(msg) + ": FLAG should be set on encoded output", (enc[0] & LLZeroCode::FLAG) != 0);
+ if (header_size > 0)
+ {
+ ensure_equals(std::string(msg) + ": non-flag header bits preserved on encode",
+ (U8)(enc[0] & ~LLZeroCode::FLAG), (U8)(header0 & ~LLZeroCode::FLAG));
+ }
+
+ std::vector<U8> dec(src.size() + 16, 0xBB);
+ bool overflow = false;
+ U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size,
+ dec.data(), (U32)dec.size(), header_size, overflow);
+ ensure(std::string(msg) + ": decode should not overflow", !overflow);
+ ensure_equals(std::string(msg) + ": decoded size should match original", dec_size, (U32)src.size());
+ ensure(std::string(msg) + ": decoded bytes should match original",
+ memcmp(dec.data(), src.data(), src.size()) == 0);
+ }
+
+ // Basic mixed body; header carries an unrelated flag bit (0x40) that must survive untouched.
+ template<> template<>
+ void zerocode_object::test<1>()
+ {
+ ensureRoundTrip("mixed body", 6, 0x40,
+ {0,0,0,5,0,0,0,0,0,0,7,8,9,0,0}, true);
+ }
+
+ // A body with no zero bytes at all cannot benefit from zero-coding.
+ template<> template<>
+ void zerocode_object::test<2>()
+ {
+ ensureRoundTrip("no zero bytes", 1, 0x00, {1,2,3,4,5,6,7,8,9}, false);
+ }
+
+ // Isolated zero-byte runs of length 1 or 2 cost as much or more than they save
+ // (marker + terminator == 2 bytes), so encode must refuse them.
+ template<> template<>
+ void zerocode_object::test<3>()
+ {
+ ensureRoundTrip("isolated single zero", 1, 0x00, {5, 0}, false);
+ ensureRoundTrip("isolated double zero", 1, 0x00, {5, 0, 0, 9}, false);
+ }
+
+ // A long enough zero run followed by other data pays off.
+ template<> template<>
+ void zerocode_object::test<4>()
+ {
+ std::vector<U8> body(20, 0);
+ body.push_back(99);
+ ensureRoundTrip("long zero run", 1, 0x00, body, true);
+ }
+
+ // Wrap boundary: runs are split into chunks of at most 255 zero bytes each
+ // (encode never emits the doubled-0x00 wire form; see test<7> for that).
+ // A run only shrinks the packet once it is longer than 2 bytes.
+ template<> template<>
+ void zerocode_object::test<5>()
+ {
+ static const U32 lengths[] = {1, 2, 253, 254, 255, 256, 257, 300, 509, 510, 511, 1000};
+ for (U32 n : lengths)
+ {
+ std::vector<U8> body(n, 0);
+ body.push_back(42); // trailing nonzero byte
+ ensureRoundTrip("wrap boundary (with trailing byte)", 1, 0x00, body, n > 2);
+ }
+ }
+
+ // Same boundary check, but with the zero run flushed at end-of-buffer
+ // (no trailing nonzero byte to force the terminator write mid-loop).
+ template<> template<>
+ void zerocode_object::test<6>()
+ {
+ static const U32 lengths[] = {1, 2, 254, 255, 256, 300};
+ for (U32 n : lengths)
+ {
+ std::vector<U8> body(n, 0);
+ ensureRoundTrip("wrap boundary (end-of-buffer flush)", 1, 0x00, body, n > 2);
+ }
+ }
+
+ // decode() must also accept the "0x00 0x00 N" doubled-marker wire format
+ // (worth +255 zero bytes per extra marker) for compatibility with any
+ // encoder other than this one's chunking strategy, even though encode()
+ // itself never emits it.
+ template<> template<>
+ void zerocode_object::test<7>()
+ {
+ // header (1 byte, FLAG set) + [0x00 marker][0x00 extra-wrap-marker][terminator=5]
+ // decoded body length = 1 (marker) + 1 (extra marker) + 255 (memset) + (5 - 1) = 261
+ std::vector<U8> encoded = {(U8)LLZeroCode::FLAG, 0x00, 0x00, 0x05};
+ std::vector<U8> dec(1024, 0xDD);
+ bool overflow = false;
+ U32 dec_size = LLZeroCode::decode(encoded.data(), (U32)encoded.size(),
+ dec.data(), (U32)dec.size(), 1, overflow);
+ ensure("wrap format: no overflow", !overflow);
+ ensure_equals("wrap format: decoded size", dec_size, (U32)262);
+ ensure_equals("wrap format: FLAG cleared on header", dec[0], (U8)0x00);
+ for (U32 i = 1; i < dec_size; ++i)
+ {
+ ensure_equals("wrap format: decoded byte is zero", dec[i], (U8)0);
+ }
+ }
+
+ // decode() is a no-op (returns 0, does not touch dst) when FLAG is not set.
+ template<> template<>
+ void zerocode_object::test<8>()
+ {
+ std::vector<U8> src = makeBuffer(6, 0x00, {1,2,3,0,0,0});
+ std::vector<U8> dec(64, 0xCC);
+ bool overflow = false;
+ U32 dec_size = LLZeroCode::decode(src.data(), (U32)src.size(),
+ dec.data(), (U32)dec.size(), 6, overflow);
+ ensure_equals("not zero-coded: decode returns 0", dec_size, (U32)0);
+ ensure("not zero-coded: no overflow", !overflow);
+ }
+
+ // encode() refuses to write into an undersized destination buffer.
+ template<> template<>
+ void zerocode_object::test<9>()
+ {
+ std::vector<U8> body(50, 0);
+ std::vector<U8> src = makeBuffer(1, 0, body);
+ std::vector<U8> enc(src.size(), 0); // smaller than the required 2 * src_size
+ S32 r = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1);
+ ensure("encode: capacity guard rejects undersized dst", r < 0);
+ }
+
+ // decode() reports overflow (rather than writing out of bounds) when dst is too small,
+ // whether the destination is smaller than a single byte's worth of headroom...
+ template<> template<>
+ void zerocode_object::test<10>()
+ {
+ std::vector<U8> body(500, 0);
+ std::vector<U8> src = makeBuffer(1, 0, body);
+ std::vector<U8> enc(2 * src.size() + 16, 0);
+ S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1);
+ ensure("decode overflow setup: encode succeeded", enc_size >= 0);
+
+ std::vector<U8> dec(1, 0); // capacity == header_size exactly
+ bool overflow = false;
+ U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size,
+ dec.data(), (U32)dec.size(), 1, overflow);
+ ensure("decode overflow (minimal capacity): overflow flagged", overflow);
+ ensure("decode overflow (minimal capacity): size bounded by capacity", dec_size <= dec.size());
+ }
+
+ // ...or comfortably larger than 256 bytes but still short of the true decoded size.
+ template<> template<>
+ void zerocode_object::test<11>()
+ {
+ std::vector<U8> body(500, 0);
+ std::vector<U8> src = makeBuffer(1, 0, body);
+ std::vector<U8> enc(2 * src.size() + 16, 0);
+ S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1);
+ ensure("decode overflow setup: encode succeeded", enc_size >= 0);
+
+ std::vector<U8> dec(300, 0); // > 256, but less than the true decoded size (~501)
+ bool overflow = false;
+ U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size,
+ dec.data(), (U32)dec.size(), 1, overflow);
+ ensure("decode overflow (insufficient capacity): overflow flagged", overflow);
+ ensure("decode overflow (insufficient capacity): size bounded by capacity", dec_size <= dec.size());
+ }
+
+ // A pathological alternating zero/non-zero pattern grows under zero-coding
+ // (every isolated zero costs 2 output bytes for 1 input byte), so encode
+ // must refuse it and leave the original buffer in use.
+ template<> template<>
+ void zerocode_object::test<12>()
+ {
+ std::vector<U8> body;
+ for (int i = 0; i < 200; ++i)
+ {
+ body.push_back(0);
+ body.push_back((U8)(i + 1));
+ }
+ ensureRoundTrip("alternating pattern", 1, 0x00, body, false);
+ }
+}