Skip to main content

hurray_io/file/sans_io/
read.rs

1//! The random-access read half of the sans-io file core.
2//!
3//! Where the stream core asks for *the next* bytes, this one asks for bytes
4//! `[offset, offset + len)` (ADR-041 § 7). The file is addressed, not consumed: the
5//! trailer sits at `size - 40`, the index is located from it, and tensors are found by
6//! name. That difference in request vocabulary is the whole reason this is a second
7//! machine rather than a mode of the first.
8
9use hurray_core::{CompositeValidator, LayoutDescriptor, TensorDescriptor};
10
11use super::sections::{align_up, parse_index, parse_kv};
12use crate::file::types::{
13    FileComposite, FileItem, FileTensor, IndexEntry, KvValue, DEFAULT_MAX_COMPOSITE_DEPTH,
14};
15use crate::file::{
16    file_flags, FILE_HEADER_SIZE, FILE_MAGIC, MAX_DATA_BUFFER_ALIGNMENT,
17    SUPPORTED_CONTAINER_VERSION_MAJOR, TRAILER_MAGIC, TRAILER_SIZE,
18};
19use crate::stream::{Destination, DestinationAlloc, DestinationRequest, SystemDestinations};
20use crate::{Error, Result};
21
22/// What the core needs next from its driver.
23///
24/// Exactly one request is outstanding at a time, and the driver answers it — with
25/// [`report_filled`][FileReaderCore::report_filled] or one of the `take_*` methods —
26/// before the core produces another (ADR-041 § 4).
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum FileStep {
29    /// Read the bytes at `[offset, offset + len)` into
30    /// [`input_dst`][FileReaderCore::input_dst], then report how many arrived.
31    ///
32    /// Filling fewer than `len` is normal — the remainder is requested again — but `0`
33    /// means the file is shorter than its own index claims, which is a truncated file.
34    Range {
35        /// Absolute offset from the start of the file.
36        offset: u64,
37        /// Bytes still wanted at `offset`. Never `0`.
38        len: usize,
39    },
40    /// The file is open and no job is in flight: call `begin_descriptor`, `begin_tensor`
41    /// or `begin_composite`.
42    Ready,
43    /// A descriptor is ready. Collect it with
44    /// [`take_descriptor`][FileReaderCore::take_descriptor].
45    Descriptor,
46    /// A tensor is ready. Collect it with [`take_tensor`][FileReaderCore::take_tensor].
47    Tensor,
48    /// A composite is ready. Collect it with
49    /// [`take_composite`][FileReaderCore::take_composite].
50    Composite,
51}
52
53/// What the core is working towards.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum Job {
56    /// A descriptor on its own, no buffers read.
57    Descriptor,
58    /// One complete tensor.
59    Tensor,
60    /// A composite: a walk over the tensors that follow its head.
61    Walk,
62}
63
64/// Where the machine is.
65#[derive(Debug, Clone, Copy)]
66enum State {
67    /// Filling the 64-byte file header.
68    Header,
69    /// Filling the 40-byte trailer.
70    Trailer,
71    /// Filling the footer index section.
72    Index,
73    /// Filling the KV metadata section.
74    Kv,
75    /// Open, nothing in flight.
76    Ready,
77    /// Filling a tensor descriptor.
78    Descriptor,
79    /// Filling buffer `index` of the tensor under construction.
80    Buffer { index: usize },
81    /// A result is waiting to be taken.
82    Done(Done),
83    /// The open sequence failed: the file is not a Hurray file, or its footer is
84    /// malformed. Unlike a failed job, this is terminal — there is nothing to retry.
85    Failed,
86}
87
88#[derive(Debug, Clone, Copy)]
89enum Done {
90    Descriptor,
91    Tensor,
92    Composite,
93}
94
95/// One composite head whose members are still arriving (ADR-041 § 10's explicit stack).
96struct Open {
97    name: String,
98    head: TensorDescriptor,
99    member_count: u32,
100    members: Vec<FileItem>,
101    validator: CompositeValidator,
102}
103
104/// A composite traversal in progress.
105///
106/// Membership is recovered by **file-offset adjacency**: the head's declared
107/// `member_count` tensors written immediately after it, recursively. The cursor walks
108/// names ordered by descriptor offset, which is write order — not index order, which
109/// `SORTED_INDEX` permutes.
110struct Walk {
111    names: Vec<String>,
112    cursor: usize,
113    stack: Vec<Open>,
114}
115
116/// The tensor currently being assembled.
117struct Pending<D> {
118    name: String,
119    descriptor: TensorDescriptor,
120    /// Buffers already filled and frozen, in declaration order.
121    buffers: Vec<bytes::Bytes>,
122    /// The destination for the buffer being filled.
123    dest: Option<D>,
124    /// Absolute offset the next buffer starts at.
125    next_offset: u64,
126}
127
128/// Reads a Hurray file from bytes a driver fetches by range.
129///
130/// The core owns the memory every payload byte lands in, so the transport's write into it
131/// is the only copy (ADR-041 § 2), and that memory carries the alignment the buffer handle
132/// *declares* (§ 5).
133///
134/// **Every request is absolute, which is what makes this core unloseable.** The read
135/// position advances only when a count is reported, so a driver that abandons a transfer —
136/// because its future was cancelled, or because a count came back it could not trust —
137/// simply calls [`step`][Self::step] again and is handed the same range. Re-filling a
138/// partly filled destination from the start of the range is idempotent. The stream core
139/// has an [`Unusable`][crate::stream::Unusable] state for a lost byte count; this one needs
140/// none, and for the same reason a job that fails leaves the core usable: the next
141/// `begin_*` starts from the index, not from wherever the file pointer happened to stop.
142///
143/// # Examples
144///
145/// Reading a file with no runtime anywhere — the driver is three `std::io` calls:
146///
147/// ```
148/// use std::io::{Read, Seek, SeekFrom};
149///
150/// use hurray_io::file::{FileReaderCore, FileStep};
151/// # use hurray_core::{
152/// #     BufferHandle, DeviceTag, ElementType, LayoutDescriptor, Shape, SyncMode,
153/// #     TensorDescriptor, MIN_BUFFER_ALIGNMENT,
154/// # };
155/// # use hurray_io::file::{FileWriteOp, FileWriterCore};
156/// # use hurray_io::stream::WriteStep;
157/// #
158/// # // A one-tensor file, built with the write core.
159/// # fn drive(mut op: FileWriteOp<'_>, sink: &mut Vec<u8>) -> hurray_io::Result<()> {
160/// #     while let WriteStep::Output { .. } = op.step()? {
161/// #         let src = op.output_src();
162/// #         sink.extend_from_slice(src);
163/// #         let n = src.len();
164/// #         op.report_written(n)?;
165/// #     }
166/// #     Ok(())
167/// # }
168/// # let handle =
169/// #     BufferHandle::new(256, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced)?;
170/// # let descriptor = TensorDescriptor::new(
171/// #     1, 0, ElementType::Float32, Shape::new(vec![8u64, 8])?, 0,
172/// #     LayoutDescriptor::RowMajor, vec![handle], None, None, None, None,
173/// # )?;
174/// # let payload: Vec<u8> = (0..=255u8).collect();
175/// # let buffers: [&[u8]; 1] = [&payload];
176/// # let mut bytes = Vec::new();
177/// # let mut writer = FileWriterCore::new();
178/// # drive(writer.header()?, &mut bytes)?;
179/// # drive(writer.tensor("embeddings", &descriptor, &buffers)?, &mut bytes)?;
180/// # drive(writer.finish(vec![])?, &mut bytes)?;
181/// let mut file = std::io::Cursor::new(bytes);
182/// let size = file.seek(SeekFrom::End(0))?;
183///
184/// let mut core = FileReaderCore::new(size);
185/// // Open: header, trailer, index, KV.
186/// loop {
187///     match core.step()? {
188///         FileStep::Range { offset, len } => {
189///             file.seek(SeekFrom::Start(offset))?;
190///             let n = file.read(&mut core.input_dst()[..len])?;
191///             core.report_filled(n)?;
192///         }
193///         FileStep::Ready => break,
194///         other => panic!("unexpected {other:?} while opening"),
195///     }
196/// }
197/// assert_eq!(core.index().len(), 1);
198///
199/// // Then one tensor, by name.
200/// core.begin_tensor("embeddings")?;
201/// let tensor = loop {
202///     match core.step()? {
203///         FileStep::Range { offset, len } => {
204///             file.seek(SeekFrom::Start(offset))?;
205///             let n = file.read(&mut core.input_dst()[..len])?;
206///             core.report_filled(n)?;
207///         }
208///         FileStep::Tensor => break core.take_tensor().expect("a tensor is ready"),
209///         other => panic!("unexpected {other:?} while reading"),
210///     }
211/// };
212/// assert_eq!(tensor.buffers[0].as_ref(), &payload[..]);
213/// // The destination carried the declared alignment, so a SIMD load is safe on it.
214/// assert_eq!(tensor.buffers[0].as_ptr() as usize % 64, 0);
215/// # Ok::<(), Box<dyn std::error::Error>>(())
216/// ```
217pub struct FileReaderCore<A: DestinationAlloc = SystemDestinations> {
218    size: u64,
219    data_buffer_alignment: u64,
220    max_composite_depth: usize,
221    destinations: A,
222    state: State,
223    job: Option<Job>,
224    /// Header flags, kept until the index CRC is checked against the trailer's.
225    flags: u32,
226    index: Vec<IndexEntry>,
227    kv: Vec<(String, KvValue)>,
228    /// `(offset, length)` of the KV section, read from the trailer.
229    kv_range: (u64, u32),
230    /// The index CRC-32C the trailer records.
231    stored_crc: u32,
232    /// Absolute offset of the next byte wanted.
233    cursor: u64,
234    /// Bytes the current target wants in total.
235    target: usize,
236    /// Bytes of that target already in place.
237    filled: usize,
238    /// Sections and descriptors land here; payloads never do (§ 2).
239    staging: Vec<u8>,
240    /// The name and data offset of the entry being read.
241    entry_name: String,
242    entry_data_offset: u64,
243    pending: Option<Pending<A::Dest>>,
244    walk: Option<Walk>,
245    descriptor_out: Option<TensorDescriptor>,
246    tensor_out: Option<FileTensor>,
247    composite_out: Option<FileComposite>,
248}
249
250// Manual, because the allocator and its destinations carry no `Debug` bound — and
251// `FileReader` has derived `Debug` since 0.1.0, so the core it now holds needs one.
252impl<A: DestinationAlloc> std::fmt::Debug for FileReaderCore<A> {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        f.debug_struct("FileReaderCore")
255            .field("size", &self.size)
256            .field("state", &self.state)
257            .field("tensors", &self.index.len())
258            .field("kv_entries", &self.kv.len())
259            .finish_non_exhaustive()
260    }
261}
262
263// The default allocator's constructor: `FileReaderCore` with no type argument.
264impl FileReaderCore<SystemDestinations> {
265    /// Creates a core for a file of `size` bytes, reading into host allocations.
266    ///
267    /// The size is a constructor argument rather than a request the core makes: every
268    /// driver already knows it — a seek to the end, a `stat`, or the transport vtable's
269    /// `size` callback — and a size the core cannot act on buys nothing.
270    ///
271    /// # Examples
272    ///
273    /// See the type-level example.
274    pub fn new(size: u64) -> Self {
275        Self::with_destinations(size, SystemDestinations)
276    }
277}
278
279impl<A: DestinationAlloc> FileReaderCore<A> {
280    /// Creates a core whose read destinations come from `destinations`.
281    ///
282    /// The seam ADR-041 left open: a binding may supply the allocation so a read lands
283    /// directly in memory its runtime owns, as long as the destination satisfies the
284    /// alignment the handle declares.
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// use hurray_io::file::FileReaderCore;
290    /// use hurray_io::stream::SystemDestinations;
291    ///
292    /// let core = FileReaderCore::with_destinations(4096, SystemDestinations);
293    /// assert!(!core.is_open());
294    /// ```
295    pub fn with_destinations(size: u64, destinations: A) -> Self {
296        Self {
297            size,
298            // Replaced by the header's value; never used before then.
299            data_buffer_alignment: 1,
300            max_composite_depth: DEFAULT_MAX_COMPOSITE_DEPTH,
301            destinations,
302            state: State::Header,
303            job: None,
304            flags: 0,
305            index: Vec::new(),
306            kv: Vec::new(),
307            kv_range: (0, 0),
308            stored_crc: 0,
309            cursor: 0,
310            target: FILE_HEADER_SIZE as usize,
311            filled: 0,
312            staging: vec![0u8; FILE_HEADER_SIZE as usize],
313            entry_name: String::new(),
314            entry_data_offset: 0,
315            pending: None,
316            walk: None,
317            descriptor_out: None,
318            tensor_out: None,
319            composite_out: None,
320        }
321    }
322
323    /// Sets the maximum composite nesting depth accepted by
324    /// [`begin_composite`][Self::begin_composite]. Default:
325    /// [`DEFAULT_MAX_COMPOSITE_DEPTH`].
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use hurray_io::file::FileReaderCore;
331    ///
332    /// let core = FileReaderCore::new(4096).with_max_composite_depth(8);
333    /// assert!(!core.is_open());
334    /// ```
335    pub fn with_max_composite_depth(mut self, max_composite_depth: usize) -> Self {
336        self.max_composite_depth = max_composite_depth;
337        self
338    }
339
340    /// Advances the machine and returns what it needs next.
341    ///
342    /// Never blocks and never waits: every transition here is a parse, an allocation or
343    /// bookkeeping, which is what lets one core serve a blocking driver and an event loop
344    /// alike (ADR-041 § 4).
345    ///
346    /// # Errors
347    ///
348    /// - [`Error::InvalidFileMagic`], [`Error::UnsupportedContainerVersion`],
349    ///   [`Error::ReservedFileFlagBits`], [`Error::InvalidTrailerMagic`],
350    ///   [`Error::IndexOverrunsTrailer`], [`Error::IndexCrc32cMismatch`],
351    ///   [`Error::DuplicateTensorName`] — the file's own structure is wrong
352    /// - [`Error::Core`] — a descriptor failed to decode
353    /// - [`Error::UnallocatableBuffer`] — a destination the allocator cannot provide
354    /// - [`Error::TornComposite`], [`Error::CompositeNestingTooDeep`],
355    ///   [`Error::NotAComposite`] — a composite walk could not be completed
356    /// - [`Error::CoreFailed`] — the open sequence failed earlier
357    ///
358    /// # Examples
359    ///
360    /// See the type-level example.
361    pub fn step(&mut self) -> Result<FileStep> {
362        loop {
363            match self.state {
364                State::Failed => return Err(Error::CoreFailed),
365                State::Ready => return Ok(FileStep::Ready),
366                State::Done(Done::Descriptor) => return Ok(FileStep::Descriptor),
367                State::Done(Done::Tensor) => return Ok(FileStep::Tensor),
368                State::Done(Done::Composite) => return Ok(FileStep::Composite),
369                State::Header
370                | State::Trailer
371                | State::Index
372                | State::Kv
373                | State::Descriptor
374                | State::Buffer { .. } => {
375                    if self.filled < self.target {
376                        return Ok(FileStep::Range {
377                            offset: self.cursor,
378                            len: self.target - self.filled,
379                        });
380                    }
381                    self.advance()?;
382                }
383            }
384        }
385    }
386
387    /// The memory the current [`FileStep::Range`] should be read into.
388    ///
389    /// Exactly as long as the outstanding [`FileStep::Range`]'s `len`, and its address
390    /// does not move while that request stands, so a driver may hold a pointer into it
391    /// across a suspension (§ 6a).
392    ///
393    /// # Examples
394    ///
395    /// See the type-level example.
396    pub fn input_dst(&mut self) -> &mut [u8] {
397        let filled = self.filled;
398        match self.state {
399            State::Buffer { .. } => match self.pending.as_mut().and_then(|p| p.dest.as_mut()) {
400                Some(dest) => &mut dest.as_mut_slice()[filled..],
401                // Unreachable while a Buffer request is outstanding; an empty slice makes
402                // a driver that ignores `step` report zero rather than corrupt memory.
403                None => &mut [],
404            },
405            State::Header | State::Trailer | State::Index | State::Kv | State::Descriptor => {
406                &mut self.staging[filled..]
407            }
408            _ => &mut [],
409        }
410    }
411
412    /// Reports that `n` bytes of the outstanding range were read.
413    ///
414    /// Records a count and nothing else: every parse, allocation and state change happens
415    /// in [`step`][Self::step], which keeps the window a cancellation can land in as
416    /// narrow as possible (§ 6).
417    ///
418    /// # Errors
419    ///
420    /// - [`Error::UnexpectedEof`] — `n` was `0`: the file is shorter than its index claims
421    /// - [`Error::InvalidProgressReport`] — `n` exceeded the bytes offered, or no range
422    ///   was outstanding
423    ///
424    /// # Examples
425    ///
426    /// See the type-level example.
427    pub fn report_filled(&mut self, n: usize) -> Result<()> {
428        let offered = match self.state {
429            State::Header
430            | State::Trailer
431            | State::Index
432            | State::Kv
433            | State::Descriptor
434            | State::Buffer { .. } => self.target - self.filled,
435            State::Failed => return Err(Error::CoreFailed),
436            _ => 0,
437        };
438        if offered == 0 || n > offered {
439            return Err(Error::InvalidProgressReport {
440                reported: n,
441                offered,
442            });
443        }
444        if n == 0 {
445            return Err(self.fail(Error::UnexpectedEof));
446        }
447        self.filled += n;
448        self.cursor += n as u64;
449        Ok(())
450    }
451
452    /// Starts reading the descriptor for `name`, without reading any buffer data.
453    ///
454    /// Supersedes any job already in flight: the index makes every offset absolute, so
455    /// there is nothing to unwind.
456    ///
457    /// # Errors
458    ///
459    /// - [`Error::TensorNotFound`] — no tensor of that name is in the index
460    /// - [`Error::CoreFailed`] — the open sequence failed, or has not finished
461    ///
462    /// # Examples
463    ///
464    /// See the type-level example.
465    pub fn begin_descriptor(&mut self, name: &str) -> Result<()> {
466        self.begin_job(Job::Descriptor)?;
467        self.start_descriptor(name)
468    }
469
470    /// Starts reading the complete tensor named `name`: descriptor, then every buffer.
471    ///
472    /// Supersedes any job already in flight.
473    ///
474    /// # Errors
475    ///
476    /// As [`begin_descriptor`][Self::begin_descriptor].
477    ///
478    /// # Examples
479    ///
480    /// See the type-level example.
481    pub fn begin_tensor(&mut self, name: &str) -> Result<()> {
482        self.begin_job(Job::Tensor)?;
483        self.start_descriptor(name)
484    }
485
486    /// Starts reading the composite whose head is named `head_name`, reassembling the head
487    /// with its members.
488    ///
489    /// # Errors
490    ///
491    /// - [`Error::TensorNotFound`] — no tensor of that name is in the index
492    /// - [`Error::CoreFailed`] — the open sequence failed, or has not finished
493    ///
494    /// Walking it may further fail with [`Error::NotAComposite`],
495    /// [`Error::TornComposite`] or [`Error::CompositeNestingTooDeep`], reported from
496    /// [`step`][Self::step].
497    ///
498    /// # Examples
499    ///
500    /// ```no_run
501    /// # fn main() -> hurray_io::Result<()> {
502    /// # let mut core = hurray_io::file::FileReaderCore::new(4096);
503    /// core.begin_composite("weight")?;
504    /// # Ok(())
505    /// # }
506    /// ```
507    pub fn begin_composite(&mut self, head_name: &str) -> Result<()> {
508        self.begin_job(Job::Walk)?;
509
510        // Offset-ordered snapshot of names: membership is recovered by write order (file
511        // offset), independent of the index array's order (which SORTED_INDEX permutes).
512        let mut ordered: Vec<(u64, String)> = self
513            .index
514            .iter()
515            .map(|e| (e.descriptor_offset, e.name.clone()))
516            .collect();
517        ordered.sort_unstable_by_key(|(offset, _)| *offset);
518
519        let start = ordered
520            .iter()
521            .position(|(_, name)| name == head_name)
522            .ok_or_else(|| Error::TensorNotFound(head_name.to_string()))?;
523
524        self.walk = Some(Walk {
525            names: ordered.into_iter().map(|(_, name)| name).collect(),
526            cursor: start,
527            stack: Vec::new(),
528        });
529        self.walk_next()
530    }
531
532    /// Takes the descriptor [`FileStep::Descriptor`] announced, leaving the core ready.
533    ///
534    /// # Examples
535    ///
536    /// See the type-level example.
537    pub fn take_descriptor(&mut self) -> Option<TensorDescriptor> {
538        let descriptor = self.descriptor_out.take()?;
539        self.finish_job();
540        Some(descriptor)
541    }
542
543    /// Takes the tensor [`FileStep::Tensor`] announced, leaving the core ready.
544    ///
545    /// # Examples
546    ///
547    /// See the type-level example.
548    pub fn take_tensor(&mut self) -> Option<FileTensor> {
549        let tensor = self.tensor_out.take()?;
550        self.finish_job();
551        Some(tensor)
552    }
553
554    /// Takes the composite [`FileStep::Composite`] announced, leaving the core ready.
555    ///
556    /// # Examples
557    ///
558    /// ```no_run
559    /// # fn main() -> hurray_io::Result<()> {
560    /// # let mut core = hurray_io::file::FileReaderCore::new(4096);
561    /// if let Some(composite) = core.take_composite() {
562    ///     println!("{} member(s)", composite.members.len());
563    /// }
564    /// # Ok(())
565    /// # }
566    /// ```
567    pub fn take_composite(&mut self) -> Option<FileComposite> {
568        let composite = self.composite_out.take()?;
569        self.finish_job();
570        Some(composite)
571    }
572
573    /// The footer index: one entry per tensor, in the order the file records it.
574    ///
575    /// Empty until [`FileStep::Ready`].
576    ///
577    /// # Examples
578    ///
579    /// See the type-level example.
580    pub fn index(&self) -> &[IndexEntry] {
581        &self.index
582    }
583
584    /// The file-level KV metadata, empty if the file has none.
585    ///
586    /// # Examples
587    ///
588    /// ```
589    /// use hurray_io::file::FileReaderCore;
590    ///
591    /// let core = FileReaderCore::new(4096);
592    /// assert!(core.kv().is_empty());
593    /// ```
594    pub fn kv(&self) -> &[(String, KvValue)] {
595        &self.kv
596    }
597
598    /// Whether the open sequence has finished, so the index and KV are available.
599    ///
600    /// # Examples
601    ///
602    /// ```
603    /// use hurray_io::file::FileReaderCore;
604    ///
605    /// assert!(!FileReaderCore::new(4096).is_open());
606    /// ```
607    pub fn is_open(&self) -> bool {
608        !matches!(
609            self.state,
610            State::Header | State::Trailer | State::Index | State::Kv | State::Failed
611        )
612    }
613
614    // ── transitions ───────────────────────────────────────────────────────────
615
616    /// A failed *job* leaves the core usable: every offset comes from the index, so the
617    /// next `begin_*` is unaffected by where this one stopped. A failure during the open
618    /// sequence is terminal, because there is no index to start again from.
619    fn fail(&mut self, error: Error) -> Error {
620        match self.state {
621            State::Header | State::Trailer | State::Index | State::Kv => self.fail_open(error),
622            _ => {
623                self.state = State::Ready;
624                self.job = None;
625                self.pending = None;
626                self.walk = None;
627                error
628            }
629        }
630    }
631
632    fn fail_open(&mut self, error: Error) -> Error {
633        self.state = State::Failed;
634        error
635    }
636
637    fn begin_job(&mut self, job: Job) -> Result<()> {
638        if !self.is_open() {
639            return Err(Error::CoreFailed);
640        }
641        self.job = Some(job);
642        self.pending = None;
643        self.walk = None;
644        self.descriptor_out = None;
645        self.tensor_out = None;
646        self.composite_out = None;
647        Ok(())
648    }
649
650    fn finish_job(&mut self) {
651        self.job = None;
652        self.state = State::Ready;
653    }
654
655    /// Stages a range of `len` bytes at `offset` for the given state.
656    fn request(&mut self, offset: u64, len: usize, state: State) {
657        self.cursor = offset;
658        self.target = len;
659        self.filled = 0;
660        self.staging.clear();
661        self.staging.resize(len, 0);
662        self.state = state;
663    }
664
665    fn advance(&mut self) -> Result<()> {
666        match self.state {
667            State::Header => self.finish_header(),
668            State::Trailer => self.finish_trailer(),
669            State::Index => self.finish_index(),
670            State::Kv => self.finish_kv(),
671            State::Descriptor => self.finish_descriptor(),
672            State::Buffer { index } => self.finish_buffer(index),
673            // `advance` is only reached from the filling states.
674            _ => Err(self.fail(Error::CoreFailed)),
675        }
676    }
677
678    fn finish_header(&mut self) -> Result<()> {
679        // Checked here rather than before the header request, so a file too short to even
680        // hold a header still fails as the truncated file it is: the header range comes
681        // back short, and a zero-byte read is `UnexpectedEof`.
682        if self.size < FILE_HEADER_SIZE + TRAILER_SIZE {
683            return Err(self.fail_open(Error::InvalidHeader(
684                "file too small to contain a valid trailer".into(),
685            )));
686        }
687
688        let header = &self.staging;
689        if header[0..8] != FILE_MAGIC[..] {
690            return Err(self.fail_open(Error::InvalidFileMagic));
691        }
692
693        let container_version_major = header[8];
694        if container_version_major > SUPPORTED_CONTAINER_VERSION_MAJOR {
695            return Err(self.fail_open(Error::UnsupportedContainerVersion {
696                major: container_version_major,
697            }));
698        }
699
700        let flags = u32::from_le_bytes(header[12..16].try_into().unwrap_or([0; 4]));
701        if flags & file_flags::RESERVED_MASK != 0 {
702            return Err(self.fail_open(Error::ReservedFileFlagBits { flags }));
703        }
704        self.flags = flags;
705
706        let alignment = u32::from_le_bytes(header[16..20].try_into().unwrap_or([0; 4]));
707        // `file-format.md § File Header` makes this a power of two in [4096, 2 MiB]. The
708        // check is not pedantry: `0` reaches the padding arithmetic as a divisor.
709        if !alignment.is_power_of_two() || alignment > MAX_DATA_BUFFER_ALIGNMENT {
710            return Err(self.fail_open(Error::InvalidHeader(format!(
711                "data_buffer_alignment {alignment} is not a power of two in \
712                 [1, {MAX_DATA_BUFFER_ALIGNMENT}]"
713            ))));
714        }
715        self.data_buffer_alignment = u64::from(alignment);
716
717        self.request(
718            self.size - TRAILER_SIZE,
719            TRAILER_SIZE as usize,
720            State::Trailer,
721        );
722        Ok(())
723    }
724
725    fn finish_trailer(&mut self) -> Result<()> {
726        let trailer = &self.staging;
727        if trailer[36..40] != TRAILER_MAGIC[..] {
728            return Err(self.fail_open(Error::InvalidTrailerMagic));
729        }
730
731        let index_offset = u64::from_le_bytes(trailer[0..8].try_into().unwrap_or([0; 8]));
732        let index_length = u64::from_le_bytes(trailer[8..16].try_into().unwrap_or([0; 8]));
733        let kv_offset = u64::from_le_bytes(trailer[16..24].try_into().unwrap_or([0; 8]));
734        let kv_length = u32::from_le_bytes(trailer[24..28].try_into().unwrap_or([0; 4]));
735        self.stored_crc = u32::from_le_bytes(trailer[28..32].try_into().unwrap_or([0; 4]));
736        self.kv_range = (kv_offset, kv_length);
737
738        // Checked: a crafted trailer can make these sum past u64, and an overflow panic in
739        // a parser fed untrusted bytes is a worse outcome than the error it would raise.
740        match index_offset.checked_add(index_length) {
741            Some(end) if end <= self.size - TRAILER_SIZE => {}
742            _ => return Err(self.fail_open(Error::IndexOverrunsTrailer)),
743        }
744
745        let length = match usize::try_from(index_length) {
746            Ok(length) => length,
747            Err(_) => return Err(self.fail_open(Error::IndexOverrunsTrailer)),
748        };
749        self.request(index_offset, length, State::Index);
750        Ok(())
751    }
752
753    fn finish_index(&mut self) -> Result<()> {
754        if self.flags & file_flags::HAS_INDEX_CRC32C != 0 {
755            let computed = crc32c::crc32c(&self.staging);
756            if computed != self.stored_crc {
757                return Err(self.fail_open(Error::IndexCrc32cMismatch {
758                    stored: self.stored_crc,
759                    computed,
760                }));
761            }
762        }
763
764        self.index = match parse_index(&self.staging) {
765            Ok(index) => index,
766            Err(e) => return Err(self.fail_open(e)),
767        };
768
769        // Accept KV if either the flag or the trailer fields indicate its presence. A
770        // streaming writer may not set HAS_KV_METADATA in the header (no seek-back), so
771        // kv_offset != 0 is treated as authoritative.
772        let (kv_offset, kv_length) = self.kv_range;
773        let has_kv = (self.flags & file_flags::HAS_KV_METADATA != 0) || kv_offset != 0;
774        if has_kv && kv_offset != 0 && kv_length != 0 {
775            self.request(kv_offset, kv_length as usize, State::Kv);
776        } else {
777            self.state = State::Ready;
778        }
779        Ok(())
780    }
781
782    fn finish_kv(&mut self) -> Result<()> {
783        self.kv = match parse_kv(&self.staging) {
784            Ok(kv) => kv,
785            Err(e) => return Err(self.fail_open(e)),
786        };
787        self.state = State::Ready;
788        Ok(())
789    }
790
791    /// Looks `name` up in the index and asks for its descriptor bytes.
792    fn start_descriptor(&mut self, name: &str) -> Result<()> {
793        let entry = self
794            .index
795            .iter()
796            .find(|e| e.name == name)
797            .ok_or_else(|| Error::TensorNotFound(name.to_string()))?;
798
799        let offset = entry.descriptor_offset;
800        let length = entry.descriptor_length as usize;
801        self.entry_name = entry.name.clone();
802        self.entry_data_offset = entry.data_offset;
803        self.request(offset, length, State::Descriptor);
804        Ok(())
805    }
806
807    fn finish_descriptor(&mut self) -> Result<()> {
808        let descriptor = match TensorDescriptor::decode(&self.staging) {
809            Ok(descriptor) => descriptor,
810            Err(e) => return Err(self.fail(e.into())),
811        };
812
813        match self.job {
814            Some(Job::Descriptor) => {
815                self.descriptor_out = Some(descriptor);
816                self.state = State::Done(Done::Descriptor);
817                Ok(())
818            }
819            Some(Job::Tensor) => self.begin_buffers(descriptor),
820            Some(Job::Walk) => self.walk_descriptor(descriptor),
821            None => Err(self.fail(Error::CoreFailed)),
822        }
823    }
824
825    /// Enters the buffer-filling states for `descriptor`, or completes it at once when it
826    /// declares no buffers — the composite head case.
827    fn begin_buffers(&mut self, descriptor: TensorDescriptor) -> Result<()> {
828        let count = descriptor.buffers.len();
829        self.pending = Some(Pending {
830            name: std::mem::take(&mut self.entry_name),
831            descriptor,
832            buffers: Vec::with_capacity(count),
833            dest: None,
834            next_offset: self.entry_data_offset,
835        });
836        if count == 0 {
837            return self.complete_tensor();
838        }
839        self.begin_buffer(0)
840    }
841
842    /// Allocates the destination for buffer `index` and asks for its bytes.
843    fn begin_buffer(&mut self, index: usize) -> Result<()> {
844        let (handle, offset) = match self.pending.as_ref() {
845            Some(pending) => match pending.descriptor.buffers.get(index) {
846                Some(handle) => (*handle, pending.next_offset),
847                None => return Err(self.fail(Error::CoreFailed)),
848            },
849            None => return Err(self.fail(Error::CoreFailed)),
850        };
851
852        let request = DestinationRequest {
853            index,
854            byte_size: handle.byte_size(),
855            alignment: handle.alignment(),
856        };
857        let len = match usize::try_from(request.byte_size) {
858            Ok(len) => len,
859            Err(_) => {
860                return Err(self.fail(Error::UnallocatableBuffer {
861                    index,
862                    byte_size: request.byte_size,
863                    alignment: request.alignment,
864                }))
865            }
866        };
867        let dest = match self.destinations.allocate(request) {
868            Ok(dest) => dest,
869            Err(e) => return Err(self.fail(e)),
870        };
871        match self.pending.as_mut() {
872            Some(pending) => pending.dest = Some(dest),
873            None => return Err(self.fail(Error::CoreFailed)),
874        }
875
876        // `request` would resize the staging buffer; a payload never lands there (§ 2).
877        self.cursor = offset;
878        self.target = len;
879        self.filled = 0;
880        self.state = State::Buffer { index };
881        Ok(())
882    }
883
884    /// Buffer `index` is full: freeze it, then move to the next one or finish the tensor.
885    fn finish_buffer(&mut self, index: usize) -> Result<()> {
886        let count = match self.pending.as_mut() {
887            Some(pending) => match pending.dest.take() {
888                Some(dest) => {
889                    pending.buffers.push(dest.freeze());
890                    pending.descriptor.buffers.len()
891                }
892                None => return Err(self.fail(Error::CoreFailed)),
893            },
894            None => return Err(self.fail(Error::CoreFailed)),
895        };
896
897        let next = index + 1;
898        if next < count {
899            // Each buffer begins on a `data_buffer_alignment` boundary, so the padding
900            // after this one is skipped rather than read.
901            let offset = align_up(self.cursor, self.data_buffer_alignment);
902            match self.pending.as_mut() {
903                Some(pending) => pending.next_offset = offset,
904                None => return Err(self.fail(Error::CoreFailed)),
905            }
906            self.begin_buffer(next)
907        } else {
908            self.complete_tensor()
909        }
910    }
911
912    fn complete_tensor(&mut self) -> Result<()> {
913        let pending = match self.pending.take() {
914            Some(pending) => pending,
915            None => return Err(self.fail(Error::CoreFailed)),
916        };
917        let tensor = FileTensor {
918            name: pending.name,
919            descriptor: pending.descriptor,
920            buffers: pending.buffers,
921        };
922
923        match self.job {
924            Some(Job::Tensor) => {
925                self.tensor_out = Some(tensor);
926                self.state = State::Done(Done::Tensor);
927                Ok(())
928            }
929            Some(Job::Walk) => self.deliver(FileItem::Tensor(tensor)),
930            _ => Err(self.fail(Error::CoreFailed)),
931        }
932    }
933
934    // ── the composite walk (§ 10) ─────────────────────────────────────────────
935    //
936    // The same shape as `CompositeAssembler`, not the same code: that one assembles the
937    // stream's nameless items as they are pushed at it, while this one pulls named items
938    // out of the index in file order. Sharing them would mean a name-shaped hole in the
939    // stream's types, which have no names on the wire.
940
941    /// Asks for the next item in the walk.
942    fn walk_next(&mut self) -> Result<()> {
943        let (name, exhausted) = match self.walk.as_mut() {
944            Some(walk) => match walk.names.get(walk.cursor) {
945                Some(name) => {
946                    let name = name.clone();
947                    walk.cursor += 1;
948                    (name, false)
949                }
950                None => (String::new(), true),
951            },
952            None => return Err(self.fail(Error::CoreFailed)),
953        };
954
955        if exhausted {
956            // The file ran out with a composite still open. The innermost one is reported:
957            // that is the level whose members actually went missing.
958            let torn = self
959                .walk
960                .as_ref()
961                .and_then(|walk| walk.stack.last())
962                .map(|open| Error::TornComposite {
963                    declared: open.member_count,
964                    actual: open.members.len() as u32,
965                })
966                .unwrap_or(Error::CoreFailed);
967            return Err(self.fail(torn));
968        }
969
970        self.start_descriptor(&name)
971    }
972
973    /// A descriptor arrived during a walk: open a composite, or read the tensor's buffers.
974    fn walk_descriptor(&mut self, descriptor: TensorDescriptor) -> Result<()> {
975        let member_count = match &descriptor.layout {
976            LayoutDescriptor::Composite(layout) => layout.member_count,
977            _ => {
978                // The name the walk started from must be a head; anything else is the
979                // caller's mistake, caught before its buffers are read.
980                if self.walk.as_ref().is_some_and(|walk| walk.stack.is_empty()) {
981                    let name = std::mem::take(&mut self.entry_name);
982                    return Err(self.fail(Error::NotAComposite(name)));
983                }
984                return self.begin_buffers(descriptor);
985            }
986        };
987
988        let too_deep = match self.walk.as_ref() {
989            Some(walk) => walk.stack.len() >= self.max_composite_depth,
990            None => return Err(self.fail(Error::CoreFailed)),
991        };
992        if too_deep {
993            return Err(self.fail(Error::CompositeNestingTooDeep {
994                limit: self.max_composite_depth,
995            }));
996        }
997
998        let validator = match CompositeValidator::new(&descriptor) {
999            Ok(validator) => validator,
1000            Err(e) => return Err(self.fail(e.into())),
1001        };
1002        let open = Open {
1003            name: std::mem::take(&mut self.entry_name),
1004            head: descriptor,
1005            member_count,
1006            members: Vec::new(),
1007            validator,
1008        };
1009        match self.walk.as_mut() {
1010            Some(walk) => walk.stack.push(open),
1011            None => return Err(self.fail(Error::CoreFailed)),
1012        }
1013        self.walk_next()
1014    }
1015
1016    /// Hands `item` to the innermost open head, closing heads — and their parents — as
1017    /// each one fills. Loops rather than recurses (§ 10).
1018    fn deliver(&mut self, item: FileItem) -> Result<()> {
1019        let mut item = item;
1020        loop {
1021            // Pop and push back rather than peek: the closing branch needs to own the
1022            // head, and `CompositeValidator::finish` consumes the validator.
1023            let top = match self.walk.as_mut() {
1024                Some(walk) => walk.stack.pop(),
1025                None => return Err(self.fail(Error::CoreFailed)),
1026            };
1027            let Some(mut top) = top else {
1028                // Nothing left open: the walk produced its composite.
1029                let FileItem::Composite(composite) = item else {
1030                    return Err(self.fail(Error::CoreFailed));
1031                };
1032                self.composite_out = Some(composite);
1033                self.walk = None;
1034                self.state = State::Done(Done::Composite);
1035                return Ok(());
1036            };
1037
1038            if let Err(e) = top.validator.push_member(item.descriptor()) {
1039                return Err(self.fail(e.into()));
1040            }
1041            top.members.push(item);
1042
1043            if (top.members.len() as u32) < top.member_count {
1044                match self.walk.as_mut() {
1045                    Some(walk) => walk.stack.push(top),
1046                    None => return Err(self.fail(Error::CoreFailed)),
1047                }
1048                return self.walk_next();
1049            }
1050
1051            let Open {
1052                name,
1053                head,
1054                members,
1055                validator,
1056                ..
1057            } = top;
1058            if let Err(e) = validator.finish() {
1059                return Err(self.fail(e.into()));
1060            }
1061            // A closed composite is itself an item: a member of its parent, or the one the
1062            // caller gets.
1063            item = FileItem::Composite(FileComposite {
1064                name,
1065                head,
1066                members,
1067            });
1068        }
1069    }
1070}