feat(bake): publish validated pak artifacts atomically

This commit is contained in:
Erik 2026-07-24 13:40:58 +02:00
parent 90b378cc70
commit 999201cca7
9 changed files with 362 additions and 14 deletions

View file

@ -15,6 +15,13 @@ public static class PakFormat {
/// with an accompanying reader migration path.
/// </summary>
public const uint CurrentFormatVersion = 1;
/// <summary>
/// Identity of the bake algorithm that produced the payloads. Version 2
/// introduces content-identity EnvCell blobs with ordinary TOC aliases.
/// The binary format remains version 1.
/// </summary>
public const uint CurrentBakeToolVersion = 2;
}
/// <summary>
@ -29,7 +36,7 @@ public static class PakFormat {
/// 20 4 languageIteration
/// 24 8 tocOffset (u64)
/// 32 4 tocCount (u32)
/// 36 4 bakeToolVersion = 1
/// 36 4 bakeToolVersion
/// 40 24 reserved (zero)
/// </code>
/// Spec: docs/superpowers/plans/2026-07-05-mp1b-pak-and-bake.md "Format v1 (normative)".

View file

@ -96,11 +96,7 @@ public sealed class PakReader : IDisposable {
// blob region must sit fully between the header and the TOC, and
// its offset must survive the (long) cast the accessor needs.
ref readonly var entry = ref _toc[i];
bool invalid =
entry.Offset < PakHeader.Size ||
entry.Offset > long.MaxValue ||
entry.Offset + entry.Length > Header.TocOffset ||
entry.Offset + entry.Length > (ulong)_fileLength;
bool invalid = !IsEntryRangeValid(entry);
if (invalid) {
_entryVerdictByTocIndex[i] = 0;
LogCorruptionOnce(i, $"TOC entry out of bounds (offset={entry.Offset}, length={entry.Length}, " +
@ -177,6 +173,47 @@ public sealed class PakReader : IDisposable {
return _toc[index];
}
/// <summary>
/// Validates the complete immutable artifact before an offline bake
/// publishes it. This is intentionally stricter than runtime lookup:
/// TOC order/ranges and the CRC of every unique physical blob must all be
/// valid. Aliases are checked once per shared receipt.
/// </summary>
public void ValidateComplete() {
var validatedReceipts = new HashSet<(ulong Offset, uint Length, uint Crc32)>();
ulong previousKey = 0;
for (int i = 0; i < _toc.Length; i++) {
ref readonly var entry = ref _toc[i];
if (i > 0 && entry.Key <= previousKey) {
throw new InvalidDataException(
$"pak TOC is not strictly sorted at entry {i}: " +
$"0x{entry.Key:X16} follows 0x{previousKey:X16}");
}
previousKey = entry.Key;
if (!IsEntryRangeValid(entry)) {
throw new InvalidDataException(
$"pak TOC entry 0x{entry.Key:X16} has an invalid range " +
$"(offset={entry.Offset}, length={entry.Length}, " +
$"file={_fileLength}, toc@{Header.TocOffset})");
}
var receipt = (entry.Offset, entry.Length, entry.Crc32);
if (!validatedReceipts.Add(receipt))
continue;
var bytes = new byte[entry.Length];
_accessor.ReadArray((long)entry.Offset, bytes, 0, (int)entry.Length);
uint actualCrc = Crc32.Compute(bytes);
if (actualCrc != entry.Crc32) {
throw new InvalidDataException(
$"pak blob 0x{entry.Key:X16} failed CRC validation: " +
$"expected 0x{entry.Crc32:X8}, got 0x{actualCrc:X8}");
}
}
}
/// <summary>O(n) linear scan, used only to cross-check the binary search in tests.</summary>
public bool DebugLinearScanContainsKey(ulong key) {
for (int i = 0; i < _toc.Length; i++) {
@ -222,6 +259,19 @@ public sealed class PakReader : IDisposable {
return -1;
}
private bool IsEntryRangeValid(in PakTocEntry entry) {
ulong fileLength = (ulong)_fileLength;
if (entry.Offset < PakHeader.Size ||
entry.Offset > long.MaxValue ||
entry.Offset > Header.TocOffset ||
entry.Offset > fileLength) {
return false;
}
return entry.Length <= Header.TocOffset - entry.Offset &&
entry.Length <= fileLength - entry.Offset;
}
public void Dispose() {
_accessor.Dispose();
_mmf.Dispose();

View file

@ -30,6 +30,7 @@ public sealed class PakWriter : IDisposable {
// (the default-0 footgun; PakReader rejects any version but
// PakFormat.CurrentFormatVersion).
_headerTemplate.FormatVersion = PakFormat.CurrentFormatVersion;
_headerTemplate.BakeToolVersion = PakFormat.CurrentBakeToolVersion;
// Write a header placeholder now; finalized in Finish() once TocOffset/TocCount are known.
_headerTemplate.WriteTo(_stream);