Skip to main content

hdf5_pure/
types.rs

1//! Simplified type representations for the high-level API.
2
3use std::collections::HashMap;
4use std::fmt;
5
6pub use crate::file_writer::AttrValue;
7
8/// Simplified datatype enum for the high-level API.
9///
10/// Maps from the detailed `crate::datatype::Datatype` to a
11/// user-friendly representation.
12#[derive(Debug, Clone, PartialEq)]
13pub enum DType {
14    F32,
15    F64,
16    I8,
17    I16,
18    I32,
19    I64,
20    U8,
21    U16,
22    U32,
23    U64,
24    String,
25    Compound(Vec<(std::string::String, DType)>),
26    Enum(Vec<std::string::String>),
27    Array(Box<DType>, Vec<u32>),
28    VariableLengthString,
29    /// HDF5 object reference (8-byte address).
30    ObjectReference,
31    Other(std::string::String),
32}
33
34impl fmt::Display for DType {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            DType::F32 => write!(f, "f32"),
38            DType::F64 => write!(f, "f64"),
39            DType::I8 => write!(f, "i8"),
40            DType::I16 => write!(f, "i16"),
41            DType::I32 => write!(f, "i32"),
42            DType::I64 => write!(f, "i64"),
43            DType::U8 => write!(f, "u8"),
44            DType::U16 => write!(f, "u16"),
45            DType::U32 => write!(f, "u32"),
46            DType::U64 => write!(f, "u64"),
47            DType::String => write!(f, "string"),
48            DType::VariableLengthString => write!(f, "vlen_string"),
49            DType::ObjectReference => write!(f, "object_ref"),
50            DType::Compound(fields) => {
51                write!(f, "compound{{")?;
52                for (i, (name, dt)) in fields.iter().enumerate() {
53                    if i > 0 {
54                        write!(f, ", ")?;
55                    }
56                    write!(f, "{name}: {dt}")?;
57                }
58                write!(f, "}}")
59            }
60            DType::Enum(names) => write!(f, "enum[{}]", names.join(", ")),
61            DType::Array(base, dims) => write!(f, "array<{base}, {dims:?}>"),
62            DType::Other(desc) => write!(f, "other({desc})"),
63        }
64    }
65}
66
67/// Convert a low-level `Datatype` to a simplified `DType`.
68pub(crate) fn classify_datatype(dt: &crate::datatype::Datatype) -> DType {
69    use crate::datatype::Datatype;
70
71    match dt {
72        Datatype::FloatingPoint { size: 4, .. } => DType::F32,
73        Datatype::FloatingPoint { size: 8, .. } => DType::F64,
74        Datatype::FloatingPoint { size, .. } => DType::Other(format!("float{}", size * 8)),
75        Datatype::FixedPoint {
76            size: 1,
77            signed: true,
78            ..
79        } => DType::I8,
80        Datatype::FixedPoint {
81            size: 2,
82            signed: true,
83            ..
84        } => DType::I16,
85        Datatype::FixedPoint {
86            size: 4,
87            signed: true,
88            ..
89        } => DType::I32,
90        Datatype::FixedPoint {
91            size: 8,
92            signed: true,
93            ..
94        } => DType::I64,
95        Datatype::FixedPoint {
96            size: 1,
97            signed: false,
98            ..
99        } => DType::U8,
100        Datatype::FixedPoint {
101            size: 2,
102            signed: false,
103            ..
104        } => DType::U16,
105        Datatype::FixedPoint {
106            size: 4,
107            signed: false,
108            ..
109        } => DType::U32,
110        Datatype::FixedPoint {
111            size: 8,
112            signed: false,
113            ..
114        } => DType::U64,
115        Datatype::FixedPoint { size, signed, .. } => {
116            let prefix = if *signed { "i" } else { "u" };
117            DType::Other(format!("{prefix}{}", size * 8))
118        }
119        Datatype::String { .. } => DType::String,
120        Datatype::VariableLength {
121            is_string: true, ..
122        } => DType::VariableLengthString,
123        Datatype::Compound { members, .. } => {
124            let fields = members
125                .iter()
126                .map(|m| (m.name.clone(), classify_datatype(&m.datatype)))
127                .collect();
128            DType::Compound(fields)
129        }
130        Datatype::Enumeration { members, .. } => {
131            let names = members.iter().map(|m| m.name.clone()).collect();
132            DType::Enum(names)
133        }
134        Datatype::Array {
135            base_type,
136            dimensions,
137        } => DType::Array(Box::new(classify_datatype(base_type)), dimensions.clone()),
138        Datatype::Reference {
139            ref_type: crate::datatype::ReferenceType::Object,
140            ..
141        } => DType::ObjectReference,
142        _ => DType::Other(format!("{dt:?}")),
143    }
144}
145
146/// Read attribute messages into a `HashMap<String, AttrValue>`.
147///
148/// Best-effort: attributes that can't be decoded are silently skipped.
149/// `base_address` is the file-level userblock offset — needed so that
150/// variable-length attribute data (stored in global heap collections with
151/// addresses relative to the base) can be located correctly.
152pub(crate) fn attrs_to_map<S: crate::source::FileSource + ?Sized>(
153    attrs: &[crate::attribute::AttributeMessage],
154    source: &S,
155    offset_size: u8,
156    length_size: u8,
157    base_address: u64,
158) -> HashMap<std::string::String, AttrValue> {
159    let mut map = HashMap::new();
160    for attr in attrs {
161        if let Some(val) = decode_attr_value(attr, source, offset_size, length_size, base_address) {
162            map.insert(attr.name.clone(), val);
163        }
164    }
165    map
166}
167
168fn decode_attr_value<S: crate::source::FileSource + ?Sized>(
169    attr: &crate::attribute::AttributeMessage,
170    source: &S,
171    offset_size: u8,
172    length_size: u8,
173    base_address: u64,
174) -> Option<AttrValue> {
175    use crate::datatype::Datatype;
176
177    match &attr.datatype {
178        Datatype::FloatingPoint { .. } => {
179            let vals = attr.read_as_f64().ok()?;
180            if vals.len() == 1 {
181                Some(AttrValue::F64(vals[0]))
182            } else {
183                Some(AttrValue::F64Array(vals))
184            }
185        }
186        Datatype::FixedPoint { signed: true, .. } => {
187            let vals = attr.read_as_i64().ok()?;
188            if vals.len() == 1 {
189                Some(AttrValue::I64(vals[0]))
190            } else {
191                Some(AttrValue::I64Array(vals))
192            }
193        }
194        Datatype::FixedPoint { signed: false, .. } => {
195            let vals = attr.read_as_u64().ok()?;
196            if vals.len() == 1 {
197                Some(AttrValue::U64(vals[0]))
198            } else {
199                // No U64Array variant, store as I64Array
200                #[expect(
201                    clippy::cast_possible_wrap,
202                    reason = "no U64Array AttrValue variant; values above i64::MAX are \
203                              reinterpreted as i64 by design (bit pattern preserved)"
204                )]
205                let i64_vals: Vec<i64> = vals.iter().map(|&v| v as i64).collect();
206                Some(AttrValue::I64Array(i64_vals))
207            }
208        }
209        Datatype::String { .. } => {
210            let strings = attr.read_as_strings().ok()?;
211            if strings.len() == 1 {
212                Some(AttrValue::String(strings[0].clone()))
213            } else {
214                Some(AttrValue::StringArray(strings))
215            }
216        }
217        Datatype::VariableLength {
218            is_string,
219            base_type,
220            ..
221        } if *is_string || is_ascii_char_vlen_base(base_type) => {
222            // Two MATLAB-relevant encodings share the same on-disk byte
223            // layout (length + heap ref + object index per element; heap
224            // object holds raw bytes without terminator):
225            //   - is_string: true             — H5T_STRING{STRSIZE=VAR}
226            //   - VLEN of H5T_STRING{SIZE=1}  — what matio / MATLAB emit
227            //
228            // The reader resolves each element from the global heap, adding
229            // `base_address` to the (relative) collection addresses.
230            let strings = crate::vl_data::read_vl_strings_from_source(
231                source,
232                &attr.raw_data,
233                attr.dataspace.num_elements(),
234                offset_size,
235                length_size,
236                base_address,
237                crate::vl_data::VlenStringReadOptions::default(),
238            )
239            .ok()?;
240            if strings.len() == 1 {
241                Some(AttrValue::String(strings[0].clone()))
242            } else {
243                Some(AttrValue::StringArray(strings))
244            }
245        }
246        _ => None,
247    }
248}
249
250/// Recognize the MATLAB-style VLEN encoding where the base type is a 1-byte
251/// ASCII string (`H5T_VLEN { H5T_STRING { STRSIZE 1, ..., CSET ASCII } }`).
252/// Other VLEN sequences of strings may exist but we only auto-decode this
253/// specific shape as a string array.
254fn is_ascii_char_vlen_base(base: &crate::datatype::Datatype) -> bool {
255    use crate::datatype::{CharacterSet, Datatype};
256    matches!(
257        base,
258        Datatype::String {
259            size: 1,
260            charset: CharacterSet::Ascii,
261            ..
262        }
263    )
264}