use std::path::Path;
use candle_core::DType;
use crate::error::CanddingError;
use crate::error::Result;
struct DtypeEntry {
code: &'static str,
dtype: DType,
itemsize: usize,
}
const SUPPORTED_DTYPES: &[DtypeEntry] = &[
DtypeEntry {
code: "f4",
dtype: DType::F32,
itemsize: 4,
},
DtypeEntry {
code: "f8",
dtype: DType::F64,
itemsize: 8,
},
DtypeEntry {
code: "i2",
dtype: DType::I16,
itemsize: 2,
},
DtypeEntry {
code: "i4",
dtype: DType::I32,
itemsize: 4,
},
DtypeEntry {
code: "i8",
dtype: DType::I64,
itemsize: 8,
},
DtypeEntry {
code: "u1",
dtype: DType::U8,
itemsize: 1,
},
DtypeEntry {
code: "u4",
dtype: DType::U32,
itemsize: 4,
},
];
pub(super) struct Header {
pub(super) dtype: DType,
pub(super) itemsize: usize,
pub(super) shape: Vec<usize>,
}
pub(super) fn parse_header(path: &Path, dict: &str) -> Result<Header> {
let mut descr = None;
let mut fortran_order = None;
let mut shape = None;
for field in split_top_level(dict) {
let Some((key, value)) = field.split_once(':') else {
continue;
};
let key = key.trim().trim_matches('\'');
let value = value.trim();
match key {
"descr" => descr = Some(value.trim_matches('\'').to_string()),
"fortran_order" => fortran_order = Some(value == "True"),
"shape" => {
shape = Some(parse_shape(value).map_err(|reason| super::header_error(path, &reason))?)
}
_ => {}
}
}
let descr =
descr.ok_or_else(|| super::header_error(path, "header dictionary has no `descr` key"))?;
let fortran_order = fortran_order
.ok_or_else(|| super::header_error(path, "header dictionary has no `fortran_order` key"))?;
let shape =
shape.ok_or_else(|| super::header_error(path, "header dictionary has no `shape` key"))?;
if fortran_order {
return Err(CanddingError::NpyFortranOrder(path.to_path_buf()));
}
let (dtype, itemsize) = resolve_dtype(path, &descr)?;
Ok(Header {
dtype,
itemsize,
shape,
})
}
fn resolve_dtype(path: &Path, descr: &str) -> Result<(DType, usize)> {
let mut chars = descr.chars();
let order = chars.next().unwrap_or('\0');
let code = chars.as_str();
let Some(entry) = SUPPORTED_DTYPES.iter().find(|entry| entry.code == code) else {
return Err(CanddingError::NpyUnsupportedDtype {
file: path.to_path_buf(),
descr: descr.to_string(),
});
};
let order_ok = matches!(order, '<' | '=') || (order == '|' && entry.itemsize == 1);
if !order_ok {
return Err(CanddingError::NpyUnsupportedByteOrder {
file: path.to_path_buf(),
order,
descr: descr.to_string(),
});
}
Ok((entry.dtype, entry.itemsize))
}
fn split_top_level(dict: &str) -> Vec<String> {
let body = dict
.trim()
.trim_matches(|c: char| c == '{' || c == '}')
.trim();
let mut fields = Vec::new();
let mut depth = 0i32;
let mut start = 0usize;
for (index, c) in body.char_indices() {
match c {
'(' => depth += 1,
')' => depth -= 1,
',' if depth == 0 => {
fields.push(body[start..index].to_string());
start = index + 1;
}
_ => {}
}
}
if start < body.len() {
fields.push(body[start..].to_string());
}
fields
}
fn parse_shape(value: &str) -> std::result::Result<Vec<usize>, String> {
let inner = value.trim().trim_matches(|c| c == '(' || c == ')');
inner
.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(|entry| {
entry
.parse::<usize>()
.map_err(|_| format!("shape entry `{entry}` is not a non-negative integer"))
})
.collect()
}