feat(bake): publish validated pak artifacts atomically
This commit is contained in:
parent
90b378cc70
commit
999201cca7
9 changed files with 362 additions and 14 deletions
|
|
@ -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)".
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue