Skip to main content

mlx_rs/io/
mod.rs

1//! GGUF container loading, inspection, construction, and saving.
2
3use std::{
4    collections::HashMap,
5    ffi::{CStr, CString},
6    fmt,
7    marker::PhantomData,
8    path::Path,
9    rc::Rc,
10};
11
12use crate::{
13    error::Exception,
14    utils::{
15        guard::{Guard, MaybeUninitArray},
16        SUCCESS,
17    },
18    Array, Dtype, Stream,
19};
20
21const NOT_FOUND: i32 = 2;
22const WRONG_METADATA_KIND: i32 = 3;
23
24/// A value stored in the GGUF metadata namespace.
25///
26/// ```rust
27/// use mlx_rs::{io::{GgufMetadataKind, GgufMetadataValue}, Array};
28///
29/// let value = GgufMetadataValue::from(Array::from_int(7));
30/// assert_eq!(value.kind(), GgufMetadataKind::Array);
31/// ```
32#[derive(Debug, Clone)]
33pub enum GgufMetadataValue {
34    /// A scalar or one-dimensional metadata array.
35    Array(Array),
36    /// A single UTF-8 string.
37    String(String),
38    /// A list of UTF-8 strings.
39    Strings(Vec<String>),
40}
41
42/// The kind of a value in the GGUF metadata namespace.
43///
44/// ```rust
45/// use mlx_rs::io::GgufMetadataKind;
46///
47/// assert_ne!(GgufMetadataKind::String, GgufMetadataKind::Strings);
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum GgufMetadataKind {
51    /// An MLX array.
52    Array,
53    /// A single string.
54    String,
55    /// A list of strings.
56    Strings,
57}
58
59/// An error from GGUF validation or an upstream MLX operation.
60///
61/// Stable Rust-side validation failures have dedicated variants. Other native runtime failures
62/// remain opaque in [`GgufError::Exception`].
63#[derive(Debug, thiserror::Error)]
64#[non_exhaustive]
65pub enum GgufError {
66    /// The load path is not an existing local file.
67    #[error("path must point to a local file")]
68    NotFile,
69
70    /// The path cannot be represented by the C string ABI.
71    #[error("path is not valid UTF-8")]
72    InvalidPathUtf8,
73
74    /// The path does not end in `.gguf`.
75    #[error("path must have a .gguf extension")]
76    UnsupportedExtension,
77
78    /// A path, key, or value contains an interior null byte.
79    #[error("GGUF text contains an interior null byte")]
80    InteriorNul,
81
82    /// Text returned by the native API is not valid UTF-8.
83    #[error("GGUF text is not valid UTF-8")]
84    InvalidUtf8,
85
86    /// The array namespace already contains this key.
87    #[error("array key {key:?} already exists")]
88    ArrayKeyAlreadyExists {
89        /// The duplicate array key.
90        key: String,
91    },
92
93    /// The metadata namespace already contains this key.
94    #[error("metadata key {key:?} already exists")]
95    MetadataKeyAlreadyExists {
96        /// The duplicate metadata key.
97        key: String,
98    },
99
100    /// A typed getter was used for a different metadata kind.
101    #[error("metadata {key:?} has kind {actual:?}, expected {expected:?}")]
102    WrongMetadataKind {
103        /// The metadata key.
104        key: String,
105        /// The kind requested by the getter.
106        expected: GgufMetadataKind,
107        /// The kind stored in the container.
108        actual: GgufMetadataKind,
109    },
110
111    /// The tensor dtype is not supported by the MLX 0.32.2 GGUF writer.
112    #[error("tensor dtype {dtype:?} cannot be written as GGUF")]
113    UnsupportedTensorDtype {
114        /// The rejected dtype.
115        dtype: Dtype,
116    },
117
118    /// The metadata-array dtype is not supported by the MLX 0.32.2 GGUF writer.
119    #[error("metadata array dtype {dtype:?} cannot be written as GGUF")]
120    UnsupportedMetadataArrayDtype {
121        /// The rejected dtype.
122        dtype: Dtype,
123    },
124
125    /// Metadata arrays may only be scalar or one-dimensional.
126    #[error("metadata arrays must be scalar or one-dimensional, found rank {rank}")]
127    InvalidMetadataArrayRank {
128        /// The rejected array rank.
129        rank: usize,
130    },
131
132    /// Metadata arrays must contain at least one element.
133    #[error("metadata arrays cannot be empty")]
134    EmptyMetadataArray,
135
136    /// An opaque error reported by MLX.
137    #[error(transparent)]
138    Exception(#[from] Exception),
139}
140
141/// A live two-namespace GGUF container.
142///
143/// Array keys can be enumerated, while the current C ABI does not expose metadata-key
144/// enumeration. The handle is intentionally non-cloneable and thread-affine.
145///
146/// ```rust
147/// use mlx_rs::io::GgufFile;
148///
149/// let file = GgufFile::new()?;
150/// assert!(file.array_keys()?.is_empty());
151/// # Ok::<(), mlx_rs::io::GgufError>(())
152/// ```
153pub struct GgufFile {
154    handle: GgufHandle,
155    thread_affinity: PhantomData<Rc<()>>,
156}
157
158impl fmt::Debug for GgufFile {
159    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160        formatter.debug_struct("GgufFile").finish_non_exhaustive()
161    }
162}
163
164impl GgufMetadataValue {
165    /// Returns the value's metadata kind.
166    pub fn kind(&self) -> GgufMetadataKind {
167        match self {
168            Self::Array(_) => GgufMetadataKind::Array,
169            Self::String(_) => GgufMetadataKind::String,
170            Self::Strings(_) => GgufMetadataKind::Strings,
171        }
172    }
173}
174
175impl From<Array> for GgufMetadataValue {
176    fn from(value: Array) -> Self {
177        Self::Array(value)
178    }
179}
180
181impl From<&Array> for GgufMetadataValue {
182    fn from(value: &Array) -> Self {
183        Self::Array(value.clone())
184    }
185}
186
187impl From<String> for GgufMetadataValue {
188    fn from(value: String) -> Self {
189        Self::String(value)
190    }
191}
192
193impl From<&str> for GgufMetadataValue {
194    fn from(value: &str) -> Self {
195        Self::String(value.to_owned())
196    }
197}
198
199impl From<Vec<String>> for GgufMetadataValue {
200    fn from(value: Vec<String>) -> Self {
201        Self::Strings(value)
202    }
203}
204
205impl GgufFile {
206    /// Creates an empty GGUF container.
207    pub fn new() -> Result<Self, GgufError> {
208        install_error_handler();
209        let handle = GgufHandle::new()?;
210        Ok(Self {
211            handle,
212            thread_affinity: PhantomData,
213        })
214    }
215
216    /// Loads a GGUF file on the thread-local stream, falling back to CPU.
217    ///
218    /// ```no_run
219    /// use mlx_rs::io::GgufFile;
220    ///
221    /// let file = GgufFile::load("model.gguf")?;
222    /// println!("{} tensors", file.array_keys()?.len());
223    /// # Ok::<(), mlx_rs::io::GgufError>(())
224    /// ```
225    pub fn load(path: impl AsRef<Path>) -> Result<Self, GgufError> {
226        let path = path.as_ref();
227        if !path.is_file() {
228            return Err(GgufError::NotFile);
229        }
230        let path = path_c_string(path)?;
231        let mut file = Self::new()?;
232        let stream = Stream::thread_local_or_cpu();
233        let status =
234            unsafe { mlx_sys::mlx_load_gguf(&mut file.handle.raw, path.as_ptr(), stream.as_ptr()) };
235        status_result(status, "loading a GGUF file")?;
236        Ok(file)
237    }
238
239    /// Saves the container to a `.gguf` path.
240    ///
241    /// Saving evaluates contained arrays and may allocate row-major materializations. It uses the
242    /// GGUF writer directly and does not consult the ambient Rust stream.
243    ///
244    /// ```no_run
245    /// use mlx_rs::{io::GgufFile, Array};
246    ///
247    /// let mut file = GgufFile::new()?;
248    /// file.insert_array("weight", &Array::from_f32(1.0))?;
249    /// file.save("model.gguf")?;
250    /// # Ok::<(), mlx_rs::io::GgufError>(())
251    /// ```
252    pub fn save(&self, path: impl AsRef<Path>) -> Result<(), GgufError> {
253        for array in self.arrays()?.into_values() {
254            validate_tensor_dtype(array.dtype())?;
255        }
256        let path = path_c_string(path.as_ref())?;
257        let status = unsafe { mlx_sys::mlx_save_gguf(path.as_ptr(), self.handle.raw) };
258        status_result(status, "saving a GGUF file")
259    }
260
261    /// Returns array keys in deterministic lexical order.
262    pub fn array_keys(&self) -> Result<Vec<String>, GgufError> {
263        let mut keys = VectorStringGuard::new()?;
264        let status = unsafe { mlx_sys::mlx_io_gguf_get_keys(&mut keys.raw, self.handle.raw) };
265        status_result(status, "enumerating GGUF array keys")?;
266        let mut result = keys.to_vec()?;
267        result.sort();
268        Ok(result)
269    }
270
271    /// Returns a new independently owned MLX array handle for `key`.
272    pub fn get_array(&self, key: &str) -> Result<Option<Array>, GgufError> {
273        let key = text_c_string(key)?;
274        let mut output = MaybeUninitArray::new();
275        let status = unsafe {
276            mlx_sys::mlx_io_gguf_get_array(output.as_mut_raw_ptr(), self.handle.raw, key.as_ptr())
277        };
278        match status {
279            SUCCESS => {
280                output.set_init_success(true);
281                Ok(Some(output.try_into_guarded()?))
282            }
283            NOT_FOUND => Ok(None),
284            _ => Err(status_exception(status, "getting a GGUF array")),
285        }
286    }
287
288    /// Copies the array namespace into independently owned MLX handles.
289    pub fn arrays(&self) -> Result<HashMap<String, Array>, GgufError> {
290        self.array_keys()?
291            .into_iter()
292            .map(|key| {
293                self.get_array(&key)?
294                    .map(|array| (key, array))
295                    .ok_or_else(|| status_exception(NOT_FOUND, "reading an enumerated GGUF array"))
296            })
297            .collect()
298    }
299
300    /// Inserts an array, rejecting duplicate keys and unsupported writer dtypes.
301    pub fn insert_array(&mut self, key: impl AsRef<str>, value: &Array) -> Result<(), GgufError> {
302        let key_text = key.as_ref();
303        if self.get_array(key_text)?.is_some() {
304            return Err(GgufError::ArrayKeyAlreadyExists {
305                key: key_text.to_owned(),
306            });
307        }
308        validate_tensor_dtype(value.dtype())?;
309        let key = text_c_string(key_text)?;
310        let status = unsafe {
311            mlx_sys::mlx_io_gguf_set_array(self.handle.raw, key.as_ptr(), value.as_ptr())
312        };
313        status_result(status, "inserting a GGUF array")
314    }
315
316    /// Returns the kind of a metadata value, or `None` when the key is absent.
317    pub fn metadata_kind(&self, key: &str) -> Result<Option<GgufMetadataKind>, GgufError> {
318        let key = text_c_string(key)?;
319        let probes = [
320            (
321                GgufMetadataKind::Array,
322                mlx_sys::mlx_io_gguf_has_metadata_array
323                    as unsafe extern "C" fn(
324                        *mut bool,
325                        mlx_sys::mlx_io_gguf,
326                        *const std::ffi::c_char,
327                    ) -> i32,
328            ),
329            (
330                GgufMetadataKind::String,
331                mlx_sys::mlx_io_gguf_has_metadata_string,
332            ),
333            (
334                GgufMetadataKind::Strings,
335                mlx_sys::mlx_io_gguf_has_metadata_vector_string,
336            ),
337        ];
338        for (kind, probe) in probes {
339            let mut present = false;
340            let status = unsafe { probe(&mut present, self.handle.raw, key.as_ptr()) };
341            match status {
342                SUCCESS if present => return Ok(Some(kind)),
343                SUCCESS => {}
344                NOT_FOUND => return Ok(None),
345                _ => return Err(status_exception(status, "checking GGUF metadata kind")),
346            }
347        }
348        Err(status_exception(
349            WRONG_METADATA_KIND,
350            "identifying GGUF metadata kind",
351        ))
352    }
353
354    /// Returns a metadata value, or `None` when the key is absent.
355    pub fn get_metadata(&self, key: &str) -> Result<Option<GgufMetadataValue>, GgufError> {
356        match self.metadata_kind(key)? {
357            Some(GgufMetadataKind::Array) => Ok(self.get_metadata_array(key)?.map(Into::into)),
358            Some(GgufMetadataKind::String) => Ok(self.get_metadata_string(key)?.map(Into::into)),
359            Some(GgufMetadataKind::Strings) => Ok(self.get_metadata_strings(key)?.map(Into::into)),
360            None => Ok(None),
361        }
362    }
363
364    /// Returns array metadata, or `None` when the key is absent.
365    pub fn get_metadata_array(&self, key: &str) -> Result<Option<Array>, GgufError> {
366        let c_key = text_c_string(key)?;
367        let mut output = MaybeUninitArray::new();
368        let status = unsafe {
369            mlx_sys::mlx_io_gguf_get_metadata_array(
370                output.as_mut_raw_ptr(),
371                self.handle.raw,
372                c_key.as_ptr(),
373            )
374        };
375        match status {
376            SUCCESS => {
377                output.set_init_success(true);
378                Ok(Some(output.try_into_guarded()?))
379            }
380            NOT_FOUND => Ok(None),
381            WRONG_METADATA_KIND => Err(self.wrong_kind(key, GgufMetadataKind::Array)?),
382            _ => Err(status_exception(status, "getting GGUF array metadata")),
383        }
384    }
385
386    /// Returns string metadata, or `None` when the key is absent.
387    pub fn get_metadata_string(&self, key: &str) -> Result<Option<String>, GgufError> {
388        let c_key = text_c_string(key)?;
389        let mut output = StringGuard::new();
390        let status = unsafe {
391            mlx_sys::mlx_io_gguf_get_metadata_string(
392                &mut output.raw,
393                self.handle.raw,
394                c_key.as_ptr(),
395            )
396        };
397        match status {
398            SUCCESS => Ok(Some(output.to_string()?)),
399            NOT_FOUND => Ok(None),
400            WRONG_METADATA_KIND => Err(self.wrong_kind(key, GgufMetadataKind::String)?),
401            _ => Err(status_exception(status, "getting GGUF string metadata")),
402        }
403    }
404
405    /// Returns string-list metadata, or `None` when the key is absent.
406    pub fn get_metadata_strings(&self, key: &str) -> Result<Option<Vec<String>>, GgufError> {
407        let c_key = text_c_string(key)?;
408        let mut output = VectorStringGuard::new()?;
409        let status = unsafe {
410            mlx_sys::mlx_io_gguf_get_metadata_vector_string(
411                &mut output.raw,
412                self.handle.raw,
413                c_key.as_ptr(),
414            )
415        };
416        match status {
417            SUCCESS => Ok(Some(output.to_vec()?)),
418            NOT_FOUND => Ok(None),
419            WRONG_METADATA_KIND => Err(self.wrong_kind(key, GgufMetadataKind::Strings)?),
420            _ => Err(status_exception(
421                status,
422                "getting GGUF string-list metadata",
423            )),
424        }
425    }
426
427    /// Inserts metadata, rejecting duplicate keys and unsupported metadata arrays.
428    pub fn insert_metadata<V>(&mut self, key: impl AsRef<str>, value: V) -> Result<(), GgufError>
429    where
430        V: Into<GgufMetadataValue>,
431    {
432        let value = value.into();
433        let key_text = key.as_ref();
434        if self.metadata_kind(key_text)?.is_some() {
435            return Err(GgufError::MetadataKeyAlreadyExists {
436                key: key_text.to_owned(),
437            });
438        }
439        if let GgufMetadataValue::Array(array) = &value {
440            validate_metadata_array(array)?;
441        }
442        let key = text_c_string(key_text)?;
443        match value {
444            GgufMetadataValue::Array(array) => {
445                let status = unsafe {
446                    mlx_sys::mlx_io_gguf_set_metadata_array(
447                        self.handle.raw,
448                        key.as_ptr(),
449                        array.as_ptr(),
450                    )
451                };
452                status_result(status, "inserting GGUF array metadata")
453            }
454            GgufMetadataValue::String(value) => {
455                let value = text_c_string(&value)?;
456                let status = unsafe {
457                    mlx_sys::mlx_io_gguf_set_metadata_string(
458                        self.handle.raw,
459                        key.as_ptr(),
460                        value.as_ptr(),
461                    )
462                };
463                status_result(status, "inserting GGUF string metadata")
464            }
465            GgufMetadataValue::Strings(values) => {
466                let strings = values
467                    .iter()
468                    .map(|value| text_c_string(value))
469                    .collect::<Result<Vec<_>, _>>()?;
470                let mut pointers = strings
471                    .iter()
472                    .map(|value| value.as_ptr())
473                    .collect::<Vec<_>>();
474                let vector = VectorStringGuard::from_data(&mut pointers)?;
475                let status = unsafe {
476                    mlx_sys::mlx_io_gguf_set_metadata_vector_string(
477                        self.handle.raw,
478                        key.as_ptr(),
479                        vector.raw,
480                    )
481                };
482                status_result(status, "inserting GGUF string-list metadata")
483            }
484        }
485    }
486
487    fn wrong_kind(&self, key: &str, expected: GgufMetadataKind) -> Result<GgufError, GgufError> {
488        let actual = self
489            .metadata_kind(key)?
490            .ok_or_else(|| status_exception(NOT_FOUND, "resolving a present GGUF metadata key"))?;
491        Ok(GgufError::WrongMetadataKind {
492            key: key.to_owned(),
493            expected,
494            actual,
495        })
496    }
497}
498
499struct GgufHandle {
500    raw: mlx_sys::mlx_io_gguf,
501}
502
503impl GgufHandle {
504    fn new() -> Result<Self, GgufError> {
505        let raw = unsafe { mlx_sys::mlx_io_gguf_new() };
506        if raw.ctx.is_null() {
507            Err(status_exception(SUCCESS + 1, "creating a GGUF container"))
508        } else {
509            Ok(Self { raw })
510        }
511    }
512}
513
514impl Drop for GgufHandle {
515    fn drop(&mut self) {
516        let _ = unsafe { mlx_sys::mlx_io_gguf_free(self.raw) };
517    }
518}
519
520struct StringGuard {
521    raw: mlx_sys::mlx_string,
522}
523
524impl StringGuard {
525    fn new() -> Self {
526        // mlx_string_new returns an empty handle whose ctx stays null until a
527        // successful getter fills it through mlx_string_set_.
528        let raw = unsafe { mlx_sys::mlx_string_new() };
529        Self { raw }
530    }
531
532    fn to_string(&self) -> Result<String, GgufError> {
533        let data = unsafe { mlx_sys::mlx_string_data(self.raw) };
534        if data.is_null() {
535            return Err(status_exception(SUCCESS + 1, "reading an MLX string"));
536        }
537        unsafe { CStr::from_ptr(data) }
538            .to_str()
539            .map(str::to_owned)
540            .map_err(|_| GgufError::InvalidUtf8)
541    }
542}
543
544impl Drop for StringGuard {
545    fn drop(&mut self) {
546        let _ = unsafe { mlx_sys::mlx_string_free(self.raw) };
547    }
548}
549
550struct VectorStringGuard {
551    raw: mlx_sys::mlx_vector_string,
552}
553
554impl VectorStringGuard {
555    fn new() -> Result<Self, GgufError> {
556        let raw = unsafe { mlx_sys::mlx_vector_string_new() };
557        Self::from_raw(raw, "creating an MLX string vector")
558    }
559
560    fn from_data(data: &mut [*const std::ffi::c_char]) -> Result<Self, GgufError> {
561        let raw = unsafe { mlx_sys::mlx_vector_string_new_data(data.as_mut_ptr(), data.len()) };
562        Self::from_raw(raw, "creating an MLX string vector from data")
563    }
564
565    fn from_raw(raw: mlx_sys::mlx_vector_string, operation: &str) -> Result<Self, GgufError> {
566        if raw.ctx.is_null() {
567            Err(status_exception(SUCCESS + 1, operation))
568        } else {
569            Ok(Self { raw })
570        }
571    }
572
573    fn to_vec(&self) -> Result<Vec<String>, GgufError> {
574        let len = unsafe { mlx_sys::mlx_vector_string_size(self.raw) };
575        (0..len)
576            .map(|index| {
577                let mut data = std::ptr::null_mut();
578                let status = unsafe { mlx_sys::mlx_vector_string_get(&mut data, self.raw, index) };
579                status_result(status, "reading an MLX string vector")?;
580                if data.is_null() {
581                    return Err(status_exception(
582                        SUCCESS + 1,
583                        "reading an MLX string vector",
584                    ));
585                }
586                unsafe { CStr::from_ptr(data) }
587                    .to_str()
588                    .map(str::to_owned)
589                    .map_err(|_| GgufError::InvalidUtf8)
590            })
591            .collect()
592    }
593}
594
595impl Drop for VectorStringGuard {
596    fn drop(&mut self) {
597        let _ = unsafe { mlx_sys::mlx_vector_string_free(self.raw) };
598    }
599}
600
601fn install_error_handler() {
602    crate::error::INIT_ERR_HANDLER.call_once(crate::error::setup_mlx_error_handler);
603}
604
605fn status_exception(status: i32, operation: &str) -> GgufError {
606    GgufError::Exception(crate::error::exception_from_status(status, operation))
607}
608
609fn status_result(status: i32, operation: &str) -> Result<(), GgufError> {
610    if status == SUCCESS {
611        Ok(())
612    } else {
613        Err(status_exception(status, operation))
614    }
615}
616
617fn text_c_string(value: &str) -> Result<CString, GgufError> {
618    CString::new(value).map_err(|_| GgufError::InteriorNul)
619}
620
621fn path_c_string(path: &Path) -> Result<CString, GgufError> {
622    if path.extension().and_then(|extension| extension.to_str()) != Some("gguf") {
623        return Err(GgufError::UnsupportedExtension);
624    }
625    let path = path.to_str().ok_or(GgufError::InvalidPathUtf8)?;
626    text_c_string(path)
627}
628
629fn validate_tensor_dtype(dtype: Dtype) -> Result<(), GgufError> {
630    if matches!(
631        dtype,
632        Dtype::Float32 | Dtype::Float16 | Dtype::Int8 | Dtype::Int16 | Dtype::Int32
633    ) {
634        Ok(())
635    } else {
636        Err(GgufError::UnsupportedTensorDtype { dtype })
637    }
638}
639
640fn validate_metadata_array(array: &Array) -> Result<(), GgufError> {
641    let rank = array.ndim();
642    if rank > 1 {
643        return Err(GgufError::InvalidMetadataArrayRank { rank });
644    }
645    if array.size() == 0 {
646        return Err(GgufError::EmptyMetadataArray);
647    }
648    let dtype = array.dtype();
649    if matches!(
650        dtype,
651        Dtype::Bool
652            | Dtype::Int8
653            | Dtype::Int16
654            | Dtype::Int32
655            | Dtype::Int64
656            | Dtype::Uint8
657            | Dtype::Uint16
658            | Dtype::Uint32
659            | Dtype::Uint64
660            | Dtype::Float32
661    ) {
662        Ok(())
663    } else {
664        Err(GgufError::UnsupportedMetadataArrayDtype { dtype })
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    #[test]
673    fn writer_prevalidation_rejects_unsupported_tensor_dtypes() {
674        for dtype in [
675            Dtype::Bool,
676            Dtype::Uint8,
677            Dtype::Uint16,
678            Dtype::Uint32,
679            Dtype::Uint64,
680            Dtype::Int64,
681            Dtype::Bfloat16,
682            Dtype::Complex64,
683        ] {
684            assert!(matches!(
685                validate_tensor_dtype(dtype),
686                Err(GgufError::UnsupportedTensorDtype { dtype: actual }) if actual == dtype
687            ));
688        }
689    }
690
691    #[test]
692    fn metadata_prevalidation_rejects_rank_empty_and_dtype() {
693        let rank = Array::from_slice(&[1_i32, 2, 3, 4], &[2, 2]);
694        assert!(matches!(
695            validate_metadata_array(&rank),
696            Err(GgufError::InvalidMetadataArrayRank { rank: 2 })
697        ));
698
699        let empty = Array::from_slice::<i32>(&[], &[0]);
700        assert!(matches!(
701            validate_metadata_array(&empty),
702            Err(GgufError::EmptyMetadataArray)
703        ));
704
705        for array in [
706            Array::from_slice(&[half::f16::from_f32(1.0)], &[1]),
707            Array::from_slice(&[half::bf16::from_f32(1.0)], &[1]),
708            Array::from_complex(crate::complex64::new(1.0, 0.0)),
709        ] {
710            assert!(matches!(
711                validate_metadata_array(&array),
712                Err(GgufError::UnsupportedMetadataArrayDtype { .. })
713            ));
714        }
715    }
716
717    #[test]
718    fn duplicate_keys_are_rejected_per_namespace() {
719        let mut file = GgufFile::new().unwrap();
720        let value = Array::from_int(1);
721        file.insert_array("same", &value).unwrap();
722        assert!(matches!(
723            file.insert_array("same", &value),
724            Err(GgufError::ArrayKeyAlreadyExists { key }) if key == "same"
725        ));
726
727        file.insert_metadata("same", "first").unwrap();
728        assert!(matches!(
729            file.insert_metadata("same", vec!["second".to_owned()]),
730            Err(GgufError::MetadataKeyAlreadyExists { key }) if key == "same"
731        ));
732    }
733
734    #[test]
735    fn paths_and_text_are_validated_before_ffi_entry() {
736        assert!(matches!(
737            GgufFile::load("definitely-absent.gguf"),
738            Err(GgufError::NotFile)
739        ));
740        let file = GgufFile::new().unwrap();
741        assert!(matches!(
742            file.save("wrong-extension.bin"),
743            Err(GgufError::UnsupportedExtension)
744        ));
745        let mut file = GgufFile::new().unwrap();
746        assert!(matches!(
747            file.insert_array("bad\0key", &Array::from_int(1)),
748            Err(GgufError::InteriorNul)
749        ));
750    }
751}