hurray_ffi/file/reader.rs
1//! Blocking random-access file reads over a caller-supplied transport (ADR-041 §§ 7, 8).
2//!
3//! [`HurrayFileReader`] is a driver: `hurray-io`'s `FileReaderCore` holds the container
4//! format and decides which bytes it wants, and this module asks the caller's `read_at`
5//! callback for exactly that range. Every request is absolute, which is why the vtable
6//! needs a positional read rather than a cursor — and why a failed call here is simply
7//! retryable.
8
9use hurray_io::file::{FileItem, FileReaderCore, FileStep};
10
11use crate::{
12 descriptor::HurrayDescriptor,
13 file::kv::{HurrayKvEntry, KvTable},
14 item::{file_item_handle, HurrayItem},
15 panic::{catch, null_check},
16 status::{
17 status_from_io_error, HurrayStatus, HURRAY_ERR_INDEX_OUT_OF_BOUNDS, HURRAY_ERR_INTERNAL,
18 HURRAY_ERR_NULL_POINTER, HURRAY_OK,
19 },
20 stream::{transport::Transport, HurrayTransport},
21 tensor::{tensor_handle, HurrayTensor},
22};
23
24// ── HurrayFileReader ──────────────────────────────────────────────────────────
25
26/// Opaque handle to a blocking file reader.
27///
28/// Not `#[repr(C)]`; the layout is an implementation detail. Created by
29/// [`hurray_file_reader_new`] and released exactly once by [`hurray_file_reader_destroy`].
30///
31/// One handle, one thread at a time.
32pub struct HurrayFileReader {
33 core: FileReaderCore,
34 transport: Transport,
35 /// Tensor names in index order, owned: a C caller holds pointers, and a pointer into
36 /// the core would not survive the next read borrowing it mutably.
37 names: Vec<String>,
38 /// The KV section, owned for the same reason.
39 kv: KvTable,
40}
41
42// ── Construction / destruction ────────────────────────────────────────────────
43
44/// Opens a Hurray file over `transport`: header, trailer, footer index and KV section.
45///
46/// The transport's `read_at` and `size` callbacks are both required — the container is
47/// located from its end, so its size is a precondition of opening it. The vtable is
48/// copied, so the caller MAY free the [`HurrayTransport`] struct immediately; its `ctx`
49/// must stay valid until the reader is destroyed, at which point the vtable's release
50/// callback is invoked.
51///
52/// Returns:
53///
54/// - [`HURRAY_OK`] — `*out_handle` is a new reader.
55/// - [`HURRAY_ERR_INVALID_MAGIC`][crate::HURRAY_ERR_INVALID_MAGIC] — not a `HRRYFILE`.
56/// - [`HURRAY_ERR_VERSION_MISMATCH`][crate::HURRAY_ERR_VERSION_MISMATCH] — a container
57/// version this build does not read.
58/// - [`HURRAY_ERR_INVALID_FILE`][crate::HURRAY_ERR_INVALID_FILE] — the footer does not
59/// check out: a CRC mismatch, an index overrunning the trailer, unknown header flags.
60/// - [`HURRAY_ERR_UNEXPECTED_EOF`][crate::HURRAY_ERR_UNEXPECTED_EOF] — the file is
61/// shorter than its own structure claims.
62/// - Whatever status a callback returned, if it failed. A failed open is terminal: there
63/// is no index to start from, so create a new reader rather than retrying this one.
64///
65/// # Safety
66///
67/// - `transport` MUST point to a readable [`HurrayTransport`] for the duration of the call.
68/// - `out_handle` MUST be a valid, non-null, writable pointer.
69#[no_mangle]
70pub unsafe extern "C" fn hurray_file_reader_new(
71 transport: *const HurrayTransport,
72 out_handle: *mut *mut HurrayFileReader,
73) -> HurrayStatus {
74 catch(|| {
75 null_check!(transport, out_handle);
76
77 // SAFETY: caller guarantees transport is readable for this call.
78 let transport = Transport::from_c(transport);
79 if !transport.can_read_at() {
80 return HURRAY_ERR_NULL_POINTER;
81 }
82
83 let size = match transport.size() {
84 Ok(size) => size,
85 Err(status) => return status,
86 };
87 let mut core = FileReaderCore::new(size);
88
89 // Opening is a job like any other: drive it to the first `Ready`.
90 match drive(&mut core, &transport) {
91 Ok(FileStep::Ready) => {}
92 Ok(_) => return HURRAY_ERR_INTERNAL,
93 Err(status) => return status,
94 }
95
96 let reader = HurrayFileReader {
97 names: core.index().iter().map(|e| e.name.clone()).collect(),
98 kv: KvTable::new(core.kv()),
99 core,
100 transport,
101 };
102 // SAFETY: Box::into_raw transfers ownership to the caller, who must call
103 // hurray_file_reader_destroy exactly once.
104 *out_handle = Box::into_raw(Box::new(reader));
105 HURRAY_OK
106 })
107}
108
109/// Destroys a reader, releases its transport context, and writes null through `handle`.
110///
111/// Tensors already produced by [`hurray_file_reader_read_tensor`] are unaffected: each
112/// owns its own buffers and outlives the reader. Name and KV pointers are *not* — they
113/// borrow from the handle and die with it.
114///
115/// # Safety
116///
117/// - `handle` MUST be a valid, non-null pointer to a `*mut HurrayFileReader` from
118/// [`hurray_file_reader_new`].
119/// - That reader MUST NOT have been destroyed already.
120#[no_mangle]
121pub unsafe extern "C" fn hurray_file_reader_destroy(
122 handle: *mut *mut HurrayFileReader,
123) -> HurrayStatus {
124 catch(|| {
125 null_check!(handle);
126 let reader = *handle;
127 if reader.is_null() {
128 return HURRAY_ERR_INTERNAL;
129 }
130 // SAFETY: created by hurray_file_reader_new via Box::into_raw; the caller
131 // guarantees this is the first and only destroy call. Dropping it runs the
132 // transport's release callback.
133 drop(Box::from_raw(reader));
134 *handle = std::ptr::null_mut();
135 HURRAY_OK
136 })
137}
138
139// ── The index ─────────────────────────────────────────────────────────────────
140
141/// Writes the number of tensors in the file's footer index.
142///
143/// Every tensor is individually addressable, composite heads and members included.
144///
145/// # Safety
146///
147/// `handle` and `out_count` MUST be valid, non-null pointers.
148#[no_mangle]
149pub unsafe extern "C" fn hurray_file_reader_tensor_count(
150 handle: *const HurrayFileReader,
151 out_count: *mut u64,
152) -> HurrayStatus {
153 catch(|| {
154 null_check!(handle, out_count);
155 // SAFETY: handle is non-null and points to a live HurrayFileReader.
156 *out_count = (*handle).names.len() as u64;
157 HURRAY_OK
158 })
159}
160
161/// Borrows the name of index entry `index` as UTF-8 bytes.
162///
163/// The bytes are **not** NUL-terminated and are owned by the reader: valid until
164/// [`hurray_file_reader_destroy`], never to be freed by the caller.
165///
166/// Returns [`HURRAY_ERR_INDEX_OUT_OF_BOUNDS`][crate::HURRAY_ERR_INDEX_OUT_OF_BOUNDS] if
167/// `index` is past the end.
168///
169/// # Safety
170///
171/// `handle`, `out_name` and `out_len` MUST be valid, non-null pointers.
172#[no_mangle]
173pub unsafe extern "C" fn hurray_file_reader_tensor_name(
174 handle: *const HurrayFileReader,
175 index: u64,
176 out_name: *mut *const u8,
177 out_len: *mut usize,
178) -> HurrayStatus {
179 catch(|| {
180 null_check!(handle, out_name, out_len);
181 // SAFETY: handle is non-null and points to a live HurrayFileReader.
182 let reader = &*handle;
183 let Some(name) = reader.names.get(index as usize) else {
184 return HURRAY_ERR_INDEX_OUT_OF_BOUNDS;
185 };
186 *out_name = name.as_ptr();
187 *out_len = name.len();
188 HURRAY_OK
189 })
190}
191
192// ── Reading ───────────────────────────────────────────────────────────────────
193
194/// Reads the descriptor of the tensor named `name`, without reading a byte of its data.
195///
196/// What a loader inspects before deciding whether it wants the tensor at all: element
197/// type, shape, layout, quantization.
198///
199/// `*out_descriptor` is **owned** by the caller and released with
200/// [`hurray_descriptor_destroy`][crate::descriptor::hurray_descriptor_destroy].
201///
202/// Returns [`HURRAY_ERR_NOT_FOUND`][crate::HURRAY_ERR_NOT_FOUND] if the name is not in
203/// the index. A failed read leaves the reader usable: the request was for an absolute
204/// range, so nothing about the reader's position is lost.
205///
206/// # Safety
207///
208/// - `handle` MUST point to a live [`HurrayFileReader`].
209/// - `name` MUST be readable for `name_len` bytes.
210/// - `out_descriptor` MUST be a valid, non-null, writable pointer.
211#[no_mangle]
212pub unsafe extern "C" fn hurray_file_reader_read_descriptor(
213 handle: *mut HurrayFileReader,
214 name: *const u8,
215 name_len: usize,
216 out_descriptor: *mut *mut HurrayDescriptor,
217) -> HurrayStatus {
218 catch(|| {
219 null_check!(handle, out_descriptor);
220 // SAFETY: caller guarantees `name_len` readable bytes at `name`.
221 let name = match crate::file::kv::str_from_c(name, name_len) {
222 Ok(name) => name,
223 Err(status) => return status,
224 };
225
226 // SAFETY: handle is non-null and points to a live HurrayFileReader, which the C
227 // ABI's one-handle-one-thread rule says nothing else is touching.
228 let HurrayFileReader {
229 core, transport, ..
230 } = &mut *handle;
231
232 if let Err(e) = core.begin_descriptor(name) {
233 return status_from_io_error(&e);
234 }
235 match drive(core, transport) {
236 Ok(FileStep::Descriptor) => match core.take_descriptor() {
237 Some(descriptor) => {
238 *out_descriptor = Box::into_raw(HurrayDescriptor::from_descriptor(descriptor));
239 HURRAY_OK
240 }
241 // step() said Descriptor; the core cannot then have none.
242 None => HURRAY_ERR_INTERNAL,
243 },
244 Ok(_) => HURRAY_ERR_INTERNAL,
245 Err(status) => status,
246 }
247 })
248}
249
250/// Reads the tensor named `name`: its descriptor and every data buffer.
251///
252/// `*out_tensor` is **owned** by the caller and released with
253/// [`hurray_tensor_destroy`][crate::hurray_tensor_destroy], which frees both halves.
254///
255/// Every buffer points at memory the core allocated at the alignment its buffer handle
256/// *declares*, which the transport wrote into directly — so a SIMD load on it is sound,
257/// and no copy of the payload happened after that write.
258///
259/// Returns [`HURRAY_ERR_NOT_FOUND`][crate::HURRAY_ERR_NOT_FOUND] for a name that is not
260/// in the index. A composite head reads back as the buffer-less tensor it is in the file;
261/// [`hurray_file_reader_read_composite`] reads it with its members instead.
262///
263/// # Safety
264///
265/// - `handle` MUST point to a live [`HurrayFileReader`].
266/// - `name` MUST be readable for `name_len` bytes.
267/// - `out_tensor` MUST be a valid, non-null, writable pointer.
268#[no_mangle]
269pub unsafe extern "C" fn hurray_file_reader_read_tensor(
270 handle: *mut HurrayFileReader,
271 name: *const u8,
272 name_len: usize,
273 out_tensor: *mut *mut HurrayTensor,
274) -> HurrayStatus {
275 catch(|| {
276 null_check!(handle, out_tensor);
277 // SAFETY: caller guarantees `name_len` readable bytes at `name`.
278 let name = match crate::file::kv::str_from_c(name, name_len) {
279 Ok(name) => name,
280 Err(status) => return status,
281 };
282
283 // SAFETY: handle is non-null and points to a live HurrayFileReader.
284 let HurrayFileReader {
285 core, transport, ..
286 } = &mut *handle;
287
288 if let Err(e) = core.begin_tensor(name) {
289 return status_from_io_error(&e);
290 }
291 match drive(core, transport) {
292 Ok(FileStep::Tensor) => match core.take_tensor() {
293 Some(tensor) => {
294 *out_tensor = Box::into_raw(tensor_handle(tensor.descriptor, tensor.buffers));
295 HURRAY_OK
296 }
297 // step() said Tensor; the core cannot then have none.
298 None => HURRAY_ERR_INTERNAL,
299 },
300 Ok(_) => HURRAY_ERR_INTERNAL,
301 Err(status) => status,
302 }
303 })
304}
305
306/// Reads the composite headed by `name`: its head, its members, and their members.
307///
308/// Membership is recovered from the head's declared `member_count` and the file order of
309/// the index entries that follow it, then validated — member count, partition exact-cover,
310/// overlay base-first ordering, at every level of nesting.
311///
312/// `*out_item` is **owned** by the caller and released with
313/// [`hurray_item_destroy`][crate::hurray_item_destroy], which frees every tensor in the
314/// subtree. Its nodes are in preorder, with each node declaring how many of the nodes
315/// after it are its members — see [`crate::item`] for the walk. Every member carries its
316/// own name, because every tensor in a file has its own index entry.
317///
318/// Returns:
319///
320/// - [`HURRAY_OK`] — `*out_item` is a new [`HurrayItem`].
321/// - [`HURRAY_ERR_NOT_FOUND`][crate::HURRAY_ERR_NOT_FOUND] — no such name in the index.
322/// - [`HURRAY_ERR_NOT_COMPOSITE`][crate::HURRAY_ERR_NOT_COMPOSITE] — the name is in the
323/// index but its descriptor is not a composite head. Refused at descriptor time, before
324/// a byte of any data region is read.
325/// - [`HURRAY_ERR_INVALID_FILE`][crate::HURRAY_ERR_INVALID_FILE] — the group is torn: the
326/// head declares more members than follow it, or a level fails validation.
327/// - [`HURRAY_ERR_LIMIT_EXCEEDED`][crate::HURRAY_ERR_LIMIT_EXCEEDED] — nesting exceeded
328/// the core's maximum composite depth.
329/// - Whatever status the `read_at` callback returned, if it failed. Every request was an
330/// absolute range, so the reader stays usable and the call MAY be retried.
331///
332/// # Safety
333///
334/// - `handle` MUST point to a live [`HurrayFileReader`].
335/// - `name` MUST be readable for `name_len` bytes.
336/// - `out_item` MUST be a valid, non-null, writable pointer.
337#[no_mangle]
338pub unsafe extern "C" fn hurray_file_reader_read_composite(
339 handle: *mut HurrayFileReader,
340 name: *const u8,
341 name_len: usize,
342 out_item: *mut *mut HurrayItem,
343) -> HurrayStatus {
344 catch(|| {
345 null_check!(handle, out_item);
346 // SAFETY: caller guarantees `name_len` readable bytes at `name`.
347 let name = match crate::file::kv::str_from_c(name, name_len) {
348 Ok(name) => name,
349 Err(status) => return status,
350 };
351
352 // SAFETY: handle is non-null and points to a live HurrayFileReader.
353 let HurrayFileReader {
354 core, transport, ..
355 } = &mut *handle;
356
357 if let Err(e) = core.begin_composite(name) {
358 return status_from_io_error(&e);
359 }
360 match drive(core, transport) {
361 Ok(FileStep::Composite) => match core.take_composite() {
362 Some(composite) => {
363 *out_item = Box::into_raw(file_item_handle(FileItem::Composite(composite)));
364 HURRAY_OK
365 }
366 // step() said Composite; the core cannot then have none.
367 None => HURRAY_ERR_INTERNAL,
368 },
369 Ok(_) => HURRAY_ERR_INTERNAL,
370 Err(status) => status,
371 }
372 })
373}
374
375// ── The KV section ────────────────────────────────────────────────────────────
376
377/// Writes the number of entries in the file's KV metadata section.
378///
379/// # Safety
380///
381/// `handle` and `out_count` MUST be valid, non-null pointers.
382#[no_mangle]
383pub unsafe extern "C" fn hurray_file_reader_kv_count(
384 handle: *const HurrayFileReader,
385 out_count: *mut u64,
386) -> HurrayStatus {
387 catch(|| {
388 null_check!(handle, out_count);
389 // SAFETY: handle is non-null and points to a live HurrayFileReader.
390 *out_count = (*handle).kv.entries().len() as u64;
391 HURRAY_OK
392 })
393}
394
395/// Copies KV entry `index` — its key and its typed value — into `out_entry`.
396///
397/// The pointers inside the entry borrow from the reader: valid until
398/// [`hurray_file_reader_destroy`], never to be freed. The entry can be handed straight to
399/// [`hurray_file_writer_finish`][crate::file::hurray_file_writer_finish], which is how a
400/// KV section is copied from one file into another without being transcribed.
401///
402/// Returns [`HURRAY_ERR_INDEX_OUT_OF_BOUNDS`][crate::HURRAY_ERR_INDEX_OUT_OF_BOUNDS] if
403/// `index` is past the end.
404///
405/// # Safety
406///
407/// `handle` MUST point to a live [`HurrayFileReader`], and `out_entry` MUST be a valid,
408/// non-null pointer writable for one [`HurrayKvEntry`].
409#[no_mangle]
410pub unsafe extern "C" fn hurray_file_reader_kv_entry(
411 handle: *const HurrayFileReader,
412 index: u64,
413 out_entry: *mut HurrayKvEntry,
414) -> HurrayStatus {
415 catch(|| {
416 null_check!(handle, out_entry);
417 // SAFETY: handle is non-null and points to a live HurrayFileReader.
418 let reader = &*handle;
419 let Some(entry) = reader.kv.entries().get(index as usize) else {
420 return HURRAY_ERR_INDEX_OUT_OF_BOUNDS;
421 };
422 *out_entry = *entry;
423 HURRAY_OK
424 })
425}
426
427// ── The driver ────────────────────────────────────────────────────────────────
428
429/// Answers every range the core asks for, and returns the step that ends the job.
430///
431/// A free function rather than a method so the core and the transport can be borrowed as
432/// disjoint fields — and so the open sequence can run before the handle exists.
433///
434/// The transfer and its byte-count report are one step with nothing suspended between
435/// them, the shape ADR-041 § 6a requires of every driver.
436fn drive(core: &mut FileReaderCore, transport: &Transport) -> Result<FileStep, HurrayStatus> {
437 loop {
438 match core.step() {
439 Err(e) => return Err(status_from_io_error(&e)),
440 // `input_dst` is exactly as long as the outstanding range, so the request's
441 // own `len` needs no second application here.
442 Ok(FileStep::Range { offset, .. }) => {
443 let filled = transport.read_at(offset, core.input_dst())?;
444 // No bytes moved on an error, and the range was absolute: the core's
445 // position is intact and the same call may simply be made again.
446 if let Err(e) = core.report_filled(filled) {
447 return Err(status_from_io_error(&e));
448 }
449 }
450 Ok(other) => return Ok(other),
451 }
452 }
453}