Files
gmipf e818a322ac Fix MS-ZIP blocks that end with short codes or copy from 32 KiB back (#7)
* Decode short codes at the end of the inflate input

InflateCodes waited for as many bits as its lookup table uses before
it decoded a literal, length or distance code. When the input ended
with fewer bits than that, the last codes were not decoded, even though
they were complete. An MS-ZIP block is a deflate stream on its own, so
its last match was lost and the block came out short (#1).

When no more input is there, the code is now looked up with the bits
that are left and used if it is not longer than those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep a full 32 KiB inflate dictionary

SetDictionary kept only the last 32767 bytes of a 32 KiB dictionary,
because the window needs one free byte between its read and write
positions. MS-ZIP sets the previous 32 KiB block as the dictionary,
and a match at the start of a block can copy from 32768 bytes back,
which then read a byte that was not in the window.

A dictionary that fills the window now starts with the read and write
positions at 0, so the first byte written replaces the oldest byte of
the dictionary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:35:22 -04:00

112 lines
3.8 KiB
C#

using System;
using System.IO;
using SabreTools.IO.Compression.MSZIP;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Compression.Test
{
public class MSZIPTests
{
[Fact]
public void DecompressorTest()
{
// Testing Note: This is a fake file that has multiple blocks
// sequentially. In real cabinet files, these are embedded in
// CFDATA blocks.
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "test-archive.msz");
byte[] inputBytes = File.ReadAllBytes(path);
var input = new MemoryStream(inputBytes);
var output = new MemoryStream();
var decompressor = Decompressor.Create();
input.SeekIfPossible(0x0000);
decompressor.CopyTo(input, output);
input.SeekIfPossible(0x3969);
decompressor.CopyTo(input, output);
Assert.Equal(65536, output.Length);
}
[Fact]
public void DecompressorShortDistanceCodeAtEndTest()
{
// One dynamic deflate block with 778 bytes of 'A'. The last match has a
// 1-bit distance code, followed by a 2-bit end of block code and 1 bit
// of padding, fewer than the 6 bits of the longest distance code
byte[] block =
[
0x43, 0x4B, 0xED, 0xC6, 0x81, 0x01, 0xC0, 0x30, 0x0C, 0xC3, 0xA0,
0xDB, 0xF4, 0xFF, 0x53, 0xD2, 0x21, 0x42, 0x4E, 0x37, 0xB6, 0x45,
];
var output = new MemoryStream();
var decompressor = Decompressor.Create();
decompressor.CopyTo(block, output);
byte[] data = output.ToArray();
for (int i = 0; i < 778; i++)
{
Assert.Equal(0x41, data[i]);
}
}
[Fact]
public void DecompressorShortLiteralCodeAtEndTest()
{
// One dynamic deflate block with 518 bytes of 'A'. The last literal has a
// 1-bit code, followed by a 2-bit end of block code and 5 bits of padding,
// fewer than the 9 bits of the longest literal/length code
byte[] block =
[
0x43, 0x4B, 0xED, 0xC1, 0x01, 0x82, 0xE4, 0x30, 0x0C, 0xC3, 0xB0, 0xB7,
0xE1, 0xFF, 0x9F, 0xA2, 0x9C, 0xCE, 0xDE, 0x2B, 0x0E, 0xF0, 0xDF, 0x7F,
0x02,
];
var output = new MemoryStream();
var decompressor = Decompressor.Create();
decompressor.CopyTo(block, output);
byte[] data = output.ToArray();
for (int i = 0; i < 518; i++)
{
Assert.Equal(0x41, data[i]);
}
}
[Fact]
public void DecompressorFullHistoryTest()
{
// First block: 32 KiB of data in a stored deflate block
byte[] first = new byte[7 + 0x8000];
first[0] = 0x43;
first[1] = 0x4B;
first[2] = 0x01;
first[3] = 0x00;
first[4] = 0x80;
first[5] = 0xFF;
first[6] = 0x7F;
for (int i = 0; i < 0x8000; i++)
{
first[7 + i] = (byte)((i % 251) + 1);
}
// Second block: one match of 258 bytes at a distance of 32768,
// which starts with the first byte of the first block
byte[] second = [0x43, 0x4B, 0x1B, 0xBD, 0xFF, 0x1F, 0x00];
var output = new MemoryStream();
var decompressor = Decompressor.Create();
decompressor.CopyTo(first, output);
decompressor.CopyTo(second, output);
byte[] data = output.ToArray();
for (int i = 0; i < 258; i++)
{
Assert.Equal(data[i], data[0x8000 + i]);
}
}
}
}