use crate::{tensor::TensorDtype, Error, Ipld, Result, TensorBlock, TensorShape};
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SafetensorInfo {
pub dtype: String,
pub shape: Vec<usize>,
pub data_offsets: [usize; 2],
}
impl SafetensorInfo {
pub fn to_tensor_dtype(&self) -> Result<TensorDtype> {
match self.dtype.as_str() {
"F32" => Ok(TensorDtype::F32),
"F64" => Ok(TensorDtype::F64),
"F16" => Ok(TensorDtype::F16),
"I8" => Ok(TensorDtype::I8),
"I32" => Ok(TensorDtype::I32),
"I64" => Ok(TensorDtype::I64),
"U8" => Ok(TensorDtype::U8),
"U32" => Ok(TensorDtype::U32),
"BOOL" => Ok(TensorDtype::Bool),
_ => Err(Error::InvalidData(format!(
"Unsupported Safetensors dtype: {}",
self.dtype
))),
}
}
pub fn size_bytes(&self) -> usize {
self.data_offsets[1] - self.data_offsets[0]
}
}
#[derive(Debug, Clone)]
pub struct SafetensorsFile {
tensors: BTreeMap<String, SafetensorInfo>,
data: Bytes,
#[allow(dead_code)]
data_offset: usize,
}
impl SafetensorsFile {
pub fn parse(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 8 {
return Err(Error::InvalidData(
"Safetensors file too small (missing header length)".to_string(),
));
}
let header_len = u64::from_le_bytes(
bytes[0..8]
.try_into()
.map_err(|_| Error::InvalidData("Failed to read header length".to_string()))?,
) as usize;
if bytes.len() < 8 + header_len {
return Err(Error::InvalidData(
"Safetensors file too small (truncated header)".to_string(),
));
}
let header_bytes = &bytes[8..8 + header_len];
let tensors: BTreeMap<String, SafetensorInfo> = serde_json::from_slice(header_bytes)
.map_err(|e| {
Error::InvalidData(format!("Failed to parse Safetensors header: {}", e))
})?;
let data_offset = 8 + header_len;
let data = Bytes::copy_from_slice(&bytes[data_offset..]);
Ok(Self {
tensors,
data,
data_offset,
})
}
pub fn tensors(&self) -> &BTreeMap<String, SafetensorInfo> {
&self.tensors
}
pub fn get_tensor_info(&self, name: &str) -> Option<&SafetensorInfo> {
self.tensors.get(name)
}
pub fn get_tensor_data(&self, name: &str) -> Result<Bytes> {
let info = self
.get_tensor_info(name)
.ok_or_else(|| Error::InvalidData(format!("Tensor '{}' not found", name)))?;
let start = info.data_offsets[0];
let end = info.data_offsets[1];
if end > self.data.len() {
return Err(Error::InvalidData(format!(
"Tensor '{}' data offset out of bounds",
name
)));
}
Ok(self.data.slice(start..end))
}
pub fn to_tensor_block(&self, name: &str) -> Result<TensorBlock> {
let info = self
.get_tensor_info(name)
.ok_or_else(|| Error::InvalidData(format!("Tensor '{}' not found", name)))?;
let data = self.get_tensor_data(name)?;
let shape = TensorShape::new(info.shape.clone());
let dtype = info.to_tensor_dtype()?;
TensorBlock::new(data, shape, dtype)
}
pub fn to_ipld_metadata(&self) -> Result<Ipld> {
let mut metadata = BTreeMap::new();
for (name, info) in &self.tensors {
let tensor_block = self.to_tensor_block(name)?;
let cid = *tensor_block.cid();
let mut tensor_meta = BTreeMap::new();
tensor_meta.insert("dtype".to_string(), Ipld::String(info.dtype.clone()));
tensor_meta.insert(
"shape".to_string(),
Ipld::List(
info.shape
.iter()
.map(|&s| Ipld::Integer(s as i128))
.collect(),
),
);
tensor_meta.insert("data".to_string(), Ipld::Link(cid.into()));
metadata.insert(name.clone(), Ipld::Map(tensor_meta));
}
Ok(Ipld::Map(metadata))
}
pub fn tensor_count(&self) -> usize {
self.tensors.len()
}
pub fn data_size(&self) -> usize {
self.data.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_safetensors() -> Vec<u8> {
let metadata = serde_json::json!({
"weight": {
"dtype": "F32",
"shape": [2, 2],
"data_offsets": [0, 16]
}
});
let header_bytes = serde_json::to_vec(&metadata).unwrap();
let header_len = header_bytes.len() as u64;
let mut file = Vec::new();
file.extend_from_slice(&header_len.to_le_bytes());
file.extend_from_slice(&header_bytes);
file.extend_from_slice(&1.0f32.to_le_bytes());
file.extend_from_slice(&2.0f32.to_le_bytes());
file.extend_from_slice(&3.0f32.to_le_bytes());
file.extend_from_slice(&4.0f32.to_le_bytes());
file
}
#[test]
fn test_parse_safetensors() {
let data = create_test_safetensors();
let file = SafetensorsFile::parse(&data).unwrap();
assert_eq!(file.tensor_count(), 1);
assert!(file.get_tensor_info("weight").is_some());
}
#[test]
fn test_tensor_info() {
let data = create_test_safetensors();
let file = SafetensorsFile::parse(&data).unwrap();
let info = file.get_tensor_info("weight").unwrap();
assert_eq!(info.dtype, "F32");
assert_eq!(info.shape, vec![2, 2]);
assert_eq!(info.data_offsets, [0, 16]);
assert_eq!(info.size_bytes(), 16);
}
#[test]
fn test_get_tensor_data() {
let data = create_test_safetensors();
let file = SafetensorsFile::parse(&data).unwrap();
let tensor_data = file.get_tensor_data("weight").unwrap();
assert_eq!(tensor_data.len(), 16);
}
#[test]
fn test_to_tensor_block() {
let data = create_test_safetensors();
let file = SafetensorsFile::parse(&data).unwrap();
let tensor = file.to_tensor_block("weight").unwrap();
assert_eq!(tensor.element_count(), 4);
assert_eq!(tensor.dtype(), TensorDtype::F32);
assert_eq!(tensor.shape().dims(), &[2, 2]);
}
#[test]
fn test_to_ipld_metadata() {
let data = create_test_safetensors();
let file = SafetensorsFile::parse(&data).unwrap();
let ipld = file.to_ipld_metadata().unwrap();
if let Ipld::Map(metadata) = ipld {
assert!(metadata.contains_key("weight"));
if let Some(Ipld::Map(tensor_meta)) = metadata.get("weight") {
assert!(tensor_meta.contains_key("dtype"));
assert!(tensor_meta.contains_key("shape"));
assert!(tensor_meta.contains_key("data"));
} else {
panic!("Expected tensor metadata to be a map");
}
} else {
panic!("Expected IPLD to be a map");
}
}
#[test]
fn test_invalid_safetensors() {
let result = SafetensorsFile::parse(&[1, 2, 3]);
assert!(result.is_err());
let mut invalid = vec![0u8; 8];
invalid.extend_from_slice(&100u64.to_le_bytes()[..8]);
let result = SafetensorsFile::parse(&invalid);
assert!(result.is_err());
}
#[test]
fn test_dtype_conversion() {
let info = SafetensorInfo {
dtype: "F32".to_string(),
shape: vec![2, 2],
data_offsets: [0, 16],
};
assert_eq!(info.to_tensor_dtype().unwrap(), TensorDtype::F32);
let invalid_info = SafetensorInfo {
dtype: "INVALID".to_string(),
shape: vec![2, 2],
data_offsets: [0, 16],
};
assert!(invalid_info.to_tensor_dtype().is_err());
}
#[test]
fn test_multiple_tensors() {
let metadata = serde_json::json!({
"weight": {
"dtype": "F32",
"shape": [2, 2],
"data_offsets": [0, 16]
},
"bias": {
"dtype": "F32",
"shape": [2],
"data_offsets": [16, 24]
}
});
let header_bytes = serde_json::to_vec(&metadata).unwrap();
let header_len = header_bytes.len() as u64;
let mut file = Vec::new();
file.extend_from_slice(&header_len.to_le_bytes());
file.extend_from_slice(&header_bytes);
file.extend_from_slice(&1.0f32.to_le_bytes());
file.extend_from_slice(&2.0f32.to_le_bytes());
file.extend_from_slice(&3.0f32.to_le_bytes());
file.extend_from_slice(&4.0f32.to_le_bytes());
file.extend_from_slice(&0.1f32.to_le_bytes());
file.extend_from_slice(&0.2f32.to_le_bytes());
let parsed = SafetensorsFile::parse(&file).unwrap();
assert_eq!(parsed.tensor_count(), 2);
let weight = parsed.to_tensor_block("weight").unwrap();
assert_eq!(weight.element_count(), 4);
let bias = parsed.to_tensor_block("bias").unwrap();
assert_eq!(bias.element_count(), 2);
}
}