Skip to main content

netcdf_reader/classic/
mod.rs

1//! Classic (CDF-1/2/5) NetCDF file format support.
2//!
3//! This module handles the original NetCDF binary format (CDF-1 classic, CDF-2
4//! 64-bit offset, and CDF-5 64-bit data). All multi-byte values are big-endian.
5
6pub mod data;
7pub mod header;
8pub(crate) mod storage;
9pub mod types;
10pub mod variable;
11
12use std::fs::File;
13use std::path::Path;
14
15use memmap2::Mmap;
16
17use crate::error::Result;
18use crate::types::NcGroup;
19use crate::NcFormat;
20
21use storage::ClassicStorage;
22
23/// An opened classic-format NetCDF file (CDF-1, CDF-2, or CDF-5).
24pub struct ClassicFile {
25    pub(crate) format: NcFormat,
26    pub(crate) root_group: NcGroup,
27    pub(crate) storage: ClassicStorage,
28    pub(crate) numrecs: u64,
29}
30
31impl ClassicFile {
32    /// Open a classic NetCDF file from disk using memory-mapping.
33    pub fn open(path: &Path, format: NcFormat) -> Result<Self> {
34        let file = File::open(path)?;
35        // SAFETY: read-only mapping; caller must not modify the file concurrently.
36        let mmap = unsafe { Mmap::map(&file)? };
37        let header = header::parse_header(&mmap, format)?;
38        reject_unsupported_classic_features(&header)?;
39        let storage = ClassicStorage::from_mmap(mmap);
40
41        let root_group = NcGroup {
42            name: "/".to_string(),
43            dimensions: header.dimensions,
44            variables: header.variables,
45            attributes: header.global_attributes,
46            groups: Vec::new(), // Classic format has no sub-groups.
47        };
48
49        Ok(ClassicFile {
50            format,
51            root_group,
52            storage,
53            numrecs: header.numrecs,
54        })
55    }
56
57    /// Open a classic NetCDF file from in-memory bytes.
58    pub fn from_bytes(bytes: &[u8], format: NcFormat) -> Result<Self> {
59        let header = header::parse_header(bytes, format)?;
60        reject_unsupported_classic_features(&header)?;
61        let storage = ClassicStorage::from_bytes(bytes.to_vec());
62
63        let root_group = NcGroup {
64            name: "/".to_string(),
65            dimensions: header.dimensions,
66            variables: header.variables,
67            attributes: header.global_attributes,
68            groups: Vec::new(),
69        };
70
71        Ok(ClassicFile {
72            format,
73            root_group,
74            storage,
75            numrecs: header.numrecs,
76        })
77    }
78
79    /// Open a classic NetCDF file from an existing memory map (avoids double mmap).
80    pub fn from_mmap(mmap: Mmap, format: NcFormat) -> Result<Self> {
81        let header = header::parse_header(&mmap, format)?;
82        reject_unsupported_classic_features(&header)?;
83        let storage = ClassicStorage::from_mmap(mmap);
84
85        let root_group = NcGroup {
86            name: "/".to_string(),
87            dimensions: header.dimensions,
88            variables: header.variables,
89            attributes: header.global_attributes,
90            groups: Vec::new(),
91        };
92
93        Ok(ClassicFile {
94            format,
95            root_group,
96            storage,
97            numrecs: header.numrecs,
98        })
99    }
100
101    /// Open a classic NetCDF file from a random-access storage backend.
102    #[cfg(feature = "netcdf4")]
103    pub fn from_storage(
104        storage: hdf5_reader::storage::DynStorage,
105        format: NcFormat,
106    ) -> Result<Self> {
107        let storage = ClassicStorage::from_range(storage);
108        let header = parse_header_from_storage(&storage, format)?;
109        reject_unsupported_classic_features(&header)?;
110
111        let root_group = NcGroup {
112            name: "/".to_string(),
113            dimensions: header.dimensions,
114            variables: header.variables,
115            attributes: header.global_attributes,
116            groups: Vec::new(),
117        };
118
119        Ok(ClassicFile {
120            format,
121            root_group,
122            storage,
123            numrecs: header.numrecs,
124        })
125    }
126
127    /// The file format (Classic, Offset64, or Cdf5).
128    pub fn format(&self) -> NcFormat {
129        self.format
130    }
131
132    /// The root group containing all dimensions, variables, and global attributes.
133    pub fn root_group(&self) -> &NcGroup {
134        &self.root_group
135    }
136
137    /// Number of records in the unlimited dimension.
138    pub fn numrecs(&self) -> u64 {
139        self.numrecs
140    }
141}
142
143fn reject_unsupported_classic_features(header: &header::ClassicHeader) -> Result<()> {
144    let has_subfiling_marker = header
145        .global_attributes
146        .iter()
147        .any(|attr| is_subfiling_attribute_name(&attr.name))
148        || header.variables.iter().any(|var| {
149            var.attributes
150                .iter()
151                .any(|attr| is_subfiling_attribute_name(&attr.name))
152        });
153
154    if has_subfiling_marker {
155        return Err(crate::Error::UnsupportedFeature(
156            "PnetCDF subfiling datasets require a virtual multi-file storage adapter".to_string(),
157        ));
158    }
159
160    Ok(())
161}
162
163fn is_subfiling_attribute_name(name: &str) -> bool {
164    let lower = name.to_ascii_lowercase();
165    lower.starts_with("_pnetcdf_subfiling") || lower.starts_with("subfiling")
166}
167
168#[cfg(feature = "netcdf4")]
169fn parse_header_from_storage(
170    storage: &ClassicStorage,
171    format: NcFormat,
172) -> Result<header::ClassicHeader> {
173    let mut len = storage.initial_header_len();
174
175    loop {
176        let prefix = storage.read_header_prefix(len)?;
177        match header::parse_header(prefix.as_ref(), format) {
178            Ok(header) => return Ok(header),
179            Err(crate::Error::UnexpectedEof { .. }) if (prefix.len() as u64) < storage.len() => {
180                let current = prefix.len().max(1);
181                len = current.saturating_mul(2);
182            }
183            Err(err) => return Err(err),
184        }
185    }
186}