1use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
53use std::collections::HashMap;
54use std::io::{Read, Seek, SeekFrom, Write};
55
56use crate::error::{IoError, Result};
57
58const NC_MAGIC: &[u8; 4] = b"CDF\x01";
64
65const NC_DIMENSION: u32 = 0x0000_000A;
67const NC_VARIABLE: u32 = 0x0000_000B;
69const NC_ATTRIBUTE: u32 = 0x0000_000C;
71
72const NC_ABSENT: u32 = 0x0000_0000;
74
75const 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum NcDataType {
90 Byte,
92 Char,
94 Short,
96 Int,
98 Float,
100 Double,
102}
103
104impl NcDataType {
105 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#[derive(Debug, Clone, PartialEq)]
148pub enum NcValue {
149 Bytes(Vec<i8>),
151 Text(String),
153 Shorts(Vec<i16>),
155 Ints(Vec<i32>),
157 Floats(Vec<f32>),
159 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#[derive(Debug, Clone)]
193pub struct NcDimension {
194 pub name: String,
196 pub length: Option<usize>,
198 pub is_unlimited: bool,
200}
201
202#[derive(Debug, Clone)]
208pub struct NcVariable {
209 pub name: String,
211 pub data_type: NcDataType,
213 pub dim_indices: Vec<usize>,
215 pub attributes: Vec<(String, NcValue)>,
217 pub(crate) data: Vec<u8>,
219}
220
221impl NcVariable {
222 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 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 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 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 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 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#[derive(Debug, Clone)]
315pub struct NcFile {
316 dims: Vec<NcDimension>,
318 global_attrs: Vec<(String, NcValue)>,
320 vars: Vec<NcVariable>,
322 num_records: usize,
324}
325
326impl NcFile {
327 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 pub fn add_dimension(&mut self, name: &str, length: Option<usize>) -> Result<()> {
344 let is_unlimited = length.is_none();
345
346 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 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 pub fn add_variable(
374 &mut self,
375 name: &str,
376 data_type: NcDataType,
377 dim_names: &[&str],
378 ) -> Result<()> {
379 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 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 pub fn add_global_attribute(&mut self, name: &str, value: NcValue) -> Result<()> {
411 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 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 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 self.update_num_records();
476 Ok(())
477 }
478
479 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 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 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 pub fn dimensions(&self) -> &[NcDimension] {
513 &self.dims
514 }
515
516 pub fn global_attributes(&self) -> &[(String, NcValue)] {
518 &self.global_attrs
519 }
520
521 pub fn variable_names(&self) -> Vec<&str> {
523 self.vars.iter().map(|v| v.name.as_str()).collect()
524 }
525
526 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 pub fn num_records(&self) -> usize {
536 self.num_records
537 }
538
539 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 pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
571 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 self.write_dim_list(writer)?;
581
582 self.write_attr_list(writer, &self.global_attrs)?;
584
585 self.write_var_list(writer)?;
587
588 Ok(())
589 }
590
591 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 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 let mut header_total = 0usize;
679 for var in &self.vars {
680 header_total += 4 + pad_to_4(var.name.len());
682 header_total += 4 + var.dim_indices.len() * 4;
684 header_total += self.attr_list_size(&var.attributes);
686 header_total += 4 + 4 + 4;
688 }
689
690 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 for (i, var) in self.vars.iter().enumerate() {
703 write_name(w, &var.name)?;
704
705 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 self.write_attr_list(w, &var.attributes)?;
715
716 w.write_u32::<BigEndian>(var.data_type.to_nc_type())
718 .map_err(|e| IoError::FileError(e.to_string()))?;
719
720 w.write_u32::<BigEndian>(data_sizes[i] as u32)
722 .map_err(|e| IoError::FileError(e.to_string()))?;
723
724 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 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 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; }
752 let mut size = 8; for dim in &self.dims {
754 size += 4 + pad_to_4(dim.name.len()); size += 4; }
757 size
758 }
759
760 fn attr_list_size(&self, attrs: &[(String, NcValue)]) -> usize {
761 if attrs.is_empty() {
762 return 8; }
764 let mut size = 8; for (name, value) in attrs {
766 size += 4 + pad_to_4(name.len()); size += 4 + 4; size += pad_to_4(value.element_count() * element_size_for_nc_type(value.nc_type()));
769 }
770 size
771 }
772
773 pub fn read_from<R: Read + Seek>(reader: &mut R) -> Result<Self> {
779 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 let num_records = reader
792 .read_u32::<BigEndian>()
793 .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
794
795 let dims = read_dim_list(reader)?;
797
798 let global_attrs = read_attr_list(reader)?;
800
801 let (mut vars, offsets, vsizes) = read_var_headers(reader)?;
803
804 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 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 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
848fn 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
867fn 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 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
884fn 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 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
942fn 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
957fn 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
996fn 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
1025fn 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 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 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 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
1130fn 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 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 let attributes = read_attr_list(r)?;
1171
1172 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 let vsize = r
1180 .read_u32::<BigEndian>()
1181 .map_err(|e| IoError::FormatError(e.to_string()))? as usize;
1182
1183 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(), });
1195 offsets.push(begin);
1196 vsizes.push(vsize);
1197 }
1198
1199 Ok((vars, offsets, vsizes))
1200}
1201
1202fn var_total_elements(var: &NcVariable, dims: &[NcDimension], num_records: usize) -> usize {
1204 if var.dim_indices.is_empty() {
1205 return 1; }
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#[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 let mut buf = Vec::new();
1302 nc.write_to(&mut buf).expect("write failed");
1303
1304 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 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 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}