1use std::io::{Read, Seek, SeekFrom};
2use std::path::Path;
3
4mod kv_cache;
5
6pub use kv_cache::{GgufKvCacheQuant, GgufKvCacheType};
7
8const MAX_GGUF_STRING_BYTES: u64 = 1_000_000;
9const MAX_GGUF_ARRAY_ELEMENTS: u64 = 1_000_000;
10const MAX_GGUF_ARRAY_DEPTH: u32 = 64;
11const MAX_GGUF_TENSOR_DIMS: u32 = 8;
12const MAX_GGUF_HEADER_KV_COUNT: usize = 1_000_000;
13const MAX_GGUF_TENSOR_COUNT: usize = 1_000_000;
14
15#[repr(u32)]
17#[derive(Debug, Clone, Copy, PartialEq)]
18enum GgufType {
19 Uint8 = 0,
20 Int8 = 1,
21 Uint16 = 2,
22 Int16 = 3,
23 Uint32 = 4,
24 Int32 = 5,
25 Float32 = 6,
26 Bool = 7,
27 String = 8,
28 Array = 9,
29 Uint64 = 10,
30 Int64 = 11,
31 Float64 = 12,
32}
33
34impl GgufType {
35 fn from_u32(v: u32) -> Option<Self> {
36 match v {
37 0 => Some(Self::Uint8),
38 1 => Some(Self::Int8),
39 2 => Some(Self::Uint16),
40 3 => Some(Self::Int16),
41 4 => Some(Self::Uint32),
42 5 => Some(Self::Int32),
43 6 => Some(Self::Float32),
44 7 => Some(Self::Bool),
45 8 => Some(Self::String),
46 9 => Some(Self::Array),
47 10 => Some(Self::Uint64),
48 11 => Some(Self::Int64),
49 12 => Some(Self::Float64),
50 _ => None,
51 }
52 }
53
54 fn fixed_size(self) -> Option<usize> {
55 match self {
56 Self::Uint8 | Self::Int8 | Self::Bool => Some(1),
57 Self::Uint16 | Self::Int16 => Some(2),
58 Self::Uint32 | Self::Int32 | Self::Float32 => Some(4),
59 Self::Uint64 | Self::Int64 | Self::Float64 => Some(8),
60 Self::String | Self::Array => None,
61 }
62 }
63}
64
65fn read_u32(f: &mut std::fs::File) -> std::io::Result<u32> {
66 let mut buf = [0u8; 4];
67 f.read_exact(&mut buf)?;
68 Ok(u32::from_le_bytes(buf))
69}
70
71fn read_u64(f: &mut std::fs::File) -> std::io::Result<u64> {
72 let mut buf = [0u8; 8];
73 f.read_exact(&mut buf)?;
74 Ok(u64::from_le_bytes(buf))
75}
76
77fn read_i32(f: &mut std::fs::File) -> std::io::Result<i32> {
78 let mut buf = [0u8; 4];
79 f.read_exact(&mut buf)?;
80 Ok(i32::from_le_bytes(buf))
81}
82
83fn read_i64(f: &mut std::fs::File) -> std::io::Result<i64> {
84 let mut buf = [0u8; 8];
85 f.read_exact(&mut buf)?;
86 Ok(i64::from_le_bytes(buf))
87}
88
89fn read_gguf_header_count(
90 f: &mut std::fs::File,
91 max: usize,
92 label: &str,
93) -> std::io::Result<usize> {
94 let value = read_i64(f)?;
95 let count = usize::try_from(value).map_err(|_| {
96 std::io::Error::new(std::io::ErrorKind::InvalidData, format!("negative {label}"))
97 })?;
98 if count > max {
99 return Err(std::io::Error::new(
100 std::io::ErrorKind::InvalidData,
101 format!("{label} too large"),
102 ));
103 }
104 Ok(count)
105}
106
107fn read_bounded_len(f: &mut std::fs::File, max: u64, label: &str) -> std::io::Result<usize> {
108 let len = read_u64(f)?;
109 if len > max {
110 return Err(std::io::Error::new(
111 std::io::ErrorKind::InvalidData,
112 format!("{label} too long"),
113 ));
114 }
115 usize::try_from(len).map_err(|_| {
116 std::io::Error::new(
117 std::io::ErrorKind::InvalidData,
118 format!("{label} too large"),
119 )
120 })
121}
122
123fn read_gguf_string(f: &mut std::fs::File) -> std::io::Result<String> {
124 let len = read_bounded_len(f, MAX_GGUF_STRING_BYTES, "string")?;
125 let mut buf = vec![0u8; len];
126 f.read_exact(&mut buf)?;
127 String::from_utf8(buf).map_err(|_| {
128 std::io::Error::new(
129 std::io::ErrorKind::InvalidData,
130 "invalid UTF-8 in GGUF string",
131 )
132 })
133}
134
135fn skip_gguf_value(f: &mut std::fs::File, typ: GgufType) -> std::io::Result<()> {
136 skip_gguf_value_with_depth(f, typ, 0)
137}
138
139fn skip_gguf_value_with_depth(
140 f: &mut std::fs::File,
141 typ: GgufType,
142 depth: u32,
143) -> std::io::Result<()> {
144 match typ {
145 GgufType::String => {
146 let _ = read_gguf_string(f)?;
147 }
148 GgufType::Array => {
149 if depth >= MAX_GGUF_ARRAY_DEPTH {
150 return Err(std::io::Error::new(
151 std::io::ErrorKind::InvalidData,
152 "GGUF nesting too deep",
153 ));
154 }
155 let elem_type = GgufType::from_u32(read_u32(f)?).ok_or_else(|| {
156 std::io::Error::new(std::io::ErrorKind::InvalidData, "bad array type")
157 })?;
158 let count = read_bounded_len(f, MAX_GGUF_ARRAY_ELEMENTS, "array")?;
159 for _ in 0..count {
160 skip_gguf_value_with_depth(f, elem_type, depth + 1)?;
161 }
162 }
163 other => {
164 let size = other.fixed_size().unwrap_or(0);
165 f.seek(SeekFrom::Current(size as i64))?;
166 }
167 }
168 Ok(())
169}
170
171fn read_gguf_value_as_u32(f: &mut std::fs::File, typ: GgufType) -> std::io::Result<Option<u32>> {
172 match typ {
173 GgufType::Uint32 => Ok(Some(read_u32(f)?)),
174 GgufType::Int32 => {
175 let value = read_i32(f)?;
176 let value = u32::try_from(value).map_err(|_| {
177 std::io::Error::new(
178 std::io::ErrorKind::InvalidData,
179 "negative Int32 where unsigned GGUF value was expected",
180 )
181 })?;
182 Ok(Some(value))
183 }
184 GgufType::Uint16 => {
185 let mut buf = [0u8; 2];
186 f.read_exact(&mut buf)?;
187 Ok(Some(u16::from_le_bytes(buf) as u32))
188 }
189 GgufType::Uint8 => {
190 let mut buf = [0u8; 1];
191 f.read_exact(&mut buf)?;
192 Ok(Some(buf[0] as u32))
193 }
194 _ => {
195 skip_gguf_value(f, typ)?;
196 Ok(None)
197 }
198 }
199}
200
201fn read_gguf_value_as_f32(f: &mut std::fs::File, typ: GgufType) -> std::io::Result<Option<f32>> {
202 match typ {
203 GgufType::Float32 => {
204 let mut buf = [0u8; 4];
205 f.read_exact(&mut buf)?;
206 Ok(Some(f32::from_le_bytes(buf)))
207 }
208 _ => {
209 skip_gguf_value(f, typ)?;
210 Ok(None)
211 }
212 }
213}
214
215fn read_gguf_value_as_string_opt(
216 f: &mut std::fs::File,
217 typ: GgufType,
218) -> std::io::Result<Option<String>> {
219 match typ {
220 GgufType::String => Ok(Some(read_gguf_string(f)?)),
221 _ => {
222 skip_gguf_value(f, typ)?;
223 Ok(None)
224 }
225 }
226}
227
228#[derive(Clone, Debug, Default)]
229pub struct GgufCompactMeta {
230 pub architecture: String,
231 pub parameter_size: Option<String>,
232 pub context_length: u32,
233 pub vocab_size: u32,
234 pub embedding_size: u32,
235 pub head_count: u32,
236 pub kv_head_count: u32,
237 pub layer_count: u32,
238 pub feed_forward_length: u32,
239 pub key_length: u32,
240 pub value_length: u32,
241 pub kv_lora_rank: u32,
242 pub tokenizer_model_name: String,
243 pub rope_scale: f32,
244 pub rope_freq_base: f32,
245 pub expert_count: u32,
246 pub expert_used_count: u32,
247 pub nextn_predict_layers: u32,
248}
249
250impl GgufCompactMeta {
251 pub fn effective_kv_head_count(&self) -> Option<u32> {
252 if self.kv_head_count > 0 {
253 Some(self.kv_head_count)
254 } else if self.head_count > 0 {
255 Some(self.head_count)
256 } else {
257 None
258 }
259 }
260}
261
262#[derive(Clone, Debug, Default, Eq, PartialEq)]
263pub struct GgufTensorByteProfile {
264 pub expert_count: u32,
265 pub expert_used_count: u32,
266 pub full_model_bytes: u64,
267 pub base_resident_bytes: u64,
268 pub expert_tensor_bytes: u64,
269 pub file_overhead_bytes: u64,
270}
271
272#[derive(Clone, Debug)]
273struct GgufTensorInfo {
274 name: String,
275 offset: u64,
276}
277
278struct GgufHeader {
281 file: std::fs::File,
282 n_tensors: usize,
283 n_kv: usize,
284}
285
286fn open_gguf_header(path: &Path) -> Option<GgufHeader> {
289 let mut f = std::fs::File::open(path).ok()?;
290
291 let mut magic = [0u8; 4];
292 f.read_exact(&mut magic).ok()?;
293 if &magic != b"GGUF" {
294 return None;
295 }
296 let version = read_u32(&mut f).ok()?;
297 if version < 2 {
298 return None;
299 }
300
301 let n_tensors = read_gguf_header_count(&mut f, MAX_GGUF_TENSOR_COUNT, "tensor count").ok()?;
302 let n_kv = read_gguf_header_count(&mut f, MAX_GGUF_HEADER_KV_COUNT, "KV count").ok()?;
303
304 Some(GgufHeader {
305 file: f,
306 n_tensors,
307 n_kv,
308 })
309}
310
311fn skip_all_kv_pairs(f: &mut std::fs::File, n_kv: usize) -> Option<()> {
314 for _ in 0..n_kv {
315 let _key = read_gguf_string(f).ok()?;
316 let vtype = GgufType::from_u32(read_u32(f).ok()?)?;
317 skip_gguf_value(f, vtype).ok()?;
318 }
319 Some(())
320}
321
322pub fn scan_gguf_compact_meta(path: &Path) -> Option<GgufCompactMeta> {
323 let GgufHeader {
324 file: mut f, n_kv, ..
325 } = open_gguf_header(path)?;
326
327 let mut meta = GgufCompactMeta::default();
328 for _ in 0..n_kv {
329 let key = read_gguf_string(&mut f).ok()?;
330 let vtype = GgufType::from_u32(read_u32(&mut f).ok()?)?;
331
332 if key == "general.architecture" {
333 meta.architecture = read_gguf_value_as_string_opt(&mut f, vtype).ok()??;
334 } else if key == "general.size_label" {
335 meta.parameter_size = read_gguf_value_as_string_opt(&mut f, vtype).ok()?;
336 } else if key == "tokenizer.ggml.model" {
337 meta.tokenizer_model_name = read_gguf_value_as_string_opt(&mut f, vtype).ok()??;
338 } else if key.ends_with(".context_length") {
339 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
340 meta.context_length = v;
341 }
342 } else if key.ends_with(".embedding_length") {
343 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
344 meta.embedding_size = v;
345 }
346 } else if key.ends_with(".head_count") && !key.ends_with("_kv") {
347 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
348 meta.head_count = v;
349 }
350 } else if key.ends_with(".attention.head_count_kv") {
351 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
352 meta.kv_head_count = v;
353 }
354 } else if key.ends_with(".block_count") {
355 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
356 meta.layer_count = v;
357 }
358 } else if key.ends_with(".feed_forward_length") {
359 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
360 meta.feed_forward_length = v;
361 }
362 } else if key.ends_with(".attention.key_length") {
363 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
364 meta.key_length = v;
365 }
366 } else if key.ends_with(".attention.value_length") {
367 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
368 meta.value_length = v;
369 }
370 } else if key.ends_with(".attention.kv_lora_rank") {
371 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
372 meta.kv_lora_rank = v;
373 }
374 } else if key.ends_with(".rope.scale") {
375 if let Ok(Some(v)) = read_gguf_value_as_f32(&mut f, vtype) {
376 meta.rope_scale = v;
377 }
378 } else if key.ends_with(".rope.freq_base") {
379 if let Ok(Some(v)) = read_gguf_value_as_f32(&mut f, vtype) {
380 meta.rope_freq_base = v;
381 }
382 } else if key.ends_with(".vocab_size") {
383 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
384 meta.vocab_size = v;
385 }
386 } else if key.ends_with(".expert_count") {
387 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
388 meta.expert_count = v;
389 }
390 } else if key.ends_with(".expert_used_count") {
391 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
392 meta.expert_used_count = v;
393 }
394 } else if key.ends_with(".nextn_predict_layers") {
395 if let Ok(Some(v)) = read_gguf_value_as_u32(&mut f, vtype) {
396 meta.nextn_predict_layers = v;
397 }
398 } else {
399 skip_gguf_value(&mut f, vtype).ok()?;
400 }
401 }
402
403 let derived_key_length = (meta.key_length == 0 && meta.head_count > 0)
404 .then(|| meta.embedding_size.checked_div(meta.head_count))
405 .flatten();
406 if let Some(key_length) = derived_key_length {
407 meta.key_length = key_length;
408 }
409 let derived_value_length = (meta.value_length == 0)
410 .then(|| {
411 meta.effective_kv_head_count()
412 .and_then(|effective_kv| meta.embedding_size.checked_div(effective_kv))
413 })
414 .flatten();
415 if let Some(value_length) = derived_value_length {
416 meta.value_length = value_length;
417 }
418
419 Some(meta)
420}
421
422fn align_offset(value: u64, alignment: u32) -> u64 {
423 let alignment = u64::from(alignment.max(1));
424 let remainder = value % alignment;
425 if remainder == 0 {
426 value
427 } else {
428 value + (alignment - remainder)
429 }
430}
431
432fn read_tensor_infos(
433 f: &mut std::fs::File,
434 n_tensors: usize,
435) -> std::io::Result<Vec<GgufTensorInfo>> {
436 let mut tensors = Vec::new();
437 tensors.try_reserve(n_tensors).map_err(|_| {
438 std::io::Error::new(
439 std::io::ErrorKind::InvalidData,
440 "GGUF tensor count requires too much memory",
441 )
442 })?;
443 for _ in 0..n_tensors {
444 let name = read_gguf_string(f)?;
445 let n_dims = read_u32(f)?;
446 if n_dims > MAX_GGUF_TENSOR_DIMS {
447 return Err(std::io::Error::new(
448 std::io::ErrorKind::InvalidData,
449 "too many GGUF tensor dimensions",
450 ));
451 }
452 for _ in 0..n_dims {
453 let _ = read_u64(f)?;
454 }
455 let _ = read_u32(f)?;
456 let offset = read_u64(f)?;
457 tensors.push(GgufTensorInfo { name, offset });
458 }
459 Ok(tensors)
460}
461
462pub fn scan_gguf_tensor_names_any(
465 path: &Path,
466 mut matches: impl FnMut(&str) -> bool,
467) -> Option<bool> {
468 let GgufHeader {
469 file: mut f,
470 n_tensors,
471 n_kv,
472 } = open_gguf_header(path)?;
473
474 skip_all_kv_pairs(&mut f, n_kv)?;
475
476 for _ in 0..n_tensors {
477 let name = read_gguf_string(&mut f).ok()?;
478 if matches(&name) {
479 return Some(true);
480 }
481 let n_dims = read_u32(&mut f).ok()?;
482 if n_dims > MAX_GGUF_TENSOR_DIMS {
483 return None;
484 }
485 for _ in 0..n_dims {
486 let _ = read_u64(&mut f).ok()?;
487 }
488 let _ggml_type = read_u32(&mut f).ok()?;
489 let _offset = read_u64(&mut f).ok()?;
490 }
491
492 Some(false)
493}
494
495fn is_expert_partitioned_tensor(name: &str) -> bool {
496 let lower = name.to_ascii_lowercase();
497 if lower.contains("shared_expert") || lower.contains("sharedexpert") || lower.contains("shexp")
498 {
499 return false;
500 }
501
502 lower.contains("ffn_gate_exps")
503 || lower.contains("ffn_up_exps")
504 || lower.contains("ffn_down_exps")
505 || lower.contains("exp_probs")
506 || lower.contains(".expert")
507 || lower.contains("_expert")
508}
509
510pub fn scan_gguf_tensor_byte_profile(path: &Path) -> Option<GgufTensorByteProfile> {
513 let GgufHeader {
514 file: mut f,
515 n_tensors,
516 n_kv,
517 } = open_gguf_header(path)?;
518 let file_len = f.metadata().ok()?.len();
519
520 let mut expert_count = 0u32;
521 let mut expert_used_count = 0u32;
522 let mut alignment = 32u32;
523
524 for _ in 0..n_kv {
525 let key = read_gguf_string(&mut f).ok()?;
526 let vtype = GgufType::from_u32(read_u32(&mut f).ok()?)?;
527
528 if key == "general.alignment" {
529 if let Ok(Some(value)) = read_gguf_value_as_u32(&mut f, vtype) {
530 alignment = value.max(1);
531 }
532 } else if key.ends_with(".expert_count") {
533 if let Ok(Some(value)) = read_gguf_value_as_u32(&mut f, vtype) {
534 expert_count = value;
535 }
536 } else if key.ends_with(".expert_used_count") {
537 if let Ok(Some(value)) = read_gguf_value_as_u32(&mut f, vtype) {
538 expert_used_count = value;
539 }
540 } else {
541 skip_gguf_value(&mut f, vtype).ok()?;
542 }
543 }
544
545 let mut tensors = read_tensor_infos(&mut f, n_tensors).ok()?;
546 if tensors.is_empty() {
547 return Some(GgufTensorByteProfile {
548 expert_count,
549 expert_used_count,
550 full_model_bytes: file_len,
551 base_resident_bytes: 0,
552 expert_tensor_bytes: 0,
553 file_overhead_bytes: file_len,
554 });
555 }
556
557 let tensor_info_end = f.stream_position().ok()?;
558 let data_start = align_offset(tensor_info_end, alignment);
559 if data_start > file_len {
560 return None;
561 }
562 let data_len = file_len - data_start;
563
564 tensors.sort_by_key(|tensor| tensor.offset);
565 if tensors.first()?.offset > data_len {
566 return None;
567 }
568
569 let mut base_resident_bytes = 0u64;
570 let mut expert_tensor_bytes = 0u64;
571 for (index, tensor) in tensors.iter().enumerate() {
572 let next_offset = tensors
573 .get(index + 1)
574 .map(|next| next.offset)
575 .unwrap_or(data_len);
576 if next_offset < tensor.offset || next_offset > data_len {
577 return None;
578 }
579 let tensor_bytes = next_offset - tensor.offset;
580 if is_expert_partitioned_tensor(&tensor.name) {
581 expert_tensor_bytes = expert_tensor_bytes.saturating_add(tensor_bytes);
582 } else {
583 base_resident_bytes = base_resident_bytes.saturating_add(tensor_bytes);
584 }
585 }
586
587 let file_overhead_bytes = file_len.saturating_sub(base_resident_bytes + expert_tensor_bytes);
588 Some(GgufTensorByteProfile {
589 expert_count,
590 expert_used_count,
591 full_model_bytes: file_len,
592 base_resident_bytes,
593 expert_tensor_bytes,
594 file_overhead_bytes,
595 })
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use std::io::Write;
602 use std::path::PathBuf;
603 use std::time::{SystemTime, UNIX_EPOCH};
604
605 fn temp_file_path(prefix: &str) -> PathBuf {
606 let unique = SystemTime::now()
607 .duration_since(UNIX_EPOCH)
608 .unwrap()
609 .as_nanos();
610 std::env::temp_dir().join(format!("{prefix}-{unique}.gguf"))
611 }
612
613 fn write_bytes(prefix: &str, bytes: &[u8]) -> PathBuf {
614 let path = temp_file_path(prefix);
615 let mut file = std::fs::File::create(&path).unwrap();
616 file.write_all(bytes).unwrap();
617 file.flush().unwrap();
618 path
619 }
620
621 fn push_array_header(bytes: &mut Vec<u8>, elem_type: GgufType, count: u64) {
622 bytes.extend_from_slice(&(elem_type as u32).to_le_bytes());
623 bytes.extend_from_slice(&count.to_le_bytes());
624 }
625
626 fn push_gguf_string(bytes: &mut Vec<u8>, value: &str) {
627 bytes.extend_from_slice(&(value.len() as u64).to_le_bytes());
628 bytes.extend_from_slice(value.as_bytes());
629 }
630
631 fn push_u32_kv(bytes: &mut Vec<u8>, key: &str, value: u32) {
632 push_gguf_string(bytes, key);
633 bytes.extend_from_slice(&(GgufType::Uint32 as u32).to_le_bytes());
634 bytes.extend_from_slice(&value.to_le_bytes());
635 }
636
637 fn push_tensor_info(bytes: &mut Vec<u8>, name: &str, offset: u64) {
638 push_gguf_string(bytes, name);
639 bytes.extend_from_slice(&1u32.to_le_bytes());
640 bytes.extend_from_slice(&16u64.to_le_bytes());
641 bytes.extend_from_slice(&(GgufType::Uint8 as u32).to_le_bytes());
642 bytes.extend_from_slice(&offset.to_le_bytes());
643 }
644
645 #[test]
646 fn skip_gguf_value_rejects_excessive_array_depth() {
647 let mut bytes = Vec::new();
648 for _ in 0..=MAX_GGUF_ARRAY_DEPTH {
649 push_array_header(&mut bytes, GgufType::Array, 1);
650 }
651 push_array_header(&mut bytes, GgufType::Uint8, 1);
652 bytes.push(0);
653
654 let path = write_bytes("model-artifact-gguf-depth", &bytes);
655 let mut file = std::fs::File::open(&path).unwrap();
656 let err = skip_gguf_value(&mut file, GgufType::Array).unwrap_err();
657 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
658 assert!(err.to_string().contains("nesting too deep"));
659 let _ = std::fs::remove_file(path);
660 }
661
662 #[test]
663 fn skip_gguf_value_rejects_excessive_array_count() {
664 let mut bytes = Vec::new();
665 push_array_header(&mut bytes, GgufType::Uint8, MAX_GGUF_ARRAY_ELEMENTS + 1);
666
667 let path = write_bytes("model-artifact-gguf-count", &bytes);
668 let mut file = std::fs::File::open(&path).unwrap();
669 let err = skip_gguf_value(&mut file, GgufType::Array).unwrap_err();
670 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
671 assert!(err.to_string().contains("array too long"));
672 let _ = std::fs::remove_file(path);
673 }
674
675 #[test]
676 fn scan_gguf_compact_meta_returns_none_on_malicious_nested_array() {
677 let mut bytes = Vec::new();
678 bytes.extend_from_slice(b"GGUF");
679 bytes.extend_from_slice(&2u32.to_le_bytes());
680 bytes.extend_from_slice(&0i64.to_le_bytes());
681 bytes.extend_from_slice(&1i64.to_le_bytes());
682 push_gguf_string(&mut bytes, "general.architecture");
683 bytes.extend_from_slice(&(GgufType::Array as u32).to_le_bytes());
684 for _ in 0..=MAX_GGUF_ARRAY_DEPTH {
685 push_array_header(&mut bytes, GgufType::Array, 1);
686 }
687 push_array_header(&mut bytes, GgufType::Uint8, 1);
688 bytes.push(0);
689
690 let path = write_bytes("model-artifact-gguf-malicious", &bytes);
691 assert!(scan_gguf_compact_meta(&path).is_none());
692 let _ = std::fs::remove_file(path);
693 }
694
695 #[test]
696 fn scan_gguf_compact_meta_derives_value_length_from_kv_heads_without_head_count() {
697 let mut bytes = Vec::new();
698 bytes.extend_from_slice(b"GGUF");
699 bytes.extend_from_slice(&2u32.to_le_bytes());
700 bytes.extend_from_slice(&0i64.to_le_bytes());
701 bytes.extend_from_slice(&2i64.to_le_bytes());
702 push_u32_kv(&mut bytes, "llama.embedding_length", 4096);
703 push_u32_kv(&mut bytes, "llama.attention.head_count_kv", 8);
704
705 let path = write_bytes("model-artifact-gguf-kv-heads", &bytes);
706 let meta = scan_gguf_compact_meta(&path).expect("should parse GGUF");
707 assert_eq!(meta.head_count, 0);
708 assert_eq!(meta.kv_head_count, 8);
709 assert_eq!(meta.key_length, 0);
710 assert_eq!(meta.value_length, 512);
711 let _ = std::fs::remove_file(path);
712 }
713
714 #[test]
715 fn scan_gguf_compact_meta_preserves_kv_head_count() {
716 let mut bytes = Vec::new();
717 bytes.extend_from_slice(b"GGUF");
718 bytes.extend_from_slice(&2u32.to_le_bytes());
719 bytes.extend_from_slice(&0i64.to_le_bytes());
720 bytes.extend_from_slice(&6i64.to_le_bytes());
721 push_u32_kv(&mut bytes, "llama.embedding_length", 4096);
722 push_u32_kv(&mut bytes, "llama.attention.head_count", 32);
723 push_u32_kv(&mut bytes, "llama.attention.head_count_kv", 8);
724 push_u32_kv(&mut bytes, "llama.block_count", 24);
725 push_u32_kv(&mut bytes, "llama.attention.key_length", 128);
726 push_u32_kv(&mut bytes, "llama.attention.value_length", 128);
727
728 let path = write_bytes("model-artifact-gguf-kv-head-count", &bytes);
729 let meta = scan_gguf_compact_meta(&path).expect("should parse GGUF");
730 assert_eq!(meta.head_count, 32);
731 assert_eq!(meta.kv_head_count, 8);
732 assert_eq!(meta.effective_kv_head_count(), Some(8));
733 assert_eq!(meta.k_cache_bytes_per_token_f16(), Some(49_152));
734 assert_eq!(meta.v_cache_bytes_per_token_f16(), Some(49_152));
735 let _ = std::fs::remove_file(path);
736 }
737
738 #[test]
739 fn scan_gguf_compact_meta_preserves_kv_lora_rank() {
740 let mut bytes = Vec::new();
741 bytes.extend_from_slice(b"GGUF");
742 bytes.extend_from_slice(&2u32.to_le_bytes());
743 bytes.extend_from_slice(&0i64.to_le_bytes());
744 bytes.extend_from_slice(&1i64.to_le_bytes());
745 push_u32_kv(&mut bytes, "glm-dsa.attention.kv_lora_rank", 512);
746
747 let path = write_bytes("model-artifact-gguf-kv-lora-rank", &bytes);
748 let meta = scan_gguf_compact_meta(&path).expect("should parse GGUF");
749 assert_eq!(meta.kv_lora_rank, 512);
750 let _ = std::fs::remove_file(path);
751 }
752
753 #[test]
754 fn scan_gguf_compact_meta_preserves_nextn_predict_layers() {
755 let mut bytes = Vec::new();
756 bytes.extend_from_slice(b"GGUF");
757 bytes.extend_from_slice(&2u32.to_le_bytes());
758 bytes.extend_from_slice(&0i64.to_le_bytes());
759 bytes.extend_from_slice(&2i64.to_le_bytes());
760 push_gguf_string(&mut bytes, "general.architecture");
761 bytes.extend_from_slice(&(GgufType::String as u32).to_le_bytes());
762 push_gguf_string(&mut bytes, "deepseek2");
763 push_u32_kv(&mut bytes, "deepseek2.nextn_predict_layers", 1);
764
765 let path = write_bytes("model-artifact-gguf-nextn", &bytes);
766 let meta = scan_gguf_compact_meta(&path).expect("should parse GGUF");
767 assert_eq!(meta.architecture, "deepseek2");
768 assert_eq!(meta.nextn_predict_layers, 1);
769 let _ = std::fs::remove_file(path);
770 }
771
772 #[test]
773 fn scan_gguf_tensor_names_any_detects_nextn_tensor_without_reading_data() {
774 let mut bytes = Vec::new();
775 bytes.extend_from_slice(b"GGUF");
776 bytes.extend_from_slice(&2u32.to_le_bytes());
777 bytes.extend_from_slice(&2i64.to_le_bytes());
778 bytes.extend_from_slice(&0i64.to_le_bytes());
779 push_tensor_info(&mut bytes, "blk.0.attn_q.weight", 0);
780 push_tensor_info(&mut bytes, "blk.23.nextn.eh_proj.weight", 64);
781
782 let path = write_bytes("model-artifact-gguf-nextn-tensor", &bytes);
783 let has_nextn = scan_gguf_tensor_names_any(&path, |name| name.contains(".nextn."))
784 .expect("tensor scan should parse");
785 assert!(has_nextn);
786 let _ = std::fs::remove_file(path);
787 }
788
789 #[test]
790 fn scan_gguf_compact_meta_rejects_negative_kv_count() {
791 let mut bytes = Vec::new();
792 bytes.extend_from_slice(b"GGUF");
793 bytes.extend_from_slice(&2u32.to_le_bytes());
794 bytes.extend_from_slice(&0i64.to_le_bytes());
795 bytes.extend_from_slice(&(-1i64).to_le_bytes());
796
797 let path = write_bytes("model-artifact-gguf-negative-kv", &bytes);
798 assert!(scan_gguf_compact_meta(&path).is_none());
799 let _ = std::fs::remove_file(path);
800 }
801
802 #[test]
803 fn scan_gguf_tensor_byte_profile_rejects_excessive_tensor_count() {
804 let mut bytes = Vec::new();
805 bytes.extend_from_slice(b"GGUF");
806 bytes.extend_from_slice(&2u32.to_le_bytes());
807 bytes.extend_from_slice(&((MAX_GGUF_TENSOR_COUNT as i64) + 1).to_le_bytes());
808 bytes.extend_from_slice(&0i64.to_le_bytes());
809
810 let path = write_bytes("model-artifact-gguf-too-many-tensors", &bytes);
811 assert!(scan_gguf_tensor_byte_profile(&path).is_none());
812 let _ = std::fs::remove_file(path);
813 }
814
815 #[test]
816 fn read_gguf_value_as_u32_rejects_negative_int32() {
817 let path = write_bytes("model-artifact-gguf-negative-int32", &(-1i32).to_le_bytes());
818 let mut file = std::fs::File::open(&path).unwrap();
819 let err = read_gguf_value_as_u32(&mut file, GgufType::Int32).unwrap_err();
820 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
821 assert!(
822 err.to_string()
823 .contains("negative Int32 where unsigned GGUF value was expected")
824 );
825 let _ = std::fs::remove_file(path);
826 }
827
828 #[test]
829 fn scan_gguf_tensor_byte_profile_splits_base_and_expert_bytes() {
830 let mut bytes = Vec::new();
831 bytes.extend_from_slice(b"GGUF");
832 bytes.extend_from_slice(&2u32.to_le_bytes());
833 bytes.extend_from_slice(&2i64.to_le_bytes());
834 bytes.extend_from_slice(&3i64.to_le_bytes());
835
836 push_u32_kv(&mut bytes, "general.alignment", 32);
837 push_u32_kv(&mut bytes, "llama.expert_count", 8);
838 push_u32_kv(&mut bytes, "llama.expert_used_count", 2);
839
840 push_tensor_info(&mut bytes, "blk.0.ffn_up_exps.weight", 0);
841 push_tensor_info(&mut bytes, "blk.0.attn_q.weight", 64);
842
843 let data_start = align_offset(bytes.len() as u64, 32) as usize;
844 bytes.resize(data_start, 0);
845 bytes.resize(data_start + 96, 0);
846
847 let path = write_bytes("model-artifact-gguf-tensors", &bytes);
848 let profile = scan_gguf_tensor_byte_profile(&path).unwrap();
849 assert_eq!(profile.expert_count, 8);
850 assert_eq!(profile.expert_used_count, 2);
851 assert_eq!(profile.expert_tensor_bytes, 64);
852 assert_eq!(profile.base_resident_bytes, 32);
853 assert_eq!(profile.full_model_bytes, bytes.len() as u64);
854 assert_eq!(
855 profile.full_model_bytes,
856 profile.base_resident_bytes + profile.expert_tensor_bytes + profile.file_overhead_bytes
857 );
858 let _ = std::fs::remove_file(path);
859 }
860}