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