#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct VorbisComments {
vendor: String,
entries: Vec<(String, String)>,
}
impl VorbisComments {
#[must_use]
pub fn new(vendor: impl Into<String>) -> Self {
Self {
vendor: vendor.into(),
entries: Vec::new(),
}
}
#[must_use]
pub fn vendor(&self) -> &str {
&self.vendor
}
pub fn set_vendor(&mut self, vendor: impl Into<String>) {
self.vendor = vendor.into();
}
pub fn add(&mut self, field: impl Into<String>, value: impl Into<String>) {
self.entries.push((field.into(), value.into()));
}
#[must_use]
pub fn get(&self, field: &str) -> Option<&str> {
self.entries
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(field))
.map(|(_, v)| v.as_str())
}
#[must_use]
pub fn get_all(&self, field: &str) -> Vec<&str> {
self.entries
.iter()
.filter(|(k, _)| k.eq_ignore_ascii_case(field))
.map(|(_, v)| v.as_str())
.collect()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn to_raw_strings(&self) -> Vec<String> {
self.entries
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect()
}
#[must_use]
pub fn encode(&self) -> Option<Vec<u8>> {
let vendor_bytes = self.vendor.as_bytes();
let vendor_len = u32::try_from(vendor_bytes.len()).ok()?;
let raw = self.to_raw_strings();
let comment_count = u32::try_from(raw.len()).ok()?;
let mut capacity: usize = 4 + vendor_bytes.len() + 4;
for r in &raw {
capacity = capacity.checked_add(4 + r.len())?;
}
let mut buf = Vec::with_capacity(capacity);
buf.extend_from_slice(&vendor_len.to_le_bytes());
buf.extend_from_slice(vendor_bytes);
buf.extend_from_slice(&comment_count.to_le_bytes());
for r in &raw {
let bytes = r.as_bytes();
let len = u32::try_from(bytes.len()).ok()?;
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(bytes);
}
Some(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_basic_comments() {
let mut comments = VorbisComments::new("test-vendor");
comments.add("ARTIST", "Someone");
comments.add("TITLE", "Something");
comments.add("artist", "Another");
assert_eq!(comments.vendor(), "test-vendor");
assert_eq!(comments.len(), 3);
assert_eq!(comments.get("ARTIST"), Some("Someone"));
assert_eq!(comments.get("artist"), Some("Someone"));
assert_eq!(comments.get_all("artist"), vec!["Someone", "Another"]);
assert_eq!(comments.get("TITLE"), Some("Something"));
assert_eq!(comments.get("ALBUM"), None);
}
#[test]
fn encodes_to_expected_binary_layout() {
let mut comments = VorbisComments::new("v");
comments.add("A", "1");
let encoded = comments.encode().unwrap();
assert_eq!(encoded.len(), 16);
assert_eq!(u32::from_le_bytes(encoded[0..4].try_into().unwrap()), 1);
assert_eq!(&encoded[4..5], b"v");
assert_eq!(u32::from_le_bytes(encoded[5..9].try_into().unwrap()), 1);
assert_eq!(u32::from_le_bytes(encoded[9..13].try_into().unwrap()), 3);
assert_eq!(&encoded[13..16], b"A=1");
}
#[test]
fn empty_comments_encode() {
let comments = VorbisComments::new("x");
let encoded = comments.encode().unwrap();
assert_eq!(u32::from_le_bytes(encoded[0..4].try_into().unwrap()), 1);
assert_eq!(&encoded[4..5], b"x");
assert_eq!(u32::from_le_bytes(encoded[5..9].try_into().unwrap()), 0);
assert_eq!(encoded.len(), 9);
}
#[test]
fn iter_preserves_insertion_order() {
let mut comments = VorbisComments::new("");
comments.add("B", "2");
comments.add("A", "1");
let pairs: Vec<_> = comments.iter().collect();
assert_eq!(pairs, vec![("B", "2"), ("A", "1")]);
}
}