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::{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 is_streaming = header::has_streaming_numrecs(&mmap, format);
38        let header = header::parse_header(&mmap, format)?;
39        let storage = ClassicStorage::from_mmap(mmap);
40        let (root_group, numrecs) = finalize_header(header, storage.len(), is_streaming)?;
41
42        Ok(ClassicFile {
43            format,
44            root_group,
45            storage,
46            numrecs,
47        })
48    }
49
50    /// Open a classic NetCDF file from in-memory bytes.
51    pub fn from_bytes(bytes: &[u8], format: NcFormat) -> Result<Self> {
52        let is_streaming = header::has_streaming_numrecs(bytes, format);
53        let header = header::parse_header(bytes, format)?;
54        let storage = ClassicStorage::from_bytes(bytes.to_vec());
55        let (root_group, numrecs) = finalize_header(header, storage.len(), is_streaming)?;
56
57        Ok(ClassicFile {
58            format,
59            root_group,
60            storage,
61            numrecs,
62        })
63    }
64
65    /// Open a classic NetCDF file from an existing memory map (avoids double mmap).
66    pub fn from_mmap(mmap: Mmap, format: NcFormat) -> Result<Self> {
67        let is_streaming = header::has_streaming_numrecs(&mmap, format);
68        let header = header::parse_header(&mmap, format)?;
69        let storage = ClassicStorage::from_mmap(mmap);
70        let (root_group, numrecs) = finalize_header(header, storage.len(), is_streaming)?;
71
72        Ok(ClassicFile {
73            format,
74            root_group,
75            storage,
76            numrecs,
77        })
78    }
79
80    /// Open a classic NetCDF file from a random-access storage backend.
81    #[cfg(feature = "netcdf4")]
82    pub fn from_storage(
83        storage: hdf5_reader::storage::DynStorage,
84        format: NcFormat,
85    ) -> Result<Self> {
86        let storage = ClassicStorage::from_range(storage);
87        let (header, is_streaming) = parse_header_from_storage(&storage, format)?;
88        let (root_group, numrecs) = finalize_header(header, storage.len(), is_streaming)?;
89
90        Ok(ClassicFile {
91            format,
92            root_group,
93            storage,
94            numrecs,
95        })
96    }
97
98    /// The file format (Classic, Offset64, or Cdf5).
99    pub fn format(&self) -> NcFormat {
100        self.format
101    }
102
103    /// The root group containing all dimensions, variables, and global attributes.
104    pub fn root_group(&self) -> &NcGroup {
105        &self.root_group
106    }
107
108    /// Number of records in the unlimited dimension.
109    pub fn numrecs(&self) -> u64 {
110        self.numrecs
111    }
112}
113
114fn finalize_header(
115    mut header: header::ClassicHeader,
116    storage_len: u64,
117    is_streaming: bool,
118) -> Result<(NcGroup, u64)> {
119    reject_unsupported_classic_features(&header)?;
120
121    if is_streaming {
122        header.numrecs = infer_streaming_numrecs(&header, storage_len)?;
123        header::apply_unlimited_dimension_size(
124            &mut header.dimensions,
125            &mut header.variables,
126            header.numrecs,
127        );
128    } else {
129        validate_record_extent(&header, storage_len)?;
130    }
131
132    let numrecs = header.numrecs;
133    let root_group = NcGroup {
134        name: "/".to_string(),
135        dimensions: header.dimensions,
136        variables: header.variables,
137        attributes: header.global_attributes,
138        groups: Vec::new(), // Classic format has no sub-groups.
139    };
140
141    Ok((root_group, numrecs))
142}
143
144/// Reject headers whose declared record count cannot fit in the file, so a
145/// tiny crafted header cannot drive huge record-buffer allocations later.
146fn validate_record_extent(header: &header::ClassicHeader, storage_len: u64) -> Result<u64> {
147    let Some(record_data_start) = header
148        .variables
149        .iter()
150        .filter(|var| var.is_record_var)
151        .map(|var| var.data_offset)
152        .min()
153    else {
154        return Ok(0);
155    };
156
157    let record_stride = data::compute_record_stride(&header.variables)?;
158    let record_bytes = header
159        .numrecs
160        .checked_mul(record_stride)
161        .and_then(|bytes| bytes.checked_add(record_data_start))
162        .ok_or_else(|| {
163            Error::InvalidData("classic record data extent overflows u64".to_string())
164        })?;
165    if record_bytes > storage_len {
166        return Err(Error::InvalidData(format!(
167            "classic header declares {} records ({record_bytes} bytes) but the file has only \
168             {storage_len} bytes",
169            header.numrecs
170        )));
171    }
172    Ok(record_bytes)
173}
174
175fn infer_streaming_numrecs(header: &header::ClassicHeader, storage_len: u64) -> Result<u64> {
176    let Some(record_data_start) = header
177        .variables
178        .iter()
179        .filter(|var| var.is_record_var)
180        .map(|var| var.data_offset)
181        .min()
182    else {
183        return Ok(0);
184    };
185
186    let record_stride = data::compute_record_stride(&header.variables)?;
187    if record_stride == 0 || storage_len <= record_data_start {
188        return Ok(0);
189    }
190
191    Ok((storage_len - record_data_start) / record_stride)
192}
193
194fn reject_unsupported_classic_features(header: &header::ClassicHeader) -> Result<()> {
195    let has_subfiling_marker = header
196        .global_attributes
197        .iter()
198        .any(|attr| is_subfiling_attribute_name(&attr.name))
199        || header.variables.iter().any(|var| {
200            var.attributes
201                .iter()
202                .any(|attr| is_subfiling_attribute_name(&attr.name))
203        });
204
205    if has_subfiling_marker {
206        return Err(crate::Error::UnsupportedFeature(
207            "PnetCDF subfiling datasets require a virtual multi-file storage adapter".to_string(),
208        ));
209    }
210
211    Ok(())
212}
213
214fn is_subfiling_attribute_name(name: &str) -> bool {
215    let lower = name.to_ascii_lowercase();
216    lower.starts_with("_pnetcdf_subfiling") || lower.starts_with("subfiling")
217}
218
219#[cfg(feature = "netcdf4")]
220fn parse_header_from_storage(
221    storage: &ClassicStorage,
222    format: NcFormat,
223) -> Result<(header::ClassicHeader, bool)> {
224    let mut len = storage.initial_header_len();
225
226    loop {
227        let prefix = storage.read_header_prefix(len)?;
228        match header::parse_header(prefix.as_ref(), format) {
229            Ok(header) => {
230                let is_streaming = header::has_streaming_numrecs(prefix.as_ref(), format);
231                return Ok((header, is_streaming));
232            }
233            Err(crate::Error::UnexpectedEof { .. }) if (prefix.len() as u64) < storage.len() => {
234                let current = prefix.len().max(1);
235                len = current.saturating_mul(2);
236            }
237            Err(err) => return Err(err),
238        }
239    }
240}