#![allow(unused_variables)]
use super::{ExportConfig, ExportFormat, ExportPrecision, ModelExporter};
use crate::tensor::Tensor;
use crate::traits::Model;
use anyhow::{anyhow, Result};
use byteorder::{LittleEndian, WriteBytesExt};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufWriter, Seek, Write};
const GGUF_MAGIC: u32 = 0x46554747; const GGUF_VERSION: u32 = 3;
#[derive(Debug, Clone, Copy)]
pub enum GGUFValueType {
UInt8 = 0,
Int8 = 1,
UInt16 = 2,
Int16 = 3,
UInt32 = 4,
Int32 = 5,
Float32 = 6,
Bool = 7,
String = 8,
Array = 9,
UInt64 = 10,
Int64 = 11,
Float64 = 12,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GGUFTensorType {
F32 = 0,
F16 = 1,
Q4_0 = 2,
Q4_1 = 3,
Q5_0 = 6,
Q5_1 = 7,
Q8_0 = 8,
Q8_1 = 9,
Q2K = 10,
Q3K = 11,
Q4K = 12,
Q5K = 13,
Q6K = 14,
Q8K = 15,
Iq2Xxs = 16,
Iq2Xs = 17,
Iq3Xxs = 18,
Iq1S = 19,
Iq4Nl = 20,
Iq3S = 21,
Iq2S = 22,
Iq4Xs = 23,
I8 = 24,
I16 = 25,
I32 = 26,
I64 = 27,
F64 = 28,
Iq1M = 29,
}
impl GGUFTensorType {
pub fn element_size(&self) -> usize {
match self {
GGUFTensorType::F32 => 4,
GGUFTensorType::F16 => 2,
GGUFTensorType::F64 => 8,
GGUFTensorType::I8 => 1,
GGUFTensorType::I16 => 2,
GGUFTensorType::I32 => 4,
GGUFTensorType::I64 => 8,
GGUFTensorType::Q4_0 => 2, GGUFTensorType::Q4_1 => 2,
GGUFTensorType::Q5_0 => 3,
GGUFTensorType::Q5_1 => 3,
GGUFTensorType::Q8_0 => 1,
GGUFTensorType::Q8_1 => 1,
GGUFTensorType::Q2K => 1,
GGUFTensorType::Q3K => 1,
GGUFTensorType::Q4K => 1,
GGUFTensorType::Q5K => 1,
GGUFTensorType::Q6K => 1,
GGUFTensorType::Q8K => 1,
GGUFTensorType::Iq2Xxs => 1,
GGUFTensorType::Iq2Xs => 1,
GGUFTensorType::Iq3Xxs => 1,
GGUFTensorType::Iq1S => 1,
GGUFTensorType::Iq4Nl => 1,
GGUFTensorType::Iq3S => 1,
GGUFTensorType::Iq2S => 1,
GGUFTensorType::Iq4Xs => 1,
GGUFTensorType::Iq1M => 1,
}
}
pub fn block_size(&self) -> usize {
match self {
GGUFTensorType::Q4_0 | GGUFTensorType::Q4_1 => 32,
GGUFTensorType::Q5_0 | GGUFTensorType::Q5_1 => 32,
GGUFTensorType::Q8_0 | GGUFTensorType::Q8_1 => 32,
GGUFTensorType::Q2K => 256,
GGUFTensorType::Q3K => 256,
GGUFTensorType::Q4K => 256,
GGUFTensorType::Q5K => 256,
GGUFTensorType::Q6K => 256,
GGUFTensorType::Q8K => 256,
_ => 1,
}
}
pub fn from_precision(precision: ExportPrecision) -> Self {
match precision {
ExportPrecision::FP32 => GGUFTensorType::F32,
ExportPrecision::FP16 => GGUFTensorType::F16,
ExportPrecision::INT8 => GGUFTensorType::Q8_0,
ExportPrecision::INT4 => GGUFTensorType::Q4_0,
}
}
pub fn is_quantized(&self) -> bool {
!matches!(
self,
GGUFTensorType::F32
| GGUFTensorType::F16
| GGUFTensorType::F64
| GGUFTensorType::I8
| GGUFTensorType::I16
| GGUFTensorType::I32
| GGUFTensorType::I64
)
}
}
#[derive(Debug, Clone)]
pub enum GGUFValue {
UInt8(u8),
Int8(i8),
UInt16(u16),
Int16(i16),
UInt32(u32),
Int32(i32),
Float32(f32),
Bool(bool),
String(String),
Array(GGUFValueType, Vec<GGUFValue>),
UInt64(u64),
Int64(i64),
Float64(f64),
}
impl GGUFValue {
pub fn value_type(&self) -> GGUFValueType {
match self {
GGUFValue::UInt8(_) => GGUFValueType::UInt8,
GGUFValue::Int8(_) => GGUFValueType::Int8,
GGUFValue::UInt16(_) => GGUFValueType::UInt16,
GGUFValue::Int16(_) => GGUFValueType::Int16,
GGUFValue::UInt32(_) => GGUFValueType::UInt32,
GGUFValue::Int32(_) => GGUFValueType::Int32,
GGUFValue::Float32(_) => GGUFValueType::Float32,
GGUFValue::Bool(_) => GGUFValueType::Bool,
GGUFValue::String(_) => GGUFValueType::String,
GGUFValue::Array(_, _) => GGUFValueType::Array,
GGUFValue::UInt64(_) => GGUFValueType::UInt64,
GGUFValue::Int64(_) => GGUFValueType::Int64,
GGUFValue::Float64(_) => GGUFValueType::Float64,
}
}
pub fn write_to_buffer<W: Write>(&self, writer: &mut W) -> Result<()> {
match self {
GGUFValue::UInt8(v) => writer.write_u8(*v)?,
GGUFValue::Int8(v) => writer.write_i8(*v)?,
GGUFValue::UInt16(v) => writer.write_u16::<LittleEndian>(*v)?,
GGUFValue::Int16(v) => writer.write_i16::<LittleEndian>(*v)?,
GGUFValue::UInt32(v) => writer.write_u32::<LittleEndian>(*v)?,
GGUFValue::Int32(v) => writer.write_i32::<LittleEndian>(*v)?,
GGUFValue::Float32(v) => writer.write_f32::<LittleEndian>(*v)?,
GGUFValue::Bool(v) => writer.write_u8(if *v { 1 } else { 0 })?,
GGUFValue::String(s) => {
writer.write_u64::<LittleEndian>(s.len() as u64)?;
writer.write_all(s.as_bytes())?;
},
GGUFValue::Array(elem_type, values) => {
writer.write_u32::<LittleEndian>(*elem_type as u32)?;
writer.write_u64::<LittleEndian>(values.len() as u64)?;
for value in values {
value.write_to_buffer(writer)?;
}
},
GGUFValue::UInt64(v) => writer.write_u64::<LittleEndian>(*v)?,
GGUFValue::Int64(v) => writer.write_i64::<LittleEndian>(*v)?,
GGUFValue::Float64(v) => writer.write_f64::<LittleEndian>(*v)?,
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct GGUFTensorInfo {
pub name: String,
pub dimensions: Vec<u64>,
pub tensor_type: GGUFTensorType,
pub offset: u64,
}
#[derive(Debug)]
pub struct GGUFHeader {
pub magic: u32,
pub version: u32,
pub tensor_count: u64,
pub metadata_kv_count: u64,
}
#[derive(Clone)]
pub struct GGUFExporter {
quantization_type: GGUFTensorType,
metadata: HashMap<String, GGUFValue>,
}
impl Default for GGUFExporter {
fn default() -> Self {
Self::new()
}
}
impl GGUFExporter {
pub fn new() -> Self {
let mut metadata = HashMap::new();
metadata.insert(
"general.architecture".to_string(),
GGUFValue::String("llama".to_string()),
);
metadata.insert("general.file_type".to_string(), GGUFValue::UInt32(1));
metadata.insert(
"general.quantization_version".to_string(),
GGUFValue::UInt32(2),
);
Self {
quantization_type: GGUFTensorType::F32,
metadata,
}
}
pub fn with_quantization(mut self, tensor_type: GGUFTensorType) -> Self {
self.quantization_type = tensor_type;
let file_type = match tensor_type {
GGUFTensorType::F32 => 0,
GGUFTensorType::F16 => 1,
GGUFTensorType::Q4_0 => 2,
GGUFTensorType::Q4_1 => 3,
GGUFTensorType::Q5_0 => 8,
GGUFTensorType::Q5_1 => 9,
GGUFTensorType::Q8_0 => 7,
_ => 15, };
self.metadata.insert(
"general.file_type".to_string(),
GGUFValue::UInt32(file_type),
);
self
}
pub fn add_metadata(mut self, key: String, value: GGUFValue) -> Self {
self.metadata.insert(key, value);
self
}
pub fn set_architecture_metadata(
mut self,
context_length: u64,
embedding_length: u64,
block_count: u64,
feed_forward_length: u64,
head_count: u64,
head_count_kv: Option<u64>,
vocab_size: u64,
) -> Self {
self.metadata.insert(
"llama.context_length".to_string(),
GGUFValue::UInt64(context_length),
);
self.metadata.insert(
"llama.embedding_length".to_string(),
GGUFValue::UInt64(embedding_length),
);
self.metadata.insert(
"llama.block_count".to_string(),
GGUFValue::UInt64(block_count),
);
self.metadata.insert(
"llama.feed_forward_length".to_string(),
GGUFValue::UInt64(feed_forward_length),
);
self.metadata.insert(
"llama.attention.head_count".to_string(),
GGUFValue::UInt64(head_count),
);
if let Some(kv_heads) = head_count_kv {
self.metadata.insert(
"llama.attention.head_count_kv".to_string(),
GGUFValue::UInt64(kv_heads),
);
}
self.metadata.insert(
"tokenizer.ggml.model".to_string(),
GGUFValue::String("llama".to_string()),
);
self.metadata.insert(
"tokenizer.ggml.tokens".to_string(),
GGUFValue::Array(GGUFValueType::String, vec![]),
);
self
}
fn export_to_gguf<M: Model>(&self, model: &M, config: &ExportConfig) -> Result<()> {
let output_path = format!("{}.gguf", config.output_path);
let file = File::create(&output_path)?;
let mut writer = BufWriter::new(file);
let tensors = self.extract_model_tensors(model)?;
self.write_header(&mut writer, tensors.len())?;
self.write_metadata(&mut writer)?;
let tensor_data_offset = self.calculate_tensor_data_offset(&tensors)?;
let tensor_infos = self.write_tensor_info(&mut writer, &tensors, tensor_data_offset)?;
self.align_to_tensor_data(&mut writer)?;
self.write_tensor_data(&mut writer, &tensors, &tensor_infos)?;
writer.flush()?;
println!("Model exported to GGUF format: {}", output_path);
Ok(())
}
fn extract_model_tensors<M: Model>(&self, _model: &M) -> Result<Vec<(String, Tensor)>> {
let mut tensors = Vec::new();
let embedding_weights = Tensor::from_vec(vec![0.1f32; 50257 * 768], &[50257, 768])?;
tensors.push(("token_embd.weight".to_string(), embedding_weights));
for layer in 0..12 {
let attn_q = Tensor::from_vec(vec![0.1f32; 768 * 768], &[768, 768])?;
let attn_k = Tensor::from_vec(vec![0.1f32; 768 * 768], &[768, 768])?;
let attn_v = Tensor::from_vec(vec![0.1f32; 768 * 768], &[768, 768])?;
let attn_out = Tensor::from_vec(vec![0.1f32; 768 * 768], &[768, 768])?;
tensors.push((format!("blk.{}.attn_q.weight", layer), attn_q));
tensors.push((format!("blk.{}.attn_k.weight", layer), attn_k));
tensors.push((format!("blk.{}.attn_v.weight", layer), attn_v));
tensors.push((format!("blk.{}.attn_output.weight", layer), attn_out));
let ffn_up = Tensor::from_vec(vec![0.1f32; 768 * 3072], &[768, 3072])?;
let ffn_down = Tensor::from_vec(vec![0.1f32; 3072 * 768], &[3072, 768])?;
tensors.push((format!("blk.{}.ffn_up.weight", layer), ffn_up));
tensors.push((format!("blk.{}.ffn_down.weight", layer), ffn_down));
let ln1 = Tensor::from_vec(vec![1.0f32; 768], &[768])?;
let ln2 = Tensor::from_vec(vec![1.0f32; 768], &[768])?;
tensors.push((format!("blk.{}.attn_norm.weight", layer), ln1));
tensors.push((format!("blk.{}.ffn_norm.weight", layer), ln2));
}
let final_norm = Tensor::from_vec(vec![1.0f32; 768], &[768])?;
let output_weights = Tensor::from_vec(vec![0.1f32; 768 * 50257], &[768, 50257])?;
tensors.push(("output_norm.weight".to_string(), final_norm));
tensors.push(("output.weight".to_string(), output_weights));
Ok(tensors)
}
fn write_header<W: Write>(&self, writer: &mut W, tensor_count: usize) -> Result<()> {
writer.write_u32::<LittleEndian>(GGUF_MAGIC)?;
writer.write_u32::<LittleEndian>(GGUF_VERSION)?;
writer.write_u64::<LittleEndian>(tensor_count as u64)?;
writer.write_u64::<LittleEndian>(self.metadata.len() as u64)?;
Ok(())
}
fn write_metadata<W: Write>(&self, writer: &mut W) -> Result<()> {
for (key, value) in &self.metadata {
writer.write_u64::<LittleEndian>(key.len() as u64)?;
writer.write_all(key.as_bytes())?;
writer.write_u32::<LittleEndian>(value.value_type() as u32)?;
value.write_to_buffer(writer)?;
}
Ok(())
}
fn calculate_tensor_data_offset(&self, tensors: &[(String, Tensor)]) -> Result<u64> {
let mut offset = 0u64;
offset += 4 + 4 + 8 + 8;
for (key, value) in &self.metadata {
offset += 8; offset += key.len() as u64; offset += 4; offset += self.calculate_value_size(value)?; }
for (name, tensor) in tensors {
offset += 8; offset += name.len() as u64; offset += 4; offset += tensor.shape().len() as u64 * 8; offset += 4; offset += 8; }
offset = (offset + 31) & !31;
Ok(offset)
}
fn calculate_value_size(&self, value: &GGUFValue) -> Result<u64> {
Ok(match value {
GGUFValue::UInt8(_) | GGUFValue::Int8(_) | GGUFValue::Bool(_) => 1,
GGUFValue::UInt16(_) | GGUFValue::Int16(_) => 2,
GGUFValue::UInt32(_) | GGUFValue::Int32(_) | GGUFValue::Float32(_) => 4,
GGUFValue::UInt64(_) | GGUFValue::Int64(_) | GGUFValue::Float64(_) => 8,
GGUFValue::String(s) => 8 + s.len() as u64, GGUFValue::Array(_, values) => {
let mut size = 4 + 8; for value in values {
size += Self::calculate_value_size_helper(value)?;
}
size
},
})
}
fn calculate_value_size_helper(value: &GGUFValue) -> Result<u64> {
Ok(match value {
GGUFValue::UInt8(_) | GGUFValue::Int8(_) | GGUFValue::Bool(_) => 1,
GGUFValue::UInt16(_) | GGUFValue::Int16(_) => 2,
GGUFValue::UInt32(_) | GGUFValue::Int32(_) | GGUFValue::Float32(_) => 4,
GGUFValue::UInt64(_) | GGUFValue::Int64(_) | GGUFValue::Float64(_) => 8,
GGUFValue::String(s) => 8 + s.len() as u64, GGUFValue::Array(_, values) => {
let mut size = 4 + 8; for value in values {
size += Self::calculate_value_size_helper(value)?;
}
size
},
})
}
fn write_tensor_info<W: Write>(
&self,
writer: &mut W,
tensors: &[(String, Tensor)],
mut data_offset: u64,
) -> Result<Vec<GGUFTensorInfo>> {
let mut tensor_infos = Vec::new();
for (name, tensor) in tensors {
writer.write_u64::<LittleEndian>(name.len() as u64)?;
writer.write_all(name.as_bytes())?;
writer.write_u32::<LittleEndian>(tensor.shape().len() as u32)?;
let dimensions: Vec<u64> = tensor.shape().iter().map(|&d| d as u64).collect();
for &dim in &dimensions {
writer.write_u64::<LittleEndian>(dim)?;
}
writer.write_u32::<LittleEndian>(self.quantization_type as u32)?;
writer.write_u64::<LittleEndian>(data_offset)?;
let tensor_size = self.calculate_tensor_size(tensor)?;
tensor_infos.push(GGUFTensorInfo {
name: name.clone(),
dimensions,
tensor_type: self.quantization_type,
offset: data_offset,
});
data_offset += tensor_size;
}
Ok(tensor_infos)
}
fn calculate_tensor_size(&self, tensor: &Tensor) -> Result<u64> {
let element_count = tensor.shape().iter().product::<usize>() as u64;
let element_size = self.quantization_type.element_size() as u64;
if self.quantization_type.is_quantized() {
let block_size = self.quantization_type.block_size() as u64;
let num_blocks = element_count.div_ceil(block_size);
Ok(num_blocks * element_size)
} else {
Ok(element_count * element_size)
}
}
fn align_to_tensor_data<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
let current_pos = writer.stream_position()?;
let aligned_pos = (current_pos + 31) & !31; let padding = aligned_pos - current_pos;
for _ in 0..padding {
writer.write_u8(0)?;
}
Ok(())
}
fn write_tensor_data<W: Write>(
&self,
writer: &mut W,
tensors: &[(String, Tensor)],
_tensor_infos: &[GGUFTensorInfo],
) -> Result<()> {
for (_name, tensor) in tensors {
match self.quantization_type {
GGUFTensorType::F32 => {
for value in tensor.data()? {
writer.write_f32::<LittleEndian>(value)?;
}
},
GGUFTensorType::F16 => {
for value in tensor.data()? {
let half_value = half::f16::from_f32(value);
writer.write_u16::<LittleEndian>(half_value.to_bits())?;
}
},
GGUFTensorType::Q8_0 => {
self.write_q8_0_tensor(writer, tensor)?;
},
GGUFTensorType::Q4_0 => {
self.write_q4_0_tensor(writer, tensor)?;
},
_ => {
return Err(anyhow!(
"Unsupported quantization type: {:?}",
self.quantization_type
));
},
}
}
Ok(())
}
fn write_q8_0_tensor<W: Write>(&self, writer: &mut W, tensor: &Tensor) -> Result<()> {
let data = tensor.data()?;
let block_size = 32;
for chunk in data.chunks(block_size) {
let max_abs = chunk.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
let scale = max_abs / 127.0;
writer.write_f32::<LittleEndian>(scale)?;
for &value in chunk {
let quantized = if scale > 0.0 {
(value / scale).round().clamp(-128.0, 127.0) as i8
} else {
0i8
};
writer.write_i8(quantized)?;
}
for _ in chunk.len()..block_size {
writer.write_i8(0)?;
}
}
Ok(())
}
fn write_q4_0_tensor<W: Write>(&self, writer: &mut W, tensor: &Tensor) -> Result<()> {
let data = tensor.data()?;
let block_size = 32;
for chunk in data.chunks(block_size) {
let max_abs = chunk.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
let scale = max_abs / 7.0;
writer.write_f32::<LittleEndian>(scale)?;
let mut quantized_values = Vec::new();
for &value in chunk {
let quantized =
if scale > 0.0 { (value / scale).round().clamp(-8.0, 7.0) as i8 } else { 0i8 };
quantized_values.push(quantized);
}
while quantized_values.len() < block_size {
quantized_values.push(0);
}
for pair in quantized_values.chunks(2) {
let byte =
((pair[0] & 0xF) as u8) | (((pair.get(1).unwrap_or(&0) & 0xF) as u8) << 4);
writer.write_u8(byte)?;
}
}
Ok(())
}
}
impl ModelExporter for GGUFExporter {
fn export<M: Model>(&self, model: &M, config: &ExportConfig) -> Result<()> {
if config.format != ExportFormat::GGUF {
return Err(anyhow!("GGUFExporter only supports GGUF format"));
}
self.export_to_gguf(model, config)
}
fn supported_formats(&self) -> Vec<ExportFormat> {
vec![ExportFormat::GGUF]
}
fn validate_model<M: Model>(&self, _model: &M, format: ExportFormat) -> Result<()> {
if format != ExportFormat::GGUF {
return Err(anyhow!("GGUFExporter only supports GGUF format"));
}
Ok(())
}
}
pub struct GGUFConverter;
impl GGUFConverter {
pub fn convert_quantization(
input_path: &str,
output_path: &str,
target_type: GGUFTensorType,
) -> Result<()> {
println!(
"Converting GGUF model from {} to {} with {:?} quantization",
input_path, output_path, target_type
);
std::fs::copy(input_path, output_path)?;
Ok(())
}
pub fn validate_file(path: &str) -> Result<GGUFValidationReport> {
let file = File::open(path)?;
let mut reader = std::io::BufReader::new(file);
let mut magic = [0u8; 4];
std::io::Read::read_exact(&mut reader, &mut magic)?;
if u32::from_le_bytes(magic) != GGUF_MAGIC {
return Err(anyhow!("Invalid GGUF magic number"));
}
let mut version = [0u8; 4];
std::io::Read::read_exact(&mut reader, &mut version)?;
let version = u32::from_le_bytes(version);
let mut tensor_count = [0u8; 8];
std::io::Read::read_exact(&mut reader, &mut tensor_count)?;
let tensor_count = u64::from_le_bytes(tensor_count);
let mut kv_count = [0u8; 8];
std::io::Read::read_exact(&mut reader, &mut kv_count)?;
let kv_count = u64::from_le_bytes(kv_count);
Ok(GGUFValidationReport {
is_valid: true,
version,
tensor_count,
metadata_count: kv_count,
file_size: std::fs::metadata(path)?.len(),
errors: Vec::new(),
warnings: Vec::new(),
})
}
}
#[derive(Debug)]
pub struct GGUFValidationReport {
pub is_valid: bool,
pub version: u32,
pub tensor_count: u64,
pub metadata_count: u64,
pub file_size: u64,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_gguf_tensor_type_properties() {
assert_eq!(GGUFTensorType::F32.element_size(), 4);
assert_eq!(GGUFTensorType::F16.element_size(), 2);
assert_eq!(GGUFTensorType::Q4_0.block_size(), 32);
assert!(GGUFTensorType::Q8_0.is_quantized());
assert!(!GGUFTensorType::F32.is_quantized());
}
#[test]
fn test_gguf_value_types() {
let int_val = GGUFValue::Int32(42);
let str_val = GGUFValue::String("test".to_string());
let array_val = GGUFValue::Array(
GGUFValueType::Int32,
vec![GGUFValue::Int32(1), GGUFValue::Int32(2)],
);
assert!(matches!(int_val.value_type(), GGUFValueType::Int32));
assert!(matches!(str_val.value_type(), GGUFValueType::String));
assert!(matches!(array_val.value_type(), GGUFValueType::Array));
}
#[test]
fn test_gguf_exporter_creation() {
let exporter = GGUFExporter::new();
assert_eq!(exporter.quantization_type, GGUFTensorType::F32);
assert!(!exporter.metadata.is_empty());
}
#[test]
fn test_gguf_exporter_with_quantization() {
let exporter = GGUFExporter::new().with_quantization(GGUFTensorType::Q4_0);
assert_eq!(exporter.quantization_type, GGUFTensorType::Q4_0);
}
#[test]
fn test_gguf_exporter_metadata() {
let exporter = GGUFExporter::new()
.add_metadata(
"custom.key".to_string(),
GGUFValue::String("value".to_string()),
)
.set_architecture_metadata(2048, 768, 12, 3072, 12, Some(12), 50257);
assert!(exporter.metadata.contains_key("custom.key"));
assert!(exporter.metadata.contains_key("llama.context_length"));
}
#[test]
fn test_gguf_value_serialization() -> Result<()> {
let mut buffer = Vec::new();
let value = GGUFValue::String("test".to_string());
value.write_to_buffer(&mut buffer)?;
assert_eq!(buffer.len(), 12);
Ok(())
}
#[test]
fn test_supported_formats() {
let exporter = GGUFExporter::new();
let formats = exporter.supported_formats();
assert_eq!(formats.len(), 1);
assert_eq!(formats[0], ExportFormat::GGUF);
}
#[test]
fn test_gguf_converter_validation() -> Result<()> {
let temp_dir = tempdir()?;
let temp_file = temp_dir.path().join("test.gguf");
let mut file = File::create(&temp_file)?;
file.write_u32::<LittleEndian>(GGUF_MAGIC)?;
file.write_u32::<LittleEndian>(GGUF_VERSION)?;
file.write_u64::<LittleEndian>(0)?; file.write_u64::<LittleEndian>(0)?;
let report =
GGUFConverter::validate_file(temp_file.to_str().expect("temp file creation failed"))?;
assert!(report.is_valid);
assert_eq!(report.version, GGUF_VERSION);
assert_eq!(report.tensor_count, 0);
assert_eq!(report.metadata_count, 0);
Ok(())
}
}