1pub mod classic;
22pub mod error;
23pub mod masked;
24pub mod types;
25pub mod unpack;
26
27#[cfg(feature = "netcdf4")]
28pub mod nc4;
29
30#[cfg(feature = "cf")]
31pub mod cf;
32
33pub use error::{Error, Result};
34pub use types::*;
35
36use std::fs::File;
37use std::path::Path;
38
39use memmap2::Mmap;
40use ndarray::ArrayD;
41#[cfg(feature = "rayon")]
42use rayon::ThreadPool;
43
44#[cfg(feature = "netcdf4")]
50pub trait NcReadable: classic::data::NcReadType + hdf5_reader::H5Type {}
51#[cfg(feature = "netcdf4")]
52impl<T: classic::data::NcReadType + hdf5_reader::H5Type> NcReadable for T {}
53
54#[cfg(not(feature = "netcdf4"))]
55pub trait NcReadable: classic::data::NcReadType {}
56#[cfg(not(feature = "netcdf4"))]
57impl<T: classic::data::NcReadType> NcReadable for T {}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum NcFormat {
62 Classic,
64 Offset64,
66 Cdf5,
68 Nc4,
70 Nc4Classic,
72}
73
74pub struct NcFile {
76 format: NcFormat,
77 inner: NcFileInner,
78}
79
80enum NcFileInner {
81 Classic(classic::ClassicFile),
82 #[cfg(feature = "netcdf4")]
83 Nc4(nc4::Nc4File),
84}
85
86const HDF5_MAGIC: [u8; 8] = [0x89, b'H', b'D', b'F', 0x0D, 0x0A, 0x1A, 0x0A];
88
89fn detect_format(data: &[u8]) -> Result<NcFormat> {
91 if data.len() < 4 {
92 return Err(Error::InvalidMagic);
93 }
94
95 if data[0] == b'C' && data[1] == b'D' && data[2] == b'F' {
97 return match data[3] {
98 1 => Ok(NcFormat::Classic),
99 2 => Ok(NcFormat::Offset64),
100 5 => Ok(NcFormat::Cdf5),
101 v => Err(Error::UnsupportedVersion(v)),
102 };
103 }
104
105 if data.len() >= 8 && data[..8] == HDF5_MAGIC {
107 return Ok(NcFormat::Nc4);
108 }
109
110 Err(Error::InvalidMagic)
111}
112
113impl NcFile {
114 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
118 Self::open_with_options(path, NcOpenOptions::default())
119 }
120
121 pub fn from_bytes(data: &[u8]) -> Result<Self> {
125 Self::from_bytes_with_options(data, NcOpenOptions::default())
126 }
127
128 pub fn from_bytes_with_options(data: &[u8], options: NcOpenOptions) -> Result<Self> {
132 let format = detect_format(data)?;
133
134 match format {
135 NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
136 let classic = classic::ClassicFile::from_bytes(data, format)?;
137 Ok(NcFile {
138 format,
139 inner: NcFileInner::Classic(classic),
140 })
141 }
142 NcFormat::Nc4 | NcFormat::Nc4Classic => {
143 #[cfg(feature = "netcdf4")]
144 {
145 let nc4 = nc4::Nc4File::from_bytes_with_options(data, options)?;
146 let actual_format = if nc4.is_classic_model() {
147 NcFormat::Nc4Classic
148 } else {
149 NcFormat::Nc4
150 };
151 Ok(NcFile {
152 format: actual_format,
153 inner: NcFileInner::Nc4(nc4),
154 })
155 }
156 #[cfg(not(feature = "netcdf4"))]
157 {
158 let _ = options;
159 Err(Error::Nc4NotEnabled)
160 }
161 }
162 }
163 }
164
165 pub fn format(&self) -> NcFormat {
167 self.format
168 }
169
170 pub fn root_group(&self) -> &NcGroup {
176 match &self.inner {
177 NcFileInner::Classic(c) => c.root_group(),
178 #[cfg(feature = "netcdf4")]
179 NcFileInner::Nc4(n) => n.root_group(),
180 }
181 }
182
183 pub fn dimensions(&self) -> &[NcDimension] {
185 &self.root_group().dimensions
186 }
187
188 pub fn variables(&self) -> &[NcVariable] {
190 &self.root_group().variables
191 }
192
193 pub fn global_attributes(&self) -> &[NcAttribute] {
195 &self.root_group().attributes
196 }
197
198 pub fn group(&self, path: &str) -> Result<&NcGroup> {
200 self.root_group()
201 .group(path)
202 .ok_or_else(|| Error::GroupNotFound(path.to_string()))
203 }
204
205 pub fn variable(&self, name: &str) -> Result<&NcVariable> {
207 self.root_group()
208 .variable(name)
209 .ok_or_else(|| Error::VariableNotFound(name.to_string()))
210 }
211
212 pub fn dimension(&self, name: &str) -> Result<&NcDimension> {
214 self.root_group()
215 .dimension(name)
216 .ok_or_else(|| Error::DimensionNotFound(name.to_string()))
217 }
218
219 pub fn global_attribute(&self, name: &str) -> Result<&NcAttribute> {
221 self.root_group()
222 .attribute(name)
223 .ok_or_else(|| Error::AttributeNotFound(name.to_string()))
224 }
225
226 pub fn read_variable<T: NcReadable>(&self, name: &str) -> Result<ArrayD<T>> {
233 match &self.inner {
234 NcFileInner::Classic(c) => c.read_variable::<T>(name),
235 #[cfg(feature = "netcdf4")]
236 NcFileInner::Nc4(n) => Ok(n.read_variable::<T>(name)?),
237 }
238 }
239
240 #[cfg(feature = "rayon")]
244 pub fn read_variable_parallel<T: NcReadable>(&self, name: &str) -> Result<ArrayD<T>> {
245 match &self.inner {
246 NcFileInner::Classic(c) => c.read_variable::<T>(name),
247 #[cfg(feature = "netcdf4")]
248 NcFileInner::Nc4(n) => Ok(n.read_variable_parallel::<T>(name)?),
249 }
250 }
251
252 #[cfg(feature = "rayon")]
256 pub fn read_variable_in_pool<T: NcReadable>(
257 &self,
258 name: &str,
259 pool: &ThreadPool,
260 ) -> Result<ArrayD<T>> {
261 match &self.inner {
262 NcFileInner::Classic(c) => c.read_variable::<T>(name),
263 #[cfg(feature = "netcdf4")]
264 NcFileInner::Nc4(n) => Ok(n.read_variable_in_pool::<T>(name, pool)?),
265 }
266 }
267
268 pub fn as_classic(&self) -> Option<&classic::ClassicFile> {
272 match &self.inner {
273 NcFileInner::Classic(c) => Some(c),
274 #[cfg(feature = "netcdf4")]
275 NcFileInner::Nc4(_) => None,
276 }
277 }
278
279 pub fn read_variable_as_f64(&self, name: &str) -> Result<ArrayD<f64>> {
285 match &self.inner {
286 NcFileInner::Classic(c) => c.read_variable_as_f64(name),
287 #[cfg(feature = "netcdf4")]
288 NcFileInner::Nc4(n) => n.read_variable_as_f64(name),
289 }
290 }
291
292 pub fn read_variable_as_string(&self, name: &str) -> Result<String> {
297 match &self.inner {
298 NcFileInner::Classic(c) => c.read_variable_as_string(name),
299 #[cfg(feature = "netcdf4")]
300 NcFileInner::Nc4(n) => n.read_variable_as_string(name),
301 }
302 }
303
304 pub fn read_variable_as_strings(&self, name: &str) -> Result<Vec<String>> {
309 match &self.inner {
310 NcFileInner::Classic(c) => c.read_variable_as_strings(name),
311 #[cfg(feature = "netcdf4")]
312 NcFileInner::Nc4(n) => n.read_variable_as_strings(name),
313 }
314 }
315
316 pub fn read_variable_unpacked(&self, name: &str) -> Result<ArrayD<f64>> {
322 let var = self.variable(name)?;
323 let params = unpack::UnpackParams::from_variable(var);
324 let mut data = self.read_variable_as_f64(name)?;
325 if let Some(p) = params {
326 p.apply(&mut data);
327 }
328 Ok(data)
329 }
330
331 pub fn read_variable_masked(&self, name: &str) -> Result<ArrayD<f64>> {
335 let var = self.variable(name)?;
336 let params = masked::MaskParams::from_variable(var);
337 let mut data = self.read_variable_as_f64(name)?;
338 if let Some(p) = params {
339 p.apply(&mut data);
340 }
341 Ok(data)
342 }
343
344 pub fn read_variable_unpacked_masked(&self, name: &str) -> Result<ArrayD<f64>> {
349 let var = self.variable(name)?;
350 let mask_params = masked::MaskParams::from_variable(var);
351 let unpack_params = unpack::UnpackParams::from_variable(var);
352 let mut data = self.read_variable_as_f64(name)?;
353 if let Some(p) = mask_params {
354 p.apply(&mut data);
355 }
356 if let Some(p) = unpack_params {
357 p.apply(&mut data);
358 }
359 Ok(data)
360 }
361
362 pub fn read_variable_slice<T: NcReadable>(
366 &self,
367 name: &str,
368 selection: &NcSliceInfo,
369 ) -> Result<ArrayD<T>> {
370 match &self.inner {
371 NcFileInner::Classic(c) => c.read_variable_slice::<T>(name, selection),
372 #[cfg(feature = "netcdf4")]
373 NcFileInner::Nc4(n) => Ok(n.read_variable_slice::<T>(name, selection)?),
374 }
375 }
376
377 #[cfg(feature = "rayon")]
382 pub fn read_variable_slice_parallel<T: NcReadable>(
383 &self,
384 name: &str,
385 selection: &NcSliceInfo,
386 ) -> Result<ArrayD<T>> {
387 match &self.inner {
388 NcFileInner::Classic(c) => c.read_variable_slice::<T>(name, selection),
389 #[cfg(feature = "netcdf4")]
390 NcFileInner::Nc4(n) => Ok(n.read_variable_slice_parallel::<T>(name, selection)?),
391 }
392 }
393
394 pub fn read_variable_slice_as_f64(
396 &self,
397 name: &str,
398 selection: &NcSliceInfo,
399 ) -> Result<ArrayD<f64>> {
400 match &self.inner {
401 NcFileInner::Classic(c) => c.read_variable_slice_as_f64(name, selection),
402 #[cfg(feature = "netcdf4")]
403 NcFileInner::Nc4(n) => n.read_variable_slice_as_f64(name, selection),
404 }
405 }
406
407 pub fn read_variable_slice_unpacked(
409 &self,
410 name: &str,
411 selection: &NcSliceInfo,
412 ) -> Result<ArrayD<f64>> {
413 let var = self.variable(name)?;
414 let params = unpack::UnpackParams::from_variable(var);
415 let mut data = self.read_variable_slice_as_f64(name, selection)?;
416 if let Some(p) = params {
417 p.apply(&mut data);
418 }
419 Ok(data)
420 }
421
422 pub fn read_variable_slice_masked(
424 &self,
425 name: &str,
426 selection: &NcSliceInfo,
427 ) -> Result<ArrayD<f64>> {
428 let var = self.variable(name)?;
429 let params = masked::MaskParams::from_variable(var);
430 let mut data = self.read_variable_slice_as_f64(name, selection)?;
431 if let Some(p) = params {
432 p.apply(&mut data);
433 }
434 Ok(data)
435 }
436
437 pub fn read_variable_slice_unpacked_masked(
439 &self,
440 name: &str,
441 selection: &NcSliceInfo,
442 ) -> Result<ArrayD<f64>> {
443 let var = self.variable(name)?;
444 let mask_params = masked::MaskParams::from_variable(var);
445 let unpack_params = unpack::UnpackParams::from_variable(var);
446 let mut data = self.read_variable_slice_as_f64(name, selection)?;
447 if let Some(p) = mask_params {
448 p.apply(&mut data);
449 }
450 if let Some(p) = unpack_params {
451 p.apply(&mut data);
452 }
453 Ok(data)
454 }
455
456 pub fn iter_slices<T: NcReadable>(
464 &self,
465 name: &str,
466 dim: usize,
467 ) -> Result<NcSliceIterator<'_, T>> {
468 let var = self.variable(name)?;
469 let ndim = var.ndim();
470 if dim >= ndim {
471 return Err(Error::InvalidData(format!(
472 "dimension index {} out of range for {}-dimensional variable '{}'",
473 dim, ndim, name
474 )));
475 }
476 let dim_size = var.dimensions[dim].size;
477 Ok(NcSliceIterator {
478 file: self,
479 name: name.to_string(),
480 dim,
481 dim_size,
482 current: 0,
483 ndim,
484 _marker: std::marker::PhantomData,
485 })
486 }
487}
488
489pub struct NcOpenOptions {
491 pub chunk_cache_bytes: usize,
493 pub chunk_cache_slots: usize,
495 #[cfg(feature = "netcdf4")]
497 pub filter_registry: Option<hdf5_reader::FilterRegistry>,
498}
499
500impl Default for NcOpenOptions {
501 fn default() -> Self {
502 NcOpenOptions {
503 chunk_cache_bytes: 64 * 1024 * 1024,
504 chunk_cache_slots: 521,
505 #[cfg(feature = "netcdf4")]
506 filter_registry: None,
507 }
508 }
509}
510
511impl NcFile {
512 pub fn open_with_options(path: impl AsRef<Path>, options: NcOpenOptions) -> Result<Self> {
514 let path = path.as_ref();
515 let file = File::open(path)?;
516 let mmap = unsafe { Mmap::map(&file)? };
518 let format = detect_format(&mmap)?;
519
520 match format {
521 NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
522 let classic = classic::ClassicFile::from_mmap(mmap, format)?;
523 Ok(NcFile {
524 format,
525 inner: NcFileInner::Classic(classic),
526 })
527 }
528 NcFormat::Nc4 | NcFormat::Nc4Classic => {
529 #[cfg(feature = "netcdf4")]
530 {
531 let hdf5 = hdf5_reader::Hdf5File::from_mmap_with_options(
532 mmap,
533 hdf5_reader::OpenOptions {
534 chunk_cache_bytes: options.chunk_cache_bytes,
535 chunk_cache_slots: options.chunk_cache_slots,
536 filter_registry: options.filter_registry,
537 },
538 )?;
539 let root_group = nc4::groups::build_root_group(&hdf5)?;
540 let nc4 = nc4::Nc4File::from_hdf5(hdf5, root_group);
541 let actual_format = if nc4.is_classic_model() {
542 NcFormat::Nc4Classic
543 } else {
544 NcFormat::Nc4
545 };
546 Ok(NcFile {
547 format: actual_format,
548 inner: NcFileInner::Nc4(nc4),
549 })
550 }
551 #[cfg(not(feature = "netcdf4"))]
552 {
553 let _ = options;
554 Err(Error::Nc4NotEnabled)
555 }
556 }
557 }
558 }
559}
560
561pub struct NcSliceIterator<'f, T: NcReadable> {
563 file: &'f NcFile,
564 name: String,
565 dim: usize,
566 dim_size: u64,
567 current: u64,
568 ndim: usize,
569 _marker: std::marker::PhantomData<T>,
570}
571
572impl<'f, T: NcReadable> Iterator for NcSliceIterator<'f, T> {
573 type Item = Result<ArrayD<T>>;
574
575 fn next(&mut self) -> Option<Self::Item> {
576 if self.current >= self.dim_size {
577 return None;
578 }
579 let mut selections = Vec::with_capacity(self.ndim);
580 for d in 0..self.ndim {
581 if d == self.dim {
582 selections.push(NcSliceInfoElem::Index(self.current));
583 } else {
584 selections.push(NcSliceInfoElem::Slice {
585 start: 0,
586 end: u64::MAX,
587 step: 1,
588 });
589 }
590 }
591 let selection = NcSliceInfo { selections };
592 self.current += 1;
593 Some(self.file.read_variable_slice::<T>(&self.name, &selection))
594 }
595
596 fn size_hint(&self) -> (usize, Option<usize>) {
597 let remaining = (self.dim_size - self.current) as usize;
598 (remaining, Some(remaining))
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn test_detect_cdf1() {
608 let data = b"CDF\x01rest_of_file";
609 assert_eq!(detect_format(data).unwrap(), NcFormat::Classic);
610 }
611
612 #[test]
613 fn test_detect_cdf2() {
614 let data = b"CDF\x02rest_of_file";
615 assert_eq!(detect_format(data).unwrap(), NcFormat::Offset64);
616 }
617
618 #[test]
619 fn test_detect_cdf5() {
620 let data = b"CDF\x05rest_of_file";
621 assert_eq!(detect_format(data).unwrap(), NcFormat::Cdf5);
622 }
623
624 #[test]
625 fn test_detect_hdf5() {
626 let mut data = vec![0x89, b'H', b'D', b'F', 0x0D, 0x0A, 0x1A, 0x0A];
627 data.extend_from_slice(b"rest_of_file");
628 assert_eq!(detect_format(&data).unwrap(), NcFormat::Nc4);
629 }
630
631 #[test]
632 fn test_detect_invalid_magic() {
633 let data = b"XXXX";
634 assert!(matches!(
635 detect_format(data).unwrap_err(),
636 Error::InvalidMagic
637 ));
638 }
639
640 #[test]
641 fn test_detect_unsupported_version() {
642 let data = b"CDF\x03";
643 assert!(matches!(
644 detect_format(data).unwrap_err(),
645 Error::UnsupportedVersion(3)
646 ));
647 }
648
649 #[test]
650 fn test_detect_too_short() {
651 let data = b"CD";
652 assert!(matches!(
653 detect_format(data).unwrap_err(),
654 Error::InvalidMagic
655 ));
656 }
657
658 #[test]
659 fn test_from_bytes_minimal_cdf1() {
660 let mut data = Vec::new();
662 data.extend_from_slice(b"CDF\x01");
663 data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0u32.to_be_bytes());
669 data.extend_from_slice(&0u32.to_be_bytes());
670 data.extend_from_slice(&0u32.to_be_bytes());
672 data.extend_from_slice(&0u32.to_be_bytes());
673
674 let file = NcFile::from_bytes(&data).unwrap();
675 assert_eq!(file.format(), NcFormat::Classic);
676 assert!(file.dimensions().is_empty());
677 assert!(file.variables().is_empty());
678 assert!(file.global_attributes().is_empty());
679 }
680
681 #[test]
682 fn test_from_bytes_cdf1_with_data() {
683 let mut data = Vec::new();
685 data.extend_from_slice(b"CDF\x01");
686 data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0x0000_000Au32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes());
693 data.push(b'x');
694 data.extend_from_slice(&[0, 0, 0]); data.extend_from_slice(&3u32.to_be_bytes());
697
698 data.extend_from_slice(&0x0000_000Cu32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(&5u32.to_be_bytes());
703 data.extend_from_slice(b"title");
704 data.extend_from_slice(&[0, 0, 0]); data.extend_from_slice(&2u32.to_be_bytes());
707 data.extend_from_slice(&4u32.to_be_bytes());
709 data.extend_from_slice(b"test"); data.extend_from_slice(&0x0000_000Bu32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(&4u32.to_be_bytes());
716 data.extend_from_slice(b"vals");
717 data.extend_from_slice(&1u32.to_be_bytes());
719 data.extend_from_slice(&0u32.to_be_bytes());
721 data.extend_from_slice(&0u32.to_be_bytes());
723 data.extend_from_slice(&0u32.to_be_bytes());
724 data.extend_from_slice(&5u32.to_be_bytes());
726 data.extend_from_slice(&12u32.to_be_bytes());
728 let data_offset = data.len() as u32 + 4; data.extend_from_slice(&data_offset.to_be_bytes());
731
732 data.extend_from_slice(&1.5f32.to_be_bytes());
734 data.extend_from_slice(&2.5f32.to_be_bytes());
735 data.extend_from_slice(&3.5f32.to_be_bytes());
736
737 let file = NcFile::from_bytes(&data).unwrap();
738 assert_eq!(file.format(), NcFormat::Classic);
739 assert_eq!(file.dimensions().len(), 1);
740 assert_eq!(file.dimensions()[0].name, "x");
741 assert_eq!(file.dimensions()[0].size, 3);
742
743 assert_eq!(file.global_attributes().len(), 1);
744 assert_eq!(file.global_attributes()[0].name, "title");
745 assert_eq!(
746 file.global_attributes()[0].value.as_string().unwrap(),
747 "test"
748 );
749
750 assert_eq!(file.variables().len(), 1);
751 let var = file.variable("vals").unwrap();
752 assert_eq!(var.dtype(), &NcType::Float);
753 assert_eq!(var.shape(), vec![3]);
754
755 let classic = file.as_classic().unwrap();
757 let arr: ndarray::ArrayD<f32> = classic.read_variable("vals").unwrap();
758 assert_eq!(arr.shape(), &[3]);
759 assert_eq!(arr[[0]], 1.5f32);
760 assert_eq!(arr[[1]], 2.5f32);
761 assert_eq!(arr[[2]], 3.5f32);
762 }
763
764 #[test]
765 fn test_variable_not_found() {
766 let mut data = Vec::new();
767 data.extend_from_slice(b"CDF\x01");
768 data.extend_from_slice(&0u32.to_be_bytes());
769 data.extend_from_slice(&0u32.to_be_bytes());
771 data.extend_from_slice(&0u32.to_be_bytes());
772 data.extend_from_slice(&0u32.to_be_bytes());
773 data.extend_from_slice(&0u32.to_be_bytes());
774 data.extend_from_slice(&0u32.to_be_bytes());
775 data.extend_from_slice(&0u32.to_be_bytes());
776
777 let file = NcFile::from_bytes(&data).unwrap();
778 assert!(matches!(
779 file.variable("nonexistent").unwrap_err(),
780 Error::VariableNotFound(_)
781 ));
782 }
783
784 #[test]
785 fn test_group_not_found() {
786 let mut data = Vec::new();
787 data.extend_from_slice(b"CDF\x01");
788 data.extend_from_slice(&0u32.to_be_bytes());
789 data.extend_from_slice(&0u32.to_be_bytes());
790 data.extend_from_slice(&0u32.to_be_bytes());
791 data.extend_from_slice(&0u32.to_be_bytes());
792 data.extend_from_slice(&0u32.to_be_bytes());
793 data.extend_from_slice(&0u32.to_be_bytes());
794 data.extend_from_slice(&0u32.to_be_bytes());
795
796 let file = NcFile::from_bytes(&data).unwrap();
797 assert!(matches!(
798 file.group("nonexistent").unwrap_err(),
799 Error::GroupNotFound(_)
800 ));
801 }
802}