Skip to main content

netcdf_reader/classic/
header.rs

1//! Parse the NetCDF classic (CDF-1/2/5) binary header.
2//!
3//! The classic header is a sequence of big-endian fields describing dimensions,
4//! global attributes, and variables. All multi-byte integers are big-endian.
5//! Strings are padded to 4-byte alignment. CDF-5 uses 8-byte counts and sizes
6//! where CDF-1/2 use 4-byte values.
7
8use crate::error::{Error, Result};
9use crate::types::{NcAttrValue, NcAttribute, NcDimension, NcType, NcVariable};
10use crate::NcFormat;
11
12use super::types::{nc_type_from_code, pad_to_4};
13
14// Header tag constants.
15const ABSENT: u32 = 0x0000_0000;
16const NC_DIMENSION: u32 = 0x0000_000A;
17const NC_VARIABLE: u32 = 0x0000_000B;
18const NC_ATTRIBUTE: u32 = 0x0000_000C;
19
20/// Streaming (indeterminate) record count sentinel.
21const STREAMING: u32 = 0xFFFF_FFFF;
22
23/// Result of parsing a classic NetCDF header.
24pub struct ClassicHeader {
25    pub dimensions: Vec<NcDimension>,
26    pub global_attributes: Vec<NcAttribute>,
27    pub variables: Vec<NcVariable>,
28    pub numrecs: u64,
29}
30
31/// A cursor for reading big-endian data from a byte slice.
32struct Cursor<'a> {
33    data: &'a [u8],
34    pos: usize,
35}
36
37impl<'a> Cursor<'a> {
38    fn new(data: &'a [u8]) -> Self {
39        Cursor { data, pos: 0 }
40    }
41
42    fn remaining(&self) -> usize {
43        self.data.len().saturating_sub(self.pos)
44    }
45
46    fn ensure(&self, n: usize) -> Result<()> {
47        if self.remaining() < n {
48            Err(Error::InvalidData(format!(
49                "unexpected end of header at offset {}: need {} bytes, have {}",
50                self.pos,
51                n,
52                self.remaining()
53            )))
54        } else {
55            Ok(())
56        }
57    }
58
59    #[allow(dead_code)]
60    fn read_u8(&mut self) -> Result<u8> {
61        self.ensure(1)?;
62        let v = self.data[self.pos];
63        self.pos += 1;
64        Ok(v)
65    }
66
67    fn read_u16_be(&mut self) -> Result<u16> {
68        self.ensure(2)?;
69        let v = u16::from_be_bytes([self.data[self.pos], self.data[self.pos + 1]]);
70        self.pos += 2;
71        Ok(v)
72    }
73
74    fn read_u32_be(&mut self) -> Result<u32> {
75        self.ensure(4)?;
76        let v = u32::from_be_bytes([
77            self.data[self.pos],
78            self.data[self.pos + 1],
79            self.data[self.pos + 2],
80            self.data[self.pos + 3],
81        ]);
82        self.pos += 4;
83        Ok(v)
84    }
85
86    fn read_i32_be(&mut self) -> Result<i32> {
87        self.ensure(4)?;
88        let v = i32::from_be_bytes([
89            self.data[self.pos],
90            self.data[self.pos + 1],
91            self.data[self.pos + 2],
92            self.data[self.pos + 3],
93        ]);
94        self.pos += 4;
95        Ok(v)
96    }
97
98    fn read_u64_be(&mut self) -> Result<u64> {
99        self.ensure(8)?;
100        let v = u64::from_be_bytes([
101            self.data[self.pos],
102            self.data[self.pos + 1],
103            self.data[self.pos + 2],
104            self.data[self.pos + 3],
105            self.data[self.pos + 4],
106            self.data[self.pos + 5],
107            self.data[self.pos + 6],
108            self.data[self.pos + 7],
109        ]);
110        self.pos += 8;
111        Ok(v)
112    }
113
114    fn read_i64_be(&mut self) -> Result<i64> {
115        self.ensure(8)?;
116        let v = i64::from_be_bytes([
117            self.data[self.pos],
118            self.data[self.pos + 1],
119            self.data[self.pos + 2],
120            self.data[self.pos + 3],
121            self.data[self.pos + 4],
122            self.data[self.pos + 5],
123            self.data[self.pos + 6],
124            self.data[self.pos + 7],
125        ]);
126        self.pos += 8;
127        Ok(v)
128    }
129
130    fn read_f32_be(&mut self) -> Result<f32> {
131        self.ensure(4)?;
132        let v = f32::from_be_bytes([
133            self.data[self.pos],
134            self.data[self.pos + 1],
135            self.data[self.pos + 2],
136            self.data[self.pos + 3],
137        ]);
138        self.pos += 4;
139        Ok(v)
140    }
141
142    fn read_f64_be(&mut self) -> Result<f64> {
143        self.ensure(8)?;
144        let v = f64::from_be_bytes([
145            self.data[self.pos],
146            self.data[self.pos + 1],
147            self.data[self.pos + 2],
148            self.data[self.pos + 3],
149            self.data[self.pos + 4],
150            self.data[self.pos + 5],
151            self.data[self.pos + 6],
152            self.data[self.pos + 7],
153        ]);
154        self.pos += 8;
155        Ok(v)
156    }
157
158    fn read_bytes(&mut self, n: usize) -> Result<&'a [u8]> {
159        self.ensure(n)?;
160        let slice = &self.data[self.pos..self.pos + n];
161        self.pos += n;
162        Ok(slice)
163    }
164
165    fn skip(&mut self, n: usize) -> Result<()> {
166        self.ensure(n)?;
167        self.pos += n;
168        Ok(())
169    }
170
171    /// Read a count field: 4 bytes for CDF-1/2, 8 bytes for CDF-5.
172    fn read_count(&mut self, format: NcFormat) -> Result<u64> {
173        match format {
174            NcFormat::Cdf5 => self.read_u64_be(),
175            _ => self.read_u32_be().map(|v| v as u64),
176        }
177    }
178
179    /// Read a padded name: 4-byte length, then chars, then padding to 4-byte boundary.
180    /// The name length prefix is always 4 bytes for CDF-1/2 and 8 bytes for CDF-5.
181    fn read_name(&mut self, format: NcFormat) -> Result<String> {
182        let len = self.read_count(format)? as usize;
183        let bytes = self.read_bytes(len)?;
184        let padded_len = pad_to_4(len);
185        let pad = padded_len - len;
186        if pad > 0 {
187            self.skip(pad)?;
188        }
189        String::from_utf8(bytes.to_vec())
190            .map_err(|e| Error::InvalidData(format!("invalid UTF-8 name: {}", e)))
191    }
192}
193
194/// Parse a complete classic NetCDF header from raw file bytes.
195///
196/// The `format` parameter must be one of `Classic`, `Offset64`, or `Cdf5`
197/// (the caller has already read and validated the magic bytes).
198pub fn parse_header(data: &[u8], format: NcFormat) -> Result<ClassicHeader> {
199    // Skip past the 4-byte magic (already validated by caller).
200    let mut cur = Cursor::new(data);
201    cur.skip(4)?;
202
203    // numrecs: 4 bytes for CDF-1/2, 8 bytes for CDF-5.
204    let numrecs_raw = cur.read_count(format)?;
205    let numrecs = if format != NcFormat::Cdf5 && (numrecs_raw as u32) == STREAMING {
206        0 // Treat streaming as 0 records (will be updated when data is read)
207    } else {
208        numrecs_raw
209    };
210
211    // dim_list
212    let mut dimensions = parse_dim_list(&mut cur, format)?;
213
214    // att_list (global attributes)
215    let global_attributes = parse_att_list(&mut cur, format)?;
216
217    // var_list
218    let mut variables = parse_var_list(&mut cur, format, &dimensions)?;
219
220    if numrecs > 0 {
221        apply_unlimited_dimension_size(&mut dimensions, &mut variables, numrecs);
222    }
223
224    Ok(ClassicHeader {
225        dimensions,
226        global_attributes,
227        variables,
228        numrecs,
229    })
230}
231
232/// Parse the dimension list.
233fn parse_dim_list(cur: &mut Cursor<'_>, format: NcFormat) -> Result<Vec<NcDimension>> {
234    let tag = cur.read_u32_be()?;
235
236    if tag == ABSENT {
237        // ABSENT is a zero tag followed by a zero count.
238        let _zero = cur.read_count(format)?;
239        return Ok(Vec::new());
240    }
241
242    if tag != NC_DIMENSION {
243        return Err(Error::InvalidData(format!(
244            "expected NC_DIMENSION tag (0x{:08X}), got 0x{:08X}",
245            NC_DIMENSION, tag
246        )));
247    }
248
249    let nelems = cur.read_count(format)? as usize;
250    let mut dims = Vec::with_capacity(nelems);
251
252    for _ in 0..nelems {
253        let name = cur.read_name(format)?;
254        let size = cur.read_count(format)?;
255        // A dimension with size 0 is the unlimited (record) dimension.
256        let is_unlimited = size == 0;
257        dims.push(NcDimension {
258            name,
259            size,
260            is_unlimited,
261        });
262    }
263
264    Ok(dims)
265}
266
267/// Parse an attribute list (used for both global and variable attributes).
268fn parse_att_list(cur: &mut Cursor<'_>, format: NcFormat) -> Result<Vec<NcAttribute>> {
269    let tag = cur.read_u32_be()?;
270
271    if tag == ABSENT {
272        let _zero = cur.read_count(format)?;
273        return Ok(Vec::new());
274    }
275
276    if tag != NC_ATTRIBUTE {
277        return Err(Error::InvalidData(format!(
278            "expected NC_ATTRIBUTE tag (0x{:08X}), got 0x{:08X}",
279            NC_ATTRIBUTE, tag
280        )));
281    }
282
283    let nelems = cur.read_count(format)? as usize;
284    let mut attrs = Vec::with_capacity(nelems);
285
286    for _ in 0..nelems {
287        let name = cur.read_name(format)?;
288        let nc_type = cur.read_u32_be()?;
289        let nvalues = cur.read_count(format)? as usize;
290        let value = read_attr_values(cur, nc_type, nvalues, format)?;
291
292        attrs.push(NcAttribute { name, value });
293    }
294
295    Ok(attrs)
296}
297
298/// Read attribute values of the given type and count.
299/// Values are padded to a 4-byte boundary in the file.
300fn read_attr_values(
301    cur: &mut Cursor<'_>,
302    nc_type: u32,
303    nvalues: usize,
304    _format: NcFormat,
305) -> Result<NcAttrValue> {
306    let typ = nc_type_from_code(nc_type)?;
307    let elem_size = typ.size();
308    let raw_bytes = nvalues * elem_size;
309    let padded = pad_to_4(raw_bytes);
310
311    match typ {
312        NcType::Byte => {
313            let bytes = cur.read_bytes(raw_bytes)?;
314            let values: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
315            cur.skip(padded - raw_bytes)?;
316            Ok(NcAttrValue::Bytes(values))
317        }
318        NcType::Char => {
319            let bytes = cur.read_bytes(raw_bytes)?;
320            // Trim trailing null bytes (common in NetCDF char attributes).
321            let s = String::from_utf8_lossy(bytes);
322            let trimmed = s.trim_end_matches('\0').to_string();
323            cur.skip(padded - raw_bytes)?;
324            Ok(NcAttrValue::Chars(trimmed))
325        }
326        NcType::Short => {
327            let mut values = Vec::with_capacity(nvalues);
328            for _ in 0..nvalues {
329                values.push(cur.read_u16_be()? as i16);
330            }
331            let pad = padded - raw_bytes;
332            cur.skip(pad)?;
333            Ok(NcAttrValue::Shorts(values))
334        }
335        NcType::Int => {
336            let mut values = Vec::with_capacity(nvalues);
337            for _ in 0..nvalues {
338                values.push(cur.read_i32_be()?);
339            }
340            Ok(NcAttrValue::Ints(values))
341        }
342        NcType::Float => {
343            let mut values = Vec::with_capacity(nvalues);
344            for _ in 0..nvalues {
345                values.push(cur.read_f32_be()?);
346            }
347            Ok(NcAttrValue::Floats(values))
348        }
349        NcType::Double => {
350            let mut values = Vec::with_capacity(nvalues);
351            for _ in 0..nvalues {
352                values.push(cur.read_f64_be()?);
353            }
354            Ok(NcAttrValue::Doubles(values))
355        }
356        NcType::UByte => {
357            let bytes = cur.read_bytes(raw_bytes)?;
358            cur.skip(padded - raw_bytes)?;
359            Ok(NcAttrValue::UBytes(bytes.to_vec()))
360        }
361        NcType::UShort => {
362            let mut values = Vec::with_capacity(nvalues);
363            for _ in 0..nvalues {
364                values.push(cur.read_u16_be()?);
365            }
366            let pad = padded - raw_bytes;
367            cur.skip(pad)?;
368            Ok(NcAttrValue::UShorts(values))
369        }
370        NcType::UInt => {
371            let mut values = Vec::with_capacity(nvalues);
372            for _ in 0..nvalues {
373                values.push(cur.read_u32_be()?);
374            }
375            Ok(NcAttrValue::UInts(values))
376        }
377        NcType::Int64 => {
378            let mut values = Vec::with_capacity(nvalues);
379            for _ in 0..nvalues {
380                values.push(cur.read_i64_be()?);
381            }
382            Ok(NcAttrValue::Int64s(values))
383        }
384        NcType::UInt64 => {
385            let mut values = Vec::with_capacity(nvalues);
386            for _ in 0..nvalues {
387                values.push(cur.read_u64_be()?);
388            }
389            Ok(NcAttrValue::UInt64s(values))
390        }
391        NcType::String
392        | NcType::Enum { .. }
393        | NcType::Compound { .. }
394        | NcType::Opaque { .. }
395        | NcType::Array { .. }
396        | NcType::VLen { .. } => Err(Error::InvalidData(format!(
397            "{:?} is not valid in classic format attributes",
398            typ
399        ))),
400    }
401}
402
403/// Parse the variable list.
404fn parse_var_list(
405    cur: &mut Cursor<'_>,
406    format: NcFormat,
407    dims: &[NcDimension],
408) -> Result<Vec<NcVariable>> {
409    let tag = cur.read_u32_be()?;
410
411    if tag == ABSENT {
412        let _zero = cur.read_count(format)?;
413        return Ok(Vec::new());
414    }
415
416    if tag != NC_VARIABLE {
417        return Err(Error::InvalidData(format!(
418            "expected NC_VARIABLE tag (0x{:08X}), got 0x{:08X}",
419            NC_VARIABLE, tag
420        )));
421    }
422
423    let nelems = cur.read_count(format)? as usize;
424    let mut vars = Vec::with_capacity(nelems);
425
426    for _ in 0..nelems {
427        let name = cur.read_name(format)?;
428
429        // Number of dimensions for this variable.
430        let ndims = cur.read_count(format)? as usize;
431
432        // Dimension IDs are NON_NEG values and widen to 64 bits in CDF-5.
433        let mut var_dims = Vec::with_capacity(ndims);
434        let mut is_record_var = false;
435        for _ in 0..ndims {
436            let dimid = cur.read_count(format)? as usize;
437            if dimid >= dims.len() {
438                return Err(Error::InvalidData(format!(
439                    "variable '{}' references dimension index {} but only {} dimensions exist",
440                    name,
441                    dimid,
442                    dims.len()
443                )));
444            }
445            if dims[dimid].is_unlimited {
446                is_record_var = true;
447            }
448            var_dims.push(dims[dimid].clone());
449        }
450
451        // Variable attributes.
452        let attributes = parse_att_list(cur, format)?;
453
454        // nc_type (always 4 bytes).
455        let nc_type_code = cur.read_u32_be()?;
456        let dtype = nc_type_from_code(nc_type_code)?;
457
458        // vsize: the size of one record's worth of data for this variable,
459        // or the total size for non-record variables.
460        // 4 bytes for CDF-1/2, 8 bytes for CDF-5.
461        let vsize = cur.read_count(format)?;
462
463        // begin (data offset): 4 bytes for CDF-1, 8 bytes for CDF-2/5.
464        let data_offset = match format {
465            NcFormat::Classic => cur.read_u32_be()? as u64,
466            NcFormat::Offset64 | NcFormat::Cdf5 => cur.read_u64_be()?,
467            _ => unreachable!("classic parser only handles CDF-1/2/5"),
468        };
469
470        // Compute record_size (the per-record slice size).
471        let record_size = if is_record_var { vsize } else { 0 };
472
473        // For non-record variables, data_size = vsize.
474        // For record variables, data_size = vsize * numrecs (computed at read time).
475        let data_size = if is_record_var { 0 } else { vsize };
476
477        vars.push(NcVariable {
478            name,
479            dimensions: var_dims,
480            dtype,
481            attributes,
482            data_offset,
483            _data_size: data_size,
484            is_record_var,
485            record_size,
486        });
487    }
488
489    Ok(vars)
490}
491
492fn apply_unlimited_dimension_size(
493    dimensions: &mut [NcDimension],
494    variables: &mut [NcVariable],
495    numrecs: u64,
496) {
497    for dim in dimensions.iter_mut().filter(|dim| dim.is_unlimited) {
498        dim.size = numrecs;
499    }
500
501    for variable in variables {
502        for dim in variable
503            .dimensions
504            .iter_mut()
505            .filter(|dim| dim.is_unlimited)
506        {
507            dim.size = numrecs;
508        }
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::NcFormat;
516
517    /// Build a minimal CDF-1 file header in memory.
518    /// This helper constructs valid header bytes for testing.
519    fn build_cdf1_header(
520        dims: &[(&str, u32)],
521        attrs: &[(&str, u32, &[u8])], // (name, nc_type, raw_value_bytes)
522        vars: &[(&str, &[u32], u32, u32, u32)], // (name, dimids, nc_type, vsize, offset)
523        numrecs: u32,
524    ) -> Vec<u8> {
525        let mut buf = Vec::new();
526
527        // Magic: CDF\x01
528        buf.extend_from_slice(b"CDF\x01");
529
530        // numrecs (4 bytes)
531        buf.extend_from_slice(&numrecs.to_be_bytes());
532
533        // dim_list
534        if dims.is_empty() {
535            // ABSENT
536            buf.extend_from_slice(&ABSENT.to_be_bytes());
537            buf.extend_from_slice(&0u32.to_be_bytes());
538        } else {
539            buf.extend_from_slice(&NC_DIMENSION.to_be_bytes());
540            buf.extend_from_slice(&(dims.len() as u32).to_be_bytes());
541            for (name, size) in dims {
542                write_name_cdf1(&mut buf, name);
543                buf.extend_from_slice(&size.to_be_bytes());
544            }
545        }
546
547        // att_list (global)
548        write_att_list_cdf1(&mut buf, attrs);
549
550        // var_list
551        if vars.is_empty() {
552            buf.extend_from_slice(&ABSENT.to_be_bytes());
553            buf.extend_from_slice(&0u32.to_be_bytes());
554        } else {
555            buf.extend_from_slice(&NC_VARIABLE.to_be_bytes());
556            buf.extend_from_slice(&(vars.len() as u32).to_be_bytes());
557            for (name, dimids, nc_type, vsize, offset) in vars {
558                write_name_cdf1(&mut buf, name);
559                // ndims
560                buf.extend_from_slice(&(dimids.len() as u32).to_be_bytes());
561                // dimids
562                for &did in *dimids {
563                    buf.extend_from_slice(&did.to_be_bytes());
564                }
565                // att_list (empty for test vars)
566                buf.extend_from_slice(&ABSENT.to_be_bytes());
567                buf.extend_from_slice(&0u32.to_be_bytes());
568                // nc_type
569                buf.extend_from_slice(&nc_type.to_be_bytes());
570                // vsize
571                buf.extend_from_slice(&vsize.to_be_bytes());
572                // begin (offset) -- 4 bytes for CDF-1
573                buf.extend_from_slice(&offset.to_be_bytes());
574            }
575        }
576
577        buf
578    }
579
580    fn write_name_cdf1(buf: &mut Vec<u8>, name: &str) {
581        let name_bytes = name.as_bytes();
582        buf.extend_from_slice(&(name_bytes.len() as u32).to_be_bytes());
583        buf.extend_from_slice(name_bytes);
584        let pad = pad_to_4(name_bytes.len()) - name_bytes.len();
585        for _ in 0..pad {
586            buf.push(0);
587        }
588    }
589
590    fn write_att_list_cdf1(buf: &mut Vec<u8>, attrs: &[(&str, u32, &[u8])]) {
591        if attrs.is_empty() {
592            buf.extend_from_slice(&ABSENT.to_be_bytes());
593            buf.extend_from_slice(&0u32.to_be_bytes());
594            return;
595        }
596        buf.extend_from_slice(&NC_ATTRIBUTE.to_be_bytes());
597        buf.extend_from_slice(&(attrs.len() as u32).to_be_bytes());
598        for (name, nc_type, value_bytes) in attrs {
599            write_name_cdf1(buf, name);
600            buf.extend_from_slice(&nc_type.to_be_bytes());
601            // For simplicity, nvalues = 1 element (caller provides exactly one element's bytes)
602            let elem_size = match nc_type {
603                1 => 1, // byte
604                2 => 1, // char
605                3 => 2, // short
606                4 => 4, // int
607                5 => 4, // float
608                6 => 8, // double
609                _ => 1,
610            };
611            let nvalues = value_bytes.len() / elem_size;
612            buf.extend_from_slice(&(nvalues as u32).to_be_bytes());
613            buf.extend_from_slice(value_bytes);
614            let pad = pad_to_4(value_bytes.len()) - value_bytes.len();
615            for _ in 0..pad {
616                buf.push(0);
617            }
618        }
619    }
620
621    fn write_count_cdf5(buf: &mut Vec<u8>, value: u64) {
622        buf.extend_from_slice(&value.to_be_bytes());
623    }
624
625    fn write_name_cdf5(buf: &mut Vec<u8>, name: &str) {
626        let name_bytes = name.as_bytes();
627        write_count_cdf5(buf, name_bytes.len() as u64);
628        buf.extend_from_slice(name_bytes);
629        let pad = pad_to_4(name_bytes.len()) - name_bytes.len();
630        for _ in 0..pad {
631            buf.push(0);
632        }
633    }
634
635    fn build_cdf5_header(
636        dims: &[(&str, u64)],
637        vars: &[(&str, &[u64], u32, u64, u64)],
638        numrecs: u64,
639    ) -> Vec<u8> {
640        let mut buf = Vec::new();
641        buf.extend_from_slice(b"CDF\x05");
642        write_count_cdf5(&mut buf, numrecs);
643
644        if dims.is_empty() {
645            buf.extend_from_slice(&ABSENT.to_be_bytes());
646            write_count_cdf5(&mut buf, 0);
647        } else {
648            buf.extend_from_slice(&NC_DIMENSION.to_be_bytes());
649            write_count_cdf5(&mut buf, dims.len() as u64);
650            for (name, size) in dims {
651                write_name_cdf5(&mut buf, name);
652                write_count_cdf5(&mut buf, *size);
653            }
654        }
655
656        buf.extend_from_slice(&ABSENT.to_be_bytes());
657        write_count_cdf5(&mut buf, 0);
658
659        if vars.is_empty() {
660            buf.extend_from_slice(&ABSENT.to_be_bytes());
661            write_count_cdf5(&mut buf, 0);
662        } else {
663            buf.extend_from_slice(&NC_VARIABLE.to_be_bytes());
664            write_count_cdf5(&mut buf, vars.len() as u64);
665            for (name, dimids, nc_type, vsize, offset) in vars {
666                write_name_cdf5(&mut buf, name);
667                write_count_cdf5(&mut buf, dimids.len() as u64);
668                for dimid in *dimids {
669                    write_count_cdf5(&mut buf, *dimid);
670                }
671                buf.extend_from_slice(&ABSENT.to_be_bytes());
672                write_count_cdf5(&mut buf, 0);
673                buf.extend_from_slice(&nc_type.to_be_bytes());
674                write_count_cdf5(&mut buf, *vsize);
675                buf.extend_from_slice(&offset.to_be_bytes());
676            }
677        }
678
679        buf
680    }
681
682    #[test]
683    fn test_empty_header() {
684        let data = build_cdf1_header(&[], &[], &[], 0);
685        let header = parse_header(&data, NcFormat::Classic).unwrap();
686        assert!(header.dimensions.is_empty());
687        assert!(header.global_attributes.is_empty());
688        assert!(header.variables.is_empty());
689        assert_eq!(header.numrecs, 0);
690    }
691
692    #[test]
693    fn test_dimensions() {
694        let data = build_cdf1_header(
695            &[("x", 10), ("y", 20), ("time", 0)], // time is unlimited
696            &[],
697            &[],
698            5,
699        );
700        let header = parse_header(&data, NcFormat::Classic).unwrap();
701        assert_eq!(header.dimensions.len(), 3);
702
703        assert_eq!(header.dimensions[0].name, "x");
704        assert_eq!(header.dimensions[0].size, 10);
705        assert!(!header.dimensions[0].is_unlimited);
706
707        assert_eq!(header.dimensions[1].name, "y");
708        assert_eq!(header.dimensions[1].size, 20);
709        assert!(!header.dimensions[1].is_unlimited);
710
711        assert_eq!(header.dimensions[2].name, "time");
712        assert_eq!(header.dimensions[2].size, 5);
713        assert!(header.dimensions[2].is_unlimited);
714
715        assert_eq!(header.numrecs, 5);
716    }
717
718    #[test]
719    fn test_global_attributes() {
720        // One NC_INT attribute with value 42.
721        let value_bytes = 42i32.to_be_bytes();
722        let data = build_cdf1_header(
723            &[],
724            &[("answer", 4, &value_bytes)], // NC_INT = 4
725            &[],
726            0,
727        );
728        let header = parse_header(&data, NcFormat::Classic).unwrap();
729        assert_eq!(header.global_attributes.len(), 1);
730        assert_eq!(header.global_attributes[0].name, "answer");
731        if let NcAttrValue::Ints(ref v) = header.global_attributes[0].value {
732            assert_eq!(v, &[42]);
733        } else {
734            panic!("expected Ints attribute");
735        }
736    }
737
738    #[test]
739    fn test_char_attribute() {
740        let text = b"hello";
741        let data = build_cdf1_header(
742            &[],
743            &[("greeting", 2, text)], // NC_CHAR = 2
744            &[],
745            0,
746        );
747        let header = parse_header(&data, NcFormat::Classic).unwrap();
748        assert_eq!(header.global_attributes.len(), 1);
749        assert_eq!(header.global_attributes[0].name, "greeting");
750        if let NcAttrValue::Chars(ref s) = header.global_attributes[0].value {
751            assert_eq!(s, "hello");
752        } else {
753            panic!("expected Chars attribute");
754        }
755    }
756
757    #[test]
758    fn test_variables() {
759        let data = build_cdf1_header(
760            &[("x", 10), ("y", 20)],
761            &[],
762            &[
763                ("temperature", &[0, 1], 5, 800, 200), // float, dimids=[x,y]
764                ("pressure", &[0, 1], 6, 1600, 1000),  // double, dimids=[x,y]
765            ],
766            0,
767        );
768        let header = parse_header(&data, NcFormat::Classic).unwrap();
769        assert_eq!(header.variables.len(), 2);
770
771        let temp = &header.variables[0];
772        assert_eq!(temp.name, "temperature");
773        assert_eq!(temp.dtype, NcType::Float);
774        assert_eq!(temp.dimensions.len(), 2);
775        assert_eq!(temp.dimensions[0].name, "x");
776        assert_eq!(temp.dimensions[1].name, "y");
777        assert_eq!(temp.data_offset, 200);
778        assert_eq!(temp._data_size, 800);
779        assert!(!temp.is_record_var);
780
781        let pres = &header.variables[1];
782        assert_eq!(pres.name, "pressure");
783        assert_eq!(pres.dtype, NcType::Double);
784        assert_eq!(pres.data_offset, 1000);
785        assert_eq!(pres._data_size, 1600);
786    }
787
788    #[test]
789    fn test_record_variable() {
790        let data = build_cdf1_header(
791            &[("time", 0), ("x", 5)], // time is unlimited
792            &[],
793            &[
794                // record variable: first dim is unlimited
795                ("values", &[0, 1], 5, 20, 100), // float, vsize=5*4=20 per record
796            ],
797            10, // 10 records
798        );
799        let header = parse_header(&data, NcFormat::Classic).unwrap();
800        assert_eq!(header.numrecs, 10);
801        assert_eq!(header.variables.len(), 1);
802
803        let var = &header.variables[0];
804        assert_eq!(var.name, "values");
805        assert!(var.is_record_var);
806        assert_eq!(var.record_size, 20);
807        assert_eq!(var._data_size, 0); // data_size=0 for record vars (computed at read time)
808        assert_eq!(var.shape(), vec![10, 5]);
809    }
810
811    #[test]
812    fn test_cdf2_offset64() {
813        // Build a CDF-2 header manually.
814        // CDF-2 is mostly the same as CDF-1 but the data offset (begin) field is 8 bytes.
815        let mut buf = Vec::new();
816        buf.extend_from_slice(b"CDF\x02");
817        // numrecs (4 bytes)
818        buf.extend_from_slice(&0u32.to_be_bytes());
819        // dim_list: one dimension "x" with size 100
820        buf.extend_from_slice(&NC_DIMENSION.to_be_bytes());
821        buf.extend_from_slice(&1u32.to_be_bytes());
822        write_name_cdf1(&mut buf, "x");
823        buf.extend_from_slice(&100u32.to_be_bytes());
824        // att_list: absent
825        buf.extend_from_slice(&ABSENT.to_be_bytes());
826        buf.extend_from_slice(&0u32.to_be_bytes());
827        // var_list: one variable
828        buf.extend_from_slice(&NC_VARIABLE.to_be_bytes());
829        buf.extend_from_slice(&1u32.to_be_bytes());
830        write_name_cdf1(&mut buf, "data");
831        buf.extend_from_slice(&1u32.to_be_bytes()); // ndims=1
832        buf.extend_from_slice(&0u32.to_be_bytes()); // dimid=0
833                                                    // att_list: absent
834        buf.extend_from_slice(&ABSENT.to_be_bytes());
835        buf.extend_from_slice(&0u32.to_be_bytes());
836        // nc_type = NC_FLOAT = 5
837        buf.extend_from_slice(&5u32.to_be_bytes());
838        // vsize (4 bytes for CDF-2)
839        buf.extend_from_slice(&400u32.to_be_bytes());
840        // begin (8 bytes for CDF-2!)
841        let offset: u64 = 0x1_0000_0000; // > 4 GB offset to test 64-bit
842        buf.extend_from_slice(&offset.to_be_bytes());
843
844        let header = parse_header(&buf, NcFormat::Offset64).unwrap();
845        assert_eq!(header.variables.len(), 1);
846        assert_eq!(header.variables[0].data_offset, 0x1_0000_0000);
847        assert_eq!(header.variables[0]._data_size, 400);
848    }
849
850    #[test]
851    fn test_cdf5_uses_64_bit_counts_for_var_metadata() {
852        let data = build_cdf5_header(
853            &[("n", 4)],
854            &[
855                ("ubyte_var", &[0], 7, 4, 128),
856                ("int64_var", &[0], 10, 32, 256),
857            ],
858            0,
859        );
860
861        let header = parse_header(&data, NcFormat::Cdf5).unwrap();
862        assert_eq!(header.variables.len(), 2);
863        assert_eq!(header.variables[0].name, "ubyte_var");
864        assert_eq!(header.variables[0].dtype, NcType::UByte);
865        assert_eq!(header.variables[0].dimensions[0].name, "n");
866        assert_eq!(header.variables[1].name, "int64_var");
867        assert_eq!(header.variables[1].dtype, NcType::Int64);
868        assert_eq!(header.variables[1].data_offset, 256);
869    }
870
871    #[test]
872    fn test_unlimited_dimension_size_tracks_numrecs() {
873        let data = build_cdf1_header(
874            &[("time", 0), ("x", 5)],
875            &[],
876            &[("series", &[0, 1], 6, 40, 128)],
877            3,
878        );
879
880        let header = parse_header(&data, NcFormat::Classic).unwrap();
881        assert_eq!(header.dimensions[0].size, 3);
882        assert_eq!(header.variables[0].shape(), vec![3, 5]);
883    }
884
885    #[test]
886    fn test_double_attribute() {
887        let pi = std::f64::consts::PI;
888        let value_bytes = pi.to_be_bytes();
889        let data = build_cdf1_header(
890            &[],
891            &[("pi", 6, &value_bytes)], // NC_DOUBLE = 6
892            &[],
893            0,
894        );
895        let header = parse_header(&data, NcFormat::Classic).unwrap();
896        assert_eq!(header.global_attributes.len(), 1);
897        if let NcAttrValue::Doubles(ref v) = header.global_attributes[0].value {
898            assert_eq!(v.len(), 1);
899            assert!((v[0] - pi).abs() < 1e-15);
900        } else {
901            panic!("expected Doubles attribute");
902        }
903    }
904
905    #[test]
906    fn test_short_attribute_with_padding() {
907        // NC_SHORT (2 bytes) with 3 values = 6 bytes, padded to 8.
908        let mut value_bytes = Vec::new();
909        value_bytes.extend_from_slice(&1i16.to_be_bytes());
910        value_bytes.extend_from_slice(&2i16.to_be_bytes());
911        value_bytes.extend_from_slice(&3i16.to_be_bytes());
912        // The build helper will add padding.
913
914        let mut buf = Vec::new();
915        buf.extend_from_slice(b"CDF\x01");
916        buf.extend_from_slice(&0u32.to_be_bytes()); // numrecs
917                                                    // dim_list: absent
918        buf.extend_from_slice(&ABSENT.to_be_bytes());
919        buf.extend_from_slice(&0u32.to_be_bytes());
920        // att_list: one short attribute with 3 values
921        buf.extend_from_slice(&NC_ATTRIBUTE.to_be_bytes());
922        buf.extend_from_slice(&1u32.to_be_bytes());
923        write_name_cdf1(&mut buf, "vals");
924        buf.extend_from_slice(&3u32.to_be_bytes()); // NC_SHORT
925        buf.extend_from_slice(&3u32.to_be_bytes()); // nvalues=3
926        buf.extend_from_slice(&value_bytes);
927        // Pad to 4-byte boundary: 6 bytes -> 2 bytes padding
928        buf.extend_from_slice(&[0, 0]);
929        // var_list: absent
930        buf.extend_from_slice(&ABSENT.to_be_bytes());
931        buf.extend_from_slice(&0u32.to_be_bytes());
932
933        let header = parse_header(&buf, NcFormat::Classic).unwrap();
934        if let NcAttrValue::Shorts(ref v) = header.global_attributes[0].value {
935            assert_eq!(v, &[1, 2, 3]);
936        } else {
937            panic!("expected Shorts attribute");
938        }
939    }
940
941    #[test]
942    fn test_name_padding() {
943        // Names with lengths 1, 2, 3, 4, 5 to test all padding cases.
944        let data = build_cdf1_header(
945            &[("a", 1), ("ab", 2), ("abc", 3), ("abcd", 4), ("abcde", 5)],
946            &[],
947            &[],
948            0,
949        );
950        let header = parse_header(&data, NcFormat::Classic).unwrap();
951        assert_eq!(header.dimensions.len(), 5);
952        assert_eq!(header.dimensions[0].name, "a");
953        assert_eq!(header.dimensions[1].name, "ab");
954        assert_eq!(header.dimensions[2].name, "abc");
955        assert_eq!(header.dimensions[3].name, "abcd");
956        assert_eq!(header.dimensions[4].name, "abcde");
957    }
958
959    #[test]
960    fn test_invalid_dimension_reference() {
961        // Variable referencing a non-existent dimension.
962        let data = build_cdf1_header(
963            &[("x", 10)], // only dim 0 exists
964            &[],
965            &[("bad_var", &[5], 4, 40, 100)], // dimid=5 is out of range
966            0,
967        );
968        let result = parse_header(&data, NcFormat::Classic);
969        assert!(result.is_err());
970    }
971
972    #[test]
973    fn test_byte_attribute() {
974        let value_bytes: &[u8] = &[0xFF]; // -1 as i8
975        let data = build_cdf1_header(
976            &[],
977            &[("flag", 1, value_bytes)], // NC_BYTE = 1
978            &[],
979            0,
980        );
981        let header = parse_header(&data, NcFormat::Classic).unwrap();
982        if let NcAttrValue::Bytes(ref v) = header.global_attributes[0].value {
983            assert_eq!(v, &[-1i8]);
984        } else {
985            panic!("expected Bytes attribute");
986        }
987    }
988
989    #[test]
990    fn test_float_attribute() {
991        let val = std::f32::consts::PI;
992        let value_bytes = val.to_be_bytes();
993        let data = build_cdf1_header(
994            &[],
995            &[("pi_approx", 5, &value_bytes)], // NC_FLOAT = 5
996            &[],
997            0,
998        );
999        let header = parse_header(&data, NcFormat::Classic).unwrap();
1000        if let NcAttrValue::Floats(ref v) = header.global_attributes[0].value {
1001            assert_eq!(v.len(), 1);
1002            assert!((v[0] - std::f32::consts::PI).abs() < 1e-6);
1003        } else {
1004            panic!("expected Floats attribute");
1005        }
1006    }
1007
1008    #[test]
1009    fn test_multiple_global_attributes() {
1010        let int_val = 100i32.to_be_bytes();
1011        let float_val = 2.5f32.to_be_bytes();
1012        let data = build_cdf1_header(
1013            &[],
1014            &[("count", 4, &int_val), ("scale", 5, &float_val)],
1015            &[],
1016            0,
1017        );
1018        let header = parse_header(&data, NcFormat::Classic).unwrap();
1019        assert_eq!(header.global_attributes.len(), 2);
1020        assert_eq!(header.global_attributes[0].name, "count");
1021        assert_eq!(header.global_attributes[1].name, "scale");
1022    }
1023}