use std::borrow::Cow;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BlobKind(Cow<'static, str>);
impl BlobKind {
pub const ICC: Self = Self::from_static("icc");
pub const EXIF: Self = Self::from_static("exif");
pub const XMP: Self = Self::from_static("xmp");
pub const IPTC: Self = Self::from_static("iptc");
pub const COVER_ART: Self = Self::from_static("cover-art");
pub const WELL_KNOWN: [Self; 5] = [
Self::ICC,
Self::EXIF,
Self::XMP,
Self::IPTC,
Self::COVER_ART,
];
pub const fn from_static(name: &'static str) -> Self {
Self(Cow::Borrowed(name))
}
pub fn custom(name: impl Into<String>) -> Self {
Self(Cow::Owned(name.into()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_well_known(&self) -> bool {
Self::WELL_KNOWN.iter().any(|k| k == self)
}
}
impl fmt::Display for BlobKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for BlobKind {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<&'static str> for BlobKind {
fn from(name: &'static str) -> Self {
Self::from_static(name)
}
}
impl From<String> for BlobKind {
fn from(name: String) -> Self {
Self(Cow::Owned(name))
}
}
impl PartialEq<str> for BlobKind {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for BlobKind {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct MetadataBlob {
pub kind: BlobKind,
pub mime: Option<String>,
pub data: Vec<u8>,
}
impl MetadataBlob {
pub fn new(kind: impl Into<BlobKind>, data: impl Into<Vec<u8>>) -> Self {
Self {
kind: kind.into(),
mime: None,
data: data.into(),
}
}
pub fn with_mime(mut self, mime: impl Into<String>) -> Self {
self.mime = Some(mime.into());
self
}
pub fn is(&self, kind: &BlobKind) -> bool {
self.kind == *kind
}
fn wire_len(&self) -> usize {
8 + self.kind.as_str().len()
+ 1
+ self.mime.as_ref().map_or(0, |m| 8 + m.len())
+ 8
+ self.data.len()
}
}
pub fn encode_blobs(blobs: &[MetadataBlob]) -> Vec<u8> {
fn put(out: &mut Vec<u8>, bytes: &[u8]) {
out.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
out.extend_from_slice(bytes);
}
if blobs.is_empty() {
return Vec::new();
}
let total: usize = 8 + blobs.iter().map(MetadataBlob::wire_len).sum::<usize>();
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&(blobs.len() as u64).to_le_bytes());
for b in blobs {
put(&mut out, b.kind.as_str().as_bytes());
match &b.mime {
Some(m) => {
out.push(1);
put(&mut out, m.as_bytes());
}
None => out.push(0),
}
put(&mut out, &b.data);
}
out
}
pub fn decode_blobs(mut bytes: &[u8]) -> Option<Vec<MetadataBlob>> {
fn take<'a>(bytes: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
let (head, tail) = bytes.split_at_checked(n)?;
*bytes = tail;
Some(head)
}
fn take_len(bytes: &mut &[u8]) -> Option<usize> {
let b = take(bytes, 8)?;
usize::try_from(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
.ok()
}
fn take_str<'a>(bytes: &mut &'a [u8]) -> Option<&'a str> {
let n = take_len(bytes)?;
std::str::from_utf8(take(bytes, n)?).ok()
}
if bytes.is_empty() {
return Some(Vec::new());
}
let count = take_len(&mut bytes)?;
let mut out = Vec::new();
for _ in 0..count {
let kind = take_str(&mut bytes)?.to_owned();
let mime = match take(&mut bytes, 1)?[0] {
0 => None,
1 => Some(take_str(&mut bytes)?.to_owned()),
_ => return None,
};
let len = take_len(&mut bytes)?;
let data = take(&mut bytes, len)?.to_vec();
out.push(MetadataBlob {
kind: BlobKind::from(kind),
mime,
data,
});
}
bytes.is_empty().then_some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kinds_compare_by_name_whatever_their_storage() {
assert_eq!(BlobKind::ICC, BlobKind::custom("icc"));
assert_eq!(BlobKind::ICC, BlobKind::from(String::from("icc")));
assert_eq!(BlobKind::from("icc"), BlobKind::ICC);
assert_ne!(BlobKind::ICC, BlobKind::EXIF);
assert_eq!(BlobKind::COVER_ART.as_str(), "cover-art");
assert_eq!(BlobKind::COVER_ART.to_string(), "cover-art");
assert!(BlobKind::COVER_ART == "cover-art");
assert!(BlobKind::XMP.is_well_known());
assert!(!BlobKind::custom("png-text").is_well_known());
assert_ne!(BlobKind::custom("ICC"), BlobKind::ICC);
assert_eq!(BlobKind::WELL_KNOWN.len(), 5);
let known = BlobKind::WELL_KNOWN;
let mut names: Vec<&str> = known.iter().map(BlobKind::as_str).collect();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), 5, "well-known names are distinct");
}
#[test]
fn blob_constructor_and_builders() {
let b = MetadataBlob::new(BlobKind::COVER_ART, vec![0xFF, 0xD8]).with_mime("image/jpeg");
assert!(b.is(&BlobKind::COVER_ART));
assert!(!b.is(&BlobKind::ICC));
assert_eq!(b.mime.as_deref(), Some("image/jpeg"));
assert_eq!(b.data, [0xFF, 0xD8]);
let c = MetadataBlob::new("exr-attributes", &b"abc"[..]);
assert_eq!(c.kind, BlobKind::custom("exr-attributes"));
assert_eq!(c.mime, None);
assert_eq!(c.data, b"abc");
}
#[test]
fn wire_form_round_trips_including_empty_and_mime() {
let blobs = vec![
MetadataBlob::new(BlobKind::ICC, vec![1, 2, 3, 4]),
MetadataBlob::new(BlobKind::COVER_ART, vec![0xFF, 0xD8, 0xFF]).with_mime("image/jpeg"),
MetadataBlob::new(BlobKind::custom("png-text"), Vec::new()),
MetadataBlob::new(BlobKind::EXIF, b"II*\0\x08\0\0\0".to_vec()).with_mime(""),
];
let wire = encode_blobs(&blobs);
assert_eq!(
wire.len(),
8 + blobs.iter().map(MetadataBlob::wire_len).sum::<usize>()
);
assert_eq!(&wire[..8], 4u64.to_le_bytes());
assert_eq!(decode_blobs(&wire), Some(blobs.clone()));
let first = encode_blobs(&blobs[..1]);
let mut expect = vec![1, 0, 0, 0, 0, 0, 0, 0];
expect.extend_from_slice(&[3, 0, 0, 0, 0, 0, 0, 0, b'i', b'c', b'c', 0]);
expect.extend_from_slice(&[4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4]);
assert_eq!(first, expect);
let second = &encode_blobs(&blobs[1..2])[8..];
assert_eq!(&second[..8], [9, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(&second[8..17], b"cover-art");
assert_eq!(&second[17..26], [1, 10, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(&second[26..36], b"image/jpeg");
assert_eq!(&second[36..], [3, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xD8, 0xFF]);
assert_eq!(encode_blobs(&[]), Vec::<u8>::new());
assert_eq!(decode_blobs(&[]), Some(Vec::new()));
}
#[test]
fn wire_form_rejects_truncation_trailing_bytes_and_bad_flags() {
let blobs = vec![
MetadataBlob::new(BlobKind::ICC, vec![1, 2, 3, 4]),
MetadataBlob::new(BlobKind::XMP, vec![5]).with_mime("application/rdf+xml"),
];
let wire = encode_blobs(&blobs);
let boundary = 8 + blobs[..1].iter().map(MetadataBlob::wire_len).sum::<usize>();
assert_eq!(decode_blobs(&wire[..boundary]), None, "boundary cut");
for cut in 1..wire.len() {
assert_eq!(decode_blobs(&wire[..cut]), None, "prefix of {cut} bytes");
}
let mut longer = wire.clone();
longer.push(0);
assert_eq!(decode_blobs(&longer), None);
let mut bad_flag = wire.clone();
bad_flag[19] = 2;
assert_eq!(decode_blobs(&bad_flag), None);
let mut bad_utf8 = wire.clone();
bad_utf8[16] = 0xFF;
assert_eq!(decode_blobs(&bad_utf8), None);
let mut huge_count = wire.clone();
huge_count[..8].copy_from_slice(&u64::MAX.to_le_bytes());
assert_eq!(decode_blobs(&huge_count), None);
let mut huge_name = wire.clone();
huge_name[8..16].copy_from_slice(&u64::MAX.to_le_bytes());
assert_eq!(decode_blobs(&huge_name), None);
let mut huge_data = encode_blobs(&blobs[..1]);
huge_data[20..28].copy_from_slice(&(1u64 << 40).to_le_bytes());
assert_eq!(decode_blobs(&huge_data), None);
let mut short_count = wire.clone();
short_count[..8].copy_from_slice(&1u64.to_le_bytes());
assert_eq!(decode_blobs(&short_count), None);
}
}