Skip to content

Commit 6625848

Browse files
authored
Merge pull request #17 from kduma-OSS/claude/pcf-partition-table-header-5QdNX
PCF: add end-of-file Trailer + chain direction; adopt in PFS-MS (Rust)
2 parents 8d1e86f + 0a5faf9 commit 6625848

33 files changed

Lines changed: 1907 additions & 116 deletions

File tree

.github/workflows/ci-pfs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
- name: Inspect generated test vector
7070
run: |
7171
ls -l pfs_ms_testvector.bin
72-
test "$(wc -c < pfs_ms_testvector.bin)" = "2986"
72+
test "$(wc -c < pfs_ms_testvector.bin)" = "3066"
7373
- uses: actions/upload-artifact@v4
7474
with:
7575
name: pfs-ms-testvector

implementations/dotnet/src/Pcf/Constants.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,30 @@ public static class Constants
4949

5050
/// <summary>The NIL UID (all zero). MUST NOT label a live partition.</summary>
5151
public static readonly byte[] NilUid = new byte[UidSize];
52+
53+
/// <summary>
54+
/// Sentinel value of <c>partition_table_offset</c> (header offset 12) meaning
55+
/// the partition-table head is recorded in the file <see cref="Trailer"/> at
56+
/// the end of the file rather than in the header (spec section 4, "File
57+
/// Trailer"). The all-ones u64 can never be a real offset, so it is
58+
/// unambiguous.
59+
/// </summary>
60+
public const ulong PtOffsetTrailer = 0xFFFF_FFFF_FFFF_FFFF;
61+
62+
/// <summary>Fixed size of the optional file trailer, in bytes.</summary>
63+
public const long TrailerSize = 20;
64+
65+
/// <summary>
66+
/// Trailer signature, 8 bytes: the file <see cref="Magic"/> reversed
67+
/// (<c>0x1A 0x0A 0x0D 'T' 'R' 'P' 'K' 0x89</c>). Placed as the final 8 bytes
68+
/// of the file so a reader can detect and validate the trailer at the end.
69+
/// </summary>
70+
public static readonly byte[] TrailerMagic =
71+
{ 0x1A, 0x0A, 0x0D, (byte)'T', (byte)'R', (byte)'P', (byte)'K', 0x89 };
72+
73+
/// <summary>Chain-direction flag: forward chain, head = first block.</summary>
74+
public const byte ChainForward = 0;
75+
76+
/// <summary>Chain-direction flag: backward chain, head = last/newest block.</summary>
77+
public const byte ChainBackward = 1;
5278
}

implementations/dotnet/src/Pcf/Container.cs

Lines changed: 141 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ private sealed class BlockInfo
4242
private uint _defaultCapacity;
4343
private HashAlgo _tableHashAlgo;
4444

45+
/// <summary>
46+
/// Resolved absolute offset of the partition-table head: the header pointer
47+
/// for a classic file, or the offset from the file <see cref="Trailer"/>
48+
/// when the header holds <see cref="Constants.PtOffsetTrailer"/>. 0 denotes
49+
/// an empty table.
50+
/// </summary>
51+
private ulong _tableHead;
52+
53+
/// <summary>Chain-direction flags resolved at open time (see <see cref="Trailer"/>).</summary>
54+
private byte _chainFlags;
55+
4556
private Container(Stream storage)
4657
{
4758
_storage = storage;
@@ -100,10 +111,22 @@ public static Container CreateWith(Stream storage, uint firstBlockCapacity, Hash
100111
};
101112
c._defaultCapacity = cap;
102113
c._tableHashAlgo = tableHashAlgo;
114+
c._tableHead = (ulong)Constants.HeaderSize;
115+
c._chainFlags = Constants.ChainForward;
103116
return c;
104117
}
105118

106-
/// <summary>Open an existing container, validating the header (spec C1, C2).</summary>
119+
/// <summary>
120+
/// Open an existing container, validating the header (spec C1, C2).
121+
///
122+
/// <para>When the header's <c>partition_table_offset</c> is the
123+
/// <see cref="Constants.PtOffsetTrailer"/> sentinel, the partition-table head
124+
/// and chain direction are read from the file <see cref="Trailer"/> (located
125+
/// by scanning backward from the end of the file). Chain traversal is
126+
/// identical in both directions (follow <c>next_table_offset</c> until 0);
127+
/// the direction only conveys which end is newest, exposed via
128+
/// <see cref="ChainIsBackward"/>.</para>
129+
/// </summary>
107130
public static Container Open(Stream storage)
108131
{
109132
var c = new Container(storage)
@@ -117,8 +140,18 @@ public static Container Open(Stream storage)
117140
c.ReadAt(0, hb, 20);
118141
c._header = FileHeader.FromBytes(hb);
119142

143+
if (c._header.PartitionTableOffset == Constants.PtOffsetTrailer)
144+
{
145+
(c._tableHead, c._chainFlags) = c.LocateTrailer();
146+
}
147+
else
148+
{
149+
c._tableHead = c._header.PartitionTableOffset;
150+
c._chainFlags = Constants.ChainForward;
151+
}
152+
120153
var blocks = new List<BlockInfo>();
121-
ulong off = c._header.PartitionTableOffset;
154+
ulong off = c._tableHead;
122155
while (off != 0)
123156
{
124157
(TableBlockHeader h, _) = c.ReadBlock(off);
@@ -144,9 +177,83 @@ public static Container Open(Stream storage)
144177
/// <summary>The backing store.</summary>
145178
public Stream Storage => _storage;
146179

147-
/// <summary>The parsed file header.</summary>
180+
/// <summary>
181+
/// The parsed file header. In trailer mode its <c>PartitionTableOffset</c>
182+
/// holds the <see cref="Constants.PtOffsetTrailer"/> sentinel; use
183+
/// <see cref="TableHead"/> for the resolved head.
184+
/// </summary>
148185
public FileHeader Header => _header;
149186

187+
/// <summary>
188+
/// The resolved absolute offset of the partition-table head (0 if empty).
189+
/// This is the value to follow regardless of header-pointer vs trailer mode.
190+
/// </summary>
191+
public ulong TableHead => _tableHead;
192+
193+
/// <summary>
194+
/// Whether the chain is backward-linked (head = newest block,
195+
/// <c>next_table_offset</c> points at the previous/older block). Classic
196+
/// header-pointer files are always forward.
197+
/// </summary>
198+
public bool ChainIsBackward => (_chainFlags & 1) != 0;
199+
200+
/// <summary>
201+
/// Locate the most recent valid file trailer by scanning backward from the
202+
/// end of the file for the last 20-byte window ending in
203+
/// <see cref="Constants.TrailerMagic"/> whose recorded head is empty (0) or
204+
/// references a parseable table block. Bytes after that trailer — an
205+
/// incomplete or aborted append — are ignored, which gives append-only
206+
/// writers crash recovery for free. In the clean case the trailer is the
207+
/// final <see cref="Constants.TrailerSize"/> bytes.
208+
/// </summary>
209+
private (ulong, byte) LocateTrailer()
210+
{
211+
long fileLen = _storage.Seek(0, SeekOrigin.End);
212+
var tb = new byte[20];
213+
long end = fileLen;
214+
while (end >= Constants.TrailerSize)
215+
{
216+
long start = end - Constants.TrailerSize;
217+
ReadAt((ulong)start, tb, 20);
218+
bool magicOk = true;
219+
for (int i = 0; i < 8; i++)
220+
{
221+
if (tb[12 + i] != Constants.TrailerMagic[i])
222+
{
223+
magicOk = false;
224+
break;
225+
}
226+
}
227+
if (magicOk)
228+
{
229+
Trailer t = Trailer.FromBytes(tb);
230+
if (t.PartitionTableOffset == 0)
231+
{
232+
return (0, t.ChainFlags);
233+
}
234+
// Guard against the magic appearing inside an aborted tail: the
235+
// recorded head must precede this trailer and parse as a block.
236+
if (start >= Constants.TableHeaderSize
237+
&& t.PartitionTableOffset <= (ulong)(start - Constants.TableHeaderSize))
238+
{
239+
try
240+
{
241+
var bhb = new byte[74];
242+
ReadAt(t.PartitionTableOffset, bhb, 74);
243+
TableBlockHeader.FromBytes(bhb);
244+
return (t.PartitionTableOffset, t.ChainFlags);
245+
}
246+
catch (PcfException)
247+
{
248+
// Spurious magic in an aborted tail; keep scanning.
249+
}
250+
}
251+
}
252+
end -= 1;
253+
}
254+
throw PcfException.BadTrailer();
255+
}
256+
150257
// ---- low-level I/O ----------------------------------------------------
151258

152259
private void ReadAt(ulong off, byte[] buf, int len)
@@ -217,7 +324,7 @@ private void WriteBlock(ulong off, ulong next, HashAlgo algo, IReadOnlyList<Part
217324
public List<PartitionEntry> Entries()
218325
{
219326
var outp = new List<PartitionEntry>();
220-
ulong off = _header.PartitionTableOffset;
327+
ulong off = _tableHead;
221328
while (off != 0)
222329
{
223330
(TableBlockHeader h, List<PartitionEntry> entries) = ReadBlock(off);
@@ -254,7 +361,7 @@ public byte[] ReadPartitionData(PartitionEntry entry)
254361

255362
private (ulong, int, PartitionEntry) Locate(byte[] uid)
256363
{
257-
ulong off = _header.PartitionTableOffset;
364+
ulong off = _tableHead;
258365
while (off != 0)
259366
{
260367
(TableBlockHeader h, List<PartitionEntry> entries) = ReadBlock(off);
@@ -423,7 +530,7 @@ public void RemovePartition(byte[] uid)
423530
/// </summary>
424531
public void Verify()
425532
{
426-
ulong off = _header.PartitionTableOffset;
533+
ulong off = _tableHead;
427534
while (off != 0)
428535
{
429536
(TableBlockHeader h, List<PartitionEntry> entries) = ReadBlock(off);
@@ -465,7 +572,7 @@ public byte[] CompactedImage()
465572
{
466573
// Gather live entries and their data, in chain order.
467574
var live = new List<KeyValuePair<PartitionEntry, byte[]>>();
468-
ulong off = _header.PartitionTableOffset;
575+
ulong off = _tableHead;
469576
while (off != 0)
470577
{
471578
(TableBlockHeader h, List<PartitionEntry> entries) = ReadBlock(off);
@@ -563,6 +670,33 @@ public void CompactInto(Stream output)
563670
output.Write(img, 0, img.Length);
564671
}
565672

673+
// ---- trailer mode -----------------------------------------------------
674+
675+
/// <summary>
676+
/// Convert the file to trailer mode: append a fixed <see cref="Trailer"/> at
677+
/// the end of the file recording the current partition-table head, then
678+
/// overwrite the header's <c>partition_table_offset</c> with the
679+
/// <see cref="Constants.PtOffsetTrailer"/> sentinel so the head is located
680+
/// via that trailer. The chain built by this writer is forward-linked, so the
681+
/// trailer records <see cref="Constants.ChainForward"/>.
682+
/// </summary>
683+
public void FinalizeWithTrailer()
684+
{
685+
var trailer = new Trailer
686+
{
687+
PartitionTableOffset = _tableHead,
688+
ChainFlags = Constants.ChainForward,
689+
};
690+
long pos = _storage.Seek(0, SeekOrigin.End);
691+
byte[] tb = trailer.ToBytes();
692+
WriteAt((ulong)pos, tb, tb.Length);
693+
_header.PartitionTableOffset = Constants.PtOffsetTrailer;
694+
byte[] hb = _header.ToBytes();
695+
WriteAt(0, hb, hb.Length);
696+
_chainFlags = Constants.ChainForward;
697+
_dataEof = (ulong)pos + (ulong)Constants.TrailerSize;
698+
}
699+
566700
// ---- helpers ----------------------------------------------------------
567701

568702
private static uint Clamp(uint v, uint lo, uint hi) => v < lo ? lo : (v > hi ? hi : v);

implementations/dotnet/src/Pcf/PcfError.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public enum PcfError
4444

4545
/// <summary>An attempt was made to add a partition whose UID already exists.</summary>
4646
DuplicateUid,
47+
48+
/// <summary>The header requested trailer-based location but no valid trailer exists.</summary>
49+
BadTrailer,
4750
}
4851

4952
/// <summary>An error raised by a PCF reader or writer operation.</summary>
@@ -93,4 +96,7 @@ internal static PcfException NotFound() =>
9396

9497
internal static PcfException DuplicateUid() =>
9598
new(PcfError.DuplicateUid, "duplicate UID");
99+
100+
internal static PcfException BadTrailer() =>
101+
new(PcfError.BadTrailer, "missing or invalid file trailer");
96102
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
3+
namespace Pcf;
4+
5+
/// <summary>
6+
/// The optional fixed 20-byte file trailer (spec section 4, "File Trailer").
7+
///
8+
/// <para>A trailer is present only when the file header's
9+
/// <c>partition_table_offset</c> holds the <see cref="Constants.PtOffsetTrailer"/>
10+
/// sentinel. It occupies the final <see cref="Constants.TrailerSize"/> bytes of
11+
/// the file and records the real offset of the partition-table head together
12+
/// with the chain direction. Because every append places a fresh trailer at the
13+
/// new end of file, the file's last bytes always point at the newest table —
14+
/// enabling append-only writers with no in-place header rewrite.</para>
15+
/// </summary>
16+
public sealed class Trailer
17+
{
18+
/// <summary>Absolute offset of the partition-table head (0 = empty container).</summary>
19+
public ulong PartitionTableOffset { get; set; }
20+
21+
/// <summary>Chain-direction flags; bit 0 selects forward (0) or backward (1).</summary>
22+
public byte ChainFlags { get; set; }
23+
24+
/// <summary>Serialise to the on-disk 20-byte layout (reserved bytes 9..12 are zero).</summary>
25+
public byte[] ToBytes()
26+
{
27+
var b = new byte[20];
28+
LittleEndian.WriteU64(b, 0, PartitionTableOffset);
29+
b[8] = ChainFlags;
30+
Buffer.BlockCopy(Constants.TrailerMagic, 0, b, 12, 8);
31+
return b;
32+
}
33+
34+
/// <summary>
35+
/// Parse from the on-disk 20-byte layout, validating the trailer magic.
36+
/// Throws <see cref="PcfException"/> (BadTrailer) if the magic does not match.
37+
/// </summary>
38+
public static Trailer FromBytes(byte[] b)
39+
{
40+
if (b.Length < Constants.TrailerSize)
41+
{
42+
throw PcfException.BadTrailer();
43+
}
44+
for (int i = 0; i < 8; i++)
45+
{
46+
if (b[12 + i] != Constants.TrailerMagic[i])
47+
{
48+
throw PcfException.BadTrailer();
49+
}
50+
}
51+
return new Trailer
52+
{
53+
PartitionTableOffset = LittleEndian.ReadU64(b, 0),
54+
ChainFlags = b[8],
55+
};
56+
}
57+
}

0 commit comments

Comments
 (0)