Skip to main content

netcdf_reader/classic/
data.rs

1//! Data reading for classic (CDF-1/2/5) NetCDF files.
2//!
3//! Two layout types:
4//! - **Non-record variables**: contiguous data at the offset stored in the variable header.
5//! - **Record variables**: data is interleaved across records. Each record contains one
6//!   slice from every record variable, in the order they appear in the header. The total
7//!   record size is the sum of all record variables' vsize values (each padded to 4-byte
8//!   boundary in CDF-1/2).
9
10use ndarray::{ArrayD, IxDyn};
11
12use crate::error::{Error, Result};
13use crate::types::{NcType, NcVariable};
14
15/// Trait for types that can be read from classic NetCDF data.
16pub trait NcReadType: Clone + Default + Send + 'static {
17    /// The NetCDF type this Rust type corresponds to.
18    fn nc_type() -> NcType;
19
20    /// Read a single element from big-endian bytes.
21    fn from_be_bytes(bytes: &[u8]) -> Result<Self>;
22
23    /// Size in bytes of one element.
24    fn element_size() -> usize;
25
26    /// Bulk decode `count` elements from a contiguous big-endian byte slice.
27    ///
28    /// Default implementation falls back to per-element decoding. Types with
29    /// multi-byte elements override this with an optimized bulk path using
30    /// `chunks_exact` + byte-swap (on LE hosts) or `copy_nonoverlapping`
31    /// (on BE hosts).
32    fn decode_bulk_be(raw: &[u8], count: usize) -> Result<Vec<Self>> {
33        let elem_size = Self::element_size();
34        let needed = count.checked_mul(elem_size).ok_or_else(|| {
35            Error::InvalidData("classic decode byte count exceeds platform usize".to_string())
36        })?;
37        if raw.len() < needed {
38            return Err(Error::InvalidData(format!(
39                "need {} bytes for {} elements, got {}",
40                needed,
41                count,
42                raw.len()
43            )));
44        }
45        let mut values = Vec::with_capacity(count);
46        for i in 0..count {
47            let start = i * elem_size;
48            values.push(Self::from_be_bytes(&raw[start..start + elem_size])?);
49        }
50        Ok(values)
51    }
52
53    /// Bulk decode elements from a contiguous big-endian byte slice into a
54    /// caller-provided destination buffer.
55    fn decode_bulk_be_into(raw: &[u8], dst: &mut [Self]) -> Result<()> {
56        let elem_size = Self::element_size();
57        let needed = dst.len().checked_mul(elem_size).ok_or_else(|| {
58            Error::InvalidData("classic decode byte count exceeds platform usize".to_string())
59        })?;
60        if raw.len() < needed {
61            return Err(Error::InvalidData(format!(
62                "need {} bytes for {} elements, got {}",
63                needed,
64                dst.len(),
65                raw.len()
66            )));
67        }
68        for (out, chunk) in dst.iter_mut().zip(raw[..needed].chunks_exact(elem_size)) {
69            *out = Self::from_be_bytes(chunk)?;
70        }
71        Ok(())
72    }
73}
74
75macro_rules! impl_nc_read_type {
76    ($ty:ty, $nc_type:expr, $size:expr) => {
77        impl NcReadType for $ty {
78            fn nc_type() -> NcType {
79                $nc_type
80            }
81
82            fn from_be_bytes(bytes: &[u8]) -> Result<Self> {
83                if bytes.len() < $size {
84                    return Err(Error::InvalidData(format!(
85                        "need {} bytes for {}, got {}",
86                        $size,
87                        stringify!($ty),
88                        bytes.len()
89                    )));
90                }
91                let mut arr = [0u8; $size];
92                arr.copy_from_slice(&bytes[..$size]);
93                Ok(<$ty>::from_be_bytes(arr))
94            }
95
96            fn element_size() -> usize {
97                $size
98            }
99
100            fn decode_bulk_be(raw: &[u8], count: usize) -> Result<Vec<Self>> {
101                let total_bytes = count.checked_mul($size).ok_or_else(|| {
102                    Error::InvalidData(
103                        "classic decode byte count exceeds platform usize".to_string(),
104                    )
105                })?;
106                if raw.len() < total_bytes {
107                    return Err(Error::InvalidData(format!(
108                        "need {} bytes for {} elements of {}, got {}",
109                        total_bytes,
110                        count,
111                        stringify!($ty),
112                        raw.len()
113                    )));
114                }
115                let bytes = &raw[..total_bytes];
116                #[cfg(target_endian = "big")]
117                {
118                    // Native BE: memcpy is safe for any element size.
119                    let mut values = Vec::<$ty>::with_capacity(count);
120                    unsafe {
121                        std::ptr::copy_nonoverlapping(
122                            bytes.as_ptr(),
123                            values.as_mut_ptr() as *mut u8,
124                            total_bytes,
125                        );
126                        values.set_len(count);
127                    }
128                    Ok(values)
129                }
130                #[cfg(target_endian = "little")]
131                {
132                    // LE host reading BE data: chunks_exact + byte-swap.
133                    Ok(bytes
134                        .chunks_exact($size)
135                        .map(|chunk| {
136                            let mut arr = [0u8; $size];
137                            arr.copy_from_slice(chunk);
138                            <$ty>::from_be_bytes(arr)
139                        })
140                        .collect())
141                }
142            }
143
144            fn decode_bulk_be_into(raw: &[u8], dst: &mut [Self]) -> Result<()> {
145                let total_bytes = dst.len().checked_mul($size).ok_or_else(|| {
146                    Error::InvalidData(
147                        "classic decode byte count exceeds platform usize".to_string(),
148                    )
149                })?;
150                if raw.len() < total_bytes {
151                    return Err(Error::InvalidData(format!(
152                        "need {} bytes for {} elements of {}, got {}",
153                        total_bytes,
154                        dst.len(),
155                        stringify!($ty),
156                        raw.len()
157                    )));
158                }
159                let bytes = &raw[..total_bytes];
160                #[cfg(target_endian = "big")]
161                {
162                    unsafe {
163                        std::ptr::copy_nonoverlapping(
164                            bytes.as_ptr(),
165                            dst.as_mut_ptr() as *mut u8,
166                            total_bytes,
167                        );
168                    }
169                    Ok(())
170                }
171                #[cfg(target_endian = "little")]
172                {
173                    for (out, chunk) in dst.iter_mut().zip(bytes.chunks_exact($size)) {
174                        let mut arr = [0u8; $size];
175                        arr.copy_from_slice(chunk);
176                        *out = <$ty>::from_be_bytes(arr);
177                    }
178                    Ok(())
179                }
180            }
181        }
182    };
183}
184
185impl_nc_read_type!(i8, NcType::Byte, 1);
186impl_nc_read_type!(i16, NcType::Short, 2);
187impl_nc_read_type!(i32, NcType::Int, 4);
188impl_nc_read_type!(f32, NcType::Float, 4);
189impl_nc_read_type!(f64, NcType::Double, 8);
190impl_nc_read_type!(u8, NcType::UByte, 1);
191impl_nc_read_type!(u16, NcType::UShort, 2);
192impl_nc_read_type!(u32, NcType::UInt, 4);
193impl_nc_read_type!(i64, NcType::Int64, 8);
194impl_nc_read_type!(u64, NcType::UInt64, 8);
195
196/// Read the entire data for a non-record variable into an ndarray.
197///
198/// The data is located at a contiguous region starting at `var.data_offset`
199/// with total size `var.data_size`.
200pub fn read_non_record_variable<T: NcReadType>(
201    file_data: &[u8],
202    var: &NcVariable,
203) -> Result<ArrayD<T>> {
204    if var.is_record_var {
205        return Err(Error::InvalidData(
206            "use read_record_variable for record variables".to_string(),
207        ));
208    }
209
210    let offset = crate::types::checked_usize_from_u64(var.data_offset, "variable data offset")?;
211    let total_elements = checked_non_record_element_count(var)?;
212    let elem_size = T::element_size();
213    let total_bytes = total_elements.checked_mul(elem_size).ok_or_else(|| {
214        Error::InvalidData(format!(
215            "variable '{}' size in bytes exceeds platform usize",
216            var.name
217        ))
218    })?;
219
220    let end = offset.checked_add(total_bytes).ok_or_else(|| {
221        Error::InvalidData(format!(
222            "variable '{}' byte range exceeds platform usize",
223            var.name
224        ))
225    })?;
226    if end > file_data.len() {
227        return Err(Error::InvalidData(format!(
228            "variable '{}' data extends beyond file: offset={}, size={}, file_len={}",
229            var.name,
230            offset,
231            total_bytes,
232            file_data.len()
233        )));
234    }
235
236    let data_slice = &file_data[offset..end];
237    let values = T::decode_bulk_be(data_slice, total_elements)?;
238
239    let shape: Vec<usize> = var
240        .shape()
241        .iter()
242        .map(|&s| crate::types::checked_usize_from_u64(s, "variable dimension"))
243        .collect::<Result<Vec<_>>>()?;
244    if shape.is_empty() {
245        // Scalar variable.
246        ArrayD::from_shape_vec(IxDyn(&[]), values)
247    } else {
248        ArrayD::from_shape_vec(IxDyn(&shape), values)
249    }
250    .map_err(|e| Error::InvalidData(format!("failed to create array: {}", e)))
251}
252
253/// Read the entire data for a non-record variable into a caller-provided buffer.
254pub fn read_non_record_variable_into<T: NcReadType>(
255    file_data: &[u8],
256    var: &NcVariable,
257    dst: &mut [T],
258) -> Result<()> {
259    if var.is_record_var {
260        return Err(Error::InvalidData(
261            "use read_record_variable_into for record variables".to_string(),
262        ));
263    }
264
265    let total_elements = checked_non_record_element_count(var)?;
266    if dst.len() != total_elements {
267        return Err(Error::InvalidData(format!(
268            "destination has {} elements, variable '{}' requires {}",
269            dst.len(),
270            var.name,
271            total_elements
272        )));
273    }
274
275    let offset = crate::types::checked_usize_from_u64(var.data_offset, "variable data offset")?;
276    let elem_size = T::element_size();
277    let total_bytes = total_elements.checked_mul(elem_size).ok_or_else(|| {
278        Error::InvalidData(format!(
279            "variable '{}' size in bytes exceeds platform usize",
280            var.name
281        ))
282    })?;
283
284    let end = offset.checked_add(total_bytes).ok_or_else(|| {
285        Error::InvalidData(format!(
286            "variable '{}' byte range exceeds platform usize",
287            var.name
288        ))
289    })?;
290    if end > file_data.len() {
291        return Err(Error::InvalidData(format!(
292            "variable '{}' data extends beyond file: offset={}, size={}, file_len={}",
293            var.name,
294            offset,
295            total_bytes,
296            file_data.len()
297        )));
298    }
299
300    T::decode_bulk_be_into(&file_data[offset..end], dst)
301}
302
303/// Read the entire data for a record variable into an ndarray.
304///
305/// Record variables are interleaved: for each of `numrecs` records, every record
306/// variable contributes `record_size` bytes (padded to 4-byte alignment for CDF-1/2).
307/// The `record_stride` is the total size of one record across all record variables.
308///
309/// Parameters:
310/// - `file_data`: the raw file bytes
311/// - `var`: the record variable to read
312/// - `numrecs`: number of records (from the file header)
313/// - `record_stride`: total bytes per record (sum of all record variables' padded vsizes)
314pub fn read_record_variable<T: NcReadType>(
315    file_data: &[u8],
316    var: &NcVariable,
317    numrecs: u64,
318    record_stride: u64,
319) -> Result<ArrayD<T>> {
320    if !var.is_record_var {
321        return Err(Error::InvalidData(
322            "use read_non_record_variable for non-record variables".to_string(),
323        ));
324    }
325
326    let elem_size = T::element_size();
327    let base_offset =
328        crate::types::checked_usize_from_u64(var.data_offset, "record variable data offset")?;
329    let numrecs_usize = crate::types::checked_usize_from_u64(numrecs, "record count")?;
330    let record_stride_usize = crate::types::checked_usize_from_u64(record_stride, "record stride")?;
331
332    // Shape: the first dimension is the unlimited dimension, replaced by numrecs.
333    let mut shape: Vec<usize> = var
334        .shape()
335        .iter()
336        .map(|&s| crate::types::checked_usize_from_u64(s, "record variable dimension"))
337        .collect::<Result<Vec<_>>>()?;
338    if shape.is_empty() {
339        return Err(Error::InvalidData(
340            "record variable must have at least one dimension".to_string(),
341        ));
342    }
343    shape[0] = numrecs_usize;
344
345    // Number of elements per record (product of all dims except the first).
346    let elements_per_record: usize = shape[1..].iter().product::<usize>().max(1);
347    let bytes_per_record = elements_per_record.checked_mul(elem_size).ok_or_else(|| {
348        Error::InvalidData(format!(
349            "record variable '{}' bytes per record exceed platform usize",
350            var.name
351        ))
352    })?;
353    let total_elements = numrecs_usize
354        .checked_mul(elements_per_record)
355        .ok_or_else(|| {
356            Error::InvalidData(format!(
357                "record variable '{}' element count exceeds platform usize",
358                var.name
359            ))
360        })?;
361
362    let mut values = Vec::with_capacity(total_elements);
363
364    for rec in 0..numrecs_usize {
365        let rec_offset = base_offset
366            .checked_add(rec.checked_mul(record_stride_usize).ok_or_else(|| {
367                Error::InvalidData(format!(
368                    "record variable '{}' byte offset exceeds platform usize",
369                    var.name
370                ))
371            })?)
372            .ok_or_else(|| {
373                Error::InvalidData(format!(
374                    "record variable '{}' byte offset exceeds platform usize",
375                    var.name
376                ))
377            })?;
378        let rec_end = rec_offset.checked_add(bytes_per_record).ok_or_else(|| {
379            Error::InvalidData(format!(
380                "record variable '{}' record range exceeds platform usize",
381                var.name
382            ))
383        })?;
384        if rec_end > file_data.len() {
385            return Err(Error::InvalidData(format!(
386                "record {} for variable '{}' extends beyond file",
387                rec, var.name
388            )));
389        }
390        let rec_slice = &file_data[rec_offset..rec_end];
391        let rec_values = T::decode_bulk_be(rec_slice, elements_per_record)?;
392        values.extend(rec_values);
393    }
394
395    ArrayD::from_shape_vec(IxDyn(&shape), values)
396        .map_err(|e| Error::InvalidData(format!("failed to create array: {}", e)))
397}
398
399/// Read the entire data for a record variable into a caller-provided buffer.
400pub fn read_record_variable_into<T: NcReadType>(
401    file_data: &[u8],
402    var: &NcVariable,
403    numrecs: u64,
404    record_stride: u64,
405    dst: &mut [T],
406) -> Result<()> {
407    if !var.is_record_var {
408        return Err(Error::InvalidData(
409            "use read_non_record_variable_into for non-record variables".to_string(),
410        ));
411    }
412
413    let elem_size = T::element_size();
414    let base_offset =
415        crate::types::checked_usize_from_u64(var.data_offset, "record variable data offset")?;
416    let numrecs_usize = crate::types::checked_usize_from_u64(numrecs, "record count")?;
417    let record_stride_usize = crate::types::checked_usize_from_u64(record_stride, "record stride")?;
418
419    if var.dimensions.is_empty() {
420        return Err(Error::InvalidData(
421            "record variable must have at least one dimension".to_string(),
422        ));
423    }
424
425    let elements_per_record = checked_record_elements_per_record(var)?;
426    let bytes_per_record = elements_per_record.checked_mul(elem_size).ok_or_else(|| {
427        Error::InvalidData(format!(
428            "record variable '{}' bytes per record exceed platform usize",
429            var.name
430        ))
431    })?;
432    let total_elements = numrecs_usize
433        .checked_mul(elements_per_record)
434        .ok_or_else(|| {
435            Error::InvalidData(format!(
436                "record variable '{}' element count exceeds platform usize",
437                var.name
438            ))
439        })?;
440    if dst.len() != total_elements {
441        return Err(Error::InvalidData(format!(
442            "destination has {} elements, variable '{}' requires {}",
443            dst.len(),
444            var.name,
445            total_elements
446        )));
447    }
448
449    for rec in 0..numrecs_usize {
450        let rec_offset = base_offset
451            .checked_add(rec.checked_mul(record_stride_usize).ok_or_else(|| {
452                Error::InvalidData(format!(
453                    "record variable '{}' byte offset exceeds platform usize",
454                    var.name
455                ))
456            })?)
457            .ok_or_else(|| {
458                Error::InvalidData(format!(
459                    "record variable '{}' byte offset exceeds platform usize",
460                    var.name
461                ))
462            })?;
463        let rec_end = rec_offset.checked_add(bytes_per_record).ok_or_else(|| {
464            Error::InvalidData(format!(
465                "record variable '{}' record range exceeds platform usize",
466                var.name
467            ))
468        })?;
469        if rec_end > file_data.len() {
470            return Err(Error::InvalidData(format!(
471                "record {} for variable '{}' extends beyond file",
472                rec, var.name
473            )));
474        }
475
476        let dst_start = rec.checked_mul(elements_per_record).ok_or_else(|| {
477            Error::InvalidData(format!(
478                "record variable '{}' destination offset exceeds platform usize",
479                var.name
480            ))
481        })?;
482        let dst_end = dst_start.checked_add(elements_per_record).ok_or_else(|| {
483            Error::InvalidData(format!(
484                "record variable '{}' destination range exceeds platform usize",
485                var.name
486            ))
487        })?;
488        T::decode_bulk_be_into(
489            &file_data[rec_offset..rec_end],
490            &mut dst[dst_start..dst_end],
491        )?;
492    }
493
494    Ok(())
495}
496
497/// Compute the record stride: total bytes per record across all record variables.
498///
499/// Each record variable's per-record contribution is its `record_size` (already stored
500/// as vsize from the header), padded to 4-byte boundary.
501pub fn compute_record_stride(variables: &[NcVariable]) -> u64 {
502    variables
503        .iter()
504        .filter(|v| v.is_record_var)
505        .map(|v| {
506            let size = v.record_size;
507            // Pad each variable's per-record size to 4-byte boundary.
508            let rem = size % 4;
509            if rem == 0 {
510                size
511            } else {
512                size + (4 - rem)
513            }
514        })
515        .sum()
516}
517
518fn checked_non_record_element_count(var: &NcVariable) -> Result<usize> {
519    let mut total = 1u64;
520    for dim in &var.dimensions {
521        total = total.checked_mul(dim.size).ok_or_else(|| {
522            Error::InvalidData("variable element count overflows u64".to_string())
523        })?;
524    }
525    crate::types::checked_usize_from_u64(total, "variable element count")
526}
527
528fn checked_record_elements_per_record(var: &NcVariable) -> Result<usize> {
529    let mut elements = 1usize;
530    for dim in var.dimensions.iter().skip(1) {
531        let size = crate::types::checked_usize_from_u64(dim.size, "record variable dimension")?;
532        elements = elements.checked_mul(size).ok_or_else(|| {
533            Error::InvalidData(format!(
534                "record variable '{}' elements per record exceed platform usize",
535                var.name
536            ))
537        })?;
538    }
539    Ok(elements)
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use crate::types::NcDimension;
546
547    #[test]
548    fn test_read_non_record_1d_float() {
549        // Create a fake file with 3 floats starting at offset 100.
550        let mut file_data = vec![0u8; 200];
551        let values = [1.0f32, 2.0f32, 3.0f32];
552        for (i, &v) in values.iter().enumerate() {
553            let bytes = v.to_be_bytes();
554            file_data[100 + i * 4..100 + i * 4 + 4].copy_from_slice(&bytes);
555        }
556
557        let var = NcVariable {
558            name: "temp".to_string(),
559            dimensions: vec![NcDimension {
560                name: "x".to_string(),
561                size: 3,
562                is_unlimited: false,
563            }],
564            dtype: NcType::Float,
565            attributes: vec![],
566            data_offset: 100,
567            _data_size: 12,
568            is_record_var: false,
569            record_size: 0,
570        };
571
572        let arr: ArrayD<f32> = read_non_record_variable(&file_data, &var).unwrap();
573        assert_eq!(arr.shape(), &[3]);
574        assert_eq!(arr[[0]], 1.0f32);
575        assert_eq!(arr[[1]], 2.0f32);
576        assert_eq!(arr[[2]], 3.0f32);
577    }
578
579    #[test]
580    fn test_read_non_record_variable_into() {
581        let mut file_data = vec![0u8; 200];
582        let values = [1.0f32, 2.0f32, 3.0f32];
583        for (i, &v) in values.iter().enumerate() {
584            file_data[100 + i * 4..100 + i * 4 + 4].copy_from_slice(&v.to_be_bytes());
585        }
586
587        let var = NcVariable {
588            name: "temp".to_string(),
589            dimensions: vec![NcDimension {
590                name: "x".to_string(),
591                size: 3,
592                is_unlimited: false,
593            }],
594            dtype: NcType::Float,
595            attributes: vec![],
596            data_offset: 100,
597            _data_size: 12,
598            is_record_var: false,
599            record_size: 0,
600        };
601
602        let mut dst = [0.0f32; 3];
603        read_non_record_variable_into(&file_data, &var, &mut dst).unwrap();
604        assert_eq!(dst, values);
605    }
606
607    #[test]
608    fn test_read_non_record_2d_int() {
609        // 2x3 array of i32 at offset 0
610        let values: Vec<i32> = vec![10, 20, 30, 40, 50, 60];
611        let mut file_data = Vec::new();
612        for &v in &values {
613            file_data.extend_from_slice(&v.to_be_bytes());
614        }
615
616        let var = NcVariable {
617            name: "grid".to_string(),
618            dimensions: vec![
619                NcDimension {
620                    name: "y".to_string(),
621                    size: 2,
622                    is_unlimited: false,
623                },
624                NcDimension {
625                    name: "x".to_string(),
626                    size: 3,
627                    is_unlimited: false,
628                },
629            ],
630            dtype: NcType::Int,
631            attributes: vec![],
632            data_offset: 0,
633            _data_size: 24,
634            is_record_var: false,
635            record_size: 0,
636        };
637
638        let arr: ArrayD<i32> = read_non_record_variable(&file_data, &var).unwrap();
639        assert_eq!(arr.shape(), &[2, 3]);
640        assert_eq!(arr[[0, 0]], 10);
641        assert_eq!(arr[[0, 2]], 30);
642        assert_eq!(arr[[1, 0]], 40);
643        assert_eq!(arr[[1, 2]], 60);
644    }
645
646    #[test]
647    fn test_read_non_record_variable_into_rejects_wrong_destination_len() {
648        let var = NcVariable {
649            name: "grid".to_string(),
650            dimensions: vec![NcDimension {
651                name: "x".to_string(),
652                size: 3,
653                is_unlimited: false,
654            }],
655            dtype: NcType::Float,
656            attributes: vec![],
657            data_offset: 0,
658            _data_size: 12,
659            is_record_var: false,
660            record_size: 0,
661        };
662
663        let mut dst = [0.0f32; 2];
664        let err = read_non_record_variable_into(&[0; 12], &var, &mut dst).unwrap_err();
665        assert!(matches!(err, Error::InvalidData(_)));
666    }
667
668    #[test]
669    fn test_compute_record_stride() {
670        let vars = vec![
671            NcVariable {
672                name: "a".to_string(),
673                dimensions: vec![],
674                dtype: NcType::Float,
675                attributes: vec![],
676                data_offset: 0,
677                _data_size: 0,
678                is_record_var: true,
679                record_size: 20, // 5 floats
680            },
681            NcVariable {
682                name: "b".to_string(),
683                dimensions: vec![],
684                dtype: NcType::Short,
685                attributes: vec![],
686                data_offset: 0,
687                _data_size: 0,
688                is_record_var: true,
689                record_size: 6, // 3 shorts -> padded to 8
690            },
691            NcVariable {
692                name: "c".to_string(),
693                dimensions: vec![],
694                dtype: NcType::Double,
695                attributes: vec![],
696                data_offset: 0,
697                _data_size: 100,
698                is_record_var: false, // not a record var, should be excluded
699                record_size: 0,
700            },
701        ];
702        // a: 20 (already 4-aligned), b: 6 -> 8 = total 28
703        assert_eq!(compute_record_stride(&vars), 28);
704    }
705
706    #[test]
707    fn test_read_record_variable() {
708        // Single record variable "temp" with shape [time, x] where x=2.
709        // 3 records, each with 2 floats = 8 bytes per record.
710        // Record stride = 8 (only one record var, already 4-aligned).
711        let mut file_data = vec![0u8; 200];
712        let base = 100usize;
713        let record_values: Vec<Vec<f32>> = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
714        for (rec, vals) in record_values.iter().enumerate() {
715            for (i, &v) in vals.iter().enumerate() {
716                let offset = base + rec * 8 + i * 4;
717                file_data[offset..offset + 4].copy_from_slice(&v.to_be_bytes());
718            }
719        }
720
721        let var = NcVariable {
722            name: "temp".to_string(),
723            dimensions: vec![
724                NcDimension {
725                    name: "time".to_string(),
726                    size: 0, // unlimited
727                    is_unlimited: true,
728                },
729                NcDimension {
730                    name: "x".to_string(),
731                    size: 2,
732                    is_unlimited: false,
733                },
734            ],
735            dtype: NcType::Float,
736            attributes: vec![],
737            data_offset: 100,
738            _data_size: 0,
739            is_record_var: true,
740            record_size: 8,
741        };
742
743        let arr: ArrayD<f32> = read_record_variable(&file_data, &var, 3, 8).unwrap();
744        assert_eq!(arr.shape(), &[3, 2]);
745        assert_eq!(arr[[0, 0]], 1.0);
746        assert_eq!(arr[[0, 1]], 2.0);
747        assert_eq!(arr[[1, 0]], 3.0);
748        assert_eq!(arr[[2, 1]], 6.0);
749    }
750
751    #[test]
752    fn test_read_record_variable_into() {
753        let mut file_data = vec![0u8; 200];
754        let base = 100usize;
755        let record_values: Vec<Vec<f32>> = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
756        for (rec, vals) in record_values.iter().enumerate() {
757            for (i, &v) in vals.iter().enumerate() {
758                let offset = base + rec * 8 + i * 4;
759                file_data[offset..offset + 4].copy_from_slice(&v.to_be_bytes());
760            }
761        }
762
763        let var = NcVariable {
764            name: "temp".to_string(),
765            dimensions: vec![
766                NcDimension {
767                    name: "time".to_string(),
768                    size: 0,
769                    is_unlimited: true,
770                },
771                NcDimension {
772                    name: "x".to_string(),
773                    size: 2,
774                    is_unlimited: false,
775                },
776            ],
777            dtype: NcType::Float,
778            attributes: vec![],
779            data_offset: 100,
780            _data_size: 0,
781            is_record_var: true,
782            record_size: 8,
783        };
784
785        let mut dst = [0.0f32; 6];
786        read_record_variable_into(&file_data, &var, 3, 8, &mut dst).unwrap();
787        assert_eq!(dst, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
788    }
789
790    #[test]
791    fn test_read_record_variable_into_rejects_wrong_destination_len() {
792        let var = NcVariable {
793            name: "temp".to_string(),
794            dimensions: vec![
795                NcDimension {
796                    name: "time".to_string(),
797                    size: 0,
798                    is_unlimited: true,
799                },
800                NcDimension {
801                    name: "x".to_string(),
802                    size: 2,
803                    is_unlimited: false,
804                },
805            ],
806            dtype: NcType::Float,
807            attributes: vec![],
808            data_offset: 0,
809            _data_size: 0,
810            is_record_var: true,
811            record_size: 8,
812        };
813
814        let mut dst = [0.0f32; 5];
815        let err = read_record_variable_into(&[0; 24], &var, 3, 8, &mut dst).unwrap_err();
816        assert!(matches!(err, Error::InvalidData(_)));
817    }
818
819    #[test]
820    fn test_read_non_record_variable_rejects_element_count_overflow() {
821        let var = NcVariable {
822            name: "huge".to_string(),
823            dimensions: vec![
824                NcDimension {
825                    name: "y".to_string(),
826                    size: u64::MAX,
827                    is_unlimited: false,
828                },
829                NcDimension {
830                    name: "x".to_string(),
831                    size: 2,
832                    is_unlimited: false,
833                },
834            ],
835            dtype: NcType::Float,
836            attributes: vec![],
837            data_offset: 0,
838            _data_size: 0,
839            is_record_var: false,
840            record_size: 0,
841        };
842
843        let err = read_non_record_variable::<f32>(&[], &var).unwrap_err();
844        assert!(matches!(err, Error::InvalidData(_)));
845    }
846
847    #[test]
848    fn test_read_record_variable_rejects_record_offset_overflow() {
849        let var = NcVariable {
850            name: "huge_record".to_string(),
851            dimensions: vec![
852                NcDimension {
853                    name: "time".to_string(),
854                    size: 0,
855                    is_unlimited: true,
856                },
857                NcDimension {
858                    name: "x".to_string(),
859                    size: 1,
860                    is_unlimited: false,
861                },
862            ],
863            dtype: NcType::Float,
864            attributes: vec![],
865            data_offset: u64::MAX,
866            _data_size: 0,
867            is_record_var: true,
868            record_size: 4,
869        };
870
871        let err = read_record_variable::<f32>(&[], &var, 1, 4).unwrap_err();
872        assert!(matches!(err, Error::InvalidData(_)));
873    }
874}