From d96a7bb60edd711df150cd6dc6bd32fb754206ae Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Fri, 23 Oct 2026 14:52:06 +0200 Subject: [PATCH 1/3] * ALZip: the real layout, ALZ bzip2 and ZipCrypto Symptom: the reader and writer used a layout of their own - a 4-byte header where ALZip writes 8, size-field widths read from the wrong nibble (0x20 taken as 4 bytes, ALZip means 2), size fields expected on directory entries that have none, and an invented end record. Real ALZip archives misparsed after the first entry and our output was not ALZip. Change, following unalz and XADMaster: - "ALZ\x01" + version header, "BLZ\x01" local headers with 0/1/2/4/8 byte size fields, "CLZ\x01"/"CLZ\x02" end records, split-marker and comment records recognised, CP949 names; - methods: stored, deflate, and ALZip's bzip2 variant (no stream header, "DLZ\x01" block markers, no block CRC or randomised bit, "DLZ\x02" end; Bzip2Decompressor's block body is now reusable for it); obfuscated deflate (method 3) is refused; - ZipCrypto decryption with the 12-byte header check, and encryption on create; every member verified against its CRC-32; - the in-place modifier wrote the invented layout and is removed; edits go through the verified rebuild. Evidence: unalz's CC0 test archives written by ALZip (stored, deflate, ALZ bzip2, ZipCrypto, CP949 names, directories) extract exactly as unalz 0.2.1 extracts them; unalz extracts three archives our writer produced (stored, deflate, encrypted, a CP949 name), and the writer reproduces the unencrypted two byte for byte. Sourcing: rung 1, alkegi/unalz (MIT) and XADMaster XADALZipParser (LGPL-2.1), notice beside the code; fixtures CC0. --- Compression.Core/Streams/Bzip2/AlzBzip2.cs | 39 ++ .../Streams/Bzip2/Bzip2Decompressor.cs | 59 +-- Compression.Tests/AlZip/AlZipModifierTests.cs | 111 ------ .../AlZip/AlZipReferenceTests.cs | 122 +++++++ Compression.Tests/AlZip/AlZipTests.cs | 2 +- .../AlZip/ReferenceVectors/.gitattributes | 2 + .../AlZip/ReferenceVectors/README.md | 10 + .../AlZip/ReferenceVectors/bzip2.alz | Bin 0 -> 272 bytes .../AlZip/ReferenceVectors/bzip2_dlz.alz | Bin 0 -> 174 bytes .../AlZip/ReferenceVectors/deflate_empty.alz | Bin 0 -> 4499 bytes .../AlZip/ReferenceVectors/deflate_pure.alz | Bin 0 -> 146 bytes .../AlZip/ReferenceVectors/dir_tree.alz | Bin 0 -> 305 bytes .../AlZip/ReferenceVectors/enc_deflate.alz | Bin 0 -> 703 bytes .../AlZip/ReferenceVectors/enc_store.alz | Bin 0 -> 3667 bytes .../AlZip/ReferenceVectors/ours-deflate.alz | Bin 0 -> 1616 bytes .../AlZip/ReferenceVectors/ours-encrypted.alz | Bin 0 -> 1664 bytes .../AlZip/ReferenceVectors/ours-store.alz | Bin 0 -> 7593 bytes .../AlZip/ReferenceVectors/store_empty.alz | Bin 0 -> 9581 bytes .../AlZip/ReferenceVectors/store_old.alz | Bin 0 -> 3655 bytes .../AlZip/ReferenceVectors/unalz-listing.txt | 44 +++ .../AlZip/ReferenceVectors/zip20.alz | Bin 0 -> 31677 bytes Compression.Tests/Compression.Tests.csproj | 4 + .../FileFormat.AlZip/AlZipCrypto.cs | 74 ++++ .../FileFormat.AlZip/AlZipEntry.cs | 34 +- .../FileFormat.AlZip/AlZipFormatDescriptor.cs | 162 ++------- .../FileFormat.AlZip/AlZipModifier.cs | 275 -------------- .../FileFormat.AlZip/AlZipReader.cs | 335 ++++++++---------- .../FileFormat.AlZip/AlZipWriter.cs | 177 +++++---- .../THIRD-PARTY-NOTICE.unalz.txt | 28 ++ Hawkynt.FileFormats.Archives/README.md | 2 +- docs/catalogue/reference-formats.csv | 2 +- 31 files changed, 646 insertions(+), 836 deletions(-) create mode 100644 Compression.Core/Streams/Bzip2/AlzBzip2.cs delete mode 100644 Compression.Tests/AlZip/AlZipModifierTests.cs create mode 100644 Compression.Tests/AlZip/AlZipReferenceTests.cs create mode 100644 Compression.Tests/AlZip/ReferenceVectors/.gitattributes create mode 100644 Compression.Tests/AlZip/ReferenceVectors/README.md create mode 100644 Compression.Tests/AlZip/ReferenceVectors/bzip2.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/bzip2_dlz.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/deflate_empty.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/deflate_pure.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/dir_tree.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/enc_deflate.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/enc_store.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/ours-deflate.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/ours-encrypted.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/ours-store.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/store_empty.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/store_old.alz create mode 100644 Compression.Tests/AlZip/ReferenceVectors/unalz-listing.txt create mode 100644 Compression.Tests/AlZip/ReferenceVectors/zip20.alz create mode 100644 Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipCrypto.cs delete mode 100644 Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipModifier.cs create mode 100644 Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/THIRD-PARTY-NOTICE.unalz.txt diff --git a/Compression.Core/Streams/Bzip2/AlzBzip2.cs b/Compression.Core/Streams/Bzip2/AlzBzip2.cs new file mode 100644 index 000000000..d039ee0d6 --- /dev/null +++ b/Compression.Core/Streams/Bzip2/AlzBzip2.cs @@ -0,0 +1,39 @@ +using Compression.Core.BitIO; + +namespace FileFormat.Bzip2; + +/// +/// ALZip's bzip2 variant (ALZ compression method 1): no "BZh" stream header, each block introduced by +/// the 32-bit marker "DLZ\x01" instead of the 48-bit block magic, no block CRC and no randomised bit +/// (the block starts at its BWT origin pointer), and the stream ended by "DLZ\x02" without a combined +/// CRC. The block payload is standard bzip2 (900k blocks). Layout as documented by alkegi/unalz +/// (MIT) and confirmed against its CC0 test archives; integrity rests on the ALZ entry's CRC-32. +/// +public static class AlzBzip2 { + private const uint BlockMarker = 0x444C5A01; // "DLZ\x01" read MSB first + private const uint EndMarker = 0x444C5A02; // "DLZ\x02" + + /// Decodes an ALZ bzip2 stream; stops at the end marker or once bytes are out. + public static byte[] Decompress(ReadOnlySpan data, long expectedSize) { + if (expectedSize > int.MaxValue) + throw new NotSupportedException("ALZ members larger than 2 GiB are not supported."); + using var input = new MemoryStream(data.ToArray(), writable: false); + var bits = new BitBuffer(input); + using var output = new MemoryStream((int)expectedSize); + try { + while (output.Length < expectedSize) { + var marker = (bits.ReadBits(16) << 16) | bits.ReadBits(16); + if (marker == EndMarker) + break; + if (marker != BlockMarker) + throw new InvalidDataException($"ALZ bzip2: expected a DLZ block marker, found 0x{marker:X8}."); + output.Write(Bzip2Decompressor.DecodeBlockBody(bits)); + } + } catch (EndOfStreamException e) { + throw new InvalidDataException("Truncated ALZ bzip2 stream.", e); + } + if (output.Length != expectedSize) + throw new InvalidDataException($"ALZ bzip2 produced {output.Length} bytes, the entry declares {expectedSize}."); + return output.ToArray(); + } +} diff --git a/Compression.Core/Streams/Bzip2/Bzip2Decompressor.cs b/Compression.Core/Streams/Bzip2/Bzip2Decompressor.cs index 0954bd3e9..048b89929 100644 --- a/Compression.Core/Streams/Bzip2/Bzip2Decompressor.cs +++ b/Compression.Core/Streams/Bzip2/Bzip2Decompressor.cs @@ -87,11 +87,31 @@ private bool ReadNextBlock() { if (randomized != 0) throw new InvalidDataException("Randomized bzip2 blocks are not supported."); + var blockData = DecodeBlockBody(this._bits); + + // Verify block CRC + var computedCrc = Crc32Bzip2(blockData); + if (computedCrc != blockCrc) + throw new InvalidDataException( + $"Bzip2 block CRC mismatch: expected 0x{blockCrc:X8}, computed 0x{computedCrc:X8}."); + + this._combinedCrc = ((this._combinedCrc << 1) | (this._combinedCrc >> 31)) ^ blockCrc; + + this._currentBlock = blockData; + this._currentBlockPos = 0; + return true; + } + + /// + /// Decodes one block from its BWT origin pointer onward (what follows the block magic, CRC and + /// randomised bit in a standard stream; ALZip's variant omits those three). + /// + internal static byte[] DecodeBlockBody(BitBuffer bits) { // BWT original pointer (24 bits) - var bwtIndex = (int)this._bits.ReadBits(24); + var bwtIndex = (int)bits.ReadBits(24); // Read symbol bitmap - var symbolUsed = ReadSymbolBitmap(); + var symbolUsed = ReadSymbolBitmap(bits); // Count symbols in use var numSymbolsInUse = 0; @@ -102,16 +122,16 @@ private bool ReadNextBlock() { var alphaSize = eobSymbol + 1; // Number of trees (3 bits) - var numTrees = (int)this._bits.ReadBits(3); + var numTrees = (int)bits.ReadBits(3); // Number of selectors (15 bits) - var numSelectors = (int)this._bits.ReadBits(15); + var numSelectors = (int)bits.ReadBits(15); // Read selectors (unary coded, MTF encoded) var selectorsMtf = new int[numSelectors]; for (var i = 0; i < numSelectors; ++i) { var v = 0; - while (this._bits.ReadBits(1) == 1) + while (bits.ReadBits(1) == 1) ++v; selectorsMtf[i] = v; } @@ -136,10 +156,10 @@ private bool ReadNextBlock() { var huffTables = new CanonicalHuffman[numTrees]; for (var t = 0; t < numTrees; ++t) { var codeLens = new int[alphaSize]; - var currentLen = (int)this._bits.ReadBits(5); + var currentLen = (int)bits.ReadBits(5); for (var s = 0; s < alphaSize; ++s) { - while (this._bits.ReadBits(1) == 1) { - if (this._bits.ReadBits(1) == 0) + while (bits.ReadBits(1) == 1) { + if (bits.ReadBits(1) == 0) ++currentLen; else --currentLen; @@ -161,7 +181,7 @@ private bool ReadNextBlock() { } var tableIdx = selectors[Math.Min(groupIndex, numSelectors - 1)]; - var sym = huffTables[tableIdx].DecodeSymbol(this._bits); + var sym = huffTables[tableIdx].DecodeSymbol(bits); ++symbolInGroup; if (sym == eobSymbol) @@ -190,29 +210,18 @@ private bool ReadNextBlock() { // RLE1 decode var blockData = Bzip2Compressor.Rle1Decode(rle1Data); - - // Verify block CRC - var computedCrc = Crc32Bzip2(blockData); - if (computedCrc != blockCrc) - throw new InvalidDataException( - $"Bzip2 block CRC mismatch: expected 0x{blockCrc:X8}, computed 0x{computedCrc:X8}."); - - this._combinedCrc = ((this._combinedCrc << 1) | (this._combinedCrc >> 31)) ^ blockCrc; - - this._currentBlock = blockData; - this._currentBlockPos = 0; - return true; + return blockData; } - private bool[] ReadSymbolBitmap() { + private static bool[] ReadSymbolBitmap(BitBuffer bits) { var used = new bool[256]; - var inUse16 = this._bits.ReadBits(16); + var inUse16 = bits.ReadBits(16); for (var i = 0; i < 16; ++i) { if ((inUse16 & (1u << (15 - i))) != 0) { - var bits = this._bits.ReadBits(16); + var word = bits.ReadBits(16); for (var j = 0; j < 16; ++j) { - if ((bits & (1u << (15 - j))) != 0) + if ((word & (1u << (15 - j))) != 0) used[i * 16 + j] = true; } } diff --git a/Compression.Tests/AlZip/AlZipModifierTests.cs b/Compression.Tests/AlZip/AlZipModifierTests.cs deleted file mode 100644 index b7c7cd770..000000000 --- a/Compression.Tests/AlZip/AlZipModifierTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -#pragma warning disable CS1591 -using Compression.Registry; -using FileFormat.AlZip; - -namespace Compression.Tests.AlZip; - -[TestFixture] -public class AlZipModifierTests { - - [Test, Category("RoundTrip")] - public void AddFile_ReadsBack() { - var ms = BuildSeedAlz(); - AlZipModifier.AddFile(ms, "added.txt", "hello-alz"u8.ToArray()); - - ms.Position = 0; - using var r = new AlZipReader(ms, leaveOpen: true); - var byName = r.Entries.ToDictionary(e => e.FileName, e => r.Extract(e)); - Assert.That(byName["added.txt"], Is.EqualTo("hello-alz"u8.ToArray())); - Assert.That(byName["seed.txt"], Is.EqualTo("seed-content"u8.ToArray())); - } - - [Test, Category("RoundTrip")] - public void AddFile_LargeFile_RoundTrips() { - var ms = BuildSeedAlz(); - var data = new byte[5000]; - for (var i = 0; i < data.Length; i++) data[i] = (byte)((i * 13 + 1) & 0xFF); - AlZipModifier.AddFile(ms, "big.bin", data); - - ms.Position = 0; - using var r = new AlZipReader(ms, leaveOpen: true); - var big = r.Entries.First(e => e.FileName == "big.bin"); - Assert.That(r.Extract(big), Is.EqualTo(data)); - } - - [Test, Category("RoundTrip")] - public void RemoveFile_DropsEntry() { - var ms = BuildSeedAlz(); - AlZipModifier.AddFile(ms, "victim.txt", "delete-me"u8.ToArray()); - AlZipModifier.AddFile(ms, "keeper.txt", "keep-me"u8.ToArray()); - Assert.That(AlZipModifier.RemoveFile(ms, "victim.txt"), Is.True); - - ms.Position = 0; - using var r = new AlZipReader(ms, leaveOpen: true); - var names = r.Entries.Select(e => e.FileName).ToHashSet(); - Assert.That(names, Does.Not.Contain("victim.txt")); - Assert.That(names, Contains.Item("keeper.txt")); - Assert.That(names, Contains.Item("seed.txt")); - - var byName = r.Entries.ToDictionary(e => e.FileName, e => r.Extract(e)); - Assert.That(byName["keeper.txt"], Is.EqualTo("keep-me"u8.ToArray())); - Assert.That(byName["seed.txt"], Is.EqualTo("seed-content"u8.ToArray())); - } - - [Test, Category("RoundTrip")] - public void RemoveFile_NotFound_ReturnsFalse() { - var ms = BuildSeedAlz(); - Assert.That(AlZipModifier.RemoveFile(ms, "ghost.txt"), Is.False); - } - - [Test, Category("RoundTrip")] - public void Replace_ViaRemoveAdd_SwapsContent() { - var ms = BuildSeedAlz(); - AlZipModifier.AddFile(ms, "doc.txt", "version-one"u8.ToArray()); - AlZipModifier.RemoveFile(ms, "doc.txt"); - AlZipModifier.AddFile(ms, "doc.txt", "version-two-replacement"u8.ToArray()); - - ms.Position = 0; - using var r = new AlZipReader(ms, leaveOpen: true); - var doc = r.Entries.Single(e => e.FileName == "doc.txt"); - Assert.That(r.Extract(doc), Is.EqualTo("version-two-replacement"u8.ToArray())); - } - - [Test, Category("RoundTrip")] - public void Descriptor_AddViaInterface_UsesInPlacePath() { - var ms = BuildSeedAlz(); - var tmp = Path.GetTempFileName(); - try { - File.WriteAllBytes(tmp, "via-if"u8.ToArray()); - ((IArchiveModifiable)new AlZipFormatDescriptor()).Add(ms, - [new ArchiveInputInfo(tmp, "via-if.txt", false)]); - - ms.Position = 0; - using var r = new AlZipReader(ms, leaveOpen: true); - var entry = r.Entries.Single(e => e.FileName == "via-if.txt"); - Assert.That(r.Extract(entry), Is.EqualTo("via-if"u8.ToArray())); - } finally { File.Delete(tmp); } - } - - [Test, Category("RoundTrip")] - public void Descriptor_RemoveViaInterface_DropsEntry() { - var ms = BuildSeedAlz(); - AlZipModifier.AddFile(ms, "drop.txt", "x"u8.ToArray()); - ((IArchiveModifiable)new AlZipFormatDescriptor()).Remove(ms, ["drop.txt"]); - - ms.Position = 0; - using var r = new AlZipReader(ms, leaveOpen: true); - Assert.That(r.Entries.Select(e => e.FileName), Does.Not.Contain("drop.txt")); - } - - // ── Helpers ──────────────────────────────────────────────────────────── - - private static MemoryStream BuildSeedAlz() { - var ms = new MemoryStream(); - using (var w = new AlZipWriter(ms, leaveOpen: true)) - w.AddFile("seed.txt", "seed-content"u8.ToArray()); - ms.Position = 0; - var copy = new MemoryStream(); - ms.CopyTo(copy); - return copy; - } -} diff --git a/Compression.Tests/AlZip/AlZipReferenceTests.cs b/Compression.Tests/AlZip/AlZipReferenceTests.cs new file mode 100644 index 000000000..466e38df2 --- /dev/null +++ b/Compression.Tests/AlZip/AlZipReferenceTests.cs @@ -0,0 +1,122 @@ +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using FileFormat.AlZip; + +namespace Compression.Tests.AlZip; + +/// +/// ALZip against unalz (alkegi/unalz, MIT). Fixtures: unalz's CC0 test archives (written by ALZip; +/// stored, deflate, ALZ-bzip2, ZipCrypto, CP949 names, directories) and archives our writer produced; +/// unalz-listing.txt is unalz's extraction of each. Every member is also checked against the +/// CRC-32 ALZip stored for it. See ReferenceVectors/README.md. +/// +[TestFixture] +public class AlZipReferenceTests { + + private const string Password = "test1234"; + + private static byte[] Vector(string name) { + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"AlZipVectors.{name}") + ?? throw new FileNotFoundException($"Embedded ALZip vector '{name}' is missing from the test assembly."); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + + private static List Listing(string vector) + => Encoding.UTF8.GetString(Vector("unalz-listing.txt")) + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.TrimEnd('\r').Split('\t')) + .Where(p => p[0] == vector) + .ToList(); + + internal static readonly DateTime FixedTime = new(2024, 5, 6, 7, 8, 10); + internal static readonly byte[] Text = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Range(0, 300).Select(i => $"ALZip line {i % 17}\r\n"))); + internal static readonly byte[] Noise = [.. Enumerable.Range(0, 3000).Select(i => (byte)((i * 2654435761u) >> 24))]; + + internal static byte[] OursArchive(string name) { + var (store, password) = name switch { + "ours-deflate.alz" => (false, (string?)null), + "ours-store.alz" => (true, null), + "ours-encrypted.alz" => (false, Password), + _ => throw new ArgumentOutOfRangeException(nameof(name)), + }; + using var ms = new MemoryStream(); + using (var w = new AlZipWriter(ms, leaveOpen: true, password: password, store: store)) { + w.AddDirectory("docs", FixedTime); + w.AddFile("docs/text.txt", Text, FixedTime); + w.AddFile("noise.bin", Noise, FixedTime); + w.AddFile("empty.txt", [], FixedTime); + w.AddFile("한글/이름.txt", Text[..100], FixedTime); + } + return ms.ToArray(); + } + + [TestCase("bzip2.alz")] + [TestCase("bzip2_dlz.alz")] + [TestCase("deflate_pure.alz")] + [TestCase("deflate_empty.alz")] + [TestCase("dir_tree.alz")] + [TestCase("store_old.alz")] + [TestCase("store_empty.alz")] + [TestCase("enc_deflate.alz")] + [TestCase("enc_store.alz")] + [TestCase("zip20.alz")] + [TestCase("ours-deflate.alz")] + [TestCase("ours-store.alz")] + [TestCase("ours-encrypted.alz")] + [Category("ThirdPartyVector")] + public void Given_Archive_When_Extracted_Then_MatchesUnalz(string vector) { + var expected = Listing(vector).ToDictionary(p => p[1], p => (Size: long.Parse(p[2]), Sha: p[3])); + using var reader = new AlZipReader(new MemoryStream(Vector(vector))); + var files = reader.Entries.Where(e => !e.IsDirectory).ToList(); + Assert.That(files.Select(e => e.FileName), Is.EquivalentTo(expected.Keys)); + foreach (var e in files) { + var data = reader.Extract(e, Password); + Assert.That(data.LongLength, Is.EqualTo(expected[e.FileName].Size), e.FileName); + Assert.That(Convert.ToHexStringLower(SHA256.HashData(data)), Is.EqualTo(expected[e.FileName].Sha), e.FileName); + } + } + + [TestCase("ours-deflate.alz")] + [TestCase("ours-store.alz")] + [Category("ThirdPartyVector")] + public void Given_SameInputs_When_Written_Then_ByteIdenticalToWhatUnalzRead(string vector) + => Assert.That(OursArchive(vector), Is.EqualTo(Vector(vector))); + + [Test, Category("ThirdPartyVector")] + public void Given_Bzip2Archive_When_Listed_Then_MethodIsBzip2() { + using var reader = new AlZipReader(new MemoryStream(Vector("bzip2.alz"))); + Assert.That(reader.Entries.Where(e => !e.IsDirectory).Select(e => e.Method).Distinct(), Is.EqualTo(new[] { 1 })); + } + + [Test, Category("EdgeCase")] + public void Given_EncryptedMember_When_WrongOrNoPassword_Then_Refused() { + using var reader = new AlZipReader(new MemoryStream(Vector("enc_deflate.alz"))); + var e = reader.Entries.First(x => !x.IsDirectory && x.OriginalSize > 0); + Assert.Throws(() => reader.Extract(e)); + Assert.Throws(() => reader.Extract(e, "wrong-password")); + } + + [Test, Category("EdgeCase")] + public void Given_CorruptMember_When_Extracted_Then_CrcRefuses() { + var bytes = Vector("store_old.alz"); + using var probe = new AlZipReader(new MemoryStream(bytes)); + var e = probe.Entries.First(x => !x.IsDirectory && x.OriginalSize > 0); + bytes[(int)e.DataOffset] ^= 0x20; + using var reader = new AlZipReader(new MemoryStream(bytes)); + Assert.Throws(() => reader.Extract(reader.Entries.First(x => x.FileName == e.FileName))); + } + + [Test, Category("EdgeCase")] + public void Given_NotAlz_When_Read_Then_InvalidData() + => Assert.Throws(() => _ = new AlZipReader(new MemoryStream("PK\x03\x04not alz"u8.ToArray()))); + + [Test, Category("EdgeCase")] + public void Given_UnknownSignature_When_Read_Then_InvalidData() { + var bytes = Vector("deflate_pure.alz"); + bytes[8] = (byte)'X'; // first local header signature + Assert.Throws(() => _ = new AlZipReader(new MemoryStream(bytes))); + } +} diff --git a/Compression.Tests/AlZip/AlZipTests.cs b/Compression.Tests/AlZip/AlZipTests.cs index be0469bcf..b0c781e59 100644 --- a/Compression.Tests/AlZip/AlZipTests.cs +++ b/Compression.Tests/AlZip/AlZipTests.cs @@ -108,7 +108,7 @@ public void Directory_Entry() { using var r = new FileFormat.AlZip.AlZipReader(archive, leaveOpen: true); Assert.That(r.Entries, Has.Count.EqualTo(2)); Assert.That(r.Entries[0].IsDirectory, Is.True); - Assert.That(r.Entries[0].FileName, Is.EqualTo("mydir/")); + Assert.That(r.Entries[0].FileName, Is.EqualTo("mydir"), "ALZip stores directory names without a trailing separator (unalz dir_tree.alz)"); Assert.That(r.Entries[1].IsDirectory, Is.False); Assert.That(r.Extract(r.Entries[1]), Is.EqualTo("hello"u8.ToArray())); } diff --git a/Compression.Tests/AlZip/ReferenceVectors/.gitattributes b/Compression.Tests/AlZip/ReferenceVectors/.gitattributes new file mode 100644 index 000000000..68a0af7e8 --- /dev/null +++ b/Compression.Tests/AlZip/ReferenceVectors/.gitattributes @@ -0,0 +1,2 @@ +# Fixture bytes are the test: never let a checkout rewrite line endings. +*.alz -text diff --git a/Compression.Tests/AlZip/ReferenceVectors/README.md b/Compression.Tests/AlZip/ReferenceVectors/README.md new file mode 100644 index 000000000..2be75616b --- /dev/null +++ b/Compression.Tests/AlZip/ReferenceVectors/README.md @@ -0,0 +1,10 @@ +# ALZip reference vectors + +| File | Origin | +| --- | --- | +| `bzip2.alz`, `bzip2_dlz.alz`, `deflate_pure.alz`, `deflate_empty.alz`, `dir_tree.alz`, `store_old.alz`, `store_empty.alz`, `enc_deflate.alz`, `enc_store.alz`, `zip20.alz` | unalz's test archives (`tests/data/`, alkegi/unalz master, fetched 2026-10-23), written by ALZip and dedicated to the public domain under CC0 1.0 by their author. Encrypted ones use the password `test1234`. | +| `ours-deflate.alz`, `ours-store.alz`, `ours-encrypted.alz` | written by our `AlZipWriter` from `AlZipReferenceTests.OursArchive` (fixed timestamps; the encrypted one with password `test1234`, its 12-byte headers random). | +| `unalz-listing.txt` | unalz 0.2.1 (built from that revision with cargo, Windows) extracting every archive above with `-P test1234`: path, size and SHA-256 of each file. | + +ALZip stores a CRC-32 for every member, so the reader's own CRC check also ties each decoded member to +what ALZip wrote. diff --git a/Compression.Tests/AlZip/ReferenceVectors/bzip2.alz b/Compression.Tests/AlZip/ReferenceVectors/bzip2.alz new file mode 100644 index 0000000000000000000000000000000000000000..9f005041e41291a6aa2a8e8ac76e9db0dbb19caf GIT binary patch literal 272 zcmZ?tiDKkpU|?_p(yR;$hb4Z;C@?TG1bvP=U(KMxnW$G%QQ`uWV_^Qhn}I>0f#Lsw zdIq!0xeW=735Ls*IW99WFlDW3bM|pxl9U|q)jr_Zj2>kl28P^)U9v0EotN^J*lJW= zxaoYW?UwY4{DtqGAGL)TEj-kImVcE@M$y7^Z6U@B=kiNQ1^i$5(^~ zE8tznj4do^&iW^wRPD-8z>x%Z77M>JLj!}4?;ZvRMg<2G29Z@Jvx6_Y9ZC^yn8$KJ vozXye!feh5eoj@wj`l4Ve4BFA9F2r8%;20bi}RCG(ggu$kl&yH$YufnGG|-i literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/bzip2_dlz.alz b/Compression.Tests/AlZip/ReferenceVectors/bzip2_dlz.alz new file mode 100644 index 0000000000000000000000000000000000000000..befeb793638d72b174e87ec414c94d9294b804d2 GIT binary patch literal 174 zcmZ?tiDKkpU|?_p(i{v5J7j*xC@?TG99So*Q_dj3SCU#$qE}K;;sTUrV9r-(Xkg-C zsQ>@}K?36jE(W#+1~9=iRdeOCUI|~`mD`pa@N+a0exc@6!*WH9DNA^Pf5H4j1K|xm zEN?hB_#U)w`5?T)x2cNd4`;*drWEypSu9V41LiSh2(R#eFq`Fqnp29p!|D_MiuMAE(E literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/deflate_empty.alz b/Compression.Tests/AlZip/ReferenceVectors/deflate_empty.alz new file mode 100644 index 0000000000000000000000000000000000000000..28a7a33251d9146eea8b7bdb291909a71de5dda2 GIT binary patch literal 4499 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 literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/deflate_pure.alz b/Compression.Tests/AlZip/ReferenceVectors/deflate_pure.alz new file mode 100644 index 0000000000000000000000000000000000000000..60caf4d41213903fe4042a372505e543381ff580 GIT binary patch literal 146 zcmZ?tiDKkpU|?_p(i{v5>lA;-C@?TFupEn&h-T1GE=jE@(JQGad3)-#V3UCe!-3tQ zd$`u9@h)Hsx!}QIAHtt&vHAJklCy?-+l%FwN7D-$G#~QVh})5TwI!@pOTtdkXi(xic9oMGJvM&l~k0R_SE%08)y*b;pwaEp{w_h z32dVb!Z@xqk)OF3IItU6Ql5`w$RmhmQH17`|4VLQYR=C?w)zP~mmosd`Q>`Qc^J5m etSred0NJB`Qa|7X$iYBoIs<(T3?DE6vY7x1o?j9G literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/enc_deflate.alz b/Compression.Tests/AlZip/ReferenceVectors/enc_deflate.alz new file mode 100644 index 0000000000000000000000000000000000000000..45768e2ab5f7f8a420cf1ae1d8914119ad5fb83c GIT binary patch literal 703 zcmV;w0zmyiOj-d70000&Oj-d203aG2`&=Oa0sx&lReDkYP7Gl#ba-@vLdW#7k*|AY zY+Q)$Ll?;}9TOA~`W^V}8kA7lnfRHUggc*2U8NEUfp)$TJ+jn$>|JY(QYz7bUxB!h z?$Zs-Ul?8B85RK*RAy7jM2E(HR+-#6C**9i4x+CS$+(if^Bx<;(2yp3^snvC3qEvSy>$c)fMp2Y+7xjOiWcZ^F2HS7$ zPZ1k4xqPDTK))=->d>e$jgV}Lcbd8sx*qF8fQNH(0#%^Pc;ulB+1rDfVwC*EF&cuF z0fYTN?d#rPrNqGZ8BA@++T`~c7jB+@`!nh6YmA!lT8a-XOOQ;{HHmh#$pkocw)aftzl!uGZxoZXucX2;9I_y`}as9RjdXHj8OO?r^%+mV{D z{Vt~ASyl&vAQ~g>92_NMM?}`ccXhyxJgjRoU(Uu^@lG$p(eiRJ)aU1=$!Xu%wwbtF zJvqBvUYJo7qi0H6V+S3Rq>nNEBd1%6IglvjkSCQtF|4G@@xWZ~xpr!TELX#6>kA2J zmX1q%RvAUrX@5MqOTT4DC;8w)TiibceI)GeiTGJb8!v2NCZIs=U0PNj$Y6E5b*J(^ zdm&g7M^|a0csIxwUYQtW1uK?Zse6U%;Km>YqwRTN<~9sO5%xkH2S&AGQo$4egaTUt l%t2QJQRUgLUm)n=k9J~571~2gS^)q600000002WwS^|F|J)Zyo literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/enc_store.alz b/Compression.Tests/AlZip/ReferenceVectors/enc_store.alz new file mode 100644 index 0000000000000000000000000000000000000000..d95b5fbd9bfb5c8ab429177f557b61da983fe53e GIT binary patch literal 3667 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 literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/ours-encrypted.alz b/Compression.Tests/AlZip/ReferenceVectors/ours-encrypted.alz new file mode 100644 index 0000000000000000000000000000000000000000..8bf1c2b109b9ba5f47b8bac4767e70f7ac22afdb GIT binary patch literal 1664 zcmV-`27mcMOj-d70000&Oj-d101yQ^rdR*~WN%}0LQGl#4FDhoIi^@400IC=$1Ip% z0N@a0Z)0;@bY*ySE_8Tw?a+p`kDb=8`Qb-cokrZc2S&d4)x30w>6O|}9$EOjE`v!f z{0f-oII)^cS63l#C!ei60V!Zgt~96+3FF+*aN6W2`!)WWrxW>RsrOaj zP&wGn0P$J3z6in6+(@Ul-LZ;7Oj-d603Zc9rdS~W0szp&0IVPdxC?G?X>(;RVrgyx z>Yc%Sw_Vup8^o5qhS{!F!M#V$lSs17HAX-%5fZn|F{2sWno| z4K#<@{BA5K2oQ{h3p(2pT(QkFNO{`vfJQY){$faAG<2z|7=gQSE>V^G*S9y0v;7eI z+AlxKDn!Pwi@N4Wx^qi$5NEtfEuMz~@>u<9$yZx_$!FpHqp4<0{NVJpYs+lHmcB3Y zfrKJ7C?BCeKL}5=EMnlzP7m^v1Lcr3=Q9Uq+S?K?3hkLqK zSOq0p<|^|chDr)4GZhN}Hg^}c|)sLQmvZ~ixrWmx~jK5kL zlOa}nH2@XZfit8i!{ZMgB$jh!{X#M`a`FrkWQ#IRta96ugsEvdHlieUhJ_8x>|l$X zKO{aFtL8k@JNaik?1$@(O$scTNP4j&N-IIFH^OWAI)qhg4a^KQ_#>uvc@{zrX0kip zAX0M{zXNVct@b#ANOdL>z4}N$u>y)U)knQBsKYobT4|RJ>{==}SWmtHaGyUq+y`rq z$c25qDp?f`xU%DDDhx4vkifjW--^74qa$<+r4CSHz8JD$h}{RXN4yDagyGF%(Lx2d z(jmKDs|N=C!KrN5|7-M!txt8nCb;pda^B=uN;RPoR5iip9dS|SsY@9pQWiRgclt7% zB#$iksGbkD;#aMZla1QnzkG{R65V2oW0(18jDn%Dj8BC?MT>)4&@!PC2XWVMsDy9&qL0ksyKiho1P&q7Zo=E$@=dn;X)V0gACXo{mYIbP zD`veR=w9h&P(hiyj?zrv=y>I?MW9MGN&}wCo)sew;O9Is%HRC((APBqpw(fdd#zZY ze5G>Q>el&<_^`{9hDd;j18+-H*)p&UNn2s(gq(i_!61(KGTt-3S503$-vD@yB*x%C z%&a}*1o05Hg!>iJy`%v!>4{&x#8tVZNr6x_pc><5f(yLL+VXw+VA1C(ILTSFS`#*Z z?~htJrDj)oQ0u!b%-ju&qpXJ8yH>xqWt+e-7j8*~WRAUyvjN81EeQ7Z#0g-hEd7`& z#W0_)-svIVYgaV-WueNgi3EdLNSmGK02%cw}Datm=k* zYpvURa*bN0ujrZ8GQ1JSB;AuyR`0wW!=A=o#IQkHoP(f;uoTUKSf%+i^TWvGodp2V zfoQ%ygG30!ce{`Cw2?aBA^QK3-)AeLQ+dh}*^eV^#Hhj5Skfv0=Ip07A-^2YLeKg$ zvAX8Mjc&vN-4u?V@Di7s8sU%w{UUPCAQe-8|GhEzxf6xC|9>>Wn$f6&$NOd?2$GFE zhftYBm292A0g0Gh74RYq_JL@dp*?e{-q}8oK4n(xC@W#T^(dBanj&E%7<>vaP-Cw) zk2Bei%oi%BBh0?Xj+2zOh6V^gyV%F1M=Zeh7YmC#X};O^ndxXzxKcO zseviu87!agM&&+zyc`xYDc`kH# zbV8-Clz^p2ygxV1XF^O`0Sy2k1v#cz5dZ=JLCYZRBxJ|YvD;k0%($m6ba-@jrFgV^ zt|*+*-fGMwf3~PRZFE28d)e+3g`<32ykG7xoiNdn&;0N7@O;@sPzugNOj-c|00000 K0000(Oj-iR-5JmT literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/ours-store.alz b/Compression.Tests/AlZip/ReferenceVectors/ours-store.alz new file mode 100644 index 0000000000000000000000000000000000000000..3cb5bcef3bef44be6212c14f7a160daac70bb83a GIT binary patch literal 7593 zcmeI1Rd^Iv7={;z1PKns2Dgy1c%VoKPLQG@1WU4oAW0xg3GUY7?k$$!5HvW&f|DQx ziUo?hQ@r1I_L(yrcAuwry&7&{vKKSw<$w46H|ygUkj%|uvDC)jDJ>qUyrcasmXPq^ zNd6#$MdJ@Nyxt(*b54>+k_TT3wA-TWUiK)vk6%D&gw``O%%+vi;P%y(RkigM+xTv@+)81-qa>f>0|=dr2}WL2NYsy@;;_fZ}~y^DGi^&aXi z)H|4m*3AED%^YvdblRp_%{Ma5eD}s2s5LQ@1te-j%}$#e8J1G@{nFgXH1&4t)ss?vP7yhHu zrv+6n{+xZ&k54Ch9_yBRneUCE`E~{TGpoYc-kH`melV`s!Qf=SRlhtS*Va}qr+$C3 zhueyJw?-7)+u_5!Du3Fu#5Q~UYpEkYrCj23eQ=)b{%>ZKOAODjy5YSsMfP|8vas5v zs2rPHJeyqRcxc+_IyZ+E*xml!>`LeQc&u;oaD4H@A<2KQd39j!ZEapnD|@P!`^x%v zMituE@zZ?o3w^U~Z2n|o>7!j!Evub0B;U?9(T&?U4odcKnEg_RSjG z?8yWiG}Tg{>qGMH@P9M2JPx|1(Y-N64|Msm5a4rc2KX`nA6*CFcN6?MfN$~;;6niZ z>Og>hMeyzbKdKPG&j;{m=F!M6qYgfM`=OYolozF&5Lp9Jt}mIHi#g0Beh>-+%z5Wv^C0`RQ~z9+!n z9trRt2|g>pPbdZOO94Lb4uCID@QnceKo?FJz_)x3@ZAZ1IKaQ31Mry%z68KKCJf-M zrvbhJ!FK|9$AkfVx6}YXl;CFpe5SPkUyR_Z1H5Cx0KVQWfbT%?c7S(G7{CwC1Mo8l zel@@s*$?p52)+ftJ0=X^+rI<&J_J7=;2jeN@YBiy{7Qn~2k_n(0KPfFcLjLIgaLd+ zMt~nn@QVQ6F<}57Qy1X(5d3+7Z~6$}Z2*655WqVo4B$r>2KWU8zX{+S69(|R0s#Ij z!9M`_;A8+l0N`Iv1$f7V0sOow03S>6M*!Y2VE~^P4)FH~{tMO4p{pIOsNF1t=A+NE{1cPeTJ>1w~A+G4_}_FP@Ehda=C)d@UMzzI+QEhKU?Id082&ye6 zjB4l9)sCUsV#26)Q(f(ARQt8A_Gm?IPhIU@R9j3K)ehFxeo3|6bhYOxYWwJFCsJ)O zVKN>BZ^VOYi+D)2MLY=JMLY=JhzHdc@sMhZco4jcco4i152`KVA=MV~Ab1z?Ab2Al zR9nPDsx9I{@Gjy(@J2kSwupyRTf~EEyNCzD8}XppA|6t05f7^EA|3>9#Di*!cu;L4 z9#mVzL#i#}LA8x|P;C(ps%^xBYKwSCwM9Irwu^XBZ6h93Tf{@EE#g77UBrWG8}Xpp zA|6t05f7^EA|6!RhzHeG_Wt}<^Q&(@`Kr%x?Quv*d}8e7z=nLw_{< GJNdtLS=pQb literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/store_empty.alz b/Compression.Tests/AlZip/ReferenceVectors/store_empty.alz new file mode 100644 index 0000000000000000000000000000000000000000..270ea0600c21f8ee5c442bc8cb4e0c70a933cf8a GIT binary patch literal 9581 zcmeHM^;Z2C1# zb3W($e&7G$Wqz2Mb7$t9d!93M?{nvQsi+IY1pok&e@7eu*n{du9{@17b%1+t!rkHj zSPB2Ng8y!68cnb=0vG`toXkz_ZU6Sp&gN#EmY%c`*w_dS3o8IR+}s`h&q2UmpDfUy zn^5{Cn-S2;#2+~_Lx`;;s_$?#^t|ju*V!nh2$>NPXh3;PB{;Jje;$*6{Cz@O4;9yI z#+#xDiSIQ#{%n*X^2Spc?_b}r|La9%pJELjHXgQvlf5OUvxoCv#~#`P0uKm0An<^| z0|E~S{69mW8m9pO9HgrUC=Sy7Ls)SCOBHT8zCQyH0tlUq?ab_LIZZy;iTnuBQ7Qhg z-z~kY{1QS;j^~WYi1Lero%yA4*T>wr`=+RG_YufPNKBS+~vl9`PvJ{5D5&Fz+nRGwUO-5?k$^fLhCIVWXny z?a_M?5n|C>aCyJJv^hRv zV8KbD<%z<(y(bBqIfNIwUJRN{2gqGX7n222)pn3We|9g<6>S%@Vd}2D@P6UcKU!qK zrH!ebx6o~9OGm@#7SGkB;8a!KXj<-XZv%*eA!FBP=r{`3*dp_w#M71%G>lS4;Aek^ z6+qsigcC$Erfhs1*qQ1`VYQn(X;t<@JHC>OsGk%!EdJTjc~~XVKO|1zoy(k_t2m~# zC5(NLb0cn@@vN?GCPA7{^oj*Pl$N!FHFx#McSB_rd*DGQ$bNj+y0ps-ld8oPAobGo zE_<#$Lf1*#0vfUBj(P2sZ1{ZGd&6U){8}>y#MbEiD`*U93xhtd00E;P3Y^!gm@W7| zWW&r!CzO<9zzgpCZAE3=Hku_%=FVyF&Zn>gAom81WmT)DF!rvlc+HSmzw4REYAu#0 zk~F6x28@{=r+qMiOTE5&L6?4&o~>?=u2xuqJ9;9X$_Go*^iZHgW!4;^&J)W8m|H<} zCX3UCVrZFSm4FrH1i{T<)Q+>o5!kfnp=q%qu=EQ7mX^Vc3CM6L@iM9OAQ5{^+-e|U zfP!WhJo>59x&$MC?V;h%q=id_+5w}^j$aZX|wtLcp33^MWmI^r9>BMa+-L@ggt&hmgYD!f2TP%#r7rk5E zgoTpveD+Gkys8)&Z%{V-q!MH1Q55J}|t>JP%OcTUG^f#x;e zI!09*5DL~4uj;4&&^)q{9+@d0*@g&Nlg5bq_}HqJ^xHsn5+qDBDIpqSuTS+BT9pv~ zmbt|ew*&dT%j>hEM<(5Cb7R4h5@F@D-`?K8)eK1xGcda4GSy+6u`-MnaC+{)^Syr8 zcdRp6{IgbZQkarrd+T!&X6z*$Z&+C(6s=MjqU}=yZF?Wgeg^rvS4EaKfmn(((g%ol zP74wBY`PF8?W}xB2$Q=zP1C2b((hY2i$M;)Z0`#f(K0sZq+MDMYYmR8dE77NXf9xF zG~gCnZ<)XsHJ+F?=ej9C2_3bu)ADPa6{e%26quCPOdSrNT3%8yS~es&!fVhr`%~x0O&i>XTo)pVw~)-Iw(~X*}Iv5_}Zo=n&lE6OSs&^{!35 z&=XGm-sZbt;{Vfk(NhNt`%&f^cIJ9J2C7<{_{&Mnr$&YS_8@j^!+M>PMAeCAjItk1GIjp1v z43I`t6eHDRhigv-Hy__cv&S0zgS_oBDJW9pMrAa!ZDOnwGb>6%IpKiAL{eyD{yAlk zY1SdCd%cceH-?x7C8Y|VH73!p4N-TSNf+q#pnET|(^&qhqde#2ct8Lqq;<$t>Cn=e z{A^HmnSH=tFRd=CG^=M2ik0Aw{#Cl4q<(-RE0OxFk_gUCL=_eBYHJqV5CepL|I^H( zPkHe5(6pg0hZL_YMTve8YQBqLGGr>=dQ86DJuto;bv7bpo}H{?zS_5Lss6b2z>b~b z>hP{vVnq=njjn*0LCgV^V@t+~oEY*m%vIb#4`$BHHi5N|Lk<&vf2DOMBHHRX1!zZ@ zX(ESARbtdy^nLM3=-tqb^^KXq6&(|;?03Y435H8_9o$MY&>DA1m)OE$gTtX;>pYhz ztL#$Xf$g8NtJ6MvHmXSqz?IzA=4apV_mqs=yPT~rto%%{uS?K3<591l)eUTM$?1*f zx8~O+3J0)Pr`HUXf9rQ02toDLXkzI;Xq^Z5PSjO;BN>X*a)?vBUuXzSIfh3R0@y1 zf4^5BoOyLtAvKTTm6SS{>;2}?hmNuQz&~3vnx`V9b*(a`G^Zd;ezz>88J4a`*-G8g zmAEKd$BhzZkLh@M-Bv|#$@_bj-yR^4aY)RZ5mG;T7m6_1#3=BWG?cflJm!(z&sgcP zF*~w%ovX2}vzs55cuQ7IS%ZIszlj!c8Fcl=kXuJNDG=r%civ+oaeNhm5jrS=)zbs3oK{4mDQmRfD`xi7e(nMRCnIyGge zN`WtS<%F+v%;MQ+9)R6Xjm{qe4Yz+*$>4}(r>rM7mwtMhiHDhrpD1E)@mwYLLTPD+ z3^D1+&$MO(njE;s4r%wb3|KZeC4F0*(daO4vQI7jPqlA@SV?grTZu$C9N)b@FUMGf z0{Mdsx5uRMHDX%B%P*f{J4!Zk9_B;>#|op8nT*4UddsZgbON|?WOYn>FdYf z9-#c_CuTjMvujl{7;I%ls%b0Ji6E>si8}kZR;~2WOS~Y-eU9AAWt1@N1|j2KWsU6C z9X^_y339O((ozQNsl05xENv(k$!(7g7QyVDY6TYhZh!le!VDe}h9I7@I4Kt(`YKHiLkym@eNn_6TY40FLjYA36RXrYYRlX57v&=p|+WD$FH=*L2 zTQ8qTc)_V0MCZRS5&eE)j&Q%XVeZ=W_GWL9g)L}#3Ban zXqMt?g$&4Qzl&e&3>##wjaNzvm+|mdi}FujbJViJoo5Jm1cgW(UTWYg%d4f)>BM%L zqnMkxMIBagPo_Ko48sO!jX7{URIsn@H|SlNwl`tLPggC-l8$wyJuIS2Ma0XHRl<-yP-Ph+*Z~XCAd#K+YZpg)3;sXw5uoE|0 zUKPp}te4F_B7+@}cBu4&6J%SuH>X zJ%?A07uI*=(UMKmADPPTG&*h30Di0<7nJBwTLxA+#?3PO@bk<4T}fvLswdVIrq-V) zJoll#H#bC2`%-$VnS6Co5FU{JuVMfpYhcq(dhv3$Gaj~+P>*P+KXGQwc~P6SJ!EP4(#K=8&*WOHgpN6?QIt>XGpTwu#iKr`m| z(rKitFEH8Rx!>fP^F-Z(Kvwe-D2x0{fxMEfI&pTWZrQ?liY5sny-sO(BX#9@dcQ!f zAD6_3u{GSx;%mZ^=?gUx8cHtFdZ8KgOyi8s3Kr!jB#3DDY|Gpb(%C(s=orXdLj)`f z>_-1Zs7ykE-;gjmYLEF`>a=q7ZUHwZp&p=%FCFS-T8o%e+V$DHob8Q$B6Q8&StVBI z)EuF09ZHV$ZhAd5z`5wkAHx{y)^v#Q+l?Yb_0&U3p)-HycLKiiOw^sy&f-1W< zK^oKoTgGW8%X>DtCOe7AymO%wRyIMQDhd$>A+41pAP_84IHdFbWEHsaLOkqzk;fG8 z9Hn@dZou|&_^E%1C;d-;6BT+5&k>7b1Ygl_V(n0{49sCCP9>#1u+f>Oix+Vh>v=yXR44{yv67&0X3=~t#x5m=g%T?EZzflXEFc_YzRh-jVw zA{=eU+)WdMyK>c>5g>W>#E;~<^vV)r%%nn41(S&}i4z0;IS{#p^QQgFg}U`e(fpl3 zt7w*WT6q;#-t%SxR2OiOd=jjp(0UAUBOfPP*_-GVz?)zLz&*{8I?yYWaT+h8|^ zeOss587N?!=&<{qG~uTH6rf)&)fm4;OE&!(o}(nRw|M&%w~U@EngN@IweuB?e`@|L7K)@6ExE zE+0&-!6r`jZgyY`dv~yvi>-q**xuFL2@L^z<7vTT^ literal 0 HcmV?d00001 diff --git a/Compression.Tests/AlZip/ReferenceVectors/unalz-listing.txt b/Compression.Tests/AlZip/ReferenceVectors/unalz-listing.txt new file mode 100644 index 000000000..7e1110882 --- /dev/null +++ b/Compression.Tests/AlZip/ReferenceVectors/unalz-listing.txt @@ -0,0 +1,44 @@ +bzip2.alz a.txt 2340 fa80a13de5c796b12cd4689588599fc15d85540369114d3cafff16edfed42f27 +bzip2.alz b.txt 2160 388048314e46e0249d96324a220754c01c1c728821fbabb1bf88f7314bd2dea5 +bzip2_dlz.alz text.txt 3600 f347c6956986bb5e2616fd001f81f0fe7d4fcbf68456613b4ed6a2641d29b82a +deflate_empty.alz empty.txt 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +deflate_empty.alz precompressed.gz 95 07ec682eb8d41b54afe790caa15773059968ff0befb3b090ef96a57856980f98 +deflate_empty.alz prog.sys 2060 a94e58afa385e60d53d5920852dff86ba296f34b25b12a7e9c8b3a6de4d71132 +deflate_empty.alz random.bin 4096 c7caf0d1f590099d235bdd36c659a428e9f7780ccf4ee44128b1150884f45e33 +deflate_empty.alz text.txt 3150 b2a9316bdcae0fd397a8430e94a6294a4ea720c4faf9e1f77d4e4e30787ba6ac +deflate_pure.alz text.txt 9000 2c44089c1478988b4f2d75dcce66a08008c0ab65e71877a8064ec93154e8b380 +dir_tree.alz work/tree/sub/deeper/deepest/three.txt 16 03c29cb7bd343b7281e59b3b64cf552a733272381841232baa063358e3de7c12 +dir_tree.alz work/tree/sub/deeper/two.txt 8 04d0ef58d049b7a6620aa4ead8455d7c3613568c9d136a60e1afe898783dadae +dir_tree.alz work/tree/sub/one.txt 8 84d80ad6c9d14e2373568ddfc660a1bb4337355a2efaaaa0abbdce32091a2f64 +dir_tree.alz work/tree/top.txt 10 33685921186a8fed52c9db4ea189ae5c1c755f5f148c9af16485362b5cef0163 +enc_deflate.alz a.txt 3150 b2a9316bdcae0fd397a8430e94a6294a4ea720c4faf9e1f77d4e4e30787ba6ac +enc_deflate.alz b.bin 512 f97a9576c02fa95055ddca8bf384caf3fbcb229bb78d0d782a322a558712f2eb +enc_store.alz text.txt 3600 f347c6956986bb5e2616fd001f81f0fe7d4fcbf68456613b4ed6a2641d29b82a +ours-deflate.alz docs/text.txt 4320 18862dacf0b4eca9e545f41065d971e81f50ca64c00177c6a1b11d74c4abfd0e +ours-deflate.alz empty.txt 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +ours-deflate.alz noise.bin 3000 416b6d6783fba12823f4180fe7b9b7a238932d6eed210a91e66122650cce468d +ours-deflate.alz 한글/이름.txt 100 69e9ad63e9666bf5a0ce5bd7ff8726d7ac9c84e0e809a8ce5f21dccfab3989e4 +ours-encrypted.alz docs/text.txt 4320 18862dacf0b4eca9e545f41065d971e81f50ca64c00177c6a1b11d74c4abfd0e +ours-encrypted.alz empty.txt 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +ours-encrypted.alz noise.bin 3000 416b6d6783fba12823f4180fe7b9b7a238932d6eed210a91e66122650cce468d +ours-encrypted.alz 한글/이름.txt 100 69e9ad63e9666bf5a0ce5bd7ff8726d7ac9c84e0e809a8ce5f21dccfab3989e4 +ours-store.alz docs/text.txt 4320 18862dacf0b4eca9e545f41065d971e81f50ca64c00177c6a1b11d74c4abfd0e +ours-store.alz empty.txt 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +ours-store.alz noise.bin 3000 416b6d6783fba12823f4180fe7b9b7a238932d6eed210a91e66122650cce468d +ours-store.alz 한글/이름.txt 100 69e9ad63e9666bf5a0ce5bd7ff8726d7ac9c84e0e809a8ce5f21dccfab3989e4 +store_empty.alz empty.txt 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +store_empty.alz precompressed.gz 95 07ec682eb8d41b54afe790caa15773059968ff0befb3b090ef96a57856980f98 +store_empty.alz prog.sys 2060 a94e58afa385e60d53d5920852dff86ba296f34b25b12a7e9c8b3a6de4d71132 +store_empty.alz random.bin 4096 c7caf0d1f590099d235bdd36c659a428e9f7780ccf4ee44128b1150884f45e33 +store_empty.alz text.txt 3150 b2a9316bdcae0fd397a8430e94a6294a4ea720c4faf9e1f77d4e4e30787ba6ac +store_old.alz text.txt 3600 f347c6956986bb5e2616fd001f81f0fe7d4fcbf68456613b4ed6a2641d29b82a +zip20.alz binary.bin 1024 785b0751fc2c53dc14a4ce3d800e69ef9ce1009eb327ccf458afe09c242c26c9 +zip20.alz empty.txt 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +zip20.alz euckr_content.txt 71 77bc237fbcc49c3138cc308632aa91e57efeffa535a988adbfac2f48bf0d9472 +zip20.alz hello.txt 131 d18ec9260c5c6902d8fc74c07cc092f45a2c4e6074e4a5b3cdeaf760eb96e7ce +zip20.alz large.txt 131364 176933d71d6ef16903b2ee4f61947af12fa8c4c450d747e4569a9bcf9267da53 +zip20.alz repeated.txt 720 1cec0d613b135373d084c68685b7476fb7c0d7f5caa28f35946ea6d302ba9225 +zip20.alz subdir/inner.txt 28 347c0d7c9a1ff5d5e1710b05dc385dea8651892de493ae0f7eade2e93e8b0c0e +zip20.alz subdir/nested/deep.txt 20 66c77c742ffe88ea0fa1e085e32b20de9f0cfd763ebdb08d27df20e6a4bb1090 +zip20.alz 뷁테스트.txt 72 696f6644df6b920646bf3e92745923109d0cfc8ad470818dde56176179408090 +zip20.alz 한글파일.txt 56 0442367f9479d95a35b91a6b80ac5f7ffc199aa91f29855b6a012070dc97af08 diff --git a/Compression.Tests/AlZip/ReferenceVectors/zip20.alz b/Compression.Tests/AlZip/ReferenceVectors/zip20.alz new file mode 100644 index 0000000000000000000000000000000000000000..b40b89b85dc0f56a4f5723cc1d6c737bb858e5a4 GIT binary patch literal 31677 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

D(H+=YV;(Uf!#E$Q+3j(=<&NA>k5+ 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 q%2=^_{N7tB9d?U4f ZfsVectors.%(Filename)%(Extension) + + + AlZipVectors.%(Filename)%(Extension) + DarVectors.%(Filename)%(Extension) diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipCrypto.cs b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipCrypto.cs new file mode 100644 index 000000000..65379e887 --- /dev/null +++ b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipCrypto.cs @@ -0,0 +1,74 @@ +namespace FileFormat.AlZip; + +///

+/// ALZip's encryption: PKWARE's traditional ZIP cipher (APPNOTE 6.1), password bytes in CP949, with +/// a 12-byte header whose last decrypted byte checks the password against the CRC's top byte (or the +/// DOS time's second byte when the entry has a data descriptor). +/// +internal sealed class AlZipCrypto { + public const int HeaderLength = 12; + private static readonly uint[] CrcTable = BuildTable(); + private uint _k0 = 305419896, _k1 = 591751049, _k2 = 878082192; + + private static uint[] BuildTable() { + var t = new uint[256]; + for (var i = 0u; i < 256; ++i) { + var c = i; + for (var j = 0; j < 8; ++j) + c = (c & 1) != 0 ? (c >> 1) ^ 0xEDB88320u : c >> 1; + t[i] = c; + } + return t; + } + + public AlZipCrypto(ReadOnlySpan password) { + foreach (var b in password) + this.Update(b); + } + + private void Update(byte c) { + this._k0 = CrcTable[(this._k0 ^ c) & 0xFF] ^ (this._k0 >> 8); + this._k1 = (this._k1 + (this._k0 & 0xFF)) * 134775813 + 1; + this._k2 = CrcTable[(this._k2 ^ (this._k1 >> 24)) & 0xFF] ^ (this._k2 >> 8); + } + + private byte KeyStream() { + var t = (ushort)(this._k2 | 2); + return (byte)((t * (t ^ 1)) >> 8); + } + + /// Decrypts the 12-byte header and reports whether its check byte matches. + public bool CheckHeader(ReadOnlySpan header, byte expected) { + byte last = 0; + foreach (var b in header) { + last = (byte)(b ^ this.KeyStream()); + this.Update(last); + } + return last == expected; + } + + public void Decrypt(Span data) { + for (var i = 0; i < data.Length; ++i) { + var p = (byte)(data[i] ^ this.KeyStream()); + this.Update(p); + data[i] = p; + } + } + + public void Encrypt(Span data) { + for (var i = 0; i < data.Length; ++i) { + var p = data[i]; + data[i] = (byte)(p ^ this.KeyStream()); + this.Update(p); + } + } + + /// An encryption header for a fresh cipher state: 11 random bytes and the check byte. + public byte[] MakeHeader(byte check) { + var header = new byte[HeaderLength]; + System.Security.Cryptography.RandomNumberGenerator.Fill(header.AsSpan(0, HeaderLength - 1)); + header[^1] = check; + this.Encrypt(header); + return header; + } +} diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipEntry.cs b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipEntry.cs index e71f1e9e0..ed822286d 100644 --- a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipEntry.cs +++ b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipEntry.cs @@ -1,41 +1,41 @@ namespace FileFormat.AlZip; -/// -/// Represents a single file entry in an ALZip (.alz) archive. -/// +/// One local file header of an ALZip archive. public sealed class AlZipEntry { - /// File name (path within archive). public required string FileName { get; init; } - /// Uncompressed size in bytes. public long OriginalSize { get; init; } - /// Compressed size in bytes. public long CompressedSize { get; init; } - /// Whether this entry is a directory. - public bool IsDirectory { get; init; } - - /// Compression method: Store(0), Bzip2(1), Deflate(2). + /// 0 store, 1 ALZ bzip2, 2 deflate, 3 obfuscated deflate. public int Method { get; init; } - /// CRC-32 checksum of uncompressed data. public uint Crc32 { get; init; } - /// Last modification date. public DateTime? LastModified { get; init; } - /// File attributes byte. + /// DOS attribute bits (0x10 directory). public byte Attributes { get; init; } - /// Offset of the compressed data in the stream. + /// Descriptor byte: bit 0 encrypted, bit 3 data descriptor, high nibble the size-field width. + public byte Descriptor { get; init; } + + internal uint DosTime { get; init; } + + internal byte[]? EncryptionHeader { get; init; } + internal long DataOffset { get; init; } - /// Human-readable method name. - public string MethodName => Method switch { + public bool IsDirectory => (this.Attributes & 0x10) != 0; + + public bool IsEncrypted => (this.Descriptor & 0x01) != 0; + + public string MethodName => this.Method switch { 0 => "Store", 1 => "Bzip2", 2 => "Deflate", - _ => $"Unknown({Method})", + 3 => "Obfuscated deflate", + _ => $"Unknown({this.Method})", }; } diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipFormatDescriptor.cs b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipFormatDescriptor.cs index 8fa3b0b60..0c9950f87 100644 --- a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipFormatDescriptor.cs +++ b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipFormatDescriptor.cs @@ -5,176 +5,78 @@ namespace FileFormat.AlZip; /// -/// ALZ archive — the proprietary container of ESTsoft's ALZip (Korean shareware archiver). +/// ALZip (ESTsoft), the Korean archiver: stored, deflate and ALZ-bzip2 members, ZipCrypto encryption. /// /// References: /// -/// http://kippler.com/win/unalz/ — unalz — open-source ALZ extractor, the de-facto format reference -/// https://en.wikipedia.org/wiki/ALZip — application background -/// The format is proprietary (ESTsoft); no official specification was published +/// https://github.com/alkegi/unalz — unalz (MIT), its CC0 test archives, and the reader the layout follows +/// https://github.com/MacPaw/XADMaster — XADALZipParser (LGPL-2.1) /// /// -public sealed class AlZipFormatDescriptor : IFormatDescriptor, IArchiveFormatOperations, IArchiveCreatable, IArchiveModifiable, IArchiveDefragmentable, IArchiveLayoutMap { - - /// Rebuild-based defrag: extracts then re-creates the ALZ archive in listing order. - public void Defragment(Stream archive) - => this.Defragment(archive, new DefragOptions { Mode = DefragMode.ConsolidateAtStart }); - - /// Rebuild-based defrag: extracts then re-creates the ALZ archive per the requested mode. - public void Defragment(Stream archive, DefragOptions options) { - DefragRebuilder.Rebuild(archive, options, - readEntries: stream => { - var r = new AlZipReader(stream, leaveOpen: true); - return r.Entries.Where(e => !e.IsDirectory).Select(e => (e.FileName, r.Extract(e))); - }, - buildImage: files => { - using var ms = new MemoryStream(); - using (var w = new AlZipWriter(ms, leaveOpen: true)) { - foreach (var (n, d) in files) w.AddFile(n, d); - } - return ms.ToArray(); - }); - } +public sealed class AlZipFormatDescriptor : IFormatDescriptor, IArchiveFormatOperations, IArchiveCreatable, IArchiveModifiable, IArchiveLayoutMap { /// public IEnumerable EnumerateLayout(Stream archive) { archive.Position = 0; - var r = new AlZipReader(archive); - foreach (var e in r.Entries) { + var r = new AlZipReader(archive, leaveOpen: true); + foreach (var e in r.Entries) if (e.CompressedSize > 0) yield return new DefragBlockInfo(e.DataOffset, e.CompressedSize, DefragBlockKind.Used, FileName: e.FileName); - } } - /// - /// Gets the id. - /// public string Id => "AlZip"; - /// - /// Gets the display name. - /// public string DisplayName => "ALZip"; - /// - /// Gets the category. - /// public FormatCategory Category => FormatCategory.Archive; - /// - /// Gets the capabilities. - /// + // R/W: create writes stored/deflate (optionally encrypted) archives unar and unalz extract; edits + // use the verified extract -> edit -> re-create rebuild (default IArchiveModifiable). public FormatCapabilities Capabilities => FormatCapabilities.CanList | FormatCapabilities.CanExtract | FormatCapabilities.CanCreate | FormatCapabilities.CanModify | FormatCapabilities.CanTest | - FormatCapabilities.SupportsMultipleEntries | FormatCapabilities.SupportsDirectories; - - /// - /// Adds (or replaces by name) files inside an existing ALZ archive. Uses - /// — Add appends a new entry over the trailing - /// CLZ end marker; Remove walks the entry chain and shifts trailing bytes. - /// - public void Add(Stream archive, IReadOnlyList inputs) { - foreach (var (name, data) in FlatFiles(inputs)) { - AlZipModifier.RemoveFile(archive, name, wipeData: true); - AlZipModifier.AddFile(archive, name, data); - } - } - - /// Removes named entries; uses . - public void Remove(Stream archive, string[] entryNames) { - foreach (var name in entryNames) - AlZipModifier.RemoveFile(archive, name, wipeData: true); - } - /// - /// Gets the default extension. - /// + FormatCapabilities.SupportsMultipleEntries | FormatCapabilities.SupportsDirectories | FormatCapabilities.SupportsPassword; public string DefaultExtension => ".alz"; - /// - /// Gets the extensions. - /// public IReadOnlyList Extensions => [".alz"]; - /// - /// Gets the compound extensions. - /// public IReadOnlyList CompoundExtensions => []; - /// - /// Gets the magic signatures. - /// - public IReadOnlyList MagicSignatures => [new([(byte)'A', (byte)'L', (byte)'Z', 0x01], Confidence: 0.95)]; - /// - /// Gets the methods. - /// - public IReadOnlyList Methods => [new("alzip", "ALZip")]; - /// - /// Gets the tar compression format id. - /// + public IReadOnlyList MagicSignatures => [new("ALZ\x01"u8.ToArray(), Confidence: 0.95)]; + public IReadOnlyList Methods => [new("deflate", "Deflate"), new("store", "Store")]; public string? TarCompressionFormatId => null; - /// - /// Gets the family. - /// public AlgorithmFamily Family => AlgorithmFamily.Archive; - /// - /// Gets the description. - /// - public string Description => "Korean ALZip archive format"; + public string Description => "Korean ALZip archive (stored, deflate and ALZ-bzip2 members; ZipCrypto)"; - /// - /// Lists the entries in the supplied container. - /// public List List(Stream stream, string? password) { using var r = new AlZipReader(stream, leaveOpen: true); return r.Entries.Select((e, i) => new ArchiveEntryInfo(i, e.FileName, e.OriginalSize, e.CompressedSize, - e.MethodName, e.IsDirectory, false, e.LastModified)).ToList(); + e.IsDirectory ? "dir" : e.MethodName, e.IsDirectory, e.IsEncrypted, e.LastModified)).ToList(); } - /// - /// Decodes the supplied input. - /// public void Extract(Stream stream, string outputDir, string? password, string[]? files) { using var r = new AlZipReader(stream, leaveOpen: true); foreach (var e in r.Entries) { - if (e.IsDirectory) continue; if (files != null && !MatchesFilter(e.FileName, files)) continue; - WriteFile(outputDir, e.FileName, r.Extract(e)); - } - } - - /// - /// Opens a single entry as a bounded read-only stream. The reader produces - /// the decoded bytes per entry; the matched bytes are wrapped in a - /// sized - /// to their logical length. - /// - public Stream OpenEntry(Stream archive, string entryName, string? password) { - ArgumentNullException.ThrowIfNull(archive); - ArgumentNullException.ThrowIfNull(entryName); - if (archive.CanSeek) archive.Position = 0; - var r = new AlZipReader(archive, leaveOpen: true); - foreach (var e in r.Entries) { - if (e.IsDirectory) continue; - if (!string.Equals(e.FileName, entryName, StringComparison.OrdinalIgnoreCase)) continue; - var bytes = r.Extract(e); - return new Compression.Registry.Streaming.BoundedEntryStream( - new MemoryStream(bytes, writable: false), bytes.Length, leaveOpen: false); + if (e.IsDirectory) { + Directory.CreateDirectory(Path.Combine(outputDir, e.FileName)); + continue; + } + WriteFile(outputDir, e.FileName, r.Extract(e, password)); } - return new Compression.Registry.Streaming.BoundedEntryStream( - new MemoryStream(System.Array.Empty(), writable: false), 0, leaveOpen: false); } - /// Native in-memory single-entry extraction routed through the bounded . public byte[] ExtractEntryToMemory(Stream archive, string entryName, string? password) { - using var s = this.OpenEntry(archive, entryName, password); - using var memoryStream = new MemoryStream(); - s.CopyTo(memoryStream); - return memoryStream.ToArray(); + ArgumentNullException.ThrowIfNull(archive); + archive.Position = 0; + using var r = new AlZipReader(archive, leaveOpen: true); + var e = r.Entries.FirstOrDefault(x => !x.IsDirectory && string.Equals(x.FileName, entryName, StringComparison.OrdinalIgnoreCase)) + ?? throw new FileNotFoundException($"No entry '{entryName}' in the ALZip archive."); + return r.Extract(e, password); } - /// - /// Performs the create operation. - /// public void Create(Stream output, IReadOnlyList inputs, FormatCreateOptions options) { - // leaveOpen: true — caller owns the stream (e.g. AtomicFileWriter flushes - // it to disk after we return; closing it here would break that contract). - using var w = new AlZipWriter(output, leaveOpen: true); - foreach (var (name, data) in FormatHelpers.FlatFiles(inputs)) - w.AddFile(name, data); + using var w = new AlZipWriter(output, leaveOpen: true, password: options.Password, store: options.MethodName == "store"); + foreach (var input in inputs) { + if (string.IsNullOrEmpty(input.ArchiveName)) continue; + var modified = input.Metadata?.LastWriteTimeUtc?.LocalDateTime + ?? (input.InMemoryContent == null && File.Exists(input.FullPath) ? File.GetLastWriteTime(input.FullPath) : null); + if (input.IsDirectory) w.AddDirectory(input.ArchiveName, modified); + else w.AddFile(input.ArchiveName, input.ReadContent(), modified); + } } } diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipModifier.cs b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipModifier.cs deleted file mode 100644 index 0564db020..000000000 --- a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipModifier.cs +++ /dev/null @@ -1,275 +0,0 @@ -#pragma warning disable CS1591 -using System.Text; -using Compression.Core.Checksums; -using Compression.Core.Deflate; - -namespace FileFormat.AlZip; - -/// -/// Random-access in-place modifier for ALZ archives. Add appends a new entry -/// over the trailing CLZ end-of-archive marker, then rewrites the marker. -/// Remove walks the local-header chain, locates the named entry, and shifts -/// trailing bytes forward to compact (ALZ has no central directory, so -/// compaction is required). -/// -/// -/// ALZ entries are length-prefixed local headers (BLZ\x01) chained together -/// and terminated by a 4-byte CLZ\x02 marker. The format is similar to LHA -/// in that there is no global TOC — header walking is required for both -/// operations. -/// -public static class AlZipModifier { - - private const uint LocalSig = 0x015A4C42; // BLZ\x01 LE - private const uint EndSig = 0x025A4C43; // CLZ\x02 LE - private static readonly byte[] AlzMagic = [0x41, 0x4C, 0x5A, 0x01]; - - /// - /// Appends a file to the archive at the position of the existing CLZ end - /// marker, then writes a new end marker. Walks the entry chain once to - /// locate the marker. - /// - public static void AddFile(Stream alz, string name, byte[] data, DateTime? lastModified = null) { - ArgumentNullException.ThrowIfNull(alz); - ArgumentNullException.ThrowIfNull(name); - ArgumentNullException.ThrowIfNull(data); - - var endOffset = FindEndMarkerOffset(alz); - alz.Position = endOffset; - - WriteEntry(alz, name, data, lastModified ?? DateTime.Now); - WriteUInt32LE(alz, EndSig); - alz.SetLength(alz.Position); - } - - /// - /// Removes the named entry. Returns true if found. Walks the chain to - /// locate the entry, optionally wipes its bytes, then shifts trailing - /// bytes forward to compact. - /// - public static bool RemoveFile(Stream alz, string name, bool wipeData = true) { - ArgumentNullException.ThrowIfNull(alz); - ArgumentNullException.ThrowIfNull(name); - - var locator = LocateEntry(alz, name); - if (!locator.Found) return false; - - if (wipeData) - ZeroRange(alz, locator.HeaderOffset, locator.TotalEntrySize); - - var afterEntry = locator.HeaderOffset + locator.TotalEntrySize; - var bytesToShift = alz.Length - afterEntry; - if (bytesToShift > 0) { - var buf = new byte[64 * 1024]; - var src = afterEntry; - var dst = locator.HeaderOffset; - while (bytesToShift > 0) { - var chunk = (int)Math.Min(buf.Length, bytesToShift); - alz.Position = src; - var read = 0; - while (read < chunk) { - var n = alz.Read(buf, read, chunk - read); - if (n <= 0) break; - read += n; - } - alz.Position = dst; - alz.Write(buf, 0, read); - src += read; - dst += read; - bytesToShift -= read; - } - } - alz.SetLength(alz.Length - locator.TotalEntrySize); - return true; - } - - // ── Header walking ────────────────────────────────────────────────────── - - private readonly record struct EntryLocator(bool Found, long HeaderOffset, long TotalEntrySize); - - private static long FindEndMarkerOffset(Stream alz) { - SkipMagic(alz); - Span sig = stackalloc byte[4]; - while (alz.Position < alz.Length) { - var sigOffset = alz.Position; - if (!ReadFully(alz, sig)) return sigOffset; - var s = ReadUInt32LE(sig); - if (s == EndSig) return sigOffset; - if (s != LocalSig) return sigOffset; // treat as terminator - var entrySize = ReadEntryBodySize(alz); - alz.Position = sigOffset + 4 + entrySize; - } - return alz.Length; - } - - private static EntryLocator LocateEntry(Stream alz, string targetName) { - SkipMagic(alz); - Span sig = stackalloc byte[4]; - while (alz.Position < alz.Length) { - var entryStart = alz.Position; - if (!ReadFully(alz, sig)) break; - var s = ReadUInt32LE(sig); - if (s != LocalSig) break; - - var headerStart = alz.Position; - var (entryBodySize, name) = ReadHeaderForLocate(alz); - var totalEntrySize = 4 + entryBodySize; - - if (string.Equals(name, targetName, StringComparison.OrdinalIgnoreCase)) - return new EntryLocator(true, entryStart, totalEntrySize); - - alz.Position = headerStart + entryBodySize; - } - return new EntryLocator(false, 0, 0); - } - - private static void SkipMagic(Stream alz) { - alz.Position = 0; - Span magic = stackalloc byte[4]; - if (!ReadFully(alz, magic) || !magic.SequenceEqual(AlzMagic)) - throw new InvalidDataException("Not a valid ALZ archive."); - } - - /// - /// Reads an entry body starting at the current stream position (just past - /// the 4-byte LocalSig) and returns the total body size in bytes - /// (header bytes + filename bytes + compressed data). - /// - private static long ReadEntryBodySize(Stream alz) { - var headerStart = alz.Position; - var (bodySize, _) = ReadHeaderForLocate(alz); - alz.Position = headerStart; - return bodySize; - } - - /// - /// Reads the variable-width entry header at the current position and - /// returns (totalBodySize, filename). After this call the stream is - /// positioned at the start of compressed data. - /// - private static (long BodySize, string Name) ReadHeaderForLocate(Stream alz) { - var headerStart = alz.Position; - Span buf2 = stackalloc byte[2]; - if (!ReadFully(alz, buf2)) return (0, ""); - var filenameLen = (ushort)(buf2[0] | (buf2[1] << 8)); - - var attr = (byte)alz.ReadByte(); - Span buf4 = stackalloc byte[4]; - if (!ReadFully(alz, buf4)) return (0, ""); // dosTime - var descriptor = (byte)alz.ReadByte(); - var sizeWidth = (descriptor & 0xF0) switch { - 0x10 => 2, - 0x20 => 4, - 0x40 => 8, - _ => 4, - }; - alz.ReadByte(); // reserved - alz.ReadByte(); // method - if (!ReadFully(alz, buf4)) return (0, ""); // crc32 - - Span sizeBuf = stackalloc byte[8]; - if (!ReadFully(alz, sizeBuf[..sizeWidth])) return (0, ""); - var compressedSize = ReadSize(sizeBuf, sizeWidth); - if (!ReadFully(alz, sizeBuf[..sizeWidth])) return (0, ""); - // uncompressed not needed here - _ = ReadSize(sizeBuf, sizeWidth); - - var nameBuf = new byte[filenameLen]; - if (!ReadFully(alz, nameBuf)) return (0, ""); - var name = Encoding.UTF8.GetString(nameBuf).Replace('\\', '/'); - - var headerBytes = alz.Position - headerStart; - var bodySize = headerBytes + compressedSize; - - // Skip past compressed payload to set stream to next-entry boundary. - alz.Position = headerStart + bodySize; - _ = attr; // suppress unused - return (bodySize, name); - } - - // ── Writing one entry ────────────────────────────────────────────────── - - private static void WriteEntry(Stream alz, string name, byte[] data, DateTime lastModified) { - var nameBytes = Encoding.UTF8.GetBytes(name); - var crc = Crc32.Compute(data); - var deflated = data.Length > 0 ? DeflateCompressor.Compress(data) : []; - byte method; - byte[] payload; - if (deflated.Length > 0 && deflated.Length < data.Length) { - method = 2; - payload = deflated; - } else { - method = 0; - payload = data; - } - - WriteUInt32LE(alz, LocalSig); - WriteUInt16LE(alz, (ushort)nameBytes.Length); - alz.WriteByte(0x20); // attr: archive - WriteUInt32LE(alz, AlZipReader.DateTimeToDosTime(lastModified)); - alz.WriteByte(0x20); // descriptor: 4-byte sizes - alz.WriteByte(0); // reserved - alz.WriteByte(method); - WriteUInt32LE(alz, crc); - WriteUInt32LE(alz, (uint)payload.Length); - WriteUInt32LE(alz, (uint)data.Length); - alz.Write(nameBytes); - alz.Write(payload); - } - - // ── Helpers ───────────────────────────────────────────────────────────── - - private static long ReadSize(ReadOnlySpan buf, int width) => width switch { - 2 => buf[0] | (buf[1] << 8), - 4 => buf[0] | ((long)buf[1] << 8) | ((long)buf[2] << 16) | ((long)buf[3] << 24), - 8 => buf[0] | ((long)buf[1] << 8) | ((long)buf[2] << 16) | ((long)buf[3] << 24) | - ((long)buf[4] << 32) | ((long)buf[5] << 40) | ((long)buf[6] << 48) | ((long)buf[7] << 56), - _ => throw new InvalidDataException($"Invalid ALZ size width {width}"), - }; - - private static uint ReadUInt32LE(ReadOnlySpan b) => - (uint)(b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)); - - private static void WriteUInt16LE(Stream s, ushort v) { - Span b = stackalloc byte[2]; - b[0] = (byte)v; b[1] = (byte)(v >> 8); - s.Write(b); - } - - private static void WriteUInt32LE(Stream s, uint v) { - Span b = stackalloc byte[4]; - b[0] = (byte)v; b[1] = (byte)(v >> 8); b[2] = (byte)(v >> 16); b[3] = (byte)(v >> 24); - s.Write(b); - } - - private static bool ReadFully(Stream s, Span buf) { - var read = 0; - while (read < buf.Length) { - var n = s.Read(buf[read..]); - if (n <= 0) return false; - read += n; - } - return true; - } - - private static bool ReadFully(Stream s, byte[] buf) { - var read = 0; - while (read < buf.Length) { - var n = s.Read(buf, read, buf.Length - read); - if (n <= 0) return false; - read += n; - } - return true; - } - - private static void ZeroRange(Stream s, long offset, long length) { - var buf = new byte[(int)Math.Min(length, 8192)]; - s.Position = offset; - var remaining = length; - while (remaining > 0) { - var chunk = (int)Math.Min(buf.Length, remaining); - s.Write(buf, 0, chunk); - remaining -= chunk; - } - } -} diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipReader.cs b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipReader.cs index 44a572143..8669b8637 100644 --- a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipReader.cs +++ b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipReader.cs @@ -1,228 +1,205 @@ +// Layout per XADMaster's XADALZipParser.m (LGPL-2.1-or-later) and alkegi/unalz (MIT); see +// THIRD-PARTY-NOTICE.unalz.txt. + +using System.Buffers.Binary; +using System.Text; using Compression.Core.Deflate; +using FileFormat.Bzip2; namespace FileFormat.AlZip; /// -/// Reads ALZip (.alz) archive files. +/// Reads single-volume ALZip archives: "ALZ\x01" + 4 bytes, local file headers ("BLZ\x01"), then the +/// central-directory and end records ("CLZ\x01", "CLZ\x02"). Members are stored, deflated or +/// ALZ-bzip2'd, optionally ZipCrypto-encrypted. Names are CP949. /// -/// -/// ALZip is a Korean archive format by ESTsoft. The format uses a simple -/// container with per-file deflate or bzip2 compression. -/// -/// Archive layout: -/// [ALZ\x01] — 4-byte magic -/// [BLZ\x01][header][data]... — repeated file entries -/// [CLZ\x02] — end-of-archive marker -/// public sealed class AlZipReader : IDisposable { + internal static ReadOnlySpan Magic => "ALZ\x01"u8; - /// Archive magic: "ALZ\x01". - internal static ReadOnlySpan Magic => [0x41, 0x4C, 0x5A, 0x01]; - - /// Local file header signature: BLZ\x01 as uint32 LE = 0x015A4C42. - private const uint LocalSig = 0x015A4C42; - - /// End-of-archive signature: CLZ\x02 as uint32 LE = 0x025A4C43. - private const uint EndSig = 0x025A4C43; + internal const uint LocalSig = 0x015A4C42; // "BLZ\x01" + internal const uint CentralSig = 0x015A4C43; // "CLZ\x01" + internal const uint EndSig = 0x025A4C43; // "CLZ\x02" + internal const uint CommentSig = 0x015A4C45; // "ELZ\x01" + internal const uint SplitSig = 0x035A4C43; // "CLZ\x03" private readonly Stream _stream; private readonly bool _leaveOpen; private readonly List _entries = []; - /// - /// Creates a new ALZip reader over the given stream. - /// - public AlZipReader(Stream stream, bool leaveOpen = false) { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - _leaveOpen = leaveOpen; - _ReadArchive(); - } - - /// File entries in the archive. - public IReadOnlyList Entries => _entries; + public IReadOnlyList Entries => this._entries; - /// - /// Extracts the raw (decompressed) data for the given entry. - /// - public byte[] Extract(AlZipEntry entry) { - ArgumentNullException.ThrowIfNull(entry); - - if (entry.IsDirectory) - return []; + /// Whether the archive is one volume of a split set (only whole entries of it are readable). + public bool IsSplit { get; private set; } - _stream.Position = entry.DataOffset; - var compressedData = new byte[entry.CompressedSize]; - _ReadExactly(compressedData); + internal static Encoding Cp949 { + get { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + return Encoding.GetEncoding(949); + } + } - return entry.Method switch { - 0 => compressedData, // Store - 2 => DeflateDecompressor.Decompress(compressedData), // Deflate - _ => throw new InvalidDataException($"Unsupported ALZ compression method: {entry.Method}"), - }; + public AlZipReader(Stream stream, bool leaveOpen = false) { + this._stream = stream ?? throw new ArgumentNullException(nameof(stream)); + this._leaveOpen = leaveOpen; + this.ReadArchive(); } - /// public void Dispose() { - if (!_leaveOpen) - _stream.Dispose(); + if (!this._leaveOpen) + this._stream.Dispose(); } - private void _ReadArchive() { - Span magic = stackalloc byte[4]; - _ReadExactly(magic); + private uint ReadUInt32() { + Span b = stackalloc byte[4]; + this._stream.ReadExactly(b); + return BinaryPrimitives.ReadUInt32LittleEndian(b); + } - if (!magic.SequenceEqual(Magic)) - throw new InvalidDataException("Not a valid ALZ file: invalid magic."); + private void ReadArchive() { + this._stream.Position = 0; + Span head = stackalloc byte[8]; + if (this._stream.ReadAtLeast(head, 8, throwOnEndOfStream: false) < 8 || !head[..4].SequenceEqual(Magic)) + throw new InvalidDataException("Not an ALZip archive (missing ALZ\\x01 header)."); Span sigBuf = stackalloc byte[4]; while (true) { - if (_stream.Read(sigBuf) < 4) + if (this._stream.ReadAtLeast(sigBuf, 4, throwOnEndOfStream: false) < 4) { + this.IsSplit |= this._entries.Count > 0; // ended without its end record break; - - var sig = ReadUInt32LE(sigBuf); - if (sig == EndSig) + } + var sig = BinaryPrimitives.ReadUInt32LittleEndian(sigBuf); + if (sig == LocalSig) { + this.ReadLocalHeader(); + continue; + } + if (sig is CentralSig or EndSig or CommentSig) + break; // central records carry nothing the local headers do not; comments sit at the end + if (sig == SplitSig) { + this.IsSplit = true; break; - - if (sig != LocalSig) - throw new InvalidDataException($"Unexpected ALZ signature: 0x{sig:X8}"); - - _ReadEntry(); + } + throw new InvalidDataException($"Unexpected ALZip signature 0x{sig:X8}."); } } - private void _ReadEntry() { - // Filename length (2 bytes LE) - Span buf2 = stackalloc byte[2]; - _ReadExactly(buf2); - var filenameLen = (ushort)(buf2[0] | (buf2[1] << 8)); - - // File attribute (1 byte) - var attr = (byte)_ReadByte(); - var isDir = (attr & 0x10) != 0; - - // DOS timestamp (4 bytes LE) - Span timeBuf = stackalloc byte[4]; - _ReadExactly(timeBuf); - var dosTime = ReadUInt32LE(timeBuf); - var lastModified = DosTimeToDateTime(dosTime); - - // File descriptor (1 byte) — upper nibble determines size field width - var descriptor = (byte)_ReadByte(); - var sizeWidth = (descriptor & 0xF0) switch { - 0x10 => 2, - 0x20 => 4, - 0x40 => 8, - _ => 4, + private void ReadLocalHeader() { + Span h = stackalloc byte[9]; + this._stream.ReadExactly(h); + var nameLength = BinaryPrimitives.ReadUInt16LittleEndian(h); + var attributes = h[2]; + var dosTime = BinaryPrimitives.ReadUInt32LittleEndian(h[3..]); + var descriptor = h[7]; + var width = (descriptor & 0xF0) switch { + 0x00 => 0, + 0x10 => 1, + 0x20 => 2, + 0x40 => 4, + 0x80 => 8, + var w => throw new InvalidDataException($"Invalid ALZip size-field width 0x{w:X2}."), }; - - // Unknown/reserved (1 byte) - _ReadByte(); - - // Compression method (1 byte) - var method = (byte)_ReadByte(); - - // CRC-32 (4 bytes LE) - Span crcBuf = stackalloc byte[4]; - _ReadExactly(crcBuf); - var crc32 = ReadUInt32LE(crcBuf); - - // Compressed size - var compressedSize = _ReadSizeField(sizeWidth); - - // Uncompressed size - var uncompressedSize = _ReadSizeField(sizeWidth); - - // Filename - var filenameBuf = new byte[filenameLen]; - _ReadExactly(filenameBuf); - var filename = System.Text.Encoding.UTF8.GetString(filenameBuf); - filename = filename.Replace('\\', '/'); - - var dataOffset = _stream.Position; - - _entries.Add(new AlZipEntry { - FileName = filename, - OriginalSize = uncompressedSize, - CompressedSize = compressedSize, - IsDirectory = isDir, + int method = 0; + uint crc = 0; + long packed = 0, size = 0; + if (width > 0) { + Span m = stackalloc byte[6]; + this._stream.ReadExactly(m); + method = m[0]; + crc = BinaryPrimitives.ReadUInt32LittleEndian(m[2..]); + packed = this.ReadSized(width); + size = this.ReadSized(width); + } + if (nameLength == 0 || nameLength > 4096) + throw new InvalidDataException("Invalid ALZip file-name length."); + var name = new byte[nameLength]; + this._stream.ReadExactly(name); + byte[]? encryptionHeader = null; + if ((descriptor & 0x01) != 0) { + encryptionHeader = new byte[AlZipCrypto.HeaderLength]; + this._stream.ReadExactly(encryptionHeader); + } + var dataOffset = this._stream.Position; + if (packed < 0 || dataOffset + packed > this._stream.Length) { + // a member continued in the next volume of a split set + this.IsSplit = true; + this._stream.Position = this._stream.Length; + } else + this._stream.Position = dataOffset + packed; + + this._entries.Add(new AlZipEntry { + FileName = Cp949.GetString(name).Replace('\\', '/'), + Attributes = attributes, + DosTime = dosTime, + LastModified = DosTimeToDateTime(dosTime), + Descriptor = descriptor, Method = method, - Crc32 = crc32, - LastModified = lastModified, - Attributes = attr, + Crc32 = crc, + CompressedSize = packed, + OriginalSize = size, + EncryptionHeader = encryptionHeader, DataOffset = dataOffset, }); + } - // Skip past compressed data - if (compressedSize > 0 && _stream.CanSeek) - _stream.Position += compressedSize; - else if (compressedSize > 0) - _SkipBytes(compressedSize); + private long ReadSized(int width) { + Span b = stackalloc byte[8]; + b.Clear(); + this._stream.ReadExactly(b[..width]); + var v = BinaryPrimitives.ReadUInt64LittleEndian(b); + return v > long.MaxValue ? -1 : (long)v; } - private long _ReadSizeField(int width) { - Span buf = stackalloc byte[8]; - _ReadExactly(buf[..width]); - return width switch { - 2 => buf[0] | (buf[1] << 8), - 4 => ReadUInt32LE(buf), - 8 => (long)ReadUInt64LE(buf), - _ => throw new InvalidDataException($"Invalid ALZ size field width: {width}"), - }; + /// Decompresses (and if needed decrypts) a member, verifying its CRC-32. + public byte[] Extract(AlZipEntry entry, string? password = null) { + ArgumentNullException.ThrowIfNull(entry); + if (entry.IsDirectory || entry.OriginalSize == 0 && entry.CompressedSize == 0) + return []; + if (entry.DataOffset + entry.CompressedSize > this._stream.Length) + throw new NotSupportedException($"ALZip member '{entry.FileName}' continues in another volume; split archives are not supported."); + var packed = new byte[entry.CompressedSize]; + this._stream.Position = entry.DataOffset; + this._stream.ReadExactly(packed); + + if (entry.IsEncrypted) { + if (string.IsNullOrEmpty(password)) + throw new InvalidOperationException($"ALZip member '{entry.FileName}' is encrypted; a password is required."); + var crypto = new AlZipCrypto(Cp949.GetBytes(password)); + var check = (entry.Descriptor & 0x08) != 0 ? (byte)(entry.DosTime >> 8) : (byte)(entry.Crc32 >> 24); + if (!crypto.CheckHeader(entry.EncryptionHeader, check)) + throw new InvalidDataException($"ALZip member '{entry.FileName}': wrong password."); + crypto.Decrypt(packed); + } + + byte[] data; + try { + data = entry.Method switch { + 0 => packed, + 1 => AlzBzip2.Decompress(packed, entry.OriginalSize), + 2 => DeflateDecompressor.Decompress(packed), + 3 => throw new NotSupportedException($"ALZip member '{entry.FileName}' uses obfuscated deflate (method 3), which is not supported."), + _ => throw new NotSupportedException($"ALZip member '{entry.FileName}' uses unknown method {entry.Method}."), + }; + } catch (Exception e) when (e is not (NotSupportedException or InvalidDataException)) { + throw new InvalidDataException($"ALZip member '{entry.FileName}': corrupt data.", e); + } + if (data.LongLength != entry.OriginalSize) + throw new InvalidDataException($"ALZip member '{entry.FileName}': size mismatch."); + if (Compression.Core.Checksums.Crc32.Compute(data) != entry.Crc32) + throw new InvalidDataException($"ALZip member '{entry.FileName}': CRC mismatch."); + return data; } internal static DateTime? DosTimeToDateTime(uint dosTime) { if (dosTime == 0) return null; try { - var time = (ushort)(dosTime & 0xFFFF); - var date = (ushort)(dosTime >> 16); - var year = ((date >> 9) & 0x7F) + 1980; - var month = (date >> 5) & 0x0F; - var day = date & 0x1F; - var hour = (time >> 11) & 0x1F; - var minute = (time >> 5) & 0x3F; - var second = (time & 0x1F) * 2; - if (month < 1 || month > 12 || day < 1 || day > 31) return null; - return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Local); - } catch { + return new DateTime((int)((dosTime >> 25) & 0x7F) + 1980, (int)((dosTime >> 21) & 0x0F), (int)((dosTime >> 16) & 0x1F), + (int)((dosTime >> 11) & 0x1F), (int)((dosTime >> 5) & 0x3F), (int)(dosTime & 0x1F) * 2, DateTimeKind.Local); + } catch (ArgumentOutOfRangeException) { return null; } } internal static uint DateTimeToDosTime(DateTime dt) { - var date = (ushort)(((dt.Year - 1980) << 9) | (dt.Month << 5) | dt.Day); - var time = (ushort)((dt.Hour << 11) | (dt.Minute << 5) | (dt.Second / 2)); - return (uint)((date << 16) | time); - } - - internal static uint ReadUInt32LE(ReadOnlySpan b) => - (uint)(b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)); - - private static ulong ReadUInt64LE(ReadOnlySpan b) => - (ulong)b[0] | ((ulong)b[1] << 8) | ((ulong)b[2] << 16) | ((ulong)b[3] << 24) | - ((ulong)b[4] << 32) | ((ulong)b[5] << 40) | ((ulong)b[6] << 48) | ((ulong)b[7] << 56); - - private int _ReadByte() { - var b = _stream.ReadByte(); - if (b < 0) throw new InvalidDataException("Unexpected end of ALZ stream."); - return b; - } - - private void _ReadExactly(Span buffer) { - var offset = 0; - while (offset < buffer.Length) { - var read = _stream.Read(buffer[offset..]); - if (read == 0) throw new InvalidDataException("Unexpected end of ALZ stream."); - offset += read; - } - } - - private void _SkipBytes(long count) { - var buf = new byte[Math.Min(count, 8192)]; - while (count > 0) { - var toRead = (int)Math.Min(count, buf.Length); - var read = _stream.Read(buf, 0, toRead); - if (read == 0) throw new InvalidDataException("Unexpected end of ALZ stream."); - count -= read; - } + if (dt.Year < 1980) dt = new DateTime(1980, 1, 1); + return (uint)(((dt.Year - 1980) << 25) | (dt.Month << 21) | (dt.Day << 16) | (dt.Hour << 11) | (dt.Minute << 5) | (dt.Second / 2)); } } diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipWriter.cs b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipWriter.cs index dd270db55..3b0b35706 100644 --- a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipWriter.cs +++ b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/AlZipWriter.cs @@ -1,124 +1,109 @@ +using System.Buffers.Binary; using Compression.Core.Checksums; using Compression.Core.Deflate; namespace FileFormat.AlZip; /// -/// Creates ALZip (.alz) archive files. +/// Writes single-volume ALZip archives as ALZip 8 lays them out: the "ALZ\x01" header with version +/// 0x0A, one "BLZ\x01" local header per member (stored or deflated, size fields as narrow as the +/// sizes allow, CP949 names, optional ZipCrypto), then the "CLZ\x01" and "CLZ\x02" end records. /// public sealed class AlZipWriter : IDisposable { - - private const uint LocalSig = 0x015A4C42; - private const uint EndSig = 0x025A4C43; - private readonly Stream _stream; private readonly bool _leaveOpen; + private readonly string? _password; + private readonly bool _store; + private bool _finished; + + public AlZipWriter(Stream stream, bool leaveOpen = false, string? password = null, bool store = false) { + this._stream = stream ?? throw new ArgumentNullException(nameof(stream)); + this._leaveOpen = leaveOpen; + this._password = string.IsNullOrEmpty(password) ? null : password; + this._store = store; + this._stream.Write("ALZ\x01\x0A\x00\x00\x00"u8); + } - /// - /// Creates a new ALZip writer over the given stream. - /// - public AlZipWriter(Stream stream, bool leaveOpen = false) { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - _leaveOpen = leaveOpen; - - // Write archive magic - _stream.Write(AlZipReader.Magic); + private static int Width(long a, long b) { + var max = Math.Max(a, b); + return max <= byte.MaxValue ? 1 : max <= ushort.MaxValue ? 2 : max <= uint.MaxValue ? 4 : 8; } - /// - /// Adds a file to the archive with deflate compression. - /// - public void AddFile(string fileName, byte[] data) { + public void AddFile(string fileName, byte[] data, DateTime? modified = null) { ArgumentNullException.ThrowIfNull(fileName); ArgumentNullException.ThrowIfNull(data); - - var nameBytes = System.Text.Encoding.UTF8.GetBytes(fileName); var crc = Crc32.Compute(data); - var compressed = data.Length > 0 - ? DeflateCompressor.Compress(data) - : []; - - // Use store if deflate doesn't help - var method = (byte)2; // Deflate - var compData = compressed; - if (compressed.Length >= data.Length) { - method = 0; // Store - compData = data; + byte method = 0; + var payload = data; + if (!this._store && data.Length > 0) { + var deflated = DeflateCompressor.Compress(data); + if (deflated.Length < data.Length) { + method = 2; + payload = deflated; + } } - - WriteUInt32LE(LocalSig); - WriteUInt16LE((ushort)nameBytes.Length); - _stream.WriteByte(0x20); // Archive attribute - WriteUInt32LE(AlZipReader.DateTimeToDosTime(DateTime.Now)); - - // File descriptor: 0x20 = 4-byte sizes - _stream.WriteByte(0x20); - - // Reserved - _stream.WriteByte(0); - - // Method - _stream.WriteByte(method); - - // CRC-32 - WriteUInt32LE(crc); - - // Compressed size (4 bytes) - WriteUInt32LE((uint)compData.Length); - - // Uncompressed size (4 bytes) - WriteUInt32LE((uint)data.Length); - - // Filename - _stream.Write(nameBytes); - - // Data - _stream.Write(compData); + var dosTime = AlZipReader.DateTimeToDosTime(modified ?? DateTime.Now); + byte[]? header = null; + if (this._password != null) { + var crypto = new AlZipCrypto(AlZipReader.Cp949.GetBytes(this._password)); + header = crypto.MakeHeader((byte)(crc >> 24)); + payload = (byte[])payload.Clone(); + crypto.Encrypt(payload); + } + this.WriteHeader(fileName, 0x20, dosTime, method, crc, payload.Length, data.Length, header); + this._stream.Write(payload); } - /// - /// Adds a directory entry to the archive. - /// - public void AddDirectory(string dirName) { + public void AddDirectory(string dirName, DateTime? modified = null) { ArgumentNullException.ThrowIfNull(dirName); - - var nameBytes = System.Text.Encoding.UTF8.GetBytes(dirName); - - WriteUInt32LE(LocalSig); - WriteUInt16LE((ushort)nameBytes.Length); - _stream.WriteByte(0x10); // Directory attribute - WriteUInt32LE(AlZipReader.DateTimeToDosTime(DateTime.Now)); - _stream.WriteByte(0x20); // 4-byte sizes - _stream.WriteByte(0); // Reserved - _stream.WriteByte(0); // Store - WriteUInt32LE(0); // CRC - WriteUInt32LE(0); // Compressed size - WriteUInt32LE(0); // Uncompressed size - _stream.Write(nameBytes); + this.WriteHeader(dirName.TrimEnd('/', '\\'), 0x10, AlZipReader.DateTimeToDosTime(modified ?? DateTime.Now), null, 0, 0, 0, null); } - /// - public void Dispose() { - // Write end-of-archive marker - WriteUInt32LE(EndSig); - - if (!_leaveOpen) - _stream.Dispose(); + private void WriteHeader(string name, byte attributes, uint dosTime, byte? method, uint crc, long packed, long size, byte[]? encryptionHeader) { + var nameBytes = AlZipReader.Cp949.GetBytes(name.Replace('/', '\\')); + if (nameBytes.Length is 0 or > 4096) + throw new ArgumentException($"Invalid ALZip member name '{name}'."); + var width = method == null ? 0 : Width(packed, size); + var widthBits = width switch { 0 => 0x00, 1 => 0x10, 2 => 0x20, 4 => 0x40, _ => 0x80 }; + var descriptor = (byte)(widthBits | (encryptionHeader != null ? 0x01 : 0)); + var buf = new byte[4 + 9 + (width > 0 ? 6 + 2 * width : 0)]; + BinaryPrimitives.WriteUInt32LittleEndian(buf, AlZipReader.LocalSig); + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4), (ushort)nameBytes.Length); + buf[6] = attributes; + BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(7), dosTime); + buf[11] = descriptor; + buf[12] = 0; + if (width > 0) { + buf[13] = method!.Value; + buf[14] = 0; + BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(15), crc); + Span v = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(v, (ulong)packed); + v[..width].CopyTo(buf.AsSpan(19)); + BinaryPrimitives.WriteUInt64LittleEndian(v, (ulong)size); + v[..width].CopyTo(buf.AsSpan(19 + width)); + } + this._stream.Write(buf); + this._stream.Write(nameBytes); + if (encryptionHeader != null) + this._stream.Write(encryptionHeader); } - private void WriteUInt16LE(ushort value) { - Span buf = stackalloc byte[2]; - buf[0] = (byte)value; - buf[1] = (byte)(value >> 8); - _stream.Write(buf); + /// Writes the end records; called by . + public void Finish() { + if (this._finished) + return; + this._finished = true; + Span tail = stackalloc byte[16]; + tail.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(tail, AlZipReader.CentralSig); + BinaryPrimitives.WriteUInt32LittleEndian(tail[12..], AlZipReader.EndSig); + this._stream.Write(tail); } - private void WriteUInt32LE(uint value) { - Span buf = stackalloc byte[4]; - buf[0] = (byte)value; - buf[1] = (byte)(value >> 8); - buf[2] = (byte)(value >> 16); - buf[3] = (byte)(value >> 24); - _stream.Write(buf); + public void Dispose() { + this.Finish(); + if (!this._leaveOpen) + this._stream.Dispose(); } } diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/THIRD-PARTY-NOTICE.unalz.txt b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/THIRD-PARTY-NOTICE.unalz.txt new file mode 100644 index 000000000..a27d86173 --- /dev/null +++ b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.AlZip/THIRD-PARTY-NOTICE.unalz.txt @@ -0,0 +1,28 @@ +The ALZip layout (AlZipReader.cs), the ALZ bzip2 variant (Compression.Core/Streams/Bzip2/AlzBzip2.cs) +and the ALZip ZipCrypto handling (AlZipCrypto.cs) follow alkegi/unalz, cross-checked with XADMaster's +XADALZipParser.m (LGPL-2.1-or-later, Copyright (c) 2017-present MacPaw Way Ltd.): + + Upstream: https://github.com/alkegi/unalz (master, fetched 2026-10-23) + Licence: MIT + +MIT License + +Copyright (c) 2026 Sungjoon Moon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Hawkynt.FileFormats.Archives/README.md b/Hawkynt.FileFormats.Archives/README.md index d3f8c6198..057a64bb6 100644 --- a/Hawkynt.FileFormats.Archives/README.md +++ b/Hawkynt.FileFormats.Archives/README.md @@ -103,7 +103,7 @@ Every State, Test, Maintenance, Compress/Decompress and Demux/Mux/Remux cell is | --- | --- | --- | :---: | :---: | --- | --- | --- | | [ACE](https://en.wikipedia.org/wiki/ACE_(compressed_file_format)) | `Ace` | `.ace` | R/W | ✅ | defrag · wipe | | [GitHub](https://github.com/droe/acefile) | | [afio](https://en.wikipedia.org/wiki/Afio) | `Afio` | `.afio` | WORM | ✅ | — | Writes stored members only; the per-file gzip extension is read but not written | [GitHub](https://github.com/kholtman/afio) | -| [ALZip](https://en.wikipedia.org/wiki/ALZip) | `AlZip` | `.alz` | R/W | ✅ | defrag · wipe | | [kippler.com](http://www.kippler.com/win/unalz/) | +| [ALZip](https://en.wikipedia.org/wiki/ALZip) | `AlZip` | `.alz` | R/W | ✅ | — | Reads stored, deflate and ALZ-bzip2 members, ZipCrypto, CP949 names; writes stored/deflate (optionally encrypted) archives unalz extracts; split archives are refused | [GitHub](https://github.com/alkegi/unalz) | | AMPK (Amiga Pack) | `Ampk` | `.ampk` | R/W | ✅ | defrag · wipe | | [Archive Team](http://fileformats.archiveteam.org/wiki/AmiPack) | | [AR](https://en.wikipedia.org/wiki/Ar_(Unix)) | `Ar` | `.a` `.ar` `.deb` | R/W | ✅ | defrag · wipe | | [freebsd.org](https://www.freebsd.org/cgi/man.cgi?query=ar&sektion=5) | | [ARC](https://en.wikipedia.org/wiki/ARC_(file_format)) | `Arc` | `.arc` | R/W | ✅ | defrag · wipe | | [GitHub](https://github.com/hyc/arc) | diff --git a/docs/catalogue/reference-formats.csv b/docs/catalogue/reference-formats.csv index ab231c065..279737ca6 100644 --- a/docs/catalogue/reference-formats.csv +++ b/docs/catalogue/reference-formats.csv @@ -41,7 +41,7 @@ PAK (NoGate ARC extension),pak,archive,,,R,,,,Y,2,,,,,,MISSING,, zoo,zoo,archive,,,R,,R,x-fmt/269,Y,4,https://en.wikipedia.org/wiki/Zoo_(file_format),Zoo,Y,Y,Y,PARTIAL,,LZH method (2) unsupported (ZooReader.cs) LBR (CP/M library),lbr;lqr;lzr,archive,,,R,,R,,Y,3,https://en.wikipedia.org/wiki/LBR_(file_format),Lbr,Y,Y,Y,OK,, LBR (Commodore library),lbr,archive,,,,,,,Y,1,,,,,,MISSING,, -ALZip,alz,archive,,,R,,R,,RW,3,https://en.wikipedia.org/wiki/ALZip,AlZip,Y,Y,Y,PARTIAL,,store and deflate only (AlZipReader.cs) +ALZip,alz,archive,,,R,,R,,RW,3,https://en.wikipedia.org/wiki/ALZip,AlZip,Y,Y,Y,PARTIAL,,"split (multi-volume) archives and obfuscated deflate (method 3) refused; writer emits stored/deflate, not ALZ bzip2 (AlZipReader.cs)" EGG (ALZip),egg,archive,,,R,,,,,1,https://en.wikipedia.org/wiki/ALZip,Egg,Y,Y,,PARTIAL,,"store and deflate only; bzip2/LZMA/AZO, encryption and split archives unsupported (EggReader.cs)" StuffIt (classic + 5),sit;sea,archive,,,R,,R,fmt/1459;fmt/1460;fmt/639,RW,4,https://en.wikipedia.org/wiki/StuffIt,StuffIt,Y,Y,Y,PARTIAL,,store and RLE only; LZW/Huffman/LZAH/Arsenic/DES methods throw (StuffItReader.cs) StuffIt X,sitx,archive,,,R,,R,fmt/399,RW,4,https://en.wikipedia.org/wiki/StuffIt,StuffItX,Y,Y,,PARTIAL,,stored and deflate only; Brimstone/Cyanide/Darkhorse/Iron throw (StuffItXReader.cs) From 461661c7fe4692d8d08dda8028519a287777dede Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:46:56 +0000 Subject: [PATCH 2/3] * refresh the application screenshot --- screenshots/main-window.png | Bin 66235 -> 66264 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/screenshots/main-window.png b/screenshots/main-window.png index a9ceb7f7ce233693ac240e941d4af64380209648..fb3eaed00171f933c63f2264f617b7a07b339c59 100644 GIT binary patch delta 28147 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 delta 28086 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 NIyd0g3pMXP`cH_0$?5 Date: Wed, 28 Oct 2026 06:49:48 +0100 Subject: [PATCH 3/3] # reject ambiguous firmware payloads before writing an image Adding a second named payload to Intel HEX could silently discard a file because the shared image builder kept only the first payload in filesystem enumeration order. Reject multiple non-metadata payloads, including an empty first payload, before any output is written. Tests verify creation refusal and byte-preserving add refusal. Verified all three regressions fail before the fix and pass after it, 93 focused tests, 34280 required tests with 1582 skips and no failures, and three package documentation checks. --- .../FirmwareHex/FirmwareHexPayloadTests.cs | 37 +++++++++++++++++++ .../FirmwareHexWriter.cs | 5 ++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 Compression.Tests/FirmwareHex/FirmwareHexPayloadTests.cs diff --git a/Compression.Tests/FirmwareHex/FirmwareHexPayloadTests.cs b/Compression.Tests/FirmwareHex/FirmwareHexPayloadTests.cs new file mode 100644 index 000000000..bbbc898af --- /dev/null +++ b/Compression.Tests/FirmwareHex/FirmwareHexPayloadTests.cs @@ -0,0 +1,37 @@ +using Compression.Registry; +using FileFormat.FirmwareHex; + +namespace Compression.Tests.FirmwareHex; + +[TestFixture] +public sealed class FirmwareHexPayloadTests { + [TestCase(false)] + [TestCase(true)] + public void Create_RejectsMultiplePayloadsWithoutWritingOutput(bool emptyFirst) { + var descriptor = new IntelHexFormatDescriptor(); + using var output = new MemoryStream(); + var inputs = new[] { + ArchiveInputInfo.InMemory("first.bin", emptyFirst ? [] : [1, 2, 3]), + ArchiveInputInfo.InMemory("second.bin", [9, 8, 7]), + }; + + Assert.Throws(() => + ((IArchiveCreatable)descriptor).Create(output, inputs, new FormatCreateOptions())); + Assert.That(output.Length, Is.Zero); + } + + [Test] + public void Add_RejectsAnotherPayloadWithoutChangingTheArchive() { + var descriptor = new IntelHexFormatDescriptor(); + using var archive = new MemoryStream(); + ((IArchiveCreatable)descriptor).Create(archive, + [ArchiveInputInfo.InMemory("firmware.bin", [1, 2, 3])], new FormatCreateOptions()); + var original = archive.ToArray(); + archive.Position = 0; + + Assert.Throws(() => ((IArchiveModifiable)descriptor).Add(archive, + [ArchiveInputInfo.InMemory("ADD.BIN", [9, 8, 7])])); + Assert.That(archive.ToArray(), Is.EqualTo(original)); + } + +} diff --git a/Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.FirmwareHex/FirmwareHexWriter.cs b/Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.FirmwareHex/FirmwareHexWriter.cs index 6c7d4b452..1c9e17266 100644 --- a/Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.FirmwareHex/FirmwareHexWriter.cs +++ b/Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.FirmwareHex/FirmwareHexWriter.cs @@ -33,6 +33,7 @@ public static class FirmwareHexWriter { /// one the reader rendered. An input list with no payload describes an image /// with no data, which both formats can write. /// + /// More than one non-metadata payload is supplied. public static FirmwareImage ImageFrom(IReadOnlyList inputs, string sourceFormat) { ArgumentNullException.ThrowIfNull(inputs); @@ -53,7 +54,9 @@ public static FirmwareImage ImageFrom(IReadOnlyList inputs, st } // Any other single input is the payload, whatever it is called: a caller // converting a .bin into a .hex should not have to rename it first. - payload ??= input.ReadContent(); + if (payload is not null) + throw new InvalidDataException($"{sourceFormat} represents one logical firmware payload; multiple payload inputs are ambiguous."); + payload = input.ReadContent(); } List<(uint Address, byte[] Data)> segments;