diff --git a/Compression.Tests/Compression.Tests.csproj b/Compression.Tests/Compression.Tests.csproj index 76b0d031b..62c023982 100644 --- a/Compression.Tests/Compression.Tests.csproj +++ b/Compression.Tests/Compression.Tests.csproj @@ -165,6 +165,10 @@ ZfsVectors.%(Filename)%(Extension) + + + PeaVectors.%(Filename)%(Extension) + SevenZipVectors.%(Filename)%(Extension) diff --git a/Compression.Tests/Hashing/WhirlpoolTests.cs b/Compression.Tests/Hashing/WhirlpoolTests.cs new file mode 100644 index 000000000..41543105a --- /dev/null +++ b/Compression.Tests/Hashing/WhirlpoolTests.cs @@ -0,0 +1,23 @@ +using Hawkynt.Algorithms.Hashing; + +namespace Compression.Tests.Hashing; + +/// +/// Whirlpool against the ISO/NESSIE vectors (cross-checked with OpenSSL's legacy provider), at the +/// tail lengths around its padding boundary: 0x80 plus a 256-bit length needs a second final block +/// once 32 or more message bytes are left over. +/// +[TestFixture] +public class WhirlpoolTests { + [TestCase("", "19fa61d75522a4669b44e39c1d2e1726c530232130d407f89afee0964997f7a73e83be698b288febcf88e3e03c4f0757ea8964e59b63d93708b138cc42a66eb3")] + [TestCase("abc", "4e2448a4c6f486bb16b6562c73b4020bf3043e3a731bce721ae1b303d97e6d4c7181eebdb6c57e277d0e34957114cbd6c797fc9d95d8b582d225292076d4eef5")] + [TestCase("The quick brown fox jumps over the lazy dog", "b97de512e91e3828b40d2b0fdce9ceb3c4a71f9bea8d88e75c4fa854df36725fd2b52eb6544edcacd6f8beddfea403cb55ae31f03ad62a5ef54e42ee82c3fb35")] + [Category("HappyPath")] + public void GivenReferenceInput_WhenHashed_ThenMatchesTheReferenceDigest(string input, string expected) + => Assert.That(Convert.ToHexStringLower(Whirlpool.Compute(System.Text.Encoding.ASCII.GetBytes(input))), Is.EqualTo(expected)); + + [Test, Category("Boundary")] + public void GivenOneBlockPlus41Bytes_WhenHashed_ThenTheTailNeedsTwoPaddingBlocks() + => Assert.That(Convert.ToHexStringLower(Whirlpool.Compute(Enumerable.Repeat((byte)'a', 105).ToArray())), + Is.EqualTo("d352ca65c7d77ae0c27a8e1ce3b8e887f05bef753e28c7b7c7ff112f1ff4c1bcaf83f749ad1e6e397d210ea14da44c4c5615a77831ad80a132e3a67cef2b1878")); +} diff --git a/Compression.Tests/Pea/PeaTests.cs b/Compression.Tests/Pea/PeaTests.cs new file mode 100644 index 000000000..371429f07 --- /dev/null +++ b/Compression.Tests/Pea/PeaTests.cs @@ -0,0 +1,139 @@ +using System.Reflection; +using System.Security.Cryptography; +using Compression.Registry; +using FileFormat.Pea; + +namespace Compression.Tests.Pea; + +/// +/// PEA: archives PeaZip 11.4 wrote with every compression level and most check algorithms +/// (Pea/ReferenceVectors), our writer, and PeaZip's own pea (headless) as the oracle for what we +/// write. +/// +[TestFixture] +public class PeaTests { + + private static readonly byte[] Fox = System.Text.Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat("The quick brown fox jumps over the lazy dog.\n", 300))); + private static readonly byte[] Hello = "hello pea\n"u8.ToArray(); + private static readonly byte[] Nested = "nested\n"u8.ToArray(); + private static readonly DateTime SourceTime = new(2001, 2, 3, 4, 5, 6, DateTimeKind.Utc); + + private string _tmpDir = null!; + + [SetUp] + public void Setup() { + this._tmpDir = Path.Combine(Path.GetTempPath(), $"cwb_pea_{Guid.NewGuid():N}"); + Directory.CreateDirectory(this._tmpDir); + } + + [TearDown] + public void Teardown() { + try { Directory.Delete(this._tmpDir, true); } catch { /* best effort */ } + } + + [TestCase("store.pea")] // PCOMPRESS0, CRC32 volume/object, no stream check + [TestCase("deflate3.pea")] // PCOMPRESS1, Adler-32 volume, CRC-64 objects, SHA-256 stream + [TestCase("deflate6.pea")] // PCOMPRESS2, CRC32, CRC32, SHA-256 + [TestCase("deflate9.pea")] // PCOMPRESS3, SHA-1 volume, MD5 objects, SHA-512 stream + [TestCase("sha3.pea")] // SHA3-256 volume, SHA3-512 objects, BLAKE2b stream + [TestCase("misc.pea")] // RIPEMD-160 volume, Whirlpool objects, BLAKE2s stream + [Category("ThemVsUs")] + public void GivenArchiveWrittenByPeaZip_WhenRead_ThenEveryObjectAndCheckVerifies(string vector) { + var entries = PeaArchive.Read(new MemoryStream(LoadVector(vector))); + + Assert.That(entries.Select(e => (e.Path, e.IsDirectory)), Is.EquivalentTo(new[] { + ("hello.txt", false), ("fox.txt", false), ("empty.txt", false), ("sub", true), ("sub/n.txt", false) })); + Assert.That(entries.Single(e => e.Path == "hello.txt").Data, Is.EqualTo(Hello)); + Assert.That(entries.Single(e => e.Path == "fox.txt").Data, Is.EqualTo(Fox)); + Assert.That(entries.Single(e => e.Path == "empty.txt").Data, Is.Empty); + Assert.That(entries.Single(e => e.Path == "sub/n.txt").Data, Is.EqualTo(Nested)); + Assert.That(entries.Single(e => e.Path == "fox.txt").LastModified, Is.EqualTo(SourceTime)); + } + + [Test, Category("ThemVsUs")] + public void GivenMultiBlockArchiveWrittenByPeaZip_WhenRead_ThenAll3MiBDecode() { + var big = PeaArchive.Read(new MemoryStream(LoadVector("big.pea"))).Single(); + var expected = Enumerable.Range(0, 3_000_000).Select(i => (byte)((i * 131 + (i >> 7)) % 256)).ToArray(); + Assert.That(big.Path, Is.EqualTo("big.bin")); + Assert.That(big.Data, Is.EqualTo(expected)); + } + + [Test, Category("Exception")] + public void GivenFlippedDataByte_WhenRead_ThenAChecksumRefusesIt() { + var bytes = LoadVector("store.pea"); + var at = bytes.AsSpan().IndexOf("The quick"u8); + bytes[at + 5] ^= 0x01; + Assert.That(() => PeaArchive.Read(new MemoryStream(bytes)), Throws.InstanceOf()); + } + + [Test, Category("Exception")] + public void GivenEncryptedStreamHeader_WhenRead_ThenRefusedAsUnsupported() { + var bytes = LoadVector("deflate6.pea"); + bytes[18] = 0x31; // EAX + Assert.That(() => PeaArchive.Read(new MemoryStream(bytes)), Throws.InstanceOf()); + } + + [Test, Category("Exception")] + public void GivenNotPea_WhenRead_ThenRefused() + => Assert.That(() => PeaArchive.Read(new MemoryStream(new byte[64])), Throws.InstanceOf()); + + // ── our writer ────────────────────────────────────────────────────── + + private static List Inputs() => [ + new("hello.txt", Hello, SourceTime), + new("dir/fox.txt", Fox, SourceTime), + new("dir/deep/empty.bin", [], SourceTime), + new("big.bin", Enumerable.Range(0, 2_500_000).Select(i => (byte)(i * 7 + (i >> 9))).ToArray(), SourceTime), + ]; + + [TestCase(0, PeaArchive.Check.Crc32, PeaArchive.Check.None, PeaArchive.Check.Crc32)] + [TestCase(1, PeaArchive.Check.Crc64, PeaArchive.Check.Sha256, PeaArchive.Check.Adler32)] + [TestCase(2, PeaArchive.Check.Crc32, PeaArchive.Check.Sha256, PeaArchive.Check.Crc32)] + [TestCase(3, PeaArchive.Check.Md5, PeaArchive.Check.Sha512, PeaArchive.Check.Sha1)] + [Category("RoundTrip")] + public void GivenFiles_WhenWrittenAndRead_ThenEveryByteRoundTrips(int compression, PeaArchive.Check obj, PeaArchive.Check stream, PeaArchive.Check volume) { + using var ms = new MemoryStream(); + PeaArchive.Write(ms, Inputs(), compression, obj, stream, volume); + var entries = PeaArchive.Read(new MemoryStream(ms.ToArray())); + foreach (var input in Inputs()) + Assert.That(entries.Single(e => e.Path == input.Path).Data, Is.EqualTo(input.Data), input.Path); + Assert.That(entries.Where(e => e.IsDirectory).Select(e => e.Path), Is.EquivalentTo(new[] { "dir", "dir/deep" })); + } + + [Test, Category("Exception")] + public void GivenPassword_WhenCreated_ThenRefused() + => Assert.That(() => new PeaFormatDescriptor().Create(new MemoryStream(), [ArchiveInputInfo.InMemory("a", [1])], new FormatCreateOptions { Password = "x" }), + Throws.InstanceOf()); + + [Test, Category("ExternalInterop")] + public void GivenOurArchive_WhenPeaZipExtractsIt_ThenEveryFileMatches() { + var pea = RequirePea(); + var path = Path.Combine(this._tmpDir, "ours.pea"); + using (var f = File.Create(path)) + PeaArchive.Write(f, Inputs()); + var outDir = Path.Combine(this._tmpDir, "out"); + Directory.CreateDirectory(outDir); + + var r = FsInteropToolbox.RunWsl($"timeout 120 xvfb-run -a '{pea}' UNPEA '{FsInteropToolbox.WinToWsl(path)}' '{FsInteropToolbox.WinToWsl(outDir)}/' RESETDATE RESETATTR EXTRACT2DIR HIDDEN '' ''"); + Assert.That(r.ExitCode, Is.EqualTo(0), $"pea UNPEA failed:\n{r.StdOut}\n{r.StdErr}"); + var root = Path.Combine(outDir, "ours.pea"); + foreach (var input in Inputs()) + Assert.That(File.ReadAllBytes(Path.Combine(root, input.Path.Replace('/', Path.DirectorySeparatorChar))), Is.EqualTo(input.Data), input.Path); + } + + private static string RequirePea() { + if (OperatingSystem.IsWindows() && !FsInteropToolbox.WslAvailable) Assert.Ignore("WSL is not available."); + var pea = Environment.GetEnvironmentVariable("PEAZIP_PEA") ?? "/opt/cwb_peazip/peazip_portable-11.4.0.LINUX.GTK2.x86_64/pea"; + var probe = FsInteropToolbox.RunWsl($"test -x '{pea}' && command -v xvfb-run"); + if (probe.ExitCode != 0) Assert.Ignore($"PeaZip's pea is not at {pea} (set PEAZIP_PEA), or xvfb-run is missing."); + return pea; + } + + private static byte[] LoadVector(string name) { + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"PeaVectors.{name}") + ?? throw new InvalidOperationException($"missing embedded vector {name}"); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } +} diff --git a/Compression.Tests/Pea/ReferenceVectors/README.md b/Compression.Tests/Pea/ReferenceVectors/README.md new file mode 100644 index 000000000..a21a457ea --- /dev/null +++ b/Compression.Tests/Pea/ReferenceVectors/README.md @@ -0,0 +1,25 @@ +# PEA reference archives + +Written by PeaZip 11.4.0's `pea` (Linux GTK2 portable build, run headless under `xvfb-run` on +Ubuntu 24.04 / WSL), never by CompressionWorkbench. + +Source tree `/tmp/peasrc`: `hello.txt` (`hello pea\n`), `fox.txt` +(`The quick brown fox jumps over the lazy dog.\n` × 300), `empty.txt` (0 bytes), `sub/n.txt` +(`nested\n`), all with mtime 2001-02-03 04:05:06 UTC; `big.bin` is the 3,000,000 bytes +`(i * 131 + (i >> 7)) mod 256`. + +Every archive was made with +`pea PEA 0 HIDDEN FROMCL /tmp/peasrc/hello.txt /tmp/peasrc/fox.txt /tmp/peasrc/empty.txt /tmp/peasrc/sub`: + +| File | Compression | Volume | Object | Stream | +| --- | --- | --- | --- | --- | +| `store.pea` | PCOMPRESS0 | CRC32 | CRC32 | NOALGO | +| `deflate3.pea` | PCOMPRESS1 | ADLER32 | CRC64 | SHA256 | +| `deflate6.pea` | PCOMPRESS2 | CRC32 | CRC32 | SHA256 | +| `deflate9.pea` | PCOMPRESS3 | SHA1 | MD5 | SHA512 | +| `sha3.pea` | PCOMPRESS2 | SHA3_256 | SHA3_512 | BLAKE2B | +| `misc.pea` | PCOMPRESS2 | RIPEMD160 | WHIRLPOOL | BLAKE2S | +| `big.pea` | PCOMPRESS2 | CRC32 | CRC32 | SHA256 (input: `big.bin` only; three 1 MiB blocks) | + +`pea UNPEA / RESETDATE RESETATTR EXTRACT2DIR HIDDEN "" ""` extracts them under +`//`, relative to the objects' common directory. diff --git a/Compression.Tests/Pea/ReferenceVectors/big.pea b/Compression.Tests/Pea/ReferenceVectors/big.pea new file mode 100644 index 000000000..3cae09bed Binary files /dev/null and b/Compression.Tests/Pea/ReferenceVectors/big.pea differ diff --git a/Compression.Tests/Pea/ReferenceVectors/deflate3.pea b/Compression.Tests/Pea/ReferenceVectors/deflate3.pea new file mode 100644 index 000000000..a5cbf4219 Binary files /dev/null and b/Compression.Tests/Pea/ReferenceVectors/deflate3.pea differ diff --git a/Compression.Tests/Pea/ReferenceVectors/deflate6.pea b/Compression.Tests/Pea/ReferenceVectors/deflate6.pea new file mode 100644 index 000000000..c88bed707 Binary files /dev/null and b/Compression.Tests/Pea/ReferenceVectors/deflate6.pea differ diff --git a/Compression.Tests/Pea/ReferenceVectors/deflate9.pea b/Compression.Tests/Pea/ReferenceVectors/deflate9.pea new file mode 100644 index 000000000..ef9319ff4 Binary files /dev/null and b/Compression.Tests/Pea/ReferenceVectors/deflate9.pea differ diff --git a/Compression.Tests/Pea/ReferenceVectors/misc.pea b/Compression.Tests/Pea/ReferenceVectors/misc.pea new file mode 100644 index 000000000..45113c5be Binary files /dev/null and b/Compression.Tests/Pea/ReferenceVectors/misc.pea differ diff --git a/Compression.Tests/Pea/ReferenceVectors/sha3.pea b/Compression.Tests/Pea/ReferenceVectors/sha3.pea new file mode 100644 index 000000000..5ce4ad02b Binary files /dev/null and b/Compression.Tests/Pea/ReferenceVectors/sha3.pea differ diff --git a/Compression.Tests/Pea/ReferenceVectors/store.pea b/Compression.Tests/Pea/ReferenceVectors/store.pea new file mode 100644 index 000000000..7501b9dc4 Binary files /dev/null and b/Compression.Tests/Pea/ReferenceVectors/store.pea differ diff --git a/Compression.Tests/PngCrushAdapters/JpegLazyMetadataTests.cs b/Compression.Tests/PngCrushAdapters/JpegLazyMetadataTests.cs index 7badab4ee..12489ba6f 100644 --- a/Compression.Tests/PngCrushAdapters/JpegLazyMetadataTests.cs +++ b/Compression.Tests/PngCrushAdapters/JpegLazyMetadataTests.cs @@ -60,7 +60,7 @@ public void Scanner_GracefullyHandlesNonJpegBytes() { Assert.That(meta, Is.EqualTo(new FrameMetadata(0, 0, 24, false))); } - [Test] + [Test, Category("Performance")] public void List_PaddedJpeg_CompletesUnderHundredMs_AndDoesNotDecodePixels() { // The legacy code path called JpegReader.FromStream from List(), which // forces a full pixel decode. To prove that path is dead, we build a JPEG diff --git a/Hawkynt.Algorithms.Hashing/Algorithms/Whirlpool.cs b/Hawkynt.Algorithms.Hashing/Algorithms/Whirlpool.cs index 4a2a892ff..70e796dff 100644 --- a/Hawkynt.Algorithms.Hashing/Algorithms/Whirlpool.cs +++ b/Hawkynt.Algorithms.Hashing/Algorithms/Whirlpool.cs @@ -43,7 +43,9 @@ public static byte[] Compute(ReadOnlySpan data) { } var remaining = data.Length - offset; - var finalBlocks = remaining + 1 > 56 ? 128 : 64; + // Whirlpool appends 0x80, pads to 32 mod 64 and ends with a 256-bit length: a second block is + // needed once the tail and the 0x80 leave fewer than 32 bytes. + var finalBlocks = remaining + 1 > 32 ? 128 : 64; var final = new byte[finalBlocks]; data[offset..].CopyTo(final); final[remaining] = 0x80; diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Pea/PeaArchive.cs b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Pea/PeaArchive.cs new file mode 100644 index 000000000..c996247e7 --- /dev/null +++ b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Pea/PeaArchive.cs @@ -0,0 +1,398 @@ +using System.Buffers.Binary; +using System.Security.Cryptography; +using System.Text; +using Compression.Core.Deflate; +using FileFormat.Zlib; +using Hawkynt.Algorithms.Hashing; + +namespace FileFormat.Pea; + +/// One object of a PEA archive. +public sealed class PeaEntry { + /// Gets the path relative to the archive's common root, '/'-separated. + public string Path { get; init; } = ""; + /// Gets the qualified name as stored (PEA stores absolute names). + public string StoredName { get; init; } = ""; + /// Gets whether the object is a directory. + public bool IsDirectory { get; init; } + /// Gets the uncompressed size. + public long Size { get; init; } + /// Gets the file attributes as stored. + public uint Attributes { get; init; } + /// Gets the modification time, when the archive's time encoding is known. + public DateTime? LastModified { get; init; } + /// Gets the decoded content (empty for directories). + public byte[] Data { get; init; } = []; +} + +/// +/// PEA (PeaZip's own format, version 1 revision 6): a 10-byte archive header, one stream with a +/// 10-byte header naming the compression (stored or zlib at level 3/6/9 in independent 1 MiB +/// blocks) and three integrity checks — per object, for the stream and for the volume — then +/// objects (name, time, attributes, size, data) and an end-of-archive trigger. This covers +/// single-volume archives with any of the hash or checksum controls; encrypted streams (EAX, +/// HMAC, Twofish, Serpent and their cascades) are refused. +/// +/// +/// Layout from the public-domain PEA file format specification and PeaZip's pea_utils.pas / +/// pea.pas (LGPL-3.0), measured against archives PeaZip 11.4 writes. +/// +public static class PeaArchive { + private const byte Magic = 0xEA; + private const int WriteBlockSize = 1 << 20; // WBUFSIZE in pea.pas + private const uint DirectoryAttribute = 0x10; + + /// A check algorithm by its PEA id. + public enum Check : byte { + /// No check. + None = 0x00, + /// Adler-32 (4 bytes, little-endian). + Adler32 = 0x01, + /// CRC-32 (4 bytes, little-endian). + Crc32 = 0x02, + /// CRC-64 ECMA-182, MSB-first, all-ones init and xor-out (8 bytes, little-endian). + Crc64 = 0x03, + /// MD5. + Md5 = 0x10, + /// RIPEMD-160. + Ripemd160 = 0x11, + /// SHA-1. + Sha1 = 0x12, + /// SHA-256. + Sha256 = 0x13, + /// SHA-512. + Sha512 = 0x14, + /// Whirlpool. + Whirlpool = 0x15, + /// SHA3-256. + Sha3_256 = 0x16, + /// SHA3-512. + Sha3_512 = 0x17, + /// BLAKE2s-256. + Blake2s = 0x18, + /// BLAKE2b-512. + Blake2b = 0x19, + } + + private static int TagSize(byte algo) => algo switch { + 0x00 => 0, + 0x01 or 0x02 => 4, + 0x03 => 8, + 0x10 => 16, + 0x11 or 0x12 => 20, + 0x13 or 0x16 or 0x18 => 32, + 0x14 or 0x15 or 0x17 or 0x19 => 64, + >= 0x30 => throw new NotSupportedException($"PEA stream control algorithm 0x{algo:X2} is encryption, which is not supported."), + _ => throw new InvalidDataException($"PEA check algorithm 0x{algo:X2} is unknown."), + }; + + private static byte[] Tag(byte algo, ReadOnlySpan data) => algo switch { + 0x00 => [], + 0x01 => Le32(Adler32Of(data)), + 0x02 => Le32(Compression.Core.Checksums.Crc32.Compute(data)), + 0x03 => Le64(Crc64We(data)), + 0x10 => MD5.HashData(data), + 0x11 => Ripemd160.Compute(data), + 0x12 => SHA1.HashData(data), + 0x13 => SHA256.HashData(data), + 0x14 => SHA512.HashData(data), + 0x15 => Whirlpool.Compute(data), + 0x16 => Sha3.Compute256(data), + 0x17 => Sha3.Compute512(data), + 0x18 => Blake2s.Compute(data), + 0x19 => Compression.Core.Checksums.Blake2b.Compute(data, 64), + _ => throw new NotSupportedException($"PEA check algorithm 0x{algo:X2} is not supported."), + }; + + private static byte[] Le32(uint v) { var b = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(b, v); return b; } + private static byte[] Le64(ulong v) { var b = new byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(b, v); return b; } + + private static uint Adler32Of(ReadOnlySpan data) { + uint a = 1, b = 0; + foreach (var x in data) { a = (a + x) % 65521; b = (b + a) % 65521; } + return (b << 16) | a; + } + + private static readonly ulong[] Crc64Table = BuildCrc64(); + + private static ulong[] BuildCrc64() { + const ulong poly = 0x42F0E1EBA9EA3693; + var t = new ulong[256]; + for (var i = 0; i < 256; ++i) { + var c = (ulong)i << 56; + for (var k = 0; k < 8; ++k) c = (c & 0x8000000000000000) != 0 ? (c << 1) ^ poly : c << 1; + t[i] = c; + } + return t; + } + + /// CRC-64 as PeaZip computes it (W. Ehrhardt's crc64: ECMA-182 polynomial, not reflected, init and xor-out all ones). + internal static ulong Crc64We(ReadOnlySpan data) { + var crc = ulong.MaxValue; + foreach (var b in data) crc = (crc << 8) ^ Crc64Table[(byte)(crc >> 56) ^ b]; + return ~crc; + } + + private static readonly Lazy Ansi = new(() => { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + return Encoding.GetEncoding(1252); // the format's "ANSI" character encoding (byte 7 = 1) + }); + + /// Reads and verifies a single-volume PEA archive. + /// Not a PEA archive, or a check fails. + /// Encrypted stream, newer revision, or unsupported compression. + public static List Read(Stream stream) { + ArgumentNullException.ThrowIfNull(stream); + if (stream.CanSeek) stream.Position = 0; + using var ms = new MemoryStream(); + stream.CopyTo(ms); + var b = ms.ToArray(); + if (b.Length < 20 || b[0] != Magic) throw new InvalidDataException("Not a PEA archive."); + if (b[1] > 1 || b[1] == 1 && b[2] > 6) throw new NotSupportedException($"PEA format {b[1]}.{b[2]} is newer than this reader."); + var volumeAlgo = b[3]; + var timeEncoding = b[6]; + if (!b.AsSpan(10, 6).SequenceEqual((ReadOnlySpan)[0, 0, (byte)'P', (byte)'O', (byte)'D', 0])) + throw new InvalidDataException("PEA stream header is missing."); + var compression = b[16]; + var streamAlgo = b[18]; + var objAlgo = b[19]; + if (compression > 3) throw new NotSupportedException($"PEA compression {compression} is not supported."); + int volumeTag = TagSize(volumeAlgo), streamTag = TagSize(streamAlgo), objTag = TagSize(objAlgo); + + // A single-volume archive ends with the volume check; everything before it is checked by it. + var end = b.Length - volumeTag; + if (end < 20) throw new InvalidDataException("PEA archive is truncated."); + if (volumeAlgo != 0 && !Tag(volumeAlgo, b.AsSpan(0, end)).AsSpan().SequenceEqual(b.AsSpan(end, volumeTag))) + throw new InvalidDataException("PEA volume check fails (damaged, or not the only volume)."); + + var p = 20; + var blockSize = 0; + if (compression != 0) { + blockSize = (int)ReadU32(b, ref p, end); + if (blockSize <= 0) throw new InvalidDataException("PEA compression block size is invalid."); + } + + var raw = new List<(string Name, uint Time, uint Attr, bool Dir, long Size, byte[] Data)>(); + while (true) { + var objStart = p; + var nameLength = ReadU16(b, ref p, end); + if (nameLength == 0) { + Need(b, p, 4, end); + var trigger = b.AsSpan(p, 4); + p += 4; + if (trigger.SequenceEqual("EOA\0"u8)) break; + throw new NotSupportedException($"PEA trigger '{Encoding.ASCII.GetString(trigger).TrimEnd('\0')}' (multiple streams) is not supported."); + } + Need(b, p, nameLength + 8, end); + var name = Ansi.Value.GetString(b, p, nameLength); + p += nameLength; + var time = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(p)); + var attr = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(p + 4)); + p += 8; + var objCheck = new MemoryStream(); + objCheck.Write(b, objStart, p - objStart); + var isDir = (attr & DirectoryAttribute) != 0; + long size = 0; + byte[] data = []; + if (!isDir) { + Need(b, p, 8, end); + size = BinaryPrimitives.ReadInt64LittleEndian(b.AsSpan(p)); + if (size < 0 || size > int.MaxValue) throw new NotSupportedException("PEA object exceeds the 2 GiB in-memory limit."); + objCheck.Write(b, p, 8); + p += 8; + data = new byte[size]; + if (size > 0) { + if (compression == 0) { + Need(b, p, (int)size, end); + b.AsSpan(p, (int)size).CopyTo(data); + objCheck.Write(b, p, (int)size); + p += (int)size; + } else { + var blocks = (size + blockSize - 1) / blockSize; + var o = 0; + for (var i = 0; i < blocks; ++i) { + var rawSize = i < blocks - 1 ? blockSize : (int)(size - (long)blockSize * (blocks - 1)); + var compSize = (int)ReadU32(b, ref p, end); + Need(b, p, compSize, end); + byte[] block; + if (compSize == rawSize) block = b.AsSpan(p, compSize).ToArray(); // stored: compression did not help + else block = ZlibStream.Decompress(b.AsSpan(p, compSize)); + if (block.Length != rawSize) throw new InvalidDataException($"PEA block of '{name}' has the wrong size."); + p += compSize; + objCheck.Write(Le32((uint)compSize)); + objCheck.Write(block); + block.CopyTo(data, o); + o += rawSize; + } + var last = ReadU32(b, ref p, end); + objCheck.Write(Le32(last)); + } + } + } + Need(b, p, objTag, end); + if (objAlgo != 0 && !Tag(objAlgo, objCheck.ToArray()).AsSpan().SequenceEqual(b.AsSpan(p, objTag))) + throw new InvalidDataException($"PEA object check fails for '{name}'."); + p += objTag; + raw.Add((name, time, attr, isDir, size, data)); + } + if (p + streamTag != end) throw new InvalidDataException("PEA archive has unexpected data after the end of the stream."); + if (streamAlgo != 0 && !Tag(streamAlgo, b.AsSpan(0, p)).AsSpan().SequenceEqual(b.AsSpan(p, streamTag))) + throw new InvalidDataException("PEA stream check fails."); + + var root = CommonRoot(raw.Select(r => r.Name)); + return raw.Select(r => new PeaEntry { + StoredName = r.Name, + Path = Relative(r.Name, root), + IsDirectory = r.Dir, + Size = r.Size, + Attributes = r.Attr, + LastModified = Time(r.Time, timeEncoding), + Data = r.Data, + }).ToList(); + } + + private static void Need(byte[] b, int p, int n, int end) { + if (n < 0 || p + n > end) throw new InvalidDataException("PEA archive is truncated."); + } + + private static uint ReadU32(byte[] b, ref int p, int end) { + Need(b, p, 4, end); + var v = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(p)); + p += 4; + return v; + } + + private static int ReadU16(byte[] b, ref int p, int end) { + Need(b, p, 2, end); + var v = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(p)); + p += 2; + return v; + } + + private static string Normalize(string name) => name.Replace('\\', '/'); + + /// The directory every stored name lives under; unpea extracts relative to it. + private static string CommonRoot(IEnumerable names) { + string? root = null; + foreach (var n in names.Select(Normalize)) { + var trimmed = n.TrimEnd('/'); + var slash = trimmed.LastIndexOf('/'); + var dir = slash < 0 ? "" : trimmed[..(slash + 1)]; + if (root is null) { root = dir; continue; } + while (root.Length > 0 && !dir.StartsWith(root, StringComparison.Ordinal)) { + var cut = root.TrimEnd('/').LastIndexOf('/'); + root = cut < 0 ? "" : root[..(cut + 1)]; + } + } + return root ?? ""; + } + + private static string Relative(string name, string root) { + var n = Normalize(name); + var rel = n.StartsWith(root, StringComparison.Ordinal) ? n[root.Length..] : n; + rel = rel.Trim('/'); + if (rel.Length >= 2 && rel[1] == ':') rel = rel[2..].TrimStart('/'); + var parts = rel.Split('/', StringSplitOptions.RemoveEmptyEntries).Where(s => s is not "." and not ".."); + var joined = string.Join('/', parts); + return joined.Length == 0 ? "_" : joined; + } + + private static DateTime? Time(uint value, byte encoding) { + try { + return encoding switch { + 0x30 => DateTimeOffset.FromUnixTimeSeconds(value).UtcDateTime, + 0x10 => DosTime(value), + _ => null, + }; + } catch (ArgumentOutOfRangeException) { + return null; + } + } + + private static DateTime? DosTime(uint v) { + var date = (int)(v >> 16); + var time = (int)(v & 0xFFFF); + if (date == 0) return null; + return new DateTime(1980 + (date >> 9), (date >> 5) & 15, date & 31, time >> 11, (time >> 5) & 63, (time & 31) * 2, DateTimeKind.Local); + } + + // ── writing ───────────────────────────────────────────────────────── + + /// An object to write. + public readonly record struct Input(string Path, byte[]? Data, DateTime LastModified); + + /// + /// Writes a single-volume PEA 1.6 archive the way PeaZip does on Unix: names are stored as + /// absolute paths under /, times as Unix seconds, zlib blocks of 1 MiB (a block that + /// does not shrink is stored), with the given object, stream and volume checks. + /// + /// 0 = stored, 1/2/3 = zlib level 3/6/9. + public static void Write(Stream output, IEnumerable inputs, int compression = 2, + Check objectCheck = Check.Crc32, Check streamCheck = Check.Sha256, Check volumeCheck = Check.Crc32) { + ArgumentNullException.ThrowIfNull(output); + if (compression is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(compression)); + var ms = new MemoryStream(); + // archive header: magic, version 1, revision 6, volume check, ECC 0, OS (Linux), time encoding (Unix), ANSI, CPU, iterations + ms.Write([Magic, 1, 6, (byte)volumeCheck, 0, 0x33, 0x30, 0x01, 0x21, 0x00]); + ms.Write([0, 0, (byte)'P', (byte)'O', (byte)'D', 0, (byte)compression, 0, (byte)streamCheck, (byte)objectCheck]); + if (compression != 0) ms.Write(Le32(WriteBlockSize)); + var level = compression switch { 1 => DeflateCompressionLevel.Fast, 3 => DeflateCompressionLevel.Maximum, _ => DeflateCompressionLevel.Default }; + + // Directories first are not required; keep the caller's order, adding parent directories once. + var seenDirs = new HashSet(StringComparer.Ordinal); + var objects = new List(); + foreach (var input in inputs) { + var path = input.Path.Replace('\\', '/').Trim('/'); + if (path.Length == 0) continue; + var parts = path.Split('/'); + for (var i = 1; i < parts.Length; ++i) { + var dir = string.Join('/', parts[..i]); + if (seenDirs.Add(dir)) objects.Add(new Input(dir, null, input.LastModified)); + } + if (input.Data is null) { if (seenDirs.Add(path)) objects.Add(input with { Path = path }); } + else objects.Add(input with { Path = path }); + } + + foreach (var obj in objects) { + var isDir = obj.Data is null; + var name = Ansi.Value.GetBytes("/" + obj.Path + (isDir ? "/" : "")); + if (name.Length > ushort.MaxValue) throw new NotSupportedException($"PEA names are at most 65535 bytes ('{obj.Path}')."); + var header = new MemoryStream(); + header.Write(Le32((uint)name.Length).AsSpan(0, 2)); + header.Write(name); + header.Write(Le32((uint)Math.Clamp(new DateTimeOffset(obj.LastModified.ToUniversalTime()).ToUnixTimeSeconds(), 0, uint.MaxValue))); + header.Write(Le32(isDir ? 0x30u : 0x20u)); + var objCheck = new MemoryStream(); + if (!isDir) header.Write(Le64((ulong)obj.Data!.Length)); + var headerBytes = header.ToArray(); + ms.Write(headerBytes); + objCheck.Write(headerBytes); + if (!isDir && obj.Data!.Length > 0) { + if (compression == 0) { + ms.Write(obj.Data); + objCheck.Write(obj.Data); + } else { + var lastSize = 0; + for (var offset = 0; offset < obj.Data.Length; offset += WriteBlockSize) { + var block = obj.Data.AsSpan(offset, Math.Min(WriteBlockSize, obj.Data.Length - offset)); + var packed = ZlibStream.Compress(block, level); + var stored = packed.Length >= block.Length; + var payload = stored ? block.ToArray() : packed; + ms.Write(Le32((uint)payload.Length)); + ms.Write(payload); + objCheck.Write(Le32((uint)payload.Length)); + objCheck.Write(block); + lastSize = block.Length; + } + ms.Write(Le32((uint)lastSize)); + objCheck.Write(Le32((uint)lastSize)); + } + } + ms.Write(Tag((byte)objectCheck, objCheck.ToArray())); + } + ms.Write([0, 0, (byte)'E', (byte)'O', (byte)'A', 0]); + ms.Write(Tag((byte)streamCheck, ms.GetBuffer().AsSpan(0, (int)ms.Length))); + ms.Write(Tag((byte)volumeCheck, ms.GetBuffer().AsSpan(0, (int)ms.Length))); + output.Write(ms.GetBuffer(), 0, (int)ms.Length); + } +} diff --git a/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Pea/PeaFormatDescriptor.cs b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Pea/PeaFormatDescriptor.cs new file mode 100644 index 000000000..eadd9a20d --- /dev/null +++ b/Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Pea/PeaFormatDescriptor.cs @@ -0,0 +1,70 @@ +using Compression.Registry; +using static Compression.Registry.FormatHelpers; + +namespace FileFormat.Pea; + +/// PEA archive (.pea), PeaZip's native format. +public sealed class PeaFormatDescriptor : IFormatDescriptor, IArchiveFormatOperations, IArchiveCreatable { + /// + public string Id => "Pea"; + /// + public string DisplayName => "PEA (PeaZip)"; + /// + public FormatCategory Category => FormatCategory.Archive; + /// + public FormatCapabilities Capabilities => + FormatCapabilities.CanList | FormatCapabilities.CanExtract | FormatCapabilities.CanCreate | FormatCapabilities.CanTest | + FormatCapabilities.SupportsMultipleEntries | FormatCapabilities.SupportsDirectories; + /// + public string DefaultExtension => ".pea"; + /// + public IReadOnlyList Extensions => [".pea"]; + /// + public IReadOnlyList CompoundExtensions => []; + // 0xEA, version 1, revision 0-6: three bytes so a stray 0xEA byte does not claim a file. + /// + public IReadOnlyList MagicSignatures => [.. Enumerable.Range(0, 7).Select(r => new MagicSignature([0xEA, 0x01, (byte)r], Confidence: 0.80))]; + /// + public IReadOnlyList Methods => [ + new("store", "PCOMPRESS0 (stored)"), new("fast", "PCOMPRESS1 (zlib 3)"), new("normal", "PCOMPRESS2 (zlib 6)"), new("best", "PCOMPRESS3 (zlib 9)"), + ]; + /// + public string? TarCompressionFormatId => null; + /// + public AlgorithmFamily Family => AlgorithmFamily.Archive; + /// + public string Description => "PeaZip's PEA archive: zlib blocks with per-object, stream and volume checks"; + + /// + public List List(Stream stream, string? password) + => PeaArchive.Read(stream).Select((e, i) => new ArchiveEntryInfo(i, e.Path, e.Size, -1, + e.IsDirectory ? "" : "PEA", e.IsDirectory, false, e.LastModified)).ToList(); + + /// + public void Extract(Stream stream, string outputDir, string? password, string[]? files) { + foreach (var e in PeaArchive.Read(stream)) { + if (e.IsDirectory || files is { Length: > 0 } && !MatchesFilter(e.Path, files)) continue; + WriteFile(outputDir, e.Path, e.Data); + } + } + + /// + public byte[] ExtractEntryToMemory(Stream archive, string entryName, string? password) + => PeaArchive.Read(archive).FirstOrDefault(e => !e.IsDirectory && string.Equals(e.Path, entryName, StringComparison.OrdinalIgnoreCase))?.Data ?? []; + + /// + public void Create(Stream output, IReadOnlyList inputs, FormatCreateOptions options) { + ArgumentNullException.ThrowIfNull(inputs); + if (!string.IsNullOrEmpty(options.Password)) + throw new NotSupportedException("PEA encryption (EAX / Twofish / Serpent with scrypt or PBKDF2) is not written."); + var compression = (options.MethodName ?? "normal").Trim().ToLowerInvariant() switch { + "store" or "pcompress0" => 0, + "fast" or "pcompress1" => 1, + "" or "normal" or "pcompress2" => 2, + "best" or "pcompress3" => 3, + var m => throw new NotSupportedException($"PEA method '{m}' is not supported."), + }; + PeaArchive.Write(output, inputs.Select(i => new PeaArchive.Input(i.ArchiveName, i.IsDirectory ? null : i.ReadContent(), + i.Metadata?.LastWriteTimeUtc?.UtcDateTime ?? DateTime.UtcNow)), compression); + } +} diff --git a/Hawkynt.FileFormats.Archives/README.md b/Hawkynt.FileFormats.Archives/README.md index 01e66b24d..b5db511a4 100644 --- a/Hawkynt.FileFormats.Archives/README.md +++ b/Hawkynt.FileFormats.Archives/README.md @@ -136,6 +136,7 @@ Every State, Test, Maintenance, Compress/Decompress and Demux/Mux/Remux cell is | [mtree](https://man.freebsd.org/cgi/man.cgi?query=mtree&sektion=5) | `Mtree` | `.mtree` | WORM | ✅ | — | Filesystem metadata manifest; does not embed file bodies, so CWB does not dereference `contents=` host paths | [FreeBSD mtree(5)](https://man.freebsd.org/cgi/man.cgi?query=mtree&sektion=5) | | [NuFX / ShrinkIt](https://en.wikipedia.org/wiki/ShrinkIt) | `NuFx` | `.shk` `.sdk` `.bxy` | R/W | ✅ | defrag · shrink · wipe | | [nulib.com](https://nulib.com/library/FTN.e08002.htm) | | PackIt | `PackIt` | `.pit` | R/W | ✅ | defrag · wipe | Reads stored and Huffman entries, writes stored; encrypted entries are refused | [GitHub](https://github.com/MacPaw/XADMaster) | +| [PEA](https://peazip.github.io/pea-file-format.html) | `Pea` | `.pea` | WORM | ✅ | — | Single-volume, unencrypted streams; every compression level and check algorithm | [peazip.github.io](https://peazip.github.io/pea-file-format.html) | | [RAR](https://en.wikipedia.org/wiki/RAR_(file_format)) | `Rar` | `.rar` | R/W | ✅ | wipe | v1–v5 readers; creation and edits emit RAR4/RAR5 without claiming WinRAR encoder parity | [rarlab.com](https://www.rarlab.com/technote.htm) | | [7z](https://en.wikipedia.org/wiki/7z) | `SevenZip` | `.7z` | R/W | ✅ | compress · wipe | Also reads Deflate64, the ARM64 / RISC-V filters and 7-Zip-zstd's Zstandard / Brotli / LZ4 coders | [7-zip.org](https://www.7-zip.org/7z.html) | | [SHAR](https://en.wikipedia.org/wiki/Shar) | `Shar` | `.shar` `.sh` | R/W | ✅ | defrag | Add appends in place; Remove re-emits the script from the survivors | [gnu.org](https://www.gnu.org/software/sharutils/) | @@ -551,7 +552,7 @@ Use it when a .NET process needs to enumerate, extract, test, create, edit or in -Every public and protected member of all 2584 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/CompressionWorkbench/blob/main/Hawkynt.FileFormats.Archives/REFERENCE.md). +Every public and protected member of all 2589 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/CompressionWorkbench/blob/main/Hawkynt.FileFormats.Archives/REFERENCE.md). diff --git a/Hawkynt.FileFormats.Archives/REFERENCE.md b/Hawkynt.FileFormats.Archives/REFERENCE.md index d6d93645f..d1218324a 100644 --- a/Hawkynt.FileFormats.Archives/REFERENCE.md +++ b/Hawkynt.FileFormats.Archives/REFERENCE.md @@ -25337,6 +25337,94 @@ Implements `IArchiveFormatOperations`, `IFormatDescriptor`. | `Extract` | `void Extract(Stream stream, string outputDir, string password, string[] files)` | Decodes the supplied input. | | `List` | `List List(Stream stream, string password)` | Lists the entries in the supplied container. | +### Namespace `FileFormat.Pea` + +[`PeaArchive`](#peaarchive) · [`PeaArchive.Check`](#peaarchivecheck) · [`PeaArchive.Input`](#peaarchiveinput) · [`PeaEntry`](#peaentry) · [`PeaFormatDescriptor`](#peaformatdescriptor) + +#### `PeaArchive` + +PEA (PeaZip's own format, version 1 revision 6): a 10-byte archive header, one stream with a 10-byte header naming the compression (stored or zlib at level 3/6/9 in independent 1 MiB blocks) and three integrity checks — per object, for the stream and for the volume — then objects (name, time, attributes, size, data) and an end-of-archive trigger. This covers single-volume archives with any of the hash or checksum controls; encrypted streams (EAX, HMAC, Twofish, Serpent and their cascades) are refused. + +| Member | Signature | Summary | +| --- | --- | --- | +| `Read` | `static List Read(Stream stream)` | Reads and verifies a single-volume PEA archive. | +| `Write` | `static void Write(Stream output, IEnumerable inputs, int compression = 2, Check objectCheck = 2, Check streamCheck = 19, Check volumeCheck = 2)` | Writes a single-volume PEA 1.6 archive the way PeaZip does on Unix: names are stored as absolute paths under `/`, times as Unix seconds, zlib blocks of 1 MiB (a block that does not shrink is stored), with the given object, stream and volume checks. | + +#### `PeaArchive.Check` + +A check algorithm by its PEA id. + +| Value | Numeric | Summary | +| --- | --- | --- | +| `None` | `0` | No check. | +| `Adler32` | `1` | Adler-32 (4 bytes, little-endian). | +| `Crc32` | `2` | CRC-32 (4 bytes, little-endian). | +| `Crc64` | `3` | CRC-64 ECMA-182, MSB-first, all-ones init and xor-out (8 bytes, little-endian). | +| `Md5` | `16` | MD5. | +| `Ripemd160` | `17` | RIPEMD-160. | +| `Sha1` | `18` | SHA-1. | +| `Sha256` | `19` | SHA-256. | +| `Sha512` | `20` | SHA-512. | +| `Whirlpool` | `21` | Whirlpool. | +| `Sha3_256` | `22` | SHA3-256. | +| `Sha3_512` | `23` | SHA3-512. | +| `Blake2s` | `24` | BLAKE2s-256. | +| `Blake2b` | `25` | BLAKE2b-512. | + +#### `PeaArchive.Input` + +An object to write. + +Implements `IEquatable`. + +| Member | Signature | Summary | +| --- | --- | --- | +| `Input` | `Input(string Path, byte[] Data, DateTime LastModified)` | An object to write. | +| `Data` | `byte[] Data { get; init; }` | | +| `LastModified` | `DateTime LastModified { get; init; }` | | +| `Path` | `string Path { get; init; }` | | + +#### `PeaEntry` + +One object of a PEA archive. + +| Member | Signature | Summary | +| --- | --- | --- | +| `PeaEntry` | `PeaEntry()` | | +| `Attributes` | `uint Attributes { get; init; }` | Gets the file attributes as stored. | +| `Data` | `byte[] Data { get; init; }` | Gets the decoded content (empty for directories). | +| `IsDirectory` | `bool IsDirectory { get; init; }` | Gets whether the object is a directory. | +| `LastModified` | `DateTime? LastModified { get; init; }` | Gets the modification time, when the archive's time encoding is known. | +| `Path` | `string Path { get; init; }` | Gets the path relative to the archive's common root, '/'-separated. | +| `Size` | `long Size { get; init; }` | Gets the uncompressed size. | +| `StoredName` | `string StoredName { get; init; }` | Gets the qualified name as stored (PEA stores absolute names). | + +#### `PeaFormatDescriptor` + +PEA archive (.pea), PeaZip's native format. + +Implements `IArchiveCreatable`, `IArchiveFormatOperations`, `IFormatDescriptor`. + +| Member | Signature | Summary | +| --- | --- | --- | +| `PeaFormatDescriptor` | `PeaFormatDescriptor()` | | +| `Capabilities` | `FormatCapabilities Capabilities { get; }` | | +| `Category` | `FormatCategory Category { get; }` | | +| `CompoundExtensions` | `IReadOnlyList CompoundExtensions { get; }` | | +| `DefaultExtension` | `string DefaultExtension { get; }` | | +| `Description` | `string Description { get; }` | | +| `DisplayName` | `string DisplayName { get; }` | | +| `Extensions` | `IReadOnlyList Extensions { get; }` | | +| `Family` | `AlgorithmFamily Family { get; }` | | +| `Id` | `string Id { get; }` | | +| `MagicSignatures` | `IReadOnlyList MagicSignatures { get; }` | | +| `Methods` | `IReadOnlyList Methods { get; }` | | +| `TarCompressionFormatId` | `string TarCompressionFormatId { get; }` | | +| `Create` | `void Create(Stream output, IReadOnlyList inputs, FormatCreateOptions options)` | | +| `ExtractEntryToMemory` | `byte[] ExtractEntryToMemory(Stream archive, string entryName, string password)` | | +| `Extract` | `void Extract(Stream stream, string outputDir, string password, string[] files)` | | +| `List` | `List List(Stream stream, string password)` | | + ### Namespace `FileFormat.Pickle` [`PickleFormatDescriptor`](#pickleformatdescriptor) diff --git a/docs/catalogue/our-registry.csv b/docs/catalogue/our-registry.csv index 7b9d6c5d5..f03d35762 100644 --- a/docs/catalogue/our-registry.csv +++ b/docs/catalogue/our-registry.csv @@ -411,6 +411,7 @@ format,Pc98,NEC PC-98 DOS,Archive,Y,.hdm;.fdi;.d88,Y,Y,Y,Y format,Pcap,PCAP (libpcap capture),Archive,,.pcap;.cap,Y,Y,, format,Pcapng,PCAP Next Generation,Archive,,.pcapng;.ntar,Y,Y,, format,Pdf,PDF (Image Extraction),Archive,,.pdf,Y,Y,Y,Y +format,Pea,PEA (PeaZip),Archive,,.pea,Y,Y,Y, format,PeResources,PE Resources (Windows binary),Archive,,.dll;.exe;.ocx;.cpl;.sys;.mui;.mun,Y,Y,, format,Petite,Petite (Win32 PE),Archive,,,Y,Y,, format,Pfs0,Nintendo PartitionFS,Archive,,.nsp;.pfs0,Y,Y,Y,Y diff --git a/docs/catalogue/reference-formats.csv b/docs/catalogue/reference-formats.csv index 4c8ceb996..614a1791a 100644 --- a/docs/catalogue/reference-formats.csv +++ b/docs/catalogue/reference-formats.csv @@ -59,7 +59,7 @@ FreeArc,arc;cdx,archive,,,,,R,fmt/1096,Y,3,https://en.wikipedia.org/wiki/FreeArc ZPAQ,zpaq,archive,,,,,R,fmt/1097,RW,3,https://mattmahoney.net/dc/zpaq206.pdf,Zpaq,Y,Y,Y,PARTIAL,,"journaling and streaming archives at every method are read and zpaq 7.15 extracts what we write; encrypted (-key), multi-part and -index archives are refused, and modify rewrites a single version instead of appending one (ZpaqReader.cs, ZpaqWriter.cs)" PAQ (paq6/7/8 family),paq6;paq7;paq8;paq8l,archive,,,,,,,Y,1,https://mattmahoney.net/dc/paq.html,Paq8,Y,Y,,PARTIAL,,a single PAQ8 variant (.paq8l); the other PAQ versions are mutually incompatible KGB Archiver,kgb;kge,archive,,,,,R,,RW,2,,,,,,MISSING,, -PEA (PeaZip),pea,archive,,,,,R,fmt/1095,Y,3,,,,,,MISSING,,https://peazip.github.io/pea-file-format.html +PEA (PeaZip),pea,archive,,,,,R,fmt/1095,Y,3,https://peazip.github.io/pea-file-format.html,Pea,Y,Y,,PARTIAL,,encrypted streams (EAX/HMAC/Twofish/Serpent and cascades) and multi-volume archives unsupported; no in-place modify DGCA,dgc,archive,,,,,R,,Y,2,,,,,,MISSING,, GCA,gca,archive,,,,,,,Y,1,,,,,,MISSING,, BGA,bza;gza,archive,,,,,R,,,1,,,,,,MISSING,,