use crate::report::{Finding, InspectOptions, MetadataKind, MetadataValue};
pub(crate) const PROPERTIES: &[(&[u8], MetadataKind)] = &[
(b"dc:creator", MetadataKind::PersonalIdentity),
(b"dc:title", MetadataKind::Comment),
(b"dc:description", MetadataKind::Comment),
(b"dc:subject", MetadataKind::Comment),
(b"xmp:CreatorTool", MetadataKind::SoftwareFingerprint),
(b"xmp:CreateDate", MetadataKind::Timestamp),
(b"xmp:ModifyDate", MetadataKind::Timestamp),
(b"xmp:MetadataDate", MetadataKind::Timestamp),
(b"xmpMM:DocumentID", MetadataKind::DocumentIdentifier),
(b"xmpMM:InstanceID", MetadataKind::DocumentIdentifier),
(
b"xmpMM:OriginalDocumentID",
MetadataKind::DocumentIdentifier,
),
(b"xmpMM:History", MetadataKind::EditingHistory),
(b"pdf:Producer", MetadataKind::SoftwareFingerprint),
(b"photoshop:", MetadataKind::SoftwareFingerprint),
(b"exif:GPS", MetadataKind::Location),
(b"tiff:Make", MetadataKind::DeviceIdentity),
(b"tiff:Model", MetadataKind::DeviceIdentity),
];
pub(crate) fn scan(packet: &[u8], location: &str, options: &InspectOptions) -> Vec<Finding> {
let mut out = Vec::new();
for (property, kind) in PROPERTIES {
if contains(packet, property) {
out.push(
Finding::new(*kind, location.to_owned(), 0)
.with_field(name_of(property))
.with_value(options, || MetadataValue::Opaque {
bytes: as_u64(property.len()),
}),
);
}
}
if out.is_empty() {
out.push(
Finding::new(
MetadataKind::Other,
location.to_owned(),
as_u64(packet.len()),
)
.with_field("Metadata"),
);
}
out
}
pub(crate) fn contains(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || needle.len() > haystack.len() {
return false;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
pub(crate) fn name_of(raw: &[u8]) -> String {
String::from_utf8_lossy(raw)
.chars()
.filter(|c| !c.is_control())
.collect()
}
fn as_u64(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
use super::*;
#[test]
fn known_properties_are_named_individually() {
let packet = br#"<x:xmpmeta><rdf:RDF><rdf:Description
xmpMM:DocumentID="uuid:0001" xmp:CreatorTool="SYNTHETIC"/></rdf:RDF></x:xmpmeta>"#;
let found = scan(packet, "APP1 (XMP)", &InspectOptions::names_only());
let fields: Vec<_> = found.iter().filter_map(|f| f.field.as_deref()).collect();
assert!(fields.contains(&"xmpMM:DocumentID"));
assert!(fields.contains(&"xmp:CreatorTool"));
}
#[test]
fn an_unrecognised_packet_is_still_reported() {
let found = scan(
b"<x:xmpmeta><rdf:RDF/></x:xmpmeta>",
"XMP packet",
&InspectOptions::names_only(),
);
assert_eq!(found.len(), 1);
assert_eq!(found[0].kind, MetadataKind::Other);
}
}