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}