1#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum NcFormat {
15 Classic,
16 Offset64,
17 Cdf5,
18 Nc4,
19 Nc4Classic,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum NcMetadataMode {
25 #[default]
26 Strict,
27 Lossy,
28}
29
30pub 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
43pub const NC_FILL_BYTE: i8 = -127;
45pub const NC_FILL_CHAR: u8 = 0;
47pub const NC_FILL_SHORT: i16 = -32767;
49pub const NC_FILL_INT: i32 = -2147483647;
51pub const NC_FILL_FLOAT: f32 = 9.969_21e36_f32;
53pub const NC_FILL_DOUBLE: f64 = 9.969_209_968_386_869e36_f64;
55pub const NC_FILL_UBYTE: u8 = 255;
57pub const NC_FILL_USHORT: u16 = 65535;
59pub const NC_FILL_UINT: u32 = 4294967295;
61pub const NC_FILL_INT64: i64 = -9223372036854775806;
63pub const NC_FILL_UINT64: u64 = 18446744073709551614;
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct NcDimension {
69 pub name: String,
70 pub size: u64,
71 pub is_unlimited: bool,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct NcCompoundField {
77 pub name: String,
78 pub offset: u64,
79 pub dtype: NcType,
80}
81
82#[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#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct NcEnumMember {
126 pub name: String,
127 pub value: NcIntegerValue,
128}
129
130#[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#[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#[derive(Debug, Clone, PartialEq)]
305pub struct NcAttribute {
306 pub name: String,
307 pub value: NcAttrValue,
308}
309
310#[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#[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
480pub 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
490pub fn pad_to_4(len: usize) -> usize {
492 len + padding_to_4(len)
493}
494
495#[derive(Debug, Clone, PartialEq, Eq)]
497pub struct NcSliceInfo {
498 pub selections: Vec<NcSliceInfoElem>,
499}
500
501#[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}