1use std::collections::HashMap;
22use std::path::Path;
23
24use crate::attribute::{Attribute, AttributeValue, Attributes};
25use crate::dimension::{Dimension, Dimensions};
26use crate::error::{NetCdfError, Result};
27use crate::metadata::{CfMetadata, NetCdfMetadata, NetCdfVersion};
28use crate::variable::{DataType, Variable, Variables};
29
30use oxinetcdf::{ByteOrder, Dtype, NcAttribute, NcFile, NcGroup};
31
32#[cfg(feature = "netcdf3")]
33use std::cell::RefCell;
34
35pub struct NetCdfReader {
39 metadata: NetCdfMetadata,
40 nc4: Option<Nc4Backend>,
43 #[cfg(feature = "netcdf3")]
44 file_nc3: Option<RefCell<netcdf3::FileReader>>,
45}
46
47struct Nc4Backend {
52 file: NcFile,
53 var_paths: HashMap<String, String>,
54}
55
56impl std::fmt::Debug for NetCdfReader {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("NetCdfReader")
59 .field("metadata", &self.metadata)
60 .finish_non_exhaustive()
61 }
62}
63
64impl NetCdfReader {
65 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
80 let path = path.as_ref();
81
82 #[cfg(feature = "netcdf3")]
85 {
86 if let Ok(file) = netcdf3::FileReader::open(path) {
87 return Self::from_netcdf3(file);
88 }
89 }
90
91 match NcFile::open(path) {
93 Ok(file) => Self::from_oxinetcdf(file),
94 Err(e) => Err(NetCdfError::InvalidFormat(format!(
95 "file is not a readable NetCDF-3 or NetCDF-4/HDF5 file: {e}"
96 ))),
97 }
98 }
99
100 fn from_oxinetcdf(file: NcFile) -> Result<Self> {
102 let root = file.root_group().map_err(map_nc_err)?;
103 let mut metadata = build_metadata_from_group(&root)?;
104 metadata.parse_cf_metadata();
105 let var_paths = root
106 .variables
107 .iter()
108 .map(|v| (v.name.clone(), v.h5_path.clone()))
109 .collect();
110
111 Ok(Self {
112 metadata,
113 nc4: Some(Nc4Backend { file, var_paths }),
114 #[cfg(feature = "netcdf3")]
115 file_nc3: None,
116 })
117 }
118
119 #[cfg(feature = "netcdf3")]
125 pub fn from_netcdf3(file: netcdf3::FileReader) -> Result<Self> {
126 let metadata = Self::read_metadata_nc3(&file)?;
127 Ok(Self {
128 metadata,
129 nc4: None,
130 file_nc3: Some(RefCell::new(file)),
131 })
132 }
133
134 #[must_use]
136 pub const fn metadata(&self) -> &NetCdfMetadata {
137 &self.metadata
138 }
139
140 #[must_use]
142 pub fn version(&self) -> NetCdfVersion {
143 self.metadata.version()
144 }
145
146 #[must_use]
148 pub fn dimensions(&self) -> &Dimensions {
149 self.metadata.dimensions()
150 }
151
152 #[must_use]
154 pub fn variables(&self) -> &Variables {
155 self.metadata.variables()
156 }
157
158 #[must_use]
160 pub fn global_attributes(&self) -> &Attributes {
161 self.metadata.global_attributes()
162 }
163
164 #[must_use]
166 pub fn cf_metadata(&self) -> Option<&CfMetadata> {
167 self.metadata.cf_metadata()
168 }
169
170 #[cfg(feature = "netcdf3")]
172 fn read_metadata_nc3(file: &netcdf3::FileReader) -> Result<NetCdfMetadata> {
173 use crate::nc3_compat;
174
175 let mut metadata = NetCdfMetadata::new_classic();
176 let dataset = file.data_set();
177
178 let dimensions = nc3_compat::read_dimensions(dataset)?;
180 for dimension in dimensions {
181 metadata.dimensions_mut().add(dimension)?;
182 }
183
184 for attr_name in dataset.get_global_attr_names() {
186 if let Some(attr) = nc3_compat::read_global_attribute(dataset, &attr_name)? {
187 metadata.global_attributes_mut().add(attr)?;
188 }
189 }
190
191 for var_name in dataset.get_var_names() {
193 let var = nc3_compat::read_variable(dataset, &var_name)?;
194 metadata.variables_mut().add(var)?;
195 }
196
197 metadata.parse_cf_metadata();
199
200 Ok(metadata)
201 }
202
203 #[cfg(feature = "netcdf3")]
205 fn convert_datatype_nc3(nc3_type: netcdf3::DataType) -> Result<DataType> {
206 use netcdf3::DataType as Nc3Type;
207
208 match nc3_type {
209 Nc3Type::I8 => Ok(DataType::I8),
210 Nc3Type::I16 => Ok(DataType::I16),
211 Nc3Type::I32 => Ok(DataType::I32),
212 Nc3Type::F32 => Ok(DataType::F32),
213 Nc3Type::F64 => Ok(DataType::F64),
214 Nc3Type::U8 => Ok(DataType::Char), }
216 }
217
218 pub fn read_f32(&self, var_name: &str) -> Result<Vec<f32>> {
224 if let Some(nc4) = &self.nc4 {
225 return nc4.read_f32(var_name);
226 }
227
228 #[cfg(feature = "netcdf3")]
229 if let Some(ref file_cell) = self.file_nc3 {
230 return Self::read_f32_nc3(&mut file_cell.borrow_mut(), var_name);
231 }
232
233 Err(NetCdfError::FeatureNotEnabled {
234 feature: "netcdf reader".to_string(),
235 message: "No reader backend available".to_string(),
236 })
237 }
238
239 pub fn read_f64(&self, var_name: &str) -> Result<Vec<f64>> {
245 if let Some(nc4) = &self.nc4 {
246 return nc4.read_f64(var_name);
247 }
248
249 #[cfg(feature = "netcdf3")]
250 if let Some(ref file_cell) = self.file_nc3 {
251 return Self::read_f64_nc3(&mut file_cell.borrow_mut(), var_name);
252 }
253
254 Err(NetCdfError::FeatureNotEnabled {
255 feature: "netcdf reader".to_string(),
256 message: "No reader backend available".to_string(),
257 })
258 }
259
260 pub fn read_i32(&self, var_name: &str) -> Result<Vec<i32>> {
266 if let Some(nc4) = &self.nc4 {
267 return nc4.read_i32(var_name);
268 }
269
270 #[cfg(feature = "netcdf3")]
271 if let Some(ref file_cell) = self.file_nc3 {
272 return Self::read_i32_nc3(&mut file_cell.borrow_mut(), var_name);
273 }
274
275 Err(NetCdfError::FeatureNotEnabled {
276 feature: "netcdf reader".to_string(),
277 message: "No reader backend available".to_string(),
278 })
279 }
280
281 #[cfg(feature = "netcdf3")]
283 fn read_f32_nc3(file: &mut netcdf3::FileReader, var_name: &str) -> Result<Vec<f32>> {
284 let dataset = file.data_set();
285 let var_info = dataset
286 .get_var(var_name)
287 .ok_or_else(|| NetCdfError::VariableNotFound {
288 name: var_name.to_string(),
289 })?;
290
291 use netcdf3::DataType as Nc3Type;
292 let data_type = var_info.data_type();
293
294 if data_type != Nc3Type::F32 {
295 return Err(NetCdfError::DataTypeMismatch {
296 expected: "F32".to_string(),
297 found: format!("{:?}", data_type),
298 });
299 }
300
301 let data = file.read_var_f32(var_name)?;
302 Ok(data)
303 }
304
305 #[cfg(feature = "netcdf3")]
307 fn read_f64_nc3(file: &mut netcdf3::FileReader, var_name: &str) -> Result<Vec<f64>> {
308 let dataset = file.data_set();
309 let var_info = dataset
310 .get_var(var_name)
311 .ok_or_else(|| NetCdfError::VariableNotFound {
312 name: var_name.to_string(),
313 })?;
314
315 use netcdf3::DataType as Nc3Type;
316 let data_type = var_info.data_type();
317
318 if data_type != Nc3Type::F64 {
319 return Err(NetCdfError::DataTypeMismatch {
320 expected: "F64".to_string(),
321 found: format!("{:?}", data_type),
322 });
323 }
324
325 let data = file.read_var_f64(var_name)?;
326 Ok(data)
327 }
328
329 #[cfg(feature = "netcdf3")]
331 fn read_i32_nc3(file: &mut netcdf3::FileReader, var_name: &str) -> Result<Vec<i32>> {
332 let dataset = file.data_set();
333 let var_info = dataset
334 .get_var(var_name)
335 .ok_or_else(|| NetCdfError::VariableNotFound {
336 name: var_name.to_string(),
337 })?;
338
339 use netcdf3::DataType as Nc3Type;
340 let data_type = var_info.data_type();
341
342 if data_type != Nc3Type::I32 {
343 return Err(NetCdfError::DataTypeMismatch {
344 expected: "I32".to_string(),
345 found: format!("{:?}", data_type),
346 });
347 }
348
349 let data = file.read_var_i32(var_name)?;
350 Ok(data)
351 }
352}
353
354impl Nc4Backend {
355 fn path(&self, var_name: &str) -> Result<&str> {
357 self.var_paths
358 .get(var_name)
359 .map(String::as_str)
360 .ok_or_else(|| NetCdfError::VariableNotFound {
361 name: var_name.to_string(),
362 })
363 }
364
365 fn read_f32(&self, var_name: &str) -> Result<Vec<f32>> {
367 let path = self.path(var_name)?;
368 let ds = self
369 .file
370 .h5()
371 .dataset(path)
372 .map_err(|e| NetCdfError::Io(format!("read '{var_name}': {e}")))?;
373 ds.as_f32().map_err(|e| NetCdfError::DataTypeMismatch {
374 expected: "f32".to_string(),
375 found: format!("{e}"),
376 })
377 }
378
379 fn read_f64(&self, var_name: &str) -> Result<Vec<f64>> {
381 let path = self.path(var_name)?;
382 let ds = self
383 .file
384 .h5()
385 .dataset(path)
386 .map_err(|e| NetCdfError::Io(format!("read '{var_name}': {e}")))?;
387 ds.as_f64().map_err(|e| NetCdfError::DataTypeMismatch {
388 expected: "f64".to_string(),
389 found: format!("{e}"),
390 })
391 }
392
393 fn read_i32(&self, var_name: &str) -> Result<Vec<i32>> {
395 let path = self.path(var_name)?;
396 let ds = self
397 .file
398 .h5()
399 .dataset(path)
400 .map_err(|e| NetCdfError::Io(format!("read '{var_name}': {e}")))?;
401 ds.as_i32().map_err(|e| NetCdfError::DataTypeMismatch {
402 expected: "i32".to_string(),
403 found: format!("{e}"),
404 })
405 }
406}
407
408fn map_nc_err(e: oxinetcdf::NcError) -> NetCdfError {
414 match e {
415 oxinetcdf::NcError::VariableNotFound(name) => NetCdfError::VariableNotFound { name },
416 oxinetcdf::NcError::Unsupported(msg) => NetCdfError::Other(format!("unsupported: {msg}")),
417 other => NetCdfError::InvalidFormat(other.to_string()),
418 }
419}
420
421fn build_metadata_from_group(root: &NcGroup) -> Result<NetCdfMetadata> {
423 let version = if root.attrs.iter().any(|a| a.name == "_nc3_strict") {
425 NetCdfVersion::NetCdf4Classic
426 } else {
427 NetCdfVersion::NetCdf4
428 };
429 let mut metadata = NetCdfMetadata::new(version);
430
431 for d in &root.dimensions {
433 add_dimension(&mut metadata, &d.name, d.len, d.is_unlimited)?;
434 }
435
436 for v in &root.variables {
438 let Some(data_type) = dtype_to_datatype(&v.dtype) else {
439 continue;
442 };
443
444 for axis in &v.dims {
447 if !metadata.dimensions().contains(&axis.name) {
448 add_dimension(&mut metadata, &axis.name, axis.len, axis.is_unlimited)?;
449 }
450 }
451
452 let dim_names: Vec<String> = v.dims.iter().map(|a| a.name.clone()).collect();
453 let mut variable = Variable::new(&v.name, data_type, dim_names)?;
454 variable.set_coordinate(v.is_coordinate);
455 for attr in &v.attrs {
456 if let Some(value) = nc_attr_to_value(attr)
457 && let Ok(a) = Attribute::new(attr.name.clone(), value)
458 {
459 variable.attributes_mut().set(a);
460 }
461 }
462 let _ = metadata.variables_mut().add(variable);
464 }
465
466 for attr in &root.attrs {
468 if let Some(value) = nc_attr_to_value(attr)
469 && let Ok(a) = Attribute::new(attr.name.clone(), value)
470 {
471 let _ = metadata.global_attributes_mut().add(a);
472 }
473 }
474
475 Ok(metadata)
476}
477
478fn add_dimension(
480 metadata: &mut NetCdfMetadata,
481 name: &str,
482 len: u64,
483 unlimited: bool,
484) -> Result<()> {
485 if metadata.dimensions().contains(name) {
486 return Ok(());
487 }
488 let len = usize::try_from(len).map_err(|_| NetCdfError::InvalidShape {
489 message: format!("dimension '{name}' length {len} exceeds usize"),
490 })?;
491 let dim = if unlimited {
492 Dimension::new_unlimited(name, len)?
493 } else {
494 Dimension::new(name, len)?
495 };
496 let _ = metadata.dimensions_mut().add(dim);
497 Ok(())
498}
499
500fn dtype_to_datatype(dtype: &Dtype) -> Option<DataType> {
505 Some(match dtype {
506 Dtype::Float { size: 4, .. } => DataType::F32,
507 Dtype::Float { size: 8, .. } => DataType::F64,
508 Dtype::Int {
509 size: 1,
510 signed: true,
511 ..
512 } => DataType::I8,
513 Dtype::Int {
514 size: 1,
515 signed: false,
516 ..
517 } => DataType::U8,
518 Dtype::Int {
519 size: 2,
520 signed: true,
521 ..
522 } => DataType::I16,
523 Dtype::Int {
524 size: 2,
525 signed: false,
526 ..
527 } => DataType::U16,
528 Dtype::Int {
529 size: 4,
530 signed: true,
531 ..
532 } => DataType::I32,
533 Dtype::Int {
534 size: 4,
535 signed: false,
536 ..
537 } => DataType::U32,
538 Dtype::Int {
539 size: 8,
540 signed: true,
541 ..
542 } => DataType::I64,
543 Dtype::Int {
544 size: 8,
545 signed: false,
546 ..
547 } => DataType::U64,
548 Dtype::String {
549 fixed_len: Some(1), ..
550 } => DataType::Char,
551 Dtype::String { .. } => DataType::String,
552 _ => return None,
553 })
554}
555
556fn nc_attr_to_value(attr: &NcAttribute) -> Option<AttributeValue> {
560 let raw = attr.raw();
561 let value = match &raw.dtype {
562 Dtype::String { .. } => AttributeValue::Text(attr.as_text().ok()?),
563 Dtype::Float { size: 4, order } => AttributeValue::F32(decode_f32(&raw.data, order)),
564 Dtype::Float { size: 8, order } => AttributeValue::F64(decode_f64(&raw.data, order)),
565 Dtype::Int {
566 size: 1,
567 signed: true,
568 ..
569 } => AttributeValue::I8(raw.data.iter().map(|&b| b as i8).collect()),
570 Dtype::Int {
571 size: 1,
572 signed: false,
573 ..
574 } => AttributeValue::U8(raw.data.clone()),
575 Dtype::Int {
576 size: 2,
577 signed: true,
578 order,
579 } => AttributeValue::I16(decode_chunks(
580 &raw.data,
581 order,
582 i16::from_le_bytes,
583 i16::from_be_bytes,
584 )),
585 Dtype::Int {
586 size: 2,
587 signed: false,
588 order,
589 } => AttributeValue::U16(decode_chunks(
590 &raw.data,
591 order,
592 u16::from_le_bytes,
593 u16::from_be_bytes,
594 )),
595 Dtype::Int {
596 size: 4,
597 signed: true,
598 order,
599 } => AttributeValue::I32(decode_chunks(
600 &raw.data,
601 order,
602 i32::from_le_bytes,
603 i32::from_be_bytes,
604 )),
605 Dtype::Int {
606 size: 4,
607 signed: false,
608 order,
609 } => AttributeValue::U32(decode_chunks(
610 &raw.data,
611 order,
612 u32::from_le_bytes,
613 u32::from_be_bytes,
614 )),
615 Dtype::Int {
616 size: 8,
617 signed: true,
618 order,
619 } => AttributeValue::I64(decode_chunks(
620 &raw.data,
621 order,
622 i64::from_le_bytes,
623 i64::from_be_bytes,
624 )),
625 Dtype::Int {
626 size: 8,
627 signed: false,
628 order,
629 } => AttributeValue::U64(decode_chunks(
630 &raw.data,
631 order,
632 u64::from_le_bytes,
633 u64::from_be_bytes,
634 )),
635 _ => return None,
636 };
637 Some(value)
638}
639
640fn decode_chunks<T, const N: usize>(
643 data: &[u8],
644 order: &ByteOrder,
645 from_le: fn([u8; N]) -> T,
646 from_be: fn([u8; N]) -> T,
647) -> Vec<T> {
648 data.chunks_exact(N)
649 .filter_map(|c| <[u8; N]>::try_from(c).ok())
650 .map(|arr| match order {
651 ByteOrder::Little => from_le(arr),
652 ByteOrder::Big => from_be(arr),
653 })
654 .collect()
655}
656
657fn decode_f32(data: &[u8], order: &ByteOrder) -> Vec<f32> {
659 decode_chunks(data, order, f32::from_le_bytes, f32::from_be_bytes)
660}
661
662fn decode_f64(data: &[u8], order: &ByteOrder) -> Vec<f64> {
664 decode_chunks(data, order, f64::from_le_bytes, f64::from_be_bytes)
665}
666
667#[cfg(test)]
668mod tests {
669 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
670 use super::*;
671 use oxinetcdf::{NcFileWriter, NcType, VarOrGroup};
672
673 fn write_real_nc4_fixture(tag: &str) -> std::path::PathBuf {
677 let path = std::env::temp_dir().join(format!(
678 "oxigdal_nc4_reader_{}_{}.nc",
679 tag,
680 std::process::id()
681 ));
682
683 let mut nc = NcFileWriter::new();
684 let lat = nc.def_dim("lat", 3).expect("def lat");
685 let lon = nc.def_dim("lon", 4).expect("def lon");
686 let temp = nc
687 .def_var("temp", &[lat, lon], NcType::Float64)
688 .expect("def temp");
689 let data: Vec<f64> = (0..12).map(|i| i as f64 * 0.5).collect();
690 nc.put_var_f64(temp, &data).expect("put temp");
691 nc.put_att_str(VarOrGroup::Var(temp), "units", "kelvin")
692 .expect("units");
693 nc.put_att_str(VarOrGroup::Var(temp), "standard_name", "air_temperature")
694 .expect("standard_name");
695 nc.put_att_str(VarOrGroup::Root, "Conventions", "CF-1.8")
696 .expect("conventions");
697 nc.put_att_str(VarOrGroup::Root, "title", "Reader Fixture")
698 .expect("title");
699 nc.close(&path).expect("close fixture");
700 path
701 }
702
703 #[test]
704 fn test_open_real_netcdf4_reads_dimensions_and_variables() {
705 let path = write_real_nc4_fixture("dims");
706 let reader = NetCdfReader::open(&path).expect("open real NetCDF-4");
707
708 assert!(reader.version().is_netcdf4());
709
710 assert_eq!(reader.dimensions().len(), 2);
712 assert_eq!(reader.dimensions().get("lat").expect("lat dim").len(), 3);
713 assert_eq!(reader.dimensions().get("lon").expect("lon dim").len(), 4);
714
715 let temp = reader.variables().get("temp").expect("temp var");
717 assert_eq!(temp.data_type(), DataType::F64);
718 assert_eq!(temp.dimension_names(), &["lat", "lon"]);
719
720 let _ = std::fs::remove_file(&path);
721 }
722
723 #[test]
724 fn test_open_real_netcdf4_reads_variable_data() {
725 let path = write_real_nc4_fixture("data");
726 let reader = NetCdfReader::open(&path).expect("open real NetCDF-4");
727
728 let data = reader.read_f64("temp").expect("read temp");
729 assert_eq!(data.len(), 12);
730 for (i, &v) in data.iter().enumerate() {
731 assert!((v - i as f64 * 0.5).abs() < 1e-12);
732 }
733
734 let _ = std::fs::remove_file(&path);
735 }
736
737 #[test]
738 fn test_open_real_netcdf4_reads_attributes_and_cf() {
739 let path = write_real_nc4_fixture("attrs");
740 let reader = NetCdfReader::open(&path).expect("open real NetCDF-4");
741
742 let cf = reader.cf_metadata().expect("cf metadata");
744 assert!(cf.is_cf_compliant());
745 assert_eq!(cf.conventions.as_deref(), Some("CF-1.8"));
746 assert_eq!(cf.title.as_deref(), Some("Reader Fixture"));
747
748 let temp = reader.variables().get("temp").expect("temp var");
750 let units = temp.attributes().get("units").expect("units attr");
751 assert_eq!(units.value().as_text().expect("text"), "kelvin");
752 assert!(temp.attributes().get("standard_name").is_some());
753
754 assert!(temp.attributes().get("DIMENSION_LIST").is_none());
756 assert!(temp.attributes().get("CLASS").is_none());
757
758 let _ = std::fs::remove_file(&path);
759 }
760
761 #[test]
762 fn test_open_missing_variable_errors() {
763 let path = write_real_nc4_fixture("missing");
764 let reader = NetCdfReader::open(&path).expect("open real NetCDF-4");
765
766 let result = reader.read_f64("does_not_exist");
767 assert!(matches!(result, Err(NetCdfError::VariableNotFound { .. })));
768
769 let _ = std::fs::remove_file(&path);
770 }
771
772 #[test]
773 fn test_open_garbage_file_errors() {
774 let temp_dir = std::env::temp_dir();
775 let temp_file = temp_dir.join(format!("oxigdal_reader_garbage_{}.bin", std::process::id()));
776 std::fs::write(&temp_file, b"this is not any netcdf file").expect("write temp");
777
778 let result = NetCdfReader::open(&temp_file);
780 assert!(result.is_err());
781 assert!(!matches!(result, Err(NetCdfError::NetCdf4NotAvailable)));
782
783 let _ = std::fs::remove_file(&temp_file);
784 }
785
786 #[test]
787 fn test_data_type_conversion() {
788 #[cfg(feature = "netcdf3")]
789 {
790 use netcdf3::DataType as Nc3Type;
791 assert_eq!(
792 NetCdfReader::convert_datatype_nc3(Nc3Type::F32).expect("convert F32"),
793 DataType::F32
794 );
795 assert_eq!(
796 NetCdfReader::convert_datatype_nc3(Nc3Type::F64).expect("convert F64"),
797 DataType::F64
798 );
799 assert_eq!(
800 NetCdfReader::convert_datatype_nc3(Nc3Type::I32).expect("convert I32"),
801 DataType::I32
802 );
803 }
804 }
805}