use crate::bytes::read_u32_le;
use crate::error::{FormatError, Result};
use crate::input::TagInput;
pub(crate) const VENDOR: &str = "musefs";
pub fn is_valid_key(key: &str) -> bool {
!key.is_empty() && key.bytes().all(|b| (0x20..=0x7D).contains(&b) && b != b'=')
}
pub(crate) fn build(tags: &[TagInput]) -> Result<Vec<u8>> {
let mut out = Vec::new();
out.extend_from_slice(
&u32::try_from(VENDOR.len())
.map_err(|_| FormatError::TooLarge)?
.to_le_bytes(),
);
out.extend_from_slice(VENDOR.as_bytes());
let valid: Vec<&TagInput> = tags.iter().filter(|t| is_valid_key(&t.key)).collect();
out.extend_from_slice(
&u32::try_from(valid.len())
.map_err(|_| FormatError::TooLarge)?
.to_le_bytes(),
);
for t in valid {
let field = crate::tagmap::key_to_vorbis(&t.key)
.map_or_else(|| t.key.to_ascii_uppercase(), str::to_string);
let comment = format!("{field}={}", t.value);
out.extend_from_slice(
&u32::try_from(comment.len())
.map_err(|_| FormatError::TooLarge)?
.to_le_bytes(),
);
out.extend_from_slice(comment.as_bytes());
}
Ok(out)
}
pub fn parse(body: &[u8]) -> Result<Vec<(String, String)>> {
let vendor_len = read_u32_le(body, 0)? as usize;
let mut pos = 4 + vendor_len;
let count = read_u32_le(body, pos)? as usize;
pos += 4;
let mut out = Vec::with_capacity(count.min(body.len() / 4));
for _ in 0..count {
let clen = read_u32_le(body, pos)? as usize;
pos += 4;
let end = pos + clen;
if end > body.len() {
return Err(FormatError::Malformed);
}
let comment = std::str::from_utf8(&body[pos..end]).map_err(|_| FormatError::Malformed)?;
if let Some((field, value)) = comment.split_once('=') {
if !field.is_empty() {
let key = crate::tagmap::vorbis_to_key(field)
.map_or_else(|| field.to_string(), str::to_string);
out.push((key, value.to_string()));
}
}
pos = end;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::TagInput;
#[test]
fn build_skips_keys_outside_vorbis_grammar() {
let tags = vec![
TagInput::new("artist", "Alice"),
TagInput::new("a=b", "c"),
TagInput::new("", "x"),
TagInput::new("title", "Song"),
];
let body = build(&tags).unwrap();
let parsed = parse(&body).unwrap();
assert_eq!(
parsed,
vec![
("artist".to_string(), "Alice".to_string()),
("title".to_string(), "Song".to_string()),
]
);
}
#[test]
fn build_is_total_over_arbitrary_keys() {
let tags = vec![
TagInput::new("a=b=c", "v"),
TagInput::new("\u{0}\u{1}", "v"),
TagInput::new("ok", "v"),
];
let body = build(&tags).unwrap();
let parsed = parse(&body).unwrap();
assert_eq!(parsed, vec![("OK".to_string(), "v".to_string())]);
}
#[test]
fn build_then_parse_round_trips() {
let tags = vec![
TagInput::new("artist", "Boards of Canada"),
TagInput::new("title", "Roygbiv"),
];
let body = build(&tags).unwrap();
let parsed = parse(&body).unwrap();
assert_eq!(
parsed,
vec![
("artist".to_string(), "Boards of Canada".to_string()),
("title".to_string(), "Roygbiv".to_string()),
]
);
}
#[test]
fn parse_rejects_bogus_huge_count_without_oom() {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&u32::MAX.to_le_bytes()); assert!(parse(&body).is_err());
}
#[test]
fn is_valid_key_enforces_vorbis_grammar() {
assert!(is_valid_key("title"));
assert!(is_valid_key("CUSTOM_THING"));
assert!(is_valid_key("}")); assert!(is_valid_key(" ")); assert!(!is_valid_key("")); assert!(!is_valid_key("a=b")); assert!(!is_valid_key("a\u{1f}b")); assert!(!is_valid_key("a\u{7f}b")); assert!(!is_valid_key("a~b")); assert!(!is_valid_key("género")); }
#[test]
fn parse_canonicalizes_known_fields_and_preserves_unknown() {
let tags = vec![
TagInput::new("albumartist", "VA"),
TagInput::new("custom_thing", "x"),
];
let body = build(&tags).unwrap();
let parsed = parse(&body).unwrap();
assert_eq!(parsed[0], ("albumartist".to_string(), "VA".to_string()));
assert_eq!(parsed[1], ("CUSTOM_THING".to_string(), "x".to_string()));
}
fn body_with_one_comment(comment: &str) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&u32::try_from(VENDOR.len()).unwrap().to_le_bytes());
body.extend_from_slice(VENDOR.as_bytes());
body.extend_from_slice(&1u32.to_le_bytes()); body.extend_from_slice(&u32::try_from(comment.len()).unwrap().to_le_bytes());
body.extend_from_slice(comment.as_bytes());
body
}
#[test]
fn parse_skips_empty_field_name() {
assert!(parse(&body_with_one_comment("=value")).unwrap().is_empty());
}
#[test]
fn parse_splits_on_first_equals() {
let parsed = parse(&body_with_one_comment("A=B=c")).unwrap();
assert_eq!(parsed, vec![("A".to_string(), "B=c".to_string())]);
}
}