diff --git a/Compression.Tests/Compression.Tests.csproj b/Compression.Tests/Compression.Tests.csproj index 023dbbe39..70f1d79a1 100644 --- a/Compression.Tests/Compression.Tests.csproj +++ b/Compression.Tests/Compression.Tests.csproj @@ -150,6 +150,10 @@ G722Vectors.%(Filename)%(Extension) + + + FossilVectors.%(Filename)%(Extension) + ZooVectors.%(Filename)%(Extension) diff --git a/Compression.Tests/Fossil/FossilTests.cs b/Compression.Tests/Fossil/FossilTests.cs new file mode 100644 index 000000000..556ec4247 --- /dev/null +++ b/Compression.Tests/Fossil/FossilTests.cs @@ -0,0 +1,110 @@ +using System.Buffers.Binary; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; +using FileSystem.Fossil; + +namespace Compression.Tests.Fossil; + +/// +/// Fossil: the reader against a volume plan9port's flfmt formatted and its +/// fossil server filled over 9P (see Fossil/ReferenceVectors/README.md), +/// compared with what that server read back; plus refusals. +/// +[TestFixture] +public class FossilTests { + + private static byte[] Image() { + using var res = typeof(FossilTests).Assembly.GetManifestResourceStream("FossilVectors.fossil.img.gz") + ?? throw new InvalidOperationException("fossil.img.gz not embedded"); + using var gz = new GZipStream(res, CompressionMode.Decompress); + using var ms = new MemoryStream(); + gz.CopyTo(ms); + return ms.ToArray(); + } + + private static Dictionary Manifest() { + using var res = typeof(FossilTests).Assembly.GetManifestResourceStream("FossilVectors.fossil.manifest") + ?? throw new InvalidOperationException("fossil.manifest not embedded"); + using var sr = new StreamReader(res); + return sr.ReadToEnd().Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.TrimEnd('\r').Split(" ", 2)).ToDictionary(p => p[1], p => p[0]); + } + + private static string Sha(byte[] d) => Convert.ToHexStringLower(SHA256.HashData(d)); + + [Test] + public void GivenServerWrittenVolume_WhenRead_ThenActiveTreeMatchesWhatTheServerReadBack() { + var r = new FossilReader(new MemoryStream(Image(), writable: false)); + var manifest = Manifest(); + var files = r.Entries.Where(e => !e.IsDirectory && e.Path.StartsWith("active/", StringComparison.Ordinal)).ToDictionary(e => e.Path); + Assert.That(files.Keys, Is.EquivalentTo(manifest.Keys)); + foreach (var (path, sha) in manifest) Assert.That(Sha(r.Extract(files[path])), Is.EqualTo(sha), path); + } + + [Test] + public void GivenServerWrittenVolume_WhenRead_ThenDirectoriesModesAndOwnersAreThoseTheServerCreated() { + var r = new FossilReader(new MemoryStream(Image(), writable: false)); + var byPath = r.Entries.ToDictionary(e => e.Path); + Assert.Multiple(() => { + Assert.That(byPath["active/docs"].IsDirectory, Is.True); + Assert.That(byPath["active/docs"].Mode & 0x1FF, Is.EqualTo(0x1FD), "d775"); + Assert.That(byPath["active/empty-dir"].IsDirectory, Is.True); + Assert.That(byPath["active/docs/notes.txt"].Mode & 0x1FF, Is.EqualTo(0x180), "600"); + Assert.That(byPath["active/hello.txt"].Uid, Is.EqualTo("sys")); + Assert.That(byPath["active/big.bin"].Source.Depth, Is.EqualTo(2), "900 KB at 4 KiB blocks needs a two-level pointer tree"); + }); + } + + [Test] + public void GivenSnapshotTakenBeforeARewrite_WhenRead_ThenTheSnapshotKeepsTheOldBytes() { + var r = new FossilReader(new MemoryStream(Image(), writable: false)); + var snap = r.Entries.Where(e => !e.IsDirectory && e.Path.StartsWith("snapshot/", StringComparison.Ordinal) && e.Path.EndsWith("/hello.txt", StringComparison.Ordinal)).ToList(); + Assert.That(snap, Has.Count.EqualTo(1), string.Join(", ", r.Entries.Select(e => e.Path))); + Assert.That(Encoding.ASCII.GetString(r.Extract(snap[0])), Is.EqualTo("hello from fossil\n")); + var active = r.Entries.Single(e => e.Path == "active/hello.txt"); + Assert.That(Encoding.ASCII.GetString(r.Extract(active)), Is.EqualTo("hello again, after the snapshot was taken\n")); + } + + [Test] + public void GivenVolume_WhenDetectedByMagic_ThenFossil() + => Assert.That(Compression.Lib.FormatDetector.DetectByMagic(Image().AsSpan(0, 192 * 1024)).ToString(), Is.EqualTo("Fossil")); + + [Test] + public void GivenWrongHeaderMagic_WhenOpened_ThenInvalidData() { + var img = Image(); + img[FossilReader.HeaderOffset] ^= 0xFF; + Assert.Throws(() => new FossilReader(new MemoryStream(img))); + } + + [Test] + public void GivenWrongSuperblockMagic_WhenOpened_ThenInvalidData() { + var img = Image(); + var bs = BinaryPrimitives.ReadUInt16BigEndian(img.AsSpan(FossilReader.HeaderOffset + 6)); + var super = BinaryPrimitives.ReadUInt32BigEndian(img.AsSpan(FossilReader.HeaderOffset + 8)); + img[(int)(super * bs)] ^= 0xFF; + Assert.Throws(() => new FossilReader(new MemoryStream(img))); + } + + [Test] + public void GivenTruncatedImage_WhenRead_ThenRefusedNotGuessed() { + var img = Image().AsSpan(0, 200 * 1024).ToArray(); + Assert.Catch(() => { + var r = new FossilReader(new MemoryStream(img)); + foreach (var e in r.Entries) r.Extract(e); + }); + } + + [Test] + public void GivenDamagedMetaBlock_WhenRead_ThenRefused() { + var img = Image(); + var magic = new byte[] { 0x56, 0x56, 0xFC, 0x7A }; + // Corrupt every meta block's magic: no directory can be listed any more. + for (var i = img.AsSpan().IndexOf(magic); i >= 0; ) { + img[i] = 0; + var next = img.AsSpan(i + 1).IndexOf(magic); + i = next < 0 ? -1 : i + 1 + next; + } + Assert.Throws(() => new FossilReader(new MemoryStream(img))); + } +} diff --git a/Compression.Tests/Fossil/ReferenceVectors/README.md b/Compression.Tests/Fossil/ReferenceVectors/README.md new file mode 100644 index 000000000..35930ae31 --- /dev/null +++ b/Compression.Tests/Fossil/ReferenceVectors/README.md @@ -0,0 +1,17 @@ +# Fossil reference volume + +`fossil.img.gz` is a 16 MiB Fossil volume (4 KiB blocks) that nothing in this repository +wrote. It is gzipped only to keep the repository small. + +| | | +| --- | --- | +| Formatted by | `flfmt -b 4096` from plan9port, commit `1f1396913fb5372408f442ed26933f5ae4b399cd`, built on Ubuntu 24.04 (WSL) | +| Written by | that plan9port's `fossil` server. Its console created the directories and files; plan9port's `9p` client wrote the data; `fsys main snap` took a local snapshot; `hello.txt` was rewritten after it; `sync`/`halt` closed the volume | +| Recipe | `generate.sh ` | +| Created | 2026-10-23 | +| Manifest | sha256 of every active file as the same server read it back over 9P | + +The volume covers nested and empty directories, an empty file, a 900,000-byte file +(a two-level pointer tree at 4 KiB blocks), modes 775/755/664/644/600, and a snapshot +whose `hello.txt` keeps the bytes from before the rewrite. `leaf.dat` comes from +`/dev/urandom`, so regenerating changes the image and the manifest together. diff --git a/Compression.Tests/Fossil/ReferenceVectors/fossil.img.gz b/Compression.Tests/Fossil/ReferenceVectors/fossil.img.gz new file mode 100644 index 000000000..d0d89827d Binary files /dev/null and b/Compression.Tests/Fossil/ReferenceVectors/fossil.img.gz differ diff --git a/Compression.Tests/Fossil/ReferenceVectors/fossil.manifest b/Compression.Tests/Fossil/ReferenceVectors/fossil.manifest new file mode 100644 index 000000000..4f98e7d39 --- /dev/null +++ b/Compression.Tests/Fossil/ReferenceVectors/fossil.manifest @@ -0,0 +1,5 @@ +46d7ffce3c46edc763a81f0392aad235724efb76cf34c5007291a25534129c12 active/big.bin +f54cbcdce6015b8729e230193ea636f06eecca9088e86ce7e525a803cd584e7d active/docs/deep/leaf.dat +3ec6319da06f237aca849b1dad73d310556423b4179661dcc25852a118975468 active/docs/notes.txt +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 active/empty +886eafda3a87ef99b654084301c104b9c098a3495cd21c03a495ba067287b1fe active/hello.txt diff --git a/Compression.Tests/Fossil/ReferenceVectors/generate.sh b/Compression.Tests/Fossil/ReferenceVectors/generate.sh new file mode 100644 index 000000000..456aae998 --- /dev/null +++ b/Compression.Tests/Fossil/ReferenceVectors/generate.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Builds fossil.img.gz + fossil.manifest with plan9port (commit 1f13969): flfmt formats a +# 16 MiB volume with 4 KiB blocks, the fossil server (console on a pty via script(1)) +# creates the tree, files are written over 9P with plan9port's 9p client, a local snapshot +# is taken, and hello.txt is rewritten after it. The manifest is what that server reads +# back over 9P: sha256 and path from the Fossil root. +# Usage: generate.sh (expects plan9port built in ~/plan9port) +set -u +out="${1:?usage: generate.sh }"; mkdir -p "$out" +export PLAN9=$HOME/plan9port PATH=$PATH:$HOME/plan9port/bin:$HOME/plan9port/bin/fossil +d=$(mktemp -d); img=$d/fossil.img; port=$((20000 + RANDOM % 20000)); A="tcp!127.0.0.1!$port" +truncate -s 16M $img +flfmt -y -b 4096 $img > /dev/null +cat > $d/cons.txt < $d/fossil.log 2>&1 &) +exec 3> $d/con +for i in $(seq 1 30); do 9p -a "$A" ls / >/dev/null 2>&1 && break; sleep 0.5; done +printf 'hello from fossil\n' | 9p -a "$A" write /hello.txt +yes "fossil pointer tree " | head -c 900000 | 9p -a "$A" write /big.bin # 900,000 bytes: 220 blocks > 204 pointers per 4 KiB block, so depth 2 +printf 'private notes\n' | 9p -a "$A" write /docs/notes.txt +head -c 5000 /dev/urandom | 9p -a "$A" write /docs/deep/leaf.dat +trap '' PIPE +echo "fsys main snap" >&3; sleep 3 +printf 'hello again, after the snapshot was taken\n' | 9p -a "$A" write /hello.txt +mkdir -p $d/x/active/docs/deep +for f in hello.txt empty big.bin docs/notes.txt docs/deep/leaf.dat; do 9p -a "$A" read /$f > $d/x/active/$f; done +{ echo "fsys main sync"; sleep 2; echo "fsys main halt"; } >&3 2>/dev/null; sleep 3 +exec 3>&- 2>/dev/null +pkill -f "$d/cons.txt" 2>/dev/null; sleep 2 +(cd $d/x && find . -type f | sort | sed 's#^\./##' | while read -r f; do echo "$(sha256sum < "$f" | cut -d' ' -f1) $f"; done) > "$out/fossil.manifest" +gzip -9 -n -c $img > "$out/fossil.img.gz" +ls -la "$out"; cat "$out/fossil.manifest" diff --git a/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/FossilFormatDescriptor.cs b/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/FossilFormatDescriptor.cs new file mode 100644 index 000000000..d930386fc --- /dev/null +++ b/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/FossilFormatDescriptor.cs @@ -0,0 +1,77 @@ +#pragma warning disable CS1591 +using Compression.Registry; +using Compression.Registry.Streaming; +using static Compression.Registry.FormatHelpers; + +namespace FileSystem.Fossil; + +/// +/// Fossil — the Plan 9 file server's on-disk format (Quinlan, McKie, Cox, 2002), as plan9port's +/// flfmt and fossil write it. Header magic 0x3776ae89 at 128 KiB. +/// +/// +/// +/// Read-only. The whole local tree is listed from the Fossil root, so the live file system +/// is active/… and local snapshots, if any, are snapshot/…. Blocks already +/// archived to Venti are refused, because there is no Venti here to fetch them from. +/// +/// +/// Proof: volumes made by plan9port's flfmt and filled by its fossil server +/// over 9P read byte-identical to what that server returns. Sourcing: rung 1 — converted +/// from plan9port's MIT-licensed fossil sources (THIRD-PARTY-NOTICE.Fossil.txt). No writer: +/// a writer must keep the label partition, epochs and tags consistent, and is not yet +/// proven against fossil. +/// +/// +public sealed class FossilFormatDescriptor : IFormatDescriptor, IArchiveFormatOperations { + public string Id => "Fossil"; + public string DisplayName => "Fossil (Plan 9)"; + public FormatCategory Category => FormatCategory.Archive; + public FormatCapabilities Capabilities => + FormatCapabilities.CanList | FormatCapabilities.CanExtract | FormatCapabilities.CanTest | + FormatCapabilities.SupportsMultipleEntries | FormatCapabilities.SupportsDirectories; + public string DefaultExtension => ".fossil"; + public IReadOnlyList Extensions => [".fossil"]; + public IReadOnlyList CompoundExtensions => []; + public IReadOnlyList MagicSignatures => [ + new([0x37, 0x76, 0xAE, 0x89, 0x00, 0x01], Offset: FossilReader.HeaderOffset, Confidence: 0.90), + ]; + public IReadOnlyList Methods => [new("stored", "Stored")]; + public string? TarCompressionFormatId => null; + public AlgorithmFamily Family => AlgorithmFamily.Archive; + public string Description => "Plan 9 Fossil file server volume (local blocks); read-only, Venti-archived blocks refused."; + + public List List(Stream stream, string? password) { + var r = new FossilReader(stream); + return r.Entries.Select((e, i) => new ArchiveEntryInfo(i, e.Path, e.Size, e.Size, "Stored", e.IsDirectory, false, + DateTime.UnixEpoch.AddSeconds(e.MTime))).ToList(); + } + + public void Extract(Stream stream, string outputDir, string? password, string[]? files) { + var r = new FossilReader(stream); + foreach (var e in r.Entries) { + if (files != null && !MatchesFilter(e.Path, files)) continue; + if (e.IsDirectory) { + Directory.CreateDirectory(Path.Combine(outputDir, e.Path.Replace('/', Path.DirectorySeparatorChar))); + continue; + } + WriteFile(outputDir, e.Path, r.Extract(e)); + } + } + + public Stream OpenEntry(Stream archive, string entryName, string? password) { + var bytes = this.ExtractEntryToMemory(archive, entryName, password); + return new BoundedEntryStream(new MemoryStream(bytes, writable: false), bytes.Length, leaveOpen: false); + } + + public byte[] ExtractEntryToMemory(Stream archive, string entryName, string? password) { + ArgumentNullException.ThrowIfNull(archive); + ArgumentNullException.ThrowIfNull(entryName); + if (archive.CanSeek) archive.Position = 0; + var r = new FossilReader(archive); + var name = entryName.Replace('\\', '/').TrimStart('/'); + var e = r.Entries.FirstOrDefault(x => !x.IsDirectory && x.Path == name) + ?? throw new FileNotFoundException($"Fossil volume has no file '{entryName}'.", entryName); + return r.Extract(e); + } +} diff --git a/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/FossilReader.cs b/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/FossilReader.cs new file mode 100644 index 000000000..678f18cf3 --- /dev/null +++ b/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/FossilReader.cs @@ -0,0 +1,262 @@ +using System.Buffers.Binary; +using System.Text; + +namespace FileSystem.Fossil; + +/// One file or directory of a Fossil volume. +/// Slash-separated path from the Fossil root (active/…, snapshot/…). +/// True for a directory. +/// File size from the data source's entry. +/// Plan 9 mode word: permission bits plus ModeDir (1<<15) and friends. +/// Owner. +/// Group. +/// Modification time (UNIX seconds). +/// The entry of the file's data source. +public sealed record FossilEntry(string Path, bool IsDirectory, long Size, uint Mode, string Uid, string Gid, uint MTime, FossilSourceEntry Source); + +/// A source ("stream") entry as stored in a directory block (40 bytes). +public readonly record struct FossilSourceEntry(uint Gen, ushort PSize, ushort DSize, byte Flags, byte Depth, long Size, uint Address, bool IsLocal, bool IsZero, uint Tag, uint Snap) { + /// Entry in use. + public bool IsActive => (this.Flags & 0x01) != 0; + /// Source holds entries (a directory stream). + public bool IsDir => (this.Flags & 0x02) != 0; +} + +/// +/// Reads a Fossil volume (Plan 9's file server, as flfmt lays it out and the +/// fossil server writes it): header at 128 KiB, superblock, label and data +/// partitions; files are trees of local blocks addressed by their block number. Blocks +/// already archived to Venti are refused — this reader has no Venti. See +/// docs/FOSSIL-ON-DISK.md. +/// +/// +/// Converted from plan9port's MIT-licensed fossil sources (pack.c, vac.c, +/// source.c, file.c); see THIRD-PARTY-NOTICE.Fossil.txt. +/// +public sealed class FossilReader { + public const uint HeaderMagic = 0x3776AE89, SuperMagic = 0x2340A3B1, MetaMagic = 0x5656FC7A, DirMagic = 0x1C4D9072; + public const int HeaderOffset = 128 * 1024, ScoreSize = 20, EntrySize = 40, MaxDepth = 7; + public const uint ModeDir = 1u << 15, ModeLink = 1u << 14, ModeSnapshot = 1u << 20; + + /// SHA-1 of the empty string: Venti's score of a zero-length block. + private static readonly byte[] ZeroScore = Convert.FromHexString("DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"); + + private readonly Stream _stream; + + /// Bytes per block. + public int BlockSize { get; } + + /// First block of the data partition. + public uint DataStart { get; } + + /// End of the data partition. + public uint DataEnd { get; } + + /// Superblock epochs. + public (uint Low, uint High) Epochs { get; } + + /// Block address of the active root. + public uint Active { get; } + + /// Superblock name (the volume label). + public string Name { get; } + + /// Every file and directory, parents first. + public IReadOnlyList Entries { get; } + + /// Opens a Fossil volume; throws for anything it cannot read faithfully. + public FossilReader(Stream stream) { + ArgumentNullException.ThrowIfNull(stream); + this._stream = stream; + if (stream.Length < HeaderOffset + 512) throw new InvalidDataException("Image too short for a Fossil header at 128 KiB."); + var h = this.Read(HeaderOffset, 512); + if (U32(h, 0) != HeaderMagic) throw new InvalidDataException("Not a Fossil volume: header magic 0x3776ae89 missing at 128 KiB."); + if (U16(h, 4) != 1) throw new InvalidDataException($"Fossil header version {U16(h, 4)} is not 1."); + this.BlockSize = U16(h, 6); + var super = U32(h, 8); + this.DataStart = U32(h, 16); + this.DataEnd = U32(h, 20); + if (this.BlockSize < 512 || this.BlockSize % 512 != 0 || this.DataEnd <= this.DataStart) + throw new InvalidDataException("Fossil header geometry is invalid."); + + var s = this.Read((long)super * this.BlockSize, 512); + if (U32(s, 0) != SuperMagic || U16(s, 4) != 1) throw new InvalidDataException("Fossil superblock magic or version is wrong."); + this.Epochs = (U32(s, 6), U32(s, 10)); + this.Active = U32(s, 22); + this.Name = CString(s.AsSpan(54, 128)); + + // The active root block holds one entry: the root source. Its first three entries are + // the root directory's entry stream, its meta stream, and the meta block naming the root. + var rootBlock = this.ReadBlock(this.Active); + var root = ParseEntry(rootBlock, 0); + if (!root.IsActive || !root.IsDir) throw new InvalidDataException("Fossil active root source is not an active directory."); + var dirSource = this.SourceEntry(root, 0); + var metaSource = this.SourceEntry(root, 1); + + var entries = new List(); + this.Walk(dirSource, metaSource, "", entries, 0); + this.Entries = entries; + } + + /// Reads a file's bytes; refuses blocks that are not local or lie outside the data partition. + public byte[] Extract(FossilEntry entry) { + ArgumentNullException.ThrowIfNull(entry); + if (entry.IsDirectory) return []; + return this.ReadSource(entry.Source); + } + + private void Walk(FossilSourceEntry dir, FossilSourceEntry meta, string prefix, List entries, int depth) { + // A directory loop shows up as unbounded nesting. + if (depth > 64) throw new InvalidDataException("Fossil directory nesting beyond 64 levels (a loop?)."); + var metaData = this.ReadSource(meta); + for (var off = 0; off < metaData.Length; off += meta.DSize) { + var mb = metaData.AsSpan(off, Math.Min(meta.DSize, metaData.Length - off)); + foreach (var de in ParseMetaBlock(mb)) { + var path = prefix.Length == 0 ? de.Elem : prefix + "/" + de.Elem; + var isDir = (de.Mode & ModeDir) != 0; + var src = this.SourceEntry(dir, de.Entry); + if (!src.IsActive || src.Gen != de.Gen) + throw new InvalidDataException($"Fossil '{path}': its source entry {de.Entry} is inactive or of another generation."); + entries.Add(new FossilEntry(path, isDir, isDir ? 0 : src.Size, de.Mode, de.Uid, de.Gid, de.MTime, src)); + if (!isDir) continue; + var msrc = this.SourceEntry(dir, de.MEntry); + if (!msrc.IsActive || msrc.Gen != de.MGen) + throw new InvalidDataException($"Fossil '{path}': its meta source entry {de.MEntry} is inactive or of another generation."); + this.Walk(src, msrc, path, entries, depth + 1); + } + } + } + + private sealed record DirEntry(string Elem, uint Entry, uint Gen, uint MEntry, uint MGen, string Uid, string Gid, uint MTime, uint Mode); + + /// DirEntries of one MetaBlock, in index order (vac.c mbUnpack/meUnpack/deUnpack). + private static List ParseMetaBlock(ReadOnlySpan p) { + var list = new List(); + if (p.Length < 12 || p.IndexOfAnyExcept((byte)0) < 0) return list; // an all-zero block holds no entries + var magic = U32(p, 0); + if (magic != MetaMagic && magic != MetaMagic - 1) throw new InvalidDataException("Fossil meta block magic is wrong."); + int size = U16(p, 4), maxIndex = U16(p, 8), nIndex = U16(p, 10); + var omin = 12 + maxIndex * 4; + if (size > p.Length || p.Length < omin || nIndex > maxIndex) throw new InvalidDataException("Fossil meta block header is inconsistent."); + for (var i = 0; i < nIndex; ++i) { + int eo = U16(p, 12 + i * 4), en = U16(p, 14 + i * 4); + if (eo < omin || eo + en > size || en < 8) throw new InvalidDataException("Fossil meta block index points outside the block."); + list.Add(ParseDirEntry(p.Slice(eo, en))); + } + return list; + } + + private static DirEntry ParseDirEntry(ReadOnlySpan p) { + if (U32(p, 0) != DirMagic) throw new InvalidDataException("Fossil directory entry magic is wrong."); + var version = U16(p, 4); + if (version is < 7 or > 9) throw new InvalidDataException($"Fossil directory entry version {version} is unknown."); + var o = 6; + var elem = Str(p, ref o); + var entry = U32(p, o); o += 4; + uint gen = 0, mentry = entry + 1, mgen = 0; + if (version >= 9) { gen = U32(p, o); mentry = U32(p, o + 4); mgen = U32(p, o + 8); o += 12; } + o += 8; // qid + if (version == 7) o += ScoreSize; // replacement score + var uid = Str(p, ref o); + var gid = Str(p, ref o); + _ = Str(p, ref o); // mid + if (o + 20 > p.Length) throw new InvalidDataException("Fossil directory entry is truncated."); + var mtime = U32(p, o); + var mode = U32(p, o + 16); + if (elem.Length == 0 || elem is "." or ".." || elem.Contains('/')) throw new InvalidDataException($"Fossil directory entry has an unusable name '{elem}'."); + return new(elem, entry, gen, mentry, mgen, uid, gid, mtime, mode); + } + + private static string Str(ReadOnlySpan p, ref int o) { + if (o + 2 > p.Length) throw new InvalidDataException("Fossil directory entry string is truncated."); + int n = U16(p, o); + if (o + 2 + n > p.Length) throw new InvalidDataException("Fossil directory entry string is truncated."); + var s = Encoding.UTF8.GetString(p.Slice(o + 2, n)); + o += 2 + n; + return s; + } + + /// Entry of a directory source. + private FossilSourceEntry SourceEntry(FossilSourceEntry dir, uint index) { + if (!dir.IsDir) throw new InvalidDataException("Fossil: entry lookup in a source that is not a directory."); + var epb = (uint)(dir.DSize / EntrySize); + if (epb == 0) throw new InvalidDataException("Fossil directory source has a data block smaller than one entry."); + var block = this.SourceBlock(dir, index / epb); + return ParseEntry(block, (int)(index % epb)); + } + + private static FossilSourceEntry ParseEntry(ReadOnlySpan b, int index) { + var p = b.Slice(index * EntrySize, EntrySize); + var flags = p[8]; + var depth = (byte)((flags >> 2) & 7); + var size = (long)U16(p, 14) << 32 | U32(p, 16); + var local = (flags & 0x20) != 0; + if (local) return new(U32(p, 0), U16(p, 4), U16(p, 6), (byte)(flags & ~0x1C), depth, size, U32(p, 36), true, false, U32(p, 32), U32(p, 28)); + var score = p.Slice(20, ScoreSize); + var zero = score.SequenceEqual(ZeroScore) || score.IndexOfAnyExcept((byte)0) < 0; + // A non-local score with 16 leading zeros is still a local block address (globalToLocal). + var addr = score[..16].IndexOfAnyExcept((byte)0) < 0 ? U32(score, 16) : uint.MaxValue; + return new(U32(p, 0), U16(p, 4), U16(p, 6), (byte)(flags & ~0x1C), depth, size, addr, addr != uint.MaxValue && !zero, zero, 0, 0); + } + + /// All bytes of a source (its entry's size). + private byte[] ReadSource(FossilSourceEntry e) { + if (e.Size < 0 || e.Size > int.MaxValue) throw new InvalidDataException("Fossil source is too large to buffer."); + var data = new byte[e.Size]; + if (e.Size == 0) return data; + if (e.DSize == 0) throw new InvalidDataException("Fossil source with data has a zero block size."); + var blocks = (e.Size + e.DSize - 1) / e.DSize; + for (long bn = 0; bn < blocks; ++bn) { + var block = this.SourceBlock(e, (uint)bn); + var at = bn * e.DSize; + var n = (int)Math.Min(e.DSize, e.Size - at); + block.AsSpan(0, Math.Min(n, block.Length)).CopyTo(data.AsSpan((int)at)); + } + return data; + } + + /// Block of a source, walking its pointer tree (source.c _sourceBlock). + private byte[] SourceBlock(FossilSourceEntry e, uint bn) { + var np = e.PSize / ScoreSize; + if (e.Depth > MaxDepth || (e.Depth > 0 && np == 0)) throw new InvalidDataException("Fossil source pointer tree is malformed."); + var index = new int[MaxDepth + 1]; + var levels = 0; + for (var b = bn; b > 0; b /= (uint)np) { + if (levels >= e.Depth) throw new InvalidDataException($"Fossil block {bn} lies beyond the source's depth {e.Depth}."); + index[levels++] = (int)(b % (uint)np); + } + if (e.IsZero) return new byte[e.DSize]; + if (!e.IsLocal) throw new InvalidDataException("Fossil source lives on Venti; this reader has no Venti connection."); + var block = this.ReadBlock(e.Address); + for (var level = e.Depth - 1; level >= 0; --level) { + var score = block.AsSpan(index[level] * ScoreSize, ScoreSize); + if (score.SequenceEqual(ZeroScore) || score.IndexOfAnyExcept((byte)0) < 0) + return new byte[level == 0 ? e.DSize : e.PSize]; + if (score[..16].IndexOfAnyExcept((byte)0) >= 0) + throw new InvalidDataException("Fossil pointer refers to a Venti block; this reader has no Venti connection."); + block = this.ReadBlock(U32(score, 16)); + } + return block; + } + + private byte[] ReadBlock(uint addr) { + if (addr >= this.DataEnd - this.DataStart) throw new InvalidDataException($"Fossil block {addr} lies outside the data partition."); + return this.Read(((long)this.DataStart + addr) * this.BlockSize, this.BlockSize); + } + + private byte[] Read(long offset, int count) { + if (offset + count > this._stream.Length) throw new InvalidDataException($"Fossil read at {offset} lies past the end of the image."); + var buf = new byte[count]; + this._stream.Position = offset; + this._stream.ReadExactly(buf); + return buf; + } + + private static string CString(ReadOnlySpan b) { + var end = b.IndexOf((byte)0); + return Encoding.UTF8.GetString(end < 0 ? b : b[..end]); + } + + private static uint U32(ReadOnlySpan b, int at) => BinaryPrimitives.ReadUInt32BigEndian(b[at..]); + private static ushort U16(ReadOnlySpan b, int at) => BinaryPrimitives.ReadUInt16BigEndian(b[at..]); +} diff --git a/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/THIRD-PARTY-NOTICE.Fossil.txt b/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/THIRD-PARTY-NOTICE.Fossil.txt new file mode 100644 index 000000000..ce3ef4e6a --- /dev/null +++ b/Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/THIRD-PARTY-NOTICE.Fossil.txt @@ -0,0 +1,27 @@ +FossilReader.cs is converted from the Fossil file server in plan9port: + + https://github.com/9fans/plan9port src/cmd/fossil (pack.c, vac.c, vac.h, source.c, file.c, dat.h) + revision 1f1396913fb5372408f442ed26933f5ae4b399cd + +Copyright © 2021 Plan 9 Foundation +Portions Copyright © 2001-2008 Russ Cox +Portions Copyright © 2008-2009 Google Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Hawkynt.FileFormats.FileSystems/README.md b/Hawkynt.FileFormats.FileSystems/README.md index 6bee1a597..4ee792651 100644 --- a/Hawkynt.FileFormats.FileSystems/README.md +++ b/Hawkynt.FileFormats.FileSystems/README.md @@ -115,6 +115,7 @@ The `Id`, `State` and verb columns are read off the descriptors by `FilesystemRe | [ext1](https://en.wikipedia.org/wiki/Extended_file_system) | `Ext1` | R/W | ✅ | ✅ moving | — | ✅ | ✅ | — | ✅ | [`dumpe2fs`](https://github.com/tytso/e2fsprogs) accepts as ext2 | 1992 ext, magic `0xEF51`; no `mkfs.ext1` exists | [Extended file system](https://en.wikipedia.org/wiki/Extended_file_system) | | [ext2/3/4](https://en.wikipedia.org/wiki/Ext4) | `Ext` | R/W | ✅ | ✅ moving | — | ✅ | ✅ | — | ✅ | [`e2fsck -fn`](https://github.com/tytso/e2fsprogs) / `dumpe2fs` / `debugfs` + reverse `mke2fs` | ext2 / ext3 / ext4; add/remove anywhere in the tree and every defrag mode except interleave run in place, never rebuilt | [ext4 documentation](https://www.kernel.org/doc/html/latest/filesystems/ext4/) | | [F2FS](https://en.wikipedia.org/wiki/F2FS) | `F2fs` | R/W | ✅ | ✅ moving | — | ✅ | ✅ | — | ✅ | host-kernel `f2fs` mount r/w + [`fsck.f2fs`](https://git.kernel.org/pub/scm/linux/kernel/git/jaegeuk/f2fs-tools.git) | — | [F2FS documentation](https://www.kernel.org/doc/html/latest/filesystems/f2fs.html) | +| [Fossil (Plan 9)](https://en.wikipedia.org/wiki/Fossil_(file_system)) | `Fossil` | R | — | — | — | — | — | — | — | volume formatted by plan9port's `flfmt` and filled by its `fossil` server over 9P (incl. a snapshot) reads byte-identical to what that server returns | Whole local tree from the Fossil root (`active/…`, `snapshot/…`); blocks archived to Venti are refused. No writer yet | [plan9port fossil](https://github.com/9fans/plan9port/tree/master/src/cmd/fossil), [on-disk notes](https://github.com/Hawkynt/CompressionWorkbench/blob/main/docs/FOSSIL-ON-DISK.md) | | [GFS (Sistina/Red Hat, original)](https://en.wikipedia.org/wiki/GFS2) | `Gfs1` | R/W | ✅ | ✅ moving | — | ✅ | ✅ | — | ✅ | own reader + struct-parity tests; the Linux `gfs2` driver rejects GFS1 | Sistina GFS | [GFS2](https://www.kernel.org/doc/html/latest/filesystems/gfs2.html) | | [GFS2 (Global File System 2)](https://en.wikipedia.org/wiki/GFS2) | `Gfs2` | R/W | ✅ | ✅ moving | — | ✅ | ✅ | — | ✅ | [`fsck.gfs2 -n`](https://pagure.io/gfs2-utils) + `debugfs.gfs2`; `mkfs.gfs2` image read by the package | Standalone `lock_nolock`; stuffed + indirect-tree files; edits rebuild while preserving image-size floor, lock table, UUID and file dinode attributes (mode, uid/gid, timestamps, policy flags) | [GFS2](https://www.kernel.org/doc/html/latest/filesystems/gfs2.html) | | [HAMMER (DragonFly BSD)](https://en.wikipedia.org/wiki/HAMMER_(file_system)) | `Hammer` | R/W | ✅ | ✅ moving | — | ✅ | ✅ | — | ✅ | DragonFly kernel mount r/w under QEMU + [`hammer show` / `hammer checkmap`](https://www.dragonflybsd.org/hammer/) | Single volume, about 1 GB minimum; edits rebuild the volume | [HAMMER](https://www.dragonflybsd.org/hammer/) | @@ -284,7 +285,7 @@ ops.Remove(image, ["var/log/old.log"]); -Every public and protected member of all 886 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/CompressionWorkbench/blob/main/Hawkynt.FileFormats.FileSystems/REFERENCE.md). +Every public and protected member of all 890 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/CompressionWorkbench/blob/main/Hawkynt.FileFormats.FileSystems/REFERENCE.md). diff --git a/Hawkynt.FileFormats.FileSystems/REFERENCE.md b/Hawkynt.FileFormats.FileSystems/REFERENCE.md index 8c41326ac..21439cab7 100644 --- a/Hawkynt.FileFormats.FileSystems/REFERENCE.md +++ b/Hawkynt.FileFormats.FileSystems/REFERENCE.md @@ -7080,6 +7080,104 @@ Builds Microsoft Xbox / Xbox 360 FATX filesystem images from scratch per the rev | `Build` | `byte[] Build(int sectorsPerCluster = 0, uint volumeId = 0)` | Builds a complete FATX image containing the previously-added files. | | `WriteTo` | `void WriteTo(Stream output, int sectorsPerCluster = 0, uint volumeId = 0)` | Writes the image into `output`: the superblock and FAT, then every cluster payload at its offset. Only a non-seekable target has to materialise the image, so a seekable one is bounded by the disk rather than by what a byte[] can address. | +### Namespace `FileSystem.Fossil` + +[`FossilEntry`](#fossilentry) · [`FossilFormatDescriptor`](#fossilformatdescriptor) · [`FossilReader`](#fossilreader) · [`FossilSourceEntry`](#fossilsourceentry) + +#### `FossilEntry` + +One file or directory of a Fossil volume. + +Implements `IEquatable`. + +| Member | Signature | Summary | +| --- | --- | --- | +| `FossilEntry` | `FossilEntry(string Path, bool IsDirectory, long Size, uint Mode, string Uid, string Gid, uint MTime, FossilSourceEntry Source)` | One file or directory of a Fossil volume. | +| `Gid` | `string Gid { get; init; }` | Group. | +| `IsDirectory` | `bool IsDirectory { get; init; }` | True for a directory. | +| `MTime` | `uint MTime { get; init; }` | Modification time (UNIX seconds). | +| `Mode` | `uint Mode { get; init; }` | Plan 9 mode word: permission bits plus `ModeDir` (1<<15) and friends. | +| `Path` | `string Path { get; init; }` | Slash-separated path from the Fossil root (`active/…`, `snapshot/…`). | +| `Size` | `long Size { get; init; }` | File size from the data source's entry. | +| `Source` | `FossilSourceEntry Source { get; init; }` | The entry of the file's data source. | +| `Uid` | `string Uid { get; init; }` | Owner. | + +#### `FossilFormatDescriptor` + +Fossil — the Plan 9 file server's on-disk format (Quinlan, McKie, Cox, 2002), as plan9port's `flfmt` and `fossil` write it. Header magic `0x3776ae89` at 128 KiB. + +Implements `IArchiveFormatOperations`, `IFormatDescriptor`. + +| Member | Signature | Summary | +| --- | --- | --- | +| `FossilFormatDescriptor` | `FossilFormatDescriptor()` | | +| `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; }` | | +| `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)` | | +| `OpenEntry` | `Stream OpenEntry(Stream archive, string entryName, string password)` | | + +#### `FossilReader` + +Reads a Fossil volume (Plan 9's file server, as `flfmt` lays it out and the `fossil` server writes it): header at 128 KiB, superblock, label and data partitions; files are trees of local blocks addressed by their block number. Blocks already archived to Venti are refused — this reader has no Venti. See `docs/FOSSIL-ON-DISK.md`. + +| Member | Signature | Summary | +| --- | --- | --- | +| `FossilReader` | `FossilReader(Stream stream)` | Opens a Fossil volume; throws `InvalidDataException` for anything it cannot read faithfully. | +| `DirMagic` | `const uint DirMagic` | | +| `EntrySize` | `const int EntrySize` | | +| `HeaderMagic` | `const uint HeaderMagic` | | +| `HeaderOffset` | `const int HeaderOffset` | | +| `MaxDepth` | `const int MaxDepth` | | +| `MetaMagic` | `const uint MetaMagic` | | +| `ModeDir` | `const uint ModeDir` | | +| `ModeLink` | `const uint ModeLink` | | +| `ModeSnapshot` | `const uint ModeSnapshot` | | +| `ScoreSize` | `const int ScoreSize` | | +| `SuperMagic` | `const uint SuperMagic` | | +| `Active` | `uint Active { get; }` | Block address of the active root. | +| `BlockSize` | `int BlockSize { get; }` | Bytes per block. | +| `DataEnd` | `uint DataEnd { get; }` | End of the data partition. | +| `DataStart` | `uint DataStart { get; }` | First block of the data partition. | +| `Entries` | `IReadOnlyList Entries { get; }` | Every file and directory, parents first. | +| `Epochs` | `ValueTuple Epochs { get; }` | Superblock epochs. | +| `Name` | `string Name { get; }` | Superblock name (the volume label). | +| `Extract` | `byte[] Extract(FossilEntry entry)` | Reads a file's bytes; refuses blocks that are not local or lie outside the data partition. | + +#### `FossilSourceEntry` + +A source ("stream") entry as stored in a directory block (40 bytes). + +Implements `IEquatable`. + +| Member | Signature | Summary | +| --- | --- | --- | +| `FossilSourceEntry` | `FossilSourceEntry(uint Gen, ushort PSize, ushort DSize, byte Flags, byte Depth, long Size, uint Address, bool IsLocal, bool IsZero, uint Tag, uint Snap)` | A source ("stream") entry as stored in a directory block (40 bytes). | +| `Address` | `uint Address { get; init; }` | | +| `DSize` | `ushort DSize { get; init; }` | | +| `Depth` | `byte Depth { get; init; }` | | +| `Flags` | `byte Flags { get; init; }` | | +| `Gen` | `uint Gen { get; init; }` | | +| `IsActive` | `bool IsActive { get; }` | Entry in use. | +| `IsDir` | `bool IsDir { get; }` | Source holds entries (a directory stream). | +| `IsLocal` | `bool IsLocal { get; init; }` | | +| `IsZero` | `bool IsZero { get; init; }` | | +| `PSize` | `ushort PSize { get; init; }` | | +| `Size` | `long Size { get; init; }` | | +| `Snap` | `uint Snap { get; init; }` | | +| `Tag` | `uint Tag { get; init; }` | | + ### Namespace `FileSystem.Gemdos` [`GemdosBpb`](#gemdosbpb) · [`GemdosExtentMap`](#gemdosextentmap) · [`GemdosFormatDescriptor`](#gemdosformatdescriptor) · [`GemdosInPlaceModifier`](#gemdosinplacemodifier) · [`GemdosOptimizer`](#gemdosoptimizer) · [`GemdosOptimizer.GemdosGeometry`](#gemdosoptimizergemdosgeometry) · [`GemdosReader`](#gemdosreader) · [`GemdosWriter`](#gemdoswriter) diff --git a/docs/FOSSIL-ON-DISK.md b/docs/FOSSIL-ON-DISK.md new file mode 100644 index 000000000..b5f1410c8 --- /dev/null +++ b/docs/FOSSIL-ON-DISK.md @@ -0,0 +1,87 @@ +# Fossil on disk + +Fossil is the Plan 9 file server (Quinlan, McKie and Cox, 2002). It keeps the active file +system and its local snapshots in one partition of copy-on-write blocks, and can archive +snapshots to Venti. This page describes what `FileSystem.Fossil` reads. + +**Sources (rung 1).** The fossil sources in plan9port +(, `src/cmd/fossil`, revision `1f13969`) are under the +MIT licence since the Plan 9 Foundation relicensed Plan 9 in 2021. The reader converts +their unpack routines (`pack.c`, `vac.c`) and their block walk (`source.c`, `file.c`). +Notice: `THIRD-PARTY-NOTICE.Fossil.txt` beside the code. + +All integers are big-endian. + +## Header (byte 131072, 512 bytes) + +| Offset | Size | Field | +| --- | --- | --- | +| 0 | 4 | magic `0x3776ae89` | +| 4 | 2 | version, 1 | +| 6 | 2 | block size | +| 8 | 4 | block number of the superblock | +| 12 | 4 | first block of the label partition | +| 16 | 4 | first block of the data partition (the end of the labels) | +| 20 | 4 | end of the data partition | + +Data block *a* lives at byte `(data + a) × blockSize`. + +## Superblock (512 bytes) + +`magic 0x2340a3b1` (4), version 1 (2), epochLow (4), epochHigh (4), next qid (8), +**active** (4, the block holding the active root), next (4), current (4), the last archived +score (20), and the name (128). + +## Entries ("sources") — 40 bytes + +| Offset | Size | Field | +| --- | --- | --- | +| 0 | 4 | gen | +| 4 | 2 | psize: pointer block size (holds psize/20 scores) | +| 6 | 2 | dsize: data block size | +| 8 | 1 | flags: 0x01 active, 0x02 directory (the source holds entries), bits 2–4 depth, 0x20 local, 0x80 no-archive | +| 14 | 6 | size in bytes (48-bit) | +| 20 | 20 | score when not local | +| 27 / 28 / 32 / 36 | 1 / 4 / 4 / 4 | local entries only: archive flag, snapshot epoch, tag, block address | + +A score with 16 leading zero bytes is a local block number in its last 4 bytes. +`DA39A3EE…0709`, the SHA-1 of nothing, is an empty block. Any other score is a Venti block. + +**Block *bn* of a source:** the entry names its top block. With `np = psize/20`, write *bn* +in base *np*. Starting from the top block, the depth−1 … 0 digits pick the score in each +pointer block. The leaf is a data block, `dsize` bytes, cut to the source size. + +## Directory tree + +The block at `active` holds one entry: the root source. Its entries 0, 1 and 2 are the +root directory's entry stream, its meta stream, and a meta stream naming the root itself. + +A meta stream is a sequence of `dsize` MetaBlocks. A MetaBlock starts with magic +`0x5656fc7a` (or one less, for the old sort order), then size, free, maxindex and nindex +(2 bytes each). Next comes an index of (offset, length) pairs, then the DirEntries. + +A DirEntry has magic `0x1c4d9072` (4), version 7–9 (2), the element name (2-byte length + +bytes), entry (4), and gen, mentry and mgen (4 each, from version 9). Then come the qid +(8), a 20-byte replacement score in version 7 only, and uid, gid and mid (strings). The +mtime, mcount, ctime, atime and mode (4 each) follow, then optional sections. + +A child is entry `entry` of its parent's entry stream, and its `gen` must match. A directory +also has a meta stream: entry `mentry`, generation `mgen`. Mode bit `1<<15` marks a +directory, and `1<<20` a snapshot. + +The Fossil root contains `active` (the live tree) and `archive`. After `snap` it also +contains `snapshot/YYYY/MMDD/HHMM`, whose sources carry the snapshot epoch. + +## What the implementation does with it + +It reads the whole local tree, files and directories with modes, owners and mtimes, +snapshots included. Entries must be active and of the right generation, every block must +lie in the data partition, and nesting is bounded. A Venti score is refused, because there +is no Venti here. Read-only: a writer must keep labels, epochs and tags consistent and is +not proven against `fossil` yet. + +Checked against a volume formatted by `flfmt` from plan9port and filled by plan9port's +`fossil` server over 9P. It holds a two-level pointer tree, nested and empty directories, +and a snapshot taken before a rewrite. It reads back byte-identical to what that server +returned. `flchk` was not usable as an oracle: without Venti it reports errors even on a +freshly formatted volume. diff --git a/docs/LICENCE-PROVENANCE.md b/docs/LICENCE-PROVENANCE.md index ca50e4c15..ba5a7df42 100644 --- a/docs/LICENCE-PROVENANCE.md +++ b/docs/LICENCE-PROVENANCE.md @@ -31,7 +31,7 @@ package carries; everything else keeps a `THIRD-PARTY-NOTICE..txt` beside | Class | Upstream rows | Notes | |---|---|---| -| A | 18 | | +| A | 19 | | | B | 17 | FFmpeg alone covers 38 codec folders | | C | 3 | specifications: `docs/CVF-CODECS-ON-DISK.md` (rewrite already under way), `docs/IT-SAMPLE-COMPRESSION-SPEC.md`, `docs/FDS-EXPANSION-SOUND-SPEC.md` | | R | 8 | | @@ -70,6 +70,7 @@ package carries; everything else keeps a `THIRD-PARTY-NOTICE..txt` beside | Claim site | Upstream | Licence | Where the notice is | |---|---|---|---| +| `Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/FossilReader.cs` | plan9port Fossil `pack.c`, `vac.c`, `vac.h`, `source.c`, `file.c`, `dat.h`, revision `1f1396913fb5372408f442ed26933f5ae4b399cd` | MIT | `Hawkynt.FileFormats.FileSystems/FileSystems/FileSystem.Fossil/THIRD-PARTY-NOTICE.Fossil.txt` | | `Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.AndroidBootImage/AndroidBootImage.cs`, `Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.AndroidBootImage/AndroidBootImageMetadata.cs` | AOSP mkbootimg `bootimg.h`, `mkbootimg.py` and `unpack_bootimg.py`, revision `d2bb0af5ba6d3198a3e99529c97eda1be0b5a093` | BSD-3-Clause and Apache-2.0 | `Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.AndroidBootImage/THIRD-PARTY-NOTICE.mkbootimg.txt` | | `Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.AndroidVbmeta/AvbVbmeta.cs`, `Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.AndroidVbmeta/AvbDescriptorText.cs` | AOSP Android Verified Boot headers and `avbtool.py`, revision `c5066a96caa7bf4150c0a8cc8cc14ab81733fdc7` | MIT | `Hawkynt.FileFormats.FileSystems/FileFormats/FileFormat.AndroidVbmeta/THIRD-PARTY-NOTICE.avb.txt` | | `Compression.Core/Dictionary/Lzbitmap/` | libzbitmap (Ernesto A. Fernández, Corellium) | MIT | `THIRD-PARTY-NOTICE.libzbitmap.txt` | diff --git a/docs/catalogue/our-registry.csv b/docs/catalogue/our-registry.csv index 9512a342d..c9b2dcc87 100644 --- a/docs/catalogue/our-registry.csv +++ b/docs/catalogue/our-registry.csv @@ -187,6 +187,7 @@ format,FlacArchive,FLAC (archive view),Audio,,,Y,Y,, format,Fli,FLI/FLC (Autodesk animation),Archive,,.fli;.flc,Y,Y,, format,Flv,Flash Video (FLV),Video,,.flv,Y,Y,Y, format,Fmod,FMOD Sample Bank,Archive,,.fsb,Y,Y,, +format,Fossil,Fossil (Plan 9),Archive,Y,.fossil,Y,Y,, format,FreeArc,FreeArc,Archive,,.arc,Y,Y,Y,Y format,Freeze,Freeze,Stream,,.f;.freeze,,Y,Y, format,Fsg,FSG (Win32 PE),Archive,,,Y,Y,, diff --git a/docs/catalogue/reference-formats.csv b/docs/catalogue/reference-formats.csv index c842b846e..02baac7c8 100644 --- a/docs/catalogue/reference-formats.csv +++ b/docs/catalogue/reference-formats.csv @@ -405,7 +405,7 @@ ext3cow,,filesystem,,,,,,,Y,1,,,,,,MISSING,, FAT8 (Disk BASIC),,filesystem,,,,,,,Y,1,,,,,,MISSING,, FATX (Xbox),fatx;xbox,filesystem,,,,,,,Y,1,,Fatx,Y,Y,Y,OK,, "UFS / FFS (BSD), UFS2",ufs;ufs1,filesystem,,,,,,,Y,1,,Ufs,Y,Y,Y,PARTIAL,,UFS1 only; UFS2 (magic 0x19540119) is not recognised -Fossil (Plan 9),,filesystem,,,,,,,Y,1,,,,,,MISSING,, +Fossil (Plan 9),,filesystem,,,,,,,Y,1,https://github.com/9fans/plan9port/tree/master/src/cmd/fossil,Fossil,Y,,,READ-ONLY-BUT-SOURCES-WRITE,flfmt,writer not yet proven against the fossil server Files-11 (ODS-1/2/5),ods2;ods5;ods-1;ods-2;ods-5;openvms,filesystem,,,,,,,Y,1,,OpenVms;Ods1,Y,Y,Y,OK,, FLEX file system,,filesystem,,,,,,,Y,1,,,,,,MISSING,, HAMMER,,filesystem,,,,,,,Y,1,,Hammer,Y,Y,Y,OK,, diff --git a/screenshots/main-window.png b/screenshots/main-window.png index bd3934451..e3795c0d0 100644 Binary files a/screenshots/main-window.png and b/screenshots/main-window.png differ