netcdf_reader/classic/
mod.rs1pub 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
23pub 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 pub fn open(path: &Path, format: NcFormat) -> Result<Self> {
34 let file = File::open(path)?;
35 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 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 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 #[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 pub fn format(&self) -> NcFormat {
100 self.format
101 }
102
103 pub fn root_group(&self) -> &NcGroup {
105 &self.root_group
106 }
107
108 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(), };
140
141 Ok((root_group, numrecs))
142}
143
144fn 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}