Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
450 changes: 450 additions & 0 deletions Compression.Core/Dictionary/Azo/AzoDecoder.cs

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions Compression.Core/Dictionary/Azo/THIRD-PARTY-NOTICE.EggDotNet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
The AZO decoder (Compression.Core/Dictionary/Azo/AzoDecoder.cs) and the EGG encryption code
(Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Egg/EggCrypto.cs: ZIP 2.0, AES and LEA variants and
the LEA block cipher) are converted to C# from EggDotNet; the EGG test archives under
Compression.Tests/Egg/ReferenceVectors come from its test corpus:

Upstream: https://github.com/akolman/EggDotNet (master, fetched 2026-10-23)
Licence: MIT

MIT License

Copyright (c) 2026 Andrew Kolman (akolman)

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.
2 changes: 1 addition & 1 deletion Compression.Core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ Use the concrete version you intend to consume; this document does not predict a

<!-- API:BEGIN generated by Hawkynt/RepositoryTemplate/package-readme — edit the XML docs in source, not here -->

Every public and protected member of all 734 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/CompressionWorkbench/blob/main/Compression.Core/REFERENCE.md).
Every public and protected member of all 735 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/CompressionWorkbench/blob/main/Compression.Core/REFERENCE.md).

<!-- API:END -->

Expand Down
12 changes: 12 additions & 0 deletions Compression.Core/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,18 @@ Decodes ARJ method 4 ("fastest"), the LZ77 variant ARJ uses when asked for speed
| --- | --- | --- |
| `Decode` | `static byte[] Decode(ReadOnlySpan<byte> compressed, int originalSize)` | Decodes `originalSize` bytes of method-4 data. |

### Namespace `Compression.Core.Dictionary.Azo`

[`AzoDecoder`](#azodecoder)

#### `AzoDecoder`

ESTsoft's AZO compression (EGG method 3): independent blocks, each an LZ77 parse coded with an adaptive binary arithmetic coder — literal and length models predicted from context, a distance history, a small dictionary of recent matches.

| Member | Signature | Summary |
| --- | --- | --- |
| `Decompress` | `static byte[] Decompress(ReadOnlySpan<byte> data, long expectedSize)` | Decodes an AZO stream into exactly `expectedSize` bytes. |

### Namespace `Compression.Core.Dictionary.Balz`

[`BalzBuildingBlock`](#balzbuildingblock)
Expand Down
4 changes: 4 additions & 0 deletions Compression.Tests/Compression.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@
<EmbeddedResource Include="Zfs\ReferenceVectors\*.gz;Zfs\ReferenceVectors\*.manifest">
<LogicalName>ZfsVectors.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<!-- EGG archives ALZip wrote, from EggDotNet's test corpus; see Egg/ReferenceVectors/README.md. -->
<EmbeddedResource Include="Egg\ReferenceVectors\*.egg">
<LogicalName>EggVectors.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<!-- G.722 streams FFmpeg 8.1.2 encoded and decoded; see Codecs/G722/ReferenceVectors/README.md. -->
<EmbeddedResource Include="Codecs\G722\ReferenceVectors\*.pcm;Codecs\G722\ReferenceVectors\*.g722">
<LogicalName>G722Vectors.%(Filename)%(Extension)</LogicalName>
Expand Down
154 changes: 154 additions & 0 deletions Compression.Tests/Egg/EggReferenceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using System.Reflection;
using System.Text;
using FileFormat.Egg;

namespace Compression.Tests.Egg;

/// <summary>
/// EGG archives ALZip wrote, from EggDotNet's test corpus (MIT). Each data block carries the CRC-32
/// ALZip computed, which the reader verifies, so a member that decodes is byte-exact; the content
/// assertions are the ones EggDotNet's own tests make. See ReferenceVectors/README.md.
/// </summary>
[TestFixture]
public class EggReferenceTests {
private const string Password = "password12345!";

private static byte[] Vector(string name) {
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"EggVectors.{name}")
?? throw new FileNotFoundException($"Embedded EGG vector '{name}' is missing from the test assembly.");
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}

private static EggReader Open(string name) => new(new MemoryStream(Vector(name)));

[TestCase("azoshort_txt.egg", "AZO")]
[TestCase("azonumbers.egg", "AZO")]
[TestCase("lzma_simple.egg", "LZMA")]
[Category("ThirdPartyVector")]
public void Given_CompressedMember_When_Extracted_Then_AlZipCrcHolds(string vector, string method) {
using var r = Open(vector);
var files = r.Entries.Where(e => !e.IsDirectory).ToList();
Assert.That(files.Select(e => e.MethodName), Has.Some.EqualTo(method));
foreach (var e in files)
Assert.That(r.Extract(e), Has.Length.EqualTo(e.UncompressedSize), e.Name);
}

[Test, Category("ThirdPartyVector")]
public void Given_LzmaArchive_When_Extracted_Then_SizesMatchEggDotNet() {
using var r = Open("lzma_simple.egg");
var e = r.Entries.Single(x => x.Name == "lorem_ipsum.txt");
Assert.That(e.CompressedSize, Is.EqualTo(5723));
Assert.That(r.Extract(e), Has.Length.EqualTo(34446));
}

[TestCase("lorem_long_zipEnc.egg")]
[TestCase("lorem_long_aes128.egg")]
[TestCase("lorem_long_aes256.egg")]
[TestCase("lorem_long_store_lea128.egg")]
[TestCase("lorem_long_store_lea256.egg")]
[Category("ThirdPartyVector")]
public void Given_EncryptedArchive_When_Decrypted_Then_LoremText(string vector) {
using var r = Open(vector);
var e = r.Entries.Single(x => !x.IsDirectory);
Assert.That(e.IsEncrypted, Is.True);
var text = Encoding.UTF8.GetString(r.Extract(e, Password));
Assert.That(text, Has.Length.EqualTo(15_238));
Assert.That(text, Does.StartWith("Lorem ipsum dolor sit amet"));
Assert.That(text, Does.EndWith("sed faucibus orci ligula eu nisi."));
}

[TestCase("lorem_long_zipEnc.egg")]
[TestCase("lorem_long_aes256.egg")]
[TestCase("lorem_long_store_lea128.egg")]
[Category("EdgeCase")]
public void Given_EncryptedArchive_When_WrongOrNoPassword_Then_Refused(string vector) {
using var r = Open(vector);
var e = r.Entries.Single(x => !x.IsDirectory);
Assert.Throws<InvalidOperationException>(() => r.Extract(e));
Assert.Throws<InvalidDataException>(() => r.Extract(e, "badpassword"));
}

[Test, Category("ThirdPartyVector")]
public void Given_SolidArchive_When_Extracted_Then_FirstEqualsLast() {
using var r = Open("solid.egg");
Assert.That(r.IsSolid, Is.True);
var files = r.Entries.Where(e => !e.IsDirectory).ToList();
Assert.That(r.Extract(files[^1]), Is.EqualTo(r.Extract(files[0])));
}

[TestCase("small_kor.egg", "작은.txt")]
[TestCase("small_shift.egg", "小さい.txt")]
[Category("ThirdPartyVector")]
public void Given_CodePageName_When_Read_Then_DecodedAndHelloWorld(string vector, string name) {
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
using var r = Open(vector);
var e = r.Entries.First();
Assert.That(e.Name, Is.EqualTo(name));
Assert.That(Encoding.UTF8.GetString(r.Extract(e)), Is.EqualTo("Hello, world!"));
}

[Test, Category("ThirdPartyVector")]
public void Given_NestedDirectories_When_Listed_Then_DeepFilePresent() {
using var r = Open("directories.egg");
Assert.That(r.Entries.Select(e => e.Name), Has.Some.EqualTo("dir1/dir2/dir3/test.txt"));
}

[Test, Category("ThirdPartyVector")]
public void Given_LargeMember_When_Listed_Then_SizesMatchEggDotNet() {
using var r = Open("zeros.egg");
var e = r.Entries.Single();
Assert.That(e.CompressedSize, Is.EqualTo(338));
Assert.That(e.UncompressedSize, Is.EqualTo(197_540_460));
}

[TestCase("posix_small.egg")]
[TestCase("globalcomment.egg")]
[Category("ThirdPartyVector")]
public void Given_Archive_When_EveryMemberExtracted_Then_CrcHolds(string vector) {
using var r = Open(vector);
foreach (var e in r.Entries.Where(e => !e.IsDirectory))
Assert.That(r.Extract(e), Has.Length.EqualTo(e.UncompressedSize), e.Name);
}

[Test, Category("EdgeCase")]
public void Given_CorruptAzoBlock_When_Extracted_Then_InvalidData() {
var bytes = Vector("azoshort_txt.egg");
using var probe = new EggReader(new MemoryStream(bytes));
var e = probe.Entries.Single();
bytes[bytes.Length - 20] ^= 0x5A;
using var r = new EggReader(new MemoryStream(bytes));
Assert.Throws<InvalidDataException>(() => r.Extract(r.Entries.Single()));
}

private static byte[] OursText() =>
Encoding.ASCII.GetBytes(string.Concat(Enumerable.Range(0, 300).Select(i => $"EGG line {i % 17}\r\n")));

private static byte[] OursRaw() => [.. Enumerable.Range(0, 3000).Select(i => (byte)(i * 7))];

// ours.egg is what our writer produced for these three inputs; EggDotNet read it back
// (ReferenceVectors/eggdotnet-listing.txt: both members Deflate, CRC valid, SHA-256 as below).
[Test, Category("ThirdPartyVector")]
public void Given_SameInputs_When_Written_Then_BytesEqualTheEggDotNetAcceptedFixture() {
using var ms = new MemoryStream();
new EggFormatDescriptor().Create(ms, [
Compression.Registry.ArchiveInputInfo.InMemory("docs/text.txt", OursText()),
Compression.Registry.ArchiveInputInfo.InMemory("empty.txt", []),
Compression.Registry.ArchiveInputInfo.InMemory("raw.bin", OursRaw()),
], new Compression.Registry.FormatCreateOptions());
Assert.That(ms.ToArray(), Is.EqualTo(Vector("ours.egg")));
}

[Test, Category("ThirdPartyVector")]
public void Given_OurFixture_When_Read_Then_MatchesEggDotNetListing() {
using var r = Open("ours.egg");
Assert.That(r.Entries.Select(e => e.Name), Is.EqualTo(new[] { "docs/text.txt", "empty.txt", "raw.bin" }));
var text = r.Extract(r.Entries.Single(e => e.Name == "docs/text.txt"));
Assert.That(Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(text)),
Is.EqualTo("53ff0c178b3267b2dffbdd59979fba67927f8cc7cc60a4eb209b7c06e92096c9"));
Assert.That(text, Is.EqualTo(OursText()));
Assert.That(r.Extract(r.Entries.Single(e => e.Name == "raw.bin")), Is.EqualTo(OursRaw()));
Assert.That(r.Extract(r.Entries.Single(e => e.Name == "empty.txt")), Is.Empty);
}
}
6 changes: 3 additions & 3 deletions Compression.Tests/Egg/EggTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,14 @@ public void Writer_LongUtf8Filename_UsesExtendedExtraFieldLength() {
[Test, Category("HappyPath")]
public void UnsupportedMethod_ListsButThrowsOnExtract() {
var payload = "ignored"u8.ToArray();
// Algorithm 4 = LZMA — listed, but extraction must throw rather than fake bytes.
var archive = BuildStoreOrRaw("movie.lzma", payload, payload, algorithm: 4);
// Algorithm 9 is not an EGG method — listed, but extraction must throw rather than fake bytes.
var archive = BuildStoreOrRaw("movie.lzma", payload, payload, algorithm: 9);

var d = new EggFormatDescriptor();
using var ms = new MemoryStream(archive);
var entries = d.List(ms, null);
Assert.That(entries, Has.Count.EqualTo(1));
Assert.That(entries[0].Method, Is.EqualTo("LZMA"));
Assert.That(entries[0].Method, Is.EqualTo("Method9"));

ms.Position = 0;
Assert.Throws<NotSupportedException>(() =>
Expand Down
2 changes: 2 additions & 0 deletions Compression.Tests/Egg/ReferenceVectors/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Fixture bytes are the test: never let a checkout rewrite line endings.
*.egg -text
24 changes: 24 additions & 0 deletions Compression.Tests/Egg/ReferenceVectors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# EGG reference vectors

All files come unmodified from EggDotNet's test corpus (`src/EggDotNet.Tests/test_files/`,
akolman/EggDotNet master, MIT, fetched 2026-10-23); they were written by ALZip.

| File | Exercises |
| --- | --- |
| `azoshort_txt.egg`, `azonumbers.egg` | AZO compression |
| `lzma_simple.egg` | LZMA (34 446 bytes in 5 723) |
| `solid.egg` | solid mode (first and last member identical) |
| `lorem_long_zipEnc.egg`, `lorem_long_aes128.egg`, `lorem_long_aes256.egg` | ZIP 2.0, AES-128 and AES-256 encryption (password `password12345!`) |
| `lorem_long_store_lea128.egg`, `lorem_long_store_lea256.egg` | LEA-128 and LEA-256 encryption (same password) |
| `small_kor.egg`, `small_shift.egg` | CP949 and Shift-JIS file names |
| `directories.egg` | nested directories |
| `zeros.egg` | a 197 540 460-byte member (listed, not extracted) |
| `posix_small.egg`, `globalcomment.egg` | POSIX file information, archive comment |

Each data block stores the CRC-32 ALZip computed, and the reader checks it, so every member these
tests extract is byte-exact; the content assertions repeat EggDotNet's own.

`ours.egg` is our writer's output (`docs/text.txt`, an empty `empty.txt`, `raw.bin`; Deflate).
EggDotNet (same revision, a net10.0 console that opens each member and checks its CRC) read it
back as recorded in `eggdotnet-listing.txt`. The empty member has no block, as ALZip writes it, so
EggDotNet lists it without opening it.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 3 additions & 0 deletions Compression.Tests/Egg/ReferenceVectors/eggdotnet-listing.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ours.egg docs/text.txt 3720 53ff0c178b3267b2dffbdd59979fba67927f8cc7cc60a4eb209b7c06e92096c9 Deflate crc=True
ours.egg empty.txt 0 (no block)
ours.egg raw.bin 3000 7291514d2492fd7ff49e10ba7df95d19d31d199b89d74bcb62cebdee1bc1a498 Deflate crc=True
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added Compression.Tests/Egg/ReferenceVectors/ours.egg
Binary file not shown.
Binary file not shown.
Binary file added Compression.Tests/Egg/ReferenceVectors/small_kor.egg
Binary file not shown.
Binary file not shown.
Binary file added Compression.Tests/Egg/ReferenceVectors/solid.egg
Binary file not shown.
Binary file added Compression.Tests/Egg/ReferenceVectors/zeros.egg
Binary file not shown.
37 changes: 37 additions & 0 deletions Compression.Tests/FirmwareHex/FirmwareHexPayloadTests.cs
Original file line number Diff line number Diff line change
@@ -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<InvalidDataException>(() =>
((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<InvalidDataException>(() => ((IArchiveModifiable)descriptor).Add(archive,
[ArchiveInputInfo.InMemory("ADD.BIN", [9, 8, 7])]));
Assert.That(archive.ToArray(), Is.EqualTo(original));
}

}
Loading
Loading