feat(pipeline): MP1b - PakWriter + mmap PakReader
TDD: PakRoundTripTests written first, confirmed a compile failure against the not-yet-existing PakWriter/PakReader types. PakWriter streams [header placeholder][64-byte-aligned blobs][TOC sorted by key], then seeks back and finalizes the header once TocOffset/TocCount are known (plan: "TOC last" so the writer doesn't need blob count up front). PakReader mmaps the whole file once, loads the TOC into a sorted array for O(log n) binary-search lookup, and verifies each blob's CRC-32 LAZILY on first access (cached per-index so a corrupt blob logs exactly once and is thereafter always treated as missing rather than handing back garbage bytes). No locks anywhere — the mapped memory is immutable for the reader's lifetime by construction. CRC-32 implemented directly (standard IEEE 802.3 table-driven variant) rather than adding a System.IO.Hashing package reference, keeping AcDream.Content's dependency surface minimal per its existing "no GL binaries" intent. 9 tests green: header round-trip, every-key-found, blob deep-equality via the Task 3 comparator, missing-key returns false, corrupted-blob (single flipped byte) is detected and treated as missing while leaving sibling blobs unaffected, 64-byte blob alignment, and TOC binary search cross-checked against a linear scan for both present and absent keys.
This commit is contained in:
parent
a5ba435839
commit
981025d58b
4 changed files with 495 additions and 0 deletions
105
src/AcDream.Content/Pak/PakWriter.cs
Normal file
105
src/AcDream.Content/Pak/PakWriter.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace AcDream.Content.Pak;
|
||||
|
||||
/// <summary>
|
||||
/// Streams pak blobs to disk: header placeholder -> 64-byte-aligned blobs ->
|
||||
/// TOC (sorted by key) -> seek back and finalize the header once
|
||||
/// tocOffset/tocCount are known. This lets the writer stream blobs without
|
||||
/// knowing the final count up front (plan: "TOC last").
|
||||
/// </summary>
|
||||
public sealed class PakWriter : IDisposable {
|
||||
private const int Alignment = 64;
|
||||
|
||||
private readonly FileStream _stream;
|
||||
private readonly PakHeader _headerTemplate;
|
||||
private readonly List<PakTocEntry> _tocEntries = new();
|
||||
private readonly HashSet<ulong> _seenKeys = new();
|
||||
private bool _finished;
|
||||
|
||||
public PakWriter(string path, PakHeader headerTemplate) {
|
||||
_stream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||
_headerTemplate = headerTemplate;
|
||||
|
||||
// Write a header placeholder now; finalized in Finish() once TocOffset/TocCount are known.
|
||||
_headerTemplate.WriteTo(_stream);
|
||||
PadToAlignment();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one blob (an <see cref="ObjectMeshData"/> serialized via
|
||||
/// <see cref="ObjectMeshDataSerializer"/>) under <paramref name="key"/>.
|
||||
/// Blobs are written immediately at the current (64-byte-aligned) stream
|
||||
/// position; the writer pads after each blob so the NEXT blob also
|
||||
/// starts aligned.
|
||||
/// </summary>
|
||||
public void AddBlob(ulong key, ObjectMeshData data) {
|
||||
ThrowIfFinished();
|
||||
if (!_seenKeys.Add(key)) {
|
||||
throw new ArgumentException($"duplicate pak key 0x{key:X16}", nameof(key));
|
||||
}
|
||||
|
||||
long offset = _stream.Position;
|
||||
using var ms = new MemoryStream();
|
||||
ObjectMeshDataSerializer.Write(data, ms);
|
||||
var bytes = ms.ToArray();
|
||||
|
||||
_stream.Write(bytes);
|
||||
PadToAlignment();
|
||||
|
||||
_tocEntries.Add(new PakTocEntry {
|
||||
Key = key,
|
||||
Offset = (ulong)offset,
|
||||
Length = (uint)bytes.Length,
|
||||
Crc32 = Content.Pak.Crc32.Compute(bytes),
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Writes the sorted TOC and finalizes the header. Must be called exactly once.</summary>
|
||||
public void Finish() {
|
||||
ThrowIfFinished();
|
||||
_finished = true;
|
||||
|
||||
var sorted = _tocEntries.OrderBy(e => e.Key).ToList();
|
||||
ulong tocOffset = (ulong)_stream.Position;
|
||||
foreach (var entry in sorted) {
|
||||
entry.WriteTo(_stream);
|
||||
}
|
||||
|
||||
var finalHeader = _headerTemplate;
|
||||
finalHeader.TocOffset = tocOffset;
|
||||
finalHeader.TocCount = (uint)sorted.Count;
|
||||
|
||||
_stream.Position = 0;
|
||||
finalHeader.WriteTo(_stream);
|
||||
_stream.Flush();
|
||||
}
|
||||
|
||||
private void PadToAlignment() {
|
||||
long pos = _stream.Position;
|
||||
long remainder = pos % Alignment;
|
||||
if (remainder == 0) return;
|
||||
long pad = Alignment - remainder;
|
||||
Span<byte> zeros = stackalloc byte[(int)pad];
|
||||
zeros.Clear();
|
||||
_stream.Write(zeros);
|
||||
}
|
||||
|
||||
private void ThrowIfFinished() {
|
||||
if (_finished) throw new InvalidOperationException("PakWriter.Finish() has already been called.");
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (!_finished) {
|
||||
// Best-effort finalize so a forgotten Finish() call doesn't leave a
|
||||
// half-written file with a placeholder header claiming 0 entries
|
||||
// while blobs are on disk — surfaced loudly instead via Finish()'s
|
||||
// own invariants on next use, but here we simply flush what exists.
|
||||
try { Finish(); } catch (InvalidOperationException) { /* already finished */ }
|
||||
}
|
||||
_stream.Dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue