1pub mod attributes;
12pub mod dimensions;
13pub mod groups;
14pub mod types;
15pub mod variables;
16
17use std::path::Path;
18use std::sync::OnceLock;
19
20use hdf5_reader::datatype_api::H5Type;
21use hdf5_reader::storage::DynStorage;
22use hdf5_reader::Hdf5File;
23use ndarray::ArrayD;
24#[cfg(feature = "rayon")]
25use rayon::ThreadPool;
26
27use crate::error::{Error, Result};
28use crate::types::{checked_shape_elements, checked_usize_from_u64, NcGroup, NcType};
29
30macro_rules! dispatch_read_as_f64 {
41 ($dtype:expr, |$T:ident| $read_expr:expr) => {{
42 use crate::types::NcType;
43 match $dtype {
44 NcType::Byte => {
45 type $T = i8;
46 let arr = $read_expr?;
47 Ok(arr.mapv(|v| v as f64))
48 }
49 NcType::Short => {
50 type $T = i16;
51 let arr = $read_expr?;
52 Ok(arr.mapv(|v| v as f64))
53 }
54 NcType::Int => {
55 type $T = i32;
56 let arr = $read_expr?;
57 Ok(arr.mapv(|v| v as f64))
58 }
59 NcType::Float => {
60 type $T = f32;
61 let arr = $read_expr?;
62 Ok(arr.mapv(|v| v as f64))
63 }
64 NcType::Double => {
65 type $T = f64;
66 Ok($read_expr?)
67 }
68 NcType::UByte => {
69 type $T = u8;
70 let arr = $read_expr?;
71 Ok(arr.mapv(|v| v as f64))
72 }
73 NcType::UShort => {
74 type $T = u16;
75 let arr = $read_expr?;
76 Ok(arr.mapv(|v| v as f64))
77 }
78 NcType::UInt => {
79 type $T = u32;
80 let arr = $read_expr?;
81 Ok(arr.mapv(|v| v as f64))
82 }
83 NcType::Int64 => {
84 type $T = i64;
85 let arr = $read_expr?;
86 Ok(arr.mapv(|v| v as f64))
87 }
88 NcType::UInt64 => {
89 type $T = u64;
90 let arr = $read_expr?;
91 Ok(arr.mapv(|v| v as f64))
92 }
93 NcType::Char => Err(Error::TypeMismatch {
94 expected: "numeric type".to_string(),
95 actual: "Char".to_string(),
96 }),
97 NcType::String => Err(Error::TypeMismatch {
98 expected: "numeric type".to_string(),
99 actual: "String".to_string(),
100 }),
101 other => Err(Error::TypeMismatch {
102 expected: "numeric type".to_string(),
103 actual: format!("{:?}", other),
104 }),
105 }
106 }};
107}
108
109pub struct Nc4File {
111 hdf5: Hdf5File,
112 metadata_mode: crate::NcMetadataMode,
113 root_metadata: OnceLock<NcGroup>,
114 metadata_tree: OnceLock<NcGroup>,
115}
116
117impl Nc4File {
118 pub(crate) fn from_hdf5(hdf5: Hdf5File, metadata_mode: crate::NcMetadataMode) -> Result<Self> {
119 Ok(Nc4File {
120 hdf5,
121 metadata_mode,
122 root_metadata: OnceLock::new(),
123 metadata_tree: OnceLock::new(),
124 })
125 }
126
127 pub fn open(path: &Path) -> Result<Self> {
129 Self::open_with_options(path, crate::NcOpenOptions::default())
130 }
131
132 pub fn open_with_options(path: &Path, options: crate::NcOpenOptions) -> Result<Self> {
134 let metadata_mode = options.metadata_mode;
135 let hdf5 = Hdf5File::open_with_options(
136 path,
137 hdf5_reader::OpenOptions {
138 chunk_cache_bytes: options.chunk_cache_bytes,
139 chunk_cache_slots: options.chunk_cache_slots,
140 filter_registry: options.filter_registry,
141 external_file_resolver: options.external_file_resolver,
142 external_link_resolver: options.external_link_resolver,
143 },
144 )?;
145 Nc4File::from_hdf5(hdf5, metadata_mode)
146 }
147
148 pub fn from_bytes(data: &[u8]) -> Result<Self> {
150 Self::from_bytes_with_options(data, crate::NcOpenOptions::default())
151 }
152
153 pub fn from_bytes_with_options(data: &[u8], options: crate::NcOpenOptions) -> Result<Self> {
155 let metadata_mode = options.metadata_mode;
156 let hdf5 = Hdf5File::from_bytes_with_options(
157 data,
158 hdf5_reader::OpenOptions {
159 chunk_cache_bytes: options.chunk_cache_bytes,
160 chunk_cache_slots: options.chunk_cache_slots,
161 filter_registry: options.filter_registry,
162 external_file_resolver: options.external_file_resolver,
163 external_link_resolver: options.external_link_resolver,
164 },
165 )?;
166 Nc4File::from_hdf5(hdf5, metadata_mode)
167 }
168
169 pub fn from_storage(storage: DynStorage) -> Result<Self> {
171 Self::from_storage_with_options(storage, crate::NcOpenOptions::default())
172 }
173
174 pub fn from_storage_with_options(
176 storage: DynStorage,
177 options: crate::NcOpenOptions,
178 ) -> Result<Self> {
179 let metadata_mode = options.metadata_mode;
180 let hdf5 = Hdf5File::from_storage_with_options(
181 storage,
182 hdf5_reader::OpenOptions {
183 chunk_cache_bytes: options.chunk_cache_bytes,
184 chunk_cache_slots: options.chunk_cache_slots,
185 filter_registry: options.filter_registry,
186 external_file_resolver: options.external_file_resolver,
187 external_link_resolver: options.external_link_resolver,
188 },
189 )?;
190 Nc4File::from_hdf5(hdf5, metadata_mode)
191 }
192
193 pub fn root_group(&self) -> Result<&NcGroup> {
195 if let Some(group) = self.metadata_tree.get() {
196 return Ok(group);
197 }
198 let metadata_tree = groups::build_root_group(&self.hdf5, self.metadata_mode)?;
199 let _ = self.metadata_tree.set(metadata_tree);
200 Ok(self
201 .metadata_tree
202 .get()
203 .expect("metadata tree must be initialized after successful build"))
204 }
205
206 pub fn is_classic_model(&self) -> bool {
211 self.hdf5
212 .root_group()
213 .ok()
214 .and_then(|g| g.attribute("_nc3_strict").ok())
215 .is_some()
216 }
217
218 pub fn dimensions(&self) -> Result<&[crate::types::NcDimension]> {
219 Ok(&self.root_metadata()?.dimensions)
220 }
221
222 pub fn variables(&self) -> Result<&[crate::types::NcVariable]> {
223 Ok(&self.root_metadata()?.variables)
224 }
225
226 pub fn global_attributes(&self) -> Result<&[crate::types::NcAttribute]> {
227 Ok(&self.root_metadata()?.attributes)
228 }
229
230 pub fn group(&self, path: &str) -> Result<&NcGroup> {
231 let normalized = normalize_group_path(path)?;
232 let root = self.root_group()?;
233 if normalized.is_empty() {
234 return Ok(root);
235 }
236 root.group(normalized)
237 .ok_or_else(|| Error::GroupNotFound(path.to_string()))
238 }
239
240 pub fn variable(&self, path: &str) -> Result<&crate::types::NcVariable> {
241 self.root_group()?
242 .variable(path)
243 .ok_or_else(|| Error::VariableNotFound(path.to_string()))
244 }
245
246 pub fn dimension(&self, path: &str) -> Result<&crate::types::NcDimension> {
247 self.root_group()?
248 .dimension(path)
249 .ok_or_else(|| Error::DimensionNotFound(path.to_string()))
250 }
251
252 pub fn global_attribute(&self, path: &str) -> Result<&crate::types::NcAttribute> {
253 self.root_group()?
254 .attribute(path)
255 .ok_or_else(|| Error::AttributeNotFound(path.to_string()))
256 }
257
258 pub fn read_variable<T: H5Type>(&self, path: &str) -> Result<ArrayD<T>> {
263 let normalized = normalize_dataset_path(path)?;
264 let dataset = self.hdf5.dataset(normalized)?;
265 Ok(dataset.read_array::<T>()?)
266 }
267
268 pub fn read_variable_into<T: H5Type>(&self, path: &str, dst: &mut [T]) -> Result<()> {
270 let normalized = normalize_dataset_path(path)?;
271 let dataset = self.hdf5.dataset(normalized)?;
272 Ok(dataset.read_into::<T>(dst)?)
273 }
274
275 pub fn read_variable_raw_bytes(&self, path: &str) -> Result<Vec<u8>> {
277 let normalized = normalize_dataset_path(path)?;
278 let dataset = self.hdf5.dataset(normalized)?;
279 Ok(dataset.read_raw_bytes()?)
280 }
281
282 pub fn read_variable_raw_bytes_into(&self, path: &str, dst: &mut [u8]) -> Result<()> {
284 let normalized = normalize_dataset_path(path)?;
285 let dataset = self.hdf5.dataset(normalized)?;
286 Ok(dataset.read_raw_bytes_into(dst)?)
287 }
288
289 pub fn read_variable_native_bytes(&self, path: &str) -> Result<Vec<u8>> {
291 let normalized = normalize_dataset_path(path)?;
292 let dataset = self.hdf5.dataset(normalized)?;
293 Ok(dataset.read_native_bytes()?)
294 }
295
296 pub fn read_variable_native_bytes_into(&self, path: &str, dst: &mut [u8]) -> Result<()> {
298 let normalized = normalize_dataset_path(path)?;
299 let dataset = self.hdf5.dataset(normalized)?;
300 Ok(dataset.read_native_bytes_into(dst)?)
301 }
302
303 pub fn iter_variable_chunks(&self, path: &str) -> Result<hdf5_reader::DatasetChunkIterator> {
305 let normalized = normalize_dataset_path(path)?;
306 let dataset = self.hdf5.dataset(normalized)?;
307 Ok(dataset.iter_chunks()?)
308 }
309
310 pub fn chunk_cache_stats(&self) -> hdf5_reader::ChunkCacheStats {
312 self.hdf5.chunk_cache_stats()
313 }
314
315 pub fn read_variable_as_string(&self, path: &str) -> Result<String> {
317 let mut strings = self.read_variable_as_strings(path)?;
318 match strings.len() {
319 1 => Ok(strings.swap_remove(0)),
320 0 => Err(Error::InvalidData(format!(
321 "variable '{}' contains no string elements",
322 path
323 ))),
324 count => Err(Error::InvalidData(format!(
325 "variable '{}' contains {count} string elements; use read_variable_as_strings()",
326 path
327 ))),
328 }
329 }
330
331 pub fn read_variable_as_strings(&self, path: &str) -> Result<Vec<String>> {
333 let normalized = normalize_dataset_path(path)?;
334 let dataset = self.hdf5.dataset(normalized)?;
335 let dtype = dataset_nc_type(&dataset)?;
336 match dtype {
337 NcType::String => Ok(dataset.read_strings()?),
338 NcType::Char => {
339 let variable = self.variable(path)?;
340 let raw = dataset.read_raw_bytes()?;
341 decode_char_variable_strings(variable, &raw)
342 }
343 _ => Err(Error::TypeMismatch {
344 expected: "String or Char".to_string(),
345 actual: format!("{dtype:?}"),
346 }),
347 }
348 }
349
350 pub fn read_variable_user_defined(
352 &self,
353 path: &str,
354 ) -> Result<ArrayD<crate::user_defined::NcValue>> {
355 let normalized = normalize_dataset_path(path)?;
356 let dataset = self.hdf5.dataset(normalized)?;
357 crate::user_defined::read_dataset_values(&dataset)
358 }
359
360 pub fn read_variable_user_defined_with<T, F>(&self, path: &str, decoder: F) -> Result<ArrayD<T>>
362 where
363 F: FnMut(crate::user_defined::NcValueView<'_>) -> Result<T>,
364 {
365 let normalized = normalize_dataset_path(path)?;
366 let dataset = self.hdf5.dataset(normalized)?;
367 crate::user_defined::read_dataset_with_decoder(&dataset, decoder)
368 }
369
370 #[cfg(feature = "rayon")]
371 pub fn read_variable_parallel<T: H5Type>(&self, path: &str) -> Result<ArrayD<T>> {
372 let normalized = normalize_dataset_path(path)?;
373 let dataset = self.hdf5.dataset(normalized)?;
374 Ok(dataset.read_array_parallel::<T>()?)
375 }
376
377 #[cfg(feature = "rayon")]
378 pub fn read_variable_in_pool<T: H5Type>(
379 &self,
380 path: &str,
381 pool: &ThreadPool,
382 ) -> Result<ArrayD<T>> {
383 let normalized = normalize_dataset_path(path)?;
384 let dataset = self.hdf5.dataset(normalized)?;
385 Ok(dataset.read_array_in_pool::<T>(pool)?)
386 }
387}
388
389fn decode_char_variable_strings(
390 variable: &crate::types::NcVariable,
391 bytes: &[u8],
392) -> Result<Vec<String>> {
393 let shape = variable.shape();
394 if shape.len() <= 1 {
395 return Ok(vec![decode_char_string(bytes)]);
396 }
397
398 let string_len = checked_usize_from_u64(
399 *shape
400 .last()
401 .ok_or_else(|| Error::InvalidData("char variable missing string axis".into()))?,
402 "char string length",
403 )?;
404 let string_count_u64 = checked_shape_elements(&shape[..shape.len() - 1], "char string count")?;
405 let string_count = checked_usize_from_u64(string_count_u64, "char string count")?;
406 let expected_bytes = string_count.checked_mul(string_len).ok_or_else(|| {
407 Error::InvalidData("char string byte count exceeds platform usize".to_string())
408 })?;
409
410 if bytes.len() < expected_bytes {
411 return Err(Error::InvalidData(format!(
412 "char variable '{}' data too short: need {} bytes, have {}",
413 variable.name,
414 expected_bytes,
415 bytes.len()
416 )));
417 }
418
419 if string_len == 0 {
420 return Ok(vec![String::new(); string_count]);
421 }
422
423 Ok(bytes[..expected_bytes]
424 .chunks_exact(string_len)
425 .map(decode_char_string)
426 .collect())
427}
428
429fn decode_char_string(bytes: &[u8]) -> String {
430 String::from_utf8_lossy(bytes)
431 .trim_end_matches('\0')
432 .to_string()
433}
434
435impl Nc4File {
436 fn root_metadata(&self) -> Result<&NcGroup> {
437 if let Some(group) = self.root_metadata.get() {
438 return Ok(group);
439 }
440 let root_metadata = groups::build_root_group_metadata(&self.hdf5, self.metadata_mode)?;
441 let _ = self.root_metadata.set(root_metadata);
442 Ok(self
443 .root_metadata
444 .get()
445 .expect("root metadata must be initialized after successful build"))
446 }
447}
448
449impl Nc4File {
450 pub fn read_variable_as_f64(&self, path: &str) -> Result<ArrayD<f64>> {
454 let normalized = normalize_dataset_path(path)?;
455 let dataset = self.hdf5.dataset(normalized)?;
456 let dtype = dataset_nc_type(&dataset)?;
457 dispatch_read_as_f64!(&dtype, |T| dataset.read_array::<T>())
458 }
459
460 pub fn read_variable_slice_as_f64(
462 &self,
463 path: &str,
464 selection: &crate::types::NcSliceInfo,
465 ) -> Result<ArrayD<f64>> {
466 let normalized = normalize_dataset_path(path)?;
467 let dataset = self.hdf5.dataset(normalized)?;
468 let hdf5_sel = to_hdf5_slice_info(selection);
469 let dtype = dataset_nc_type(&dataset)?;
470 dispatch_read_as_f64!(&dtype, |T| dataset.read_slice::<T>(&hdf5_sel))
471 }
472
473 pub fn read_variable_slice<T: H5Type>(
475 &self,
476 path: &str,
477 selection: &crate::types::NcSliceInfo,
478 ) -> Result<ArrayD<T>> {
479 let normalized = normalize_dataset_path(path)?;
480 let dataset = self.hdf5.dataset(normalized)?;
481 let hdf5_sel = to_hdf5_slice_info(selection);
482 Ok(dataset.read_slice::<T>(&hdf5_sel)?)
483 }
484
485 #[cfg(feature = "rayon")]
490 pub fn read_variable_slice_parallel<T: H5Type>(
491 &self,
492 path: &str,
493 selection: &crate::types::NcSliceInfo,
494 ) -> Result<ArrayD<T>> {
495 let normalized = normalize_dataset_path(path)?;
496 let dataset = self.hdf5.dataset(normalized)?;
497 let hdf5_sel = to_hdf5_slice_info(selection);
498 Ok(dataset.read_slice_parallel::<T>(&hdf5_sel)?)
499 }
500}
501
502fn to_hdf5_slice_info(selection: &crate::types::NcSliceInfo) -> hdf5_reader::SliceInfo {
503 hdf5_reader::SliceInfo {
504 selections: selection
505 .selections
506 .iter()
507 .map(|s| match s {
508 crate::types::NcSliceInfoElem::Index(idx) => {
509 hdf5_reader::SliceInfoElem::Index(*idx)
510 }
511 crate::types::NcSliceInfoElem::Slice { start, end, step } => {
512 hdf5_reader::SliceInfoElem::Slice {
513 start: *start,
514 end: *end,
515 step: *step,
516 }
517 }
518 })
519 .collect(),
520 }
521}
522
523fn normalize_dataset_path(path: &str) -> Result<&str> {
524 let trimmed = path.trim_matches('/');
525 if trimmed.is_empty() {
526 return Err(Error::VariableNotFound(path.to_string()));
527 }
528 Ok(trimmed)
529}
530
531fn normalize_group_path(path: &str) -> Result<&str> {
532 Ok(path.trim_matches('/'))
533}
534
535fn dataset_nc_type(dataset: &hdf5_reader::Dataset) -> Result<NcType> {
536 self::types::hdf5_to_nc_type(dataset.dtype()).map_err(|err| {
537 Error::InvalidData(format!(
538 "dataset '{}' cannot be mapped to a NetCDF-4 type: {err}",
539 dataset.name()
540 ))
541 })
542}