use super::{
is_aligned_64, AprV2Header, AprV2Metadata, TensorDType, TensorIndexEntry, V2FormatError,
HEADER_SIZE_V2,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Read;
#[derive(Debug)]
pub struct AprV2Reader {
header: AprV2Header,
metadata: AprV2Metadata,
tensor_index: Vec<TensorIndexEntry>,
data: Vec<u8>,
}
#[derive(Debug)]
pub struct AprV2ReaderRef<'a> {
header: AprV2Header,
metadata: AprV2Metadata,
tensor_index: Vec<TensorIndexEntry>,
data: &'a [u8],
}
fn parse_metadata_section(
data: &[u8],
metadata_offset: u64,
metadata_size: u32,
) -> Result<AprV2Metadata, V2FormatError> {
let start = usize::try_from(metadata_offset)
.map_err(|_| V2FormatError::InvalidHeader("metadata_offset exceeds usize".to_string()))?;
let end = start
.checked_add(metadata_size as usize)
.ok_or_else(|| V2FormatError::InvalidHeader("metadata offset+size overflow".to_string()))?;
let slice = data
.get(start..end)
.ok_or_else(|| V2FormatError::InvalidHeader("file too small for metadata".to_string()))?;
AprV2Metadata::from_json(slice)
}
fn parse_tensor_index_section(
data: &[u8],
tensor_index_offset: u64,
tensor_count: u32,
) -> Result<Vec<TensorIndexEntry>, V2FormatError> {
let mut pos = usize::try_from(tensor_index_offset).map_err(|_| {
V2FormatError::InvalidTensorIndex("tensor_index_offset exceeds usize".to_string())
})?;
let mut tensor_index = Vec::with_capacity(tensor_count as usize);
for _ in 0..tensor_count {
let remaining = data.get(pos..).ok_or_else(|| {
V2FormatError::InvalidTensorIndex("tensor index offset past end of file".to_string())
})?;
let (entry, consumed) = TensorIndexEntry::from_bytes(remaining)?;
tensor_index.push(entry);
pos = pos.checked_add(consumed).ok_or_else(|| {
V2FormatError::InvalidTensorIndex("tensor index position overflow".to_string())
})?;
}
for i in 1..tensor_index.len() {
if tensor_index[i].name < tensor_index[i - 1].name {
return Err(V2FormatError::InvalidTensorIndex(
"tensor index not sorted".to_string(),
));
}
}
Ok(tensor_index)
}
impl AprV2Reader {
pub fn from_bytes(data: &[u8]) -> Result<Self, V2FormatError> {
if data.len() < HEADER_SIZE_V2 {
return Err(V2FormatError::InvalidHeader("file too small".to_string()));
}
let header = AprV2Header::from_bytes(data)?;
if !header.verify_checksum() {
return Err(V2FormatError::ChecksumMismatch);
}
if !header.flags.is_layout_valid() {
return Err(V2FormatError::InvalidHeader(
"LAYOUT-002 violation: APR file has LAYOUT_COLUMN_MAJOR flag set. \
This indicates a dirty import from GGUF without proper transpose. \
Re-import the model using `apr import` with LAYOUT-002 enforcement."
.to_string(),
));
}
let metadata = parse_metadata_section(data, header.metadata_offset, header.metadata_size)?;
let tensor_index =
parse_tensor_index_section(data, header.tensor_index_offset, header.tensor_count)?;
Ok(Self {
header,
metadata,
tensor_index,
data: data.to_vec(),
})
}
pub fn from_reader<R: Read>(reader: &mut R) -> Result<Self, V2FormatError> {
let mut data = Vec::new();
reader
.read_to_end(&mut data)
.map_err(|e| V2FormatError::IoError(e.to_string()))?;
Self::from_bytes(&data)
}
#[must_use]
pub fn header(&self) -> &AprV2Header {
&self.header
}
#[must_use]
pub fn metadata(&self) -> &AprV2Metadata {
&self.metadata
}
#[must_use]
pub fn tensor_names(&self) -> Vec<&str> {
self.tensor_index.iter().map(|e| e.name.as_str()).collect()
}
#[must_use]
pub fn get_tensor(&self, name: &str) -> Option<&TensorIndexEntry> {
self.tensor_index.iter().find(|e| e.name == name)
}
#[must_use]
pub fn get_tensor_data(&self, name: &str) -> Option<&[u8]> {
let entry = self.get_tensor(name)?;
let abs_offset = self.header.data_offset.checked_add(entry.offset)?;
let start = usize::try_from(abs_offset).ok()?;
let end = start.checked_add(usize::try_from(entry.size).ok()?)?;
self.data.get(start..end)
}
#[must_use]
pub fn get_f32_tensor(&self, name: &str) -> Option<Vec<f32>> {
let entry = self.get_tensor(name)?;
if entry.dtype != TensorDType::F32 {
return None;
}
let data = self.get_tensor_data(name)?;
let floats: Vec<f32> = data
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
Some(floats)
}
#[must_use]
pub fn verify_alignment(&self) -> bool {
let data_offset = self.header.data_offset as usize;
self.tensor_index
.iter()
.all(|e| is_aligned_64(data_offset + e.offset as usize))
}
#[must_use]
pub fn tensor_index(&self) -> &[TensorIndexEntry] {
&self.tensor_index
}
}
impl<'a> AprV2ReaderRef<'a> {
pub fn from_bytes(data: &'a [u8]) -> Result<Self, V2FormatError> {
if data.len() < HEADER_SIZE_V2 {
return Err(V2FormatError::InvalidHeader("file too small".to_string()));
}
let header = AprV2Header::from_bytes(data)?;
if !header.verify_checksum() {
return Err(V2FormatError::ChecksumMismatch);
}
if !header.flags.is_layout_valid() {
return Err(V2FormatError::InvalidHeader(
"LAYOUT-002 violation: APR file has LAYOUT_COLUMN_MAJOR flag set. \
This indicates a dirty import from GGUF without proper transpose. \
Re-import the model using `apr import` with LAYOUT-002 enforcement."
.to_string(),
));
}
let metadata = parse_metadata_section(data, header.metadata_offset, header.metadata_size)?;
let tensor_index =
parse_tensor_index_section(data, header.tensor_index_offset, header.tensor_count)?;
Ok(Self {
header,
metadata,
tensor_index,
data, })
}
#[must_use]
pub fn header(&self) -> &AprV2Header {
&self.header
}
#[must_use]
pub fn metadata(&self) -> &AprV2Metadata {
&self.metadata
}
#[must_use]
pub fn tensor_names(&self) -> Vec<&str> {
self.tensor_index.iter().map(|e| e.name.as_str()).collect()
}
#[must_use]
pub fn get_tensor(&self, name: &str) -> Option<&TensorIndexEntry> {
self.tensor_index.iter().find(|e| e.name == name)
}
#[must_use]
pub fn get_tensor_data(&self, name: &str) -> Option<&[u8]> {
let entry = self.get_tensor(name)?;
let abs_offset = self.header.data_offset.checked_add(entry.offset)?;
let start = usize::try_from(abs_offset).ok()?;
let end = start.checked_add(usize::try_from(entry.size).ok()?)?;
self.data.get(start..end)
}
#[must_use]
pub fn get_f32_tensor(&self, name: &str) -> Option<Vec<f32>> {
let entry = self.get_tensor(name)?;
if entry.dtype != TensorDType::F32 {
return None;
}
let data = self.get_tensor_data(name)?;
let floats: Vec<f32> = data
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
Some(floats)
}
#[must_use]
pub fn verify_alignment(&self) -> bool {
let data_offset = self.header.data_offset as usize;
self.tensor_index
.iter()
.all(|e| is_aligned_64(data_offset + e.offset as usize))
}
#[must_use]
pub fn tensor_index(&self) -> &[TensorIndexEntry] {
&self.tensor_index
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShardManifest {
pub version: String,
pub shard_count: usize,
pub total_size: u64,
pub tensor_count: usize,
pub shards: Vec<ShardInfo>,
pub weight_map: HashMap<String, usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShardInfo {
pub filename: String,
pub index: usize,
pub size: u64,
pub tensors: Vec<String>,
}