use crate::bytes::Reader;
use crate::formats::xmp;
use crate::report::{Finding, InspectOptions, MetadataKind, MetadataValue};
const MAX_ITEMISED_COMMENTS: u32 = 512;
pub(crate) const EMPTY: [u8; 8] = [0; 8];
const COMMENT_KINDS: &[(&str, MetadataKind)] = &[
("ARTIST", MetadataKind::PersonalIdentity),
("ALBUMARTIST", MetadataKind::PersonalIdentity),
("PERFORMER", MetadataKind::PersonalIdentity),
("COMPOSER", MetadataKind::PersonalIdentity),
("CONDUCTOR", MetadataKind::PersonalIdentity),
("COPYRIGHT", MetadataKind::PersonalIdentity),
("ORGANIZATION", MetadataKind::PersonalIdentity),
("ENCODED-BY", MetadataKind::PersonalIdentity),
("CONTACT", MetadataKind::PersonalIdentity),
("LOCATION", MetadataKind::Location),
("GEO", MetadataKind::Location),
("GPS", MetadataKind::Location),
("DATE", MetadataKind::Timestamp),
("YEAR", MetadataKind::Timestamp),
("ENCODER", MetadataKind::SoftwareFingerprint),
("ENCODING", MetadataKind::SoftwareFingerprint),
("SOURCEMEDIA", MetadataKind::SoftwareFingerprint),
("MUSICBRAINZ", MetadataKind::DocumentIdentifier),
("CDDB", MetadataKind::DocumentIdentifier),
("ISRC", MetadataKind::DocumentIdentifier),
("REPLAYGAIN", MetadataKind::Other),
("COMMENT", MetadataKind::Comment),
("DESCRIPTION", MetadataKind::Comment),
("METADATA_BLOCK_PICTURE", MetadataKind::Thumbnail),
];
pub(crate) fn comments(
payload: &[u8],
location: &str,
size: u64,
options: &InspectOptions,
out: &mut Vec<Finding>,
) {
if !itemise(payload, location, options, out) {
out.push(Finding::new(MetadataKind::Other, location, size).with_field("Comments"));
}
}
fn itemise(
payload: &[u8],
location: &str,
options: &InspectOptions,
out: &mut Vec<Finding>,
) -> bool {
let mut r = Reader::new(payload);
let Some(vendor) = r
.u32_le()
.and_then(|n| usize::try_from(n).ok())
.and_then(|n| r.take(n))
else {
return false;
};
if !vendor.is_empty() {
out.push(
Finding::new(
MetadataKind::SoftwareFingerprint,
location,
as_u64(vendor.len()),
)
.with_field("vendor")
.with_value(options, || MetadataValue::Text(xmp::name_of(vendor))),
);
}
let Some(declared) = r.u32_le() else {
return false;
};
for _ in 0..declared.min(MAX_ITEMISED_COMMENTS) {
let Some(item) = r
.u32_le()
.and_then(|n| usize::try_from(n).ok())
.and_then(|n| r.take(n))
else {
return false;
};
let (name, value) = split_comment(item);
out.push(
Finding::new(kind_of(&name), location, as_u64(item.len()))
.with_field(name)
.with_value(options, || MetadataValue::Text(xmp::name_of(value))),
);
}
true
}
fn split_comment(item: &[u8]) -> (String, &[u8]) {
match item.iter().position(|byte| *byte == b'=') {
Some(at) => (
xmp::name_of(item.get(0..at).unwrap_or_default()),
item.get(at.saturating_add(1)..).unwrap_or_default(),
),
None => (xmp::name_of(item), &[]),
}
}
fn kind_of(name: &str) -> MetadataKind {
let upper = name.to_uppercase();
COMMENT_KINDS
.iter()
.find(|(candidate, _)| upper.starts_with(candidate))
.map_or(MetadataKind::Other, |(_, kind)| *kind)
}
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::*;
fn body(vendor: &[u8], items: &[&[u8]]) -> Vec<u8> {
let le32 = |n: usize| u32::try_from(n).unwrap().to_le_bytes();
let mut out = le32(vendor.len()).to_vec();
out.extend_from_slice(vendor);
out.extend_from_slice(&le32(items.len()));
for item in items {
out.extend_from_slice(&le32(item.len()));
out.extend_from_slice(item);
}
out
}
fn found(payload: &[u8]) -> Vec<Finding> {
let mut out = Vec::new();
comments(
payload,
"OpusTags",
u64::try_from(payload.len()).unwrap(),
&InspectOptions::names_only(),
&mut out,
);
out
}
#[test]
fn the_vendor_string_and_every_item_are_reported_under_the_callers_location() {
let out = found(&body(
b"libopus SYNTHETIC-VENDOR",
&[b"ARTIST=SYNTHETIC", b"LOCATION=51.5,-0.1"],
));
assert!(out.iter().all(|f| f.location == "OpusTags"));
assert_eq!(out[0].field.as_deref(), Some("vendor"));
assert_eq!(out[1].kind, MetadataKind::PersonalIdentity);
assert_eq!(out[2].kind, MetadataKind::Location);
}
#[test]
fn a_body_whose_lengths_do_not_add_up_still_produces_a_finding() {
let mut payload = 0xFFFF_FFFFu32.to_le_bytes().to_vec();
payload.extend_from_slice(b"SYNTHETIC-UNREADABLE");
let out = found(&payload);
assert_eq!(out.len(), 1);
assert_eq!(out[0].field.as_deref(), Some("Comments"));
}
#[test]
fn the_empty_body_is_a_zero_vendor_and_a_zero_count() {
assert!(found(&EMPTY).is_empty());
let mut r = Reader::new(&EMPTY);
assert_eq!(r.u32_le(), Some(0));
assert_eq!(r.u32_le(), Some(0));
}
#[test]
fn a_field_name_is_ranked_by_prefix_and_case_insensitively() {
assert_eq!(
kind_of("musicbrainz_trackid"),
MetadataKind::DocumentIdentifier
);
assert_eq!(kind_of("SOMETHING_ELSE"), MetadataKind::Other);
}
}