use std::{fs::OpenOptions, io::Read};
use super::*;
use crate::dtype::Dtype;
fn fresh_dir(tag: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("mlxrs-io-{tag}-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn save_then_load_safetensors_round_trips_values_shape_dtype() {
let dir = fresh_dir("st-roundtrip");
let path = dir.join("weights.safetensors");
let a = Array::from_slice::<f32>(&[1.0_f32, 2.0, 3.0, 4.0], &(2usize, 2)).unwrap();
let b = Array::from_slice::<i32>(&[10_i32, 20, 30], &(3usize,)).unwrap();
let mut arrays: HashMap<String, Array> = HashMap::new();
arrays.insert("a.weight".to_string(), a);
arrays.insert("b.bias".to_string(), b);
save_safetensors(&path, &arrays).unwrap();
assert!(path.exists());
let mut loaded = load_safetensors(&path).unwrap();
assert_eq!(loaded.len(), 2);
let la = loaded.get_mut("a.weight").unwrap();
assert_eq!(la.shape(), vec![2, 2]);
assert_eq!(la.dtype().unwrap(), Dtype::F32);
assert_eq!(la.to_vec::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
let lb = loaded.get_mut("b.bias").unwrap();
assert_eq!(lb.shape(), vec![3]);
assert_eq!(lb.dtype().unwrap(), Dtype::I32);
assert_eq!(lb.to_vec::<i32>().unwrap(), vec![10, 20, 30]);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn save_load_safetensors_with_metadata_round_trips_side_table() {
let dir = fresh_dir("st-meta-roundtrip");
let path = dir.join("weights.safetensors");
let w = Array::from_slice::<f32>(&[5.0_f32, 6.0], &(2usize,)).unwrap();
let mut arrays: HashMap<String, Array> = HashMap::new();
arrays.insert("w".to_string(), w);
let mut meta: HashMap<String, String> = HashMap::new();
meta.insert("format".to_string(), "mlx".to_string());
meta.insert("author".to_string(), "mlxrs-test".to_string());
save_safetensors_with_metadata(&path, &arrays, &meta).unwrap();
let (mut a2, m2) = load_safetensors_with_metadata(&path).unwrap();
let w_back = a2.get_mut("w").unwrap().to_vec::<f32>().unwrap();
assert_eq!(w_back, vec![5.0_f32, 6.0]);
assert_eq!(m2.get("format").map(String::as_str), Some("mlx"));
assert_eq!(m2.get("author").map(String::as_str), Some("mlxrs-test"));
let discarded = load_safetensors(&path).unwrap();
assert!(discarded.contains_key("w"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn load_safetensors_missing_file_errors() {
let dir = fresh_dir("st-missing");
let path = dir.join("does-not-exist.safetensors");
assert!(!path.exists());
assert!(load_safetensors(&path).is_err());
assert!(load_safetensors_with_metadata(&path).is_err());
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn save_safetensors_path_with_interior_nul_is_rejected() {
use std::os::unix::ffi::OsStrExt;
let p = std::path::Path::new(std::ffi::OsStr::from_bytes(b"weights\0.safetensors"));
let arrays: HashMap<String, Array> = HashMap::new();
match save_safetensors(p, &arrays).unwrap_err() {
Error::InteriorNul(payload) => {
assert_eq!(payload.context(), "io::path_cstring");
assert_eq!(payload.bytes_kind(), "path");
}
other => panic!("expected InteriorNul, got {other:?}"),
}
}
#[test]
fn save_safetensors_view_array_key_with_interior_nul_is_rejected() {
let dir = fresh_dir("st-nul-key");
let path = dir.join("w.safetensors");
let arr = Array::from_slice::<f32>(&[1.0_f32], &(1usize,)).unwrap();
let entries = std::iter::once(("bad\0key", &arr));
let meta: HashMap<String, String> = HashMap::new();
match save_safetensors_view(&path, entries, &meta).unwrap_err() {
Error::InteriorNul(payload) => {
assert_eq!(payload.context(), "io::map_arrays insert");
assert_eq!(payload.bytes_kind(), "array key");
}
other => panic!("expected InteriorNul(array key), got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn save_safetensors_metadata_key_with_interior_nul_is_rejected() {
let dir = fresh_dir("st-nul-meta-key");
let path = dir.join("w.safetensors");
let arrays: HashMap<String, Array> = HashMap::new();
let mut meta: HashMap<String, String> = HashMap::new();
meta.insert("bad\0key".to_string(), "v".to_string());
match save_safetensors_with_metadata(&path, &arrays, &meta).unwrap_err() {
Error::InteriorNul(payload) => {
assert_eq!(payload.context(), "io::map_meta insert");
assert_eq!(payload.bytes_kind(), "metadata key");
}
other => panic!("expected InteriorNul(metadata key), got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn save_safetensors_metadata_value_with_interior_nul_is_rejected() {
let dir = fresh_dir("st-nul-meta-val");
let path = dir.join("w.safetensors");
let arrays: HashMap<String, Array> = HashMap::new();
let mut meta: HashMap<String, String> = HashMap::new();
meta.insert("k".to_string(), "bad\0value".to_string());
match save_safetensors_with_metadata(&path, &arrays, &meta).unwrap_err() {
Error::InteriorNul(payload) => {
assert_eq!(payload.context(), "io::map_meta insert");
assert_eq!(payload.bytes_kind(), "metadata value");
}
other => panic!("expected InteriorNul(metadata value), got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn save_safetensors_to_file_round_trips_via_path_load() {
let dir = fresh_dir("fd-roundtrip");
let path = dir.join("via_fd.safetensors");
let arr = Array::from_slice::<f32>(&[7.0_f32, 8.0, 9.0], &(3usize,)).unwrap();
let mut meta: HashMap<String, String> = HashMap::new();
meta.insert("format".to_string(), "mlx".to_string());
let mut f = File::create(&path).unwrap();
save_safetensors_to_file(&mut f, std::iter::once(("only", &arr)), &meta).unwrap();
f.sync_all().unwrap();
drop(f);
let mut loaded = load_safetensors(&path).unwrap();
assert_eq!(loaded.len(), 1);
let l = loaded.get_mut("only").unwrap();
assert_eq!(l.shape(), vec![3]);
assert_eq!(l.dtype().unwrap(), Dtype::F32);
assert_eq!(l.to_vec::<f32>().unwrap(), vec![7.0, 8.0, 9.0]);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn save_safetensors_to_file_array_key_nul_errs_before_truncate() {
let dir = fresh_dir("fd-nul-preserve");
let path = dir.join("prefilled.bin");
std::fs::write(&path, b"PREEXISTING-CONTENT").unwrap();
let mut f = OpenOptions::new()
.write(true)
.read(true)
.open(&path)
.unwrap();
let arr = Array::from_slice::<f32>(&[1.0_f32], &(1usize,)).unwrap();
let meta: HashMap<String, String> = HashMap::new();
let err =
save_safetensors_to_file(&mut f, std::iter::once(("bad\0key", &arr)), &meta).unwrap_err();
assert!(matches!(err, Error::InteriorNul(_)));
drop(f);
assert_eq!(std::fs::read(&path).unwrap(), b"PREEXISTING-CONTENT");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn save_safetensors_to_file_read_only_fd_is_file_io() {
let dir = fresh_dir("fd-readonly");
let path = dir.join("ro.safetensors");
std::fs::write(&path, b"seed").unwrap();
let mut f = File::open(&path).unwrap();
let arr = Array::from_slice::<f32>(&[1.0_f32], &(1usize,)).unwrap();
let meta: HashMap<String, String> = HashMap::new();
match save_safetensors_to_file(&mut f, std::iter::once(("w", &arr)), &meta) {
Err(Error::FileIo(p)) => {
assert!(
p.op() == FileOp::Other("set_len") || p.op() == FileOp::Write,
"read-only fd should fail at set_len or in the write callback, got op {:?}",
p.op()
);
}
other => panic!("expected Error::FileIo on read-only fd, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cb_is_open_and_label() {
let dir = fresh_dir("cb-open-label");
let path = dir.join("f.bin");
let mut f = File::create(&path).unwrap();
let state = WriterState::new(&mut f);
let desc = state.as_desc();
let (is_open, label) = unsafe {
let is_open = cb_is_open(desc);
let label_ptr = cb_label(desc);
assert!(!label_ptr.is_null());
let label = CStr::from_ptr(label_ptr).to_string_lossy().into_owned();
(is_open, label)
};
assert!(is_open);
assert_eq!(label, "mlxrs::io::save_safetensors_to_file(&mut File)");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cb_tell_seek_good_whences() {
let dir = fresh_dir("cb-tell-seek");
let path = dir.join("f.bin");
std::fs::write(&path, b"0123456789").unwrap();
let mut f = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
let state = WriterState::new(&mut f);
let desc = state.as_desc();
let (after_set, after_cur, after_end, good_a, good_b, good_after_bad) = unsafe {
cb_seek(desc, 3, libc::SEEK_SET); let after_set = cb_tell(desc);
cb_seek(desc, 2, libc::SEEK_CUR); let after_cur = cb_tell(desc);
cb_seek(desc, 0, libc::SEEK_END); let after_end = cb_tell(desc);
let good_a = cb_good(desc);
let good_b = cb_good(desc);
cb_seek(desc, 0, 9999);
let good_after_bad = cb_good(desc);
(
after_set,
after_cur,
after_end,
good_a,
good_b,
good_after_bad,
)
};
assert_eq!(after_set, 3);
assert_eq!(after_cur, 5);
assert_eq!(after_end, 10);
assert!(good_a);
assert!(good_b);
assert!(!good_after_bad);
assert!(state.into_err().is_some());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cb_write_appends_and_guards_null_and_zero() {
let dir = fresh_dir("cb-write");
let path = dir.join("f.bin");
let mut f = File::create(&path).unwrap();
let payload: &[u8] = b"HELLO";
{
let state = WriterState::new(&mut f);
let desc = state.as_desc();
unsafe {
cb_write(desc, payload.as_ptr().cast::<c_char>(), 0); cb_write(desc, payload.as_ptr().cast::<c_char>(), payload.len()); }
assert!(state.into_err().is_none());
}
f.sync_all().unwrap();
let mut back = String::new();
File::open(&path)
.unwrap()
.read_to_string(&mut back)
.unwrap();
assert_eq!(back, "HELLO");
let mut f2 = File::create(dir.join("g.bin")).unwrap();
let state = WriterState::new(&mut f2);
let desc = state.as_desc();
unsafe { cb_write(desc, std::ptr::null(), 8) };
let e = state.into_err().expect("NULL data must capture an error");
assert_eq!(e.kind(), std::io::ErrorKind::Other);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cb_write_to_read_only_fd_captures_error() {
let dir = fresh_dir("cb-write-ro");
let path = dir.join("ro.bin");
std::fs::write(&path, b"seed").unwrap();
let mut f = File::open(&path).unwrap(); let state = WriterState::new(&mut f);
let desc = state.as_desc();
let payload: &[u8] = b"X";
unsafe { cb_write(desc, payload.as_ptr().cast::<c_char>(), 1) };
assert!(
state.into_err().is_some(),
"writing to a read-only fd must capture an io::Error"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cb_read_paths_capture_misuse() {
let dir = fresh_dir("cb-read");
let path = dir.join("f.bin");
let mut f = File::create(&path).unwrap();
let mut buf = [0u8; 4];
let state = WriterState::new(&mut f);
let desc = state.as_desc();
unsafe { cb_read(desc, buf.as_mut_ptr().cast::<c_char>(), 4) };
assert!(state.into_err().is_some(), "cb_read must capture misuse");
let state2 = WriterState::new(&mut f);
let desc2 = state2.as_desc();
unsafe { cb_read_at_offset(desc2, buf.as_mut_ptr().cast::<c_char>(), 4, 0) };
assert!(
state2.into_err().is_some(),
"cb_read_at_offset must capture misuse"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn with_state_panic_is_captured_not_propagated() {
let dir = fresh_dir("cb-panic");
let path = dir.join("f.bin");
let mut f = File::create(&path).unwrap();
let state = WriterState::new(&mut f);
let desc = state.as_desc();
let r: Option<()> = with_state(desc, |_state, _file| panic!("boom in callback"));
assert!(r.is_none(), "a panicking callback must yield None");
let e = state
.into_err()
.expect("a panicking callback must capture a synthetic error");
assert_eq!(e.kind(), std::io::ErrorKind::Other);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn writer_state_set_err_first_wins() {
let dir = fresh_dir("state-firstwins");
let path = dir.join("f.bin");
let mut f = File::create(&path).unwrap();
let state = WriterState::new(&mut f);
state.set_err(std::io::Error::new(std::io::ErrorKind::NotFound, "first"));
state.set_err(std::io::Error::other("second"));
let e = state.into_err().unwrap();
assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
assert_eq!(e.to_string(), "first");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cb_free_is_noop() {
let dir = fresh_dir("cb-free");
let path = dir.join("f.bin");
let mut f = File::create(&path).unwrap();
let state = WriterState::new(&mut f);
let desc = state.as_desc();
unsafe { cb_free(desc) };
assert!(state.into_err().is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn save_safetensors_to_file_non_seekable_fd_fails_at_seek() {
use std::os::unix::io::FromRawFd;
let mut fds = [0_i32; 2];
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
assert_eq!(rc, 0, "pipe() must succeed to set up the non-seekable fd");
let read_fd = fds[0];
let write_fd = fds[1];
let mut wf = unsafe { File::from_raw_fd(write_fd) };
let arr = Array::from_slice::<f32>(&[1.0_f32, 2.0], &(2usize,)).unwrap();
let meta: HashMap<String, String> = HashMap::new();
match save_safetensors_to_file(&mut wf, std::iter::once(("w", &arr)), &meta) {
Err(Error::FileIo(p)) => {
assert_eq!(
p.op(),
FileOp::Other("seek"),
"a non-seekable fd must fail at the rewind seek, got op {:?}",
p.op()
);
}
other => panic!("expected Error::FileIo(seek) on a non-seekable fd, got {other:?}"),
}
drop(wf);
unsafe {
libc::close(read_fd);
}
}
#[cfg(unix)]
#[test]
fn cb_tell_and_seek_on_non_seekable_fd_capture_error() {
use std::os::unix::io::FromRawFd;
let mut fds = [0_i32; 2];
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
assert_eq!(rc, 0, "pipe() must succeed to set up the non-seekable fd");
let read_fd = fds[0];
let write_fd = fds[1];
let mut wf = unsafe { File::from_raw_fd(write_fd) };
{
let state = WriterState::new(&mut wf);
let desc = state.as_desc();
let tell = unsafe {
let t = cb_tell(desc);
cb_seek(desc, 0, libc::SEEK_SET);
t
};
assert_eq!(tell, 0, "cb_tell on a non-seekable fd reports 0 on error");
assert!(
state.into_err().is_some(),
"tell/seek on a non-seekable fd must capture an io::Error"
);
}
drop(wf);
unsafe {
libc::close(read_fd);
}
}
#[cfg(feature = "gguf")]
#[test]
fn gguf_has_meta_maps_rc() {
assert!(gguf_has_meta(0, true).unwrap());
assert!(!gguf_has_meta(0, false).unwrap());
assert!(!gguf_has_meta(2, true).unwrap());
assert!(!gguf_has_meta(2, false).unwrap());
assert!(gguf_has_meta(-1, false).is_err());
assert!(gguf_has_meta(7, true).is_err());
}
#[cfg(feature = "gguf")]
#[test]
fn gguf_metadata_as_str_tags() {
assert_eq!(GgufMetadata::String("x".to_string()).as_str(), "string");
let list = GgufMetadata::StringList(vec!["a".to_string(), "b".to_string()]);
assert_eq!(list.as_str(), "string_list");
assert_eq!(GgufMetadata::String("x".to_string()).to_string(), "string");
}
#[cfg(feature = "gguf")]
#[test]
fn gguf_round_trips_weights_and_typed_metadata() {
let dir = fresh_dir("gguf-roundtrip");
let path = dir.join("model.gguf");
let w = Array::from_slice::<f32>(&[1.0_f32, 2.0, 3.0, 4.0], &(2usize, 2)).unwrap();
let meta_arr = Array::from_slice::<i32>(&[42_i32], &(1usize,)).unwrap();
assert_eq!(
GgufMetadata::Array(meta_arr.try_clone().unwrap()).as_str(),
"array"
);
let mut weights: HashMap<String, Array> = HashMap::new();
weights.insert("blk.0.weight".to_string(), w);
let mut metadata: HashMap<String, GgufMetadata> = HashMap::new();
metadata.insert(
"general.name".to_string(),
GgufMetadata::String("demo".to_string()),
);
metadata.insert(
"tokenizer.tokens".to_string(),
GgufMetadata::StringList(vec!["<a>".to_string(), "<b>".to_string()]),
);
metadata.insert("general.count".to_string(), GgufMetadata::Array(meta_arr));
save_gguf(&path, &weights, &metadata).unwrap();
assert!(path.exists());
let (mut lw, lm) = load_gguf(&path).unwrap();
assert_eq!(
lw.get_mut("blk.0.weight").unwrap().to_vec::<f32>().unwrap(),
vec![1.0, 2.0, 3.0, 4.0]
);
if let Some(GgufMetadata::String(s)) = lm.get("general.name") {
assert_eq!(s, "demo");
}
if let Some(GgufMetadata::StringList(v)) = lm.get("tokenizer.tokens") {
assert_eq!(v, &vec!["<a>".to_string(), "<b>".to_string()]);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "gguf")]
#[test]
fn gguf_load_resolves_array_string_list_metadata_branches() {
let dir = fresh_dir("gguf-meta-branches");
let path = dir.join("model.gguf");
let meta_arr = Array::from_slice::<i32>(&[7_i32, 8, 9], &(3usize,)).unwrap();
let plain_w = Array::from_slice::<f32>(&[1.5_f32, 2.5], &(2usize,)).unwrap();
let collide_arr_w = Array::from_slice::<f32>(&[0.0_f32], &(1usize,)).unwrap();
let collide_str_w = Array::from_slice::<f32>(&[0.0_f32], &(1usize,)).unwrap();
let collide_list_w = Array::from_slice::<f32>(&[0.0_f32], &(1usize,)).unwrap();
let mut weights: HashMap<String, Array> = HashMap::new();
weights.insert("blk.0.weight".to_string(), plain_w);
weights.insert("meta.array.key".to_string(), collide_arr_w);
weights.insert("meta.string.key".to_string(), collide_str_w);
weights.insert("meta.list.key".to_string(), collide_list_w);
let mut metadata: HashMap<String, GgufMetadata> = HashMap::new();
metadata.insert("meta.array.key".to_string(), GgufMetadata::Array(meta_arr));
metadata.insert(
"meta.string.key".to_string(),
GgufMetadata::String("hello-gguf".to_string()),
);
metadata.insert(
"meta.list.key".to_string(),
GgufMetadata::StringList(vec![
"tok0".to_string(),
"tok1".to_string(),
"tok2".to_string(),
]),
);
save_gguf(&path, &weights, &metadata).unwrap();
assert!(path.exists());
let (mut lw, lm) = load_gguf(&path).unwrap();
assert_eq!(
lw.get_mut("blk.0.weight").unwrap().to_vec::<f32>().unwrap(),
vec![1.5_f32, 2.5]
);
assert!(
!lw.contains_key("meta.array.key"),
"a tensor+array-metadata name resolves into metadata, not weights"
);
match lm.get("meta.array.key") {
Some(GgufMetadata::Array(a)) => {
let mut a = a.try_clone().unwrap();
assert_eq!(a.shape(), vec![3]);
assert_eq!(a.dtype().unwrap(), Dtype::I32);
assert_eq!(a.to_vec::<i32>().unwrap(), vec![7, 8, 9]);
}
other => panic!("expected Array metadata for meta.array.key, got {other:?}"),
}
match lm.get("meta.string.key") {
Some(GgufMetadata::String(s)) => assert_eq!(s, "hello-gguf"),
other => panic!("expected String metadata for meta.string.key, got {other:?}"),
}
match lm.get("meta.list.key") {
Some(GgufMetadata::StringList(v)) => {
assert_eq!(
v,
&vec!["tok0".to_string(), "tok1".to_string(), "tok2".to_string()]
);
}
other => panic!("expected StringList metadata for meta.list.key, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "gguf")]
#[test]
fn gguf_save_weight_key_with_interior_nul_is_rejected() {
let dir = fresh_dir("gguf-nul-weight-key");
let path = dir.join("m.gguf");
let w = Array::from_slice::<f32>(&[1.0_f32], &(1usize,)).unwrap();
let mut weights: HashMap<String, Array> = HashMap::new();
weights.insert("bad\0weight".to_string(), w);
let metadata: HashMap<String, GgufMetadata> = HashMap::new();
match save_gguf(&path, &weights, &metadata).unwrap_err() {
Error::InteriorNul(payload) => {
assert_eq!(payload.context(), "io::gguf_save: weights insert");
assert_eq!(payload.bytes_kind(), "gguf weight key");
}
other => panic!("expected InteriorNul(gguf weight key), got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "gguf")]
#[test]
fn gguf_save_metadata_key_with_interior_nul_is_rejected() {
let dir = fresh_dir("gguf-nul-meta-key");
let path = dir.join("m.gguf");
let weights: HashMap<String, Array> = HashMap::new();
let mut metadata: HashMap<String, GgufMetadata> = HashMap::new();
metadata.insert(
"bad\0meta".to_string(),
GgufMetadata::String("v".to_string()),
);
match save_gguf(&path, &weights, &metadata).unwrap_err() {
Error::InteriorNul(payload) => {
assert_eq!(payload.context(), "io::gguf_save: metadata insert");
assert_eq!(payload.bytes_kind(), "gguf metadata key");
}
other => panic!("expected InteriorNul(gguf metadata key), got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "gguf")]
#[test]
fn gguf_save_metadata_string_value_with_interior_nul_is_rejected() {
let dir = fresh_dir("gguf-nul-meta-strval");
let path = dir.join("m.gguf");
let weights: HashMap<String, Array> = HashMap::new();
let mut metadata: HashMap<String, GgufMetadata> = HashMap::new();
metadata.insert(
"general.name".to_string(),
GgufMetadata::String("bad\0value".to_string()),
);
match save_gguf(&path, &weights, &metadata).unwrap_err() {
Error::InteriorNul(payload) => {
assert_eq!(payload.context(), "io::gguf_save: metadata string insert");
assert_eq!(payload.bytes_kind(), "gguf metadata string value");
}
other => panic!("expected InteriorNul(gguf metadata string value), got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "gguf")]
#[test]
fn gguf_save_metadata_list_entry_with_interior_nul_is_rejected() {
let dir = fresh_dir("gguf-nul-meta-listentry");
let path = dir.join("m.gguf");
let weights: HashMap<String, Array> = HashMap::new();
let mut metadata: HashMap<String, GgufMetadata> = HashMap::new();
metadata.insert(
"tokenizer.tokens".to_string(),
GgufMetadata::StringList(vec!["ok".to_string(), "bad\0entry".to_string()]),
);
match save_gguf(&path, &weights, &metadata).unwrap_err() {
Error::InteriorNul(payload) => {
assert_eq!(
payload.context(),
"io::gguf_save: metadata list-entry append"
);
assert_eq!(payload.bytes_kind(), "gguf metadata list entry");
}
other => panic!("expected InteriorNul(gguf metadata list entry), got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}