Skip to main content

gguf_rs_lib/reader/
stream_reader.rs

1//! Stream-based GGUF reader for non-seekable streams
2
3use crate::error::{GGUFError, Result};
4use crate::format::constants::GGUF_MAX_METADATA_DECODED_SIZE;
5use crate::format::types::GGUFTensorType as TensorType;
6use crate::format::{GGUFHeader, Metadata, TensorInfo};
7use crate::tensor::{TensorData, TensorInfo as TensorInfoNew, TensorShape};
8use std::collections::HashMap;
9use std::io::{BufReader, Read};
10
11const MIN_TENSOR_TRACKING_CAPACITY: usize = 64;
12const MAX_READER_CHUNK_SIZE: usize = 1024 * 1024;
13
14/// A reader for GGUF files from non-seekable streams
15#[derive(Debug)]
16pub struct GGUFStreamReader<R> {
17    /// The underlying reader
18    reader: R,
19    /// File header
20    header: GGUFHeader,
21    /// Metadata
22    metadata: Metadata,
23    /// Tensor information
24    tensor_infos: Vec<TensorInfoNew>,
25    /// Tensor descriptor index by name
26    tensor_name_index: HashMap<String, usize>,
27    /// Current position in the stream
28    position: u64,
29    /// Whether we've reached the tensor data section
30    at_tensor_data: bool,
31    /// Absolute start of the tensor data section
32    tensor_data_offset: u64,
33    /// Relative byte span from the data-section start through the last tensor
34    tensor_data_span: u64,
35    /// Required alignment for tensor offsets
36    tensor_alignment: u64,
37    /// Maximum temporary buffer used while growing owned tensor data
38    tensor_buffer_size: usize,
39    /// Descriptor indices ordered by tensor data offset, then descriptor order
40    tensor_order: Vec<usize>,
41    /// Cursor into `tensor_order` for the next tensor to read
42    next_tensor: usize,
43}
44
45/// Configuration for stream reading
46#[derive(Debug, Clone)]
47pub struct StreamReaderConfig {
48    /// Preferred buffer size for tensor reads; temporary chunks are capped at 1 MiB.
49    pub buffer_size: usize,
50    /// Request checksum validation.
51    ///
52    /// GGUF has no standard checksum field; setting this to `true` returns
53    /// [`GGUFError::FeatureUnavailable`].
54    pub validate_checksums: bool,
55    /// Maximum number of serialized metadata bytes to read.
56    pub max_metadata_size: usize,
57    /// Maximum decoded allocation budget for metadata.
58    pub max_decoded_metadata_size: usize,
59    /// Maximum number of tensors to prevent DoS
60    pub max_tensor_count: usize,
61}
62
63impl Default for StreamReaderConfig {
64    fn default() -> Self {
65        Self {
66            buffer_size: 64 * 1024,              // 64KB
67            validate_checksums: false,           // GGUF v3 has no checksum field
68            max_metadata_size: 16 * 1024 * 1024, // 16MB
69            max_decoded_metadata_size: GGUF_MAX_METADATA_DECODED_SIZE,
70            max_tensor_count: 100_000,
71        }
72    }
73}
74
75impl<R: Read> GGUFStreamReader<R> {
76    /// Create a new stream reader with default configuration
77    pub fn new(reader: R) -> Result<Self> {
78        Self::with_config(reader, StreamReaderConfig::default())
79    }
80
81    /// Create a new stream reader with custom configuration
82    pub fn with_config(mut reader: R, config: StreamReaderConfig) -> Result<Self> {
83        if config.validate_checksums {
84            return Err(GGUFError::FeatureUnavailable("stream checksum validation".to_string()));
85        }
86        let mut position = 0u64;
87
88        // Read header
89        let header = GGUFHeader::read_from(&mut reader)?;
90        header.validate_comprehensive()?;
91        position = position
92            .checked_add(GGUFHeader::size() as u64)
93            .ok_or_else(|| GGUFError::Format("Stream position overflow".to_string()))?;
94
95        // Check limits
96        let max_tensor_count = u64::try_from(config.max_tensor_count).unwrap_or(u64::MAX);
97        if header.tensor_count > max_tensor_count {
98            return Err(GGUFError::Format(format!(
99                "Too many tensors: {} exceeds limit of {}",
100                header.tensor_count, config.max_tensor_count
101            )));
102        }
103
104        // Read metadata
105        let metadata = Metadata::read_from_with_limits(
106            &mut reader,
107            header.metadata_kv_count,
108            config.max_metadata_size,
109            config.max_decoded_metadata_size,
110        )?;
111        let metadata_size = metadata.checked_serialized_size().ok_or_else(|| {
112            GGUFError::InvalidMetadata("Serialized metadata size overflows usize".to_string())
113        })?;
114        position = position
115            .checked_add(u64::try_from(metadata_size).map_err(|_| {
116                GGUFError::InvalidMetadata("Metadata size does not fit u64".to_string())
117            })?)
118            .ok_or_else(|| GGUFError::Format("Stream position overflow".to_string()))?;
119        let tensor_alignment = u64::try_from(metadata.tensor_alignment()?).map_err(|_| {
120            GGUFError::InvalidMetadata("Tensor alignment does not fit u64".to_string())
121        })?;
122
123        // Read tensor information
124        let tensor_capacity = usize::try_from(header.tensor_count).map_err(|_| {
125            GGUFError::InvalidTensorData("Tensor count does not fit this platform".to_string())
126        })?;
127        let mut tensor_infos = Vec::new();
128        for _ in 0..header.tensor_count {
129            let tensor_info = TensorInfo::read_from(&mut reader)?;
130            let descriptor_size = tensor_info.checked_serialized_size().ok_or_else(|| {
131                GGUFError::InvalidTensorData("Tensor descriptor size overflows usize".to_string())
132            })?;
133            position = position
134                .checked_add(u64::try_from(descriptor_size).map_err(|_| {
135                    GGUFError::InvalidTensorData(
136                        "Tensor descriptor size does not fit u64".to_string(),
137                    )
138                })?)
139                .ok_or_else(|| GGUFError::Format("Stream position overflow".to_string()))?;
140
141            // Convert to our TensorInfo format
142            let shape = TensorShape::new(tensor_info.dimensions)?;
143            let tensor_type = TensorType::from_u32(tensor_info.tensor_type)?;
144
145            let new_tensor_info =
146                TensorInfoNew::new(tensor_info.name, shape, tensor_type, tensor_info.offset);
147            new_tensor_info.validate()?;
148            if !new_tensor_info.data_offset().is_multiple_of(tensor_alignment) {
149                return Err(GGUFError::InvalidTensorData(format!(
150                    "Tensor '{}' offset {} is not aligned to {} bytes",
151                    new_tensor_info.name(),
152                    new_tensor_info.data_offset(),
153                    tensor_alignment
154                )));
155            }
156            try_reserve_vec_slot(&mut tensor_infos, tensor_capacity, "tensor descriptor list")?;
157            tensor_infos.push(new_tensor_info);
158        }
159
160        let mut tensor_name_index = HashMap::new();
161        for (index, tensor_info) in tensor_infos.iter().enumerate() {
162            try_reserve_map_slot(&mut tensor_name_index, tensor_infos.len(), "tensor name index")?;
163            let name = try_clone_tensor_name(tensor_info.name())?;
164            if tensor_name_index.insert(name, index).is_some() {
165                return Err(GGUFError::InvalidTensorData(format!(
166                    "Duplicate tensor name: {}",
167                    tensor_info.name()
168                )));
169            }
170        }
171
172        let mut tensor_order = Vec::new();
173        tensor_order.try_reserve_exact(tensor_capacity).map_err(|_| {
174            GGUFError::InvalidTensorData("Unable to allocate tensor order".to_string())
175        })?;
176        tensor_order.extend(0..tensor_infos.len());
177        tensor_order.sort_unstable_by_key(|&index| (tensor_infos[index].data_offset(), index));
178
179        // Calculate alignment padding
180        let tensor_data_offset = checked_align_u64(position, tensor_alignment)?;
181        let tensor_data_span =
182            validate_tensor_layout(&tensor_infos, &tensor_order, tensor_data_offset)?;
183        let padding_size = usize::try_from(tensor_data_offset - position)
184            .map_err(|_| GGUFError::Format("Alignment padding does not fit usize".to_string()))?;
185        if padding_size > 0 {
186            read_discard(&mut reader, padding_size)?;
187            position = tensor_data_offset;
188        }
189
190        Ok(Self {
191            reader,
192            header,
193            metadata,
194            tensor_infos,
195            tensor_name_index,
196            position,
197            at_tensor_data: true,
198            tensor_data_offset,
199            tensor_data_span,
200            tensor_alignment,
201            tensor_buffer_size: config.buffer_size,
202            tensor_order,
203            next_tensor: 0,
204        })
205    }
206
207    /// Get the file header
208    pub fn header(&self) -> &GGUFHeader {
209        &self.header
210    }
211
212    /// Get the metadata
213    pub fn metadata(&self) -> &Metadata {
214        &self.metadata
215    }
216
217    /// Get tensor information
218    pub fn tensor_infos(&self) -> &[TensorInfoNew] {
219        &self.tensor_infos
220    }
221
222    /// Get a specific tensor info by name
223    pub fn get_tensor_info(&self, name: &str) -> Option<&TensorInfoNew> {
224        self.tensor_name_index.get(name).and_then(|&index| self.tensor_infos.get(index))
225    }
226
227    /// Get tensor names in order
228    pub fn tensor_names(&self) -> Vec<&str> {
229        self.tensor_infos.iter().map(|t| t.name()).collect()
230    }
231
232    /// Get the number of tensors
233    pub fn tensor_count(&self) -> usize {
234        self.tensor_infos.len()
235    }
236
237    /// Return the tensor alignment declared by file metadata.
238    pub fn tensor_alignment(&self) -> u64 {
239        self.tensor_alignment
240    }
241
242    /// Read the next tensor's data in stream order.
243    ///
244    /// Tensor payloads are returned as owned data and are not retained in the
245    /// descriptor list, keeping reader memory bounded by the current tensor.
246    pub fn read_next_tensor(&mut self) -> Result<Option<(String, TensorData)>> {
247        if !self.at_tensor_data {
248            return Ok(None);
249        }
250
251        if let Some(&index) = self.tensor_order.get(self.next_tensor) {
252            let tensor_info = &self.tensor_infos[index];
253            let data_size_u64 = tensor_info.checked_expected_data_size()?;
254            let data_size = usize::try_from(data_size_u64).map_err(|_| {
255                GGUFError::InvalidTensorData(format!(
256                    "Tensor '{}' size does not fit this platform",
257                    tensor_info.name()
258                ))
259            })?;
260            let target_position =
261                self.tensor_data_offset.checked_add(tensor_info.data_offset()).ok_or_else(
262                    || GGUFError::InvalidTensorData("Tensor offset overflows u64".to_string()),
263                )?;
264            let tensor_name = try_clone_tensor_name(tensor_info.name())?;
265
266            // Skip to the right position if needed
267            if self.position < target_position {
268                let skip_bytes =
269                    usize::try_from(target_position - self.position).map_err(|_| {
270                        GGUFError::InvalidTensorData("Tensor gap does not fit usize".to_string())
271                    })?;
272                self.skip_bytes(skip_bytes)?;
273            } else if self.position > target_position {
274                return Err(GGUFError::InvalidTensorData(format!(
275                    "Tensor '{}' starts at {}, before current stream position {}",
276                    tensor_name, target_position, self.position
277                )));
278            }
279
280            // Read tensor data
281            let data = read_exact_owned(&mut self.reader, data_size, self.tensor_buffer_size)?;
282            self.position = self.position.checked_add(data_size_u64).ok_or_else(|| {
283                GGUFError::InvalidTensorData("Stream position overflows u64".to_string())
284            })?;
285
286            self.next_tensor = self.next_tensor.checked_add(1).ok_or_else(|| {
287                GGUFError::InvalidTensorData("Tensor stream cursor overflows usize".to_string())
288            })?;
289
290            Ok(Some((tensor_name, TensorData::new_owned(data))))
291        } else {
292            Ok(None)
293        }
294    }
295
296    /// Read all tensors in stream order
297    pub fn read_all_tensors(&mut self) -> Result<HashMap<String, TensorData>> {
298        let mut tensors = HashMap::new();
299        let remaining = self.tensor_order.len().saturating_sub(self.next_tensor);
300        tensors.try_reserve(remaining).map_err(|_| {
301            GGUFError::InvalidTensorData("Unable to allocate tensor data map".to_string())
302        })?;
303
304        while let Some((name, data)) = self.read_next_tensor()? {
305            tensors.insert(name, data);
306        }
307
308        Ok(tensors)
309    }
310
311    /// Skip a certain number of bytes in the stream
312    fn skip_bytes(&mut self, count: usize) -> Result<()> {
313        const SKIP_BUFFER_SIZE: usize = 8192;
314        let mut buffer = [0u8; SKIP_BUFFER_SIZE];
315        let mut remaining = count;
316
317        while remaining > 0 {
318            let to_read = remaining.min(SKIP_BUFFER_SIZE);
319            self.reader.read_exact(&mut buffer[..to_read])?;
320            remaining -= to_read;
321            self.position = self
322                .position
323                .checked_add(to_read as u64)
324                .ok_or_else(|| GGUFError::Format("Stream position overflow".to_string()))?;
325        }
326
327        Ok(())
328    }
329
330    /// Get current position in the stream
331    pub fn position(&self) -> u64 {
332        self.position
333    }
334
335    /// Check if we're at the tensor data section
336    pub fn at_tensor_data(&self) -> bool {
337        self.at_tensor_data
338    }
339
340    /// Create a streaming iterator over tensors
341    pub fn tensor_iterator(self) -> TensorIterator<R> {
342        TensorIterator::new(self)
343    }
344
345    /// Get a summary of what we've read so far
346    pub fn summary(&self) -> StreamReaderSummary {
347        let tensor_types: HashMap<TensorType, usize> = {
348            let mut types = HashMap::new();
349            for tensor_info in &self.tensor_infos {
350                *types.entry(tensor_info.tensor_type()).or_insert(0) += 1;
351            }
352            types
353        };
354
355        let total_tensor_size = self
356            .tensor_infos
357            .iter()
358            .fold(0u64, |total, tensor| total.saturating_add(tensor.expected_data_size()));
359
360        StreamReaderSummary {
361            header: self.header.clone(),
362            metadata_count: self.metadata.len(),
363            tensor_count: self.tensor_infos.len(),
364            total_tensor_size,
365            current_position: self.position,
366            tensor_data_offset: self.tensor_data_offset,
367            tensor_data_span: self.tensor_data_span,
368            tensor_types,
369        }
370    }
371
372    /// Validate the stream data we've read so far
373    pub fn validate(&self) -> Result<()> {
374        // Validate header consistency
375        if self.header.tensor_count as usize != self.tensor_infos.len() {
376            return Err(GGUFError::Format(
377                "Header tensor count doesn't match actual tensor count".to_string(),
378            ));
379        }
380
381        if self.header.metadata_kv_count as usize != self.metadata.len() {
382            return Err(GGUFError::Format(
383                "Header metadata count doesn't match actual metadata count".to_string(),
384            ));
385        }
386
387        // Validate tensor infos
388        for tensor_info in &self.tensor_infos {
389            tensor_info.validate()?;
390        }
391
392        Ok(())
393    }
394
395    /// Convert to underlying reader (consuming the stream reader)
396    pub fn into_inner(self) -> R {
397        self.reader
398    }
399}
400
401fn read_discard<R: Read>(reader: &mut R, mut size: usize) -> Result<()> {
402    const BUFFER_SIZE: usize = 8192;
403    let mut buffer = [0u8; BUFFER_SIZE];
404    while size > 0 {
405        let chunk = size.min(BUFFER_SIZE);
406        reader.read_exact(&mut buffer[..chunk])?;
407        size -= chunk;
408    }
409    Ok(())
410}
411
412fn read_exact_owned<R: Read>(reader: &mut R, size: usize, buffer_size: usize) -> Result<Vec<u8>> {
413    if size == 0 {
414        return Ok(Vec::new());
415    }
416    if buffer_size == 0 {
417        return Err(GGUFError::InvalidTensorData(
418            "Stream reader buffer size must be greater than zero".to_string(),
419        ));
420    }
421
422    let chunk_size = size.min(buffer_size).min(MAX_READER_CHUNK_SIZE);
423    let mut chunk = Vec::new();
424    chunk.try_reserve_exact(chunk_size).map_err(|_| {
425        GGUFError::InvalidTensorData("Unable to allocate tensor read chunk".to_string())
426    })?;
427    chunk.resize(chunk_size, 0);
428    let mut data = Vec::new();
429    let mut remaining = size;
430    while remaining > 0 {
431        let to_read = remaining.min(chunk.len());
432        reader.read_exact(&mut chunk[..to_read])?;
433        data.try_reserve(to_read).map_err(|_| {
434            GGUFError::InvalidTensorData("Unable to allocate tensor data buffer".to_string())
435        })?;
436        data.extend_from_slice(&chunk[..to_read]);
437        remaining -= to_read;
438    }
439    Ok(data)
440}
441
442fn try_clone_tensor_name(name: &str) -> Result<String> {
443    let mut owned = String::new();
444    owned
445        .try_reserve_exact(name.len())
446        .map_err(|_| GGUFError::InvalidTensorData("Unable to allocate tensor name".to_string()))?;
447    owned.push_str(name);
448    Ok(owned)
449}
450
451fn try_reserve_vec_slot<T>(values: &mut Vec<T>, total: usize, description: &str) -> Result<()> {
452    if values.len() < values.capacity() {
453        return Ok(());
454    }
455    let target = values.capacity().saturating_mul(2).max(MIN_TENSOR_TRACKING_CAPACITY).min(total);
456    values
457        .try_reserve_exact(target.saturating_sub(values.len()))
458        .map_err(|_| GGUFError::InvalidTensorData(format!("Unable to allocate {description}")))
459}
460
461fn try_reserve_map_slot<K: std::hash::Hash + Eq, V>(
462    values: &mut HashMap<K, V>,
463    total: usize,
464    description: &str,
465) -> Result<()> {
466    if values.len() < values.capacity() {
467        return Ok(());
468    }
469    let target = values.capacity().saturating_mul(2).max(MIN_TENSOR_TRACKING_CAPACITY).min(total);
470    values
471        .try_reserve(target.saturating_sub(values.len()))
472        .map_err(|_| GGUFError::InvalidTensorData(format!("Unable to allocate {description}")))
473}
474
475fn validate_tensor_layout(
476    tensor_infos: &[TensorInfoNew],
477    tensor_order: &[usize],
478    tensor_data_offset: u64,
479) -> Result<u64> {
480    let mut previous: Option<(u64, &str)> = None;
481    let mut tensor_data_span = 0u64;
482    for &index in tensor_order {
483        let tensor = tensor_infos.get(index).ok_or_else(|| {
484            GGUFError::InvalidTensorData("Tensor order index is out of bounds".to_string())
485        })?;
486        let end = tensor
487            .data_offset()
488            .checked_add(tensor.checked_expected_data_size()?)
489            .ok_or_else(|| {
490                GGUFError::InvalidTensorData(format!(
491                    "Tensor '{}' range overflows u64",
492                    tensor.name()
493                ))
494            })?;
495        tensor_data_offset.checked_add(end).ok_or_else(|| {
496            GGUFError::InvalidTensorData(format!(
497                "Tensor '{}' absolute range overflows u64",
498                tensor.name()
499            ))
500        })?;
501        if let Some((previous_end, previous_name)) = previous {
502            if previous_end > tensor.data_offset() {
503                return Err(GGUFError::InvalidTensorData(format!(
504                    "Tensor data overlap detected: '{}' ends at {}, after '{}' starts at {}",
505                    previous_name,
506                    previous_end,
507                    tensor.name(),
508                    tensor.data_offset()
509                )));
510            }
511        }
512        tensor_data_span = tensor_data_span.max(end);
513        previous = Some((end, tensor.name()));
514    }
515    Ok(tensor_data_span)
516}
517
518/// Iterator over tensors in a stream
519pub struct TensorIterator<R> {
520    reader: GGUFStreamReader<R>,
521    finished: bool,
522}
523
524impl<R: Read> TensorIterator<R> {
525    fn new(reader: GGUFStreamReader<R>) -> Self {
526        Self { reader, finished: false }
527    }
528}
529
530impl<R: Read> Iterator for TensorIterator<R> {
531    type Item = Result<(String, TensorData)>;
532
533    fn next(&mut self) -> Option<Self::Item> {
534        if self.finished {
535            return None;
536        }
537
538        match self.reader.read_next_tensor() {
539            Ok(Some(tensor)) => Some(Ok(tensor)),
540            Ok(None) => {
541                self.finished = true;
542                None
543            }
544            Err(e) => {
545                self.finished = true;
546                Some(Err(e))
547            }
548        }
549    }
550}
551
552/// Summary of stream reading progress
553#[derive(Debug, Clone)]
554pub struct StreamReaderSummary {
555    /// File header
556    pub header: GGUFHeader,
557    /// Number of metadata entries
558    pub metadata_count: usize,
559    /// Total number of tensors
560    pub tensor_count: usize,
561    /// Total size of all tensor data
562    pub total_tensor_size: u64,
563    /// Current position in stream
564    pub current_position: u64,
565    /// Absolute start of the tensor data section
566    pub tensor_data_offset: u64,
567    /// Relative span through the end of the last tensor, including gaps
568    pub tensor_data_span: u64,
569    /// Count of each tensor type
570    pub tensor_types: HashMap<TensorType, usize>,
571}
572
573impl StreamReaderSummary {
574    /// Percentage of the tensor-data span consumed by the stream reader.
575    pub fn progress_percentage(&self) -> f64 {
576        if self.tensor_data_span == 0 {
577            return 100.0;
578        }
579
580        let consumed = self
581            .current_position
582            .saturating_sub(self.tensor_data_offset)
583            .min(self.tensor_data_span);
584        (consumed as f64 / self.tensor_data_span as f64) * 100.0
585    }
586}
587
588/// Convenience function to create a stream reader from any Read type
589pub fn stream_reader_from_read<R: Read>(reader: R) -> Result<GGUFStreamReader<R>> {
590    GGUFStreamReader::new(reader)
591}
592
593/// Convenience function to create a buffered stream reader
594pub fn buffered_stream_reader<R: Read>(reader: R) -> Result<GGUFStreamReader<BufReader<R>>> {
595    let buf_reader = BufReader::new(reader);
596    GGUFStreamReader::new(buf_reader)
597}
598
599impl std::fmt::Display for StreamReaderSummary {
600    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
601        writeln!(f, "GGUF Stream Summary:")?;
602        writeln!(f, "  Version: {}", self.header.version)?;
603        writeln!(f, "  Tensors: {}", self.tensor_count)?;
604        writeln!(f, "  Metadata entries: {}", self.metadata_count)?;
605        writeln!(f, "  Total tensor size: {} bytes", self.total_tensor_size)?;
606        writeln!(f, "  Tensor data span: {} bytes", self.tensor_data_span)?;
607        writeln!(f, "  Current position: {} bytes", self.current_position)?;
608        writeln!(f, "  Progress: {:.1}%", self.progress_percentage())?;
609        writeln!(f, "  Tensor types:")?;
610
611        for (tensor_type, count) in &self.tensor_types {
612            writeln!(f, "    {}: {}", tensor_type.name(), count)?;
613        }
614
615        Ok(())
616    }
617}
618
619fn checked_align_u64(position: u64, alignment: u64) -> Result<u64> {
620    if alignment == 0 {
621        return Err(GGUFError::InvalidMetadata("Tensor alignment cannot be zero".to_string()));
622    }
623    let remainder = position % alignment;
624    let padding = if remainder == 0 { 0 } else { alignment - remainder };
625    position
626        .checked_add(padding)
627        .ok_or_else(|| GGUFError::Format("Tensor data offset overflows u64".to_string()))
628}
629
630#[cfg(all(test, feature = "std"))]
631mod tests {
632    use super::*;
633    use crate::format::constants::*;
634    use std::io::Cursor;
635
636    fn create_stream_gguf_data() -> Vec<u8> {
637        let mut data = Vec::new();
638
639        // Header
640        data.extend_from_slice(&GGUF_MAGIC.to_le_bytes());
641        data.extend_from_slice(&GGUF_VERSION.to_le_bytes());
642        data.extend_from_slice(&2u64.to_le_bytes()); // 2 tensors
643        data.extend_from_slice(&1u64.to_le_bytes()); // 1 metadata entry
644
645        // Metadata
646        data.extend_from_slice(&4u64.to_le_bytes()); // key length
647        data.extend_from_slice(b"name"); // key
648        data.extend_from_slice(&8u32.to_le_bytes()); // string type
649        data.extend_from_slice(&5u64.to_le_bytes()); // value length
650        data.extend_from_slice(b"model"); // value
651
652        // Store positions where we'll write offsets
653        let mut offset_positions = Vec::new();
654
655        // Tensor info 1
656        data.extend_from_slice(&8u64.to_le_bytes()); // name length
657        data.extend_from_slice(b"tensor_a"); // name
658        data.extend_from_slice(&1u32.to_le_bytes()); // 1 dimension
659        data.extend_from_slice(&4u64.to_le_bytes()); // dim 0
660        data.extend_from_slice(&0u32.to_le_bytes()); // F32 type
661        offset_positions.push(data.len()); // Remember where offset goes
662        data.extend_from_slice(&0u64.to_le_bytes()); // offset placeholder
663
664        // Tensor info 2
665        data.extend_from_slice(&8u64.to_le_bytes()); // name length
666        data.extend_from_slice(b"tensor_b"); // name
667        data.extend_from_slice(&1u32.to_le_bytes()); // 1 dimension
668        data.extend_from_slice(&3u64.to_le_bytes()); // dim 0
669        data.extend_from_slice(&0u32.to_le_bytes()); // F32 type
670        offset_positions.push(data.len()); // Remember where offset goes
671        data.extend_from_slice(&0u64.to_le_bytes()); // offset placeholder
672
673        // Align to 32 bytes
674        while data.len() % 32 != 0 {
675            data.push(0);
676        }
677
678        // Tensor offsets are relative to the aligned tensor data section.
679        let tensor_a_pos = offset_positions[0];
680        data[tensor_a_pos..tensor_a_pos + 8].copy_from_slice(&0u64.to_le_bytes());
681
682        // Tensor B starts on the next 32-byte boundary after tensor A.
683        let tensor_b_pos = offset_positions[1];
684        data[tensor_b_pos..tensor_b_pos + 8].copy_from_slice(&32u64.to_le_bytes());
685
686        // Tensor data A (4 F32 = 16 bytes)
687        data.extend_from_slice(&[0xAA; 16]);
688
689        // Inter-tensor alignment padding
690        data.extend_from_slice(&[0u8; 16]);
691
692        // Tensor data B (3 F32 = 12 bytes)
693        data.extend_from_slice(&[0xBB; 12]);
694
695        data
696    }
697
698    fn create_out_of_order_stream_gguf_data() -> Vec<u8> {
699        let mut data = create_stream_gguf_data();
700        let tensor_a_name = data.windows(8).position(|window| window == b"tensor_a").unwrap();
701        let tensor_b_name = data.windows(8).position(|window| window == b"tensor_b").unwrap();
702        let tensor_a_start = tensor_a_name - 8;
703        let tensor_b_start = tensor_b_name - 8;
704        let descriptor_size = tensor_b_start - tensor_a_start;
705        let tensor_a = data[tensor_a_start..tensor_a_start + descriptor_size].to_vec();
706        let tensor_b = data[tensor_b_start..tensor_b_start + descriptor_size].to_vec();
707        data[tensor_a_start..tensor_a_start + descriptor_size].copy_from_slice(&tensor_b);
708        data[tensor_b_start..tensor_b_start + descriptor_size].copy_from_slice(&tensor_a);
709        data
710    }
711
712    fn set_tensor_offset(data: &mut [u8], name: &[u8; 8], offset: u64) {
713        let name_position = data.windows(name.len()).position(|window| window == name).unwrap();
714        let offset_position = name_position + name.len() + 4 + 8 + 4;
715        data[offset_position..offset_position + 8].copy_from_slice(&offset.to_le_bytes());
716    }
717
718    #[test]
719    fn test_stream_reader_creation() {
720        let data = create_stream_gguf_data();
721        let cursor = Cursor::new(data);
722
723        let reader = GGUFStreamReader::new(cursor).unwrap();
724        assert_eq!(reader.tensor_count(), 2);
725        assert_eq!(reader.metadata().len(), 1);
726        assert!(reader.at_tensor_data());
727    }
728
729    #[test]
730    fn test_stream_reader_config() {
731        let data = create_stream_gguf_data();
732        let cursor = Cursor::new(data);
733
734        let config = StreamReaderConfig {
735            buffer_size: 1024,
736            validate_checksums: false,
737            max_metadata_size: 1024,
738            max_decoded_metadata_size: 1024,
739            max_tensor_count: 10,
740        };
741
742        let reader = GGUFStreamReader::with_config(cursor, config).unwrap();
743        assert_eq!(reader.tensor_count(), 2);
744
745        let unsupported = StreamReaderConfig { validate_checksums: true, ..Default::default() };
746        assert!(matches!(
747            GGUFStreamReader::with_config(Cursor::new(create_stream_gguf_data()), unsupported),
748            Err(GGUFError::FeatureUnavailable(_))
749        ));
750    }
751
752    #[test]
753    fn test_stream_metadata_budgets_are_configurable() {
754        let serialized_error = GGUFStreamReader::with_config(
755            Cursor::new(create_stream_gguf_data()),
756            StreamReaderConfig { max_metadata_size: 1, ..Default::default() },
757        )
758        .unwrap_err();
759        assert!(serialized_error.to_string().contains("Metadata exceeds byte limit"));
760
761        let decoded_error = GGUFStreamReader::with_config(
762            Cursor::new(create_stream_gguf_data()),
763            StreamReaderConfig { max_decoded_metadata_size: 0, ..Default::default() },
764        )
765        .unwrap_err();
766        assert!(decoded_error.to_string().contains("Decoded metadata allocation exceeds budget"));
767    }
768
769    #[test]
770    fn test_read_next_tensor() {
771        let data = create_stream_gguf_data();
772        let cursor = Cursor::new(data);
773
774        let mut reader = GGUFStreamReader::new(cursor).unwrap();
775
776        // Read first tensor
777        let result = reader.read_next_tensor().unwrap();
778        assert!(result.is_some());
779        let (name, tensor_data) = result.unwrap();
780        assert_eq!(name, "tensor_a");
781        assert_eq!(tensor_data.len(), 16);
782
783        // Read second tensor
784        let result = reader.read_next_tensor().unwrap();
785        assert!(result.is_some());
786        let (name, tensor_data) = result.unwrap();
787        assert_eq!(name, "tensor_b");
788        assert_eq!(tensor_data.len(), 12);
789
790        // No more tensors
791        let result = reader.read_next_tensor().unwrap();
792        assert!(result.is_none());
793    }
794
795    #[test]
796    fn test_read_next_tensor_uses_precomputed_data_order() {
797        let data = create_out_of_order_stream_gguf_data();
798        let mut reader = GGUFStreamReader::new(Cursor::new(data)).unwrap();
799
800        assert_eq!(reader.tensor_order, [1, 0]);
801        assert_eq!(reader.tensor_name_index["tensor_b"], 0);
802        assert_eq!(reader.tensor_name_index["tensor_a"], 1);
803        let (first_name, first_data) = reader.read_next_tensor().unwrap().unwrap();
804        assert_eq!(first_name, "tensor_a");
805        assert_eq!(first_data.as_slice(), &[0xAA; 16]);
806        assert_eq!(reader.next_tensor, 1);
807
808        let (second_name, second_data) = reader.read_next_tensor().unwrap().unwrap();
809        assert_eq!(second_name, "tensor_b");
810        assert_eq!(second_data.as_slice(), &[0xBB; 12]);
811        assert_eq!(reader.next_tensor, 2);
812        assert!(reader.read_next_tensor().unwrap().is_none());
813    }
814
815    #[test]
816    fn test_streamed_payload_is_not_retained_by_reader() {
817        let data = create_stream_gguf_data();
818        let mut reader = GGUFStreamReader::new(Cursor::new(data)).unwrap();
819
820        let (name, returned) = reader.read_next_tensor().unwrap().unwrap();
821        assert!(matches!(returned, TensorData::Owned(_)));
822        assert!(reader.get_tensor_info(&name).unwrap().data().is_none());
823        assert!(reader.tensor_infos().iter().all(|tensor| !tensor.has_data()));
824    }
825
826    #[test]
827    fn test_duplicate_tensor_names_are_rejected() {
828        let mut data = create_stream_gguf_data();
829        let tensor_b_name = data.windows(8).position(|window| window == b"tensor_b").unwrap();
830        data[tensor_b_name..tensor_b_name + 8].copy_from_slice(b"tensor_a");
831
832        let error = GGUFStreamReader::new(Cursor::new(data)).unwrap_err();
833        assert!(matches!(
834            error,
835            GGUFError::InvalidTensorData(message) if message.contains("Duplicate tensor name")
836        ));
837    }
838
839    #[test]
840    fn test_overlapping_tensor_ranges_are_rejected_during_construction() {
841        let mut data = create_stream_gguf_data();
842        set_tensor_offset(&mut data, b"tensor_b", 0);
843
844        let error = GGUFStreamReader::new(Cursor::new(data)).unwrap_err();
845        assert!(matches!(
846            error,
847            GGUFError::InvalidTensorData(message) if message.contains("overlap")
848        ));
849    }
850
851    #[test]
852    fn test_overflowing_tensor_range_is_rejected_during_construction() {
853        let mut data = create_stream_gguf_data();
854        set_tensor_offset(&mut data, b"tensor_a", u64::MAX - 31);
855
856        let error = GGUFStreamReader::new(Cursor::new(data)).unwrap_err();
857        assert!(matches!(
858            error,
859            GGUFError::InvalidTensorData(message) if message.contains("range overflows")
860        ));
861    }
862
863    #[test]
864    fn test_tensor_tracking_growth_is_bounded() {
865        let mut descriptors = Vec::<u8>::new();
866        try_reserve_vec_slot(&mut descriptors, usize::MAX, "test descriptors").unwrap();
867        assert!(descriptors.capacity() >= MIN_TENSOR_TRACKING_CAPACITY);
868        assert!(descriptors.capacity() < 1024);
869
870        let mut names = HashMap::<u8, usize>::new();
871        try_reserve_map_slot(&mut names, usize::MAX, "test names").unwrap();
872        assert!(names.capacity() >= MIN_TENSOR_TRACKING_CAPACITY);
873        assert!(names.capacity() < 1024);
874    }
875
876    #[test]
877    fn test_read_all_tensors() {
878        let data = create_stream_gguf_data();
879        let cursor = Cursor::new(data);
880
881        let mut reader = GGUFStreamReader::new(cursor).unwrap();
882        let tensors = reader.read_all_tensors().unwrap();
883
884        assert_eq!(tensors.len(), 2);
885        assert!(tensors.contains_key("tensor_a"));
886        assert!(tensors.contains_key("tensor_b"));
887        assert_eq!(tensors["tensor_a"].len(), 16);
888        assert_eq!(tensors["tensor_b"].len(), 12);
889    }
890
891    #[test]
892    fn test_tensor_iterator() {
893        let data = create_stream_gguf_data();
894        let cursor = Cursor::new(data);
895
896        let reader = GGUFStreamReader::new(cursor).unwrap();
897        let mut iterator = reader.tensor_iterator();
898
899        // First tensor
900        let first = iterator.next().unwrap().unwrap();
901        assert_eq!(first.0, "tensor_a");
902        assert_eq!(first.1.len(), 16);
903
904        // Second tensor
905        let second = iterator.next().unwrap().unwrap();
906        assert_eq!(second.0, "tensor_b");
907        assert_eq!(second.1.len(), 12);
908
909        // End of iteration
910        assert!(iterator.next().is_none());
911    }
912
913    #[test]
914    fn test_stream_summary() {
915        let data = create_stream_gguf_data();
916        let cursor = Cursor::new(data);
917
918        let mut reader = GGUFStreamReader::new(cursor).unwrap();
919        let summary = reader.summary();
920
921        assert_eq!(summary.tensor_count, 2);
922        assert_eq!(summary.metadata_count, 1);
923        assert_eq!(summary.total_tensor_size, 28); // 16 + 12 bytes
924        assert_eq!(summary.tensor_data_span, 44); // 16 bytes, 16-byte gap, 12 bytes
925        assert_eq!(summary.current_position, summary.tensor_data_offset);
926        assert_eq!(summary.progress_percentage(), 0.0);
927        assert!(summary.tensor_types.contains_key(&TensorType::F32));
928
929        while reader.read_next_tensor().unwrap().is_some() {}
930        let completed = reader.summary();
931        assert_eq!(completed.current_position, completed.tensor_data_offset + 44);
932        assert_eq!(completed.progress_percentage(), 100.0);
933    }
934
935    #[test]
936    fn test_stream_validation() {
937        let data = create_stream_gguf_data();
938        let cursor = Cursor::new(data);
939
940        let reader = GGUFStreamReader::new(cursor).unwrap();
941        assert!(reader.validate().is_ok());
942    }
943
944    #[test]
945    fn test_limits_exceeded() {
946        let data = create_stream_gguf_data();
947        let cursor = Cursor::new(data);
948
949        let config = StreamReaderConfig {
950            max_tensor_count: 1, // Only allow 1 tensor, but we have 2
951            ..Default::default()
952        };
953
954        let result = GGUFStreamReader::with_config(cursor, config);
955        assert!(result.is_err());
956    }
957
958    #[test]
959    fn test_convenience_functions() {
960        let data = create_stream_gguf_data();
961
962        // Test stream_reader_from_read
963        let cursor = Cursor::new(data.clone());
964        let reader = stream_reader_from_read(cursor).unwrap();
965        assert_eq!(reader.tensor_count(), 2);
966
967        // Test buffered_stream_reader
968        let cursor = Cursor::new(data);
969        let reader = buffered_stream_reader(cursor).unwrap();
970        assert_eq!(reader.tensor_count(), 2);
971    }
972
973    #[test]
974    fn test_summary_display() {
975        let data = create_stream_gguf_data();
976        let cursor = Cursor::new(data);
977
978        let reader = GGUFStreamReader::new(cursor).unwrap();
979        let summary = reader.summary();
980        let display_str = format!("{}", summary);
981
982        assert!(display_str.contains("GGUF Stream Summary"));
983        assert!(display_str.contains("Tensors: 2"));
984        assert!(display_str.contains("F32"));
985    }
986
987    #[test]
988    fn test_into_inner() {
989        let data = create_stream_gguf_data();
990        let cursor = Cursor::new(data);
991
992        let reader = GGUFStreamReader::new(cursor).unwrap();
993        let _inner = reader.into_inner(); // Should consume the reader successfully
994    }
995}