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