Skip to main content

ik_llama_cpp_2/
gguf.rs

1//! Safe wrapper around `gguf_context` for reading GGUF file metadata.
2//!
3//! Provides metadata-only access to GGUF files without loading tensor data.
4//! Useful for inspecting model architecture parameters before loading a model.
5//!
6//! Mirrors `llama-cpp-2`'s `gguf` module, adapted for ik_llama.cpp's C API:
7//! ik's `gguf_*` functions index key-value pairs and tensors with `int`
8//! (`i32`), whereas upstream llama.cpp migrated these to `int64_t`.
9
10use std::ffi::{CStr, CString, NulError};
11use std::path::{Path, PathBuf};
12use std::ptr::NonNull;
13
14/// Errors returned when opening a GGUF file.
15#[derive(Debug, thiserror::Error)]
16pub enum GgufError {
17    /// The path contained an interior NUL byte and could not be converted to a
18    /// C string.
19    #[error("gguf path contained an interior NUL byte")]
20    Nul(#[from] NulError),
21    /// The path was not valid UTF-8 and could not be converted to a C string.
22    #[error("gguf path was not valid UTF-8")]
23    InvalidPath,
24    /// `gguf_init_from_file` returned null (missing file or not a valid GGUF).
25    #[error("failed to open or parse GGUF file: {0}")]
26    Init(PathBuf),
27}
28
29/// A safe wrapper around `gguf_context`.
30///
31/// Opens a GGUF file and parses only the metadata header; tensor weights are
32/// never loaded into memory (`no_alloc = true`).
33///
34/// # Aborts
35///
36/// The typed value getters (`val_u32`, `val_str`, `arr_str`, …) and index-based
37/// accessors call ik functions guarded by `GGML_ASSERT`: passing an
38/// out-of-range index (`>= n_kv()` / `>= n_tensors()`) or reading a value with
39/// the wrong type aborts the **process**. Validate first via [`Self::n_kv`],
40/// [`Self::find_key`], and [`Self::kv_type`] before calling a typed getter.
41#[derive(Debug)]
42pub struct GgufContext {
43    ctx: NonNull<ik_llama_cpp_sys::gguf_context>,
44}
45
46impl GgufContext {
47    /// Open a GGUF file and parse its metadata header.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`GgufError`] if the path is not valid UTF-8, contains a NUL
52    /// byte, or the file does not exist / is not a valid GGUF file.
53    pub fn from_file(path: &Path) -> Result<Self, GgufError> {
54        let path_str = path.to_str().ok_or(GgufError::InvalidPath)?;
55        let c_path = CString::new(path_str)?;
56        let params = ik_llama_cpp_sys::gguf_init_params {
57            no_alloc: true,
58            ctx: std::ptr::null_mut(),
59        };
60        // SAFETY: `c_path` is a valid NUL-terminated C string that outlives the
61        // call, and `params` is a valid, fully-initialized struct.
62        let ptr = unsafe { ik_llama_cpp_sys::gguf_init_from_file(c_path.as_ptr(), params) };
63        let ctx = NonNull::new(ptr).ok_or_else(|| GgufError::Init(path.to_path_buf()))?;
64        Ok(Self { ctx })
65    }
66
67    /// The GGUF format version of the file.
68    #[must_use]
69    pub fn version(&self) -> i32 {
70        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
71        unsafe { ik_llama_cpp_sys::gguf_get_version(self.ctx.as_ptr()) }
72    }
73
74    /// The tensor-data alignment (in bytes) declared by the file.
75    #[must_use]
76    pub fn alignment(&self) -> usize {
77        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
78        unsafe { ik_llama_cpp_sys::gguf_get_alignment(self.ctx.as_ptr()) }
79    }
80
81    /// Total number of key-value pairs in the metadata.
82    #[must_use]
83    pub fn n_kv(&self) -> i32 {
84        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
85        unsafe { ik_llama_cpp_sys::gguf_get_n_kv(self.ctx.as_ptr()) }
86    }
87
88    /// Find the index of a key by name. Returns `-1` if not found.
89    #[must_use]
90    pub fn find_key(&self, key: &str) -> i32 {
91        let Ok(c_key) = CString::new(key) else {
92            return -1;
93        };
94        // SAFETY: `self.ctx` is valid; `c_key` is a valid NUL-terminated string
95        // that outlives the call.
96        unsafe { ik_llama_cpp_sys::gguf_find_key(self.ctx.as_ptr(), c_key.as_ptr()) }
97    }
98
99    /// Return the key name at the given index, or `None` if the pointer is null
100    /// or not valid UTF-8.
101    #[must_use]
102    pub fn key_at(&self, idx: i32) -> Option<&str> {
103        // SAFETY: `self.ctx` is valid; the returned pointer (if non-null) points
104        // to a NUL-terminated string owned by the context.
105        let ptr = unsafe { ik_llama_cpp_sys::gguf_get_key(self.ctx.as_ptr(), idx) };
106        if ptr.is_null() {
107            return None;
108        }
109        // SAFETY: `ptr` is non-null and NUL-terminated; it lives as long as
110        // `self`, which bounds the returned `&str`.
111        unsafe { CStr::from_ptr(ptr).to_str().ok() }
112    }
113
114    /// Return the value type of the KV pair at `idx`.
115    #[must_use]
116    pub fn kv_type(&self, idx: i32) -> ik_llama_cpp_sys::gguf_type {
117        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
118        unsafe { ik_llama_cpp_sys::gguf_get_kv_type(self.ctx.as_ptr(), idx) }
119    }
120
121    /// Return the element type of the array KV pair at `idx`.
122    ///
123    /// Only meaningful when [`kv_type`](Self::kv_type) is `GGUF_TYPE_ARRAY`.
124    #[must_use]
125    pub fn arr_type(&self, idx: i32) -> ik_llama_cpp_sys::gguf_type {
126        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
127        unsafe { ik_llama_cpp_sys::gguf_get_arr_type(self.ctx.as_ptr(), idx) }
128    }
129
130    /// Read a `uint8` value. Aborts (inside ik_llama.cpp) if the stored type is
131    /// not `GGUF_TYPE_UINT8` — check [`kv_type`](Self::kv_type) first if unsure.
132    #[must_use]
133    pub fn val_u8(&self, idx: i32) -> u8 {
134        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
135        unsafe { ik_llama_cpp_sys::gguf_get_val_u8(self.ctx.as_ptr(), idx) }
136    }
137
138    /// Read an `int8` value.
139    #[must_use]
140    pub fn val_i8(&self, idx: i32) -> i8 {
141        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
142        unsafe { ik_llama_cpp_sys::gguf_get_val_i8(self.ctx.as_ptr(), idx) }
143    }
144
145    /// Read a `uint16` value.
146    #[must_use]
147    pub fn val_u16(&self, idx: i32) -> u16 {
148        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
149        unsafe { ik_llama_cpp_sys::gguf_get_val_u16(self.ctx.as_ptr(), idx) }
150    }
151
152    /// Read an `int16` value.
153    #[must_use]
154    pub fn val_i16(&self, idx: i32) -> i16 {
155        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
156        unsafe { ik_llama_cpp_sys::gguf_get_val_i16(self.ctx.as_ptr(), idx) }
157    }
158
159    /// Read a `uint32` value. Aborts (inside ik_llama.cpp) if the stored type is
160    /// not `GGUF_TYPE_UINT32` — check [`kv_type`](Self::kv_type) first if unsure.
161    #[must_use]
162    pub fn val_u32(&self, idx: i32) -> u32 {
163        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
164        unsafe { ik_llama_cpp_sys::gguf_get_val_u32(self.ctx.as_ptr(), idx) }
165    }
166
167    /// Read an `int32` value.
168    #[must_use]
169    pub fn val_i32(&self, idx: i32) -> i32 {
170        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
171        unsafe { ik_llama_cpp_sys::gguf_get_val_i32(self.ctx.as_ptr(), idx) }
172    }
173
174    /// Read a `float32` value.
175    #[must_use]
176    pub fn val_f32(&self, idx: i32) -> f32 {
177        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
178        unsafe { ik_llama_cpp_sys::gguf_get_val_f32(self.ctx.as_ptr(), idx) }
179    }
180
181    /// Read a `uint64` value.
182    #[must_use]
183    pub fn val_u64(&self, idx: i32) -> u64 {
184        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
185        unsafe { ik_llama_cpp_sys::gguf_get_val_u64(self.ctx.as_ptr(), idx) }
186    }
187
188    /// Read an `int64` value.
189    #[must_use]
190    pub fn val_i64(&self, idx: i32) -> i64 {
191        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
192        unsafe { ik_llama_cpp_sys::gguf_get_val_i64(self.ctx.as_ptr(), idx) }
193    }
194
195    /// Read a `float64` value.
196    #[must_use]
197    pub fn val_f64(&self, idx: i32) -> f64 {
198        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
199        unsafe { ik_llama_cpp_sys::gguf_get_val_f64(self.ctx.as_ptr(), idx) }
200    }
201
202    /// Read a `bool` value.
203    #[must_use]
204    pub fn val_bool(&self, idx: i32) -> bool {
205        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
206        unsafe { ik_llama_cpp_sys::gguf_get_val_bool(self.ctx.as_ptr(), idx) }
207    }
208
209    /// Read a string value. Returns `None` if the pointer is null or not valid
210    /// UTF-8.
211    #[must_use]
212    pub fn val_str(&self, idx: i32) -> Option<&str> {
213        // SAFETY: `self.ctx` is valid; the returned pointer (if non-null) points
214        // to a NUL-terminated string owned by the context.
215        let ptr = unsafe { ik_llama_cpp_sys::gguf_get_val_str(self.ctx.as_ptr(), idx) };
216        if ptr.is_null() {
217            return None;
218        }
219        // SAFETY: `ptr` is non-null and NUL-terminated; it lives as long as
220        // `self`, which bounds the returned `&str`.
221        unsafe { CStr::from_ptr(ptr).to_str().ok() }
222    }
223
224    /// Number of elements in the array KV pair at `idx`.
225    ///
226    /// Only meaningful when [`kv_type`](Self::kv_type) is `GGUF_TYPE_ARRAY`.
227    #[must_use]
228    pub fn arr_len(&self, idx: i32) -> i32 {
229        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
230        unsafe { ik_llama_cpp_sys::gguf_get_arr_n(self.ctx.as_ptr(), idx) }
231    }
232
233    /// Read the `i`-th string of a string-array KV pair.
234    ///
235    /// Returns `None` if the pointer is null or not valid UTF-8. Only valid
236    /// when [`arr_type`](Self::arr_type) is `GGUF_TYPE_STRING`.
237    #[must_use]
238    pub fn arr_str(&self, idx: i32, i: i32) -> Option<&str> {
239        // SAFETY: `self.ctx` is valid; the returned pointer (if non-null) points
240        // to a NUL-terminated string owned by the context.
241        let ptr = unsafe { ik_llama_cpp_sys::gguf_get_arr_str(self.ctx.as_ptr(), idx, i) };
242        if ptr.is_null() {
243            return None;
244        }
245        // SAFETY: `ptr` is non-null and NUL-terminated; it lives as long as
246        // `self`, which bounds the returned `&str`.
247        unsafe { CStr::from_ptr(ptr).to_str().ok() }
248    }
249
250    /// Total number of tensors described in the file.
251    #[must_use]
252    pub fn n_tensors(&self) -> i32 {
253        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
254        unsafe { ik_llama_cpp_sys::gguf_get_n_tensors(self.ctx.as_ptr()) }
255    }
256
257    /// Return the name of the tensor at index `i`, or `None` if the pointer is
258    /// null or not valid UTF-8.
259    #[must_use]
260    pub fn tensor_name(&self, i: i32) -> Option<&str> {
261        // SAFETY: `self.ctx` is valid; the returned pointer (if non-null) points
262        // to a NUL-terminated string owned by the context.
263        let ptr = unsafe { ik_llama_cpp_sys::gguf_get_tensor_name(self.ctx.as_ptr(), i) };
264        if ptr.is_null() {
265            return None;
266        }
267        // SAFETY: `ptr` is non-null and NUL-terminated; it lives as long as
268        // `self`, which bounds the returned `&str`.
269        unsafe { CStr::from_ptr(ptr).to_str().ok() }
270    }
271
272    /// Return the [`ggml_type`](ik_llama_cpp_sys::ggml_type) of the tensor at
273    /// index `i`.
274    #[must_use]
275    pub fn tensor_type(&self, i: i32) -> ik_llama_cpp_sys::ggml_type {
276        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
277        unsafe { ik_llama_cpp_sys::gguf_get_tensor_type(self.ctx.as_ptr(), i) }
278    }
279
280    /// Return the data offset (in bytes, relative to the start of the tensor
281    /// data section) of the tensor at index `i`.
282    #[must_use]
283    pub fn tensor_offset(&self, i: i32) -> usize {
284        // SAFETY: `self.ctx` is a valid, non-null context for its lifetime.
285        unsafe { ik_llama_cpp_sys::gguf_get_tensor_offset(self.ctx.as_ptr(), i) }
286    }
287}
288
289/// Return the human-readable name of a [`gguf_type`](ik_llama_cpp_sys::gguf_type)
290/// (e.g. `"u32"`, `"str"`, `"arr"`), or `None` if the pointer is null or not
291/// valid UTF-8.
292#[must_use]
293pub fn type_name(kind: ik_llama_cpp_sys::gguf_type) -> Option<&'static str> {
294    // SAFETY: `gguf_type_name` returns a pointer to a static NUL-terminated
295    // string (or null) for any input; it borrows nothing from a context.
296    let ptr = unsafe { ik_llama_cpp_sys::gguf_type_name(kind) };
297    if ptr.is_null() {
298        return None;
299    }
300    // SAFETY: `ptr` is non-null and points to a `'static` NUL-terminated string.
301    unsafe { CStr::from_ptr(ptr).to_str().ok() }
302}
303
304impl Drop for GgufContext {
305    fn drop(&mut self) {
306        // SAFETY: `self.ctx` was returned by `gguf_init_from_file` and has not
307        // been freed; it is freed exactly once here.
308        unsafe { ik_llama_cpp_sys::gguf_free(self.ctx.as_ptr()) }
309    }
310}
311
312#[cfg(all(test, feature = "_smoke"))]
313mod tests {
314    use super::*;
315
316    /// Reads `general.architecture` from the GGUF pointed to by `IK_TEST_MODEL`
317    /// and asserts it is `qwen35`. Skips (does not fail) when the env var is
318    /// unset so the suite stays green without a model on disk.
319    #[test]
320    fn reads_architecture() {
321        let Ok(model) = std::env::var("IK_TEST_MODEL") else {
322            eprintln!("IK_TEST_MODEL not set; skipping gguf smoke test");
323            return;
324        };
325        let ctx = GgufContext::from_file(Path::new(&model)).expect("open gguf");
326        assert!(ctx.n_kv() > 0, "expected at least one KV pair");
327
328        let idx = ctx.find_key("general.architecture");
329        assert!(idx >= 0, "general.architecture key missing");
330        assert_eq!(ctx.val_str(idx), Some("qwen35"));
331    }
332}