1use crate::error::{Error, Result};
2
3#[derive(Debug, Clone)]
5pub struct NcDimension {
6 pub name: String,
7 pub size: u64,
8 pub is_unlimited: bool,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct NcCompoundField {
14 pub name: String,
15 pub offset: u64,
16 pub dtype: NcType,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum NcIntegerValue {
22 I8(i8),
23 U8(u8),
24 I16(i16),
25 U16(u16),
26 I32(i32),
27 U32(u32),
28 I64(i64),
29 U64(u64),
30}
31
32impl NcIntegerValue {
33 pub fn as_i128(self) -> Option<i128> {
35 match self {
36 NcIntegerValue::I8(value) => Some(value as i128),
37 NcIntegerValue::U8(value) => Some(value as i128),
38 NcIntegerValue::I16(value) => Some(value as i128),
39 NcIntegerValue::U16(value) => Some(value as i128),
40 NcIntegerValue::I32(value) => Some(value as i128),
41 NcIntegerValue::U32(value) => Some(value as i128),
42 NcIntegerValue::I64(value) => Some(value as i128),
43 NcIntegerValue::U64(value) => Some(i128::from(value)),
44 }
45 }
46
47 pub fn as_u128(self) -> Option<u128> {
49 match self {
50 NcIntegerValue::I8(value) => u128::try_from(value).ok(),
51 NcIntegerValue::U8(value) => Some(value as u128),
52 NcIntegerValue::I16(value) => u128::try_from(value).ok(),
53 NcIntegerValue::U16(value) => Some(value as u128),
54 NcIntegerValue::I32(value) => u128::try_from(value).ok(),
55 NcIntegerValue::U32(value) => Some(value as u128),
56 NcIntegerValue::I64(value) => u128::try_from(value).ok(),
57 NcIntegerValue::U64(value) => Some(value as u128),
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct NcEnumMember {
65 pub name: String,
66 pub value: NcIntegerValue,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum NcType {
72 Byte,
74 Char,
76 Short,
78 Int,
80 Float,
82 Double,
84 UByte,
86 UShort,
88 UInt,
90 Int64,
92 UInt64,
94 String,
96 Enum {
98 base: Box<NcType>,
99 members: Vec<NcEnumMember>,
100 },
101 Compound {
103 size: u32,
104 fields: Vec<NcCompoundField>,
105 },
106 Opaque { size: u32, tag: String },
108 Array { base: Box<NcType>, dims: Vec<u64> },
110 VLen { base: Box<NcType> },
112}
113
114impl NcType {
115 pub fn size(&self) -> Result<usize> {
117 match self {
118 NcType::Byte | NcType::Char | NcType::UByte => Ok(1),
119 NcType::Short | NcType::UShort => Ok(2),
120 NcType::Int | NcType::UInt | NcType::Float => Ok(4),
121 NcType::Int64 | NcType::UInt64 | NcType::Double => Ok(8),
122 NcType::String => Ok(std::mem::size_of::<usize>()),
124 NcType::Enum { base, .. } => base.size(),
125 NcType::Compound { size, .. } => Ok(*size as usize),
126 NcType::Opaque { size, .. } => Ok(*size as usize),
127 NcType::Array { base, dims } => {
128 let base_size = base.size()?;
129 let count = dims.iter().try_fold(1usize, |acc, &dim| {
130 let dim = usize::try_from(dim).map_err(|_| {
131 Error::InvalidData(
132 "NetCDF array type dimension exceeds platform usize capacity"
133 .to_string(),
134 )
135 })?;
136 acc.checked_mul(dim).ok_or_else(|| {
137 Error::InvalidData(
138 "NetCDF array type element count exceeds platform usize capacity"
139 .to_string(),
140 )
141 })
142 })?;
143 base_size.checked_mul(count).ok_or_else(|| {
144 Error::InvalidData(
145 "NetCDF array type byte size exceeds platform usize capacity".to_string(),
146 )
147 })
148 }
149 NcType::VLen { .. } => Ok(std::mem::size_of::<usize>()), }
151 }
152
153 pub fn classic_type_code(&self) -> Option<u32> {
155 match self {
156 NcType::Byte => Some(1),
157 NcType::Char => Some(2),
158 NcType::Short => Some(3),
159 NcType::Int => Some(4),
160 NcType::Float => Some(5),
161 NcType::Double => Some(6),
162 NcType::UByte => Some(7),
163 NcType::UShort => Some(8),
164 NcType::UInt => Some(9),
165 NcType::Int64 => Some(10),
166 NcType::UInt64 => Some(11),
167 NcType::String
169 | NcType::Enum { .. }
170 | NcType::Compound { .. }
171 | NcType::Opaque { .. }
172 | NcType::Array { .. }
173 | NcType::VLen { .. } => None,
174 }
175 }
176
177 pub fn is_primitive(&self) -> bool {
179 matches!(
180 self,
181 NcType::Byte
182 | NcType::Char
183 | NcType::Short
184 | NcType::Int
185 | NcType::Float
186 | NcType::Double
187 | NcType::UByte
188 | NcType::UShort
189 | NcType::UInt
190 | NcType::Int64
191 | NcType::UInt64
192 | NcType::String
193 )
194 }
195}
196
197#[derive(Debug, Clone)]
199pub enum NcAttrValue {
200 Bytes(Vec<i8>),
201 Chars(String),
202 Shorts(Vec<i16>),
203 Ints(Vec<i32>),
204 Floats(Vec<f32>),
205 Doubles(Vec<f64>),
206 UBytes(Vec<u8>),
207 UShorts(Vec<u16>),
208 UInts(Vec<u32>),
209 Int64s(Vec<i64>),
210 UInt64s(Vec<u64>),
211 Strings(Vec<String>),
212}
213
214impl NcAttrValue {
215 pub fn as_string(&self) -> Option<String> {
217 match self {
218 NcAttrValue::Chars(s) => Some(s.clone()),
219 NcAttrValue::Strings(v) if v.len() == 1 => Some(v[0].clone()),
220 _ => None,
221 }
222 }
223
224 pub fn as_f64(&self) -> Option<f64> {
226 match self {
227 NcAttrValue::Bytes(v) => v.first().map(|&x| x as f64),
228 NcAttrValue::Shorts(v) => v.first().map(|&x| x as f64),
229 NcAttrValue::Ints(v) => v.first().map(|&x| x as f64),
230 NcAttrValue::Floats(v) => v.first().map(|&x| x as f64),
231 NcAttrValue::Doubles(v) => v.first().copied(),
232 NcAttrValue::UBytes(v) => v.first().map(|&x| x as f64),
233 NcAttrValue::UShorts(v) => v.first().map(|&x| x as f64),
234 NcAttrValue::UInts(v) => v.first().map(|&x| x as f64),
235 NcAttrValue::Int64s(v) => v.first().map(|&x| x as f64),
236 NcAttrValue::UInt64s(v) => v.first().map(|&x| x as f64),
237 NcAttrValue::Chars(_) | NcAttrValue::Strings(_) => None,
238 }
239 }
240
241 pub fn as_f64_vec(&self) -> Option<Vec<f64>> {
243 match self {
244 NcAttrValue::Bytes(v) => Some(v.iter().map(|&x| x as f64).collect()),
245 NcAttrValue::Shorts(v) => Some(v.iter().map(|&x| x as f64).collect()),
246 NcAttrValue::Ints(v) => Some(v.iter().map(|&x| x as f64).collect()),
247 NcAttrValue::Floats(v) => Some(v.iter().map(|&x| x as f64).collect()),
248 NcAttrValue::Doubles(v) => Some(v.clone()),
249 NcAttrValue::UBytes(v) => Some(v.iter().map(|&x| x as f64).collect()),
250 NcAttrValue::UShorts(v) => Some(v.iter().map(|&x| x as f64).collect()),
251 NcAttrValue::UInts(v) => Some(v.iter().map(|&x| x as f64).collect()),
252 NcAttrValue::Int64s(v) => Some(v.iter().map(|&x| x as f64).collect()),
253 NcAttrValue::UInt64s(v) => Some(v.iter().map(|&x| x as f64).collect()),
254 NcAttrValue::Chars(_) | NcAttrValue::Strings(_) => None,
255 }
256 }
257}
258
259#[derive(Debug, Clone)]
261pub struct NcAttribute {
262 pub name: String,
263 pub value: NcAttrValue,
264}
265
266#[derive(Debug, Clone)]
268pub struct NcVariable {
269 pub name: String,
270 pub dimensions: Vec<NcDimension>,
271 pub dtype: NcType,
272 pub attributes: Vec<NcAttribute>,
273 pub(crate) data_offset: u64,
276 pub(crate) _data_size: u64,
278 pub(crate) is_record_var: bool,
280 pub(crate) record_size: u64,
282}
283
284impl NcVariable {
285 pub fn name(&self) -> &str {
287 &self.name
288 }
289
290 pub fn dimensions(&self) -> &[NcDimension] {
292 &self.dimensions
293 }
294
295 pub fn coordinate_dimension(&self) -> Option<&NcDimension> {
301 match self.dimensions.as_slice() {
302 [dim] if dim.name == self.name => Some(dim),
303 _ => None,
304 }
305 }
306
307 pub fn is_coordinate_variable(&self) -> bool {
309 self.coordinate_dimension().is_some()
310 }
311
312 pub fn is_coordinate_variable_for(&self, dimension_name: &str) -> bool {
315 self.coordinate_dimension()
316 .is_some_and(|dim| dim.name == dimension_name)
317 }
318
319 pub fn dtype(&self) -> &NcType {
321 &self.dtype
322 }
323
324 pub fn shape(&self) -> Vec<u64> {
326 self.dimensions.iter().map(|d| d.size).collect()
327 }
328
329 pub fn attributes(&self) -> &[NcAttribute] {
331 &self.attributes
332 }
333
334 pub fn attribute(&self, name: &str) -> Option<&NcAttribute> {
336 self.attributes.iter().find(|a| a.name == name)
337 }
338
339 pub fn ndim(&self) -> usize {
341 self.dimensions.len()
342 }
343
344 pub fn num_elements(&self) -> Result<u64> {
346 match self.dimensions.as_slice() {
347 [] => Ok(1), [dim] => Ok(dim.size),
349 dimensions => {
350 let mut total = 1u64;
351 for dim in dimensions {
352 total = total.checked_mul(dim.size).ok_or_else(|| {
353 Error::InvalidData(
354 "NetCDF variable element count overflows u64".to_string(),
355 )
356 })?;
357 }
358 Ok(total)
359 }
360 }
361 }
362}
363
364#[derive(Debug, Clone)]
366pub struct NcGroup {
367 pub name: String,
368 pub dimensions: Vec<NcDimension>,
369 pub variables: Vec<NcVariable>,
370 pub attributes: Vec<NcAttribute>,
371 pub groups: Vec<NcGroup>,
372}
373
374impl NcGroup {
375 pub fn variable(&self, name: &str) -> Option<&NcVariable> {
377 let (group_path, variable_name) = split_parent_path(name)?;
378 let group = self.group(group_path)?;
379 group.variables.iter().find(|v| v.name == variable_name)
380 }
381
382 pub fn dimension(&self, name: &str) -> Option<&NcDimension> {
384 let (group_path, dimension_name) = split_parent_path(name)?;
385 let group = self.group(group_path)?;
386 group.dimensions.iter().find(|d| d.name == dimension_name)
387 }
388
389 pub fn coordinate_variable(&self, name: &str) -> Option<&NcVariable> {
394 let (group_path, dimension_name) = split_parent_path(name)?;
395 let group = self.group(group_path)?;
396 group
397 .variables
398 .iter()
399 .find(|var| var.is_coordinate_variable_for(dimension_name))
400 }
401
402 pub fn coordinate_variables(&self) -> impl Iterator<Item = &NcVariable> {
404 self.variables
405 .iter()
406 .filter(|var| var.is_coordinate_variable())
407 }
408
409 pub fn attribute(&self, name: &str) -> Option<&NcAttribute> {
411 let (group_path, attribute_name) = split_parent_path(name)?;
412 let group = self.group(group_path)?;
413 group.attributes.iter().find(|a| a.name == attribute_name)
414 }
415
416 pub fn group(&self, name: &str) -> Option<&NcGroup> {
418 let trimmed = name.trim_matches('/');
419 if trimmed.is_empty() {
420 return Some(self);
421 }
422
423 let mut group = self;
424 for component in trimmed.split('/').filter(|part| !part.is_empty()) {
425 group = group.groups.iter().find(|child| child.name == component)?;
426 }
427
428 Some(group)
429 }
430}
431
432fn split_parent_path(path: &str) -> Option<(&str, &str)> {
433 let trimmed = path.trim_matches('/');
434 if trimmed.is_empty() {
435 return None;
436 }
437
438 match trimmed.rsplit_once('/') {
439 Some((group_path, leaf_name)) if !leaf_name.is_empty() => Some((group_path, leaf_name)),
440 Some(_) => None,
441 None => Some(("", trimmed)),
442 }
443}
444
445pub(crate) fn checked_usize_from_u64(value: u64, context: &str) -> crate::Result<usize> {
446 usize::try_from(value)
447 .map_err(|_| crate::Error::InvalidData(format!("{context} exceeds platform usize")))
448}
449
450pub(crate) fn checked_mul_u64(lhs: u64, rhs: u64, context: &str) -> crate::Result<u64> {
451 lhs.checked_mul(rhs)
452 .ok_or_else(|| crate::Error::InvalidData(format!("{context} exceeds u64 capacity")))
453}
454
455pub(crate) fn checked_shape_elements(shape: &[u64], context: &str) -> crate::Result<u64> {
456 shape
457 .iter()
458 .try_fold(1u64, |acc, &dim| checked_mul_u64(acc, dim, context))
459}
460
461#[derive(Debug, Clone)]
465pub struct NcSliceInfo {
466 pub selections: Vec<NcSliceInfoElem>,
467}
468
469#[derive(Debug, Clone)]
471pub enum NcSliceInfoElem {
472 Index(u64),
474 Slice { start: u64, end: u64, step: u64 },
476}
477
478impl NcSliceInfo {
479 pub fn all(ndim: usize) -> Self {
481 NcSliceInfo {
482 selections: vec![
483 NcSliceInfoElem::Slice {
484 start: 0,
485 end: u64::MAX,
486 step: 1,
487 };
488 ndim
489 ],
490 }
491 }
492}
493
494#[cfg(feature = "netcdf4")]
495impl NcSliceInfo {
496 pub(crate) fn to_hdf5_slice_info(&self) -> hdf5_reader::SliceInfo {
498 hdf5_reader::SliceInfo {
499 selections: self
500 .selections
501 .iter()
502 .map(|s| match s {
503 NcSliceInfoElem::Index(idx) => hdf5_reader::SliceInfoElem::Index(*idx),
504 NcSliceInfoElem::Slice { start, end, step } => {
505 hdf5_reader::SliceInfoElem::Slice {
506 start: *start,
507 end: *end,
508 step: *step,
509 }
510 }
511 })
512 .collect(),
513 }
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 fn sample_group_tree() -> NcGroup {
522 NcGroup {
523 name: "/".to_string(),
524 dimensions: vec![NcDimension {
525 name: "root_dim".to_string(),
526 size: 2,
527 is_unlimited: false,
528 }],
529 variables: vec![NcVariable {
530 name: "root_var".to_string(),
531 dimensions: vec![],
532 dtype: NcType::Int,
533 attributes: vec![],
534 data_offset: 0,
535 _data_size: 0,
536 is_record_var: false,
537 record_size: 4,
538 }],
539 attributes: vec![NcAttribute {
540 name: "title".to_string(),
541 value: NcAttrValue::Chars("root".to_string()),
542 }],
543 groups: vec![NcGroup {
544 name: "obs".to_string(),
545 dimensions: vec![NcDimension {
546 name: "time".to_string(),
547 size: 3,
548 is_unlimited: false,
549 }],
550 variables: vec![NcVariable {
551 name: "temperature".to_string(),
552 dimensions: vec![],
553 dtype: NcType::Float,
554 attributes: vec![],
555 data_offset: 0,
556 _data_size: 0,
557 is_record_var: false,
558 record_size: 4,
559 }],
560 attributes: vec![],
561 groups: vec![NcGroup {
562 name: "surface".to_string(),
563 dimensions: vec![],
564 variables: vec![NcVariable {
565 name: "pressure".to_string(),
566 dimensions: vec![],
567 dtype: NcType::Double,
568 attributes: vec![],
569 data_offset: 0,
570 _data_size: 0,
571 is_record_var: false,
572 record_size: 8,
573 }],
574 attributes: vec![NcAttribute {
575 name: "units".to_string(),
576 value: NcAttrValue::Chars("hPa".to_string()),
577 }],
578 groups: vec![],
579 }],
580 }],
581 }
582 }
583
584 #[test]
585 fn group_path_lookup() {
586 let root = sample_group_tree();
587
588 let surface = root.group("obs/surface").unwrap();
589 assert_eq!(surface.name, "surface");
590 assert!(root.group("/obs/surface").is_some());
591 assert!(root.group("missing").is_none());
592 }
593
594 #[test]
595 fn variable_path_lookup() {
596 let root = sample_group_tree();
597
598 assert_eq!(root.variable("root_var").unwrap().name(), "root_var");
599 assert_eq!(
600 root.variable("obs/temperature").unwrap().dtype(),
601 &NcType::Float
602 );
603 assert_eq!(
604 root.variable("/obs/surface/pressure").unwrap().dtype(),
605 &NcType::Double
606 );
607 assert!(root.variable("pressure").is_none());
608 }
609
610 #[test]
611 fn dimension_and_attribute_path_lookup() {
612 let root = sample_group_tree();
613
614 assert_eq!(root.dimension("root_dim").unwrap().size, 2);
615 assert_eq!(root.dimension("obs/time").unwrap().size, 3);
616 assert_eq!(
617 root.attribute("title").unwrap().value.as_string().unwrap(),
618 "root"
619 );
620 assert_eq!(
621 root.attribute("obs/surface/units")
622 .unwrap()
623 .value
624 .as_string()
625 .unwrap(),
626 "hPa"
627 );
628 }
629
630 #[test]
631 fn coordinate_variable_detection_and_lookup() {
632 let time_dim = NcDimension {
633 name: "time".to_string(),
634 size: 3,
635 is_unlimited: false,
636 };
637 let lat_dim = NcDimension {
638 name: "lat".to_string(),
639 size: 2,
640 is_unlimited: false,
641 };
642 let time = NcVariable {
643 name: "time".to_string(),
644 dimensions: vec![time_dim.clone()],
645 dtype: NcType::Double,
646 attributes: vec![],
647 data_offset: 0,
648 _data_size: 0,
649 is_record_var: false,
650 record_size: 8,
651 };
652 let temperature = NcVariable {
653 name: "temperature".to_string(),
654 dimensions: vec![time_dim.clone(), lat_dim.clone()],
655 dtype: NcType::Float,
656 attributes: vec![],
657 data_offset: 0,
658 _data_size: 0,
659 is_record_var: false,
660 record_size: 4,
661 };
662 let group = NcGroup {
663 name: "/".to_string(),
664 dimensions: vec![time_dim, lat_dim],
665 variables: vec![time.clone(), temperature],
666 attributes: vec![],
667 groups: vec![],
668 };
669
670 assert!(time.is_coordinate_variable());
671 assert_eq!(time.coordinate_dimension().unwrap().name, "time");
672 assert_eq!(group.coordinate_variable("time").unwrap().name(), "time");
673 assert!(group.coordinate_variable("lat").is_none());
674
675 let names: Vec<&str> = group.coordinate_variables().map(NcVariable::name).collect();
676 assert_eq!(names, vec!["time"]);
677 }
678
679 #[test]
680 fn checked_shape_elements_overflow() {
681 let err = checked_shape_elements(&[u64::MAX, 2], "test overflow").unwrap_err();
682 assert!(matches!(err, crate::Error::InvalidData(_)));
683 }
684
685 #[test]
686 fn array_type_size_rejects_overflow() {
687 let ty = NcType::Array {
688 base: Box::new(NcType::UInt64),
689 dims: vec![u64::MAX, 2],
690 };
691
692 let err = ty.size().unwrap_err();
693 assert!(err.to_string().contains("array type"));
694 }
695
696 #[test]
697 fn variable_num_elements_rejects_overflow() {
698 let var = NcVariable {
699 name: "huge".to_string(),
700 dimensions: vec![
701 NcDimension {
702 name: "x".to_string(),
703 size: u64::MAX,
704 is_unlimited: false,
705 },
706 NcDimension {
707 name: "y".to_string(),
708 size: 2,
709 is_unlimited: false,
710 },
711 ],
712 dtype: NcType::Float,
713 attributes: vec![],
714 data_offset: 0,
715 _data_size: 0,
716 is_record_var: false,
717 record_size: 4,
718 };
719
720 let err = var.num_elements().unwrap_err();
721 assert!(err.to_string().contains("element count"));
722 }
723}