Skip to main content

c2pa_ml/
format.rs

1//! Format detection and the format-dispatching public API.
2
3use crate::asset_type::ModelType;
4use crate::error::Error;
5use crate::manifest::ManifestSource;
6use crate::{gguf, onnx, safetensors};
7
8/// A supported ML model container format.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Format {
11    /// GGUF (llama.cpp).
12    Gguf,
13    /// SafeTensors.
14    SafeTensors,
15    /// ONNX (`ModelProto`).
16    Onnx,
17}
18
19impl Format {
20    /// Detect the container format from the leading bytes.
21    ///
22    /// GGUF and SafeTensors are recognized structurally; ONNX has no magic
23    /// number and is matched last as a best-effort protobuf shape check (see
24    /// [`onnx::is_onnx`]), so it can yield a false positive on unrelated
25    /// protobuf. When the format is known in advance, prefer
26    /// [`embed_manifest_as`] over auto-detection.
27    pub fn detect(data: &[u8]) -> Option<Format> {
28        if gguf::is_gguf(data) {
29            Some(Format::Gguf)
30        } else if safetensors::is_safetensors(data) {
31            Some(Format::SafeTensors)
32        } else if onnx::is_onnx(data) {
33            Some(Format::Onnx)
34        } else {
35            None
36        }
37    }
38
39    /// A short human-readable name.
40    pub fn name(self) -> &'static str {
41        match self {
42            Format::Gguf => "GGUF",
43            Format::SafeTensors => "SafeTensors",
44            Format::Onnx => "ONNX",
45        }
46    }
47
48    /// The most specific C2PA [`ModelType`] for this container. GGUF and
49    /// SafeTensors are framework-agnostic containers, so they map to the generic
50    /// `c2pa.types.model`; ONNX maps to `c2pa.types.model.onnx`.
51    pub fn model_type(self) -> ModelType {
52        match self {
53            Format::Gguf | Format::SafeTensors => ModelType::Generic,
54            Format::Onnx => ModelType::Onnx,
55        }
56    }
57}
58
59/// A report on a model's C2PA embedding.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct Report {
62    /// The detected container format.
63    pub format: Format,
64    /// The model carries an embedded Manifest Store.
65    pub has_embedded_manifest: bool,
66    /// The model carries a remote manifest URI.
67    pub has_remote_uri: bool,
68}
69
70impl Report {
71    /// A model is compliant when it references at least one manifest.
72    pub fn is_compliant(&self) -> bool {
73        self.has_embedded_manifest || self.has_remote_uri
74    }
75}
76
77/// Embed a C2PA manifest into a model, auto-detecting the container format.
78///
79/// Returns [`Error::UnknownFormat`] when the format cannot be detected; call
80/// [`embed_manifest_as`] with an explicit [`Format`] in that case.
81pub fn embed_manifest(data: &[u8], source: &ManifestSource) -> Result<Vec<u8>, Error> {
82    embed_manifest_as(data, detect(data)?, source)
83}
84
85/// Embed a C2PA manifest into a model of a known format.
86pub fn embed_manifest_as(
87    data: &[u8],
88    format: Format,
89    source: &ManifestSource,
90) -> Result<Vec<u8>, Error> {
91    match format {
92        Format::Gguf => gguf::embed(data, source),
93        Format::SafeTensors => safetensors::embed(data, source),
94        Format::Onnx => onnx::embed(data, source),
95    }
96}
97
98/// Read the embedded C2PA Manifest Store from a model, auto-detecting the format.
99pub fn read_manifest(data: &[u8]) -> Result<Vec<u8>, Error> {
100    match detect(data)? {
101        Format::Gguf => gguf::read_store(data),
102        Format::SafeTensors => safetensors::read_store(data),
103        Format::Onnx => onnx::read_store(data),
104    }
105}
106
107/// Read the remote manifest URI from a model, if present, auto-detecting the
108/// format.
109pub fn read_manifest_uri(data: &[u8]) -> Result<Option<String>, Error> {
110    match detect(data)? {
111        Format::Gguf => gguf::read_uri(data),
112        Format::SafeTensors => safetensors::read_uri(data),
113        Format::Onnx => onnx::read_uri(data),
114    }
115}
116
117/// Remove any C2PA metadata from a model, auto-detecting the format.
118pub fn remove_manifest(data: &[u8]) -> Result<Vec<u8>, Error> {
119    match detect(data)? {
120        Format::Gguf => gguf::remove(data),
121        Format::SafeTensors => safetensors::remove(data),
122        Format::Onnx => onnx::remove(data),
123    }
124}
125
126/// Report on a model's C2PA embedding, auto-detecting the format.
127pub fn verify(data: &[u8]) -> Result<Report, Error> {
128    let format = detect(data)?;
129    let has_embedded_manifest = match read_manifest(data) {
130        Ok(_) => true,
131        Err(Error::NotFound) => false,
132        Err(e) => return Err(e),
133    };
134    let has_remote_uri = read_manifest_uri(data)?.is_some();
135    Ok(Report {
136        format,
137        has_embedded_manifest,
138        has_remote_uri,
139    })
140}
141
142fn detect(data: &[u8]) -> Result<Format, Error> {
143    Format::detect(data).ok_or(Error::UnknownFormat)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::gguf::tests::sample_gguf;
150    use crate::onnx::tests::sample_onnx;
151    use crate::safetensors::tests::sample_safetensors;
152
153    #[test]
154    fn detects_each_format() {
155        assert_eq!(Format::detect(&sample_gguf()), Some(Format::Gguf));
156        assert_eq!(
157            Format::detect(&sample_safetensors(None)),
158            Some(Format::SafeTensors)
159        );
160        assert_eq!(Format::detect(&sample_onnx()), Some(Format::Onnx));
161        assert_eq!(Format::detect(b"random bytes here!!"), None);
162    }
163
164    #[test]
165    fn model_types() {
166        assert_eq!(Format::Onnx.model_type().as_str(), "c2pa.types.model.onnx");
167        assert_eq!(Format::Gguf.model_type().as_str(), "c2pa.types.model");
168    }
169
170    #[test]
171    fn dispatch_round_trip_all_formats() {
172        for data in [sample_gguf(), sample_safetensors(None), sample_onnx()] {
173            let out = embed_manifest(&data, &ManifestSource::embedded(vec![1, 2, 3])).unwrap();
174            assert_eq!(read_manifest(&out).unwrap(), vec![1, 2, 3]);
175            let report = verify(&out).unwrap();
176            assert!(report.is_compliant());
177            assert!(report.has_embedded_manifest);
178            let cleaned = remove_manifest(&out).unwrap();
179            assert!(matches!(read_manifest(&cleaned), Err(Error::NotFound)));
180        }
181    }
182
183    #[test]
184    fn unknown_format_errors() {
185        assert!(matches!(
186            embed_manifest(b"nope", &ManifestSource::embedded(vec![1])),
187            Err(Error::UnknownFormat)
188        ));
189    }
190}