Skip to main content

scirs2_io/
netcdf_lite.rs

1//! Pure Rust NetCDF Classic format reader/writer
2//!
3//! Implements the NetCDF Classic (version 1) binary format without any C dependencies.
4//! The NetCDF Classic format stores array-oriented scientific data with:
5//! - Named dimensions (including unlimited/record dimensions)
6//! - Typed variables with associated dimensions
7//! - Global and per-variable attributes
8//!
9//! # Supported data types
10//!
11//! | Type   | NetCDF name | Bytes |
12//! |--------|-------------|-------|
13//! | `i8`   | NC_BYTE     | 1     |
14//! | `i16`  | NC_SHORT    | 2     |
15//! | `i32`  | NC_INT      | 4     |
16//! | `f32`  | NC_FLOAT    | 4     |
17//! | `f64`  | NC_DOUBLE   | 8     |
18//! | `char` | NC_CHAR     | 1     |
19//!
20//! # File format
21//!
22//! NetCDF Classic files are big-endian and use a structured binary layout:
23//! - Magic bytes: `CDF\x01`
24//! - Number of records (unlimited dimension length)
25//! - Dimension list
26//! - Global attribute list
27//! - Variable list (header + data offsets)
28//! - Variable data (contiguous or record-based)
29//!
30//! # Examples
31//!
32//! ```rust
33//! use scirs2_io::netcdf_lite::{NcFile, NcDataType, NcValue};
34//!
35//! // Create a new NetCDF file in memory
36//! let mut nc = NcFile::new();
37//! nc.add_dimension("x", Some(3)).expect("add dim failed");
38//! nc.add_dimension("y", Some(4)).expect("add dim failed");
39//! nc.add_variable("temperature", NcDataType::Float, &["x", "y"])
40//!     .expect("add var failed");
41//! nc.add_global_attribute("title", NcValue::Text("Test Data".to_string()))
42//!     .expect("add attr failed");
43//!
44//! // Write and read back
45//! let mut buf = Vec::new();
46//! nc.write_to(&mut buf).expect("write failed");
47//!
48//! let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
49//! assert_eq!(loaded.dimensions().len(), 2);
50//! ```
51
52use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
53use std::collections::HashMap;
54use std::io::{Read, Seek, SeekFrom, Write};
55
56use crate::error::{IoError, Result};
57
58// =============================================================================
59// Constants - NetCDF Classic format magic numbers and tags
60// =============================================================================
61
62/// NetCDF Classic format magic bytes
63const NC_MAGIC: &[u8; 4] = b"CDF\x01";
64
65/// Tag for dimension list
66const NC_DIMENSION: u32 = 0x0000_000A;
67/// Tag for variable list
68const NC_VARIABLE: u32 = 0x0000_000B;
69/// Tag for attribute list
70const NC_ATTRIBUTE: u32 = 0x0000_000C;
71
72/// Tag indicating absent (empty) list
73const NC_ABSENT: u32 = 0x0000_0000;
74
75// NetCDF data type codes
76const NC_BYTE: u32 = 1;
77const NC_CHAR: u32 = 2;
78const NC_SHORT: u32 = 3;
79const NC_INT: u32 = 4;
80const NC_FLOAT: u32 = 5;
81const NC_DOUBLE: u32 = 6;
82
83// =============================================================================
84// Data types
85// =============================================================================
86
87/// NetCDF data type
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum NcDataType {
90    /// Signed 8-bit integer (NC_BYTE)
91    Byte,
92    /// Character / text (NC_CHAR)
93    Char,
94    /// Signed 16-bit integer (NC_SHORT)
95    Short,
96    /// Signed 32-bit integer (NC_INT)
97    Int,
98    /// 32-bit IEEE float (NC_FLOAT)
99    Float,
100    /// 64-bit IEEE float (NC_DOUBLE)
101    Double,
102}
103
104impl NcDataType {
105    /// Size in bytes for one element of this type
106    pub fn element_size(self) -> usize {
107        match self {
108            NcDataType::Byte | NcDataType::Char => 1,
109            NcDataType::Short => 2,
110            NcDataType::Int | NcDataType::Float => 4,
111            NcDataType::Double => 8,
112        }
113    }
114
115    fn to_nc_type(self) -> u32 {
116        match self {
117            NcDataType::Byte => NC_BYTE,
118            NcDataType::Char => NC_CHAR,
119            NcDataType::Short => NC_SHORT,
120            NcDataType::Int => NC_INT,
121            NcDataType::Float => NC_FLOAT,
122            NcDataType::Double => NC_DOUBLE,
123        }
124    }
125
126    fn from_nc_type(code: u32) -> Result<Self> {
127        match code {
128            NC_BYTE => Ok(NcDataType::Byte),
129            NC_CHAR => Ok(NcDataType::Char),
130            NC_SHORT => Ok(NcDataType::Short),
131            NC_INT => Ok(NcDataType::Int),
132            NC_FLOAT => Ok(NcDataType::Float),
133            NC_DOUBLE => Ok(NcDataType::Double),
134            _ => Err(IoError::FormatError(format!(
135                "Unknown NetCDF data type code: {}",
136                code
137            ))),
138        }
139    }
140}
141
142// =============================================================================
143// Attribute values
144// =============================================================================
145
146/// A NetCDF attribute value
147#[derive(Debug, Clone, PartialEq)]
148pub enum NcValue {
149    /// Array of signed bytes
150    Bytes(Vec<i8>),
151    /// Text string (NC_CHAR array)
152    Text(String),
153    /// Array of signed 16-bit integers
154    Shorts(Vec<i16>),
155    /// Array of signed 32-bit integers
156    Ints(Vec<i32>),
157    /// Array of 32-bit floats
158    Floats(Vec<f32>),
159    /// Array of 64-bit floats
160    Doubles(Vec<f64>),
161}
162
163impl NcValue {
164    fn nc_type(&self) -> u32 {
165        match self {
166            NcValue::Bytes(_) => NC_BYTE,
167            NcValue::Text(_) => NC_CHAR,
168            NcValue::Shorts(_) => NC_SHORT,
169            NcValue::Ints(_) => NC_INT,
170            NcValue::Floats(_) => NC_FLOAT,
171            NcValue::Doubles(_) => NC_DOUBLE,
172        }
173    }
174
175    fn element_count(&self) -> usize {
176        match self {
177            NcValue::Bytes(v) => v.len(),
178            NcValue::Text(s) => s.len(),
179            NcValue::Shorts(v) => v.len(),
180            NcValue::Ints(v) => v.len(),
181            NcValue::Floats(v) => v.len(),
182            NcValue::Doubles(v) => v.len(),
183        }
184    }
185}
186
187// =============================================================================
188// Dimension
189// =============================================================================
190
191/// A named dimension
192#[derive(Debug, Clone)]
193pub struct NcDimension {
194    /// Dimension name
195    pub name: String,
196    /// Dimension length (None = unlimited/record dimension)
197    pub length: Option<usize>,
198    /// Whether this is the unlimited (record) dimension
199    pub is_unlimited: bool,
200}
201
202// =============================================================================
203// Variable metadata
204// =============================================================================
205
206/// Metadata for a variable stored in the file
207#[derive(Debug, Clone)]
208pub struct NcVariable {
209    /// Variable name
210    pub name: String,
211    /// Data type
212    pub data_type: NcDataType,
213    /// Dimension indices (referencing the file's dimension list)
214    pub dim_indices: Vec<usize>,
215    /// Variable-level attributes
216    pub attributes: Vec<(String, NcValue)>,
217    /// Raw data bytes (stored in big-endian)
218    pub(crate) data: Vec<u8>,
219}
220
221impl NcVariable {
222    /// Get the shape of this variable based on dimension lengths
223    pub fn shape(&self, dimensions: &[NcDimension], num_records: usize) -> Vec<usize> {
224        self.dim_indices
225            .iter()
226            .map(|&idx| {
227                if dimensions[idx].is_unlimited {
228                    num_records
229                } else {
230                    dimensions[idx].length.unwrap_or(0)
231                }
232            })
233            .collect()
234    }
235
236    /// Get total number of elements
237    pub fn total_elements(&self, dimensions: &[NcDimension], num_records: usize) -> usize {
238        let shape = self.shape(dimensions, num_records);
239        if shape.is_empty() {
240            1
241        } else {
242            shape.iter().product()
243        }
244    }
245
246    /// Read data as f64 values
247    pub fn as_f64(&self, dimensions: &[NcDimension], num_records: usize) -> Result<Vec<f64>> {
248        let n = self.total_elements(dimensions, num_records);
249        let mut cursor = std::io::Cursor::new(&self.data);
250        let mut result = Vec::with_capacity(n);
251        for _ in 0..n {
252            let val = match self.data_type {
253                NcDataType::Byte => cursor
254                    .read_i8()
255                    .map_err(|e| IoError::FormatError(e.to_string()))?
256                    as f64,
257                NcDataType::Short => cursor
258                    .read_i16::<BigEndian>()
259                    .map_err(|e| IoError::FormatError(e.to_string()))?
260                    as f64,
261                NcDataType::Int => cursor
262                    .read_i32::<BigEndian>()
263                    .map_err(|e| IoError::FormatError(e.to_string()))?
264                    as f64,
265                NcDataType::Float => cursor
266                    .read_f32::<BigEndian>()
267                    .map_err(|e| IoError::FormatError(e.to_string()))?
268                    as f64,
269                NcDataType::Double => cursor
270                    .read_f64::<BigEndian>()
271                    .map_err(|e| IoError::FormatError(e.to_string()))?,
272                NcDataType::Char => cursor
273                    .read_u8()
274                    .map_err(|e| IoError::FormatError(e.to_string()))?
275                    as f64,
276            };
277            result.push(val);
278        }
279        Ok(result)
280    }
281
282    /// Read data as f32 values
283    pub fn as_f32(&self, dimensions: &[NcDimension], num_records: usize) -> Result<Vec<f32>> {
284        self.as_f64(dimensions, num_records)
285            .map(|v| v.into_iter().map(|x| x as f32).collect())
286    }
287
288    /// Read data as i32 values
289    pub fn as_i32(&self, dimensions: &[NcDimension], num_records: usize) -> Result<Vec<i32>> {
290        self.as_f64(dimensions, num_records)
291            .map(|v| v.into_iter().map(|x| x as i32).collect())
292    }
293
294    /// Read data as text (for NC_CHAR variables)
295    pub fn as_text(&self) -> Result<String> {
296        if self.data_type != NcDataType::Char {
297            return Err(IoError::ConversionError(
298                "Variable is not NC_CHAR type".to_string(),
299            ));
300        }
301        let s = String::from_utf8_lossy(&self.data);
302        Ok(s.trim_end_matches('\0').to_string())
303    }
304}
305
306// =============================================================================
307// NcFile - the main file structure
308// =============================================================================
309
310/// A NetCDF Classic format file in memory
311///
312/// Holds all dimensions, variables (with data), and global attributes.
313/// Can be read from and written to any `Read`/`Write` stream.
314#[derive(Debug, Clone)]
315pub struct NcFile {
316    /// Dimensions
317    dims: Vec<NcDimension>,
318    /// Global attributes
319    global_attrs: Vec<(String, NcValue)>,
320    /// Variables (with embedded data)
321    vars: Vec<NcVariable>,
322    /// Number of records (unlimited dimension current length)
323    num_records: usize,
324}
325
326impl NcFile {
327    /// Create a new empty NetCDF file
328    pub fn new() -> Self {
329        NcFile {
330            dims: Vec::new(),
331            global_attrs: Vec::new(),
332            vars: Vec::new(),
333            num_records: 0,
334        }
335    }
336
337    /// Add a dimension
338    ///
339    /// - `length = Some(n)` for a fixed dimension of size n
340    /// - `length = None` for the unlimited (record) dimension
341    ///
342    /// Only one unlimited dimension is allowed per file.
343    pub fn add_dimension(&mut self, name: &str, length: Option<usize>) -> Result<()> {
344        let is_unlimited = length.is_none();
345
346        // Check for duplicate name
347        if self.dims.iter().any(|d| d.name == name) {
348            return Err(IoError::FormatError(format!(
349                "Dimension '{}' already exists",
350                name
351            )));
352        }
353
354        // Only one unlimited dimension allowed
355        if is_unlimited && self.dims.iter().any(|d| d.is_unlimited) {
356            return Err(IoError::FormatError(
357                "Only one unlimited dimension is allowed in NetCDF Classic format".to_string(),
358            ));
359        }
360
361        self.dims.push(NcDimension {
362            name: name.to_string(),
363            length,
364            is_unlimited,
365        });
366
367        Ok(())
368    }
369
370    /// Add a variable
371    ///
372    /// Dimension names must already be defined via `add_dimension`.
373    pub fn add_variable(
374        &mut self,
375        name: &str,
376        data_type: NcDataType,
377        dim_names: &[&str],
378    ) -> Result<()> {
379        // Check for duplicate
380        if self.vars.iter().any(|v| v.name == name) {
381            return Err(IoError::FormatError(format!(
382                "Variable '{}' already exists",
383                name
384            )));
385        }
386
387        // Resolve dimension indices
388        let mut dim_indices = Vec::with_capacity(dim_names.len());
389        for &dname in dim_names {
390            let idx = self
391                .dims
392                .iter()
393                .position(|d| d.name == dname)
394                .ok_or_else(|| IoError::FormatError(format!("Dimension '{}' not found", dname)))?;
395            dim_indices.push(idx);
396        }
397
398        self.vars.push(NcVariable {
399            name: name.to_string(),
400            data_type,
401            dim_indices,
402            attributes: Vec::new(),
403            data: Vec::new(),
404        });
405
406        Ok(())
407    }
408
409    /// Add a global attribute
410    pub fn add_global_attribute(&mut self, name: &str, value: NcValue) -> Result<()> {
411        // Replace if exists
412        if let Some(pos) = self.global_attrs.iter().position(|(n, _)| n == name) {
413            self.global_attrs[pos] = (name.to_string(), value);
414        } else {
415            self.global_attrs.push((name.to_string(), value));
416        }
417        Ok(())
418    }
419
420    /// Add a variable attribute
421    pub fn add_variable_attribute(
422        &mut self,
423        var_name: &str,
424        attr_name: &str,
425        value: NcValue,
426    ) -> Result<()> {
427        let var = self
428            .vars
429            .iter_mut()
430            .find(|v| v.name == var_name)
431            .ok_or_else(|| IoError::NotFound(format!("Variable '{}' not found", var_name)))?;
432
433        if let Some(pos) = var.attributes.iter().position(|(n, _)| n == attr_name) {
434            var.attributes[pos] = (attr_name.to_string(), value);
435        } else {
436            var.attributes.push((attr_name.to_string(), value));
437        }
438        Ok(())
439    }
440
441    /// Set variable data from f64 slice
442    pub fn set_variable_f64(&mut self, var_name: &str, data: &[f64]) -> Result<()> {
443        let var = self
444            .vars
445            .iter_mut()
446            .find(|v| v.name == var_name)
447            .ok_or_else(|| IoError::NotFound(format!("Variable '{}' not found", var_name)))?;
448
449        let mut buf = Vec::with_capacity(data.len() * var.data_type.element_size());
450        for &val in data {
451            match var.data_type {
452                NcDataType::Byte => buf
453                    .write_i8(val as i8)
454                    .map_err(|e| IoError::FileError(e.to_string()))?,
455                NcDataType::Short => buf
456                    .write_i16::<BigEndian>(val as i16)
457                    .map_err(|e| IoError::FileError(e.to_string()))?,
458                NcDataType::Int => buf
459                    .write_i32::<BigEndian>(val as i32)
460                    .map_err(|e| IoError::FileError(e.to_string()))?,
461                NcDataType::Float => buf
462                    .write_f32::<BigEndian>(val as f32)
463                    .map_err(|e| IoError::FileError(e.to_string()))?,
464                NcDataType::Double => buf
465                    .write_f64::<BigEndian>(val)
466                    .map_err(|e| IoError::FileError(e.to_string()))?,
467                NcDataType::Char => buf
468                    .write_u8(val as u8)
469                    .map_err(|e| IoError::FileError(e.to_string()))?,
470            }
471        }
472        var.data = buf;
473
474        // Update num_records if variable uses unlimited dimension
475        self.update_num_records();
476        Ok(())
477    }
478
479    /// Set variable data from f32 slice
480    pub fn set_variable_f32(&mut self, var_name: &str, data: &[f32]) -> Result<()> {
481        let f64_data: Vec<f64> = data.iter().map(|&x| x as f64).collect();
482        self.set_variable_f64(var_name, &f64_data)
483    }
484
485    /// Set variable data from i32 slice
486    pub fn set_variable_i32(&mut self, var_name: &str, data: &[i32]) -> Result<()> {
487        let f64_data: Vec<f64> = data.iter().map(|&x| x as f64).collect();
488        self.set_variable_f64(var_name, &f64_data)
489    }
490
491    /// Set variable data as text (for NC_CHAR variables)
492    pub fn set_variable_text(&mut self, var_name: &str, text: &str) -> Result<()> {
493        let var = self
494            .vars
495            .iter_mut()
496            .find(|v| v.name == var_name)
497            .ok_or_else(|| IoError::NotFound(format!("Variable '{}' not found", var_name)))?;
498
499        if var.data_type != NcDataType::Char {
500            return Err(IoError::ConversionError(format!(
501                "Variable '{}' is not NC_CHAR type",
502                var_name
503            )));
504        }
505
506        var.data = text.as_bytes().to_vec();
507        self.update_num_records();
508        Ok(())
509    }
510
511    /// Get dimensions
512    pub fn dimensions(&self) -> &[NcDimension] {
513        &self.dims
514    }
515
516    /// Get global attributes
517    pub fn global_attributes(&self) -> &[(String, NcValue)] {
518        &self.global_attrs
519    }
520
521    /// Get variable names
522    pub fn variable_names(&self) -> Vec<&str> {
523        self.vars.iter().map(|v| v.name.as_str()).collect()
524    }
525
526    /// Get a variable by name
527    pub fn variable(&self, name: &str) -> Result<&NcVariable> {
528        self.vars
529            .iter()
530            .find(|v| v.name == name)
531            .ok_or_else(|| IoError::NotFound(format!("Variable '{}' not found", name)))
532    }
533
534    /// Get number of records (unlimited dimension length)
535    pub fn num_records(&self) -> usize {
536        self.num_records
537    }
538
539    /// Update num_records from current variable data
540    fn update_num_records(&mut self) {
541        for var in &self.vars {
542            if var.dim_indices.is_empty() {
543                continue;
544            }
545            if self.dims[var.dim_indices[0]].is_unlimited && !var.data.is_empty() {
546                let elem_size = var.data_type.element_size();
547                let per_record_elements: usize = var.dim_indices[1..]
548                    .iter()
549                    .map(|&idx| self.dims[idx].length.unwrap_or(1))
550                    .product::<usize>()
551                    .max(1);
552                let total_elements = var.data.len() / elem_size;
553                let records = if per_record_elements > 0 {
554                    total_elements / per_record_elements
555                } else {
556                    0
557                };
558                if records > self.num_records {
559                    self.num_records = records;
560                }
561            }
562        }
563    }
564
565    // =========================================================================
566    // Writing
567    // =========================================================================
568
569    /// Write the NetCDF file to a writer
570    pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
571        // Magic + numrecs
572        writer
573            .write_all(NC_MAGIC)
574            .map_err(|e| IoError::FileError(e.to_string()))?;
575        writer
576            .write_u32::<BigEndian>(self.num_records as u32)
577            .map_err(|e| IoError::FileError(e.to_string()))?;
578
579        // Dimensions
580        self.write_dim_list(writer)?;
581
582        // Global attributes
583        self.write_attr_list(writer, &self.global_attrs)?;
584
585        // Variables header + data
586        self.write_var_list(writer)?;
587
588        Ok(())
589    }
590
591    /// Write to a file path
592    pub fn write_to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
593        let file = std::fs::File::create(path).map_err(|e| IoError::FileError(e.to_string()))?;
594        let mut writer = std::io::BufWriter::new(file);
595        self.write_to(&mut writer)?;
596        writer
597            .flush()
598            .map_err(|e| IoError::FileError(e.to_string()))?;
599        Ok(())
600    }
601
602    fn write_dim_list<W: Write>(&self, w: &mut W) -> Result<()> {
603        if self.dims.is_empty() {
604            w.write_u32::<BigEndian>(NC_ABSENT)
605                .map_err(|e| IoError::FileError(e.to_string()))?;
606            w.write_u32::<BigEndian>(0)
607                .map_err(|e| IoError::FileError(e.to_string()))?;
608            return Ok(());
609        }
610
611        w.write_u32::<BigEndian>(NC_DIMENSION)
612            .map_err(|e| IoError::FileError(e.to_string()))?;
613        w.write_u32::<BigEndian>(self.dims.len() as u32)
614            .map_err(|e| IoError::FileError(e.to_string()))?;
615
616        for dim in &self.dims {
617            write_name(w, &dim.name)?;
618            let len = if dim.is_unlimited {
619                0u32
620            } else {
621                dim.length.unwrap_or(0) as u32
622            };
623            w.write_u32::<BigEndian>(len)
624                .map_err(|e| IoError::FileError(e.to_string()))?;
625        }
626        Ok(())
627    }
628
629    fn write_attr_list<W: Write>(&self, w: &mut W, attrs: &[(String, NcValue)]) -> Result<()> {
630        if attrs.is_empty() {
631            w.write_u32::<BigEndian>(NC_ABSENT)
632                .map_err(|e| IoError::FileError(e.to_string()))?;
633            w.write_u32::<BigEndian>(0)
634                .map_err(|e| IoError::FileError(e.to_string()))?;
635            return Ok(());
636        }
637
638        w.write_u32::<BigEndian>(NC_ATTRIBUTE)
639            .map_err(|e| IoError::FileError(e.to_string()))?;
640        w.write_u32::<BigEndian>(attrs.len() as u32)
641            .map_err(|e| IoError::FileError(e.to_string()))?;
642
643        for (name, value) in attrs {
644            write_name(w, name)?;
645            write_attr_value(w, value)?;
646        }
647        Ok(())
648    }
649
650    fn write_var_list<W: Write>(&self, w: &mut W) -> Result<()> {
651        if self.vars.is_empty() {
652            w.write_u32::<BigEndian>(NC_ABSENT)
653                .map_err(|e| IoError::FileError(e.to_string()))?;
654            w.write_u32::<BigEndian>(0)
655                .map_err(|e| IoError::FileError(e.to_string()))?;
656            return Ok(());
657        }
658
659        w.write_u32::<BigEndian>(NC_VARIABLE)
660            .map_err(|e| IoError::FileError(e.to_string()))?;
661        w.write_u32::<BigEndian>(self.vars.len() as u32)
662            .map_err(|e| IoError::FileError(e.to_string()))?;
663
664        // First pass: calculate header size to determine data offsets
665        // For simplicity, we embed data right after the header section.
666        // We pre-compute data sizes for the offset fields.
667        let mut data_sizes: Vec<usize> = Vec::with_capacity(self.vars.len());
668        for var in &self.vars {
669            let raw_size = var.data.len();
670            let padded = pad_to_4(raw_size);
671            data_sizes.push(padded);
672        }
673
674        // We need to know the current offset after writing all variable headers.
675        // But we need the offset *within the file* for the `begin` field.
676        // Since we write variable headers followed immediately by data,
677        // we calculate header sizes.
678        let mut header_total = 0usize;
679        for var in &self.vars {
680            // name
681            header_total += 4 + pad_to_4(var.name.len());
682            // ndims + dim_ids
683            header_total += 4 + var.dim_indices.len() * 4;
684            // vatt_list
685            header_total += self.attr_list_size(&var.attributes);
686            // nc_type + vsize + begin
687            header_total += 4 + 4 + 4;
688        }
689
690        // Current position = 8 (magic + numrecs) + dim_list_size + gatt_list_size + 8 (var tag + count) + header_total
691        // But since we write data inline, we just need offsets relative to file start.
692        // For correctness, we calculate the absolute start of the first var data.
693
694        // Calculate sizes for preceding sections
695        let dim_list_size = self.dim_list_size();
696        let gatt_list_size = self.attr_list_size(&self.global_attrs);
697        let file_header_size = 8 + dim_list_size + gatt_list_size + 8 + header_total;
698
699        let mut current_data_offset = file_header_size;
700
701        // Write variable headers
702        for (i, var) in self.vars.iter().enumerate() {
703            write_name(w, &var.name)?;
704
705            // Dimension ID list
706            w.write_u32::<BigEndian>(var.dim_indices.len() as u32)
707                .map_err(|e| IoError::FileError(e.to_string()))?;
708            for &dim_idx in &var.dim_indices {
709                w.write_u32::<BigEndian>(dim_idx as u32)
710                    .map_err(|e| IoError::FileError(e.to_string()))?;
711            }
712
713            // Variable attributes
714            self.write_attr_list(w, &var.attributes)?;
715
716            // nc_type
717            w.write_u32::<BigEndian>(var.data_type.to_nc_type())
718                .map_err(|e| IoError::FileError(e.to_string()))?;
719
720            // vsize (padded data size)
721            w.write_u32::<BigEndian>(data_sizes[i] as u32)
722                .map_err(|e| IoError::FileError(e.to_string()))?;
723
724            // begin (offset to data)
725            w.write_u32::<BigEndian>(current_data_offset as u32)
726                .map_err(|e| IoError::FileError(e.to_string()))?;
727
728            current_data_offset += data_sizes[i];
729        }
730
731        // Write variable data
732        for (i, var) in self.vars.iter().enumerate() {
733            w.write_all(&var.data)
734                .map_err(|e| IoError::FileError(e.to_string()))?;
735
736            // Pad to 4-byte boundary
737            let padding_needed = data_sizes[i] - var.data.len();
738            if padding_needed > 0 {
739                let pad = vec![0u8; padding_needed];
740                w.write_all(&pad)
741                    .map_err(|e| IoError::FileError(e.to_string()))?;
742            }
743        }
744
745        Ok(())
746    }
747
748    fn dim_list_size(&self) -> usize {
749        if self.dims.is_empty() {
750            return 8; // tag + count
751        }
752        let mut size = 8; // tag + count
753        for dim in &self.dims {
754            size += 4 + pad_to_4(dim.name.len()); // name
755            size += 4; // length
756        }
757        size
758    }
759
760    fn attr_list_size(&self, attrs: &[(String, NcValue)]) -> usize {
761        if attrs.is_empty() {
762            return 8; // tag + count
763        }
764        let mut size = 8; // tag + count
765        for (name, value) in attrs {
766            size += 4 + pad_to_4(name.len()); // name
767            size += 4 + 4; // nc_type + nelems
768            size += pad_to_4(value.element_count() * element_size_for_nc_type(value.nc_type()));
769        }
770        size
771    }
772
773    // =========================================================================
774    // Reading
775    // =========================================================================
776
777    /// Read a NetCDF Classic file from a reader
778    pub fn read_from<R: Read + Seek>(reader: &mut R) -> Result<Self> {
779        // Read and verify magic
780        let mut magic = [0u8; 4];
781        reader
782            .read_exact(&mut magic)
783            .map_err(|e| IoError::FormatError(format!("Failed to read magic: {}", e)))?;
784        if &magic != NC_MAGIC {
785            return Err(IoError::FormatError(
786                "Not a NetCDF Classic format file (bad magic)".to_string(),
787            ));
788        }
789
790        // Number of records
791        let num_records = reader
792            .read_u32::<BigEndian>()
793            .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
794
795        // Dimensions
796        let dims = read_dim_list(reader)?;
797
798        // Global attributes
799        let global_attrs = read_attr_list(reader)?;
800
801        // Variables (headers only first)
802        let (mut vars, offsets, vsizes) = read_var_headers(reader)?;
803
804        // Read variable data from offsets
805        for (i, var) in vars.iter_mut().enumerate() {
806            let offset = offsets[i];
807            let vsize = vsizes[i];
808
809            reader
810                .seek(SeekFrom::Start(offset as u64))
811                .map_err(|e| IoError::FormatError(format!("Failed to seek to var data: {}", e)))?;
812
813            // Calculate actual data size (without padding)
814            let total_elements = var_total_elements(var, &dims, num_records);
815            let actual_size = total_elements * var.data_type.element_size();
816            let read_size = actual_size.min(vsize);
817
818            let mut data = vec![0u8; read_size];
819            reader
820                .read_exact(&mut data)
821                .map_err(|e| IoError::FormatError(format!("Failed to read var data: {}", e)))?;
822
823            var.data = data;
824        }
825
826        Ok(NcFile {
827            dims,
828            global_attrs,
829            vars,
830            num_records,
831        })
832    }
833
834    /// Read from a file path
835    pub fn read_from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
836        let file = std::fs::File::open(path).map_err(|e| IoError::FileError(e.to_string()))?;
837        let mut reader = std::io::BufReader::new(file);
838        Self::read_from(&mut reader)
839    }
840}
841
842impl Default for NcFile {
843    fn default() -> Self {
844        Self::new()
845    }
846}
847
848// =============================================================================
849// Helper functions
850// =============================================================================
851
852/// Pad a size to the next multiple of 4
853fn pad_to_4(n: usize) -> usize {
854    (n + 3) & !3
855}
856
857fn element_size_for_nc_type(nc_type: u32) -> usize {
858    match nc_type {
859        NC_BYTE | NC_CHAR => 1,
860        NC_SHORT => 2,
861        NC_INT | NC_FLOAT => 4,
862        NC_DOUBLE => 8,
863        _ => 1,
864    }
865}
866
867/// Write a name (length-prefixed, padded to 4 bytes)
868fn write_name<W: Write>(w: &mut W, name: &str) -> Result<()> {
869    let bytes = name.as_bytes();
870    w.write_u32::<BigEndian>(bytes.len() as u32)
871        .map_err(|e| IoError::FileError(e.to_string()))?;
872    w.write_all(bytes)
873        .map_err(|e| IoError::FileError(e.to_string()))?;
874    // Pad to 4-byte boundary
875    let padding = pad_to_4(bytes.len()) - bytes.len();
876    if padding > 0 {
877        let pad = vec![0u8; padding];
878        w.write_all(&pad)
879            .map_err(|e| IoError::FileError(e.to_string()))?;
880    }
881    Ok(())
882}
883
884/// Write an attribute value
885fn write_attr_value<W: Write>(w: &mut W, value: &NcValue) -> Result<()> {
886    w.write_u32::<BigEndian>(value.nc_type())
887        .map_err(|e| IoError::FileError(e.to_string()))?;
888    w.write_u32::<BigEndian>(value.element_count() as u32)
889        .map_err(|e| IoError::FileError(e.to_string()))?;
890
891    let elem_size = element_size_for_nc_type(value.nc_type());
892    let raw_size = value.element_count() * elem_size;
893
894    match value {
895        NcValue::Bytes(v) => {
896            for &b in v {
897                w.write_i8(b)
898                    .map_err(|e| IoError::FileError(e.to_string()))?;
899            }
900        }
901        NcValue::Text(s) => {
902            w.write_all(s.as_bytes())
903                .map_err(|e| IoError::FileError(e.to_string()))?;
904        }
905        NcValue::Shorts(v) => {
906            for &val in v {
907                w.write_i16::<BigEndian>(val)
908                    .map_err(|e| IoError::FileError(e.to_string()))?;
909            }
910        }
911        NcValue::Ints(v) => {
912            for &val in v {
913                w.write_i32::<BigEndian>(val)
914                    .map_err(|e| IoError::FileError(e.to_string()))?;
915            }
916        }
917        NcValue::Floats(v) => {
918            for &val in v {
919                w.write_f32::<BigEndian>(val)
920                    .map_err(|e| IoError::FileError(e.to_string()))?;
921            }
922        }
923        NcValue::Doubles(v) => {
924            for &val in v {
925                w.write_f64::<BigEndian>(val)
926                    .map_err(|e| IoError::FileError(e.to_string()))?;
927            }
928        }
929    }
930
931    // Pad to 4-byte boundary
932    let padding = pad_to_4(raw_size) - raw_size;
933    if padding > 0 {
934        let pad = vec![0u8; padding];
935        w.write_all(&pad)
936            .map_err(|e| IoError::FileError(e.to_string()))?;
937    }
938
939    Ok(())
940}
941
942/// Read a padded name from a reader
943fn read_name<R: Read>(r: &mut R) -> Result<String> {
944    let len = r
945        .read_u32::<BigEndian>()
946        .map_err(|e| IoError::FormatError(format!("Failed to read name length: {}", e)))?
947        as usize;
948    let padded_len = pad_to_4(len);
949    let mut buf = vec![0u8; padded_len];
950    r.read_exact(&mut buf)
951        .map_err(|e| IoError::FormatError(format!("Failed to read name: {}", e)))?;
952    buf.truncate(len);
953    String::from_utf8(buf)
954        .map_err(|e| IoError::FormatError(format!("Invalid UTF-8 in name: {}", e)))
955}
956
957/// Read a dimension list
958fn read_dim_list<R: Read>(r: &mut R) -> Result<Vec<NcDimension>> {
959    let tag = r
960        .read_u32::<BigEndian>()
961        .map_err(|e| IoError::FormatError(e.to_string()))?;
962    let count = r
963        .read_u32::<BigEndian>()
964        .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
965
966    if tag == NC_ABSENT || count == 0 {
967        return Ok(Vec::new());
968    }
969
970    if tag != NC_DIMENSION {
971        return Err(IoError::FormatError(format!(
972            "Expected NC_DIMENSION tag, got 0x{:08X}",
973            tag
974        )));
975    }
976
977    let mut dims = Vec::with_capacity(count);
978    for _ in 0..count {
979        let name = read_name(r)?;
980        let len = r
981            .read_u32::<BigEndian>()
982            .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
983
984        let is_unlimited = len == 0;
985        let length = if is_unlimited { None } else { Some(len) };
986
987        dims.push(NcDimension {
988            name,
989            length,
990            is_unlimited,
991        });
992    }
993    Ok(dims)
994}
995
996/// Read an attribute list
997fn read_attr_list<R: Read>(r: &mut R) -> Result<Vec<(String, NcValue)>> {
998    let tag = r
999        .read_u32::<BigEndian>()
1000        .map_err(|e| IoError::FormatError(e.to_string()))?;
1001    let count = r
1002        .read_u32::<BigEndian>()
1003        .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
1004
1005    if tag == NC_ABSENT || count == 0 {
1006        return Ok(Vec::new());
1007    }
1008
1009    if tag != NC_ATTRIBUTE {
1010        return Err(IoError::FormatError(format!(
1011            "Expected NC_ATTRIBUTE tag, got 0x{:08X}",
1012            tag
1013        )));
1014    }
1015
1016    let mut attrs = Vec::with_capacity(count);
1017    for _ in 0..count {
1018        let name = read_name(r)?;
1019        let value = read_attr_value(r)?;
1020        attrs.push((name, value));
1021    }
1022    Ok(attrs)
1023}
1024
1025/// Read an attribute value
1026fn read_attr_value<R: Read>(r: &mut R) -> Result<NcValue> {
1027    let nc_type = r
1028        .read_u32::<BigEndian>()
1029        .map_err(|e| IoError::FormatError(e.to_string()))?;
1030    let nelems = r
1031        .read_u32::<BigEndian>()
1032        .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
1033
1034    let elem_size = element_size_for_nc_type(nc_type);
1035    let raw_size = nelems * elem_size;
1036    let padded_size = pad_to_4(raw_size);
1037
1038    let value = match nc_type {
1039        NC_BYTE => {
1040            let mut v = Vec::with_capacity(nelems);
1041            for _ in 0..nelems {
1042                v.push(
1043                    r.read_i8()
1044                        .map_err(|e| IoError::FormatError(e.to_string()))?,
1045                );
1046            }
1047            // Read padding
1048            let padding = padded_size - raw_size;
1049            if padding > 0 {
1050                let mut pad = vec![0u8; padding];
1051                r.read_exact(&mut pad)
1052                    .map_err(|e| IoError::FormatError(e.to_string()))?;
1053            }
1054            NcValue::Bytes(v)
1055        }
1056        NC_CHAR => {
1057            let mut buf = vec![0u8; nelems];
1058            r.read_exact(&mut buf)
1059                .map_err(|e| IoError::FormatError(e.to_string()))?;
1060            // Read padding
1061            let padding = padded_size - raw_size;
1062            if padding > 0 {
1063                let mut pad = vec![0u8; padding];
1064                r.read_exact(&mut pad)
1065                    .map_err(|e| IoError::FormatError(e.to_string()))?;
1066            }
1067            let s = String::from_utf8_lossy(&buf)
1068                .trim_end_matches('\0')
1069                .to_string();
1070            NcValue::Text(s)
1071        }
1072        NC_SHORT => {
1073            let mut v = Vec::with_capacity(nelems);
1074            for _ in 0..nelems {
1075                v.push(
1076                    r.read_i16::<BigEndian>()
1077                        .map_err(|e| IoError::FormatError(e.to_string()))?,
1078                );
1079            }
1080            let padding = padded_size - raw_size;
1081            if padding > 0 {
1082                let mut pad = vec![0u8; padding];
1083                r.read_exact(&mut pad)
1084                    .map_err(|e| IoError::FormatError(e.to_string()))?;
1085            }
1086            NcValue::Shorts(v)
1087        }
1088        NC_INT => {
1089            let mut v = Vec::with_capacity(nelems);
1090            for _ in 0..nelems {
1091                v.push(
1092                    r.read_i32::<BigEndian>()
1093                        .map_err(|e| IoError::FormatError(e.to_string()))?,
1094                );
1095            }
1096            NcValue::Ints(v)
1097        }
1098        NC_FLOAT => {
1099            let mut v = Vec::with_capacity(nelems);
1100            for _ in 0..nelems {
1101                v.push(
1102                    r.read_f32::<BigEndian>()
1103                        .map_err(|e| IoError::FormatError(e.to_string()))?,
1104                );
1105            }
1106            NcValue::Floats(v)
1107        }
1108        NC_DOUBLE => {
1109            let mut v = Vec::with_capacity(nelems);
1110            for _ in 0..nelems {
1111                v.push(
1112                    r.read_f64::<BigEndian>()
1113                        .map_err(|e| IoError::FormatError(e.to_string()))?,
1114                );
1115            }
1116            NcValue::Doubles(v)
1117        }
1118        _ => {
1119            // Skip unknown type
1120            let mut skip = vec![0u8; padded_size];
1121            r.read_exact(&mut skip)
1122                .map_err(|e| IoError::FormatError(e.to_string()))?;
1123            NcValue::Bytes(Vec::new())
1124        }
1125    };
1126
1127    Ok(value)
1128}
1129
1130/// Read variable headers (without data)
1131fn read_var_headers<R: Read>(r: &mut R) -> Result<(Vec<NcVariable>, Vec<usize>, Vec<usize>)> {
1132    let tag = r
1133        .read_u32::<BigEndian>()
1134        .map_err(|e| IoError::FormatError(e.to_string()))?;
1135    let count = r
1136        .read_u32::<BigEndian>()
1137        .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
1138
1139    if tag == NC_ABSENT || count == 0 {
1140        return Ok((Vec::new(), Vec::new(), Vec::new()));
1141    }
1142
1143    if tag != NC_VARIABLE {
1144        return Err(IoError::FormatError(format!(
1145            "Expected NC_VARIABLE tag, got 0x{:08X}",
1146            tag
1147        )));
1148    }
1149
1150    let mut vars = Vec::with_capacity(count);
1151    let mut offsets = Vec::with_capacity(count);
1152    let mut vsizes = Vec::with_capacity(count);
1153
1154    for _ in 0..count {
1155        let name = read_name(r)?;
1156
1157        // Dimension IDs
1158        let ndims = r
1159            .read_u32::<BigEndian>()
1160            .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
1161        let mut dim_indices = Vec::with_capacity(ndims);
1162        for _ in 0..ndims {
1163            dim_indices.push(
1164                r.read_u32::<BigEndian>()
1165                    .map_err(|e| IoError::FormatError(e.to_string()))? as usize,
1166            );
1167        }
1168
1169        // Variable attributes
1170        let attributes = read_attr_list(r)?;
1171
1172        // nc_type
1173        let nc_type = r
1174            .read_u32::<BigEndian>()
1175            .map_err(|e| IoError::FormatError(e.to_string()))?;
1176        let data_type = NcDataType::from_nc_type(nc_type)?;
1177
1178        // vsize
1179        let vsize = r
1180            .read_u32::<BigEndian>()
1181            .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
1182
1183        // begin (offset)
1184        let begin = r
1185            .read_u32::<BigEndian>()
1186            .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
1187
1188        vars.push(NcVariable {
1189            name,
1190            data_type,
1191            dim_indices,
1192            attributes,
1193            data: Vec::new(), // filled later
1194        });
1195        offsets.push(begin);
1196        vsizes.push(vsize);
1197    }
1198
1199    Ok((vars, offsets, vsizes))
1200}
1201
1202/// Calculate total elements for a variable
1203fn var_total_elements(var: &NcVariable, dims: &[NcDimension], num_records: usize) -> usize {
1204    if var.dim_indices.is_empty() {
1205        return 1; // scalar
1206    }
1207    var.dim_indices
1208        .iter()
1209        .map(|&idx| {
1210            if dims[idx].is_unlimited {
1211                num_records
1212            } else {
1213                dims[idx].length.unwrap_or(0)
1214            }
1215        })
1216        .product::<usize>()
1217        .max(0)
1218}
1219
1220// =============================================================================
1221// Tests
1222// =============================================================================
1223
1224#[cfg(test)]
1225mod tests {
1226    use super::*;
1227
1228    #[test]
1229    fn test_create_empty_file() {
1230        let nc = NcFile::new();
1231        assert!(nc.dimensions().is_empty());
1232        assert!(nc.global_attributes().is_empty());
1233        assert!(nc.variable_names().is_empty());
1234    }
1235
1236    #[test]
1237    fn test_add_dimensions() {
1238        let mut nc = NcFile::new();
1239        nc.add_dimension("x", Some(10))
1240            .expect("Failed to add x dim");
1241        nc.add_dimension("y", Some(20))
1242            .expect("Failed to add y dim");
1243        nc.add_dimension("time", None)
1244            .expect("Failed to add unlimited dim");
1245
1246        assert_eq!(nc.dimensions().len(), 3);
1247        assert_eq!(nc.dimensions()[0].name, "x");
1248        assert_eq!(nc.dimensions()[0].length, Some(10));
1249        assert!(!nc.dimensions()[0].is_unlimited);
1250        assert_eq!(nc.dimensions()[2].name, "time");
1251        assert!(nc.dimensions()[2].is_unlimited);
1252    }
1253
1254    #[test]
1255    fn test_duplicate_dimension_rejected() {
1256        let mut nc = NcFile::new();
1257        nc.add_dimension("x", Some(10)).expect("first add ok");
1258        let result = nc.add_dimension("x", Some(5));
1259        assert!(result.is_err());
1260    }
1261
1262    #[test]
1263    fn test_only_one_unlimited_allowed() {
1264        let mut nc = NcFile::new();
1265        nc.add_dimension("time", None).expect("first unlimited ok");
1266        let result = nc.add_dimension("step", None);
1267        assert!(result.is_err());
1268    }
1269
1270    #[test]
1271    fn test_add_variable() {
1272        let mut nc = NcFile::new();
1273        nc.add_dimension("x", Some(3)).expect("dim failed");
1274        nc.add_dimension("y", Some(4)).expect("dim failed");
1275        nc.add_variable("temp", NcDataType::Float, &["x", "y"])
1276            .expect("var failed");
1277
1278        let names = nc.variable_names();
1279        assert_eq!(names.len(), 1);
1280        assert_eq!(names[0], "temp");
1281    }
1282
1283    #[test]
1284    fn test_variable_undefined_dimension() {
1285        let mut nc = NcFile::new();
1286        nc.add_dimension("x", Some(3)).expect("dim failed");
1287        let result = nc.add_variable("temp", NcDataType::Float, &["x", "z"]);
1288        assert!(result.is_err());
1289    }
1290
1291    #[test]
1292    fn test_roundtrip_float_data() {
1293        let mut nc = NcFile::new();
1294        nc.add_dimension("x", Some(3)).expect("dim failed");
1295        nc.add_variable("vals", NcDataType::Float, &["x"])
1296            .expect("var failed");
1297        nc.set_variable_f32("vals", &[1.5, 2.5, 3.5])
1298            .expect("set failed");
1299
1300        // Write to buffer
1301        let mut buf = Vec::new();
1302        nc.write_to(&mut buf).expect("write failed");
1303
1304        // Read back
1305        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1306
1307        assert_eq!(loaded.dimensions().len(), 1);
1308        assert_eq!(loaded.variable_names(), vec!["vals"]);
1309
1310        let var = loaded.variable("vals").expect("var not found");
1311        let data = var
1312            .as_f32(loaded.dimensions(), loaded.num_records())
1313            .expect("as_f32 failed");
1314        assert_eq!(data.len(), 3);
1315        assert!((data[0] - 1.5).abs() < 1e-6);
1316        assert!((data[1] - 2.5).abs() < 1e-6);
1317        assert!((data[2] - 3.5).abs() < 1e-6);
1318    }
1319
1320    #[test]
1321    fn test_roundtrip_double_data() {
1322        let mut nc = NcFile::new();
1323        nc.add_dimension("n", Some(4)).expect("dim failed");
1324        nc.add_variable("data", NcDataType::Double, &["n"])
1325            .expect("var failed");
1326        nc.set_variable_f64("data", &[1.0, 2.0, 3.0, 4.0])
1327            .expect("set failed");
1328
1329        let mut buf = Vec::new();
1330        nc.write_to(&mut buf).expect("write failed");
1331
1332        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1333        let var = loaded.variable("data").expect("var not found");
1334        let data = var
1335            .as_f64(loaded.dimensions(), loaded.num_records())
1336            .expect("as_f64 failed");
1337        assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0]);
1338    }
1339
1340    #[test]
1341    fn test_roundtrip_int_data() {
1342        let mut nc = NcFile::new();
1343        nc.add_dimension("n", Some(5)).expect("dim failed");
1344        nc.add_variable("ids", NcDataType::Int, &["n"])
1345            .expect("var failed");
1346        nc.set_variable_i32("ids", &[10, 20, 30, 40, 50])
1347            .expect("set failed");
1348
1349        let mut buf = Vec::new();
1350        nc.write_to(&mut buf).expect("write failed");
1351
1352        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1353        let var = loaded.variable("ids").expect("var not found");
1354        let data = var
1355            .as_i32(loaded.dimensions(), loaded.num_records())
1356            .expect("as_i32 failed");
1357        assert_eq!(data, vec![10, 20, 30, 40, 50]);
1358    }
1359
1360    #[test]
1361    fn test_roundtrip_text_data() {
1362        let mut nc = NcFile::new();
1363        nc.add_dimension("len", Some(12)).expect("dim failed");
1364        nc.add_variable("msg", NcDataType::Char, &["len"])
1365            .expect("var failed");
1366        nc.set_variable_text("msg", "Hello World!")
1367            .expect("set failed");
1368
1369        let mut buf = Vec::new();
1370        nc.write_to(&mut buf).expect("write failed");
1371
1372        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1373        let var = loaded.variable("msg").expect("var not found");
1374        let text = var.as_text().expect("as_text failed");
1375        assert_eq!(text, "Hello World!");
1376    }
1377
1378    #[test]
1379    fn test_roundtrip_global_attributes() {
1380        let mut nc = NcFile::new();
1381        nc.add_global_attribute("title", NcValue::Text("My Dataset".to_string()))
1382            .expect("attr failed");
1383        nc.add_global_attribute("version", NcValue::Ints(vec![2]))
1384            .expect("attr failed");
1385        nc.add_global_attribute("scale", NcValue::Doubles(vec![0.01]))
1386            .expect("attr failed");
1387
1388        let mut buf = Vec::new();
1389        nc.write_to(&mut buf).expect("write failed");
1390
1391        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1392        let attrs = loaded.global_attributes();
1393        assert_eq!(attrs.len(), 3);
1394
1395        assert_eq!(attrs[0].0, "title");
1396        if let NcValue::Text(ref s) = attrs[0].1 {
1397            assert_eq!(s, "My Dataset");
1398        } else {
1399            panic!("Expected text attribute");
1400        }
1401
1402        assert_eq!(attrs[1].0, "version");
1403        if let NcValue::Ints(ref v) = attrs[1].1 {
1404            assert_eq!(v, &[2]);
1405        } else {
1406            panic!("Expected int attribute");
1407        }
1408    }
1409
1410    #[test]
1411    fn test_roundtrip_variable_attributes() {
1412        let mut nc = NcFile::new();
1413        nc.add_dimension("x", Some(3)).expect("dim failed");
1414        nc.add_variable("temp", NcDataType::Float, &["x"])
1415            .expect("var failed");
1416        nc.add_variable_attribute("temp", "units", NcValue::Text("Celsius".to_string()))
1417            .expect("attr failed");
1418        nc.add_variable_attribute("temp", "scale_factor", NcValue::Floats(vec![0.01]))
1419            .expect("attr failed");
1420        nc.set_variable_f32("temp", &[20.0, 21.5, 22.0])
1421            .expect("set failed");
1422
1423        let mut buf = Vec::new();
1424        nc.write_to(&mut buf).expect("write failed");
1425
1426        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1427        let var = loaded.variable("temp").expect("var not found");
1428        assert_eq!(var.attributes.len(), 2);
1429        assert_eq!(var.attributes[0].0, "units");
1430        if let NcValue::Text(ref s) = var.attributes[0].1 {
1431            assert_eq!(s, "Celsius");
1432        } else {
1433            panic!("Expected text attr");
1434        }
1435    }
1436
1437    #[test]
1438    fn test_roundtrip_2d_data() {
1439        let mut nc = NcFile::new();
1440        nc.add_dimension("x", Some(2)).expect("dim failed");
1441        nc.add_dimension("y", Some(3)).expect("dim failed");
1442        nc.add_variable("grid", NcDataType::Double, &["x", "y"])
1443            .expect("var failed");
1444        // Row-major: [1,2,3, 4,5,6]
1445        nc.set_variable_f64("grid", &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
1446            .expect("set failed");
1447
1448        let mut buf = Vec::new();
1449        nc.write_to(&mut buf).expect("write failed");
1450
1451        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1452        let var = loaded.variable("grid").expect("var not found");
1453        let data = var
1454            .as_f64(loaded.dimensions(), loaded.num_records())
1455            .expect("as_f64 failed");
1456        assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1457    }
1458
1459    #[test]
1460    fn test_roundtrip_unlimited_dimension() {
1461        let mut nc = NcFile::new();
1462        nc.add_dimension("time", None).expect("dim failed");
1463        nc.add_dimension("x", Some(3)).expect("dim failed");
1464        nc.add_variable("data", NcDataType::Float, &["time", "x"])
1465            .expect("var failed");
1466        // 2 records, each with 3 values
1467        nc.set_variable_f32("data", &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
1468            .expect("set failed");
1469
1470        assert_eq!(nc.num_records(), 2);
1471
1472        let mut buf = Vec::new();
1473        nc.write_to(&mut buf).expect("write failed");
1474
1475        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1476        assert_eq!(loaded.num_records(), 2);
1477
1478        let var = loaded.variable("data").expect("var not found");
1479        let shape = var.shape(loaded.dimensions(), loaded.num_records());
1480        assert_eq!(shape, vec![2, 3]);
1481
1482        let data = var
1483            .as_f32(loaded.dimensions(), loaded.num_records())
1484            .expect("as_f32 failed");
1485        assert_eq!(data.len(), 6);
1486        assert!((data[0] - 1.0).abs() < 1e-6);
1487        assert!((data[5] - 6.0).abs() < 1e-6);
1488    }
1489
1490    #[test]
1491    fn test_multiple_variables() {
1492        let mut nc = NcFile::new();
1493        nc.add_dimension("x", Some(3)).expect("dim failed");
1494        nc.add_dimension("y", Some(2)).expect("dim failed");
1495
1496        nc.add_variable("temp", NcDataType::Float, &["x", "y"])
1497            .expect("var failed");
1498        nc.add_variable("pressure", NcDataType::Double, &["x"])
1499            .expect("var failed");
1500
1501        nc.set_variable_f32("temp", &[20.0, 21.0, 22.0, 23.0, 24.0, 25.0])
1502            .expect("set failed");
1503        nc.set_variable_f64("pressure", &[1013.0, 1012.5, 1012.0])
1504            .expect("set failed");
1505
1506        let mut buf = Vec::new();
1507        nc.write_to(&mut buf).expect("write failed");
1508
1509        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1510        assert_eq!(loaded.variable_names().len(), 2);
1511
1512        let temp = loaded.variable("temp").expect("var not found");
1513        let temp_data = temp
1514            .as_f32(loaded.dimensions(), loaded.num_records())
1515            .expect("as_f32 failed");
1516        assert_eq!(temp_data.len(), 6);
1517        assert!((temp_data[0] - 20.0).abs() < 1e-4);
1518
1519        let pressure = loaded.variable("pressure").expect("var not found");
1520        let p_data = pressure
1521            .as_f64(loaded.dimensions(), loaded.num_records())
1522            .expect("as_f64 failed");
1523        assert_eq!(p_data.len(), 3);
1524        assert!((p_data[0] - 1013.0).abs() < 1e-10);
1525    }
1526
1527    #[test]
1528    fn test_byte_data() {
1529        let mut nc = NcFile::new();
1530        nc.add_dimension("n", Some(4)).expect("dim failed");
1531        nc.add_variable("flags", NcDataType::Byte, &["n"])
1532            .expect("var failed");
1533        nc.set_variable_f64("flags", &[0.0, 1.0, 2.0, -1.0])
1534            .expect("set failed");
1535
1536        let mut buf = Vec::new();
1537        nc.write_to(&mut buf).expect("write failed");
1538
1539        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1540        let var = loaded.variable("flags").expect("var not found");
1541        let data = var
1542            .as_f64(loaded.dimensions(), loaded.num_records())
1543            .expect("as_f64 failed");
1544        assert_eq!(data[0], 0.0);
1545        assert_eq!(data[1], 1.0);
1546        assert_eq!(data[2], 2.0);
1547        assert_eq!(data[3], -1.0);
1548    }
1549
1550    #[test]
1551    fn test_short_data() {
1552        let mut nc = NcFile::new();
1553        nc.add_dimension("n", Some(3)).expect("dim failed");
1554        nc.add_variable("vals", NcDataType::Short, &["n"])
1555            .expect("var failed");
1556        nc.set_variable_f64("vals", &[100.0, -200.0, 300.0])
1557            .expect("set failed");
1558
1559        let mut buf = Vec::new();
1560        nc.write_to(&mut buf).expect("write failed");
1561
1562        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1563        let var = loaded.variable("vals").expect("var not found");
1564        let data = var
1565            .as_f64(loaded.dimensions(), loaded.num_records())
1566            .expect("as_f64 failed");
1567        assert_eq!(data[0], 100.0);
1568        assert_eq!(data[1], -200.0);
1569        assert_eq!(data[2], 300.0);
1570    }
1571
1572    #[test]
1573    fn test_file_roundtrip() {
1574        let dir = std::env::temp_dir().join("scirs2_nc_lite_test");
1575        let _ = std::fs::create_dir_all(&dir);
1576        let path = dir.join("test.nc");
1577
1578        let mut nc = NcFile::new();
1579        nc.add_dimension("x", Some(5)).expect("dim failed");
1580        nc.add_variable("data", NcDataType::Double, &["x"])
1581            .expect("var failed");
1582        nc.set_variable_f64("data", &[1.0, 2.0, 3.0, 4.0, 5.0])
1583            .expect("set failed");
1584        nc.add_global_attribute("title", NcValue::Text("Test File".to_string()))
1585            .expect("attr failed");
1586
1587        nc.write_to_file(&path).expect("write failed");
1588
1589        let loaded = NcFile::read_from_file(&path).expect("read failed");
1590        assert_eq!(loaded.dimensions().len(), 1);
1591        assert_eq!(loaded.variable_names(), vec!["data"]);
1592
1593        let var = loaded.variable("data").expect("var not found");
1594        let data = var
1595            .as_f64(loaded.dimensions(), loaded.num_records())
1596            .expect("as_f64 failed");
1597        assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
1598
1599        let _ = std::fs::remove_dir_all(&dir);
1600    }
1601
1602    #[test]
1603    fn test_empty_file_roundtrip() {
1604        let nc = NcFile::new();
1605        let mut buf = Vec::new();
1606        nc.write_to(&mut buf).expect("write failed");
1607
1608        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1609        assert!(loaded.dimensions().is_empty());
1610        assert!(loaded.variable_names().is_empty());
1611        assert!(loaded.global_attributes().is_empty());
1612    }
1613
1614    #[test]
1615    fn test_short_attribute_values() {
1616        let mut nc = NcFile::new();
1617        nc.add_global_attribute("short_vals", NcValue::Shorts(vec![10, 20, 30]))
1618            .expect("attr failed");
1619        nc.add_global_attribute("byte_vals", NcValue::Bytes(vec![1, 2, -1]))
1620            .expect("attr failed");
1621
1622        let mut buf = Vec::new();
1623        nc.write_to(&mut buf).expect("write failed");
1624
1625        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1626        let attrs = loaded.global_attributes();
1627        assert_eq!(attrs.len(), 2);
1628
1629        if let NcValue::Shorts(ref v) = attrs[0].1 {
1630            assert_eq!(v, &[10, 20, 30]);
1631        } else {
1632            panic!("Expected shorts");
1633        }
1634
1635        if let NcValue::Bytes(ref v) = attrs[1].1 {
1636            assert_eq!(v, &[1, 2, -1]);
1637        } else {
1638            panic!("Expected bytes");
1639        }
1640    }
1641
1642    #[test]
1643    fn test_float_attribute_values() {
1644        let mut nc = NcFile::new();
1645        nc.add_global_attribute("scale", NcValue::Floats(vec![0.5, 1.0]))
1646            .expect("attr failed");
1647
1648        let mut buf = Vec::new();
1649        nc.write_to(&mut buf).expect("write failed");
1650
1651        let loaded = NcFile::read_from(&mut std::io::Cursor::new(&buf)).expect("read failed");
1652        if let NcValue::Floats(ref v) = loaded.global_attributes()[0].1 {
1653            assert!((v[0] - 0.5).abs() < 1e-6);
1654            assert!((v[1] - 1.0).abs() < 1e-6);
1655        } else {
1656            panic!("Expected floats");
1657        }
1658    }
1659
1660    #[test]
1661    fn test_bad_magic_rejected() {
1662        let bad_data = b"NOTCDF\x00\x00";
1663        let result = NcFile::read_from(&mut std::io::Cursor::new(bad_data.as_ref()));
1664        assert!(result.is_err());
1665    }
1666
1667    #[test]
1668    fn test_replace_global_attribute() {
1669        let mut nc = NcFile::new();
1670        nc.add_global_attribute("title", NcValue::Text("Old".to_string()))
1671            .expect("attr failed");
1672        nc.add_global_attribute("title", NcValue::Text("New".to_string()))
1673            .expect("replace failed");
1674
1675        assert_eq!(nc.global_attributes().len(), 1);
1676        if let NcValue::Text(ref s) = nc.global_attributes()[0].1 {
1677            assert_eq!(s, "New");
1678        }
1679    }
1680
1681    #[test]
1682    fn test_replace_variable_attribute() {
1683        let mut nc = NcFile::new();
1684        nc.add_dimension("x", Some(1)).expect("dim failed");
1685        nc.add_variable("v", NcDataType::Float, &["x"])
1686            .expect("var failed");
1687        nc.add_variable_attribute("v", "units", NcValue::Text("m".to_string()))
1688            .expect("attr failed");
1689        nc.add_variable_attribute("v", "units", NcValue::Text("km".to_string()))
1690            .expect("replace failed");
1691
1692        let var = nc.variable("v").expect("var not found");
1693        assert_eq!(var.attributes.len(), 1);
1694        if let NcValue::Text(ref s) = var.attributes[0].1 {
1695            assert_eq!(s, "km");
1696        }
1697    }
1698}