use crate::detect::Format;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum MetadataKind {
Location,
DeviceIdentity,
PersonalIdentity,
SoftwareFingerprint,
Timestamp,
Thumbnail,
EditingHistory,
DocumentIdentifier,
ColourProfile,
Comment,
Other,
}
impl MetadataKind {
#[must_use]
pub const fn id(self) -> &'static str {
match self {
Self::Location => "location",
Self::DeviceIdentity => "device-identity",
Self::PersonalIdentity => "personal-identity",
Self::SoftwareFingerprint => "software-fingerprint",
Self::Timestamp => "timestamp",
Self::Thumbnail => "thumbnail",
Self::EditingHistory => "editing-history",
Self::DocumentIdentifier => "document-identifier",
Self::ColourProfile => "colour-profile",
Self::Comment => "comment",
Self::Other => "other",
}
}
#[must_use]
pub const fn sensitivity(self) -> Sensitivity {
match self {
Self::Location | Self::DeviceIdentity | Self::PersonalIdentity | Self::Thumbnail => {
Sensitivity::Direct
}
Self::Timestamp
| Self::EditingHistory
| Self::DocumentIdentifier
| Self::ColourProfile
| Self::Comment => Sensitivity::Correlating,
Self::SoftwareFingerprint | Self::Other => Sensitivity::Incidental,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Sensitivity {
Direct,
Correlating,
Incidental,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum MetadataValue {
Text(String),
Opaque {
bytes: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Finding {
pub kind: MetadataKind,
pub location: String,
pub field: Option<String>,
pub bytes: u64,
pub value: Option<MetadataValue>,
}
impl Finding {
#[must_use]
pub fn new(kind: MetadataKind, location: impl Into<String>, bytes: u64) -> Self {
Self {
kind,
location: location.into(),
field: None,
bytes,
value: None,
}
}
#[must_use]
pub fn with_field(mut self, field: impl Into<String>) -> Self {
self.field = Some(field.into());
self
}
#[must_use]
pub fn with_value(
mut self,
options: &InspectOptions,
value: impl FnOnce() -> MetadataValue,
) -> Self {
if options.include_values {
self.value = Some(value());
}
self
}
#[must_use]
pub const fn sensitivity(&self) -> Sensitivity {
self.kind.sensitivity()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Note {
UnparsedRegion {
location: String,
bytes: u64,
},
IncrementalHistory {
revisions: usize,
},
OrphanedObjectsRemoved {
objects: usize,
},
OutOfScopeContent {
location: String,
},
FilenameMayIdentify,
CapabilityRemoved {
location: String,
capability: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct MetadataReport {
pub format: Format,
pub findings: Vec<Finding>,
pub notes: Vec<Note>,
}
impl MetadataReport {
#[must_use]
pub const fn empty(format: Format) -> Self {
Self {
format,
findings: Vec::new(),
notes: Vec::new(),
}
}
#[must_use]
pub fn has_findings(&self) -> bool {
!self.findings.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct StripReport {
pub format: Format,
pub removed: Vec<Finding>,
pub retained: Vec<Retained>,
pub notes: Vec<Note>,
pub input_bytes: u64,
pub output_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Retained {
pub location: String,
pub reason: RetentionReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RetentionReason {
RemovalWouldAlterPayload,
StructurallyRequired,
DerivedFromPayload,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct InspectOptions {
pub include_values: bool,
}
impl InspectOptions {
#[must_use]
pub const fn names_only() -> Self {
Self {
include_values: false,
}
}
#[must_use]
pub const fn with_values() -> Self {
Self {
include_values: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn values_are_absent_unless_the_caller_asks() {
let options = InspectOptions::names_only();
let f = Finding::new(MetadataKind::Location, "APP1 (Exif)", 42)
.with_field("GPSLatitude")
.with_value(&options, || MetadataValue::Text("51.5074".into()));
assert_eq!(f.field.as_deref(), Some("GPSLatitude"));
assert_eq!(
f.value, None,
"a default inspection must name the field and withhold the coordinate"
);
}
#[test]
fn values_are_present_when_the_caller_opts_in() {
let options = InspectOptions::with_values();
let f = Finding::new(MetadataKind::PersonalIdentity, "/Info", 12)
.with_value(&options, || MetadataValue::Text("A. Name".into()));
assert_eq!(f.value, Some(MetadataValue::Text("A. Name".into())));
}
#[test]
fn directly_identifying_kinds_outrank_incidental_ones() {
assert!(Sensitivity::Direct < Sensitivity::Correlating);
assert!(Sensitivity::Correlating < Sensitivity::Incidental);
assert_eq!(MetadataKind::Location.sensitivity(), Sensitivity::Direct);
assert_eq!(
MetadataKind::SoftwareFingerprint.sensitivity(),
Sensitivity::Incidental
);
}
#[test]
fn an_embedded_thumbnail_ranks_as_directly_identifying() {
assert_eq!(MetadataKind::Thumbnail.sensitivity(), Sensitivity::Direct);
}
}