vhdx/log/core.rs
1//! VHDX Log Section Parser
2//!
3//! Implements zero-copy parsing of the VHDX log ring buffer.
4//! The log consists of variable-sized entries (4KB aligned), each containing
5//! a header, descriptors, and data sectors.
6
7use std::borrow::Cow;
8use std::fmt;
9use std::sync::OnceLock;
10
11use crate::constants::{
12 DESCRIPTOR_SIZE, ENTRY_HEADER_SIZE, SECTOR_SIZE, SIGNATURE_DESC, SIGNATURE_LOGE, SIGNATURE_ZERO,
13};
14use crate::error::{Error, Result, SignaturePosition};
15use crate::types::{Crc32c, Guid};
16
17// ---------------------------------------------------------------------------
18// Log layout
19// ---------------------------------------------------------------------------
20// Log
21// ---------------------------------------------------------------------------
22
23/// View into the VHDX log ring buffer.
24///
25/// The log is a circular buffer stored contiguously at a location specified
26/// in the VHDX header. It consists of variable-sized entries that are at
27/// least 4 KB aligned.
28#[derive(Clone, Copy)]
29pub struct Log<'a> {
30 data: &'a [u8],
31}
32
33impl<'a> Log<'a> {
34 /// Create a new `Log` view over the raw log buffer.
35 ///
36 /// The buffer length must be a multiple of 4 KB (MB-aligned on disk,
37 /// but we just need 4 KB alignment for entry parsing).
38 ///
39 /// # Errors
40 ///
41 /// Returns `Error::LogEntryCorrupted` if the buffer size is not a
42 /// multiple of 4 KB.
43 pub(crate) fn new(data: &'a [u8]) -> Result<Self> {
44 if !data.len().is_multiple_of(SECTOR_SIZE as usize) {
45 return Err(Error::LogEntryCorrupted(
46 "log buffer size is not a multiple of 4KB".into(),
47 ));
48 }
49 Ok(Self { data })
50 }
51
52 /// Return the total size of the log buffer in bytes.
53 #[must_use]
54 pub(crate) fn len(&self) -> usize {
55 self.data.len()
56 }
57
58 /// Return `true` if the log buffer is empty.
59 #[must_use]
60 #[cfg(test)]
61 pub(crate) fn is_empty(&self) -> bool {
62 self.data.is_empty()
63 }
64
65 /// Get a log entry by index (0-based, scanning from the start of the buffer).
66 ///
67 /// Entries are located by walking the buffer: entry 0 starts at offset 0,
68 /// entry 1 starts at `entry0.header().entry_length()` bytes from the start, etc.
69 ///
70 /// # Errors
71 ///
72 /// Returns an error if the index is out of bounds or an entry is malformed.
73 pub fn entry(&self, index: usize) -> Result<Entry<'_>> {
74 let mut offset: usize = 0;
75 for i in 0..=index {
76 if offset >= self.data.len() {
77 return Err(Error::InvalidParameter(format!(
78 "log entry index {index} out of bounds"
79 )));
80 }
81 if i == index {
82 return self.parse_entry_at(offset);
83 }
84 // Skip this entry by reading its length from the header
85 let entry_length = u32_at(&self.data[offset + 8..offset + 12]).ok_or_else(|| {
86 Error::LogEntryCorrupted("log buffer too small for entry header".into())
87 })?;
88 if entry_length == 0 || !(entry_length as usize).is_multiple_of(SECTOR_SIZE as usize) {
89 return Err(Error::LogEntryCorrupted(format!(
90 "invalid entry length {entry_length} at index {i}"
91 )));
92 }
93 offset += entry_length as usize;
94 }
95 Err(Error::InvalidParameter(format!(
96 "log entry index {index} not found"
97 )))
98 }
99
100 /// Parse a log entry at a specific byte offset within the log buffer.
101 ///
102 /// The offset must be 4KB-aligned and the entry must be fully contained
103 /// within the buffer.
104 ///
105 /// # Errors
106 ///
107 /// Returns errors from [`parse_entry_at`](Self::parse_entry_at):
108 /// `Error::InvalidSignature` if the entry signature is not `"loge"`,
109 /// `Error::LogEntryCorrupted` if the entry length is invalid or the
110 /// entry extends beyond the buffer.
111 pub(crate) fn entry_at(&self, offset: usize) -> Result<Entry<'_>> {
112 self.parse_entry_at(offset)
113 }
114
115 /// Iterate over all valid log entries in the buffer.
116 ///
117 /// Scans entries sequentially from the beginning. Stops when the buffer
118 /// is exhausted or an invalid entry is encountered.
119 pub fn entries(&self) -> impl Iterator<Item = Entry<'_>> + '_ {
120 LogEntryIter {
121 log: self,
122 offset: 0,
123 done: false,
124 }
125 }
126
127 /// Parse an entry starting at `offset` within the log buffer.
128 fn parse_entry_at(&self, offset: usize) -> Result<Entry<'_>> {
129 if offset + ENTRY_HEADER_SIZE as usize > self.data.len() {
130 return Err(Error::LogEntryCorrupted(
131 "insufficient data for log entry header".into(),
132 ));
133 }
134 let entry_data = &self.data[offset..];
135
136 // Validate signature
137 let sig = &entry_data[0..4];
138 if sig != SIGNATURE_LOGE.into_inner().to_le_bytes() {
139 let mut found = [0u8; 4];
140 found.copy_from_slice(sig);
141 return Err(Error::InvalidSignature {
142 position: SignaturePosition::LogEntry,
143 expected: crate::error::pad_signature_4to8(
144 SIGNATURE_LOGE.into_inner().to_le_bytes(),
145 ),
146 found: crate::error::pad_signature_4to8(found),
147 });
148 }
149
150 // Read entry_length and validate
151 let entry_length = u32_at(&entry_data[8..12])
152 .ok_or_else(|| Error::LogEntryCorrupted("entry_length read failed".into()))?;
153 if entry_length == 0 || !(entry_length as usize).is_multiple_of(SECTOR_SIZE as usize) {
154 return Err(Error::LogEntryCorrupted(format!(
155 "entry length {entry_length} is not a multiple of 4KB"
156 )));
157 }
158 let total = entry_length as usize;
159 if offset + total > self.data.len() {
160 return Err(Error::LogEntryCorrupted(format!(
161 "entry extends beyond log buffer (offset={offset}, length={total}, buf={})",
162 self.data.len()
163 )));
164 }
165
166 Ok(Entry {
167 data: &entry_data[..total],
168 assembled_sectors: OnceLock::new(),
169 })
170 }
171}
172
173/// Iterator over log entries in a sequential scan.
174struct LogEntryIter<'a> {
175 log: &'a Log<'a>,
176 offset: usize,
177 done: bool,
178}
179
180impl<'a> Iterator for LogEntryIter<'a> {
181 type Item = Entry<'a>;
182
183 fn next(&mut self) -> Option<Self::Item> {
184 if self.done || self.offset >= self.log.data.len() {
185 return None;
186 }
187 // Check if there's room for at least a header
188 if self.offset + ENTRY_HEADER_SIZE as usize > self.log.data.len() {
189 self.done = true;
190 return None;
191 }
192 let remaining = &self.log.data[self.offset..];
193 // Check signature — if not "loge", stop
194 if remaining[0..4] != SIGNATURE_LOGE.into_inner().to_le_bytes() {
195 self.done = true;
196 return None;
197 }
198 if let Ok(entry) = self.log.parse_entry_at(self.offset) {
199 let entry_length = entry.header().entry_length() as usize;
200 self.offset += entry_length;
201 Some(entry)
202 } else {
203 self.done = true;
204 None
205 }
206 }
207}
208
209// ---------------------------------------------------------------------------
210// Entry
211// ---------------------------------------------------------------------------
212
213/// A single log entry, containing a header, descriptors, and data sectors.
214#[derive(Debug)]
215pub struct Entry<'a> {
216 /// The full entry data (header + descriptor sectors + data sectors).
217 data: &'a [u8],
218 /// Lazily initialized per-sector `OnceLock` cells. Each cell holds the full
219 /// reassembled 4096-byte sector on first access: `LeadingBytes(8) + Middle(4084) + TrailingBytes(4)`.
220 assembled_sectors: OnceLock<Vec<OnceLock<[u8; SECTOR_SIZE as usize]>>>,
221}
222
223impl<'a> Entry<'a> {
224 /// Return the entry header (first 64 bytes).
225 pub fn header(&self) -> LogEntryHeader<'_> {
226 LogEntryHeader {
227 data: &self.data[..ENTRY_HEADER_SIZE as usize],
228 }
229 }
230
231 /// Get a descriptor by index (0-based).
232 ///
233 /// # Errors
234 ///
235 /// Returns an error if the index is out of range or the descriptor is malformed.
236 pub fn descriptor(&self, index: usize) -> Result<Descriptor<'_>> {
237 let desc_count = self.header().descriptor_count() as usize;
238 if index >= desc_count {
239 return Err(Error::InvalidParameter(format!(
240 "descriptor index {index} out of range (count={desc_count})"
241 )));
242 }
243 let raw = self.descriptor_bytes(index)?;
244 let sig = &raw[0..4];
245 if sig == SIGNATURE_DESC.into_inner().to_le_bytes() {
246 Ok(Descriptor::Data(DataDescriptor { data: raw }))
247 } else if sig == SIGNATURE_ZERO.into_inner().to_le_bytes() {
248 Ok(Descriptor::Zero(ZeroDescriptor { data: raw }))
249 } else {
250 let mut found = [0u8; 4];
251 found.copy_from_slice(sig);
252 Err(Error::LogEntryCorrupted(format!(
253 "LOG_DESCRIPTOR_SIGNATURE_INVALID: unknown signature {found:?} at descriptor {index}"
254 )))
255 }
256 }
257
258 /// Iterate over all descriptors in this entry.
259 ///
260 /// Each item is validated; signature errors produce `Err`.
261 pub fn descriptors(&self) -> impl Iterator<Item = Result<Descriptor<'_>>> + '_ {
262 let count = self.header().descriptor_count() as usize;
263 (0..count).map(|i| self.descriptor(i))
264 }
265
266 /// Iterate over data sectors in this entry.
267 ///
268 /// The number of data sectors equals the number of data descriptors.
269 /// Data sectors start after all descriptor sectors.
270 ///
271 /// On first call, a Vec of empty `OnceLock` cells is initialized (one per
272 /// data descriptor). Each sector is assembled individually on first access
273 /// via `DataSector::data()`.
274 pub fn data(&self) -> impl Iterator<Item = DataSector<'_>> + '_ {
275 // Initialize the Vec of empty OnceLock cells — one per DATA descriptor
276 let caches = self.assembled_sectors.get_or_init(|| {
277 let desc_count = self.header().descriptor_count() as usize;
278 let mut data_count = 0;
279 for di in 0..desc_count {
280 if let Ok(Descriptor::Data(_)) = self.descriptor(di) {
281 data_count += 1;
282 }
283 }
284 (0..data_count).map(|_| OnceLock::new()).collect()
285 });
286
287 let desc_count = self.header().descriptor_count() as usize;
288 // Number of descriptor sectors: first sector holds 64-byte header + 126 descriptors,
289 // subsequent sectors hold 128 descriptors each.
290 let desc_sectors = if desc_count == 0 {
291 1 // first sector still exists with header only
292 } else {
293 let after_first = desc_count.saturating_sub(126);
294 1 + after_first.div_ceil(128)
295 };
296 let data_offset = desc_sectors * SECTOR_SIZE as usize;
297 let entry_length = self.data.len();
298 let raw_data = self.data;
299
300 // Build list of (descriptor_index,) for data descriptors only
301 let mut data_indices: Vec<usize> = Vec::new();
302 for di in 0..desc_count {
303 if let Ok(Descriptor::Data(_)) = self.descriptor(di) {
304 data_indices.push(di);
305 }
306 }
307
308 data_indices
309 .into_iter()
310 .enumerate()
311 .filter_map(move |(sector_idx, di)| {
312 let sector_start = data_offset + sector_idx * SECTOR_SIZE as usize;
313 if sector_start + SECTOR_SIZE as usize > entry_length {
314 return None;
315 }
316 let Ok(Descriptor::Data(desc)) = self.descriptor(di) else {
317 return None;
318 };
319 Some(DataSector {
320 data: &raw_data[sector_start..sector_start + SECTOR_SIZE as usize],
321 leading_bytes: desc.leading_bytes_raw(),
322 trailing_bytes: desc.trailing_bytes_raw(),
323 cache: &caches[sector_idx],
324 })
325 })
326 }
327
328 /// Validate the CRC-32C checksum of this entry.
329 ///
330 /// The checksum covers the entire entry (per `EntryLength`), with the
331 /// Checksum field (bytes 4..8) set to zero during computation.
332 ///
333 /// This method avoids allocation by zeroing the checksum field in place
334 /// (previously used `self.data.to_vec()`).
335 ///
336 /// # Errors
337 ///
338 /// Returns `Error::InvalidChecksum` if the computed CRC-32C does not
339 /// match the stored checksum.
340 pub(crate) fn verify_checksum(&self) -> Result<()> {
341 let stored = self.header().checksum();
342 let computed = Crc32c::from_raw(crate::common::crc32c_zeroed_checksum(self.data));
343
344 if computed != stored {
345 return Err(Error::InvalidChecksum {
346 expected: stored.value(),
347 actual: computed.value(),
348 });
349 }
350 Ok(())
351 }
352
353 /// Get the raw bytes for descriptor at the given index.
354 ///
355 /// Descriptors are laid out starting at byte 64 (after the header).
356 /// First sector: bytes 64..4096 → 126 descriptors (32 bytes each).
357 /// Subsequent sectors: full 128 descriptors each.
358 fn descriptor_bytes(&self, index: usize) -> Result<&'a [u8]> {
359 let abs_offset = if index < 126 {
360 // First descriptor sector: header (64) + index * 32
361 ENTRY_HEADER_SIZE as usize + index * DESCRIPTOR_SIZE as usize
362 } else {
363 // Subsequent descriptor sectors
364 let remaining = index - 126;
365 let sector_index = remaining / 128;
366 let within_sector = remaining % 128;
367 SECTOR_SIZE as usize * (1 + sector_index) + within_sector * DESCRIPTOR_SIZE as usize
368 };
369 if abs_offset + DESCRIPTOR_SIZE as usize > self.data.len() {
370 return Err(Error::LogEntryCorrupted(format!(
371 "descriptor {index} at offset {abs_offset} extends beyond entry"
372 )));
373 }
374 Ok(&self.data[abs_offset..abs_offset + DESCRIPTOR_SIZE as usize])
375 }
376}
377
378// ---------------------------------------------------------------------------
379// LogEntryHeader
380// ---------------------------------------------------------------------------
381
382/// Log entry header (64 bytes).
383///
384/// Layout (MS-VHDX §2.3.1.1):
385/// ```text
386/// [0..4] Signature ("loge")
387/// [4..8] Checksum (CRC-32C)
388/// [8..12] EntryLength
389/// [12..16] Tail
390/// [16..24] SequenceNumber
391/// [24..28] DescriptorCount
392/// [28..32] Reserved
393/// [32..48] LogGuid
394/// [48..56] FlushedFileOffset
395/// [56..64] LastFileOffset
396/// ```
397pub struct LogEntryHeader<'a> {
398 data: &'a [u8],
399}
400
401impl<'a> LogEntryHeader<'a> {
402 /// Entry signature. MUST be `"loge"` (0x65676F6C).
403 ///
404 /// # Panics
405 ///
406 /// Panics if the header slice is shorter than 4 bytes.
407 #[must_use]
408 pub fn signature(&self) -> &'a [u8; 4] {
409 self.data[0..4].try_into().expect("header is 64 bytes")
410 }
411
412 /// CRC-32C checksum computed over the entire entry (checksum field zeroed).
413 #[must_use]
414 pub fn checksum(&self) -> Crc32c {
415 Crc32c::from_raw(u32_at(&self.data[4..8]).unwrap_or(0))
416 }
417
418 /// Total length of the entry in bytes. MUST be a multiple of 4KB.
419 #[must_use]
420 pub fn entry_length(&self) -> u32 {
421 u32_at(&self.data[8..12]).unwrap_or(0)
422 }
423
424 /// Offset from log start to the first entry of the active sequence.
425 /// MUST be a multiple of 4KB.
426 #[must_use]
427 pub fn tail(&self) -> u32 {
428 u32_at(&self.data[12..16]).unwrap_or(0)
429 }
430
431 /// Monotonically increasing sequence number. MUST be > 0.
432 #[must_use]
433 pub fn sequence_number(&self) -> u64 {
434 u64_at(&self.data[16..24]).unwrap_or(0)
435 }
436
437 /// Number of descriptors in this entry.
438 #[must_use]
439 pub fn descriptor_count(&self) -> u32 {
440 u32_at(&self.data[24..28]).unwrap_or(0)
441 }
442
443 /// Reserved. MUST be 0.
444 #[must_use]
445 pub fn reserved(&self) -> u32 {
446 u32_at(&self.data[28..32]).unwrap_or(0)
447 }
448
449 /// `LogGuid` that was present in the file header when this entry was written.
450 #[must_use]
451 pub fn log_guid(&self) -> Guid {
452 let mut bytes = [0u8; 16];
453 bytes.copy_from_slice(&self.data[32..48]);
454 Guid::from_bytes(bytes)
455 }
456
457 /// VHDX file size guaranteed to be stable on disk.
458 #[must_use]
459 pub fn flushed_file_offset(&self) -> u64 {
460 u64_at(&self.data[48..56]).unwrap_or(0)
461 }
462
463 /// File size that all allocated structures fit into.
464 #[must_use]
465 pub fn last_file_offset(&self) -> u64 {
466 u64_at(&self.data[56..64]).unwrap_or(0)
467 }
468}
469
470// ---------------------------------------------------------------------------
471// Descriptor enum
472// ---------------------------------------------------------------------------
473
474/// A log entry descriptor — either a data or zero descriptor.
475///
476/// The variant is determined by the 4-byte signature:
477/// - `"desc"` → `Data(DataDescriptor)`
478/// - `"zero"` → `Zero(ZeroDescriptor)`
479/// - Any other signature is treated as corruption.
480pub enum Descriptor<'a> {
481 Data(DataDescriptor<'a>),
482 Zero(ZeroDescriptor<'a>),
483}
484
485impl Descriptor<'_> {
486 /// Return the sequence number from the descriptor.
487 #[must_use]
488 pub(crate) fn sequence_number(&self) -> u64 {
489 match self {
490 Descriptor::Data(d) => d.sequence_number(),
491 Descriptor::Zero(z) => z.sequence_number(),
492 }
493 }
494}
495
496impl fmt::Debug for Descriptor<'_> {
497 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
498 match self {
499 Descriptor::Data(d) => f
500 .debug_struct("Descriptor::Data")
501 .field("file_offset", &d.file_offset())
502 .field("sequence_number", &d.sequence_number())
503 .finish(),
504 Descriptor::Zero(z) => f
505 .debug_struct("Descriptor::Zero")
506 .field("file_offset", &z.file_offset())
507 .field("zero_length", &z.zero_length())
508 .field("sequence_number", &z.sequence_number())
509 .finish(),
510 }
511 }
512}
513
514// ---------------------------------------------------------------------------
515// DataDescriptor
516// ---------------------------------------------------------------------------
517
518/// Data descriptor (32 bytes).
519///
520/// Layout (MS-VHDX §2.3.1.3):
521/// ```text
522/// [0..4] DataSignature ("desc")
523/// [4..8] TrailingBytes
524/// [8..16] LeadingBytes
525/// [16..24] FileOffset
526/// [24..32] SequenceNumber
527/// ```
528pub struct DataDescriptor<'a> {
529 data: &'a [u8],
530}
531
532impl<'a> DataDescriptor<'a> {
533 /// Signature. MUST be `"desc"` (0x63736564).
534 ///
535 /// # Panics
536 ///
537 /// Panics if the descriptor slice is shorter than 4 bytes.
538 #[must_use]
539 pub fn signature(&self) -> &'a [u8; 4] {
540 self.data[0..4].try_into().expect("descriptor is 32 bytes")
541 }
542
543 /// Trailing 4 bytes removed from the 4KB sector update.
544 #[must_use]
545 pub fn trailing_bytes(&self) -> u32 {
546 u32_at(&self.data[4..8]).unwrap_or(0)
547 }
548
549 /// Leading 8 bytes removed from the 4KB sector update.
550 #[must_use]
551 pub fn leading_bytes(&self) -> u64 {
552 u64_at(&self.data[8..16]).unwrap_or(0)
553 }
554
555 /// File offset where the data must be written. MUST be 4KB aligned.
556 #[must_use]
557 pub fn file_offset(&self) -> u64 {
558 u64_at(&self.data[16..24]).unwrap_or(0)
559 }
560
561 /// MUST match the entry header's `SequenceNumber`.
562 #[must_use]
563 pub fn sequence_number(&self) -> u64 {
564 u64_at(&self.data[24..32]).unwrap_or(0)
565 }
566
567 /// Return the leading bytes as a raw slice (8 bytes).
568 #[must_use]
569 pub(crate) fn leading_bytes_raw(&self) -> &'a [u8] {
570 &self.data[8..16]
571 }
572
573 /// Return the trailing bytes as a raw slice (4 bytes).
574 #[must_use]
575 pub(crate) fn trailing_bytes_raw(&self) -> &'a [u8] {
576 &self.data[4..8]
577 }
578}
579
580// ---------------------------------------------------------------------------
581// ZeroDescriptor
582// ---------------------------------------------------------------------------
583
584/// Zero descriptor (32 bytes).
585///
586/// Layout (MS-VHDX §2.3.1.2):
587/// ```text
588/// [0..4] ZeroSignature ("zero")
589/// [4..8] Reserved
590/// [8..16] ZeroLength
591/// [16..24] FileOffset
592/// [24..32] SequenceNumber
593/// ```
594pub struct ZeroDescriptor<'a> {
595 data: &'a [u8],
596}
597
598impl<'a> ZeroDescriptor<'a> {
599 /// Signature. MUST be `"zero"` (0x6F72657A).
600 ///
601 /// # Panics
602 ///
603 /// Panics if the descriptor slice is shorter than 4 bytes.
604 #[must_use]
605 pub fn signature(&self) -> &'a [u8; 4] {
606 self.data[0..4].try_into().expect("descriptor is 32 bytes")
607 }
608
609 /// Reserved. MUST be 0.
610 #[must_use]
611 pub fn reserved(&self) -> u32 {
612 u32_at(&self.data[4..8]).unwrap_or(0)
613 }
614
615 /// Length of the section to zero. MUST be 4KB aligned.
616 #[must_use]
617 pub fn zero_length(&self) -> u64 {
618 u64_at(&self.data[8..16]).unwrap_or(0)
619 }
620
621 /// File offset to zero. MUST be 4KB aligned.
622 #[must_use]
623 pub fn file_offset(&self) -> u64 {
624 u64_at(&self.data[16..24]).unwrap_or(0)
625 }
626
627 /// MUST match the entry header's `SequenceNumber`.
628 #[must_use]
629 pub fn sequence_number(&self) -> u64 {
630 u64_at(&self.data[24..32]).unwrap_or(0)
631 }
632}
633
634// ---------------------------------------------------------------------------
635// DataSector
636// ---------------------------------------------------------------------------
637
638/// Data sector (4096 bytes).
639///
640/// Layout (MS-VHDX §2.3.1.4):
641/// ```text
642/// [0..4] DataSignature ("data")
643/// [4..8] SequenceHigh (high 4 bytes of SequenceNumber)
644/// [8..4092] Data (4084 bytes — middle portion of original sector)
645/// [4092..4096] SequenceLow (low 4 bytes of SequenceNumber)
646/// ```
647pub struct DataSector<'a> {
648 /// Raw sector bytes in the log buffer (4096 bytes).
649 pub(super) data: &'a [u8],
650 /// Leading 8 bytes from the data descriptor.
651 leading_bytes: &'a [u8],
652 /// Trailing 4 bytes from the data descriptor.
653 trailing_bytes: &'a [u8],
654 /// Per-sector lazy cache for the assembled 4096-byte sector.
655 cache: &'a OnceLock<[u8; SECTOR_SIZE as usize]>,
656}
657
658impl<'a> DataSector<'a> {
659 /// Signature. MUST be `"data"` (0x61746164).
660 ///
661 /// # Panics
662 ///
663 /// Panics if the data sector slice is shorter than 4 bytes.
664 #[must_use]
665 pub fn signature(&self) -> &'a [u8; 4] {
666 self.data[0..4]
667 .try_into()
668 .expect("data sector is 4096 bytes")
669 }
670
671 /// The reconstructed full 64-bit sequence number.
672 #[must_use]
673 pub fn sequence_number(&self) -> u64 {
674 let high = u32::from_le_bytes(self.data[4..8].try_into().unwrap_or([0; 4]));
675 let low = u32::from_le_bytes(self.data[4092..4096].try_into().unwrap_or([0; 4]));
676 (u64::from(high) << 32) | u64::from(low)
677 }
678
679 /// Return the assembled full 4096-byte sector.
680 ///
681 /// The assembled data is: `LeadingBytes(8B) + middle(4084B) + TrailingBytes(4B)`.
682 /// Lazily assembled on first access via per-sector `OnceLock` cache.
683 #[must_use]
684 pub fn data(&self) -> Cow<'a, [u8]> {
685 let assembled = self.cache.get_or_init(|| {
686 let mut buf = [0u8; SECTOR_SIZE as usize];
687 buf[0..8].copy_from_slice(&self.leading_bytes[..8]);
688 buf[8..4092].copy_from_slice(&self.data[8..4092]);
689 buf[4092..4096].copy_from_slice(&self.trailing_bytes[..4]);
690 buf
691 });
692 Cow::Borrowed(assembled)
693 }
694}
695
696// ---------------------------------------------------------------------------
697// DataSectorAssembly — helper for assembling full 4096-byte sectors
698// ---------------------------------------------------------------------------
699
700/// An assembled data sector containing the full 4096 bytes.
701///
702/// Created by pairing a `DataSector` with a `DataDescriptor` to reconstruct
703/// the original sector: `LeadingBytes(8) + Data(4084) + TrailingBytes(4)`.
704///
705/// This type is kept `pub(crate)` for cross-validation in tests only.
706/// Normal code should use `DataSector::data()` which returns a borrowed
707/// view into the Entry's lazily-assembled buffer.
708#[cfg(test)]
709pub(crate) struct DataSectorAssembly {
710 buf: [u8; SECTOR_SIZE as usize],
711}
712
713#[cfg(test)]
714impl DataSectorAssembly {
715 /// Assemble a full 4096-byte sector from a data sector and its descriptor.
716 ///
717 /// The assembled data is: `LeadingBytes(8B) + DataSector middle(4084B) + TrailingBytes(4B)`.
718 pub fn new(descriptor: &DataDescriptor<'_>, sector: &DataSector<'_>) -> Self {
719 let mut buf = [0u8; SECTOR_SIZE as usize];
720 // Leading 8 bytes
721 buf[0..8].copy_from_slice(descriptor.leading_bytes_raw());
722 // Middle 4084 bytes
723 buf[8..4092].copy_from_slice(§or.data[8..4092]);
724 // Trailing 4 bytes
725 buf[4092..4096].copy_from_slice(descriptor.trailing_bytes_raw());
726 Self { buf }
727 }
728
729 /// Return the assembled 4096-byte sector.
730 #[must_use]
731 pub fn data(&self) -> &[u8] {
732 &self.buf
733 }
734}
735
736// ---------------------------------------------------------------------------
737// Helpers
738// ---------------------------------------------------------------------------
739
740/// Read a little-endian u32 from a 4-byte slice.
741fn u32_at(buf: &[u8]) -> Option<u32> {
742 if buf.len() < 4 {
743 return None;
744 }
745 Some(u32::from_le_bytes(buf[..4].try_into().unwrap()))
746}
747
748/// Read a little-endian u64 from an 8-byte slice.
749fn u64_at(buf: &[u8]) -> Option<u64> {
750 if buf.len() < 8 {
751 return None;
752 }
753 Some(u64::from_le_bytes(buf[..8].try_into().unwrap()))
754}