Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 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
b171283
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
6afb631
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
d69e707
* refresh the archive browser screenshot
github-actions[bot] 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
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
d7162dd
* isolate the CVF clean-room rewrite from pending features
Hawkynt Oct 27, 2026
d31ee62
* refresh the application screenshot
github-actions[bot] Oct 27, 2026
19c95c3
* retain main artwork for the CVF clean-room rewrite
Hawkynt Oct 27, 2026
bd88473
* reconcile CVF and G.722 clean-room provenance
Hawkynt Oct 28, 2026
67c60ff
# reject ambiguous firmware payloads before writing an image
Hawkynt Oct 28, 2026
764e7e6
* validate firmware payload refusal with current disk support
Hawkynt Oct 28, 2026
376b531
* integrate shared firmware payload validation
Hawkynt Oct 28, 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
39 changes: 39 additions & 0 deletions Compression.Core/Dictionary/StackerSd4/StackerSd4BuildingBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Buffers.Binary;
using Compression.Core.FileSystems.Cvf;
using Compression.Registry;

namespace Compression.Core.Dictionary.StackerSd4;

/// <summary>
/// Stacker 4's SD-4 codec as a raw primitive: LZ77 over a 2720-byte window, Huffman-coded with a
/// run-length coded code table (<c>docs/CVF-CODECS-ON-DISK.md</c>). The building-block wrapper stores
/// the uncompressed size (32-bit little-endian) before one SD-4 stream covering the whole input.
/// </summary>
public sealed class StackerSd4BuildingBlock : IBuildingBlock {
/// <inheritdoc/>
public string Id => "BB_StackerSd4";
/// <inheritdoc/>
public string DisplayName => "Stacker SD-4";
/// <inheritdoc/>
public string Description => "Stac Stacker 4 cluster codec: LZ77 (2720-byte window) with canonical Huffman codes and a run-length coded table";
/// <inheritdoc/>
public AlgorithmFamily Family => AlgorithmFamily.Dictionary;

/// <inheritdoc/>
public byte[] Compress(ReadOnlySpan<byte> data) {
var stream = Sd4Codec.Encode(data, level: 3);
var result = new byte[4 + stream.Length];
BinaryPrimitives.WriteInt32LittleEndian(result, data.Length);
stream.CopyTo(result, 4);
return result;
}

/// <inheritdoc/>
public byte[] Decompress(ReadOnlySpan<byte> data) {
if (data.Length < 4) throw new InvalidDataException("Stacker SD-4: missing size header.");
var size = BinaryPrimitives.ReadInt32LittleEndian(data);
if (size < 0) throw new InvalidDataException("Stacker SD-4: negative size.");
var stream = data[4..].ToArray();
return Sd4Codec.Decode(stream, stream.Length, size);
}
}
81 changes: 81 additions & 0 deletions Compression.Core/FileSystems/Cvf/ClusterMatchFinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace Compression.Core.FileSystems.Cvf;

/// <summary>
/// Hash-chain LZ77 match finder over one cluster, shared by the DS/JM and SD-4 encoders. Every match
/// it reports lies wholly inside the cluster: no CVF codec carries a dictionary from one cluster to
/// the next.
/// </summary>
internal sealed class ClusterMatchFinder {
private const int HashBits = 13;
private const int MinLength = 3;

private readonly byte[] _data;
private readonly int[] _head = new int[1 << HashBits];
private readonly int[] _previous;
private readonly int _maxDistance;
private readonly int _maxLength;
private readonly int _maxChain;
private readonly int _niceLength;
private int _inserted;

/// <param name="data">The cluster.</param>
/// <param name="maxDistance">The farthest distance the format can express.</param>
/// <param name="maxLength">The longest match the format can express.</param>
/// <param name="level">Search effort, 1 (fastest) and up: how many earlier positions with the same
/// hash are tried, and how long a match must be to end the search early.</param>
public ClusterMatchFinder(byte[] data, int maxDistance, int maxLength, int level) {
this._data = data;
this._previous = new int[data.Length];
this._maxDistance = maxDistance;
this._maxLength = maxLength;
(this._maxChain, this._niceLength) = level switch {
<= 1 => (16, 64),
2 => (64, 258),
3 => (256, 1024),
_ => (1024, int.MaxValue),
};
Array.Fill(this._head, -1);
}

/// <summary>
/// The longest match for the bytes at <paramref name="position"/>, preferring the nearest on ties;
/// <c>(0, 0)</c> when there is none of at least three bytes. Positions before
/// <paramref name="position"/> are indexed first, so calls must come in ascending order.
/// </summary>
/// <param name="position">Where the match starts.</param>
/// <param name="end">Exclusive bound the match may not run past; the cluster end when omitted.</param>
public (int Length, int Distance) Find(int position, int end = int.MaxValue) {
this.IndexUpTo(position);
var data = this._data;
var limit = Math.Min(this._maxLength, Math.Min(data.Length, end) - position);
if (limit < MinLength) return (0, 0);

int bestLength = 0, bestDistance = 0;
var candidate = this._head[Hash(data, position)];
for (var chain = this._maxChain; candidate >= 0 && chain > 0; --chain, candidate = this._previous[candidate]) {
var distance = position - candidate;
if (distance > this._maxDistance) break;
if (data[candidate + bestLength] != data[position + bestLength]) continue;

var length = 0;
while (length < limit && data[candidate + length] == data[position + length]) ++length;
if (length <= bestLength) continue;

(bestLength, bestDistance) = (length, distance);
if (length == limit || length >= this._niceLength) break;
}
return bestLength >= MinLength ? (bestLength, bestDistance) : (0, 0);
}

private void IndexUpTo(int position) {
var last = Math.Min(position, this._data.Length - MinLength + 1);
for (; this._inserted < last; ++this._inserted) {
var h = Hash(this._data, this._inserted);
this._previous[this._inserted] = this._head[h];
this._head[h] = this._inserted;
}
}

private static int Hash(byte[] data, int position)
=> (int)(((uint)data[position] << 16 | (uint)data[position + 1] << 8 | data[position + 2]) * 2654435761u >> (32 - HashBits));
}
Loading
Loading