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 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    }
129
130    let numrecs = header.numrecs;
131    let root_group = NcGroup {
132        name: "/".to_string(),
133        dimensions: header.dimensions,
134        variables: header.variables,
135        attributes: header.global_attributes,
136        groups: Vec::new(), // Classic format has no sub-groups.
137    };
138
139    Ok((root_group, numrecs))
140}
141
142fn infer_streaming_numrecs(header: &header::ClassicHeader, storage_len: u64) -> Result<u64> {
143    let Some(record_data_start) = header
144        .variables
145        .iter()
146        .filter(|var| var.is_record_var)
147        .map(|var| var.data_offset)
148        .min()
149    else {
150        return Ok(0);
151    };
152
153    let record_stride = data::compute_record_stride(&header.variables)?;
154    if record_stride == 0 || storage_len <= record_data_start {
155        return Ok(0);
156    }
157
158    Ok((storage_len - record_data_start) / record_stride)
159}
160
161fn reject_unsupported_classic_features(header: &header::ClassicHeader) -> Result<()> {
162    let has_subfiling_marker = header
163        .global_attributes
164        .iter()
165        .any(|attr| is_subfiling_attribute_name(&attr.name))
166        || header.variables.iter().any(|var| {
167            var.attributes
168                .iter()
169                .any(|attr| is_subfiling_attribute_name(&attr.name))
170        });
171
172    if has_subfiling_marker {
173        return Err(crate::Error::UnsupportedFeature(
174            "PnetCDF subfiling datasets require a virtual multi-file storage adapter".to_string(),
175        ));
176    }
177
178    Ok(())
179}
180
181fn is_subfiling_attribute_name(name: &str) -> bool {
182    let lower = name.to_ascii_lowercase();
183    lower.starts_with("_pnetcdf_subfiling") || lower.starts_with("subfiling")
184}
185
186#[cfg(feature = "netcdf4")]
187fn parse_header_from_storage(
188    storage: &ClassicStorage,
189    format: NcFormat,
190) -> Result<(header::ClassicHeader, bool)> {
191    let mut len = storage.initial_header_len();
192
193    loop {
194        let prefix = storage.read_header_prefix(len)?;
195        match header::parse_header(prefix.as_ref(), format) {
196            Ok(header) => {
197                let is_streaming = header::has_streaming_numrecs(prefix.as_ref(), format);
198                return Ok((header, is_streaming));
199            }
200            Err(crate::Error::UnexpectedEof { .. }) if (prefix.len() as u64) < storage.len() => {
201                let current = prefix.len().max(1);
202                len = current.saturating_mul(2);
203            }
204            Err(err) => return Err(err),
205        }
206    }
207}