apr_format/v2/
reader_impl.rs1use super::{
18 is_aligned_64, AprV2Header, AprV2Metadata, TensorDType, TensorIndexEntry, V2FormatError,
19 HEADER_SIZE_V2,
20};
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::io::Read;
24
25#[derive(Debug)]
27pub struct AprV2Reader {
28 header: AprV2Header,
29 metadata: AprV2Metadata,
30 tensor_index: Vec<TensorIndexEntry>,
31 data: Vec<u8>,
32}
33
34#[derive(Debug)]
49pub struct AprV2ReaderRef<'a> {
50 header: AprV2Header,
51 metadata: AprV2Metadata,
52 tensor_index: Vec<TensorIndexEntry>,
53 data: &'a [u8],
54}
55
56fn parse_metadata_section(
64 data: &[u8],
65 metadata_offset: u64,
66 metadata_size: u32,
67) -> Result<AprV2Metadata, V2FormatError> {
68 let start = usize::try_from(metadata_offset)
69 .map_err(|_| V2FormatError::InvalidHeader("metadata_offset exceeds usize".to_string()))?;
70 let end = start
71 .checked_add(metadata_size as usize)
72 .ok_or_else(|| V2FormatError::InvalidHeader("metadata offset+size overflow".to_string()))?;
73 let slice = data
74 .get(start..end)
75 .ok_or_else(|| V2FormatError::InvalidHeader("file too small for metadata".to_string()))?;
76 AprV2Metadata::from_json(slice)
77}
78
79fn parse_tensor_index_section(
87 data: &[u8],
88 tensor_index_offset: u64,
89 tensor_count: u32,
90) -> Result<Vec<TensorIndexEntry>, V2FormatError> {
91 let mut pos = usize::try_from(tensor_index_offset).map_err(|_| {
92 V2FormatError::InvalidTensorIndex("tensor_index_offset exceeds usize".to_string())
93 })?;
94
95 let mut tensor_index = Vec::with_capacity(tensor_count as usize);
96 for _ in 0..tensor_count {
97 let remaining = data.get(pos..).ok_or_else(|| {
101 V2FormatError::InvalidTensorIndex("tensor index offset past end of file".to_string())
102 })?;
103 let (entry, consumed) = TensorIndexEntry::from_bytes(remaining)?;
104 tensor_index.push(entry);
105 pos = pos.checked_add(consumed).ok_or_else(|| {
106 V2FormatError::InvalidTensorIndex("tensor index position overflow".to_string())
107 })?;
108 }
109
110 for i in 1..tensor_index.len() {
112 if tensor_index[i].name < tensor_index[i - 1].name {
113 return Err(V2FormatError::InvalidTensorIndex(
114 "tensor index not sorted".to_string(),
115 ));
116 }
117 }
118
119 Ok(tensor_index)
120}
121
122impl AprV2Reader {
123 pub fn from_bytes(data: &[u8]) -> Result<Self, V2FormatError> {
132 if data.len() < HEADER_SIZE_V2 {
133 return Err(V2FormatError::InvalidHeader("file too small".to_string()));
134 }
135
136 let header = AprV2Header::from_bytes(data)?;
138
139 if !header.verify_checksum() {
141 return Err(V2FormatError::ChecksumMismatch);
142 }
143
144 if !header.flags.is_layout_valid() {
146 return Err(V2FormatError::InvalidHeader(
147 "LAYOUT-002 violation: APR file has LAYOUT_COLUMN_MAJOR flag set. \
148 This indicates a dirty import from GGUF without proper transpose. \
149 Re-import the model using `apr import` with LAYOUT-002 enforcement."
150 .to_string(),
151 ));
152 }
153
154 let metadata = parse_metadata_section(data, header.metadata_offset, header.metadata_size)?;
157
158 let tensor_index =
160 parse_tensor_index_section(data, header.tensor_index_offset, header.tensor_count)?;
161
162 Ok(Self {
163 header,
164 metadata,
165 tensor_index,
166 data: data.to_vec(),
167 })
168 }
169
170 pub fn from_reader<R: Read>(reader: &mut R) -> Result<Self, V2FormatError> {
175 let mut data = Vec::new();
176 reader
177 .read_to_end(&mut data)
178 .map_err(|e| V2FormatError::IoError(e.to_string()))?;
179 Self::from_bytes(&data)
180 }
181
182 #[must_use]
184 pub fn header(&self) -> &AprV2Header {
185 &self.header
186 }
187
188 #[must_use]
190 pub fn metadata(&self) -> &AprV2Metadata {
191 &self.metadata
192 }
193
194 #[must_use]
196 pub fn tensor_names(&self) -> Vec<&str> {
197 self.tensor_index.iter().map(|e| e.name.as_str()).collect()
198 }
199
200 #[must_use]
202 pub fn get_tensor(&self, name: &str) -> Option<&TensorIndexEntry> {
203 self.tensor_index.iter().find(|e| e.name == name)
204 }
205
206 #[must_use]
208 pub fn get_tensor_data(&self, name: &str) -> Option<&[u8]> {
209 let entry = self.get_tensor(name)?;
210 let abs_offset = self.header.data_offset.checked_add(entry.offset)?;
215 let start = usize::try_from(abs_offset).ok()?;
216 let end = start.checked_add(usize::try_from(entry.size).ok()?)?;
217 self.data.get(start..end)
218 }
219
220 #[must_use]
222 pub fn get_f32_tensor(&self, name: &str) -> Option<Vec<f32>> {
223 let entry = self.get_tensor(name)?;
224 if entry.dtype != TensorDType::F32 {
225 return None;
226 }
227
228 let data = self.get_tensor_data(name)?;
229 let floats: Vec<f32> = data
230 .chunks_exact(4)
231 .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
232 .collect();
233
234 Some(floats)
235 }
236
237 #[must_use]
244 pub fn verify_alignment(&self) -> bool {
245 let data_offset = self.header.data_offset as usize;
246 self.tensor_index
247 .iter()
248 .all(|e| is_aligned_64(data_offset + e.offset as usize))
249 }
250
251 #[must_use]
253 pub fn tensor_index(&self) -> &[TensorIndexEntry] {
254 &self.tensor_index
255 }
256}
257
258impl<'a> AprV2ReaderRef<'a> {
259 pub fn from_bytes(data: &'a [u8]) -> Result<Self, V2FormatError> {
271 if data.len() < HEADER_SIZE_V2 {
272 return Err(V2FormatError::InvalidHeader("file too small".to_string()));
273 }
274
275 let header = AprV2Header::from_bytes(data)?;
277
278 if !header.verify_checksum() {
280 return Err(V2FormatError::ChecksumMismatch);
281 }
282
283 if !header.flags.is_layout_valid() {
285 return Err(V2FormatError::InvalidHeader(
286 "LAYOUT-002 violation: APR file has LAYOUT_COLUMN_MAJOR flag set. \
287 This indicates a dirty import from GGUF without proper transpose. \
288 Re-import the model using `apr import` with LAYOUT-002 enforcement."
289 .to_string(),
290 ));
291 }
292
293 let metadata = parse_metadata_section(data, header.metadata_offset, header.metadata_size)?;
296
297 let tensor_index =
299 parse_tensor_index_section(data, header.tensor_index_offset, header.tensor_count)?;
300
301 Ok(Self {
302 header,
303 metadata,
304 tensor_index,
305 data, })
307 }
308
309 #[must_use]
311 pub fn header(&self) -> &AprV2Header {
312 &self.header
313 }
314
315 #[must_use]
317 pub fn metadata(&self) -> &AprV2Metadata {
318 &self.metadata
319 }
320
321 #[must_use]
323 pub fn tensor_names(&self) -> Vec<&str> {
324 self.tensor_index.iter().map(|e| e.name.as_str()).collect()
325 }
326
327 #[must_use]
329 pub fn get_tensor(&self, name: &str) -> Option<&TensorIndexEntry> {
330 self.tensor_index.iter().find(|e| e.name == name)
331 }
332
333 #[must_use]
335 pub fn get_tensor_data(&self, name: &str) -> Option<&[u8]> {
336 let entry = self.get_tensor(name)?;
337 let abs_offset = self.header.data_offset.checked_add(entry.offset)?;
342 let start = usize::try_from(abs_offset).ok()?;
343 let end = start.checked_add(usize::try_from(entry.size).ok()?)?;
344 self.data.get(start..end)
345 }
346
347 #[must_use]
352 pub fn get_f32_tensor(&self, name: &str) -> Option<Vec<f32>> {
353 let entry = self.get_tensor(name)?;
354 if entry.dtype != TensorDType::F32 {
355 return None;
356 }
357
358 let data = self.get_tensor_data(name)?;
359 let floats: Vec<f32> = data
360 .chunks_exact(4)
361 .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
362 .collect();
363
364 Some(floats)
365 }
366
367 #[must_use]
372 pub fn verify_alignment(&self) -> bool {
373 let data_offset = self.header.data_offset as usize;
374 self.tensor_index
375 .iter()
376 .all(|e| is_aligned_64(data_offset + e.offset as usize))
377 }
378
379 #[must_use]
381 pub fn tensor_index(&self) -> &[TensorIndexEntry] {
382 &self.tensor_index
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct ShardManifest {
393 pub version: String,
395 pub shard_count: usize,
397 pub total_size: u64,
399 pub tensor_count: usize,
401 pub shards: Vec<ShardInfo>,
403 pub weight_map: HashMap<String, usize>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ShardInfo {
410 pub filename: String,
412 pub index: usize,
414 pub size: u64,
416 pub tensors: Vec<String>,
418}