use std::collections::HashMap;
use std::fmt;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct GgufHeader {
pub magic: [u8; 4],
pub version: u32,
pub tensor_count: u64,
pub metadata_kv_count: u64,
}
#[derive(Debug, Clone)]
pub enum GgufValue {
U8(u8),
I8(i8),
U16(u16),
I16(i16),
U32(u32),
I32(i32),
U64(u64),
I64(i64),
F32(f32),
F64(f64),
Bool(bool),
String(String),
Array(Vec<GgufValue>),
}
#[derive(Debug, Clone)]
pub struct GgufTensorInfo {
pub name: String,
pub n_dims: u32,
pub dims: Vec<u64>,
pub dtype: u32,
pub offset: u64,
}
pub type GgufResult<T> = Result<T, GgufError>;
#[derive(Debug, Clone)]
pub enum GgufError {
InvalidMagic([u8; 4]),
UnsupportedVersion(u32),
Io(String),
InvalidData(String),
TensorNotFound(String),
}
impl fmt::Display for GgufError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GgufError::InvalidMagic(magic) => {
write!(f, "Invalid GGUF magic: {:?}", magic)
}
GgufError::UnsupportedVersion(v) => {
write!(f, "Unsupported GGUF version: {}", v)
}
GgufError::Io(msg) => write!(f, "IO error: {}", msg),
GgufError::InvalidData(msg) => write!(f, "Invalid data: {}", msg),
GgufError::TensorNotFound(name) => write!(f, "Tensor not found: {}", name),
}
}
}
impl std::error::Error for GgufError {}
#[derive(Debug)]
pub struct GgufLoader {
path: String,
header: Option<GgufHeader>,
tensors: Vec<GgufTensorInfo>,
metadata: HashMap<String, GgufValue>,
}
impl GgufLoader {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_string_lossy().to_string(),
header: None,
tensors: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn validate_path(&self) -> GgufResult<()> {
let path = Path::new(&self.path);
if !path.exists() {
return Err(GgufError::Io(format!("File not found: {}", self.path)));
}
if path.extension().map_or(true, |ext| ext != "gguf") {
return Err(GgufError::InvalidData(
"File does not have .gguf extension".to_string(),
));
}
Ok(())
}
pub fn parse_header(&mut self, data: &[u8]) -> GgufResult<()> {
if data.len() < 24 {
return Err(GgufError::InvalidData(
"File too small for header".to_string(),
));
}
let magic: [u8; 4] = data[0..4].try_into().expect("invariant: slice is 4 bytes");
if &magic != b"GGUF" {
return Err(GgufError::InvalidMagic(magic));
}
let version =
u32::from_le_bytes(data[4..8].try_into().expect("invariant: slice is 4 bytes"));
if !(2..=3).contains(&version) {
return Err(GgufError::UnsupportedVersion(version));
}
let tensor_count =
u64::from_le_bytes(data[8..16].try_into().expect("invariant: slice is 8 bytes"));
let metadata_kv_count = u64::from_le_bytes(
data[16..24]
.try_into()
.expect("invariant: slice is 8 bytes"),
);
self.header = Some(GgufHeader {
magic,
version,
tensor_count,
metadata_kv_count,
});
Ok(())
}
pub fn header(&self) -> Option<&GgufHeader> {
self.header.as_ref()
}
pub fn tensor_count(&self) -> u64 {
self.header.as_ref().map_or(0, |h| h.tensor_count)
}
pub fn path(&self) -> &str {
&self.path
}
}