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::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 }
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(), };
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}