1use anyhow::{bail, Context, Result};
13use serde::Serialize;
14use std::collections::BTreeMap;
15use std::io::Read;
16use std::path::Path;
17
18use crate::ir::{Certainty, CheckpointMatch, Structure};
19
20#[derive(Debug, Clone)]
21pub struct TensorInfo {
22 pub shape: Vec<usize>,
23 pub dtype: String,
24}
25
26pub type Header = BTreeMap<String, TensorInfo>;
27const MAX_HEADER_BYTES: u64 = 100_000_000;
28
29pub fn read_header(path: &Path) -> Result<Header> {
34 let mut file =
35 std::fs::File::open(path).with_context(|| format!("opening {}", path.display()))?;
36 let file_len = file
37 .metadata()
38 .with_context(|| format!("reading metadata for {}", path.display()))?
39 .len();
40
41 let mut prefix = [0u8; 8];
42 file.read_exact(&mut prefix)
43 .with_context(|| format!("{} is too short to be a safetensors file", path.display()))?;
44 let header_len_u64 = u64::from_le_bytes(prefix);
45 if header_len_u64 > MAX_HEADER_BYTES {
46 bail!(
47 "{} declares a {} byte header, exceeding the {} byte safety limit",
48 path.display(),
49 header_len_u64,
50 MAX_HEADER_BYTES
51 );
52 }
53 let header_end = 8u64
54 .checked_add(header_len_u64)
55 .filter(|end| *end <= file_len)
56 .with_context(|| format!("{} declares a header longer than the file", path.display()))?;
57 let header_len = usize::try_from(header_len_u64)
58 .with_context(|| format!("safetensors header of {} is too large", path.display()))?;
59
60 let mut bytes = vec![0u8; header_len];
61 file.read_exact(&mut bytes)
62 .with_context(|| format!("reading safetensors header of {}", path.display()))?;
63
64 debug_assert_eq!(header_end, 8 + header_len as u64);
65 let json: serde_json::Value = serde_json::from_slice(&bytes)
66 .with_context(|| format!("parsing safetensors header of {}", path.display()))?;
67 let obj = json
68 .as_object()
69 .with_context(|| format!("safetensors header of {} is not an object", path.display()))?;
70
71 let data_len = file_len - header_end;
72 let mut header = Header::new();
73 let mut occupied: Vec<(u64, u64, String)> = Vec::new();
74
75 for (name, value) in obj {
76 if name == "__metadata__" {
77 continue;
78 }
79 let info = parse_tensor_entry(name, value, data_len)
80 .with_context(|| format!("tensor `{name}` in {}", path.display()))?;
81 occupied.push((info.offset_start, info.offset_end, name.clone()));
82 header.insert(
83 name.clone(),
84 TensorInfo {
85 shape: info.shape,
86 dtype: info.dtype,
87 },
88 );
89 }
90
91 occupied.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
92 for window in occupied.windows(2) {
93 let (a_start, a_end, a_name) = &window[0];
94 let (b_start, _b_end, b_name) = &window[1];
95 if *b_start < *a_end {
96 bail!(
97 "overlapping data_offsets in {}: `{}` [{a_start},{a_end}) overlaps `{b_name}` starting at {b_start}",
98 path.display(),
99 a_name
100 );
101 }
102 }
103
104 Ok(header)
105}
106
107struct ParsedTensor {
108 shape: Vec<usize>,
109 dtype: String,
110 offset_start: u64,
111 offset_end: u64,
112}
113
114fn parse_tensor_entry(
115 name: &str,
116 value: &serde_json::Value,
117 data_len: u64,
118) -> Result<ParsedTensor> {
119 let obj = value
120 .as_object()
121 .with_context(|| format!("entry `{name}` is not an object"))?;
122
123 let shape_value = obj
124 .get("shape")
125 .with_context(|| format!("tensor `{name}` is missing shape"))?;
126 let shape_arr = shape_value
127 .as_array()
128 .with_context(|| format!("tensor `{name}` shape must be an array"))?;
129 let mut shape = Vec::with_capacity(shape_arr.len());
130 for (i, dim) in shape_arr.iter().enumerate() {
131 let n = dim.as_u64().with_context(|| {
132 format!("tensor `{name}` shape[{i}] must be a non-negative integer")
133 })?;
134 let n = usize::try_from(n)
135 .with_context(|| format!("tensor `{name}` shape[{i}] = {n} is too large"))?;
136 shape.push(n);
137 }
138
139 let dtype = obj
140 .get("dtype")
141 .and_then(|d| d.as_str())
142 .with_context(|| format!("tensor `{name}` is missing a string dtype"))?
143 .to_string();
144 let element_size = dtype_nbytes(&dtype)
145 .with_context(|| format!("tensor `{name}` has unsupported or unknown dtype `{dtype}`"))?;
146
147 let offsets_value = obj
148 .get("data_offsets")
149 .with_context(|| format!("tensor `{name}` is missing data_offsets"))?;
150 let offsets = offsets_value
151 .as_array()
152 .with_context(|| format!("tensor `{name}` data_offsets must be an array"))?;
153 if offsets.len() != 2 {
154 bail!("tensor `{name}` data_offsets must have exactly two entries");
155 }
156 let offset_start = offsets[0].as_u64().with_context(|| {
157 format!("tensor `{name}` data_offsets[0] must be a non-negative integer")
158 })?;
159 let offset_end = offsets[1].as_u64().with_context(|| {
160 format!("tensor `{name}` data_offsets[1] must be a non-negative integer")
161 })?;
162 if offset_end < offset_start {
163 bail!("tensor `{name}` data_offsets end {offset_end} is before start {offset_start}");
164 }
165 if offset_end > data_len {
166 bail!(
167 "tensor `{name}` data_offsets end {offset_end} exceeds data region length {data_len}"
168 );
169 }
170
171 let declared = offset_end - offset_start;
172 let expected = tensor_nbytes(&shape, element_size)
173 .with_context(|| format!("tensor `{name}` byte size overflows"))?;
174 if declared != expected {
175 bail!(
176 "tensor `{name}` data_offsets span {declared} bytes but shape {:?} dtype {dtype} requires {expected}",
177 shape
178 );
179 }
180
181 Ok(ParsedTensor {
182 shape,
183 dtype,
184 offset_start,
185 offset_end,
186 })
187}
188
189fn dtype_nbytes(dtype: &str) -> Result<u64> {
190 let n = match dtype {
191 "BOOL" | "U8" | "I8" | "F8_E4M3" | "F8_E5M2" => 1u64,
192 "I16" | "U16" | "F16" | "BF16" => 2,
193 "I32" | "U32" | "F32" => 4,
194 "I64" | "U64" | "F64" => 8,
195 _ => bail!("unsupported safetensors dtype `{dtype}`"),
196 };
197 Ok(n)
198}
199
200fn tensor_nbytes(shape: &[usize], element_size: u64) -> Result<u64> {
201 let mut n = element_size;
202 for &dim in shape {
203 let dim = u64::try_from(dim).context("shape dimension does not fit u64")?;
204 n = n
205 .checked_mul(dim)
206 .context("shape × dtype byte size overflows u64")?;
207 }
208 Ok(n)
209}
210
211#[derive(Debug, Default, Serialize)]
212pub struct VerifyReport {
213 pub root: String,
215 pub matched: usize,
217 pub missing_certain: Vec<String>,
220 pub missing_conditional: Vec<String>,
223 pub unclaimed: Vec<String>,
225 pub skipped_other_root: usize,
227 pub checkpoint_tensors: usize,
228}
229
230pub fn verify(structure: &mut Structure, header: &Header, root: &str) -> VerifyReport {
237 let mut report = VerifyReport {
238 checkpoint_tensors: header.len(),
239 root: root.to_string(),
240 ..Default::default()
241 };
242 let mut claimed: Vec<bool> = vec![false; header.len()];
243 let names: Vec<&String> = header.keys().collect();
244
245 for index in 0..structure.params.len() {
246 if structure.params[index].root != root {
247 report.skipped_other_root += 1;
248 continue;
249 }
250 let key = structure.params[index].key.clone();
251
252 if key.is_template() {
253 let hits: Vec<usize> = names
254 .iter()
255 .enumerate()
256 .filter(|(_, name)| key.matches(name))
257 .map(|(i, _)| i)
258 .collect();
259 if hits.is_empty() {
260 structure.params[index].checkpoint = CheckpointMatch::Missing;
261 record_missing(
262 &mut report,
263 &structure.params[index].certainty,
264 key.to_string(),
265 );
266 } else {
267 for hit in &hits {
268 claimed[*hit] = true;
269 }
270 structure.params[index].checkpoint = CheckpointMatch::FoundMany {
271 count: hits.len(),
272 sample: names[hits[0]].to_string(),
273 };
274 report.matched += 1;
275 }
276 continue;
277 }
278
279 let exact = key.to_string();
280 match names.iter().position(|n| **n == exact) {
281 Some(hit) => {
282 claimed[hit] = true;
283 let info = &header[names[hit]];
284 structure.params[index].checkpoint = CheckpointMatch::Found {
285 name: exact,
286 shape: info.shape.clone(),
287 dtype: info.dtype.clone(),
288 };
289 report.matched += 1;
290 }
291 None => {
292 structure.params[index].checkpoint = CheckpointMatch::Missing;
293 record_missing(&mut report, &structure.params[index].certainty, exact);
294 }
295 }
296 }
297
298 for (index, claimed) in claimed.iter().enumerate() {
299 if !claimed {
300 report.unclaimed.push(names[index].to_string());
301 }
302 }
303 report.missing_certain.sort();
304 report.missing_conditional.sort();
305 report.unclaimed.sort();
306 report
307}
308
309fn record_missing(report: &mut VerifyReport, certainty: &Certainty, key: String) {
311 match certainty {
312 Certainty::Certain => report.missing_certain.push(key),
313 _ => report.missing_conditional.push(key),
314 }
315}