use std::collections::HashMap;
use crate::convert::TryToUsize;
use crate::file_writer::AttrValue;
use crate::mat::error::MatError;
use crate::mat::string_object::{
MATLAB_CLASS_STRING, MATLAB_OBJECT_DECODE_OPAQUE, MATLAB_STRING_SAVEOBJ_VERSION,
MCOS_MAGIC_NUMBER,
};
use crate::mat::value::{MatValue, NumVec, ScalarNum};
use crate::reader::{Dataset, File, Object};
use crate::types::DType;
const MCOS_RESERVED_CELL_PREFIX: usize = 2;
const FILEWRAPPER_HEADER_LEN: usize = 40;
const FILEWRAPPER_NUM_OFFSETS: usize = 8;
const CLASS_ENTRY_WORDS: usize = 4;
const OBJECT_ENTRY_WORDS: usize = 6;
const PROP_TRIPLE_WORDS: usize = 3;
const FIELD_TYPE_NAME: u32 = 0;
const FIELD_TYPE_HEAP: u32 = 1;
const FIELD_TYPE_INLINE: u32 = 2;
pub(crate) struct Mcos<'f> {
cells: Vec<Object<'f>>,
wrapper: FileWrapper,
}
struct FileWrapper {
names: Vec<String>,
classes: Vec<ClassEntry>,
objects: Vec<ObjectRecord>,
seg1: Vec<PropBlock>,
seg2: Vec<PropBlock>,
}
struct ClassEntry {
namespace_idx: u32,
name_idx: u32,
}
struct ObjectRecord {
class_id: u32,
saveobj_id: u32,
normalobj_id: u32,
#[expect(
dead_code,
reason = "dependency_id (R5 dynamic-property link) is parsed for completeness; \
the decoded value classes do not use dynamic properties yet"
)]
dependency_id: u32,
}
struct PropBlock {
props: Vec<Triple>,
}
struct Triple {
name_idx: u32,
field_type: u32,
value: u32,
}
pub(crate) struct PropRef {
pub(crate) name: String,
pub(crate) value: PropValue,
}
pub(crate) enum PropValue {
Heap(u32),
Inline(u32),
Name(String),
}
impl<'f> Mcos<'f> {
pub(crate) fn parse(file: &'f File) -> Result<Option<Self>, MatError> {
let subsystem = match file.group("#subsystem#") {
Ok(g) => g,
Err(_) => return Ok(None),
};
let mcos = subsystem.dataset("MCOS").map_err(MatError::Hdf5)?;
let cells = mcos.dereference().map_err(MatError::Hdf5)?;
let blob = match cells.first() {
Some(Object::Dataset(d)) => d.read_u8().map_err(MatError::Hdf5)?,
Some(Object::Group(_)) => {
return Err(MatError::Custom(
"#subsystem#/MCOS cell 0 is a group; expected the FileWrapper metadata blob"
.into(),
));
}
None => {
return Err(MatError::Custom(
"#subsystem#/MCOS store is empty; expected at least the FileWrapper blob"
.into(),
));
}
};
let wrapper = FileWrapper::parse(&blob)?;
Ok(Some(Self { cells, wrapper }))
}
pub(crate) fn class_name(&self, class_id: u32) -> Result<String, MatError> {
let entry = self
.wrapper
.classes
.get(class_id.to_usize()?)
.ok_or_else(|| MatError::Custom(format!("MCOS class id {class_id} out of range")))?;
let name = self.name(entry.name_idx)?;
if entry.namespace_idx != 0 {
Ok(format!("{}.{name}", self.name(entry.namespace_idx)?))
} else {
Ok(name)
}
}
pub(crate) fn object_class_name(&self, object_id: u32) -> Result<String, MatError> {
self.class_name(self.object_record(object_id)?.class_id)
}
pub(crate) fn properties(&self, object_id: u32) -> Result<Vec<PropRef>, MatError> {
let record = self.object_record(object_id)?;
let (blocks, block_id) = if record.saveobj_id != 0 {
(&self.wrapper.seg1, record.saveobj_id)
} else {
(&self.wrapper.seg2, record.normalobj_id)
};
if block_id == 0 {
return Ok(Vec::new());
}
let block = blocks.get(block_id.to_usize()?).ok_or_else(|| {
MatError::Custom(format!(
"MCOS object {object_id} references property block {block_id} out of range"
))
})?;
let mut out = Vec::with_capacity(block.props.len());
for triple in &block.props {
let name = self.name(triple.name_idx)?;
let value = match triple.field_type {
FIELD_TYPE_NAME => PropValue::Name(self.name(triple.value)?),
FIELD_TYPE_HEAP => PropValue::Heap(triple.value),
FIELD_TYPE_INLINE => PropValue::Inline(triple.value),
other => {
return Err(MatError::Custom(format!(
"MCOS property {name:?} has unknown field_type {other}"
)));
}
};
out.push(PropRef { name, value });
}
Ok(out)
}
pub(crate) fn heap_object(&self, value: u32) -> Result<&Object<'f>, MatError> {
let idx = MCOS_RESERVED_CELL_PREFIX
.checked_add(value.to_usize()?)
.ok_or_else(|| MatError::Custom("MCOS heap index overflow".into()))?;
self.cells.get(idx).ok_or_else(|| {
MatError::Custom(format!(
"MCOS heap value {value} maps to cell {idx}, but the store has {} cells",
self.cells.len()
))
})
}
fn object_record(&self, object_id: u32) -> Result<&ObjectRecord, MatError> {
if object_id == 0 {
return Err(MatError::Custom(
"opaque object id 0 is invalid (ids are 1-based)".into(),
));
}
self.wrapper
.objects
.get(object_id.to_usize()?)
.ok_or_else(|| {
MatError::Custom(format!(
"MCOS object id {object_id} out of range ({} objects)",
self.wrapper.objects.len().saturating_sub(1)
))
})
}
pub(crate) fn name(&self, idx: u32) -> Result<String, MatError> {
if idx == 0 {
return Ok(String::new());
}
self.wrapper
.names
.get(idx.to_usize()? - 1)
.cloned()
.ok_or_else(|| MatError::Custom(format!("MCOS name index {idx} out of range")))
}
pub(crate) fn decode_string(&self, ds: &Dataset<'_>) -> Result<MatValue, MatError> {
let metadata = ds.read_u32().map_err(MatError::Hdf5)?;
let object_ids = parse_opaque_metadata(&metadata)?.object_ids;
let mut values: Vec<String> = Vec::new();
for object_id in object_ids {
let payload = self.saveobj_payload(object_id)?;
values.extend(decode_string_saveobj(&payload)?);
}
Ok(match values.len() {
1 => MatValue::String(values.pop().expect("len checked")),
_ => MatValue::Cell(values.into_iter().map(MatValue::String).collect()),
})
}
fn saveobj_payload(&self, object_id: u32) -> Result<Vec<u64>, MatError> {
if object_id == 0 {
return Err(MatError::Custom(
"opaque object id 0 is invalid (ids are 1-based)".into(),
));
}
let idx = MCOS_RESERVED_CELL_PREFIX + (object_id.to_usize()? - 1);
let cell = self.cells.get(idx).ok_or_else(|| {
MatError::Custom(format!(
"opaque object id {object_id} maps to MCOS cell {idx}, but the store has {} cells",
self.cells.len()
))
})?;
match cell {
Object::Dataset(d) => {
let dtype = d.dtype().map_err(MatError::Hdf5)?;
if dtype != DType::U64 {
return Err(MatError::Custom(format!(
"MCOS saveobj cell {idx} has datatype {dtype:?}; expected uint64"
)));
}
d.read_u64().map_err(MatError::Hdf5)
}
Object::Group(_) => Err(MatError::Custom(format!(
"MCOS cell {idx} is a group; expected a saveobj dataset"
))),
}
}
}
impl FileWrapper {
fn parse(blob: &[u8]) -> Result<Self, MatError> {
if blob.len() < FILEWRAPPER_HEADER_LEN {
return Err(MatError::Custom(format!(
"FileWrapper blob too short: {} bytes (need at least {FILEWRAPPER_HEADER_LEN})",
blob.len()
)));
}
let num_strings = read_u32(blob, 4)?.to_usize()?;
let mut offsets = [0usize; FILEWRAPPER_NUM_OFFSETS];
for (i, slot) in offsets.iter_mut().enumerate() {
let v = read_u32(blob, 8 + i * 4)?.to_usize()?;
if v > blob.len() {
return Err(MatError::Custom(format!(
"FileWrapper region offset {i} = {v} exceeds blob length {}",
blob.len()
)));
}
*slot = v;
}
if offsets[0] < FILEWRAPPER_HEADER_LEN {
return Err(MatError::Custom(
"FileWrapper name-heap offset overlaps the header".into(),
));
}
for w in offsets.windows(2) {
if w[1] < w[0] {
return Err(MatError::Custom(
"FileWrapper region offsets are not non-decreasing".into(),
));
}
}
let names = parse_names(blob, FILEWRAPPER_HEADER_LEN, offsets[0], num_strings)?;
let classes = parse_class_table(blob, offsets[0], offsets[1])?;
let seg1 = parse_property_blocks(blob, offsets[1], offsets[2])?;
let objects = parse_object_table(blob, offsets[2], offsets[3])?;
let seg2 = parse_property_blocks(blob, offsets[3], offsets[4])?;
Ok(Self {
names,
classes,
objects,
seg1,
seg2,
})
}
}
fn parse_names(blob: &[u8], start: usize, end: usize, num: usize) -> Result<Vec<String>, MatError> {
let region = blob
.get(start..end)
.ok_or_else(|| MatError::Custom("FileWrapper name heap out of range".into()))?;
let mut names = Vec::with_capacity(num);
let mut iter = region.split(|&b| b == 0);
for _ in 0..num {
let bytes = iter.next().ok_or_else(|| {
MatError::Custom("FileWrapper name heap ended before all names were read".into())
})?;
names.push(String::from_utf8_lossy(bytes).into_owned());
}
Ok(names)
}
fn parse_class_table(blob: &[u8], start: usize, end: usize) -> Result<Vec<ClassEntry>, MatError> {
let stride = CLASS_ENTRY_WORDS * 4;
let mut classes = Vec::new();
let mut off = start;
while off + stride <= end {
classes.push(ClassEntry {
namespace_idx: read_u32(blob, off)?,
name_idx: read_u32(blob, off + 4)?,
});
off += stride;
}
Ok(classes)
}
fn parse_object_table(
blob: &[u8],
start: usize,
end: usize,
) -> Result<Vec<ObjectRecord>, MatError> {
let stride = OBJECT_ENTRY_WORDS * 4;
let mut objects = Vec::new();
let mut off = start;
while off + stride <= end {
objects.push(ObjectRecord {
class_id: read_u32(blob, off)?,
saveobj_id: read_u32(blob, off + 12)?,
normalobj_id: read_u32(blob, off + 16)?,
dependency_id: read_u32(blob, off + 20)?,
});
off += stride;
}
Ok(objects)
}
fn parse_property_blocks(
blob: &[u8],
start: usize,
end: usize,
) -> Result<Vec<PropBlock>, MatError> {
let triple_stride = PROP_TRIPLE_WORDS * 4;
let mut blocks = Vec::new();
let mut pos = start;
while pos + 4 <= end {
let nprops = read_u32(blob, pos)?.to_usize()?;
pos += 4;
let triples_bytes = nprops
.checked_mul(triple_stride)
.ok_or_else(|| MatError::Custom("MCOS property block size overflow".into()))?;
let triples_end = pos
.checked_add(triples_bytes)
.ok_or_else(|| MatError::Custom("MCOS property block size overflow".into()))?;
if triples_end > end {
return Err(MatError::Custom(
"MCOS property block overruns its region".into(),
));
}
let mut props = Vec::with_capacity(nprops);
for _ in 0..nprops {
props.push(Triple {
name_idx: read_u32(blob, pos)?,
field_type: read_u32(blob, pos + 4)?,
value: read_u32(blob, pos + 8)?,
});
pos += triple_stride;
}
blocks.push(PropBlock { props });
let rem = pos % 8;
if rem != 0 {
pos += 8 - rem;
}
}
Ok(blocks)
}
fn read_u32(blob: &[u8], off: usize) -> Result<u32, MatError> {
let bytes = blob
.get(off..off + 4)
.ok_or_else(|| MatError::Custom(format!("FileWrapper read past end at byte {off}")))?;
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
pub(crate) fn inline_to_scalar(value: u32) -> ScalarNum {
if value <= 1 {
ScalarNum::Bool(value != 0)
} else {
ScalarNum::U32(value)
}
}
pub(crate) fn decode_object_fields(
class_name: String,
fields: Vec<(String, MatValue)>,
) -> Result<MatValue, MatError> {
match class_name.as_str() {
"string" => decode_string_object(fields),
"datetime" => decode_datetime(class_name, fields),
"duration" => decode_duration(class_name, fields),
"categorical" => decode_categorical(class_name, fields),
"table" => Ok(decode_table(class_name, fields)),
"timetable" => Ok(decode_timetable(class_name, fields)),
"containers.Map" => Ok(decode_containermap(class_name, fields)),
_ => Ok(MatValue::Opaque { class_name, fields }),
}
}
pub(crate) fn build_enum_value(class_name: String, names: Vec<String>) -> MatValue {
let name_cells = names.into_iter().map(MatValue::String).collect();
MatValue::Opaque {
fields: vec![
(
"class_name".to_owned(),
MatValue::String(class_name.clone()),
),
("names".to_owned(), MatValue::Cell(name_cells)),
],
class_name,
}
}
pub(crate) const TABLE_META_KEY: &str = "@__hdf5_pure_table_meta__";
fn decode_table(class_name: String, mut fields: Vec<(String, MatValue)>) -> MatValue {
if let Some(MatValue::Struct(inner)) = take_field(&mut fields, "any") {
fields = inner;
}
let columns = match take_field(&mut fields, "data") {
Some(MatValue::Cell(cols)) => cols,
Some(other) => vec![other],
None => Vec::new(),
};
let names = field_strings(&mut fields, &["varnames", "varNames"]);
let row_names = take_field(&mut fields, "rownames").map(cell_of_strings);
let num_rows = take_field(&mut fields, "nrows");
keyed_table(class_name, columns, &names, row_names, num_rows, None)
}
fn decode_timetable(class_name: String, mut fields: Vec<(String, MatValue)>) -> MatValue {
if let Some(MatValue::Struct(inner)) = take_field(&mut fields, "any") {
fields = inner;
}
let columns = match take_field(&mut fields, "data") {
Some(MatValue::Cell(cols)) => cols,
Some(other) => vec![other],
None => Vec::new(),
};
let names = field_strings(&mut fields, &["varNames", "varnames"]);
let row_times = take_field(&mut fields, "rowTimes");
let num_rows = take_field(&mut fields, "numRows").or_else(|| take_field(&mut fields, "nrows"));
keyed_table(class_name, columns, &names, None, num_rows, row_times)
}
fn keyed_table(
class_name: String,
columns: Vec<MatValue>,
names: &[String],
row_names: Option<MatValue>,
num_rows: Option<MatValue>,
row_times: Option<MatValue>,
) -> MatValue {
let mut out: Vec<(String, MatValue)> = Vec::with_capacity(columns.len() + 1);
for (i, col) in columns.into_iter().enumerate() {
let name = names
.get(i)
.cloned()
.unwrap_or_else(|| format!("Var{}", i + 1));
out.push((name, col));
}
let mut meta: Vec<(String, MatValue)> = Vec::new();
if let Some(rn) = row_names {
meta.push(("row_names".to_owned(), rn));
}
if let Some(rt) = row_times {
meta.push(("row_times".to_owned(), rt));
}
if let Some(nr) = num_rows {
meta.push(("num_rows".to_owned(), nr));
}
out.push((TABLE_META_KEY.to_owned(), MatValue::Struct(meta)));
MatValue::Opaque {
class_name,
fields: out,
}
}
fn field_strings(fields: &mut Vec<(String, MatValue)>, candidates: &[&str]) -> Vec<String> {
for name in candidates {
if let Some(value) = take_field(fields, name) {
return matvalue_strings(value);
}
}
Vec::new()
}
fn cell_of_strings(value: MatValue) -> MatValue {
MatValue::Cell(
matvalue_strings(value)
.into_iter()
.map(MatValue::String)
.collect(),
)
}
fn matvalue_strings(value: MatValue) -> Vec<String> {
match value {
MatValue::String(s) => vec![s],
MatValue::Cell(elems) => elems
.into_iter()
.map(|e| match e {
MatValue::String(s) => s,
_ => String::new(),
})
.collect(),
_ => Vec::new(),
}
}
fn decode_string_object(mut fields: Vec<(String, MatValue)>) -> Result<MatValue, MatError> {
let payload =
take_field(&mut fields, "any").or_else(|| fields.into_iter().next().map(|(_, v)| v));
let units = match payload {
Some(value) => matvalue_to_u64(value)?,
None => return Ok(MatValue::String(String::new())),
};
let mut values = decode_string_saveobj(&units)?;
Ok(match values.len() {
1 => MatValue::String(values.pop().expect("len checked")),
_ => MatValue::Cell(values.into_iter().map(MatValue::String).collect()),
})
}
fn matvalue_to_u64(value: MatValue) -> Result<Vec<u64>, MatError> {
match value {
MatValue::Vec1D(NumVec::U64(v)) => Ok(v),
MatValue::Matrix {
vec: NumVec::U64(v),
..
} => Ok(v),
MatValue::Scalar(ScalarNum::U64(v)) => Ok(vec![v]),
other => Err(MatError::Custom(format!(
"string object payload is not a uint64 array (got {})",
other.kind()
))),
}
}
fn decode_datetime(
class_name: String,
mut fields: Vec<(String, MatValue)>,
) -> Result<MatValue, MatError> {
let pairs = match take_field(&mut fields, "data") {
Some(data) => complex_pairs(data, "datetime `data`")?,
None => Vec::new(),
};
let (millis_utc, sub_ms): (Vec<f64>, Vec<f64>) = pairs.into_iter().unzip();
let mut out = vec![
(
"millis_utc".to_owned(),
MatValue::Vec1D(NumVec::F64(millis_utc)),
),
("sub_ms".to_owned(), MatValue::Vec1D(NumVec::F64(sub_ms))),
];
if let Some(tz) = take_field(&mut fields, "tz").or_else(|| take_field(&mut fields, "tmz")) {
out.push(("tz".to_owned(), string_or_empty(tz)));
}
if let Some(fmt) = take_field(&mut fields, "fmt") {
out.push(("fmt".to_owned(), string_or_empty(fmt)));
}
Ok(MatValue::Opaque {
class_name,
fields: out,
})
}
fn decode_duration(
class_name: String,
mut fields: Vec<(String, MatValue)>,
) -> Result<MatValue, MatError> {
let millis = match take_field(&mut fields, "millis") {
Some(millis) => numeric_f64_vec(millis, "duration `millis`")?,
None => Vec::new(),
};
let mut out = vec![("millis".to_owned(), MatValue::Vec1D(NumVec::F64(millis)))];
if let Some(fmt) = take_field(&mut fields, "fmt") {
out.push(("fmt".to_owned(), string_or_empty(fmt)));
}
Ok(MatValue::Opaque {
class_name,
fields: out,
})
}
fn decode_categorical(
class_name: String,
mut fields: Vec<(String, MatValue)>,
) -> Result<MatValue, MatError> {
let codes = take_field(&mut fields, "codes")
.map(flatten_to_1d)
.unwrap_or_else(|| MatValue::Vec1D(NumVec::U32(Vec::new())));
let categories = take_field(&mut fields, "categoryNames")
.or_else(|| take_field(&mut fields, "categories"))
.unwrap_or(MatValue::Cell(Vec::new()));
let is_ordinal =
take_field(&mut fields, "isOrdinal").unwrap_or(MatValue::Scalar(ScalarNum::Bool(false)));
let is_protected =
take_field(&mut fields, "isProtected").unwrap_or(MatValue::Scalar(ScalarNum::Bool(false)));
Ok(MatValue::Opaque {
class_name,
fields: vec![
("codes".to_owned(), codes),
("categories".to_owned(), categories),
("is_ordinal".to_owned(), is_ordinal),
("is_protected".to_owned(), is_protected),
],
})
}
fn decode_containermap(class_name: String, mut fields: Vec<(String, MatValue)>) -> MatValue {
let mut ser = match take_field(&mut fields, "serialization") {
Some(MatValue::Struct(inner)) => inner,
_ => return MatValue::Opaque { class_name, fields },
};
let (keys_raw, values_raw) =
match (take_field(&mut ser, "keys"), take_field(&mut ser, "values")) {
(Some(k), Some(v)) => (k, v),
(k, v) => return containermap_lossless(class_name, fields, ser, k, v),
};
let key_strings = map_key_strings(&keys_raw);
if key_strings.len() != value_split_count(&values_raw) || has_duplicate_key(&key_strings) {
return containermap_lossless(class_name, fields, ser, Some(keys_raw), Some(values_raw));
}
let entries = key_strings
.into_iter()
.zip(map_values(values_raw))
.collect();
MatValue::Opaque {
class_name,
fields: entries,
}
}
fn containermap_lossless(
class_name: String,
mut fields: Vec<(String, MatValue)>,
mut ser: Vec<(String, MatValue)>,
keys: Option<MatValue>,
values: Option<MatValue>,
) -> MatValue {
if let Some(k) = keys {
ser.push(("keys".to_owned(), k));
}
if let Some(v) = values {
ser.push(("values".to_owned(), v));
}
fields.push(("serialization".to_owned(), MatValue::Struct(ser)));
MatValue::Opaque { class_name, fields }
}
fn map_key_strings(value: &MatValue) -> Vec<String> {
match value {
MatValue::String(s) => vec![s.clone()],
MatValue::Scalar(s) => vec![scalar_key_string(s)],
MatValue::Vec1D(v) | MatValue::Matrix { vec: v, .. } => numvec_key_strings(v),
MatValue::Cell(elems) => elems
.iter()
.map(|e| match e {
MatValue::String(s) => s.clone(),
MatValue::Scalar(s) => scalar_key_string(s),
_ => String::new(),
})
.collect(),
_ => Vec::new(),
}
}
fn value_split_count(value: &MatValue) -> usize {
match value {
MatValue::Cell(elems) => elems.len(),
MatValue::Vec1D(v) | MatValue::Matrix { vec: v, .. } => v.len(),
_ => 1,
}
}
fn has_duplicate_key(keys: &[String]) -> bool {
let mut seen = std::collections::HashSet::with_capacity(keys.len());
!keys.iter().all(|k| seen.insert(k))
}
fn map_values(value: MatValue) -> Vec<MatValue> {
match value {
MatValue::Cell(elems) => elems,
MatValue::Vec1D(v) | MatValue::Matrix { vec: v, .. } => numvec_to_scalars(v),
other @ (MatValue::Scalar(_) | MatValue::String(_)) => vec![other],
other => vec![other],
}
}
fn scalar_key_string(s: &ScalarNum) -> String {
match s {
ScalarNum::F64(x) => format!("{x}"),
ScalarNum::F32(x) => format!("{x}"),
ScalarNum::I64(x) => x.to_string(),
ScalarNum::I32(x) => x.to_string(),
ScalarNum::I16(x) => x.to_string(),
ScalarNum::I8(x) => x.to_string(),
ScalarNum::U64(x) => x.to_string(),
ScalarNum::U32(x) => x.to_string(),
ScalarNum::U16(x) => x.to_string(),
ScalarNum::U8(x) => x.to_string(),
ScalarNum::Bool(b) => u8::from(*b).to_string(),
}
}
fn numvec_key_strings(v: &NumVec) -> Vec<String> {
match v {
NumVec::F64(xs) => xs.iter().map(|x| format!("{x}")).collect(),
NumVec::F32(xs) => xs.iter().map(|x| format!("{x}")).collect(),
NumVec::I64(xs) => xs.iter().map(ToString::to_string).collect(),
NumVec::I32(xs) => xs.iter().map(ToString::to_string).collect(),
NumVec::I16(xs) => xs.iter().map(ToString::to_string).collect(),
NumVec::I8(xs) => xs.iter().map(ToString::to_string).collect(),
NumVec::U64(xs) => xs.iter().map(ToString::to_string).collect(),
NumVec::U32(xs) => xs.iter().map(ToString::to_string).collect(),
NumVec::U16(xs) => xs.iter().map(ToString::to_string).collect(),
NumVec::U8(xs) => xs.iter().map(ToString::to_string).collect(),
NumVec::Bool(xs) => xs.iter().map(|b| u8::from(*b).to_string()).collect(),
}
}
fn numvec_to_scalars(v: NumVec) -> Vec<MatValue> {
match v {
NumVec::F64(xs) => xs.into_iter().map(|x| scalar(ScalarNum::F64(x))).collect(),
NumVec::F32(xs) => xs.into_iter().map(|x| scalar(ScalarNum::F32(x))).collect(),
NumVec::I64(xs) => xs.into_iter().map(|x| scalar(ScalarNum::I64(x))).collect(),
NumVec::I32(xs) => xs.into_iter().map(|x| scalar(ScalarNum::I32(x))).collect(),
NumVec::I16(xs) => xs.into_iter().map(|x| scalar(ScalarNum::I16(x))).collect(),
NumVec::I8(xs) => xs.into_iter().map(|x| scalar(ScalarNum::I8(x))).collect(),
NumVec::U64(xs) => xs.into_iter().map(|x| scalar(ScalarNum::U64(x))).collect(),
NumVec::U32(xs) => xs.into_iter().map(|x| scalar(ScalarNum::U32(x))).collect(),
NumVec::U16(xs) => xs.into_iter().map(|x| scalar(ScalarNum::U16(x))).collect(),
NumVec::U8(xs) => xs.into_iter().map(|x| scalar(ScalarNum::U8(x))).collect(),
NumVec::Bool(xs) => xs.into_iter().map(|b| scalar(ScalarNum::Bool(b))).collect(),
}
}
fn scalar(s: ScalarNum) -> MatValue {
MatValue::Scalar(s)
}
fn take_field(fields: &mut Vec<(String, MatValue)>, name: &str) -> Option<MatValue> {
let pos = fields.iter().position(|(n, _)| n == name)?;
Some(fields.remove(pos).1)
}
fn string_or_empty(value: MatValue) -> MatValue {
match value {
MatValue::String(_) => value,
_ => MatValue::String(String::new()),
}
}
fn numeric_f64_vec(value: MatValue, what: &str) -> Result<Vec<f64>, MatError> {
match value {
MatValue::Scalar(s) => Ok(vec![scalar_to_f64(s)]),
MatValue::Vec1D(v) => Ok(numvec_to_f64(v)),
MatValue::Matrix { vec, .. } => Ok(numvec_to_f64(vec)),
other => Err(MatError::Custom(format!(
"expected a real numeric array for {what}, got {}",
other.kind()
))),
}
}
fn complex_pairs(value: MatValue, what: &str) -> Result<Vec<(f64, f64)>, MatError> {
Ok(match value {
MatValue::ComplexScalar64 { re, im } => vec![(re, im)],
MatValue::ComplexScalar32 { re, im } => vec![(f64::from(re), f64::from(im))],
MatValue::ComplexVec64(pairs) => pairs,
MatValue::ComplexVec32(pairs) => pairs
.into_iter()
.map(|(r, i)| (f64::from(r), f64::from(i)))
.collect(),
MatValue::ComplexMatrix64 { pairs, .. } => pairs,
MatValue::ComplexMatrix32 { pairs, .. } => pairs
.into_iter()
.map(|(r, i)| (f64::from(r), f64::from(i)))
.collect(),
MatValue::Scalar(s) => vec![(scalar_to_f64(s), 0.0)],
MatValue::Vec1D(v) => numvec_to_f64(v).into_iter().map(|r| (r, 0.0)).collect(),
MatValue::Matrix { vec, .. } => numvec_to_f64(vec).into_iter().map(|r| (r, 0.0)).collect(),
other => {
return Err(MatError::Custom(format!(
"expected a complex double array for {what}, got {}",
other.kind()
)));
}
})
}
fn flatten_to_1d(value: MatValue) -> MatValue {
match value {
MatValue::Matrix { vec, .. } => MatValue::Vec1D(vec),
MatValue::Scalar(s) => MatValue::Vec1D(scalar_to_numvec(s)),
other => other,
}
}
fn scalar_to_numvec(s: ScalarNum) -> NumVec {
match s {
ScalarNum::F64(x) => NumVec::F64(vec![x]),
ScalarNum::F32(x) => NumVec::F32(vec![x]),
ScalarNum::I64(x) => NumVec::I64(vec![x]),
ScalarNum::I32(x) => NumVec::I32(vec![x]),
ScalarNum::I16(x) => NumVec::I16(vec![x]),
ScalarNum::I8(x) => NumVec::I8(vec![x]),
ScalarNum::U64(x) => NumVec::U64(vec![x]),
ScalarNum::U32(x) => NumVec::U32(vec![x]),
ScalarNum::U16(x) => NumVec::U16(vec![x]),
ScalarNum::U8(x) => NumVec::U8(vec![x]),
ScalarNum::Bool(b) => NumVec::Bool(vec![b]),
}
}
fn numvec_to_f64(v: NumVec) -> Vec<f64> {
match v {
NumVec::F64(x) => x,
NumVec::F32(x) => x.into_iter().map(f64::from).collect(),
NumVec::I64(x) => x.into_iter().map(|v| v as f64).collect(),
NumVec::I32(x) => x.into_iter().map(f64::from).collect(),
NumVec::I16(x) => x.into_iter().map(f64::from).collect(),
NumVec::I8(x) => x.into_iter().map(f64::from).collect(),
NumVec::U64(x) => x.into_iter().map(|v| v as f64).collect(),
NumVec::U32(x) => x.into_iter().map(f64::from).collect(),
NumVec::U16(x) => x.into_iter().map(f64::from).collect(),
NumVec::U8(x) => x.into_iter().map(f64::from).collect(),
NumVec::Bool(x) => x.into_iter().map(|b| if b { 1.0 } else { 0.0 }).collect(),
}
}
fn scalar_to_f64(s: ScalarNum) -> f64 {
match s {
ScalarNum::F64(x) => x,
ScalarNum::F32(x) => f64::from(x),
ScalarNum::I64(x) => x as f64,
ScalarNum::I32(x) => f64::from(x),
ScalarNum::I16(x) => f64::from(x),
ScalarNum::I8(x) => f64::from(x),
ScalarNum::U64(x) => x as f64,
ScalarNum::U32(x) => f64::from(x),
ScalarNum::U16(x) => f64::from(x),
ScalarNum::U8(x) => f64::from(x),
ScalarNum::Bool(b) => {
if b {
1.0
} else {
0.0
}
}
}
}
pub(crate) struct OpaqueMeta {
pub(crate) object_ids: Vec<u32>,
pub(crate) class_id: u32,
}
pub(crate) fn parse_opaque_metadata(data: &[u32]) -> Result<OpaqueMeta, MatError> {
if data.len() < 3 {
return Err(MatError::Custom(format!(
"opaque metadata too short: {} words",
data.len()
)));
}
if data[0] != MCOS_MAGIC_NUMBER {
return Err(MatError::Custom(format!(
"opaque metadata magic mismatch: {:#x}",
data[0]
)));
}
let ndims = (data[1] as u64).to_usize()?;
let dims_end = 2usize
.checked_add(ndims)
.ok_or_else(|| MatError::Custom("opaque metadata ndims overflow".into()))?;
if data.len() < dims_end + 1 {
return Err(MatError::Custom(
"opaque metadata truncated before object ids".into(),
));
}
let num_objects = checked_product(&data[2..dims_end])?;
let ids_end = dims_end
.checked_add(num_objects)
.ok_or_else(|| MatError::Custom("opaque metadata object count overflow".into()))?;
if data.len() != ids_end + 1 {
return Err(MatError::Custom(format!(
"opaque metadata length {} does not match header (expected {})",
data.len(),
ids_end + 1
)));
}
Ok(OpaqueMeta {
object_ids: data[dims_end..ids_end].to_vec(),
class_id: data[ids_end],
})
}
fn decode_string_saveobj(payload: &[u64]) -> Result<Vec<String>, MatError> {
if payload.len() < 2 {
return Err(MatError::Custom("string saveobj payload too short".into()));
}
if payload[0] != MATLAB_STRING_SAVEOBJ_VERSION {
return Err(MatError::Custom(format!(
"unsupported string saveobj version: {}",
payload[0]
)));
}
let ndims = payload[1].to_usize()?;
let dims_end = 2usize
.checked_add(ndims)
.ok_or_else(|| MatError::Custom("string saveobj ndims overflow".into()))?;
if payload.len() < dims_end {
return Err(MatError::Custom(
"string saveobj truncated before lengths".into(),
));
}
let count = checked_product_u64(&payload[2..dims_end])?;
let lens_end = dims_end
.checked_add(count)
.ok_or_else(|| MatError::Custom("string saveobj count overflow".into()))?;
if payload.len() < lens_end {
return Err(MatError::Custom(
"string saveobj truncated before code units".into(),
));
}
let lens = &payload[dims_end..lens_end];
let mut total_units: usize = 0;
for &len in lens {
if len != u64::MAX {
total_units = total_units
.checked_add(len.to_usize()?)
.ok_or_else(|| MatError::Custom("string saveobj unit count overflow".into()))?;
}
}
let mut units: Vec<u16> = Vec::with_capacity(total_units);
'words: for &word in &payload[lens_end..] {
for shift in [0u32, 16, 32, 48] {
if units.len() == total_units {
break 'words;
}
#[expect(
clippy::cast_possible_truncation,
reason = "extracting one packed 16-bit UTF-16 code unit from a 64-bit word"
)]
units.push((word >> shift) as u16);
}
}
if units.len() < total_units {
return Err(MatError::Custom(
"string saveobj code-unit data is shorter than the declared lengths".into(),
));
}
let mut strings = Vec::with_capacity(lens.len());
let mut pos = 0usize;
for &len in lens {
if len == u64::MAX {
strings.push(String::new());
continue;
}
let len = len.to_usize()?;
let slice = &units[pos..pos + len];
pos += len;
let s = String::from_utf16(slice).map_err(|e| MatError::Utf16Decode(e.to_string()))?;
strings.push(s);
}
Ok(strings)
}
pub(crate) fn raw_matlab_class(attrs: &HashMap<String, AttrValue>) -> Option<String> {
match attrs.get("MATLAB_class") {
Some(AttrValue::AsciiString(s)) | Some(AttrValue::String(s)) => Some(s.clone()),
Some(AttrValue::StringArray(v)) if v.len() == 1 => Some(v[0].clone()),
_ => None,
}
}
pub(crate) fn matlab_object_decode(attrs: &HashMap<String, AttrValue>) -> Option<i64> {
let v = match attrs.get("MATLAB_object_decode")? {
AttrValue::I64(v) => *v,
AttrValue::I32(v) => i64::from(*v),
AttrValue::U32(v) => i64::from(*v),
AttrValue::U64(v) => i64::try_from(*v).ok()?,
_ => return None,
};
(v != 0).then_some(v)
}
pub(crate) fn is_mcos_decode(decode: i64) -> bool {
decode == i64::from(MATLAB_OBJECT_DECODE_OPAQUE)
}
pub(crate) fn is_string_class(class: &str, decode: i64) -> bool {
is_mcos_decode(decode) && class == MATLAB_CLASS_STRING
}
fn checked_product(values: &[u32]) -> Result<usize, MatError> {
let mut acc = 1usize;
for &v in values {
acc = acc
.checked_mul((v as u64).to_usize()?)
.ok_or_else(|| MatError::Custom("opaque dimension product overflow".into()))?;
}
Ok(acc)
}
fn checked_product_u64(values: &[u64]) -> Result<usize, MatError> {
let mut acc = 1usize;
for &v in values {
acc = acc
.checked_mul(v.to_usize()?)
.ok_or_else(|| MatError::Custom("string saveobj dimension product overflow".into()))?;
}
Ok(acc)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mat::string_object::build_string_filewrapper_metadata;
#[test]
fn parses_production_string_filewrapper_blob() {
let blob = build_string_filewrapper_metadata(1);
let fw = FileWrapper::parse(&blob).expect("production blob must parse");
assert_eq!(fw.names, vec!["any".to_owned(), "string".to_owned()]);
assert_eq!(fw.classes.len(), 2);
assert_eq!(fw.classes[1].namespace_idx, 0);
assert_eq!(fw.classes[1].name_idx, 2);
assert_eq!(fw.objects.len(), 2);
assert_eq!(fw.objects[1].class_id, 1);
assert_eq!(fw.objects[1].saveobj_id, 1);
assert_eq!(fw.objects[1].normalobj_id, 0);
assert_eq!(fw.seg1.len(), 2);
let block = &fw.seg1[1];
assert_eq!(block.props.len(), 1);
assert_eq!(block.props[0].field_type, FIELD_TYPE_HEAP);
assert_eq!(block.props[0].value, 0);
}
#[test]
fn parses_multi_object_production_blob() {
let blob = build_string_filewrapper_metadata(3);
let fw = FileWrapper::parse(&blob).expect("production blob must parse");
assert_eq!(fw.objects.len(), 4);
for id in 1..=3u32 {
assert_eq!(fw.objects[id as usize].class_id, 1);
assert_eq!(fw.objects[id as usize].saveobj_id, id);
}
assert_eq!(fw.seg1.len(), 4);
for id in 1..=3u32 {
let block = &fw.seg1[id as usize];
assert_eq!(block.props[0].value, id - 1);
}
}
#[test]
fn rejects_short_blob() {
assert!(matches!(
FileWrapper::parse(&[0u8; 8]),
Err(MatError::Custom(_))
));
}
#[test]
fn inline_logical_and_integer() {
assert!(matches!(inline_to_scalar(0), ScalarNum::Bool(false)));
assert!(matches!(inline_to_scalar(1), ScalarNum::Bool(true)));
assert!(matches!(inline_to_scalar(7), ScalarNum::U32(7)));
}
#[test]
fn parse_opaque_metadata_extracts_ids_and_class() {
let meta = [MCOS_MAGIC_NUMBER, 2, 1, 1, 4, 2];
let parsed = parse_opaque_metadata(&meta).unwrap();
assert_eq!(parsed.object_ids, vec![4]);
assert_eq!(parsed.class_id, 2);
}
#[test]
fn parse_opaque_metadata_rejects_bad_magic() {
let meta = [0x1234_5678, 2, 1, 1, 1, 1];
assert!(parse_opaque_metadata(&meta).is_err());
}
fn str_cell(items: &[&str]) -> MatValue {
MatValue::Cell(
items
.iter()
.map(|s| MatValue::String((*s).to_owned()))
.collect(),
)
}
fn f64_cell(items: &[f64]) -> MatValue {
MatValue::Cell(
items
.iter()
.map(|x| MatValue::Scalar(ScalarNum::F64(*x)))
.collect(),
)
}
fn serialization(keys: MatValue, values: MatValue) -> Vec<(String, MatValue)> {
vec![(
"serialization".to_owned(),
MatValue::Struct(vec![
("keys".to_owned(), keys),
("values".to_owned(), values),
]),
)]
}
#[test]
fn containermap_rekeys_to_key_value_fields() {
let v = decode_containermap(
"containers.Map".to_owned(),
serialization(str_cell(&["a", "b"]), f64_cell(&[1.0, 2.0])),
);
match v {
MatValue::Opaque { fields, .. } => {
let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(names, ["a", "b"]);
assert!(!names.contains(&"serialization"));
}
other => panic!("expected re-keyed opaque, got {}", other.kind()),
}
}
#[test]
fn containermap_falls_back_losslessly_on_count_mismatch() {
let v = decode_containermap(
"containers.Map".to_owned(),
serialization(str_cell(&["a", "b"]), f64_cell(&[1.0])),
);
match v {
MatValue::Opaque { fields, .. } => {
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].0, "serialization");
}
other => panic!("expected lossless opaque, got {}", other.kind()),
}
}
#[test]
fn containermap_falls_back_losslessly_on_duplicate_keys() {
let v = decode_containermap(
"containers.Map".to_owned(),
serialization(f64_cell(&[1.0, 1.0]), str_cell(&["x", "y"])),
);
match v {
MatValue::Opaque { fields, .. } => {
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].0, "serialization");
}
other => panic!("expected lossless opaque, got {}", other.kind()),
}
}
}