From fbf678bc8f12517eb59ba707038231c8e7192468 Mon Sep 17 00:00:00 2001 From: kwy404 Date: Thu, 1 Oct 2026 13:50:20 -0300 Subject: [PATCH] decompress: decode legacy frames whose header is split across calls ZSTD_decompressStream() only recognized a legacy frame when the current call started with the full magic number and at least 5 bytes. When the first call supplied 1 to 4 bytes, the early magic check rejected the legacy prefix, and on later calls the legacy detection looked at the current input instead of the bytes already buffered in headerBuffer. Accept legacy magic prefixes in the early check, detect legacy frames from headerBuffer, and replay the buffered bytes to the legacy decoder before the rest of the input. Fixes #4802 --- lib/decompress/zstd_decompress.c | 13 +++++++++++-- tests/legacy.c | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/decompress/zstd_decompress.c b/lib/decompress/zstd_decompress.c index 325f0b54dfc..4a97abe40e8 100644 --- a/lib/decompress/zstd_decompress.c +++ b/lib/decompress/zstd_decompress.c @@ -465,6 +465,9 @@ size_t ZSTD_getFrameHeader_advanced(ZSTD_FrameHeader* zfhPtr, const void* src, s assert(src != NULL); ZSTD_memcpy(hbuf, src, toCopy); if ( MEM_readLE32(hbuf) != ZSTD_MAGICNUMBER ) { +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) + if (ZSTD_isLegacy(hbuf, 4)) return minInputSize; /* legacy frames are supported too */ +#endif /* not a zstd frame : let's check if it's a skippable frame */ MEM_writeLE32(hbuf, ZSTD_MAGIC_SKIPPABLE_START); ZSTD_memcpy(hbuf, src, toCopy); @@ -2140,7 +2143,7 @@ size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inB } if (ZSTD_isError(hSize)) { #if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) - U32 const legacyVersion = ZSTD_isLegacy(istart, iend-istart); + U32 const legacyVersion = ZSTD_isLegacy(zds->headerBuffer, zds->lhSize); if (legacyVersion) { ZSTD_DDict const* const ddict = ZSTD_getDDict(zds); const void* const dict = ddict ? ZSTD_DDict_dictContent(ddict) : NULL; @@ -2152,7 +2155,13 @@ size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inB zds->previousLegacyVersion, legacyVersion, dict, dictSize), ""); zds->legacyVersion = zds->previousLegacyVersion = legacyVersion; - { size_t const hint = ZSTD_decompressLegacyStream(zds->legacyContext, legacyVersion, output, input); + { /* the frame header may have been buffered by previous calls : replay it first */ + ZSTD_inBuffer header; + size_t hint; + header.src = zds->headerBuffer; header.size = zds->lhSize; header.pos = 0; + FORWARD_IF_ERROR(ZSTD_decompressLegacyStream(zds->legacyContext, legacyVersion, output, &header), ""); + input->pos = (size_t)(ip - (const char*)(input->src)); + hint = ZSTD_decompressLegacyStream(zds->legacyContext, legacyVersion, output, input); if (hint==0) zds->streamStage = zdss_init; /* or stay in stage zdss_loadHeader */ return hint; } } diff --git a/tests/legacy.c b/tests/legacy.c index 18a590f1d51..81bfd423f3e 100644 --- a/tests/legacy.c +++ b/tests/legacy.c @@ -156,8 +156,10 @@ static int testSimpleAPI(const char* compressed, size_t compressedSize, } +/* @maxChunk : nb of bytes given to each ZSTD_decompressStream() call */ static int testStreamingAPI(const char* compressed, size_t compressedSize, - const char* expected, size_t expectedSize) + const char* expected, size_t expectedSize, + size_t maxChunk) { int error_code = 0; size_t const outBuffSize = ZSTD_DStreamOutSize(); @@ -168,7 +170,6 @@ static int testStreamingAPI(const char* compressed, size_t compressedSize, int needsInit = 1; input.src = compressed; - input.size = compressedSize; if (outBuff == NULL) { DISPLAY("ERROR: Could not allocate memory\n"); @@ -182,13 +183,16 @@ static int testStreamingAPI(const char* compressed, size_t compressedSize, while (1) { ZSTD_outBuffer output = {outBuff, outBuffSize, 0}; + input.size = (compressedSize - input.pos > maxChunk) ? input.pos + maxChunk : compressedSize; if (needsInit) { size_t const ret = ZSTD_initDStream(stream); if (ZSTD_isError(ret)) { DISPLAY("ERROR: ZSTD_initDStream: %s\n", ZSTD_getErrorName(ret)); error_code = 1; break; - } } + } + needsInit = 0; + } { size_t const ret = ZSTD_decompressStream(stream, &output, &input); if (ZSTD_isError(ret)) { @@ -212,7 +216,7 @@ static int testStreamingAPI(const char* compressed, size_t compressedSize, break; } outputPos += output.pos; - if (input.pos == input.size && output.pos < output.size) { + if (input.pos == compressedSize && output.pos < output.size) { break; } } @@ -297,7 +301,10 @@ int main(void) /* all blocks being identical, the first @nbStreamFrames ones * are what the streaming subset must decode to */ if (testStreamingAPI(streamable, streamableSize, - expected, blockSize * nbStreamFrames)) break; + expected, blockSize * nbStreamFrames, streamableSize)) break; + /* one byte at a time : frame headers arrive split across calls */ + if (testStreamingAPI(streamable, streamableSize, + expected, blockSize * nbStreamFrames, 1)) break; result = 0; } while (0);