@@ -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 ) ;
0 commit comments