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}