1pub mod classic;
28pub mod error;
29pub mod masked;
30pub mod types;
31pub mod unpack;
32
33#[cfg(feature = "netcdf4")]
34pub mod nc4;
35#[cfg(feature = "netcdf4")]
36pub mod user_defined;
37
38#[cfg(feature = "cf")]
39pub mod cf;
40
41pub use error::{Error, Result};
42#[cfg(feature = "netcdf4")]
43pub use hdf5_reader::storage::DynStorage;
44#[cfg(feature = "netcdf4")]
45pub use hdf5_reader::{
46 BlockCacheStats, BlockCacheStorage, BytesStorage, ChunkCacheStats, DatasetChunk,
47 DatasetChunkIterator, ExternalFileResolver, ExternalLinkResolver, FileStorage,
48 FilesystemExternalFileResolver, FilesystemExternalLinkResolver, MmapStorage,
49 RangeRequestStorage, Storage, StorageBuffer,
50};
51pub use types::*;
52#[cfg(feature = "netcdf4")]
53pub use user_defined::{
54 NcArrayValue, NcCompoundFieldView, NcCompoundValueField, NcEnumValue, NcValue, NcValueView,
55};
56
57use std::fs::File;
58use std::io::Read;
59use std::path::Path;
60#[cfg(feature = "netcdf4")]
61use std::sync::Arc;
62
63use memmap2::Mmap;
64use ndarray::ArrayD;
65#[cfg(feature = "rayon")]
66use rayon::ThreadPool;
67
68#[cfg(feature = "netcdf4")]
74pub trait NcReadable: classic::data::NcReadType + hdf5_reader::H5Type {}
75#[cfg(feature = "netcdf4")]
76impl<T: classic::data::NcReadType + hdf5_reader::H5Type> NcReadable for T {}
77
78#[cfg(not(feature = "netcdf4"))]
79pub trait NcReadable: classic::data::NcReadType {}
80#[cfg(not(feature = "netcdf4"))]
81impl<T: classic::data::NcReadType> NcReadable for T {}
82
83pub struct NcFile {
85 format: NcFormat,
86 inner: NcFileInner,
87}
88
89enum NcFileInner {
90 Classic(classic::ClassicFile),
91 #[cfg(feature = "netcdf4")]
92 Nc4(Box<nc4::Nc4File>),
93}
94
95const HDF5_MAGIC: [u8; 8] = [0x89, b'H', b'D', b'F', 0x0D, 0x0A, 0x1A, 0x0A];
97
98fn detect_format(data: &[u8]) -> Result<NcFormat> {
100 if data.len() < 4 {
101 return Err(Error::InvalidMagic);
102 }
103
104 if data[0] == b'C' && data[1] == b'D' && data[2] == b'F' {
106 return match data[3] {
107 1 => Ok(NcFormat::Classic),
108 2 => Ok(NcFormat::Offset64),
109 5 => Ok(NcFormat::Cdf5),
110 v => Err(Error::UnsupportedVersion(v)),
111 };
112 }
113
114 if data.len() >= 8 && data[..8] == HDF5_MAGIC {
116 return Ok(NcFormat::Nc4);
117 }
118
119 Err(Error::InvalidMagic)
120}
121
122fn read_magic_prefix(reader: &mut impl Read) -> std::io::Result<([u8; 8], usize)> {
123 let mut magic = [0u8; 8];
124 let mut read_len = 0;
125 while read_len < magic.len() {
126 let n = reader.read(&mut magic[read_len..])?;
127 if n == 0 {
128 break;
129 }
130 read_len += n;
131 }
132 Ok((magic, read_len))
133}
134
135#[cfg(feature = "cf")]
136fn parent_group_path(path: &str) -> &str {
137 let trimmed = path.trim_matches('/');
138 trimmed
139 .rsplit_once('/')
140 .map(|(group_path, _)| group_path)
141 .unwrap_or("")
142}
143
144impl NcFile {
145 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
149 Self::open_with_options(path, NcOpenOptions::default())
150 }
151
152 pub fn from_bytes(data: &[u8]) -> Result<Self> {
156 Self::from_bytes_with_options(data, NcOpenOptions::default())
157 }
158
159 #[cfg(feature = "netcdf4")]
163 pub fn from_storage(storage: DynStorage) -> Result<Self> {
164 Self::from_storage_with_options(storage, NcOpenOptions::default())
165 }
166
167 #[cfg(feature = "netcdf4")]
169 pub fn from_storage_with_options(storage: DynStorage, options: NcOpenOptions) -> Result<Self> {
170 let magic_len = storage.len().min(HDF5_MAGIC.len() as u64) as usize;
171 let magic = storage.read_range(0, magic_len)?;
172 let format = detect_format(magic.as_ref())?;
173
174 match format {
175 NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
176 let classic = classic::ClassicFile::from_storage(storage, format)?;
177 Ok(NcFile {
178 format,
179 inner: NcFileInner::Classic(classic),
180 })
181 }
182 NcFormat::Nc4 | NcFormat::Nc4Classic => {
183 let nc4 = nc4::Nc4File::from_storage_with_options(storage, options)?;
184 let actual_format = if nc4.is_classic_model() {
185 NcFormat::Nc4Classic
186 } else {
187 NcFormat::Nc4
188 };
189 Ok(NcFile {
190 format: actual_format,
191 inner: NcFileInner::Nc4(Box::new(nc4)),
192 })
193 }
194 }
195 }
196
197 pub fn from_bytes_with_options(data: &[u8], options: NcOpenOptions) -> Result<Self> {
201 let format = detect_format(data)?;
202
203 match format {
204 NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
205 let classic = classic::ClassicFile::from_bytes(data, format)?;
206 Ok(NcFile {
207 format,
208 inner: NcFileInner::Classic(classic),
209 })
210 }
211 NcFormat::Nc4 | NcFormat::Nc4Classic => {
212 #[cfg(feature = "netcdf4")]
213 {
214 let nc4 = nc4::Nc4File::from_bytes_with_options(data, options)?;
215 let actual_format = if nc4.is_classic_model() {
216 NcFormat::Nc4Classic
217 } else {
218 NcFormat::Nc4
219 };
220 Ok(NcFile {
221 format: actual_format,
222 inner: NcFileInner::Nc4(Box::new(nc4)),
223 })
224 }
225 #[cfg(not(feature = "netcdf4"))]
226 {
227 let _ = options;
228 Err(Error::Nc4NotEnabled)
229 }
230 }
231 }
232 }
233
234 pub fn format(&self) -> NcFormat {
236 self.format
237 }
238
239 pub fn root_group(&self) -> Result<&NcGroup> {
245 match &self.inner {
246 NcFileInner::Classic(c) => Ok(c.root_group()),
247 #[cfg(feature = "netcdf4")]
248 NcFileInner::Nc4(n) => n.root_group(),
249 }
250 }
251
252 pub fn dimensions(&self) -> Result<&[NcDimension]> {
254 match &self.inner {
255 NcFileInner::Classic(c) => Ok(&c.root_group().dimensions),
256 #[cfg(feature = "netcdf4")]
257 NcFileInner::Nc4(n) => n.dimensions(),
258 }
259 }
260
261 pub fn variables(&self) -> Result<&[NcVariable]> {
263 match &self.inner {
264 NcFileInner::Classic(c) => Ok(&c.root_group().variables),
265 #[cfg(feature = "netcdf4")]
266 NcFileInner::Nc4(n) => n.variables(),
267 }
268 }
269
270 pub fn global_attributes(&self) -> Result<&[NcAttribute]> {
272 match &self.inner {
273 NcFileInner::Classic(c) => Ok(&c.root_group().attributes),
274 #[cfg(feature = "netcdf4")]
275 NcFileInner::Nc4(n) => n.global_attributes(),
276 }
277 }
278
279 pub fn group(&self, path: &str) -> Result<&NcGroup> {
281 match &self.inner {
282 NcFileInner::Classic(c) => c
283 .root_group()
284 .group(path)
285 .ok_or_else(|| Error::GroupNotFound(path.to_string())),
286 #[cfg(feature = "netcdf4")]
287 NcFileInner::Nc4(n) => n.group(path),
288 }
289 }
290
291 pub fn variable(&self, name: &str) -> Result<&NcVariable> {
293 match &self.inner {
294 NcFileInner::Classic(c) => c
295 .root_group()
296 .variable(name)
297 .ok_or_else(|| Error::VariableNotFound(name.to_string())),
298 #[cfg(feature = "netcdf4")]
299 NcFileInner::Nc4(n) => n.variable(name),
300 }
301 }
302
303 pub fn dimension(&self, name: &str) -> Result<&NcDimension> {
305 match &self.inner {
306 NcFileInner::Classic(c) => c
307 .root_group()
308 .dimension(name)
309 .ok_or_else(|| Error::DimensionNotFound(name.to_string())),
310 #[cfg(feature = "netcdf4")]
311 NcFileInner::Nc4(n) => n.dimension(name),
312 }
313 }
314
315 pub fn coordinate_variable(&self, name: &str) -> Result<&NcVariable> {
317 self.root_group()?
318 .coordinate_variable(name)
319 .ok_or_else(|| Error::VariableNotFound(format!("coordinate variable for {name}")))
320 }
321
322 #[cfg(feature = "cf")]
324 pub fn cf_coordinate_axes(&self, group_path: &str) -> Result<Vec<cf::CfCoordinateAxis<'_>>> {
325 let group = self.group(group_path)?;
326 Ok(cf::discover_coordinate_axes(group))
327 }
328
329 #[cfg(feature = "cf")]
331 pub fn cf_variable_axes(&self, name: &str) -> Result<Vec<cf::CfCoordinateAxis<'_>>> {
332 let variable = self.variable(name)?;
333 let group = self.group(parent_group_path(name))?;
334 Ok(cf::discover_variable_axes(variable, group))
335 }
336
337 #[cfg(feature = "cf")]
339 pub fn cf_time_coordinates(&self, group_path: &str) -> Result<Vec<cf::CfTimeCoordinate<'_>>> {
340 let group = self.group(group_path)?;
341 cf::discover_time_coordinates(group)
342 }
343
344 #[cfg(feature = "cf")]
346 pub fn cf_variable_time_coordinate(
347 &self,
348 name: &str,
349 ) -> Result<Option<cf::CfTimeCoordinate<'_>>> {
350 let variable = self.variable(name)?;
351 let group = self.group(parent_group_path(name))?;
352 cf::discover_variable_time_coordinate(variable, group)
353 }
354
355 pub fn global_attribute(&self, name: &str) -> Result<&NcAttribute> {
357 match &self.inner {
358 NcFileInner::Classic(c) => c
359 .root_group()
360 .attribute(name)
361 .ok_or_else(|| Error::AttributeNotFound(name.to_string())),
362 #[cfg(feature = "netcdf4")]
363 NcFileInner::Nc4(n) => n.global_attribute(name),
364 }
365 }
366
367 pub fn read_variable<T: NcReadable>(&self, name: &str) -> Result<ArrayD<T>> {
374 match &self.inner {
375 NcFileInner::Classic(c) => c.read_variable::<T>(name),
376 #[cfg(feature = "netcdf4")]
377 NcFileInner::Nc4(n) => Ok(n.read_variable::<T>(name)?),
378 }
379 }
380
381 pub fn read_variable_into<T: NcReadable>(&self, name: &str, dst: &mut [T]) -> Result<()> {
383 match &self.inner {
384 NcFileInner::Classic(c) => c.read_variable_into::<T>(name, dst),
385 #[cfg(feature = "netcdf4")]
386 NcFileInner::Nc4(n) => Ok(n.read_variable_into::<T>(name, dst)?),
387 }
388 }
389
390 #[cfg(feature = "netcdf4")]
392 pub fn read_variable_raw_bytes(&self, name: &str) -> Result<Vec<u8>> {
393 match &self.inner {
394 NcFileInner::Classic(_) => Err(Error::TypeMismatch {
395 expected: "NetCDF-4 variable".to_string(),
396 actual: "classic NetCDF variable".to_string(),
397 }),
398 NcFileInner::Nc4(n) => Ok(n.read_variable_raw_bytes(name)?),
399 }
400 }
401
402 #[cfg(feature = "netcdf4")]
404 pub fn read_variable_raw_bytes_into(&self, name: &str, dst: &mut [u8]) -> Result<()> {
405 match &self.inner {
406 NcFileInner::Classic(_) => Err(Error::TypeMismatch {
407 expected: "NetCDF-4 variable".to_string(),
408 actual: "classic NetCDF variable".to_string(),
409 }),
410 NcFileInner::Nc4(n) => Ok(n.read_variable_raw_bytes_into(name, dst)?),
411 }
412 }
413
414 #[cfg(feature = "netcdf4")]
416 pub fn read_variable_native_bytes(&self, name: &str) -> Result<Vec<u8>> {
417 match &self.inner {
418 NcFileInner::Classic(_) => Err(Error::TypeMismatch {
419 expected: "NetCDF-4 variable".to_string(),
420 actual: "classic NetCDF variable".to_string(),
421 }),
422 NcFileInner::Nc4(n) => Ok(n.read_variable_native_bytes(name)?),
423 }
424 }
425
426 #[cfg(feature = "netcdf4")]
428 pub fn read_variable_native_bytes_into(&self, name: &str, dst: &mut [u8]) -> Result<()> {
429 match &self.inner {
430 NcFileInner::Classic(_) => Err(Error::TypeMismatch {
431 expected: "NetCDF-4 variable".to_string(),
432 actual: "classic NetCDF variable".to_string(),
433 }),
434 NcFileInner::Nc4(n) => Ok(n.read_variable_native_bytes_into(name, dst)?),
435 }
436 }
437
438 #[cfg(feature = "netcdf4")]
440 pub fn iter_variable_chunks(&self, name: &str) -> Result<DatasetChunkIterator> {
441 match &self.inner {
442 NcFileInner::Classic(_) => Err(Error::TypeMismatch {
443 expected: "NetCDF-4 chunked variable".to_string(),
444 actual: "classic NetCDF variable".to_string(),
445 }),
446 NcFileInner::Nc4(n) => Ok(n.iter_variable_chunks(name)?),
447 }
448 }
449
450 #[cfg(feature = "netcdf4")]
452 pub fn chunk_cache_stats(&self) -> Option<ChunkCacheStats> {
453 match &self.inner {
454 NcFileInner::Classic(_) => None,
455 NcFileInner::Nc4(n) => Some(n.chunk_cache_stats()),
456 }
457 }
458
459 #[cfg(feature = "rayon")]
463 pub fn read_variable_parallel<T: NcReadable>(&self, name: &str) -> Result<ArrayD<T>> {
464 match &self.inner {
465 NcFileInner::Classic(c) => c.read_variable_parallel::<T>(name),
466 #[cfg(feature = "netcdf4")]
467 NcFileInner::Nc4(n) => Ok(n.read_variable_parallel::<T>(name)?),
468 }
469 }
470
471 #[cfg(feature = "rayon")]
475 pub fn read_variable_in_pool<T: NcReadable>(
476 &self,
477 name: &str,
478 pool: &ThreadPool,
479 ) -> Result<ArrayD<T>> {
480 match &self.inner {
481 NcFileInner::Classic(c) => pool.install(|| c.read_variable_parallel::<T>(name)),
482 #[cfg(feature = "netcdf4")]
483 NcFileInner::Nc4(n) => Ok(n.read_variable_in_pool::<T>(name, pool)?),
484 }
485 }
486
487 pub fn as_classic(&self) -> Option<&classic::ClassicFile> {
491 match &self.inner {
492 NcFileInner::Classic(c) => Some(c),
493 #[cfg(feature = "netcdf4")]
494 NcFileInner::Nc4(_) => None,
495 }
496 }
497
498 pub fn read_variable_as_f64(&self, name: &str) -> Result<ArrayD<f64>> {
504 match &self.inner {
505 NcFileInner::Classic(c) => c.read_variable_as_f64(name),
506 #[cfg(feature = "netcdf4")]
507 NcFileInner::Nc4(n) => n.read_variable_as_f64(name),
508 }
509 }
510
511 pub fn read_variable_as_string(&self, name: &str) -> Result<String> {
516 match &self.inner {
517 NcFileInner::Classic(c) => c.read_variable_as_string(name),
518 #[cfg(feature = "netcdf4")]
519 NcFileInner::Nc4(n) => n.read_variable_as_string(name),
520 }
521 }
522
523 pub fn read_variable_as_strings(&self, name: &str) -> Result<Vec<String>> {
528 match &self.inner {
529 NcFileInner::Classic(c) => c.read_variable_as_strings(name),
530 #[cfg(feature = "netcdf4")]
531 NcFileInner::Nc4(n) => n.read_variable_as_strings(name),
532 }
533 }
534
535 #[cfg(feature = "netcdf4")]
541 pub fn read_variable_user_defined(&self, name: &str) -> Result<ArrayD<NcValue>> {
542 match &self.inner {
543 NcFileInner::Classic(_) => Err(Error::TypeMismatch {
544 expected: "NetCDF-4 user-defined variable".to_string(),
545 actual: "classic NetCDF variable".to_string(),
546 }),
547 NcFileInner::Nc4(n) => n.read_variable_user_defined(name),
548 }
549 }
550
551 #[cfg(feature = "netcdf4")]
557 pub fn read_variable_user_defined_with<T, F>(&self, name: &str, decoder: F) -> Result<ArrayD<T>>
558 where
559 F: FnMut(NcValueView<'_>) -> Result<T>,
560 {
561 match &self.inner {
562 NcFileInner::Classic(_) => Err(Error::TypeMismatch {
563 expected: "NetCDF-4 user-defined variable".to_string(),
564 actual: "classic NetCDF variable".to_string(),
565 }),
566 NcFileInner::Nc4(n) => n.read_variable_user_defined_with(name, decoder),
567 }
568 }
569
570 pub fn read_variable_unpacked(&self, name: &str) -> Result<ArrayD<f64>> {
576 let var = self.variable(name)?;
577 let params = unpack::UnpackParams::from_variable(var);
578 let mut data = self.read_variable_as_f64(name)?;
579 if let Some(p) = params {
580 p.apply(&mut data);
581 }
582 Ok(data)
583 }
584
585 pub fn read_variable_masked(&self, name: &str) -> Result<ArrayD<f64>> {
589 let var = self.variable(name)?;
590 let params = masked::MaskParams::from_variable(var);
591 let mut data = self.read_variable_as_f64(name)?;
592 if let Some(p) = params {
593 p.apply(&mut data);
594 }
595 Ok(data)
596 }
597
598 pub fn read_variable_unpacked_masked(&self, name: &str) -> Result<ArrayD<f64>> {
603 let var = self.variable(name)?;
604 let mask_params = masked::MaskParams::from_variable(var);
605 let unpack_params = unpack::UnpackParams::from_variable(var);
606 let mut data = self.read_variable_as_f64(name)?;
607 if let Some(p) = mask_params {
608 p.apply(&mut data);
609 }
610 if let Some(p) = unpack_params {
611 p.apply(&mut data);
612 }
613 Ok(data)
614 }
615
616 pub fn read_variable_slice<T: NcReadable>(
620 &self,
621 name: &str,
622 selection: &NcSliceInfo,
623 ) -> Result<ArrayD<T>> {
624 match &self.inner {
625 NcFileInner::Classic(c) => c.read_variable_slice::<T>(name, selection),
626 #[cfg(feature = "netcdf4")]
627 NcFileInner::Nc4(n) => Ok(n.read_variable_slice::<T>(name, selection)?),
628 }
629 }
630
631 #[cfg(feature = "rayon")]
636 pub fn read_variable_slice_parallel<T: NcReadable>(
637 &self,
638 name: &str,
639 selection: &NcSliceInfo,
640 ) -> Result<ArrayD<T>> {
641 match &self.inner {
642 NcFileInner::Classic(c) => c.read_variable_slice_parallel::<T>(name, selection),
643 #[cfg(feature = "netcdf4")]
644 NcFileInner::Nc4(n) => Ok(n.read_variable_slice_parallel::<T>(name, selection)?),
645 }
646 }
647
648 pub fn read_variable_slice_as_f64(
650 &self,
651 name: &str,
652 selection: &NcSliceInfo,
653 ) -> Result<ArrayD<f64>> {
654 match &self.inner {
655 NcFileInner::Classic(c) => c.read_variable_slice_as_f64(name, selection),
656 #[cfg(feature = "netcdf4")]
657 NcFileInner::Nc4(n) => n.read_variable_slice_as_f64(name, selection),
658 }
659 }
660
661 pub fn read_variable_slice_unpacked(
663 &self,
664 name: &str,
665 selection: &NcSliceInfo,
666 ) -> Result<ArrayD<f64>> {
667 let var = self.variable(name)?;
668 let params = unpack::UnpackParams::from_variable(var);
669 let mut data = self.read_variable_slice_as_f64(name, selection)?;
670 if let Some(p) = params {
671 p.apply(&mut data);
672 }
673 Ok(data)
674 }
675
676 pub fn read_variable_slice_masked(
678 &self,
679 name: &str,
680 selection: &NcSliceInfo,
681 ) -> Result<ArrayD<f64>> {
682 let var = self.variable(name)?;
683 let params = masked::MaskParams::from_variable(var);
684 let mut data = self.read_variable_slice_as_f64(name, selection)?;
685 if let Some(p) = params {
686 p.apply(&mut data);
687 }
688 Ok(data)
689 }
690
691 pub fn read_variable_slice_unpacked_masked(
693 &self,
694 name: &str,
695 selection: &NcSliceInfo,
696 ) -> Result<ArrayD<f64>> {
697 let var = self.variable(name)?;
698 let mask_params = masked::MaskParams::from_variable(var);
699 let unpack_params = unpack::UnpackParams::from_variable(var);
700 let mut data = self.read_variable_slice_as_f64(name, selection)?;
701 if let Some(p) = mask_params {
702 p.apply(&mut data);
703 }
704 if let Some(p) = unpack_params {
705 p.apply(&mut data);
706 }
707 Ok(data)
708 }
709
710 pub fn iter_slices<T: NcReadable>(
718 &self,
719 name: &str,
720 dim: usize,
721 ) -> Result<NcSliceIterator<'_, T>> {
722 let var = self.variable(name)?;
723 let ndim = var.ndim();
724 if dim >= ndim {
725 return Err(Error::InvalidData(format!(
726 "dimension index {} out of range for {}-dimensional variable '{}'",
727 dim, ndim, name
728 )));
729 }
730 let dim_size = var.dimensions[dim].size;
731 Ok(NcSliceIterator {
732 file: self,
733 name: name.to_string(),
734 dim,
735 dim_size,
736 current: 0,
737 ndim,
738 _marker: std::marker::PhantomData,
739 })
740 }
741}
742
743pub struct NcOpenOptions {
745 pub chunk_cache_bytes: usize,
747 pub chunk_cache_slots: usize,
749 pub metadata_mode: NcMetadataMode,
751 #[cfg(feature = "netcdf4")]
753 pub filter_registry: Option<hdf5_reader::FilterRegistry>,
754 #[cfg(feature = "netcdf4")]
757 pub external_file_resolver: Option<Arc<dyn hdf5_reader::ExternalFileResolver>>,
758 #[cfg(feature = "netcdf4")]
760 pub external_link_resolver: Option<Arc<dyn hdf5_reader::ExternalLinkResolver>>,
761}
762
763impl Default for NcOpenOptions {
764 fn default() -> Self {
765 NcOpenOptions {
766 chunk_cache_bytes: 64 * 1024 * 1024,
767 chunk_cache_slots: 521,
768 metadata_mode: NcMetadataMode::Strict,
769 #[cfg(feature = "netcdf4")]
770 filter_registry: None,
771 #[cfg(feature = "netcdf4")]
772 external_file_resolver: None,
773 #[cfg(feature = "netcdf4")]
774 external_link_resolver: None,
775 }
776 }
777}
778
779impl NcFile {
780 pub fn open_with_options(path: impl AsRef<Path>, options: NcOpenOptions) -> Result<Self> {
782 let path = path.as_ref();
783 let mut file = File::open(path)?;
784 let (magic, n) = read_magic_prefix(&mut file)?;
785 let format = detect_format(&magic[..n])?;
786
787 match format {
788 NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
789 let file = File::open(path)?;
790 let mmap = unsafe { Mmap::map(&file)? };
792 let classic = classic::ClassicFile::from_mmap(mmap, format)?;
793 Ok(NcFile {
794 format,
795 inner: NcFileInner::Classic(classic),
796 })
797 }
798 NcFormat::Nc4 | NcFormat::Nc4Classic => {
799 #[cfg(feature = "netcdf4")]
800 {
801 let metadata_mode = options.metadata_mode;
802 let hdf5 = hdf5_reader::Hdf5File::open_with_options(
803 path,
804 hdf5_reader::OpenOptions {
805 chunk_cache_bytes: options.chunk_cache_bytes,
806 chunk_cache_slots: options.chunk_cache_slots,
807 filter_registry: options.filter_registry,
808 external_file_resolver: options.external_file_resolver,
809 external_link_resolver: options.external_link_resolver,
810 },
811 )?;
812 let nc4 = nc4::Nc4File::from_hdf5(hdf5, metadata_mode)?;
813 let actual_format = if nc4.is_classic_model() {
814 NcFormat::Nc4Classic
815 } else {
816 NcFormat::Nc4
817 };
818 Ok(NcFile {
819 format: actual_format,
820 inner: NcFileInner::Nc4(Box::new(nc4)),
821 })
822 }
823 #[cfg(not(feature = "netcdf4"))]
824 {
825 let _ = options;
826 Err(Error::Nc4NotEnabled)
827 }
828 }
829 }
830 }
831}
832
833pub struct NcSliceIterator<'f, T: NcReadable> {
835 file: &'f NcFile,
836 name: String,
837 dim: usize,
838 dim_size: u64,
839 current: u64,
840 ndim: usize,
841 _marker: std::marker::PhantomData<T>,
842}
843
844impl<'f, T: NcReadable> Iterator for NcSliceIterator<'f, T> {
845 type Item = Result<ArrayD<T>>;
846
847 fn next(&mut self) -> Option<Self::Item> {
848 if self.current >= self.dim_size {
849 return None;
850 }
851 let mut selections = Vec::with_capacity(self.ndim);
852 for d in 0..self.ndim {
853 if d == self.dim {
854 selections.push(NcSliceInfoElem::Index(self.current));
855 } else {
856 selections.push(NcSliceInfoElem::Slice {
857 start: 0,
858 end: u64::MAX,
859 step: 1,
860 });
861 }
862 }
863 let selection = NcSliceInfo { selections };
864 self.current += 1;
865 Some(self.file.read_variable_slice::<T>(&self.name, &selection))
866 }
867
868 fn size_hint(&self) -> (usize, Option<usize>) {
869 let remaining_u64 = self.dim_size.saturating_sub(self.current);
870 let remaining = remaining_u64.min(usize::MAX as u64) as usize;
871 (remaining, Some(remaining))
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878 #[cfg(feature = "netcdf4")]
879 use std::sync::Arc;
880 #[cfg(feature = "netcdf4")]
881 use std::sync::Mutex;
882
883 #[test]
884 fn detect_cdf1() {
885 let data = b"CDF\x01rest_of_file";
886 assert_eq!(detect_format(data).unwrap(), NcFormat::Classic);
887 }
888
889 #[test]
890 fn detect_cdf2() {
891 let data = b"CDF\x02rest_of_file";
892 assert_eq!(detect_format(data).unwrap(), NcFormat::Offset64);
893 }
894
895 #[test]
896 fn detect_cdf5() {
897 let data = b"CDF\x05rest_of_file";
898 assert_eq!(detect_format(data).unwrap(), NcFormat::Cdf5);
899 }
900
901 #[test]
902 fn detect_hdf5() {
903 let mut data = vec![0x89, b'H', b'D', b'F', 0x0D, 0x0A, 0x1A, 0x0A];
904 data.extend_from_slice(b"rest_of_file");
905 assert_eq!(detect_format(&data).unwrap(), NcFormat::Nc4);
906 }
907
908 #[test]
909 fn detect_invalid_magic() {
910 let data = b"XXXX";
911 assert!(matches!(
912 detect_format(data).unwrap_err(),
913 Error::InvalidMagic
914 ));
915 }
916
917 #[test]
918 fn detect_unsupported_version() {
919 let data = b"CDF\x03";
920 assert!(matches!(
921 detect_format(data).unwrap_err(),
922 Error::UnsupportedVersion(3)
923 ));
924 }
925
926 #[test]
927 fn detect_too_short() {
928 let data = b"CD";
929 assert!(matches!(
930 detect_format(data).unwrap_err(),
931 Error::InvalidMagic
932 ));
933 }
934
935 #[test]
936 fn from_bytes_minimal_cdf1() {
937 let mut data = Vec::new();
939 data.extend_from_slice(b"CDF\x01");
940 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());
946 data.extend_from_slice(&0u32.to_be_bytes());
947 data.extend_from_slice(&0u32.to_be_bytes());
949 data.extend_from_slice(&0u32.to_be_bytes());
950
951 let file = NcFile::from_bytes(&data).unwrap();
952 assert_eq!(file.format(), NcFormat::Classic);
953 assert!(file.dimensions().unwrap().is_empty());
954 assert!(file.variables().unwrap().is_empty());
955 assert!(file.global_attributes().unwrap().is_empty());
956 }
957
958 #[cfg(feature = "netcdf4")]
959 struct CountingStorage {
960 data: Arc<[u8]>,
961 reads: Mutex<Vec<(u64, usize)>>,
962 }
963
964 #[cfg(feature = "netcdf4")]
965 impl CountingStorage {
966 fn new(data: Vec<u8>) -> Self {
967 Self {
968 data: Arc::<[u8]>::from(data),
969 reads: Mutex::new(Vec::new()),
970 }
971 }
972
973 fn reads(&self) -> Vec<(u64, usize)> {
974 self.reads.lock().unwrap().clone()
975 }
976 }
977
978 #[cfg(feature = "netcdf4")]
979 impl hdf5_reader::Storage for CountingStorage {
980 fn len(&self) -> u64 {
981 self.data.len() as u64
982 }
983
984 fn read_range(
985 &self,
986 offset: u64,
987 len: usize,
988 ) -> hdf5_reader::error::Result<hdf5_reader::StorageBuffer> {
989 self.reads.lock().unwrap().push((offset, len));
990 let start = usize::try_from(offset)
991 .map_err(|_| hdf5_reader::error::Error::OffsetOutOfBounds(offset))?;
992 let end = start
993 .checked_add(len)
994 .ok_or(hdf5_reader::error::Error::OffsetOutOfBounds(offset))?;
995 if end > self.data.len() {
996 return Err(hdf5_reader::error::Error::UnexpectedEof {
997 offset,
998 needed: len as u64,
999 available: self.len().saturating_sub(offset),
1000 });
1001 }
1002 Ok(hdf5_reader::StorageBuffer::from_vec(
1003 self.data[start..end].to_vec(),
1004 ))
1005 }
1006 }
1007
1008 #[cfg(feature = "netcdf4")]
1009 fn classic_large_offset_fixture() -> Vec<u8> {
1010 fn write_name(buf: &mut Vec<u8>, name: &str) {
1011 buf.extend_from_slice(&(name.len() as u32).to_be_bytes());
1012 buf.extend_from_slice(name.as_bytes());
1013 while buf.len() % 4 != 0 {
1014 buf.push(0);
1015 }
1016 }
1017
1018 const DATA_OFFSET: usize = 70 * 1024;
1019 let mut buf = Vec::new();
1020 buf.extend_from_slice(b"CDF\x01");
1021 buf.extend_from_slice(&0u32.to_be_bytes());
1022 buf.extend_from_slice(&0x0000_000Au32.to_be_bytes());
1023 buf.extend_from_slice(&1u32.to_be_bytes());
1024 write_name(&mut buf, "x");
1025 buf.extend_from_slice(&4u32.to_be_bytes());
1026 buf.extend_from_slice(&0u32.to_be_bytes());
1027 buf.extend_from_slice(&0u32.to_be_bytes());
1028 buf.extend_from_slice(&0x0000_000Bu32.to_be_bytes());
1029 buf.extend_from_slice(&1u32.to_be_bytes());
1030 write_name(&mut buf, "data");
1031 buf.extend_from_slice(&1u32.to_be_bytes());
1032 buf.extend_from_slice(&0u32.to_be_bytes());
1033 buf.extend_from_slice(&0u32.to_be_bytes());
1034 buf.extend_from_slice(&0u32.to_be_bytes());
1035 buf.extend_from_slice(&4u32.to_be_bytes());
1036 buf.extend_from_slice(&16u32.to_be_bytes());
1037 buf.extend_from_slice(&(DATA_OFFSET as u32).to_be_bytes());
1038
1039 buf.resize(DATA_OFFSET, 0);
1040 for value in [10i32, 20, 30, 40] {
1041 buf.extend_from_slice(&value.to_be_bytes());
1042 }
1043 buf
1044 }
1045
1046 fn write_cdf1_name(buf: &mut Vec<u8>, name: &str) {
1047 buf.extend_from_slice(&(name.len() as u32).to_be_bytes());
1048 buf.extend_from_slice(name.as_bytes());
1049 while buf.len() % 4 != 0 {
1050 buf.push(0);
1051 }
1052 }
1053
1054 fn write_cdf5_count(buf: &mut Vec<u8>, value: u64) {
1055 buf.extend_from_slice(&value.to_be_bytes());
1056 }
1057
1058 fn write_cdf5_name(buf: &mut Vec<u8>, name: &str) {
1059 write_cdf5_count(buf, name.len() as u64);
1060 buf.extend_from_slice(name.as_bytes());
1061 while buf.len() % 4 != 0 {
1062 buf.push(0);
1063 }
1064 }
1065
1066 fn cdf5_huge_dimension_fixture() -> Vec<u8> {
1067 let mut buf = Vec::new();
1068 buf.extend_from_slice(b"CDF\x05");
1069 write_cdf5_count(&mut buf, 0);
1070
1071 buf.extend_from_slice(&0x0000_000Au32.to_be_bytes());
1072 write_cdf5_count(&mut buf, 1);
1073 write_cdf5_name(&mut buf, "n");
1074 write_cdf5_count(&mut buf, u64::MAX);
1075
1076 buf.extend_from_slice(&0u32.to_be_bytes());
1077 write_cdf5_count(&mut buf, 0);
1078
1079 buf.extend_from_slice(&0x0000_000Bu32.to_be_bytes());
1080 write_cdf5_count(&mut buf, 1);
1081 write_cdf5_name(&mut buf, "big");
1082 write_cdf5_count(&mut buf, 1);
1083 write_cdf5_count(&mut buf, 0);
1084 buf.extend_from_slice(&0u32.to_be_bytes());
1085 write_cdf5_count(&mut buf, 0);
1086 buf.extend_from_slice(&4u32.to_be_bytes());
1087 write_cdf5_count(&mut buf, 4);
1088 let offset_pos = buf.len();
1089 buf.extend_from_slice(&0u64.to_be_bytes());
1090
1091 let data_offset = buf.len() as u64;
1092 buf[offset_pos..offset_pos + 8].copy_from_slice(&data_offset.to_be_bytes());
1093 buf.extend_from_slice(&123i32.to_be_bytes());
1094 buf
1095 }
1096
1097 fn subfiling_marker_fixture() -> Vec<u8> {
1098 let mut buf = Vec::new();
1099 buf.extend_from_slice(b"CDF\x01");
1100 buf.extend_from_slice(&0u32.to_be_bytes());
1101 buf.extend_from_slice(&0u32.to_be_bytes());
1102 buf.extend_from_slice(&0u32.to_be_bytes());
1103
1104 buf.extend_from_slice(&0x0000_000Cu32.to_be_bytes());
1105 buf.extend_from_slice(&1u32.to_be_bytes());
1106 write_cdf1_name(&mut buf, "_PnetCDF_SubFiling_enabled");
1107 buf.extend_from_slice(&4u32.to_be_bytes());
1108 buf.extend_from_slice(&1u32.to_be_bytes());
1109 buf.extend_from_slice(&1i32.to_be_bytes());
1110
1111 buf.extend_from_slice(&0u32.to_be_bytes());
1112 buf.extend_from_slice(&0u32.to_be_bytes());
1113 buf
1114 }
1115
1116 fn streaming_cdf1_record_fixture(values: &[i32], trailing: &[u8]) -> Vec<u8> {
1117 let mut buf = Vec::new();
1118 buf.extend_from_slice(b"CDF\x01");
1119 buf.extend_from_slice(&u32::MAX.to_be_bytes());
1120
1121 buf.extend_from_slice(&0x0000_000Au32.to_be_bytes());
1122 buf.extend_from_slice(&1u32.to_be_bytes());
1123 write_cdf1_name(&mut buf, "time");
1124 buf.extend_from_slice(&0u32.to_be_bytes());
1125
1126 buf.extend_from_slice(&0u32.to_be_bytes());
1127 buf.extend_from_slice(&0u32.to_be_bytes());
1128
1129 buf.extend_from_slice(&0x0000_000Bu32.to_be_bytes());
1130 buf.extend_from_slice(&1u32.to_be_bytes());
1131 write_cdf1_name(&mut buf, "temp");
1132 buf.extend_from_slice(&1u32.to_be_bytes());
1133 buf.extend_from_slice(&0u32.to_be_bytes());
1134 buf.extend_from_slice(&0u32.to_be_bytes());
1135 buf.extend_from_slice(&0u32.to_be_bytes());
1136 buf.extend_from_slice(&4u32.to_be_bytes());
1137 buf.extend_from_slice(&4u32.to_be_bytes());
1138 let offset_pos = buf.len();
1139 buf.extend_from_slice(&0u32.to_be_bytes());
1140
1141 let data_offset = buf.len() as u32;
1142 buf[offset_pos..offset_pos + 4].copy_from_slice(&data_offset.to_be_bytes());
1143
1144 for value in values {
1145 buf.extend_from_slice(&value.to_be_bytes());
1146 }
1147 buf.extend_from_slice(trailing);
1148 buf
1149 }
1150
1151 fn streaming_cdf2_record_fixture(values: &[i32]) -> Vec<u8> {
1152 let mut buf = Vec::new();
1153 buf.extend_from_slice(b"CDF\x02");
1154 buf.extend_from_slice(&u32::MAX.to_be_bytes());
1155
1156 buf.extend_from_slice(&0x0000_000Au32.to_be_bytes());
1157 buf.extend_from_slice(&1u32.to_be_bytes());
1158 write_cdf1_name(&mut buf, "time");
1159 buf.extend_from_slice(&0u32.to_be_bytes());
1160
1161 buf.extend_from_slice(&0u32.to_be_bytes());
1162 buf.extend_from_slice(&0u32.to_be_bytes());
1163
1164 buf.extend_from_slice(&0x0000_000Bu32.to_be_bytes());
1165 buf.extend_from_slice(&1u32.to_be_bytes());
1166 write_cdf1_name(&mut buf, "temp");
1167 buf.extend_from_slice(&1u32.to_be_bytes());
1168 buf.extend_from_slice(&0u32.to_be_bytes());
1169 buf.extend_from_slice(&0u32.to_be_bytes());
1170 buf.extend_from_slice(&0u32.to_be_bytes());
1171 buf.extend_from_slice(&4u32.to_be_bytes());
1172 buf.extend_from_slice(&4u32.to_be_bytes());
1173 let offset_pos = buf.len();
1174 buf.extend_from_slice(&0u64.to_be_bytes());
1175
1176 let data_offset = buf.len() as u64;
1177 buf[offset_pos..offset_pos + 8].copy_from_slice(&data_offset.to_be_bytes());
1178
1179 for value in values {
1180 buf.extend_from_slice(&value.to_be_bytes());
1181 }
1182 buf
1183 }
1184
1185 fn streaming_cdf1_two_record_var_fixture(records: &[(i32, i16)]) -> Vec<u8> {
1186 let mut buf = Vec::new();
1187 buf.extend_from_slice(b"CDF\x01");
1188 buf.extend_from_slice(&u32::MAX.to_be_bytes());
1189
1190 buf.extend_from_slice(&0x0000_000Au32.to_be_bytes());
1191 buf.extend_from_slice(&1u32.to_be_bytes());
1192 write_cdf1_name(&mut buf, "time");
1193 buf.extend_from_slice(&0u32.to_be_bytes());
1194
1195 buf.extend_from_slice(&0u32.to_be_bytes());
1196 buf.extend_from_slice(&0u32.to_be_bytes());
1197
1198 buf.extend_from_slice(&0x0000_000Bu32.to_be_bytes());
1199 buf.extend_from_slice(&2u32.to_be_bytes());
1200
1201 write_cdf1_name(&mut buf, "temp");
1202 buf.extend_from_slice(&1u32.to_be_bytes());
1203 buf.extend_from_slice(&0u32.to_be_bytes());
1204 buf.extend_from_slice(&0u32.to_be_bytes());
1205 buf.extend_from_slice(&0u32.to_be_bytes());
1206 buf.extend_from_slice(&4u32.to_be_bytes());
1207 buf.extend_from_slice(&4u32.to_be_bytes());
1208 let temp_offset_pos = buf.len();
1209 buf.extend_from_slice(&0u32.to_be_bytes());
1210
1211 write_cdf1_name(&mut buf, "flag");
1212 buf.extend_from_slice(&1u32.to_be_bytes());
1213 buf.extend_from_slice(&0u32.to_be_bytes());
1214 buf.extend_from_slice(&0u32.to_be_bytes());
1215 buf.extend_from_slice(&0u32.to_be_bytes());
1216 buf.extend_from_slice(&3u32.to_be_bytes());
1217 buf.extend_from_slice(&2u32.to_be_bytes());
1218 let flag_offset_pos = buf.len();
1219 buf.extend_from_slice(&0u32.to_be_bytes());
1220
1221 let data_offset = buf.len() as u32;
1222 let flag_offset = data_offset + 4;
1223 buf[temp_offset_pos..temp_offset_pos + 4].copy_from_slice(&data_offset.to_be_bytes());
1224 buf[flag_offset_pos..flag_offset_pos + 4].copy_from_slice(&flag_offset.to_be_bytes());
1225
1226 for &(temp, flag) in records {
1227 buf.extend_from_slice(&temp.to_be_bytes());
1228 buf.extend_from_slice(&flag.to_be_bytes());
1229 buf.extend_from_slice(&[0, 0]);
1230 }
1231 buf
1232 }
1233
1234 #[test]
1235 fn cdf5_huge_dimension_can_slice_but_full_read_errors_cleanly() {
1236 let file = NcFile::from_bytes(&cdf5_huge_dimension_fixture()).unwrap();
1237 let selection = NcSliceInfo {
1238 selections: vec![NcSliceInfoElem::Index(0)],
1239 };
1240
1241 let sliced: ndarray::ArrayD<i32> = file.read_variable_slice("big", &selection).unwrap();
1242 assert_eq!(sliced.as_slice().unwrap(), &[123]);
1243
1244 let err = file.read_variable::<i32>("big").unwrap_err();
1245 assert!(matches!(err, Error::InvalidData(_)));
1246 }
1247
1248 #[test]
1249 fn classic_subfiling_marker_returns_unsupported_feature() {
1250 let err = match NcFile::from_bytes(&subfiling_marker_fixture()) {
1251 Ok(_) => panic!("subfiling marker should be rejected"),
1252 Err(err) => err,
1253 };
1254 assert!(matches!(
1255 err,
1256 Error::UnsupportedFeature(message) if message.contains("PnetCDF subfiling")
1257 ));
1258 }
1259
1260 #[test]
1261 fn streaming_cdf1_numrecs_are_derived_from_file_length() {
1262 let file = NcFile::from_bytes(&streaming_cdf1_record_fixture(&[10, 20, 30], &[])).unwrap();
1263
1264 assert_eq!(file.as_classic().unwrap().numrecs(), 3);
1265 assert_eq!(file.dimension("time").unwrap().size, 3);
1266 assert_eq!(file.variable("temp").unwrap().shape(), vec![3]);
1267
1268 let values: ndarray::ArrayD<i32> = file.read_variable("temp").unwrap();
1269 assert_eq!(values.as_slice().unwrap(), &[10, 20, 30]);
1270 }
1271
1272 #[test]
1273 fn streaming_cdf1_numrecs_ignore_trailing_partial_record() {
1274 let file =
1275 NcFile::from_bytes(&streaming_cdf1_record_fixture(&[10, 20], &[0xAA, 0xBB])).unwrap();
1276
1277 assert_eq!(file.as_classic().unwrap().numrecs(), 2);
1278 assert_eq!(file.variable("temp").unwrap().shape(), vec![2]);
1279
1280 let values: ndarray::ArrayD<i32> = file.read_variable("temp").unwrap();
1281 assert_eq!(values.as_slice().unwrap(), &[10, 20]);
1282 }
1283
1284 #[test]
1285 fn streaming_cdf2_numrecs_are_derived_from_file_length() {
1286 let file = NcFile::from_bytes(&streaming_cdf2_record_fixture(&[10, 20, 30])).unwrap();
1287
1288 assert_eq!(file.format(), NcFormat::Offset64);
1289 assert_eq!(file.as_classic().unwrap().numrecs(), 3);
1290 assert_eq!(file.dimension("time").unwrap().size, 3);
1291 assert_eq!(file.variable("temp").unwrap().shape(), vec![3]);
1292
1293 let values: ndarray::ArrayD<i32> = file.read_variable("temp").unwrap();
1294 assert_eq!(values.as_slice().unwrap(), &[10, 20, 30]);
1295 }
1296
1297 #[test]
1298 fn streaming_cdf1_numrecs_use_full_stride_for_multiple_record_variables() {
1299 let file = NcFile::from_bytes(&streaming_cdf1_two_record_var_fixture(&[
1300 (10, 1),
1301 (20, 2),
1302 (30, 3),
1303 ]))
1304 .unwrap();
1305
1306 assert_eq!(file.as_classic().unwrap().numrecs(), 3);
1307 assert_eq!(file.variable("temp").unwrap().shape(), vec![3]);
1308 assert_eq!(file.variable("flag").unwrap().shape(), vec![3]);
1309
1310 let temps: ndarray::ArrayD<i32> = file.read_variable("temp").unwrap();
1311 let flags: ndarray::ArrayD<i16> = file.read_variable("flag").unwrap();
1312 assert_eq!(temps.as_slice().unwrap(), &[10, 20, 30]);
1313 assert_eq!(flags.as_slice().unwrap(), &[1, 2, 3]);
1314 }
1315
1316 #[cfg(feature = "netcdf4")]
1317 #[test]
1318 fn streaming_cdf1_from_storage_derives_numrecs() {
1319 let file = NcFile::from_storage(Arc::new(BytesStorage::new(
1320 streaming_cdf1_record_fixture(&[10, 20, 30], &[]),
1321 )))
1322 .unwrap();
1323
1324 assert_eq!(file.as_classic().unwrap().numrecs(), 3);
1325 assert_eq!(file.dimension("time").unwrap().size, 3);
1326
1327 let values: ndarray::ArrayD<i32> = file.read_variable("temp").unwrap();
1328 assert_eq!(values.as_slice().unwrap(), &[10, 20, 30]);
1329 }
1330
1331 #[cfg(feature = "netcdf4")]
1332 #[test]
1333 fn classic_from_storage_keeps_open_range_backed() {
1334 let data = classic_large_offset_fixture();
1335 let full_len = data.len();
1336 let storage = Arc::new(CountingStorage::new(data));
1337
1338 let file = NcFile::from_storage(storage.clone()).unwrap();
1339 assert_eq!(file.format(), NcFormat::Classic);
1340 assert!(!storage
1341 .reads()
1342 .iter()
1343 .any(|&(offset, len)| offset == 0 && len == full_len));
1344
1345 let values: ndarray::ArrayD<i32> = file.read_variable("data").unwrap();
1346 assert_eq!(values.as_slice().unwrap(), &[10, 20, 30, 40]);
1347 assert!(storage
1348 .reads()
1349 .iter()
1350 .any(|&(offset, len)| { offset == (70 * 1024) as u64 && len == 16 }));
1351 }
1352
1353 #[cfg(feature = "netcdf4")]
1354 #[test]
1355 fn classic_from_storage_slice_reads_planned_range() {
1356 let storage = Arc::new(CountingStorage::new(classic_large_offset_fixture()));
1357 let file = NcFile::from_storage(storage.clone()).unwrap();
1358 let selection = NcSliceInfo {
1359 selections: vec![NcSliceInfoElem::Slice {
1360 start: 1,
1361 end: 3,
1362 step: 1,
1363 }],
1364 };
1365
1366 let values: ndarray::ArrayD<i32> = file.read_variable_slice("data", &selection).unwrap();
1367 assert_eq!(values.as_slice().unwrap(), &[20, 30]);
1368 assert!(storage
1369 .reads()
1370 .iter()
1371 .any(|&(offset, len)| { offset == (70 * 1024 + 4) as u64 && len == 8 }));
1372 }
1373
1374 #[cfg(feature = "netcdf4")]
1375 #[test]
1376 fn from_storage_minimal_cdf1() {
1377 let mut data = Vec::new();
1379 data.extend_from_slice(b"CDF\x01");
1380 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());
1386 data.extend_from_slice(&0u32.to_be_bytes());
1387 data.extend_from_slice(&0u32.to_be_bytes());
1389 data.extend_from_slice(&0u32.to_be_bytes());
1390
1391 let file = NcFile::from_storage(Arc::new(BytesStorage::new(data))).unwrap();
1392 assert_eq!(file.format(), NcFormat::Classic);
1393 assert!(file.dimensions().unwrap().is_empty());
1394 assert!(file.variables().unwrap().is_empty());
1395 assert!(file.global_attributes().unwrap().is_empty());
1396 }
1397
1398 #[cfg(feature = "netcdf4")]
1399 #[test]
1400 fn from_storage_short_input_reports_invalid_magic() {
1401 let err = NcFile::from_storage(Arc::new(BytesStorage::new(vec![b'C', b'D'])))
1402 .err()
1403 .expect("short storage should not parse as NetCDF");
1404 assert!(matches!(err, Error::InvalidMagic));
1405 }
1406
1407 #[test]
1408 fn from_bytes_cdf1_with_data() {
1409 let mut data = Vec::new();
1411 data.extend_from_slice(b"CDF\x01");
1412 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());
1419 data.push(b'x');
1420 data.extend_from_slice(&[0, 0, 0]); data.extend_from_slice(&3u32.to_be_bytes());
1423
1424 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());
1429 data.extend_from_slice(b"title");
1430 data.extend_from_slice(&[0, 0, 0]); data.extend_from_slice(&2u32.to_be_bytes());
1433 data.extend_from_slice(&4u32.to_be_bytes());
1435 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());
1442 data.extend_from_slice(b"vals");
1443 data.extend_from_slice(&1u32.to_be_bytes());
1445 data.extend_from_slice(&0u32.to_be_bytes());
1447 data.extend_from_slice(&0u32.to_be_bytes());
1449 data.extend_from_slice(&0u32.to_be_bytes());
1450 data.extend_from_slice(&5u32.to_be_bytes());
1452 data.extend_from_slice(&12u32.to_be_bytes());
1454 let data_offset = data.len() as u32 + 4; data.extend_from_slice(&data_offset.to_be_bytes());
1457
1458 data.extend_from_slice(&1.5f32.to_be_bytes());
1460 data.extend_from_slice(&2.5f32.to_be_bytes());
1461 data.extend_from_slice(&3.5f32.to_be_bytes());
1462
1463 let file = NcFile::from_bytes(&data).unwrap();
1464 assert_eq!(file.format(), NcFormat::Classic);
1465 assert_eq!(file.dimensions().unwrap().len(), 1);
1466 assert_eq!(file.dimensions().unwrap()[0].name, "x");
1467 assert_eq!(file.dimensions().unwrap()[0].size, 3);
1468
1469 assert_eq!(file.global_attributes().unwrap().len(), 1);
1470 assert_eq!(file.global_attributes().unwrap()[0].name, "title");
1471 assert_eq!(
1472 file.global_attributes().unwrap()[0]
1473 .value
1474 .as_string()
1475 .unwrap(),
1476 "test"
1477 );
1478
1479 assert_eq!(file.variables().unwrap().len(), 1);
1480 let var = file.variable("vals").unwrap();
1481 assert_eq!(var.dtype(), &NcType::Float);
1482 assert_eq!(var.shape(), vec![3]);
1483
1484 let classic = file.as_classic().unwrap();
1486 let arr: ndarray::ArrayD<f32> = classic.read_variable("vals").unwrap();
1487 assert_eq!(arr.shape(), &[3]);
1488 assert_eq!(arr[[0]], 1.5f32);
1489 assert_eq!(arr[[1]], 2.5f32);
1490 assert_eq!(arr[[2]], 3.5f32);
1491 }
1492
1493 #[test]
1494 fn variable_not_found() {
1495 let mut data = Vec::new();
1496 data.extend_from_slice(b"CDF\x01");
1497 data.extend_from_slice(&0u32.to_be_bytes());
1498 data.extend_from_slice(&0u32.to_be_bytes());
1500 data.extend_from_slice(&0u32.to_be_bytes());
1501 data.extend_from_slice(&0u32.to_be_bytes());
1502 data.extend_from_slice(&0u32.to_be_bytes());
1503 data.extend_from_slice(&0u32.to_be_bytes());
1504 data.extend_from_slice(&0u32.to_be_bytes());
1505
1506 let file = NcFile::from_bytes(&data).unwrap();
1507 assert!(matches!(
1508 file.variable("nonexistent").unwrap_err(),
1509 Error::VariableNotFound(_)
1510 ));
1511 }
1512
1513 #[test]
1514 fn group_not_found() {
1515 let mut data = Vec::new();
1516 data.extend_from_slice(b"CDF\x01");
1517 data.extend_from_slice(&0u32.to_be_bytes());
1518 data.extend_from_slice(&0u32.to_be_bytes());
1519 data.extend_from_slice(&0u32.to_be_bytes());
1520 data.extend_from_slice(&0u32.to_be_bytes());
1521 data.extend_from_slice(&0u32.to_be_bytes());
1522 data.extend_from_slice(&0u32.to_be_bytes());
1523 data.extend_from_slice(&0u32.to_be_bytes());
1524
1525 let file = NcFile::from_bytes(&data).unwrap();
1526 assert!(matches!(
1527 file.group("nonexistent").unwrap_err(),
1528 Error::GroupNotFound(_)
1529 ));
1530 }
1531}