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}