1use std::io::Write;
26
27use kime_tensor::{Blob, DType};
28use serde_json::{Map, Value, json};
29
30use crate::error::{Error, Result};
31use crate::safetensors::MAX_HEADER;
32use crate::tensors::{Entry, Tensors, byte_len, check_disjoint};
33
34pub const MAGIC: [u8; 8] = *b"KIME\x01\0\0\0";
36pub const VERSION: u32 = 1;
38pub const ALIGN: usize = 4096;
40const FIXED: usize = 72;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct FileEntry {
45 pub name: String,
47 pub start: usize,
49 pub end: usize,
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub struct Index {
56 pub json: Map<String, Value>,
58 pub family: String,
60 pub id: String,
62 pub hash: String,
64 pub tensors: Vec<Entry>,
66 pub files: Vec<FileEntry>,
68 pub data_start: usize,
70}
71
72fn u64_at(b: &[u8], at: usize) -> u64 {
73 u64::from_le_bytes(b[at..at + 8].try_into().expect("8 bytes"))
74}
75
76#[must_use]
78pub fn is_kime(bytes: &[u8]) -> bool {
79 bytes.starts_with(&MAGIC)
80}
81
82pub fn parse(bytes: &[u8]) -> Result<Index> {
93 let bad = |msg: String| Error::Format(format!(".kime: {msg}"));
94 if bytes.len() < ALIGN {
95 return Err(bad(format!("{} bytes is shorter than the header block", bytes.len())));
96 }
97 if !is_kime(bytes) {
98 return Err(bad("wrong magic".into()));
99 }
100 let version = u32::from_le_bytes(bytes[8..12].try_into().expect("4 bytes"));
101 if version != VERSION {
102 return Err(bad(format!("format version {version}, this build reads {VERSION}")));
103 }
104 if bytes[12..16].iter().chain(&bytes[FIXED..ALIGN]).any(|&b| b != 0) {
105 return Err(bad("reserved header bytes are not zero".into()));
106 }
107 let (json_len, data_start, data_len) =
108 (u64_at(bytes, 16), u64_at(bytes, 24), u64_at(bytes, 32));
109 if json_len > MAX_HEADER as u64 {
110 return Err(bad(format!("index of {json_len} bytes is over the limit")));
111 }
112 let json_end = ALIGN + json_len as usize;
113 if !data_start.is_multiple_of(ALIGN as u64)
114 || data_start < json_end as u64
115 || data_start.checked_add(data_len) != Some(bytes.len() as u64)
116 {
117 return Err(bad(format!(
118 "data section {data_start}+{data_len} does not follow the index and end the {} byte file",
119 bytes.len()
120 )));
121 }
122 let data_start = data_start as usize;
123 let data_len = data_len as usize;
124 let json_bytes = &bytes[ALIGN..json_end];
125 if blake3::hash(json_bytes).as_bytes() != &bytes[40..72] {
126 return Err(bad("index does not match its hash".into()));
127 }
128 let Ok(Value::Object(json)) = serde_json::from_slice::<Value>(json_bytes) else {
129 return Err(bad("index is not a JSON object".into()));
130 };
131 let str_field = |k: &str| {
132 json.get(k)
133 .and_then(Value::as_str)
134 .map(str::to_string)
135 .ok_or_else(|| bad(format!("index has no {k:?}")))
136 };
137 if str_field("format")? != "kime/1" {
138 return Err(bad("index format is not kime/1".into()));
139 }
140 let hash = str_field("hash")?;
141 if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
142 return Err(bad("hash is not 64 hex digits".into()));
143 }
144 let range = |item: &Map<String, Value>, what: &str| -> Result<(usize, usize)> {
145 let num = |k: &str| {
146 item.get(k)
147 .and_then(Value::as_u64)
148 .and_then(|n| usize::try_from(n).ok())
149 .ok_or_else(|| bad(format!("{what}: {k:?} is not a non negative integer")))
150 };
151 let (off, len) = (num("offset")?, num("len")?);
152 match off.checked_add(len) {
153 Some(end) if end <= data_len => Ok((data_start + off, data_start + end)),
154 _ => Err(bad(format!("{what}: {off}+{len} falls outside {data_len} data bytes"))),
155 }
156 };
157 let list = |k: &str| {
158 json.get(k).and_then(Value::as_array).ok_or_else(|| bad(format!("index has no {k:?} list")))
159 };
160 let mut tensors = Vec::new();
161 for t in list("tensors")? {
162 let t = t.as_object().ok_or_else(|| bad("a tensor entry is not an object".into()))?;
163 let name = t
164 .get("name")
165 .and_then(Value::as_str)
166 .ok_or_else(|| bad("a tensor has no name".into()))?
167 .to_string();
168 let what = format!("tensor {name:?}");
169 let dtype = t
170 .get("dtype")
171 .and_then(Value::as_str)
172 .and_then(DType::from_name)
173 .ok_or_else(|| bad(format!("{what}: missing or unknown dtype")))?;
174 let shape = t
175 .get("shape")
176 .and_then(Value::as_array)
177 .and_then(|a| {
178 a.iter().map(|d| usize::try_from(d.as_u64()?).ok()).collect::<Option<Vec<_>>>()
179 })
180 .ok_or_else(|| bad(format!("{what}: shape must be a list of non negative integers")))?;
181 let (start, end) = range(t, &what)?;
182 if !(start - data_start).is_multiple_of(ALIGN) {
183 return Err(bad(format!("{what}: offset is not aligned to {ALIGN}")));
184 }
185 let want =
186 byte_len(dtype, &shape).ok_or_else(|| bad(format!("{what}: shape overflows")))?;
187 if end - start != want {
188 return Err(bad(format!(
189 "{what}: holds {} bytes but {dtype} {shape:?} needs {want}",
190 end - start
191 )));
192 }
193 tensors.push(Entry { name, dtype, shape, start, end });
194 }
195 let mut files = Vec::new();
196 for f in list("files")? {
197 let f = f.as_object().ok_or_else(|| bad("a file entry is not an object".into()))?;
198 let name = f
199 .get("name")
200 .and_then(Value::as_str)
201 .filter(|n| safe_name(n))
202 .ok_or_else(|| bad("a file has no name or an unsafe one".into()))?
203 .to_string();
204 let (start, end) = range(f, &format!("file {name:?}"))?;
205 files.push(FileEntry { name, start, end });
206 }
207 let mut ranges: Vec<_> = tensors
208 .iter()
209 .map(|e| (e.start, e.end, e.name.as_str()))
210 .chain(files.iter().map(|f| (f.start, f.end, f.name.as_str())))
211 .collect();
212 check_disjoint(&mut ranges).map_err(|e| bad(e.to_string()))?;
213 Ok(Index {
214 family: str_field("family")?,
215 id: str_field("id")?,
216 hash,
217 tensors,
218 files,
219 data_start,
220 json,
221 })
222}
223
224fn safe_name(name: &str) -> bool {
226 !name.is_empty()
227 && !name.starts_with('/')
228 && !name.contains('\\')
229 && name.split('/').all(|part| !part.is_empty() && part != "." && part != "..")
230}
231
232pub fn verify(bytes: &[u8], index: &Index) -> Result<()> {
239 let got = blake3::Hasher::new().update_rayon(&bytes[index.data_start..]).finalize().to_hex();
240 if got.as_str() == index.hash {
241 Ok(())
242 } else {
243 Err(Error::Format(format!(".kime: data hash is {got}, the index says {}", index.hash)))
244 }
245}
246
247#[derive(Debug)]
249pub struct Contents<'a> {
250 pub family: &'a str,
252 pub id: &'a str,
254 pub tensors: &'a Tensors,
256 pub files: Vec<(&'a str, &'a [u8])>,
258 pub extra: Map<String, Value>,
260}
261
262pub fn write(c: &Contents<'_>, out: &mut impl Write) -> Result<String> {
272 let io = |e: std::io::Error| Error::Io("<output>".into(), e);
273 for (name, _) in &c.files {
274 if !safe_name(name) {
275 return Err(Error::Format(format!("refusing to pack file name {name:?}")));
276 }
277 }
278 let pieces: Vec<&[u8]> = (0..c.tensors.entries().len())
280 .map(|i| c.tensors.view(i).bytes)
281 .chain(c.files.iter().map(|(_, b)| *b))
282 .collect();
283 let mut offsets = Vec::with_capacity(pieces.len());
284 let mut at = 0usize;
285 for p in &pieces {
286 offsets.push(at);
287 at = (at + p.len()).next_multiple_of(ALIGN);
288 }
289 let data_len = at;
290 let zeros = [0u8; ALIGN];
291 let mut hasher = blake3::Hasher::new();
292 let mut written = 0;
293 for (p, &off) in pieces.iter().zip(&offsets) {
294 hasher.update(&zeros[..off - written]);
295 hasher.update(p);
296 written = off + p.len();
297 }
298 hasher.update(&zeros[..data_len - written]);
299 let hash = hasher.finalize().to_hex().to_string();
300
301 let n = c.tensors.entries().len();
302 let tensors: Vec<Value> = c
303 .tensors
304 .entries()
305 .iter()
306 .zip(&offsets)
307 .map(|(e, &off)| {
308 json!({"name": e.name, "dtype": e.dtype.name(), "shape": e.shape, "offset": off, "len": e.end - e.start})
309 })
310 .collect();
311 let files: Vec<Value> = c
312 .files
313 .iter()
314 .zip(&offsets[n..])
315 .map(|((name, b), &off)| json!({"name": name, "offset": off, "len": b.len()}))
316 .collect();
317 let mut index = Map::new();
318 index.insert("format".into(), "kime/1".into());
319 index.insert("family".into(), c.family.into());
320 index.insert("id".into(), c.id.into());
321 index.insert("hash".into(), hash.clone().into());
322 for (k, v) in &c.extra {
323 index.insert(k.clone(), v.clone());
324 }
325 index.insert("tensors".into(), tensors.into());
326 index.insert("files".into(), files.into());
327 let json = serde_json::to_vec(&Value::Object(index)).expect("plain values");
328 let data_start = (ALIGN + json.len()).next_multiple_of(ALIGN);
329
330 let mut head = vec![0u8; ALIGN];
331 head[..8].copy_from_slice(&MAGIC);
332 head[8..12].copy_from_slice(&VERSION.to_le_bytes());
333 head[16..24].copy_from_slice(&(json.len() as u64).to_le_bytes());
334 head[24..32].copy_from_slice(&(data_start as u64).to_le_bytes());
335 head[32..40].copy_from_slice(&(data_len as u64).to_le_bytes());
336 head[40..72].copy_from_slice(blake3::hash(&json).as_bytes());
337 out.write_all(&head).map_err(io)?;
338 out.write_all(&json).map_err(io)?;
339 out.write_all(&zeros[..data_start - ALIGN - json.len()]).map_err(io)?;
340 let mut written = 0;
341 for (p, &off) in pieces.iter().zip(&offsets) {
342 out.write_all(&zeros[..off - written]).map_err(io)?;
343 out.write_all(p).map_err(io)?;
344 written = off + p.len();
345 }
346 out.write_all(&zeros[..data_len - written]).map_err(io)?;
347 Ok(hash)
348}
349
350pub fn load(blob: Blob) -> Result<(Index, Tensors)> {
356 let index = parse(&blob)?;
357 let tensors = Tensors::new(blob, index.tensors.clone())?;
358 Ok((index, tensors))
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use crate::safetensors;
365
366 fn sample() -> Tensors {
367 let header = r#"{"a":{"dtype":"F32","shape":[2],"data_offsets":[0,8]},"b":{"dtype":"F16","shape":[3],"data_offsets":[8,14]}}"#;
368 let mut v = (header.len() as u64).to_le_bytes().to_vec();
369 v.extend_from_slice(header.as_bytes());
370 v.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
371 safetensors::load(Blob::owned(v)).unwrap().0
372 }
373
374 fn pack(t: &Tensors) -> Vec<u8> {
375 let c = Contents {
376 family: "laya",
377 id: "test",
378 tensors: t,
379 files: vec![("encoder/config.json", b"{}")],
380 extra: Map::new(),
381 };
382 let mut out = Vec::new();
383 write(&c, &mut out).unwrap();
384 out
385 }
386
387 #[test]
388 fn round_trip() {
389 let t = sample();
390 let bytes = pack(&t);
391 assert_eq!(bytes.len() % ALIGN, 0);
392 let index = parse(&bytes).unwrap();
393 verify(&bytes, &index).unwrap();
394 assert_eq!(index.files[0].name, "encoder/config.json");
395 assert_eq!(&bytes[index.files[0].start..index.files[0].end], b"{}");
396 let (_, t2) = load(Blob::owned(bytes)).unwrap();
397 for (a, b) in t.entries().iter().zip(t2.entries()) {
398 assert_eq!((&a.name, a.dtype, &a.shape), (&b.name, b.dtype, &b.shape));
399 assert_eq!(t2.blob()[b.start..b.end], t.blob()[a.start..a.end]);
400 assert_eq!(b.start % ALIGN, 0);
401 }
402 }
403
404 #[test]
405 fn rejects_damage() {
406 let good = pack(&sample());
407 let mut bad = good.clone();
408 let n = bad.len();
409 bad[n - 1] ^= 1;
410 let index = parse(&bad).unwrap();
411 assert!(verify(&bad, &index).unwrap_err().to_string().contains("data hash"));
412 let mut bad = good.clone();
413 bad[ALIGN + 3] ^= 1;
414 assert!(parse(&bad).unwrap_err().to_string().contains("does not match its hash"));
415 let mut bad = good.clone();
416 bad[100] = 1;
417 assert!(parse(&bad).unwrap_err().to_string().contains("reserved"));
418 let mut bad = good.clone();
419 bad[32..40].copy_from_slice(&u64::MAX.to_le_bytes());
420 assert!(parse(&bad).unwrap_err().to_string().contains("data section"));
421 assert!(parse(&good[..ALIGN - 1]).is_err());
422 assert!(
423 !safe_name("../x") && !safe_name("a//b") && !safe_name("/etc") && safe_name("a/b.json")
424 );
425 }
426}