use crate::mat::error::MatError;
pub const MCOS_MAGIC_NUMBER: u32 = 0xDD00_0000;
pub const FILEWRAPPER_VERSION: u32 = 4;
pub const MATLAB_STRING_SAVEOBJ_VERSION: u64 = 1;
pub const MATLAB_OBJECT_DECODE_OPAQUE: i32 = 3;
pub const MATLAB_CLASS_STRING: &str = "string";
pub const MATLAB_CLASS_FILEWRAPPER: &str = "FileWrapper__";
pub fn create_string_object_metadata(object_id: u32) -> [u32; 6] {
[MCOS_MAGIC_NUMBER, 2, 1, 1, object_id, 1]
}
pub fn encode_string_saveobj_payload(
values: &[String],
matlab_dims: &[usize],
) -> Result<Vec<u64>, MatError> {
let expected = product(matlab_dims).ok_or(MatError::Custom(
"string saveobj dims overflow usize".to_owned(),
))?;
if expected != values.len() {
return Err(MatError::Custom(format!(
"string payload length {} does not match product of dims {}",
values.len(),
expected
)));
}
let max_utf16_units: usize = values.iter().map(|s| s.len()).sum();
let header_count = 2 + matlab_dims.len() + values.len();
let mut payload = Vec::with_capacity(header_count + max_utf16_units.div_ceil(4));
payload.push(MATLAB_STRING_SAVEOBJ_VERSION);
payload.push(matlab_dims.len() as u64);
payload.extend(matlab_dims.iter().map(|&dim| dim as u64));
let mut units: Vec<u16> = Vec::with_capacity(max_utf16_units);
for value in values {
let before = units.len();
units.extend(value.encode_utf16());
payload.push((units.len() - before) as u64);
}
units.resize(units.len().next_multiple_of(4), 0);
for chunk in units.chunks_exact(4) {
let v = (chunk[0] as u64)
| ((chunk[1] as u64) << 16)
| ((chunk[2] as u64) << 32)
| ((chunk[3] as u64) << 48);
payload.push(v);
}
Ok(payload)
}
pub fn build_string_filewrapper_metadata(object_count: usize) -> Vec<u8> {
let mut names_bytes = b"any\0string\0".to_vec();
while !names_bytes.len().is_multiple_of(8) {
names_bytes.push(0);
}
let mut region_offsets = [0u32; 8];
let mut regions = Vec::new();
let version = u32_bytes(&[FILEWRAPPER_VERSION]);
let num_names = u32_bytes(&[2]);
let class_id_metadata = u32_bytes(&[0, 0, 0, 0, 0, 2, 0, 0]);
let mut saveobj_metadata = Vec::with_capacity(2 + object_count * 4);
saveobj_metadata.extend_from_slice(&[0, 0]);
for idx in 0..object_count {
#[expect(
clippy::cast_possible_truncation,
reason = "object index written into a 4-byte on-disk saveobj field; MAT files never hold more than u32::MAX objects"
)]
saveobj_metadata.extend_from_slice(&[1, 1, 1, idx as u32]);
}
let saveobj_metadata = u32_bytes(&saveobj_metadata);
let mut object_id_metadata = Vec::with_capacity(6 + object_count * 6);
object_id_metadata.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
#[expect(
clippy::cast_possible_truncation,
reason = "object count bounds a 4-byte on-disk object-id field; MAT files never hold more than u32::MAX objects"
)]
for id in 1..=object_count as u32 {
object_id_metadata.extend_from_slice(&[1, 0, 0, id, 0, id]);
}
let object_id_metadata = u32_bytes(&object_id_metadata);
let nobj_metadata = u32_bytes(&[0, 0]);
let mut dynprop_metadata = Vec::with_capacity(2 + object_count * 2);
dynprop_metadata.extend_from_slice(&[0, 0]);
for _ in 0..object_count {
dynprop_metadata.extend_from_slice(&[0, 0]);
}
let dynprop_metadata = u32_bytes(&dynprop_metadata);
let region6 = Vec::new();
let region7 = vec![0u8; 8];
#[expect(
clippy::cast_possible_truncation,
reason = "region lengths accumulated into 4-byte on-disk filewrapper offsets; \
metadata regions are far smaller than u32::MAX"
)]
{
let mut offset = 40u32 + names_bytes.len() as u32;
region_offsets[0] = offset;
offset += class_id_metadata.len() as u32;
region_offsets[1] = offset;
offset += saveobj_metadata.len() as u32;
region_offsets[2] = offset;
offset += object_id_metadata.len() as u32;
region_offsets[3] = offset;
offset += nobj_metadata.len() as u32;
region_offsets[4] = offset;
offset += dynprop_metadata.len() as u32;
region_offsets[5] = offset;
offset += region6.len() as u32;
region_offsets[6] = offset;
offset += region7.len() as u32;
region_offsets[7] = offset;
}
regions.extend_from_slice(&version);
regions.extend_from_slice(&num_names);
regions.extend_from_slice(&u32_bytes(®ion_offsets));
regions.extend_from_slice(&names_bytes);
regions.extend_from_slice(&class_id_metadata);
regions.extend_from_slice(&saveobj_metadata);
regions.extend_from_slice(&object_id_metadata);
regions.extend_from_slice(&nobj_metadata);
regions.extend_from_slice(&dynprop_metadata);
regions.extend_from_slice(®ion6);
regions.extend_from_slice(®ion7);
regions
}
fn u32_bytes(values: &[u32]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
fn product(extents: &[usize]) -> Option<usize> {
extents
.iter()
.try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metadata_layout_matches_matlab() {
assert_eq!(
create_string_object_metadata(7),
[MCOS_MAGIC_NUMBER, 2, 1, 1, 7, 1]
);
}
#[test]
fn saveobj_roundtrips_one_string() {
let payload = encode_string_saveobj_payload(&["hello".to_owned()], &[1, 1]).unwrap();
assert_eq!(&payload[..5], &[1, 2, 1, 1, 5]);
}
#[test]
fn saveobj_validates_dim_product() {
let err =
encode_string_saveobj_payload(&["a".to_owned(), "b".to_owned()], &[3, 1]).unwrap_err();
assert!(matches!(err, MatError::Custom(_)));
}
}