use oxih5::{ByteOrder, Dataset, Dtype, OxiH5Error};
use oxih5_core::{Charset, RefType};
use super::types_3::{HDF5DataType, StringEncoding};
use crate::error::{IoError, Result};
fn decode_err(what: &str, err: OxiH5Error) -> IoError {
IoError::FormatError(format!("Failed to decode {what} data: {err}"))
}
fn unsupported(dtype: &Dtype, target: &str) -> IoError {
IoError::UnsupportedFormat(format!(
"HDF5 datatype {dtype} cannot be converted to {target}"
))
}
fn retyped(dataset: &Dataset, dtype: Dtype) -> Dataset {
Dataset {
data: dataset.data.clone(),
shape: dataset.shape.clone(),
dtype,
attributes: Vec::new(),
max_dims: None,
}
}
fn bitfield_as_int(size: usize, order: ByteOrder) -> Dtype {
Dtype::Int {
size,
signed: false,
order,
}
}
pub(super) fn convert_dtype(dtype: &Dtype) -> HDF5DataType {
match dtype {
Dtype::Int { size, signed, .. } => HDF5DataType::Integer {
size: *size,
signed: *signed,
},
Dtype::Float { size, .. } => HDF5DataType::Float { size: *size },
Dtype::String { charset, .. } => HDF5DataType::String {
encoding: match charset {
Charset::Ascii => StringEncoding::ASCII,
Charset::Utf8 => StringEncoding::UTF8,
},
},
Dtype::Compound { fields } => HDF5DataType::Compound {
fields: fields
.iter()
.map(|field| (field.name.clone(), convert_dtype(&field.dtype)))
.collect(),
},
Dtype::Array { base, dims } => HDF5DataType::Array {
base_type: Box::new(convert_dtype(base)),
shape: dims.clone(),
},
Dtype::Enum { members, .. } => HDF5DataType::Enum {
values: members.clone(),
},
Dtype::VarLen { base } => HDF5DataType::Array {
base_type: Box::new(convert_dtype(base)),
shape: vec![0],
},
Dtype::Opaque { size, .. } | Dtype::Bitfield { size, .. } => HDF5DataType::Integer {
size: *size,
signed: false,
},
Dtype::Reference { ref_type } => HDF5DataType::Integer {
size: match ref_type {
RefType::Object => 8,
RefType::Region => 12,
},
signed: false,
},
}
}
pub(crate) fn dataset_to_f64(dataset: &Dataset) -> Result<Vec<f64>> {
match &dataset.dtype {
Dtype::Float { size: 2, .. } => Ok(dataset
.as_f16()
.map_err(|e| decode_err("float16", e))?
.into_iter()
.map(f64::from)
.collect()),
Dtype::Float { size: 4, .. } => Ok(dataset
.as_f32()
.map_err(|e| decode_err("float32", e))?
.into_iter()
.map(f64::from)
.collect()),
Dtype::Float { size: 8, .. } => dataset.as_f64().map_err(|e| decode_err("float64", e)),
Dtype::Int {
size: 1,
signed: true,
..
} => Ok(dataset
.as_i8()
.map_err(|e| decode_err("int8", e))?
.into_iter()
.map(f64::from)
.collect()),
Dtype::Int {
size: 2,
signed: true,
..
} => Ok(dataset
.as_i16()
.map_err(|e| decode_err("int16", e))?
.into_iter()
.map(f64::from)
.collect()),
Dtype::Int {
size: 4,
signed: true,
..
} => Ok(dataset
.as_i32()
.map_err(|e| decode_err("int32", e))?
.into_iter()
.map(f64::from)
.collect()),
Dtype::Int {
size: 8,
signed: true,
..
} => Ok(dataset
.as_i64()
.map_err(|e| decode_err("int64", e))?
.into_iter()
.map(|v| v as f64)
.collect()),
Dtype::Int {
size: 1,
signed: false,
..
} => Ok(dataset
.as_u8()
.map_err(|e| decode_err("uint8", e))?
.into_iter()
.map(f64::from)
.collect()),
Dtype::Int {
size: 2,
signed: false,
..
} => Ok(dataset
.as_u16()
.map_err(|e| decode_err("uint16", e))?
.into_iter()
.map(f64::from)
.collect()),
Dtype::Int {
size: 4,
signed: false,
..
} => Ok(dataset
.as_u32()
.map_err(|e| decode_err("uint32", e))?
.into_iter()
.map(f64::from)
.collect()),
Dtype::Int {
size: 8,
signed: false,
..
} => Ok(dataset
.as_u64()
.map_err(|e| decode_err("uint64", e))?
.into_iter()
.map(|v| v as f64)
.collect()),
Dtype::Enum { base, .. } => dataset_to_f64(&retyped(dataset, (**base).clone())),
Dtype::Bitfield { size, order } => {
dataset_to_f64(&retyped(dataset, bitfield_as_int(*size, *order)))
}
other => Err(unsupported(other, "f64")),
}
}
pub(crate) fn dataset_to_i64(dataset: &Dataset) -> Result<Vec<i64>> {
match &dataset.dtype {
Dtype::Int {
size: 1,
signed: true,
..
} => Ok(dataset
.as_i8()
.map_err(|e| decode_err("int8", e))?
.into_iter()
.map(i64::from)
.collect()),
Dtype::Int {
size: 2,
signed: true,
..
} => Ok(dataset
.as_i16()
.map_err(|e| decode_err("int16", e))?
.into_iter()
.map(i64::from)
.collect()),
Dtype::Int {
size: 4,
signed: true,
..
} => Ok(dataset
.as_i32()
.map_err(|e| decode_err("int32", e))?
.into_iter()
.map(i64::from)
.collect()),
Dtype::Int {
size: 8,
signed: true,
..
} => dataset.as_i64().map_err(|e| decode_err("int64", e)),
Dtype::Int {
size: 1,
signed: false,
..
} => Ok(dataset
.as_u8()
.map_err(|e| decode_err("uint8", e))?
.into_iter()
.map(i64::from)
.collect()),
Dtype::Int {
size: 2,
signed: false,
..
} => Ok(dataset
.as_u16()
.map_err(|e| decode_err("uint16", e))?
.into_iter()
.map(i64::from)
.collect()),
Dtype::Int {
size: 4,
signed: false,
..
} => Ok(dataset
.as_u32()
.map_err(|e| decode_err("uint32", e))?
.into_iter()
.map(i64::from)
.collect()),
Dtype::Int {
size: 8,
signed: false,
..
} => dataset
.as_u64()
.map_err(|e| decode_err("uint64", e))?
.into_iter()
.map(|v| {
i64::try_from(v).map_err(|_| {
IoError::ConversionError(format!(
"uint64 value {v} exceeds i64::MAX and cannot be represented"
))
})
})
.collect(),
Dtype::Float { .. } => Ok(dataset_to_f64(dataset)?
.into_iter()
.map(|v| v as i64)
.collect()),
Dtype::Enum { base, .. } => dataset_to_i64(&retyped(dataset, (**base).clone())),
Dtype::Bitfield { size, order } => {
dataset_to_i64(&retyped(dataset, bitfield_as_int(*size, *order)))
}
other => Err(unsupported(other, "i64")),
}
}
pub(crate) fn is_integral(dtype: &Dtype) -> bool {
match dtype {
Dtype::Int { size, .. } | Dtype::Bitfield { size, .. } => {
matches!(size, 1 | 2 | 4 | 8)
}
Dtype::Enum { base, .. } => is_integral(base),
_ => false,
}
}
pub(crate) fn is_floating(dtype: &Dtype) -> bool {
matches!(
dtype,
Dtype::Float {
size: 2 | 4 | 8,
..
}
)
}
#[cfg(test)]
mod tests {
use super::*;
use oxih5_core::CompoundField;
fn ds(data: Vec<u8>, shape: Vec<usize>, dtype: Dtype) -> Dataset {
Dataset {
data,
shape,
dtype,
attributes: Vec::new(),
max_dims: None,
}
}
fn int(size: usize, signed: bool) -> Dtype {
Dtype::Int {
size,
signed,
order: ByteOrder::Little,
}
}
#[test]
fn test_widen_all_integer_widths_to_f64() {
let cases: Vec<(Dtype, Vec<u8>, Vec<f64>)> = vec![
(int(1, true), vec![0xFF, 0x01], vec![-1.0, 1.0]),
(int(1, false), vec![0xFF, 0x01], vec![255.0, 1.0]),
(int(2, true), vec![0xFF, 0xFF, 0x02, 0x00], vec![-1.0, 2.0]),
(
int(2, false),
vec![0xFF, 0xFF, 0x02, 0x00],
vec![65535.0, 2.0],
),
(int(4, true), vec![0xFF, 0xFF, 0xFF, 0xFF], vec![-1.0]),
(
int(4, false),
vec![0xFF, 0xFF, 0xFF, 0xFF],
vec![4294967295.0],
),
(int(8, true), vec![0xFF; 8], vec![-1.0]),
(int(8, false), vec![0x02, 0, 0, 0, 0, 0, 0, 0], vec![2.0]),
];
for (dtype, bytes, expected) in cases {
let n = expected.len();
let dataset = ds(bytes, vec![n], dtype.clone());
let got = dataset_to_f64(&dataset)
.unwrap_or_else(|e| panic!("{dtype} should widen to f64: {e}"));
assert_eq!(got, expected, "mismatch for {dtype}");
}
}
#[test]
fn test_widen_float_widths_to_f64() {
let f32_ds = ds(
1.5f32.to_le_bytes().to_vec(),
vec![1],
Dtype::Float {
size: 4,
order: ByteOrder::Little,
},
);
assert_eq!(dataset_to_f64(&f32_ds).expect("f32 widens"), vec![1.5]);
let f16_ds = ds(
0x3E00u16.to_le_bytes().to_vec(),
vec![1],
Dtype::Float {
size: 2,
order: ByteOrder::Little,
},
);
assert_eq!(dataset_to_f64(&f16_ds).expect("f16 widens"), vec![1.5]);
}
#[test]
fn test_big_endian_integer_widens() {
let dataset = ds(
vec![0x00, 0x00, 0x01, 0x00],
vec![1],
Dtype::Int {
size: 4,
signed: true,
order: ByteOrder::Big,
},
);
assert_eq!(dataset_to_f64(&dataset).expect("BE decode"), vec![256.0]);
}
#[test]
fn test_enum_decodes_via_base_type() {
let dataset = ds(
vec![0x07, 0x00, 0x00, 0x00],
vec![1],
Dtype::Enum {
base: Box::new(int(4, true)),
members: vec![("SEVEN".to_string(), 7)],
},
);
assert_eq!(dataset_to_i64(&dataset).expect("enum decode"), vec![7]);
assert_eq!(dataset_to_f64(&dataset).expect("enum decode"), vec![7.0]);
}
#[test]
fn test_bitfield_decodes_as_unsigned() {
let dataset = ds(
vec![0xF0],
vec![1],
Dtype::Bitfield {
size: 1,
order: ByteOrder::Little,
},
);
assert_eq!(
dataset_to_i64(&dataset).expect("bitfield decode"),
vec![240]
);
}
#[test]
fn test_uint64_overflow_is_reported_not_wrapped() {
let dataset = ds(vec![0xFF; 8], vec![1], int(8, false));
let err = dataset_to_i64(&dataset).expect_err("u64::MAX must not fit i64");
assert!(
matches!(err, IoError::ConversionError(_)),
"expected ConversionError, got {err:?}"
);
assert_eq!(
dataset_to_f64(&dataset).expect("u64 widens to f64"),
vec![u64::MAX as f64]
);
}
#[test]
fn test_non_numeric_dtype_is_refused() {
let dataset = ds(
b"hi".to_vec(),
vec![1],
Dtype::String {
fixed_len: Some(2),
charset: Charset::Utf8,
},
);
let err = dataset_to_f64(&dataset).expect_err("strings are not numeric");
assert!(matches!(err, IoError::UnsupportedFormat(_)));
}
#[test]
fn test_convert_dtype_preserves_array_rank() {
let dtype = Dtype::Array {
base: Box::new(Dtype::Float {
size: 4,
order: ByteOrder::Little,
}),
dims: vec![2, 3, 4],
};
match convert_dtype(&dtype) {
HDF5DataType::Array { base_type, shape } => {
assert_eq!(shape, vec![2, 3, 4], "all three axes must survive");
assert!(matches!(*base_type, HDF5DataType::Float { size: 4 }));
}
other => panic!("expected Array, got {other:?}"),
}
}
#[test]
fn test_convert_dtype_maps_non_string_types_truthfully() {
assert_eq!(
convert_dtype(&Dtype::Bitfield {
size: 2,
order: ByteOrder::Little
}),
HDF5DataType::Integer {
size: 2,
signed: false
}
);
assert_eq!(
convert_dtype(&Dtype::Opaque {
size: 5,
tag: "blob".to_string()
}),
HDF5DataType::Integer {
size: 5,
signed: false
}
);
assert_eq!(
convert_dtype(&Dtype::Reference {
ref_type: RefType::Object
}),
HDF5DataType::Integer {
size: 8,
signed: false
}
);
assert_eq!(
convert_dtype(&Dtype::Reference {
ref_type: RefType::Region
}),
HDF5DataType::Integer {
size: 12,
signed: false
}
);
}
#[test]
fn test_convert_dtype_compound_fields() {
let dtype = Dtype::Compound {
fields: vec![
CompoundField {
name: "id".to_string(),
offset: 0,
dtype: int(4, true),
},
CompoundField {
name: "label".to_string(),
offset: 4,
dtype: Dtype::String {
fixed_len: Some(8),
charset: Charset::Ascii,
},
},
],
};
match convert_dtype(&dtype) {
HDF5DataType::Compound { fields } => {
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].0, "id");
assert_eq!(
fields[1].1,
HDF5DataType::String {
encoding: StringEncoding::ASCII
}
);
}
other => panic!("expected Compound, got {other:?}"),
}
}
#[test]
fn test_convert_dtype_enum_members() {
let dtype = Dtype::Enum {
base: Box::new(int(2, true)),
members: vec![("RED".to_string(), 0), ("GREEN".to_string(), 1)],
};
match convert_dtype(&dtype) {
HDF5DataType::Enum { values } => {
assert_eq!(
values,
vec![("RED".to_string(), 0), ("GREEN".to_string(), 1)]
);
}
other => panic!("expected Enum, got {other:?}"),
}
}
#[test]
fn test_routing_predicates() {
assert!(is_integral(&int(4, true)));
assert!(is_integral(&Dtype::Bitfield {
size: 8,
order: ByteOrder::Little
}));
assert!(is_integral(&Dtype::Enum {
base: Box::new(int(1, false)),
members: vec![],
}));
assert!(!is_integral(&int(3, true)));
assert!(is_floating(&Dtype::Float {
size: 8,
order: ByteOrder::Little
}));
assert!(!is_floating(&int(8, true)));
}
}