Skip to main content

hurray_io/stream/
reader.rs

1use bytes::{Bytes, BytesMut};
2use hurray_core::{CompositeValidator, LayoutDescriptor, SyncMode, TensorDescriptor};
3use tokio::io::{AsyncRead, AsyncReadExt};
4
5use crate::stream::frame;
6use crate::{Error, Result};
7
8/// Default maximum descriptor size: 16 MiB.
9///
10/// Limits memory allocation when reading untrusted streams.
11pub const DEFAULT_MAX_DESCRIPTOR_BYTES: u64 = 16 * 1024 * 1024;
12
13/// Default maximum composite nesting depth.
14///
15/// A composite member may itself be a composite (ADR-027 § Binding). This bounds the
16/// recursion so a maliciously deep composite on an untrusted stream cannot exhaust the
17/// stack. Matches the format's rank cap of 64 in spirit.
18pub const DEFAULT_MAX_COMPOSITE_DEPTH: usize = 64;
19
20/// Options for [`StreamReader`].
21pub struct StreamReaderOptions {
22    /// Maximum byte length of a single descriptor. Default: 16 MiB.
23    pub max_descriptor_bytes: u64,
24    /// Maximum byte length of a single buffer. Default: [`u64::MAX`] (unbounded).
25    pub max_buffer_bytes: u64,
26    /// Reject buffers whose `sync_mode` is not [`SyncMode::ProducerSynced`].
27    ///
28    /// Enable when the stream crosses machine boundaries.
29    pub enforce_cross_machine_sync: bool,
30    /// Maximum composite nesting depth for [`next_item`][StreamReader::next_item].
31    /// Default: [`DEFAULT_MAX_COMPOSITE_DEPTH`].
32    pub max_composite_depth: usize,
33}
34
35impl Default for StreamReaderOptions {
36    fn default() -> Self {
37        Self {
38            max_descriptor_bytes: DEFAULT_MAX_DESCRIPTOR_BYTES,
39            max_buffer_bytes: u64::MAX,
40            enforce_cross_machine_sync: false,
41            max_composite_depth: DEFAULT_MAX_COMPOSITE_DEPTH,
42        }
43    }
44}
45
46/// A decoded tensor: its descriptor plus zero-copy buffer views.
47///
48/// Each element in `buffers` corresponds to the [`hurray_core::BufferHandle`]
49/// at the same index in `descriptor.buffers`.
50#[derive(Debug)]
51pub struct StreamTensor {
52    /// The decoded tensor descriptor.
53    pub descriptor: TensorDescriptor,
54    /// Raw buffer bytes, one [`Bytes`] per buffer handle.
55    pub buffers: Vec<Bytes>,
56}
57
58/// One decoded item from the stream: either a plain tensor or a composite.
59///
60/// Returned by [`next_item`][StreamReader::next_item]. A composite's members are
61/// themselves [`StreamItem`]s, so nested composites (ADR-027 § Binding) are represented
62/// as a tree.
63#[derive(Debug)]
64pub enum StreamItem {
65    /// A single (non-composite) tensor.
66    Tensor(StreamTensor),
67    /// A composite: a data-less head plus its ordered members.
68    Composite(StreamComposite),
69}
70
71impl StreamItem {
72    /// The item's governing descriptor: the tensor's descriptor, or the composite head.
73    pub fn descriptor(&self) -> &TensorDescriptor {
74        match self {
75            StreamItem::Tensor(t) => &t.descriptor,
76            StreamItem::Composite(c) => &c.head,
77        }
78    }
79}
80
81/// A decoded composite tensor: its data-less head plus its ordered members.
82///
83/// Membership and ordering are exactly as they appeared on the wire (head precedes its
84/// members; ADR-027 § Binding). The head, `member_count`, and per-rule constraints
85/// (partition coverage, overlay ordering) have already been validated via
86/// [`CompositeValidator`].
87#[derive(Debug)]
88pub struct StreamComposite {
89    /// The composite head descriptor (owns no data buffers).
90    pub head: TensorDescriptor,
91    /// The members, in wire order. Each may itself be a composite (nesting).
92    pub members: Vec<StreamItem>,
93}
94
95/// Reads tensors from an async source in the Hurray streaming wire format.
96///
97/// Call [`next_tensor`][StreamReader::next_tensor] in a loop until it returns
98/// `Ok(None)` (clean EOF).
99///
100/// # Examples
101///
102/// ```no_run
103/// # #[tokio::main]
104/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
105/// use hurray_io::stream::StreamReader;
106///
107/// let wire: &[u8] = &[]; // replace with actual data
108/// let mut reader = StreamReader::new(wire);
109/// while let Some(tensor) = reader.next_tensor().await? {
110///     println!(
111///         "tensor with {} buffer(s)",
112///         tensor.buffers.len()
113///     );
114/// }
115/// # Ok(())
116/// # }
117/// ```
118pub struct StreamReader<R> {
119    inner: R,
120    options: StreamReaderOptions,
121}
122
123impl<R: AsyncRead + Unpin> StreamReader<R> {
124    /// Creates a reader with default options.
125    pub fn new(inner: R) -> Self {
126        Self {
127            inner,
128            options: StreamReaderOptions::default(),
129        }
130    }
131
132    /// Creates a reader that enforces `ProducerSynced` on every buffer.
133    ///
134    /// Use when the stream was produced by a remote machine and GPU/semaphore
135    /// sync primitives are not meaningful locally.
136    pub fn cross_machine(inner: R) -> Self {
137        Self {
138            inner,
139            options: StreamReaderOptions {
140                enforce_cross_machine_sync: true,
141                ..Default::default()
142            },
143        }
144    }
145
146    /// Creates a reader with custom options.
147    pub fn with_options(inner: R, options: StreamReaderOptions) -> Self {
148        Self { inner, options }
149    }
150
151    /// Reads the next tensor from the stream.
152    ///
153    /// Returns `Ok(None)` on a clean EOF (no bytes remaining before a descriptor starts).
154    ///
155    /// # Errors
156    ///
157    /// - [`Error::UnexpectedEof`] — stream ended mid-descriptor or mid-buffer
158    /// - [`Error::InvalidHeader`] — malformed descriptor prefix
159    /// - [`Error::FrameTooLarge`] — descriptor or buffer exceeds configured limit
160    /// - [`Error::InvalidCrossMachineSyncMode`] — cross-machine mode and a non-`ProducerSynced` buffer
161    /// - [`Error::Core`] — descriptor decode failed
162    /// - [`Error::Io`] — underlying read error
163    pub async fn next_tensor(&mut self) -> Result<Option<StreamTensor>> {
164        let desc = match frame::read_descriptor(&mut self.inner, self.options.max_descriptor_bytes)
165            .await?
166        {
167            Some(d) => d,
168            None => return Ok(None),
169        };
170
171        let buffers = self.read_buffers(&desc).await?;
172
173        Ok(Some(StreamTensor {
174            descriptor: desc,
175            buffers,
176        }))
177    }
178
179    /// Reads the next item, assembling composites (head + members) into a
180    /// [`StreamItem::Composite`] and validating them.
181    ///
182    /// When the next descriptor on the wire is a composite head (`layout_tag = 0x0B`), this
183    /// reads its declared `member_count` members — each of which may itself be a composite
184    /// (recursion) — validates the group with [`CompositeValidator`] (member count, plus
185    /// partition coverage / overlay ordering), and returns them together. Otherwise it
186    /// behaves like [`next_tensor`][StreamReader::next_tensor], returning a
187    /// [`StreamItem::Tensor`]. Returns `Ok(None)` on a clean EOF.
188    ///
189    /// # Examples
190    ///
191    /// ```no_run
192    /// # #[tokio::main]
193    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
194    /// use hurray_io::stream::{StreamItem, StreamReader};
195    ///
196    /// let wire: &[u8] = &[]; // replace with actual data
197    /// let mut reader = StreamReader::new(wire);
198    /// while let Some(item) = reader.next_item().await? {
199    ///     match item {
200    ///         StreamItem::Tensor(t) => println!("tensor, {} buffer(s)", t.buffers.len()),
201    ///         StreamItem::Composite(c) => println!("composite, {} member(s)", c.members.len()),
202    ///     }
203    /// }
204    /// # Ok(())
205    /// # }
206    /// ```
207    ///
208    /// # Errors
209    ///
210    /// In addition to the errors of [`next_tensor`][StreamReader::next_tensor]:
211    ///
212    /// - [`Error::TornComposite`] — the stream ended before the head's `member_count`
213    ///   members were read
214    /// - [`Error::CompositeNestingTooDeep`] — nesting exceeded `max_composite_depth`
215    /// - [`Error::Core`] — composite validation failed (e.g. partition does not cover the
216    ///   index space, overlay ordering, member-count mismatch)
217    pub async fn next_item(&mut self) -> Result<Option<StreamItem>> {
218        self.read_item(0).await
219    }
220
221    /// Reads and freezes every buffer declared by `desc`, enforcing the buffer-size limit
222    /// and (in cross-machine mode) the sync-mode requirement.
223    async fn read_buffers(&mut self, desc: &TensorDescriptor) -> Result<Vec<Bytes>> {
224        let mut buffers = Vec::with_capacity(desc.buffers.len());
225
226        for (i, handle) in desc.buffers.iter().enumerate() {
227            if self.options.enforce_cross_machine_sync
228                && handle.sync_mode() != SyncMode::ProducerSynced
229            {
230                return Err(Error::InvalidCrossMachineSyncMode {
231                    index: i,
232                    actual: handle.sync_mode().to_byte(),
233                });
234            }
235
236            let byte_size = handle.byte_size();
237
238            if byte_size > self.options.max_buffer_bytes {
239                return Err(Error::FrameTooLarge {
240                    kind: "buffer",
241                    value: byte_size,
242                    limit: self.options.max_buffer_bytes,
243                });
244            }
245
246            // One allocation per buffer; resize then freeze gives a Bytes with no extra copy.
247            let mut buf = BytesMut::with_capacity(byte_size as usize);
248            buf.resize(byte_size as usize, 0);
249            self.inner
250                .read_exact(&mut buf)
251                .await
252                .map_err(frame::map_unexpected_eof)?;
253            buffers.push(buf.freeze());
254        }
255
256        Ok(buffers)
257    }
258
259    /// Reads one item at composite-nesting `depth`, recursing into members.
260    async fn read_item(&mut self, depth: usize) -> Result<Option<StreamItem>> {
261        let desc = match frame::read_descriptor(&mut self.inner, self.options.max_descriptor_bytes)
262            .await?
263        {
264            Some(d) => d,
265            None => return Ok(None),
266        };
267
268        let member_count = match &desc.layout {
269            LayoutDescriptor::Composite(c) => c.member_count,
270            // Not a composite head: read its buffers and return a plain tensor.
271            _ => {
272                let buffers = self.read_buffers(&desc).await?;
273                return Ok(Some(StreamItem::Tensor(StreamTensor {
274                    descriptor: desc,
275                    buffers,
276                })));
277            }
278        };
279
280        if depth >= self.options.max_composite_depth {
281            return Err(Error::CompositeNestingTooDeep {
282                limit: self.options.max_composite_depth,
283            });
284        }
285
286        // Validate the group as its members arrive, reusing the core validator: member
287        // count, and per-rule constraints (partition coverage, overlay base-first ordering).
288        let mut validator = CompositeValidator::new(&desc)?;
289        let mut members = Vec::with_capacity(member_count as usize);
290        for i in 0..member_count {
291            // Box the recursive call: an async fn cannot name its own future inline.
292            let item = match Box::pin(self.read_item(depth + 1)).await? {
293                Some(item) => item,
294                None => {
295                    return Err(Error::TornComposite {
296                        declared: member_count,
297                        actual: i,
298                    })
299                }
300            };
301            validator.push_member(item.descriptor())?;
302            members.push(item);
303        }
304        validator.finish()?;
305
306        Ok(Some(StreamItem::Composite(StreamComposite {
307            head: desc,
308            members,
309        })))
310    }
311
312    /// Returns the underlying reader.
313    pub fn into_inner(self) -> R {
314        self.inner
315    }
316}