1use alloc::{
7 collections::BTreeMap,
8 format,
9 string::{String, ToString},
10 vec::Vec,
11};
12use zencodec::exif::{ByteOrder, Exif};
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct Entry {
18 path: String,
19 kind: String,
20 value: String,
21}
22impl Entry {
23 pub fn path(&self) -> &str {
25 &self.path
26 }
27 pub fn kind(&self) -> &str {
29 &self.kind
30 }
31 pub fn value(&self) -> &str {
33 &self.value
34 }
35}
36#[derive(Clone, Debug, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum Finding {
40 InvalidExif,
42 ExifCoverageLimited,
45 MakerNote { path: String, bytes: usize },
47 Limit,
49 InvalidXmp(String),
51}
52#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct Report {
55 entries: Vec<Entry>,
56 findings: Vec<Finding>,
57}
58impl Report {
59 pub fn exif(bytes: &[u8]) -> Self {
61 let mut out = Self::default();
62 if bytes.len() > 16 * 1024 * 1024 {
63 out.findings.push(Finding::Limit);
64 return out;
65 }
66 let Some(exif) = Exif::parse(bytes) else {
67 out.findings.push(Finding::InvalidExif);
68 return out;
69 };
70 out.findings.push(Finding::ExifCoverageLimited);
71 let mut occurrences = BTreeMap::<String, usize>::new();
72 let mut budget = 0usize;
73 for e in exif.entries() {
74 budget = budget
75 .saturating_add(e.value.len().saturating_mul(24))
76 .saturating_add(256);
77 if budget > 16 * 1024 * 1024 {
78 out.findings.push(Finding::Limit);
79 break;
80 }
81 let key = format!("exif/{:?}/0x{:04x}", e.ifd, e.tag);
82 let index = occurrences.entry(key.clone()).or_default();
83 let path = format!("{key}[{index}]");
84 *index += 1;
85 if e.tag == 0x927c {
86 out.findings.push(Finding::MakerNote {
87 path: path.clone(),
88 bytes: e.value.len(),
89 });
90 }
91 out.entries.push(Entry {
92 path,
93 kind: format!("TIFF type={} count={}", e.kind, e.count),
94 value: display_value(e.kind, e.value, exif.byte_order()),
95 });
96 }
97 out.entries.sort_by(|a, b| a.path.cmp(&b.path));
98 out
99 }
100 #[cfg(feature = "xmp")]
103 pub fn xmp(xml: &str) -> Self {
104 match crate::xmp::Packet::parse(xml).and_then(|p| p.fields()) {
105 Ok(fields) => Self {
106 entries: fields
107 .into_iter()
108 .map(|f| Entry {
109 path: f.path,
110 kind: "XML".into(),
111 value: f.value,
112 })
113 .collect(),
114 findings: Vec::new(),
115 },
116 Err(e) => Self {
117 entries: Vec::new(),
118 findings: alloc::vec![Finding::InvalidXmp(e.to_string())],
119 },
120 }
121 }
122 pub fn entries(&self) -> &[Entry] {
124 &self.entries
125 }
126 pub fn findings(&self) -> &[Finding] {
128 &self.findings
129 }
130 pub fn diff<'a>(&'a self, after: &'a Self) -> Vec<Change<'a>> {
133 let before: BTreeMap<_, _> = self.entries.iter().map(|e| (e.path(), e)).collect();
134 let next: BTreeMap<_, _> = after.entries.iter().map(|e| (e.path(), e)).collect();
135 let mut changes = Vec::new();
136 for (path, e) in &before {
137 match next.get(path) {
138 None => changes.push(Change::Removed(e)),
139 Some(n) if e != n => changes.push(Change::Modified {
140 before: e,
141 after: n,
142 }),
143 _ => {}
144 }
145 }
146 for (path, e) in &next {
147 if !before.contains_key(path) {
148 changes.push(Change::Added(e));
149 }
150 }
151 changes
152 }
153}
154#[derive(Debug, PartialEq, Eq)]
156#[non_exhaustive]
157pub enum Change<'a> {
158 Added(&'a Entry),
159 Removed(&'a Entry),
160 Modified { before: &'a Entry, after: &'a Entry },
161}
162fn display_value(kind: u16, bytes: &[u8], order: ByteOrder) -> String {
163 if kind == 2 || kind == 129 {
164 return match core::str::from_utf8(bytes) {
165 Ok(text) => format!("{text:?}"),
166 Err(_) => format!("bytes={bytes:02x?}"),
167 };
168 }
169 let width = match kind {
170 1 | 6 => 1,
171 3 | 8 => 2,
172 4 | 9 | 11 | 13 => 4,
173 5 | 10 | 12 => 8,
174 _ => return format!("{bytes:02x?}"),
175 };
176 let number = |b: &[u8]| -> u64 {
177 match order {
178 ByteOrder::Little => b
179 .iter()
180 .enumerate()
181 .fold(0, |v, (i, b)| v | ((*b as u64) << (i * 8))),
182 ByteOrder::Big => b.iter().fold(0, |v, b| (v << 8) | *b as u64),
183 }
184 };
185 let values: Vec<_> = bytes
186 .chunks_exact(width)
187 .map(|b| match kind {
188 6 => (number(b) as i8).to_string(),
189 8 => (number(b) as i16).to_string(),
190 9 => (number(b) as i32).to_string(),
191 5 => format!("{}/{}", number(&b[..4]), number(&b[4..])),
192 10 => format!("{}/{}", number(&b[..4]) as i32, number(&b[4..]) as i32),
193 11 => format!(
194 "{:?} [bits={:08x}]",
195 f32::from_bits(number(b) as u32),
196 number(b)
197 ),
198 12 => format!("{:?} [bits={:016x}]", f64::from_bits(number(b)), number(b)),
199 _ => number(b).to_string(),
200 })
201 .collect();
202 format!("[{}]", values.join(", "))
203}