Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
42456f2
* screenshots refresh only on branches that change the UI
Hawkynt Oct 26, 2026
11bdfd2
+ ZIP method 95 (XZ): read and write
Hawkynt Oct 26, 2026
c01351b
* regenerate references
Hawkynt Oct 26, 2026
0edd488
# XAR: bzip2, lzma and xz members decoded and checked against the TOC
Hawkynt Oct 26, 2026
393d095
* regenerate references
Hawkynt Oct 26, 2026
3bcfeb8
# LHA: -lh2- and -lh3- decoded with their own decoders, writing them …
Hawkynt Oct 26, 2026
6493e5c
* regenerate references
Hawkynt Oct 26, 2026
e199a5f
# LHA: header levels 1/3, lh1, lzs, lz5, pm1, pm2 and lhx read as the…
Hawkynt Oct 26, 2026
fc95094
# PackIt and DiskDoubler: real on-disk formats, decode or refuse
Hawkynt Oct 26, 2026
2350d2a
* regenerate references
Hawkynt Oct 26, 2026
fd7e239
* FreeArc: real FreeArc 0.67 archives, read and write
Hawkynt Oct 26, 2026
dd3a154
* regenerate references
Hawkynt Oct 26, 2026
685d627
* NSIS: real installer format, solid and non-solid, read-only
Hawkynt Oct 26, 2026
7200477
* regenerate references
Hawkynt Oct 26, 2026
ffc73b3
+ PartitionedDisk write support, 4Kn GPT, BSD/Amiga/Sun/SGI labels, r…
Hawkynt Oct 26, 2026
2cb06be
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
ad124fa
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
fc8ce06
* regenerate references
Hawkynt Oct 26, 2026
1cfcb4f
# partition add counts in the disk's own sector size on 4Kn disks
Hawkynt Oct 26, 2026
dfd763c
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
c6d48b5
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
1ab5fe6
* CVF codecs: clean-room DS/JM/SQ reimplementation, SD-4 encoder with…
Hawkynt Oct 26, 2026
148a622
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
089bc8b
* regenerate references
Hawkynt Oct 26, 2026
481be6f
* Inno Setup: real file extraction, setup data 1.3 to 7.1, read-only
Hawkynt Oct 26, 2026
b171283
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
6afb631
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
96c9580
* regenerate references
Hawkynt Oct 26, 2026
61af4a1
* ZPAQ: real journaling/streaming format, zpaq 7.15 reads what we write
Hawkynt Oct 26, 2026
d69e707
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
3b92dff
* regenerate references
Hawkynt Oct 26, 2026
6c6dd7b
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
f7ceb83
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
ddd6109
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
987e25d
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
dd7c332
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
a5b5eea
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
c869b83
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
aa8748a
* record LHA decoder provenance and merge CI prerequisites
Hawkynt Oct 26, 2026
fd0f4d9
* record lhasa provenance and merge CI prerequisites
Hawkynt Oct 26, 2026
1c3d3f2
* record DiskDoubler provenance and merge CI prerequisites
Hawkynt Oct 26, 2026
05f41f3
* merge validated provenance prerequisites for FreeArc
Hawkynt Oct 26, 2026
32f1345
* merge validated provenance prerequisites for NSIS
Hawkynt Oct 26, 2026
6c11eee
* merge validated provenance prerequisites for partitioned disks
Hawkynt Oct 26, 2026
8f36f0b
* merge validated provenance prerequisites for 4Kn partition add
Hawkynt Oct 26, 2026
7b8eaa1
* reconcile CVF provenance with the clean-room rewrite
Hawkynt Oct 26, 2026
eb4c589
* record Inno Setup provenance and merge CI prerequisites
Hawkynt Oct 26, 2026
41d6a9c
* record ZPAQ provenance and merge validated train prerequisites
Hawkynt Oct 26, 2026
35a2a9d
* isolate ZPAQ interoperability from pending features
Hawkynt Oct 27, 2026
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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ jobs:
# reiser4progs owns Reiser4: fsck.reiser4 and debugfs.reiser4 judge every volume the
# Reiser4 writer creates or edits, in the gate below.
sudo apt-get install -y reiser4progs
# zpaq (7.15) owns ZPAQ: it must list and extract what the ZPAQ writer produces, and the
# reader must decode every method zpaq writes, in the gate below.
sudo apt-get install -y zpaq
sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true
sudo modprobe gfs2 || true

Expand Down Expand Up @@ -235,6 +238,19 @@ jobs:
--filter "FullyQualifiedName~Compression.Tests.Reiser4.Reiser4ProfileExternalTests|FullyQualifiedName~Compression.Tests.Reiser4.Reiser4MutationExternalTests|FullyQualifiedName~Compression.Tests.Reiser4.Reiser4NativeTreeExternalTests"
--logger "trx;LogFileName=reiser4-oracle-tests.trx"

# Hard gate, both directions: zpaq must list and extract byte for byte what the ZPAQ writer
# produces (every method), the reader must decode what zpaq writes (every method, versions,
# deduplication, streaming), and our compressed data blocks must equal zpaq 7.15's.
- name: ZPAQ oracle
if: matrix.os == 'ubuntu-latest'
env:
CWB_REQUIRE_ZPAQ: "1"
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "FullyQualifiedName~Compression.Tests.Zpaq.ZpaqExternalOracleTests"
--logger "trx;LogFileName=zpaq-oracle-tests.trx"

# bcachefs-tools' own checker judges every volume this package writes or
# edits: the allocation information, the usage totals and the namespace are
# cross-examined against each other, and bucket generations against the
Expand Down
2 changes: 1 addition & 1 deletion Compression.Tests/BuildingBlocks/ZpaqBuildingBlockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void RepeatedSentence_RoundTripsAndCompresses() {

[Test, Category("HappyPath"), Category("RoundTrip")]
public void HighlyRepetitive_RoundTripsAndCompresses() {
var data = new byte[256];
var data = new byte[4096]; // large enough that the ~120-byte ZPAQ block header does not dominate
Array.Fill(data, (byte)0x61);
var compressed = Bb.Compress(data);
var round = Bb.Decompress(compressed);
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 @@ -91,6 +91,10 @@
<EmbeddedResource Include="FreeArc\ReferenceVectors\*.arc">
<LogicalName>FreeArcVectors.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<!-- Archives the reference zpaq 7.15 wrote, with a manifest of their contents; see Zpaq/ReferenceVectors/README.md. -->
<EmbeddedResource Include="Zpaq\ReferenceVectors\*.zpaq;Zpaq\ReferenceVectors\manifest.tsv">
<LogicalName>ZpaqVectors.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<!-- CVF cluster payloads the dmsdos compressors wrote; see Cvf/ReferenceVectors/README.md. -->
<EmbeddedResource Include="Cvf\ReferenceVectors\*.bin">
<LogicalName>CvfVectors.%(Filename)%(Extension)</LogicalName>
Expand Down
19 changes: 19 additions & 0 deletions Compression.Tests/Zpaq/ReferenceVectors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# zpaq 7.15 reference archives

Every `.zpaq` file here was written by the reference archiver **zpaq v7.15** (Matt Mahoney,
public domain), the Windows x64 build `zpaq64.exe` shipped in
<http://mattmahoney.net/dc/zpaq715.zip> ("compiled Aug 17 2016"), on 2026-10-19.

`mkfixtures.py` (documentation, not a build input) generated the input files deterministically
and ran `zpaq64.exe a <archive> <dir> [-method M]` in the work directory, so the stored names are
relative (`set/text.txt`). Only the archives and `manifest.tsv` are checked in: the manifest gives,
per archive and version, every name with its size and SHA-256 (size `-1` marks a directory).

| File | Layout | How it was made | What it exercises |
|------|--------|-----------------|-------------------|
| `set.m0.zpaq` .. `set.m5.zpaq` | journaling | `a set.mN.zpaq set -method N` | every predefined method: 0 (dedup only), 1/2 (LZ77, packed codes, hash and suffix-array match finders), 3 (BWT or LZ77+CM), 4/5 (CM with ICM/ISSE chains, match model, mixers, SSE); E8E9 on `x86.bin`; `dup.txt` deduplicated against `text.txt`; an empty file; a subdirectory |
| `versions.zpaq` | journaling | three `a versions.zpaq v` runs: add `notes.txt`+`a.bin`; change `a.bin`, add `b.txt`; delete `notes.txt` | three transactions, an update, a deletion record, fragments shared across versions |
| `stream.zpaq` | streaming | `a stream.zpaq st -method s4.3ci1` | streaming layout (one block per file, filename and date in the segment header), BWT + order-0 ICM + order-1 ISSE |

`zpaq64.exe l versions.zpaq -all` reports `3 versions, 6 files, 4 fragments`; `zpaq64.exe l
stream.zpaq` reports two files of unknown size (the streaming layout records none).
52 changes: 52 additions & 0 deletions Compression.Tests/Zpaq/ReferenceVectors/manifest.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# archive version name size sha256 (size -1 = directory)
set.m0.zpaq 1 set/dup.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m0.zpaq 1 set/empty.dat 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
set.m0.zpaq 1 set/random.bin 4096 3a370c921e1d689daae1f95c1bc8fd70c3a2294d9a602603866c88f1678133ed
set.m0.zpaq 1 set/sub/ -1 -
set.m0.zpaq 1 set/sub/table.bin 16384 c30bfd6c26ff4d6994184a48cfe912d634d84741a506a379a756a88d03ce17c8
set.m0.zpaq 1 set/text.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m0.zpaq 1 set/x86.bin 16384 f80aea167fd0f16c0e8c59c394b8316b5626494ce468e4ef358e9b32cd3c05d4
set.m1.zpaq 1 set/dup.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m1.zpaq 1 set/empty.dat 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
set.m1.zpaq 1 set/random.bin 4096 3a370c921e1d689daae1f95c1bc8fd70c3a2294d9a602603866c88f1678133ed
set.m1.zpaq 1 set/sub/ -1 -
set.m1.zpaq 1 set/sub/table.bin 16384 c30bfd6c26ff4d6994184a48cfe912d634d84741a506a379a756a88d03ce17c8
set.m1.zpaq 1 set/text.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m1.zpaq 1 set/x86.bin 16384 f80aea167fd0f16c0e8c59c394b8316b5626494ce468e4ef358e9b32cd3c05d4
set.m2.zpaq 1 set/dup.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m2.zpaq 1 set/empty.dat 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
set.m2.zpaq 1 set/random.bin 4096 3a370c921e1d689daae1f95c1bc8fd70c3a2294d9a602603866c88f1678133ed
set.m2.zpaq 1 set/sub/ -1 -
set.m2.zpaq 1 set/sub/table.bin 16384 c30bfd6c26ff4d6994184a48cfe912d634d84741a506a379a756a88d03ce17c8
set.m2.zpaq 1 set/text.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m2.zpaq 1 set/x86.bin 16384 f80aea167fd0f16c0e8c59c394b8316b5626494ce468e4ef358e9b32cd3c05d4
set.m3.zpaq 1 set/dup.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m3.zpaq 1 set/empty.dat 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
set.m3.zpaq 1 set/random.bin 4096 3a370c921e1d689daae1f95c1bc8fd70c3a2294d9a602603866c88f1678133ed
set.m3.zpaq 1 set/sub/ -1 -
set.m3.zpaq 1 set/sub/table.bin 16384 c30bfd6c26ff4d6994184a48cfe912d634d84741a506a379a756a88d03ce17c8
set.m3.zpaq 1 set/text.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m3.zpaq 1 set/x86.bin 16384 f80aea167fd0f16c0e8c59c394b8316b5626494ce468e4ef358e9b32cd3c05d4
set.m4.zpaq 1 set/dup.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m4.zpaq 1 set/empty.dat 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
set.m4.zpaq 1 set/random.bin 4096 3a370c921e1d689daae1f95c1bc8fd70c3a2294d9a602603866c88f1678133ed
set.m4.zpaq 1 set/sub/ -1 -
set.m4.zpaq 1 set/sub/table.bin 16384 c30bfd6c26ff4d6994184a48cfe912d634d84741a506a379a756a88d03ce17c8
set.m4.zpaq 1 set/text.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m4.zpaq 1 set/x86.bin 16384 f80aea167fd0f16c0e8c59c394b8316b5626494ce468e4ef358e9b32cd3c05d4
set.m5.zpaq 1 set/dup.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m5.zpaq 1 set/empty.dat 0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
set.m5.zpaq 1 set/random.bin 4096 3a370c921e1d689daae1f95c1bc8fd70c3a2294d9a602603866c88f1678133ed
set.m5.zpaq 1 set/sub/ -1 -
set.m5.zpaq 1 set/sub/table.bin 16384 c30bfd6c26ff4d6994184a48cfe912d634d84741a506a379a756a88d03ce17c8
set.m5.zpaq 1 set/text.txt 20000 6d7dcfff654e4f0b73c9c6748e20101a654d91340e808028d9cce43314e176b6
set.m5.zpaq 1 set/x86.bin 16384 f80aea167fd0f16c0e8c59c394b8316b5626494ce468e4ef358e9b32cd3c05d4
versions.zpaq 1 v/a.bin 3000 e17a27da77a519cad73adb40b725139379c057af5023170700519facc6e378e7
versions.zpaq 1 v/notes.txt 6000 76c3dcfd3f6bd471dcbd9d27638a1663c5d1d0342dc1d3733d30e475922f7eff
versions.zpaq 2 v/a.bin 3000 eaf76ffcb88a0e9add0d967f5c7c7f1918ac1c087fbf90c0c73030328d11684b
versions.zpaq 2 v/b.txt 5000 09baec6ca8b2af5f64ce5cd913b5172d83008bc0d418dafe44b9f0f14264a121
versions.zpaq 2 v/notes.txt 6000 76c3dcfd3f6bd471dcbd9d27638a1663c5d1d0342dc1d3733d30e475922f7eff
versions.zpaq 3 v/a.bin 3000 eaf76ffcb88a0e9add0d967f5c7c7f1918ac1c087fbf90c0c73030328d11684b
versions.zpaq 3 v/b.txt 5000 09baec6ca8b2af5f64ce5cd913b5172d83008bc0d418dafe44b9f0f14264a121
stream.zpaq 1 st/one.txt 9000 fe0aff6997ba887be6c6d568be054f5600765c45a0ff3762a7c39f2d34a9f56a
stream.zpaq 1 st/two.bin 7000 46f2d7bf41d57c5c516fad8b79c3630e264e86d8a43f22dae0dac6db793e87ba
113 changes: 113 additions & 0 deletions Compression.Tests/Zpaq/ReferenceVectors/mkfixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Builds the zpaq 7.15 reference archives for Compression.Tests/Zpaq/ReferenceVectors.

Usage: python -I mkfixtures.py <zpaq.exe> <workdir> <outdir>
The inputs are generated deterministically here; only the archives and a manifest
(name, size, SHA-256 per version) are checked in.
"""
import hashlib, os, shutil, subprocess, sys

zpaq, work, out = sys.argv[1], sys.argv[2], sys.argv[3]

class Lcg:
def __init__(self, seed): self.s = seed & 0xffffffff
def next(self):
self.s = (self.s * 1103515245 + 12345) & 0xffffffff
return self.s >> 16

WORDS = ("the of and to in is that for it as with was on be by at this which from or an are "
"archive journal fragment block version compress model context mixer predictor stream "
"zpaq level method index header data file directory size date hash checksum").split()

def text(n, seed):
r = Lcg(seed); b = bytearray()
while len(b) < n:
w = WORDS[r.next() % len(WORDS)]
b += w.encode()
k = r.next() % 16
b += b'.\n' if k == 0 else (b', ' if k == 1 else b' ')
return bytes(b[:n])

def rand(n, seed):
r = Lcg(seed); return bytes(r.next() & 255 for _ in range(n))

def x86(n, seed):
r = Lcg(seed); b = bytearray()
while len(b) < n:
k = r.next() % 8
if k == 0: b += bytes([0xE8, r.next() & 255, r.next() & 15, 0, 0])
elif k == 1: b += bytes([0x8B, 0x45 + (r.next() & 7) * 8, r.next() & 0x7C])
elif k == 2: b += bytes([0xE9, r.next() & 255, 0xFF, 0xFF, 0xFF])
else: b += bytes([0x89, 0x44, 0x24, (r.next() & 15) * 4])
return bytes(b[:n])

def table(n):
return bytes(((i * 7) ^ (i >> 4)) & 255 if i % 4 else (i >> 2) & 255 for i in range(n))

def write(root, name, data):
p = os.path.join(root, name)
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, 'wb') as f: f.write(data)

def run(*args, cwd):
r = subprocess.run([zpaq, *args], cwd=cwd, capture_output=True, text=True)
if r.returncode != 0:
raise SystemExit(f"zpaq {' '.join(args)} failed:\n{r.stdout}\n{r.stderr}")

def manifest_of(root):
rows = []
for dp, dns, fns in os.walk(root):
for d in dns:
rel = os.path.relpath(os.path.join(dp, d), os.path.dirname(root)).replace('\\', '/') + '/'
rows.append((rel, -1, '-'))
for f in fns:
p = os.path.join(dp, f)
rel = os.path.relpath(p, os.path.dirname(root)).replace('\\', '/')
data = open(p, 'rb').read()
rows.append((rel, len(data), hashlib.sha256(data).hexdigest()))
return sorted(rows)

shutil.rmtree(work, ignore_errors=True); os.makedirs(work); os.makedirs(out, exist_ok=True)
lines = ["# archive\tversion\tname\tsize\tsha256 (size -1 = directory)"]

# One file set, every predefined method.
s = os.path.join(work, 'set')
t = text(20000, 1)
write(s, 'text.txt', t)
write(s, 'dup.txt', t) # deduplicated against text.txt
write(s, 'x86.bin', x86(16384, 2))
write(s, 'random.bin', rand(4096, 3))
write(s, 'empty.dat', b'')
write(s, 'sub/table.bin', table(16384))
setrows = manifest_of(s)
for m in range(6):
name = f'set.m{m}.zpaq'
run('a', name, 'set', '-method', str(m), cwd=work)
shutil.copy(os.path.join(work, name), out)
lines += [f"{name}\t1\t{n}\t{z}\t{h}" for n, z, h in setrows]

# Three versions: add, modify + add, delete.
v = os.path.join(work, 'v')
write(v, 'notes.txt', text(6000, 4))
write(v, 'a.bin', rand(3000, 5))
lines += [f"versions.zpaq\t1\t{n}\t{z}\t{h}" for n, z, h in manifest_of(v)]
run('a', 'versions.zpaq', 'v', cwd=work)
write(v, 'a.bin', rand(3000, 6))
write(v, 'b.txt', text(5000, 7))
lines += [f"versions.zpaq\t2\t{n}\t{z}\t{h}" for n, z, h in manifest_of(v)]
run('a', 'versions.zpaq', 'v', cwd=work)
os.remove(os.path.join(v, 'notes.txt'))
lines += [f"versions.zpaq\t3\t{n}\t{z}\t{h}" for n, z, h in manifest_of(v)]
run('a', 'versions.zpaq', 'v', cwd=work)
shutil.copy(os.path.join(work, 'versions.zpaq'), out)

# Streaming layout (method s...): BWT + order-0/1 CM, one block per file.
st = os.path.join(work, 'st')
write(st, 'one.txt', text(9000, 8))
write(st, 'two.bin', x86(7000, 9))
run('a', 'stream.zpaq', 'st', '-method', 's4.3ci1', cwd=work)
shutil.copy(os.path.join(work, 'stream.zpaq'), out)
lines += [f"stream.zpaq\t1\t{n}\t{z}\t{h}" for n, z, h in manifest_of(st) if z >= 0]

with open(os.path.join(out, 'manifest.tsv'), 'w', newline='\n') as f:
f.write('\n'.join(lines) + '\n')
print('ok')
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 not shown.
128 changes: 128 additions & 0 deletions Compression.Tests/Zpaq/ZpaqEngineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Text;
using Compression.Tests.Csc;
using FileFormat.Zpaq;

namespace Compression.Tests.Zpaq;

/// <summary>
/// The libzpaq conversion below the archive layer: suffix arrays, the ZPAQL compiler and
/// interpreter, method expansion, and single-block streams.
/// </summary>
[TestFixture]
public class ZpaqEngineTests {

// ── suffix array ─────────────────────────────────────────────────────────

private static int[] NaiveSuffixArray(byte[] s) =>
Enumerable.Range(0, s.Length).OrderBy(i => i, Comparer<int>.Create((a, b) => s.AsSpan(a).SequenceCompareTo(s.AsSpan(b)))).ToArray();

[Test, Category("HappyPath")]
public void SuffixArray_MatchesNaiveSort([Values(0, 1, 2, 3, 17, 100, 1000)] int length, [Values(2, 4, 256)] int alphabet) {
var rng = new Random(length * 31 + alphabet);
var s = new byte[length];
for (var i = 0; i < s.Length; ++i)
s[i] = (byte)rng.Next(alphabet);
Assert.That(ZpaqSuffixArray.Build(s), Is.EqualTo(NaiveSuffixArray(s)));
}

[Test, Category("BoundaryCase")]
public void SuffixArray_PeriodicAndConstantInputs() {
foreach (var s in new[] { Enumerable.Repeat((byte)7, 500).ToArray(), Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat("abcab", 97))), Encoding.ASCII.GetBytes("mississippi") })
Assert.That(ZpaqSuffixArray.Build(s), Is.EqualTo(NaiveSuffixArray(s)));
}

// ── squash / stretch (specification section 7, checked by libzpaq's own self test) ──

[Test, Category("HappyPath")]
public void SquashAndStretch_MatchLibzpaqChecksums() {
uint stsum = 0, sqsum = 0;
for (var i = 32767; i >= 0; --i)
stsum = stsum * 3 + (uint)ZpaqPredictor.Stretch(i);
for (var i = 4095; i >= 0; --i)
sqsum = sqsum * 3 + (uint)ZpaqPredictor.Squash(i - 2048);
Assert.Multiple(() => {
Assert.That(stsum, Is.EqualTo(3887533746u));
Assert.That(sqsum, Is.EqualTo(2278286169u));
});
}

// ── compiler ─────────────────────────────────────────────────────────────

[Test, Category("HappyPath")]
public void Compiler_ProducesLibzpaqMinCfgBytecode() {
// min.cfg from the libzpaq documentation; the expected bytes are libzpaq's built-in "Model 1".
const string minCfg = "comp 1 2 0 0 2 0 icm 16 1 isse 19 0 hcomp *b=a a=0 d=0 hash b-- hash *d=a d++ b-- hash b-- hash *d=a halt post 0 end";
var hz = new ZpaqlMachine();
var pz = new ZpaqlMachine();
_ = new ZpaqCompiler(minCfg, null, hz, pz);
var ms = new ZpaqBuffer();
hz.Write(ms, false);
byte[] model1 = [26, 0, 1, 2, 0, 0, 2, 3, 16, 8, 19, 0, 0, 96, 4, 28, 59, 10, 59, 112, 25, 10, 59, 10, 59, 112, 56, 0];
Assert.That(ms.ToArray(), Is.EqualTo(model1));
}

[Test, Category("Exception")]
public void Compiler_RejectsBadConfigs([Values("comp 0 0 0 0 1 0 bogus hcomp halt end", "comp 0 0 0 0 0 hcomp endif halt end", "comp 0 0 0 0 0 hcomp halt")] string config) =>
Assert.That(() => new ZpaqCompiler(config, null, new(), new()), Throws.InstanceOf<InvalidDataException>());

[Test, Category("HappyPath")]
public void MakeConfig_ExpandsArguments() {
var args = new int[9];
var config = ZpaqMethods.MakeConfig("x6.3ci1", args);
Assert.Multiple(() => {
Assert.That(args[..2], Is.EqualTo(new[] { 6, 3 }));
Assert.That(config, Does.StartWith("comp 9 16 $1+20 $1+20 2\n0 icm"));
Assert.That(config, Does.Contain("pcomp bwtrle c ;"));
Assert.That(ZpaqMethods.MakeConfig("0", args), Is.EqualTo("comp 0 0 0 0 0 hcomp end\n"));
});
}

// ── single-block streams ─────────────────────────────────────────────────

private static byte[] RoundTrip(byte[] data, string method) {
var output = new ZpaqBuffer();
ZpaqMethods.CompressBlock(new ZpaqBuffer(data), output, method, "name", null, storeSha1: true);
return ZpaqStream.Decompress(output.Span);
}

[Test, Category("RoundTrip")]
public void Block_EveryTransformAndModel_RoundTrips([Values("0", "x0.0c0.0.255", "x0.1.4.0.3.20", "x0.2.4.0.3.20c0.0.511", "x0.3ci1", "x0.4", "x0.5.4.0.3.20", "x0.6.4.0.3.20", "x0.7ci1",
"x0.1.4.0.5.21", "x0.0c256.0.255.1511.255i1aw1m16s8.32.255", "x0.0c0.1010.255t", "x0.0ci1,1,1,1,2aw2mm16tst")] string method) {
byte[] data = [.. CscSamples.Text(9000, 31), .. CscSamples.X86(7000, 32), .. CscSamples.RandomBytes(2000, 33)];
Assert.That(RoundTrip(data, method), Is.EqualTo(data));
}

[Test, Category("RoundTrip")]
public void Block_LevelsWithHints_RoundTrip([Values("1", "2", "3", "4", "5")] string level, [Values(",0,0", ",200,1", ",120,2", ",250,3")] string hints) {
var data = CscSamples.Mixed();
Assert.That(RoundTrip(data, level + "0" + hints), Is.EqualTo(data));
}

[Test, Category("BoundaryCase")]
public void Block_EmptyAndOneByte_RoundTrip([Values("0", "x0.1.4.0.3.20", "x0.2.4.0.3.20", "x0.3ci1", "x0.0ci1")] string method) {
Assert.Multiple(() => {
Assert.That(RoundTrip([], method), Is.Empty);
Assert.That(RoundTrip([42], method), Is.EqualTo(new byte[] { 42 }));
});
}

[Test, Category("Exception")]
public void Block_FlippedChecksum_IsDetected() {
var output = new ZpaqBuffer();
ZpaqMethods.CompressBlock(new ZpaqBuffer(CscSamples.Text(5000, 34)), output, "x0.0ci1", null, null, storeSha1: true);
var bytes = output.ToArray();
bytes[^3] ^= 1; // inside the SHA-1 trailer, before the end-of-block byte
Assert.That(() => ZpaqStream.Decompress(bytes), Throws.InstanceOf<InvalidDataException>());
}

[Test, Category("HappyPath")]
public void E8E9_IsUndoneByThePostprocessor() {
var data = CscSamples.X86(30_000, 35);
var copy = (byte[])data.Clone();
ZpaqMethods.E8E9(copy);
Assert.Multiple(() => {
Assert.That(copy, Is.Not.EqualTo(data), "the transform changes call targets");
Assert.That(RoundTrip(data, "x0.4"), Is.EqualTo(data));
});
}
}
Loading
Loading