Skip to main content

netcdf_reader/nc4/
attributes.rs

1//! Filter and convert HDF5 attributes to NetCDF-4 attributes.
2//!
3//! NetCDF-4 uses several internal HDF5 attributes that should not be exposed
4//! to users:
5//! - `_NCProperties`: file provenance metadata
6//! - `_Netcdf4Dimid`: dimension ID assignment
7//! - `_Netcdf4Coordinates`: coordinate variable tracking
8//! - `DIMENSION_LIST`: dimension-to-variable references
9//! - `REFERENCE_LIST`: variable-to-dimension back-references
10//! - `CLASS`: dimension scale marker
11//! - `NAME`: dimension scale name
12
13use hdf5_reader::group::Group;
14use hdf5_reader::messages::datatype::{Datatype, VarLenKind};
15
16use crate::error::{Error, Result};
17use crate::types::{NcAttrValue, NcAttribute};
18
19/// Internal attribute names that should be hidden from the NetCDF user API.
20const INTERNAL_ATTRIBUTES: &[&str] = &[
21    "_NCProperties",
22    "_Netcdf4Dimid",
23    "_Netcdf4Coordinates",
24    "_nc3_strict",
25    "DIMENSION_LIST",
26    "REFERENCE_LIST",
27    "CLASS",
28    "NAME",
29];
30
31/// Returns true if the given attribute name is an internal NetCDF-4/HDF5 attribute
32/// that should not be exposed to users.
33pub fn is_internal_attribute(name: &str) -> bool {
34    INTERNAL_ATTRIBUTES.contains(&name)
35}
36
37/// Extract user-visible attributes from an HDF5 group, filtering out
38/// internal NetCDF-4 attributes.
39pub fn extract_group_attributes(
40    group: &Group,
41    metadata_mode: crate::NcMetadataMode,
42) -> Result<Vec<NcAttribute>> {
43    collect_visible_attributes(&group.attributes()?, metadata_mode)
44}
45
46/// Extract user-visible attributes from an HDF5 dataset, filtering out
47/// internal attributes.
48pub fn extract_variable_attributes(
49    dataset: &hdf5_reader::Dataset,
50    metadata_mode: crate::NcMetadataMode,
51) -> Result<Vec<NcAttribute>> {
52    collect_visible_attributes(&dataset.attributes(), metadata_mode)
53}
54
55fn collect_visible_attributes(
56    attrs: &[hdf5_reader::Attribute],
57    metadata_mode: crate::NcMetadataMode,
58) -> Result<Vec<NcAttribute>> {
59    let mut nc_attrs = Vec::new();
60    for attr in attrs {
61        if is_internal_attribute(&attr.name) {
62            continue;
63        }
64        if let Some(nc_attr) = convert_attribute(attr, metadata_mode)? {
65            nc_attrs.push(nc_attr);
66        }
67    }
68    Ok(nc_attrs)
69}
70
71pub fn convert_visible_attribute(
72    attr: &hdf5_reader::Attribute,
73    metadata_mode: crate::NcMetadataMode,
74) -> Result<Option<NcAttribute>> {
75    if is_internal_attribute(&attr.name) {
76        return Ok(None);
77    }
78    convert_attribute(attr, metadata_mode)
79}
80
81/// Convert an HDF5 attribute to a NetCDF attribute.
82fn convert_attribute(
83    attr: &hdf5_reader::Attribute,
84    metadata_mode: crate::NcMetadataMode,
85) -> Result<Option<NcAttribute>> {
86    let Some(value) = convert_attribute_value(attr, metadata_mode)? else {
87        return Ok(None);
88    };
89
90    Ok(Some(NcAttribute {
91        name: attr.name.clone(),
92        value,
93    }))
94}
95
96/// Convert an HDF5 attribute's value to an NcAttrValue.
97fn convert_attribute_value(
98    attr: &hdf5_reader::Attribute,
99    metadata_mode: crate::NcMetadataMode,
100) -> Result<Option<NcAttrValue>> {
101    let strict = metadata_mode == crate::NcMetadataMode::Strict;
102
103    let unsupported = || {
104        Error::InvalidData(format!(
105            "attribute '{}' uses unsupported NetCDF-4 datatype {:?}",
106            attr.name, attr.datatype
107        ))
108    };
109
110    let read_attr = |result: hdf5_reader::error::Result<NcAttrValue>| match result {
111        Ok(value) => Ok(Some(value)),
112        Err(err) if strict => Err(Error::InvalidData(format!(
113            "attribute '{}' could not be decoded: {err}",
114            attr.name
115        ))),
116        Err(_) => Ok(None),
117    };
118
119    match &attr.datatype {
120        Datatype::FixedPoint { size, signed, .. } => match (size, signed) {
121            (1, true) => read_attr(attr.read_1d::<i8>().map(NcAttrValue::Bytes)),
122            (1, false) => read_attr(attr.read_1d::<u8>().map(NcAttrValue::UBytes)),
123            (2, true) => read_attr(attr.read_1d::<i16>().map(NcAttrValue::Shorts)),
124            (2, false) => read_attr(attr.read_1d::<u16>().map(NcAttrValue::UShorts)),
125            (4, true) => read_attr(attr.read_1d::<i32>().map(NcAttrValue::Ints)),
126            (4, false) => read_attr(attr.read_1d::<u32>().map(NcAttrValue::UInts)),
127            (8, true) => read_attr(attr.read_1d::<i64>().map(NcAttrValue::Int64s)),
128            (8, false) => read_attr(attr.read_1d::<u64>().map(NcAttrValue::UInt64s)),
129            _ if strict => Err(unsupported()),
130            _ => Ok(None),
131        },
132        Datatype::FloatingPoint { size, .. } => match size {
133            4 => read_attr(attr.read_1d::<f32>().map(NcAttrValue::Floats)),
134            8 => read_attr(attr.read_1d::<f64>().map(NcAttrValue::Doubles)),
135            _ if strict => Err(unsupported()),
136            _ => Ok(None),
137        },
138        Datatype::String { .. } => {
139            if attr.shape.is_empty() {
140                read_attr(attr.read_string().map(NcAttrValue::Chars))
141            } else {
142                read_attr(attr.read_strings().map(NcAttrValue::Strings))
143            }
144        }
145        Datatype::VarLen {
146            base,
147            kind: VarLenKind::String,
148            ..
149        } if matches!(
150            base.as_ref(),
151            Datatype::FixedPoint {
152                size: 1,
153                signed: false,
154                ..
155            }
156        ) =>
157        {
158            if attr.shape.is_empty() {
159                read_attr(attr.read_string().map(NcAttrValue::Chars))
160            } else {
161                read_attr(attr.read_strings().map(NcAttrValue::Strings))
162            }
163        }
164        _ if strict => Err(unsupported()),
165        _ => Ok(None),
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use hdf5_reader::Attribute;
172    use hdf5_reader::ByteOrder;
173
174    use super::*;
175
176    #[test]
177    fn strict_mode_rejects_unsupported_attribute_types() {
178        let attr = Attribute {
179            name: "opaque_attr".to_string(),
180            datatype: Datatype::Opaque {
181                size: 4,
182                tag: "test".to_string(),
183            },
184            shape: vec![1],
185            raw_data: vec![0, 0, 0, 0],
186            decoded_strings: None,
187        };
188
189        let err = convert_attribute_value(&attr, crate::NcMetadataMode::Strict).unwrap_err();
190        assert!(matches!(err, Error::InvalidData(_)));
191    }
192
193    #[test]
194    fn lossy_mode_skips_unsupported_attribute_types() {
195        let attr = Attribute {
196            name: "bad_int".to_string(),
197            datatype: Datatype::FixedPoint {
198                size: 16,
199                signed: true,
200                byte_order: ByteOrder::LittleEndian,
201            },
202            shape: vec![1],
203            raw_data: vec![0; 16],
204            decoded_strings: None,
205        };
206
207        assert!(convert_attribute_value(&attr, crate::NcMetadataMode::Lossy)
208            .unwrap()
209            .is_none());
210    }
211}