1use std::path::Path;
5
6use serde::Serialize;
7use serde_json::Value;
8
9use crate::{c2pa, exif, png};
10
11#[derive(Debug, Clone, Serialize)]
12pub struct Report {
13 pub file: String,
14 pub path: String,
15 pub bytes: u64,
16 pub mime_type: Option<String>,
17 #[serde(serialize_with = "as_object")]
21 pub camera: Vec<(String, Value)>,
22 #[serde(serialize_with = "as_object")]
26 pub gps: Vec<(String, Value)>,
27 #[serde(serialize_with = "as_object")]
28 pub credit: Vec<(String, Value)>,
29 pub png_text: Vec<png::TextChunk>,
30 pub generator_keywords: Vec<String>,
31 pub c2pa: Option<Value>,
32 pub c2pa_status: c2pa::Status,
33 pub c2pa_error: String,
34 pub ai: Option<String>,
36 pub ai_source: Option<String>,
38 pub has_any_credit: bool,
39}
40
41fn as_object<S: serde::Serializer>(pairs: &[(String, Value)], s: S) -> Result<S::Ok, S::Error> {
42 use serde::ser::SerializeMap;
43 let mut map = s.serialize_map(Some(pairs.len()))?;
44 for (k, v) in pairs {
45 map.serialize_entry(k, v)?;
46 }
47 map.end()
48}
49
50pub fn inspect(path: &Path, tool: Option<&str>) -> Result<Report, String> {
51 let meta = exif::read_metadata(path)?;
52 let mut report = c2pa::read_report(path, tool);
53 c2pa::refine_with_metadata(&mut report, &meta);
54
55 let gps: Vec<(String, Value)> = exif::GPS_FIELDS
58 .iter()
59 .filter_map(|key| meta.get(*key).map(|v| ((*key).to_string(), v.clone())))
60 .collect();
61 let credit = exif::ordered_group(&meta, &exif::CREDIT_FIELDS);
62 let camera = exif::ordered_group(&meta, &exif::CAMERA_FIELDS);
63
64 let png_text = png::read_text(path);
65 let mut generator_keywords: Vec<String> = png_text
66 .iter()
67 .filter(|c| c.is_generator())
68 .map(|c| c.keyword.clone())
69 .collect();
70 generator_keywords.sort();
71 generator_keywords.dedup();
72
73 let mut ai: Option<String> = None;
78 let mut ai_source: Option<String> = None;
79 if let Some(source) = c2pa::digital_source(report.manifest.as_ref()) {
80 ai = Some(source.to_string());
81 ai_source = Some("c2pa".into());
82 }
83 if ai.is_none() && !generator_keywords.is_empty() {
84 ai = Some("generative".into());
85 ai_source = Some("png-text-chunk".into());
86 }
87
88 let absolute = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
89 Ok(Report {
90 file: path
91 .file_name()
92 .and_then(|s| s.to_str())
93 .unwrap_or_default()
94 .to_string(),
95 path: absolute.display().to_string(),
96 bytes: std::fs::metadata(path).map(|m| m.len()).unwrap_or(0),
97 mime_type: meta.get("File:MIMEType").map(exif::render),
98 camera,
99 gps,
100 credit: credit.clone(),
101 png_text,
102 generator_keywords,
103 c2pa: report.manifest,
104 c2pa_status: report.status,
105 c2pa_error: report.error,
106 ai,
107 ai_source,
108 has_any_credit: !credit.is_empty(),
109 })
110}
111
112#[derive(Debug, Clone, Serialize)]
114pub struct C2paSummary {
115 pub manifest: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub validation_state: Option<String>,
118 pub integrity: String,
119 pub signer_trusted: String,
120 pub signer: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub title: Option<String>,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub validation_status: Option<Value>,
125 pub means: String,
126}
127
128pub fn c2pa_summary(report: &Report) -> C2paSummary {
129 if report.c2pa_status != c2pa::Status::Present {
130 let means = match report.c2pa_status {
131 c2pa::Status::Absent => {
132 "No content credential in this file. That is not evidence about how the image was \
133 made: most files have never had one."
134 .to_string()
135 }
136 c2pa::Status::DetectedUnverified => {
137 "A credential is in this file but c2patool is not installed, so it could not be \
138 validated. Install c2patool to read it."
139 .to_string()
140 }
141 c2pa::Status::Unavailable => {
142 "c2patool is not installed, so no credential could be read.".to_string()
143 }
144 _ => {
145 if report.c2pa_error.is_empty() {
146 "the validator failed".to_string()
147 } else {
148 report.c2pa_error.clone()
149 }
150 }
151 };
152 return C2paSummary {
153 manifest: report.c2pa_status.as_str().to_string(),
154 validation_state: None,
155 integrity: "unknown".into(),
156 signer_trusted: "unknown".into(),
157 signer: None,
158 title: None,
159 validation_status: None,
160 means,
161 };
162 }
163 let manifest = report.c2pa.as_ref();
164 let state = c2pa::validation_state(manifest);
165 let altered = c2pa::asset_altered(manifest);
166 let untrusted = c2pa::identity_untrusted(manifest);
167 let means = if altered {
168 "The pixels are not the ones that were signed: this image was altered after signing."
169 .to_string()
170 } else {
171 let trust = if untrusted {
172 "The signing certificate is not on the validator trust list, so this says nothing \
173 about who signed it."
174 } else {
175 "The signer is on the validator trust list."
176 };
177 format!("The asset binding holds, so the image matches its credential. {trust}")
178 };
179 C2paSummary {
180 manifest: "present".into(),
181 integrity: if altered {
182 "altered".into()
183 } else if state == "Valid" {
184 "valid".into()
185 } else {
186 "unknown".into()
187 },
188 signer_trusted: if untrusted {
189 "no".into()
190 } else if state == "Valid" {
191 "yes".into()
192 } else {
193 "unknown".into()
194 },
195 validation_state: Some(state),
196 signer: c2pa::signer(manifest),
197 title: c2pa::title(manifest),
198 validation_status: manifest.and_then(|m| m.get("validation_status")).cloned(),
199 means,
200 }
201}