use std::collections::HashMap;
use std::hash::BuildHasher;
use std::io::{BufWriter, Seek, Write};
use std::path::Path;
use crate::error::AnamnesisError;
use crate::parse::gguf::{align_up, GgufMetadataArray, GgufMetadataValue, GgufType};
const GGUF_MAGIC: &[u8; 4] = b"GGUF";
const GGUF_WRITE_VERSION: u32 = 3;
const DEFAULT_ALIGNMENT: u32 = 32;
const ALIGNMENT_KEY: &str = "general.alignment";
const WRITER_BUF_SIZE: usize = 64 * 1024;
#[derive(Debug, Clone, Copy)]
pub struct GgufWriteTensor<'a> {
pub name: &'a str,
pub shape: &'a [usize],
pub dtype: GgufType,
pub data: &'a [u8],
}
pub fn write_gguf<S: BuildHasher>(
path: impl AsRef<Path>,
tensors: &[GgufWriteTensor<'_>],
metadata: &HashMap<String, GgufMetadataValue, S>,
) -> crate::Result<()> {
let file = std::fs::File::create(path.as_ref()).map_err(AnamnesisError::Io)?;
let writer = BufWriter::with_capacity(WRITER_BUF_SIZE, file);
write_gguf_to_writer(writer, tensors, metadata)
}
pub fn write_gguf_to_writer<W: Write + Seek, S: BuildHasher>(
mut writer: W,
tensors: &[GgufWriteTensor<'_>],
metadata: &HashMap<String, GgufMetadataValue, S>,
) -> crate::Result<()> {
for tensor in tensors {
validate_tensor(tensor)?;
}
let (alignment_u32, needs_inject_alignment) = resolve_alignment(metadata)?;
let alignment_u64 = u64::from(alignment_u32);
let mut sorted_keys: Vec<&str> = metadata.keys().map(String::as_str).collect();
if needs_inject_alignment {
sorted_keys.push(ALIGNMENT_KEY);
}
let injected: GgufMetadataValue = GgufMetadataValue::U32(alignment_u32);
sorted_keys.sort_unstable();
let effective_kv: Vec<(&str, &GgufMetadataValue)> = sorted_keys
.iter()
.map(|&key| {
let value = if needs_inject_alignment && key == ALIGNMENT_KEY {
&injected
} else {
metadata.get(key).ok_or_else(|| AnamnesisError::Parse {
reason: format!("GGUF write: metadata key `{key}` vanished mid-iteration"),
})?
};
Ok((key, value))
})
.collect::<crate::Result<Vec<_>>>()?;
let mut kv_block: Vec<u8> = Vec::new();
for (key, value) in &effective_kv {
write_string(&mut kv_block, key)?;
write_metadata_value(&mut kv_block, value)?;
}
#[allow(clippy::as_conversions)]
let kv_block_len_u64 = kv_block.len() as u64;
let header_size_u64: u64 = 24;
let tensor_info_start = header_size_u64
.checked_add(kv_block_len_u64)
.ok_or_else(|| AnamnesisError::Parse {
reason: "GGUF write: tensor_info_start overflow".into(),
})?;
let mut tensor_info_block: Vec<u8> = Vec::new();
let mut relative_offset: u64 = 0;
for tensor in tensors {
#[allow(clippy::as_conversions)]
let data_len_u64 = tensor.data.len() as u64;
relative_offset = align_up(relative_offset, alignment_u64).map_err(|e| match e {
AnamnesisError::Parse { reason } => AnamnesisError::Parse {
reason: format!("GGUF write tensor `{}`: {reason}", tensor.name),
},
other @ (AnamnesisError::Unsupported { .. } | AnamnesisError::Io(_)) => other,
})?;
write_string(&mut tensor_info_block, tensor.name)?;
#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
let n_dims_u32 = tensor.shape.len() as u32;
write_u32_le(&mut tensor_info_block, n_dims_u32)?;
for &dim in tensor.shape {
#[allow(clippy::as_conversions)]
let dim_u64 = dim as u64;
write_u64_le(&mut tensor_info_block, dim_u64)?;
}
write_u32_le(&mut tensor_info_block, gguf_type_to_u32(tensor.dtype))?;
write_u64_le(&mut tensor_info_block, relative_offset)?;
relative_offset =
relative_offset
.checked_add(data_len_u64)
.ok_or_else(|| AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: relative offset overflow at \
{relative_offset} + {data_len_u64}",
tensor.name
),
})?;
}
#[allow(clippy::as_conversions)]
let tensor_info_block_len_u64 = tensor_info_block.len() as u64;
let tensor_info_end = tensor_info_start
.checked_add(tensor_info_block_len_u64)
.ok_or_else(|| AnamnesisError::Parse {
reason: "GGUF write: tensor_info_end overflow".into(),
})?;
let tensor_data_start = if tensors.is_empty() {
tensor_info_end
} else {
align_up(tensor_info_end, alignment_u64)?
};
writer.write_all(GGUF_MAGIC).map_err(AnamnesisError::Io)?;
write_u32_le(&mut writer, GGUF_WRITE_VERSION)?;
#[allow(clippy::as_conversions)]
let tensor_count_u64 = tensors.len() as u64;
write_u64_le(&mut writer, tensor_count_u64)?;
#[allow(clippy::as_conversions)]
let kv_count_u64 = effective_kv.len() as u64;
write_u64_le(&mut writer, kv_count_u64)?;
writer.write_all(&kv_block).map_err(AnamnesisError::Io)?;
writer
.write_all(&tensor_info_block)
.map_err(AnamnesisError::Io)?;
let padding_to_data = tensor_data_start
.checked_sub(tensor_info_end)
.ok_or_else(|| AnamnesisError::Parse {
reason: "GGUF write: tensor_data_start arithmetic underflow".into(),
})?;
write_zeros(&mut writer, padding_to_data)?;
let mut emitted: u64 = 0;
for tensor in tensors {
#[allow(clippy::as_conversions)]
let data_len_u64 = tensor.data.len() as u64;
let desired = align_up(emitted, alignment_u64)?;
let gap = desired
.checked_sub(emitted)
.ok_or_else(|| AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: inter-tensor padding underflow",
tensor.name
),
})?;
write_zeros(&mut writer, gap)?;
writer.write_all(tensor.data).map_err(AnamnesisError::Io)?;
emitted = desired
.checked_add(data_len_u64)
.ok_or_else(|| AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: emitted-byte counter overflow",
tensor.name
),
})?;
}
writer.flush().map_err(AnamnesisError::Io)?;
Ok(())
}
const MAX_TENSOR_DIMS_USZ: usize = 8;
fn validate_tensor(tensor: &GgufWriteTensor<'_>) -> crate::Result<()> {
if tensor.dtype.is_quantized() {
return Err(AnamnesisError::Unsupported {
format: "GGUF".into(),
detail: format!(
"writing quantized GGUF dtype {} requires Phase 7.5 encoders \
(Phase 6 ships only scalar dtype passthrough emit)",
tensor.dtype
),
});
}
if tensor.shape.is_empty() {
return Err(AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: shape has zero dimensions",
tensor.name
),
});
}
if tensor.shape.len() > MAX_TENSOR_DIMS_USZ {
return Err(AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: {}-D shape exceeds parser cap {MAX_TENSOR_DIMS_USZ}",
tensor.name,
tensor.shape.len()
),
});
}
let mut n_elements: u64 = 1;
for (axis, &dim) in tensor.shape.iter().enumerate() {
if dim == 0 {
return Err(AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: dimension {axis} is zero",
tensor.name
),
});
}
#[allow(clippy::as_conversions)]
let dim_u64 = dim as u64;
n_elements = n_elements
.checked_mul(dim_u64)
.ok_or_else(|| AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: element-count overflow at axis {axis}",
tensor.name
),
})?;
}
let expected_bytes_u64 = tensor.dtype.byte_size_for_n_elements(n_elements)?;
let expected_bytes =
usize::try_from(expected_bytes_u64).map_err(|_| AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: byte length {expected_bytes_u64} overflows usize",
tensor.name
),
})?;
if tensor.data.len() != expected_bytes {
return Err(AnamnesisError::Parse {
reason: format!(
"GGUF write tensor `{}`: data length {} does not match \
shape/dtype-implied {expected_bytes} bytes",
tensor.name,
tensor.data.len()
),
});
}
Ok(())
}
fn resolve_alignment<S: BuildHasher>(
metadata: &HashMap<String, GgufMetadataValue, S>,
) -> crate::Result<(u32, bool)> {
match metadata.get(ALIGNMENT_KEY) {
Some(GgufMetadataValue::U32(v)) if *v != 0 => Ok((*v, false)),
Some(GgufMetadataValue::U32(_)) => Err(AnamnesisError::Unsupported {
format: "GGUF".into(),
detail: "general.alignment must be non-zero".into(),
}),
Some(_) => Err(AnamnesisError::Unsupported {
format: "GGUF".into(),
detail: "general.alignment must be UINT32".into(),
}),
None => Ok((DEFAULT_ALIGNMENT, true)),
}
}
fn write_u8(w: &mut impl Write, v: u8) -> crate::Result<()> {
w.write_all(&[v]).map_err(AnamnesisError::Io)
}
fn write_i8(w: &mut impl Write, v: i8) -> crate::Result<()> {
#[allow(clippy::as_conversions, clippy::cast_sign_loss)]
let byte = v as u8;
write_u8(w, byte)
}
fn write_u16_le(w: &mut impl Write, v: u16) -> crate::Result<()> {
w.write_all(&v.to_le_bytes()).map_err(AnamnesisError::Io)
}
fn write_i16_le(w: &mut impl Write, v: i16) -> crate::Result<()> {
w.write_all(&v.to_le_bytes()).map_err(AnamnesisError::Io)
}
fn write_u32_le(w: &mut impl Write, v: u32) -> crate::Result<()> {
w.write_all(&v.to_le_bytes()).map_err(AnamnesisError::Io)
}
fn write_i32_le(w: &mut impl Write, v: i32) -> crate::Result<()> {
w.write_all(&v.to_le_bytes()).map_err(AnamnesisError::Io)
}
fn write_u64_le(w: &mut impl Write, v: u64) -> crate::Result<()> {
w.write_all(&v.to_le_bytes()).map_err(AnamnesisError::Io)
}
fn write_i64_le(w: &mut impl Write, v: i64) -> crate::Result<()> {
w.write_all(&v.to_le_bytes()).map_err(AnamnesisError::Io)
}
fn write_f32_le(w: &mut impl Write, v: f32) -> crate::Result<()> {
w.write_all(&v.to_le_bytes()).map_err(AnamnesisError::Io)
}
fn write_f64_le(w: &mut impl Write, v: f64) -> crate::Result<()> {
w.write_all(&v.to_le_bytes()).map_err(AnamnesisError::Io)
}
fn write_bool(w: &mut impl Write, v: bool) -> crate::Result<()> {
write_u8(w, u8::from(v))
}
fn write_string(w: &mut impl Write, s: &str) -> crate::Result<()> {
let bytes = s.as_bytes();
#[allow(clippy::as_conversions)]
let len_u64 = bytes.len() as u64;
write_u64_le(w, len_u64)?;
w.write_all(bytes).map_err(AnamnesisError::Io)
}
fn write_zeros(w: &mut impl Write, n: u64) -> crate::Result<()> {
const ZEROS: [u8; 256] = [0u8; 256];
let mut remaining = n;
while remaining > 0 {
let chunk = usize::try_from(remaining)
.unwrap_or(usize::MAX)
.min(ZEROS.len());
#[allow(clippy::indexing_slicing)]
let slice = &ZEROS[..chunk];
w.write_all(slice).map_err(AnamnesisError::Io)?;
#[allow(clippy::as_conversions)]
let chunk_u64 = chunk as u64;
remaining -= chunk_u64;
}
Ok(())
}
fn write_metadata_value(w: &mut impl Write, value: &GgufMetadataValue) -> crate::Result<()> {
match value {
GgufMetadataValue::U8(v) => {
write_u32_le(w, 0)?;
write_u8(w, *v)
}
GgufMetadataValue::I8(v) => {
write_u32_le(w, 1)?;
write_i8(w, *v)
}
GgufMetadataValue::U16(v) => {
write_u32_le(w, 2)?;
write_u16_le(w, *v)
}
GgufMetadataValue::I16(v) => {
write_u32_le(w, 3)?;
write_i16_le(w, *v)
}
GgufMetadataValue::U32(v) => {
write_u32_le(w, 4)?;
write_u32_le(w, *v)
}
GgufMetadataValue::I32(v) => {
write_u32_le(w, 5)?;
write_i32_le(w, *v)
}
GgufMetadataValue::F32(v) => {
write_u32_le(w, 6)?;
write_f32_le(w, *v)
}
GgufMetadataValue::Bool(v) => {
write_u32_le(w, 7)?;
write_bool(w, *v)
}
GgufMetadataValue::String(s) => {
write_u32_le(w, 8)?;
write_string(w, s)
}
GgufMetadataValue::Array(arr) => {
write_u32_le(w, 9)?;
write_typed_array(w, arr.as_ref())
}
GgufMetadataValue::U64(v) => {
write_u32_le(w, 10)?;
write_u64_le(w, *v)
}
GgufMetadataValue::I64(v) => {
write_u32_le(w, 11)?;
write_i64_le(w, *v)
}
GgufMetadataValue::F64(v) => {
write_u32_le(w, 12)?;
write_f64_le(w, *v)
}
}
}
fn write_typed_array(w: &mut impl Write, arr: &GgufMetadataArray) -> crate::Result<()> {
match arr {
GgufMetadataArray::U8(v) => {
write_u32_le(w, 0)?;
write_array_len(w, v.len())?;
for &x in v {
write_u8(w, x)?;
}
Ok(())
}
GgufMetadataArray::I8(v) => {
write_u32_le(w, 1)?;
write_array_len(w, v.len())?;
for &x in v {
write_i8(w, x)?;
}
Ok(())
}
GgufMetadataArray::U16(v) => {
write_u32_le(w, 2)?;
write_array_len(w, v.len())?;
for &x in v {
write_u16_le(w, x)?;
}
Ok(())
}
GgufMetadataArray::I16(v) => {
write_u32_le(w, 3)?;
write_array_len(w, v.len())?;
for &x in v {
write_i16_le(w, x)?;
}
Ok(())
}
GgufMetadataArray::U32(v) => {
write_u32_le(w, 4)?;
write_array_len(w, v.len())?;
for &x in v {
write_u32_le(w, x)?;
}
Ok(())
}
GgufMetadataArray::I32(v) => {
write_u32_le(w, 5)?;
write_array_len(w, v.len())?;
for &x in v {
write_i32_le(w, x)?;
}
Ok(())
}
GgufMetadataArray::F32(v) => {
write_u32_le(w, 6)?;
write_array_len(w, v.len())?;
for &x in v {
write_f32_le(w, x)?;
}
Ok(())
}
GgufMetadataArray::Bool(v) => {
write_u32_le(w, 7)?;
write_array_len(w, v.len())?;
for &x in v {
write_bool(w, x)?;
}
Ok(())
}
GgufMetadataArray::String(v) => {
write_u32_le(w, 8)?;
write_array_len(w, v.len())?;
for s in v {
write_string(w, s)?;
}
Ok(())
}
GgufMetadataArray::Array(v) => {
write_u32_le(w, 9)?;
write_array_len(w, v.len())?;
for inner in v {
write_typed_array(w, inner)?;
}
Ok(())
}
GgufMetadataArray::U64(v) => {
write_u32_le(w, 10)?;
write_array_len(w, v.len())?;
for &x in v {
write_u64_le(w, x)?;
}
Ok(())
}
GgufMetadataArray::I64(v) => {
write_u32_le(w, 11)?;
write_array_len(w, v.len())?;
for &x in v {
write_i64_le(w, x)?;
}
Ok(())
}
GgufMetadataArray::F64(v) => {
write_u32_le(w, 12)?;
write_array_len(w, v.len())?;
for &x in v {
write_f64_le(w, x)?;
}
Ok(())
}
}
}
fn write_array_len(w: &mut impl Write, len: usize) -> crate::Result<()> {
#[allow(clippy::as_conversions)]
let len_u64 = len as u64;
write_u64_le(w, len_u64)
}
const fn gguf_type_to_u32(dtype: GgufType) -> u32 {
match dtype {
GgufType::F32 => 0,
GgufType::F16 => 1,
GgufType::Q4_0 => 2,
GgufType::Q4_1 => 3,
GgufType::Q5_0 => 6,
GgufType::Q5_1 => 7,
GgufType::Q8_0 => 8,
GgufType::Q8_1 => 9,
GgufType::Q2_K => 10,
GgufType::Q3_K => 11,
GgufType::Q4_K => 12,
GgufType::Q5_K => 13,
GgufType::Q6_K => 14,
GgufType::Q8_K => 15,
GgufType::IQ2_XXS => 16,
GgufType::IQ2_XS => 17,
GgufType::IQ3_XXS => 18,
GgufType::IQ1_S => 19,
GgufType::IQ4_NL => 20,
GgufType::IQ3_S => 21,
GgufType::IQ2_S => 22,
GgufType::IQ4_XS => 23,
GgufType::I8 => 24,
GgufType::I16 => 25,
GgufType::I32 => 26,
GgufType::I64 => 27,
GgufType::F64 => 28,
GgufType::IQ1_M => 29,
GgufType::BF16 => 30,
GgufType::TQ1_0 => 34,
GgufType::TQ2_0 => 35,
GgufType::MXFP4 => 39,
}
}
#[cfg(test)]
#[allow(
clippy::indexing_slicing,
clippy::as_conversions,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::float_cmp,
clippy::wildcard_enum_match_arm
)]
mod tests {
use super::*;
use crate::parse::gguf::parse_gguf;
fn write_to_tempfile(
tensors: &[GgufWriteTensor<'_>],
metadata: &HashMap<String, GgufMetadataValue>,
) -> tempfile::NamedTempFile {
let tmp = tempfile::Builder::new()
.suffix(".gguf")
.tempfile()
.expect("create tempfile");
write_gguf(tmp.path(), tensors, metadata).expect("write_gguf");
tmp
}
#[test]
fn roundtrip_empty_no_tensors() {
let mut metadata = HashMap::new();
metadata.insert(
"general.architecture".into(),
GgufMetadataValue::String("anamnesis-test".into()),
);
let tmp = write_to_tempfile(&[], &metadata);
let parsed = parse_gguf(tmp.path()).expect("parse_gguf");
assert_eq!(parsed.version(), 3);
assert!(parsed.is_empty());
assert_eq!(parsed.alignment(), 32);
let arch = parsed
.metadata()
.get("general.architecture")
.and_then(GgufMetadataValue::as_string)
.unwrap_or("");
assert_eq!(arch, "anamnesis-test");
assert_eq!(
parsed
.metadata()
.get("general.alignment")
.and_then(GgufMetadataValue::as_u32),
Some(32)
);
}
#[test]
fn roundtrip_single_f32() {
let data: Vec<u8> = (0..6)
.flat_map(|i: u32| (i as f32 * 2.0).to_le_bytes())
.collect();
let shape = [2usize, 3];
let tensors = [GgufWriteTensor {
name: "w",
shape: &shape,
dtype: GgufType::F32,
data: &data,
}];
let tmp = write_to_tempfile(&tensors, &HashMap::new());
let parsed = parse_gguf(tmp.path()).expect("parse_gguf");
let collected: Vec<_> = parsed.tensors().collect();
assert_eq!(collected.len(), 1);
let t = &collected[0];
assert_eq!(t.name, "w");
assert_eq!(t.shape, &[2, 3]);
assert_eq!(t.dtype, GgufType::F32);
assert_eq!(t.data.as_ref(), data.as_slice());
}
#[test]
fn roundtrip_mixed_bf16_f32_i32() {
let bf16_data: Vec<u8> = (0..8).flat_map(|i: u16| i.to_le_bytes()).collect();
let f32_data: Vec<u8> = (0..4).flat_map(|i: u32| (i as f32).to_le_bytes()).collect();
let i32_data: Vec<u8> = (0..2).flat_map(|i: i32| i.to_le_bytes()).collect();
let bf16_shape = [4usize, 2];
let f32_shape = [4usize];
let i32_shape = [2usize];
let tensors = [
GgufWriteTensor {
name: "bf16_tensor",
shape: &bf16_shape,
dtype: GgufType::BF16,
data: &bf16_data,
},
GgufWriteTensor {
name: "f32_tensor",
shape: &f32_shape,
dtype: GgufType::F32,
data: &f32_data,
},
GgufWriteTensor {
name: "i32_tensor",
shape: &i32_shape,
dtype: GgufType::I32,
data: &i32_data,
},
];
let tmp = write_to_tempfile(&tensors, &HashMap::new());
let parsed = parse_gguf(tmp.path()).expect("parse_gguf");
let collected: Vec<_> = parsed.tensors().collect();
assert_eq!(collected.len(), 3);
assert_eq!(collected[0].name, "bf16_tensor");
assert_eq!(collected[0].dtype, GgufType::BF16);
assert_eq!(collected[0].data.as_ref(), bf16_data.as_slice());
assert_eq!(collected[1].name, "f32_tensor");
assert_eq!(collected[1].dtype, GgufType::F32);
assert_eq!(collected[1].data.as_ref(), f32_data.as_slice());
assert_eq!(collected[2].name, "i32_tensor");
assert_eq!(collected[2].dtype, GgufType::I32);
assert_eq!(collected[2].data.as_ref(), i32_data.as_slice());
for info in parsed.tensor_info() {
assert_eq!(
info.data_offset % 32,
0,
"tensor `{}` misaligned",
info.name
);
}
}
#[test]
fn roundtrip_metadata_kv() {
let mut metadata = HashMap::new();
metadata.insert(
"string_key".into(),
GgufMetadataValue::String("hello".into()),
);
metadata.insert("u32_key".into(), GgufMetadataValue::U32(42));
metadata.insert(
"f32_key".into(),
GgufMetadataValue::F32(std::f32::consts::PI),
);
metadata.insert("bool_key".into(), GgufMetadataValue::Bool(true));
metadata.insert(
"string_array_key".into(),
GgufMetadataValue::Array(Box::new(GgufMetadataArray::String(vec![
"a".into(),
"b".into(),
"c".into(),
]))),
);
metadata.insert(
"u64_array_key".into(),
GgufMetadataValue::Array(Box::new(GgufMetadataArray::U64(vec![1, 2, 3, 4]))),
);
let tmp = write_to_tempfile(&[], &metadata);
let parsed = parse_gguf(tmp.path()).expect("parse_gguf");
assert_eq!(
parsed
.metadata()
.get("string_key")
.and_then(GgufMetadataValue::as_string),
Some("hello")
);
assert_eq!(
parsed
.metadata()
.get("u32_key")
.and_then(GgufMetadataValue::as_u32),
Some(42)
);
match parsed.metadata().get("f32_key") {
Some(GgufMetadataValue::F32(v)) => assert!((v - std::f32::consts::PI).abs() < 1e-7),
other => panic!("expected F32, got {other:?}"),
}
assert_eq!(
parsed
.metadata()
.get("bool_key")
.and_then(GgufMetadataValue::as_bool),
Some(true)
);
match parsed
.metadata()
.get("string_array_key")
.and_then(GgufMetadataValue::as_array)
{
Some(GgufMetadataArray::String(v)) => {
assert_eq!(v, &vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]);
}
other => panic!("expected String array, got {other:?}"),
}
match parsed
.metadata()
.get("u64_array_key")
.and_then(GgufMetadataValue::as_array)
{
Some(GgufMetadataArray::U64(v)) => assert_eq!(v, &vec![1, 2, 3, 4]),
other => panic!("expected U64 array, got {other:?}"),
}
}
#[test]
fn reject_quantized_dtype() {
let data = vec![0u8; 144];
let shape = [256usize];
let tensors = [GgufWriteTensor {
name: "q",
shape: &shape,
dtype: GgufType::Q4_K,
data: &data,
}];
let tmp = tempfile::Builder::new()
.suffix(".gguf")
.tempfile()
.expect("create tempfile");
let err = write_gguf(tmp.path(), &tensors, &HashMap::new()).expect_err("should reject");
match err {
AnamnesisError::Unsupported { format, detail } => {
assert_eq!(format, "GGUF");
assert!(detail.contains("Phase 7.5"), "unexpected detail: {detail}");
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn reject_data_length_mismatch() {
let data = vec![0u8; 8];
let shape = [2usize, 3];
let tensors = [GgufWriteTensor {
name: "w",
shape: &shape,
dtype: GgufType::F32,
data: &data,
}];
let tmp = tempfile::Builder::new()
.suffix(".gguf")
.tempfile()
.expect("create tempfile");
let err = write_gguf(tmp.path(), &tensors, &HashMap::new()).expect_err("should reject");
match err {
AnamnesisError::Parse { reason } => {
assert!(
reason.contains("data length"),
"unexpected reason: {reason}"
);
}
other => panic!("expected Parse, got {other:?}"),
}
}
#[test]
fn roundtrip_with_custom_alignment_8() {
let data: Vec<u8> = (0..3).flat_map(|i: u32| (i as f32).to_le_bytes()).collect();
let shape = [3usize];
let tensors = [GgufWriteTensor {
name: "w",
shape: &shape,
dtype: GgufType::F32,
data: &data,
}];
let mut metadata = HashMap::new();
metadata.insert(ALIGNMENT_KEY.into(), GgufMetadataValue::U32(8));
let tmp = write_to_tempfile(&tensors, &metadata);
let parsed = parse_gguf(tmp.path()).expect("parse_gguf");
assert_eq!(parsed.alignment(), 8);
for info in parsed.tensor_info() {
assert_eq!(info.data_offset % 8, 0, "tensor `{}` misaligned", info.name);
}
let collected: Vec<_> = parsed.tensors().collect();
assert_eq!(collected[0].data.as_ref(), data.as_slice());
}
#[test]
fn reject_zero_alignment() {
let mut metadata = HashMap::new();
metadata.insert(ALIGNMENT_KEY.into(), GgufMetadataValue::U32(0));
let tmp = tempfile::Builder::new()
.suffix(".gguf")
.tempfile()
.expect("create tempfile");
let err = write_gguf(tmp.path(), &[], &metadata).expect_err("should reject");
match err {
AnamnesisError::Unsupported { format, detail } => {
assert_eq!(format, "GGUF");
assert!(detail.contains("non-zero"), "unexpected detail: {detail}");
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
}