Skip to main content

netcdf_core/
lib.rs

1//! Shared NetCDF data model used by reader and writer crates.
2
3/// Errors produced by shared NetCDF model helpers.
4#[derive(Debug, thiserror::Error)]
5pub enum Error {
6    #[error("invalid data: {0}")]
7    InvalidData(String),
8}
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// NetCDF file format.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum NcFormat {
15    Classic,
16    Offset64,
17    Cdf5,
18    Nc4,
19    Nc4Classic,
20}
21
22/// NetCDF-4 metadata reconstruction policy.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum NcMetadataMode {
25    #[default]
26    Strict,
27    Lossy,
28}
29
30/// NetCDF classic type codes.
31pub const NC_BYTE: u32 = 1;
32pub const NC_CHAR: u32 = 2;
33pub const NC_SHORT: u32 = 3;
34pub const NC_INT: u32 = 4;
35pub const NC_FLOAT: u32 = 5;
36pub const NC_DOUBLE: u32 = 6;
37pub const NC_UBYTE: u32 = 7;
38pub const NC_USHORT: u32 = 8;
39pub const NC_UINT: u32 = 9;
40pub const NC_INT64: u32 = 10;
41pub const NC_UINT64: u32 = 11;
42
43/// Default fill value for NC_BYTE variables when prefilled data is needed.
44pub const NC_FILL_BYTE: i8 = -127;
45/// Default fill value for NC_CHAR variables when prefilled data is needed.
46pub const NC_FILL_CHAR: u8 = 0;
47/// Default fill value for NC_SHORT variables when prefilled data is needed.
48pub const NC_FILL_SHORT: i16 = -32767;
49/// Default fill value for NC_INT variables when prefilled data is needed.
50pub const NC_FILL_INT: i32 = -2147483647;
51/// Default fill value for NC_FLOAT variables when prefilled data is needed.
52pub const NC_FILL_FLOAT: f32 = 9.969_21e36_f32;
53/// Default fill value for NC_DOUBLE variables when prefilled data is needed.
54pub const NC_FILL_DOUBLE: f64 = 9.969_209_968_386_869e36_f64;
55/// Default fill value for NC_UBYTE variables when prefilled data is needed.
56pub const NC_FILL_UBYTE: u8 = 255;
57/// Default fill value for NC_USHORT variables when prefilled data is needed.
58pub const NC_FILL_USHORT: u16 = 65535;
59/// Default fill value for NC_UINT variables when prefilled data is needed.
60pub const NC_FILL_UINT: u32 = 4294967295;
61/// Default fill value for NC_INT64 variables when prefilled data is needed.
62pub const NC_FILL_INT64: i64 = -9223372036854775806;
63/// Default fill value for NC_UINT64 variables when prefilled data is needed.
64pub const NC_FILL_UINT64: u64 = 18446744073709551614;
65
66/// A NetCDF dimension.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct NcDimension {
69    pub name: String,
70    pub size: u64,
71    pub is_unlimited: bool,
72}
73
74/// A field within a compound type.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct NcCompoundField {
77    pub name: String,
78    pub offset: u64,
79    pub dtype: NcType,
80}
81
82/// A typed integer value used by NetCDF-4 enum definitions and values.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum NcIntegerValue {
85    I8(i8),
86    U8(u8),
87    I16(i16),
88    U16(u16),
89    I32(i32),
90    U32(u32),
91    I64(i64),
92    U64(u64),
93}
94
95impl NcIntegerValue {
96    pub fn as_i128(self) -> Option<i128> {
97        match self {
98            NcIntegerValue::I8(value) => Some(value as i128),
99            NcIntegerValue::U8(value) => Some(value as i128),
100            NcIntegerValue::I16(value) => Some(value as i128),
101            NcIntegerValue::U16(value) => Some(value as i128),
102            NcIntegerValue::I32(value) => Some(value as i128),
103            NcIntegerValue::U32(value) => Some(value as i128),
104            NcIntegerValue::I64(value) => Some(value as i128),
105            NcIntegerValue::U64(value) => Some(i128::from(value)),
106        }
107    }
108
109    pub fn as_u128(self) -> Option<u128> {
110        match self {
111            NcIntegerValue::I8(value) => u128::try_from(value).ok(),
112            NcIntegerValue::U8(value) => Some(value as u128),
113            NcIntegerValue::I16(value) => u128::try_from(value).ok(),
114            NcIntegerValue::U16(value) => Some(value as u128),
115            NcIntegerValue::I32(value) => u128::try_from(value).ok(),
116            NcIntegerValue::U32(value) => Some(value as u128),
117            NcIntegerValue::I64(value) => u128::try_from(value).ok(),
118            NcIntegerValue::U64(value) => Some(value as u128),
119        }
120    }
121}
122
123/// A named member of a NetCDF-4 enum type.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct NcEnumMember {
126    pub name: String,
127    pub value: NcIntegerValue,
128}
129
130/// NetCDF data types.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub enum NcType {
133    Byte,
134    Char,
135    Short,
136    Int,
137    Float,
138    Double,
139    UByte,
140    UShort,
141    UInt,
142    Int64,
143    UInt64,
144    String,
145    Enum {
146        base: Box<NcType>,
147        members: Vec<NcEnumMember>,
148    },
149    Compound {
150        size: u32,
151        fields: Vec<NcCompoundField>,
152    },
153    Opaque {
154        size: u32,
155        tag: String,
156    },
157    Array {
158        base: Box<NcType>,
159        dims: Vec<u64>,
160    },
161    VLen {
162        base: Box<NcType>,
163    },
164}
165
166impl NcType {
167    pub fn size(&self) -> Result<usize> {
168        match self {
169            NcType::Byte | NcType::Char | NcType::UByte => Ok(1),
170            NcType::Short | NcType::UShort => Ok(2),
171            NcType::Int | NcType::UInt | NcType::Float => Ok(4),
172            NcType::Int64 | NcType::UInt64 | NcType::Double => Ok(8),
173            NcType::String => Ok(std::mem::size_of::<usize>()),
174            NcType::Enum { base, .. } => base.size(),
175            NcType::Compound { size, .. } => Ok(*size as usize),
176            NcType::Opaque { size, .. } => Ok(*size as usize),
177            NcType::Array { base, dims } => {
178                let base_size = base.size()?;
179                let count = dims.iter().try_fold(1usize, |acc, &dim| {
180                    let dim = usize::try_from(dim).map_err(|_| {
181                        Error::InvalidData(
182                            "NetCDF array type dimension exceeds platform usize capacity"
183                                .to_string(),
184                        )
185                    })?;
186                    acc.checked_mul(dim).ok_or_else(|| {
187                        Error::InvalidData(
188                            "NetCDF array type element count exceeds platform usize capacity"
189                                .to_string(),
190                        )
191                    })
192                })?;
193                base_size.checked_mul(count).ok_or_else(|| {
194                    Error::InvalidData(
195                        "NetCDF array type byte size exceeds platform usize capacity".to_string(),
196                    )
197                })
198            }
199            NcType::VLen { .. } => Ok(std::mem::size_of::<usize>()),
200        }
201    }
202
203    pub fn classic_type_code(&self) -> Option<u32> {
204        match self {
205            NcType::Byte => Some(NC_BYTE),
206            NcType::Char => Some(NC_CHAR),
207            NcType::Short => Some(NC_SHORT),
208            NcType::Int => Some(NC_INT),
209            NcType::Float => Some(NC_FLOAT),
210            NcType::Double => Some(NC_DOUBLE),
211            NcType::UByte => Some(NC_UBYTE),
212            NcType::UShort => Some(NC_USHORT),
213            NcType::UInt => Some(NC_UINT),
214            NcType::Int64 => Some(NC_INT64),
215            NcType::UInt64 => Some(NC_UINT64),
216            NcType::String
217            | NcType::Enum { .. }
218            | NcType::Compound { .. }
219            | NcType::Opaque { .. }
220            | NcType::Array { .. }
221            | NcType::VLen { .. } => None,
222        }
223    }
224
225    pub fn is_primitive(&self) -> bool {
226        matches!(
227            self,
228            NcType::Byte
229                | NcType::Char
230                | NcType::Short
231                | NcType::Int
232                | NcType::Float
233                | NcType::Double
234                | NcType::UByte
235                | NcType::UShort
236                | NcType::UInt
237                | NcType::Int64
238                | NcType::UInt64
239                | NcType::String
240        )
241    }
242}
243
244/// A NetCDF attribute value.
245#[derive(Debug, Clone, PartialEq)]
246pub enum NcAttrValue {
247    Bytes(Vec<i8>),
248    Chars(String),
249    Shorts(Vec<i16>),
250    Ints(Vec<i32>),
251    Floats(Vec<f32>),
252    Doubles(Vec<f64>),
253    UBytes(Vec<u8>),
254    UShorts(Vec<u16>),
255    UInts(Vec<u32>),
256    Int64s(Vec<i64>),
257    UInt64s(Vec<u64>),
258    Strings(Vec<String>),
259}
260
261impl NcAttrValue {
262    pub fn as_string(&self) -> Option<String> {
263        match self {
264            NcAttrValue::Chars(s) => Some(s.clone()),
265            NcAttrValue::Strings(v) if v.len() == 1 => Some(v[0].clone()),
266            _ => None,
267        }
268    }
269
270    pub fn as_f64(&self) -> Option<f64> {
271        match self {
272            NcAttrValue::Bytes(v) => v.first().map(|&x| x as f64),
273            NcAttrValue::Shorts(v) => v.first().map(|&x| x as f64),
274            NcAttrValue::Ints(v) => v.first().map(|&x| x as f64),
275            NcAttrValue::Floats(v) => v.first().map(|&x| x as f64),
276            NcAttrValue::Doubles(v) => v.first().copied(),
277            NcAttrValue::UBytes(v) => v.first().map(|&x| x as f64),
278            NcAttrValue::UShorts(v) => v.first().map(|&x| x as f64),
279            NcAttrValue::UInts(v) => v.first().map(|&x| x as f64),
280            NcAttrValue::Int64s(v) => v.first().map(|&x| x as f64),
281            NcAttrValue::UInt64s(v) => v.first().map(|&x| x as f64),
282            NcAttrValue::Chars(_) | NcAttrValue::Strings(_) => None,
283        }
284    }
285
286    pub fn as_f64_vec(&self) -> Option<Vec<f64>> {
287        match self {
288            NcAttrValue::Bytes(v) => Some(v.iter().map(|&x| x as f64).collect()),
289            NcAttrValue::Shorts(v) => Some(v.iter().map(|&x| x as f64).collect()),
290            NcAttrValue::Ints(v) => Some(v.iter().map(|&x| x as f64).collect()),
291            NcAttrValue::Floats(v) => Some(v.iter().map(|&x| x as f64).collect()),
292            NcAttrValue::Doubles(v) => Some(v.clone()),
293            NcAttrValue::UBytes(v) => Some(v.iter().map(|&x| x as f64).collect()),
294            NcAttrValue::UShorts(v) => Some(v.iter().map(|&x| x as f64).collect()),
295            NcAttrValue::UInts(v) => Some(v.iter().map(|&x| x as f64).collect()),
296            NcAttrValue::Int64s(v) => Some(v.iter().map(|&x| x as f64).collect()),
297            NcAttrValue::UInt64s(v) => Some(v.iter().map(|&x| x as f64).collect()),
298            NcAttrValue::Chars(_) | NcAttrValue::Strings(_) => None,
299        }
300    }
301}
302
303/// A NetCDF attribute.
304#[derive(Debug, Clone, PartialEq)]
305pub struct NcAttribute {
306    pub name: String,
307    pub value: NcAttrValue,
308}
309
310/// A NetCDF variable. The offset fields are public for reader/writer crate
311/// interoperability; they are not portable semantic metadata.
312#[derive(Debug, Clone, PartialEq)]
313pub struct NcVariable {
314    pub name: String,
315    pub dimensions: Vec<NcDimension>,
316    pub dtype: NcType,
317    pub attributes: Vec<NcAttribute>,
318    #[doc(hidden)]
319    pub data_offset: u64,
320    #[doc(hidden)]
321    pub _data_size: u64,
322    #[doc(hidden)]
323    pub is_record_var: bool,
324    #[doc(hidden)]
325    pub record_size: u64,
326}
327
328impl NcVariable {
329    pub fn name(&self) -> &str {
330        &self.name
331    }
332
333    pub fn dimensions(&self) -> &[NcDimension] {
334        &self.dimensions
335    }
336
337    pub fn coordinate_dimension(&self) -> Option<&NcDimension> {
338        match self.dimensions.as_slice() {
339            [dim] if dim.name == self.name => Some(dim),
340            _ => None,
341        }
342    }
343
344    pub fn is_coordinate_variable(&self) -> bool {
345        self.coordinate_dimension().is_some()
346    }
347
348    pub fn is_coordinate_variable_for(&self, dimension_name: &str) -> bool {
349        self.coordinate_dimension()
350            .is_some_and(|dim| dim.name == dimension_name)
351    }
352
353    pub fn dtype(&self) -> &NcType {
354        &self.dtype
355    }
356
357    pub fn shape(&self) -> Vec<u64> {
358        self.dimensions.iter().map(|d| d.size).collect()
359    }
360
361    pub fn attributes(&self) -> &[NcAttribute] {
362        &self.attributes
363    }
364
365    pub fn attribute(&self, name: &str) -> Option<&NcAttribute> {
366        self.attributes.iter().find(|a| a.name == name)
367    }
368
369    pub fn ndim(&self) -> usize {
370        self.dimensions.len()
371    }
372
373    pub fn num_elements(&self) -> Result<u64> {
374        match self.dimensions.as_slice() {
375            [] => Ok(1),
376            [dim] => Ok(dim.size),
377            dimensions => {
378                let mut total = 1u64;
379                for dim in dimensions {
380                    total = total.checked_mul(dim.size).ok_or_else(|| {
381                        Error::InvalidData(
382                            "NetCDF variable element count overflows u64".to_string(),
383                        )
384                    })?;
385                }
386                Ok(total)
387            }
388        }
389    }
390}
391
392/// A NetCDF group.
393#[derive(Debug, Clone, PartialEq)]
394pub struct NcGroup {
395    pub name: String,
396    pub dimensions: Vec<NcDimension>,
397    pub variables: Vec<NcVariable>,
398    pub attributes: Vec<NcAttribute>,
399    pub groups: Vec<NcGroup>,
400}
401
402impl NcGroup {
403    pub fn variable(&self, name: &str) -> Option<&NcVariable> {
404        let (group_path, variable_name) = split_parent_path(name)?;
405        let group = self.group(group_path)?;
406        group.variables.iter().find(|v| v.name == variable_name)
407    }
408
409    pub fn dimension(&self, name: &str) -> Option<&NcDimension> {
410        let (group_path, dimension_name) = split_parent_path(name)?;
411        let group = self.group(group_path)?;
412        group.dimensions.iter().find(|d| d.name == dimension_name)
413    }
414
415    pub fn coordinate_variable(&self, name: &str) -> Option<&NcVariable> {
416        let (group_path, dimension_name) = split_parent_path(name)?;
417        let group = self.group(group_path)?;
418        group
419            .variables
420            .iter()
421            .find(|var| var.is_coordinate_variable_for(dimension_name))
422    }
423
424    pub fn coordinate_variables(&self) -> impl Iterator<Item = &NcVariable> {
425        self.variables
426            .iter()
427            .filter(|var| var.is_coordinate_variable())
428    }
429
430    pub fn attribute(&self, name: &str) -> Option<&NcAttribute> {
431        let (group_path, attribute_name) = split_parent_path(name)?;
432        let group = self.group(group_path)?;
433        group.attributes.iter().find(|a| a.name == attribute_name)
434    }
435
436    pub fn group(&self, name: &str) -> Option<&NcGroup> {
437        let trimmed = name.trim_matches('/');
438        if trimmed.is_empty() {
439            return Some(self);
440        }
441
442        let mut group = self;
443        for component in trimmed.split('/').filter(|part| !part.is_empty()) {
444            group = group.groups.iter().find(|child| child.name == component)?;
445        }
446
447        Some(group)
448    }
449}
450
451fn split_parent_path(path: &str) -> Option<(&str, &str)> {
452    let trimmed = path.trim_matches('/');
453    if trimmed.is_empty() {
454        return None;
455    }
456
457    match trimmed.rsplit_once('/') {
458        Some((group_path, leaf_name)) if !leaf_name.is_empty() => Some((group_path, leaf_name)),
459        Some(_) => None,
460        None => Some(("", trimmed)),
461    }
462}
463
464pub fn checked_usize_from_u64(value: u64, context: &str) -> Result<usize> {
465    usize::try_from(value)
466        .map_err(|_| Error::InvalidData(format!("{context} exceeds platform usize")))
467}
468
469pub fn checked_mul_u64(lhs: u64, rhs: u64, context: &str) -> Result<u64> {
470    lhs.checked_mul(rhs)
471        .ok_or_else(|| Error::InvalidData(format!("{context} exceeds u64 capacity")))
472}
473
474pub fn checked_shape_elements(shape: &[u64], context: &str) -> Result<u64> {
475    shape
476        .iter()
477        .try_fold(1u64, |acc, &dim| checked_mul_u64(acc, dim, context))
478}
479
480/// Compute the amount of padding needed to reach a 4-byte boundary.
481pub fn padding_to_4(len: usize) -> usize {
482    let rem = len % 4;
483    if rem == 0 {
484        0
485    } else {
486        4 - rem
487    }
488}
489
490/// Round up to the next 4-byte boundary.
491pub fn pad_to_4(len: usize) -> usize {
492    len + padding_to_4(len)
493}
494
495/// Hyperslab selection for reading/writing slices of NetCDF variables.
496#[derive(Debug, Clone, PartialEq, Eq)]
497pub struct NcSliceInfo {
498    pub selections: Vec<NcSliceInfoElem>,
499}
500
501/// A single dimension selection within a hyperslab.
502#[derive(Debug, Clone, PartialEq, Eq)]
503pub enum NcSliceInfoElem {
504    Index(u64),
505    Slice { start: u64, end: u64, step: u64 },
506}
507
508impl NcSliceInfo {
509    pub fn all(ndim: usize) -> Self {
510        NcSliceInfo {
511            selections: vec![
512                NcSliceInfoElem::Slice {
513                    start: 0,
514                    end: u64::MAX,
515                    step: 1,
516                };
517                ndim
518            ],
519        }
520    }
521}