Files
pile/crates/pile-flac/src/vorbiscomment.rs
2026-03-05 21:35:19 -08:00

195 lines
4.5 KiB
Rust

//! Decode and write Vorbis comment blocks
use base64::Engine;
use smartstring::{LazyCompact, SmartString};
use std::{
io::{Cursor, Read, Write},
str::FromStr,
};
use super::tagtype::TagType;
use crate::{
FlacDecodeError, FlacEncodeError,
blocks::{FlacMetablockDecode, FlacMetablockEncode, FlacPictureBlock},
};
/// A decoded vorbis comment block
#[derive(Debug)]
pub struct VorbisComment {
/// This comment's vendor string
pub vendor: SmartString<LazyCompact>,
/// List of (tag, value)
/// Repeated tags are allowed!
pub comments: Vec<(TagType, SmartString<LazyCompact>)>,
/// A list of pictures found in this comment
pub pictures: Vec<FlacPictureBlock>,
}
impl VorbisComment {
/// Try to decode the given data as a vorbis comment block
pub fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
let mut d = Cursor::new(data);
// This is re-used whenever we need to read four bytes
let mut block = [0u8; 4];
let vendor = {
d.read_exact(&mut block)?;
let length = u32::from_le_bytes(block);
#[expect(clippy::expect_used)]
let mut text = vec![
0u8;
length
.try_into()
.expect("vendor length does not fit into usize")
];
d.read_exact(&mut text)?;
String::from_utf8(text)?
};
d.read_exact(&mut block)?;
#[expect(clippy::expect_used)]
let n_comments: usize = u32::from_le_bytes(block)
.try_into()
.expect("comment count does not fit into usize");
let mut comments = Vec::new();
let mut pictures = Vec::new();
for _ in 0..n_comments {
let comment = {
d.read_exact(&mut block)?;
let length = u32::from_le_bytes(block);
#[expect(clippy::expect_used)]
let mut text = vec![
0u8;
length
.try_into()
.expect("comment length does not fit into usize")
];
d.read_exact(&mut text)?;
String::from_utf8(text)?
};
let (var, val) = comment
.split_once('=')
.ok_or(FlacDecodeError::MalformedCommentString(comment.clone()))?;
if !val.is_empty() {
if var.to_uppercase() == "METADATA_BLOCK_PICTURE" {
pictures.push(FlacPictureBlock::decode(
&base64::prelude::BASE64_STANDARD
.decode(val)
.map_err(FlacDecodeError::MalformedPicture)?,
)?);
} else {
// Make sure empty strings are saved as "None"
comments.push((
TagType::from_str(var).unwrap_or(TagType::Other(var.into())),
val.into(),
));
}
};
}
Ok(Self {
vendor: vendor.into(),
comments,
pictures,
})
}
}
impl VorbisComment {
/// Get the number of bytes that `encode()` will write.
#[expect(clippy::expect_used)]
pub fn get_len(&self) -> u32 {
let mut sum: u32 = 0;
sum += u32::try_from(self.vendor.len()).expect("vendor length does not fit into u32") + 4;
sum += 4;
for (tagtype, value) in &self.comments {
let tagtype_str = tagtype.to_vorbis_string();
let str = format!("{tagtype_str}={value}");
sum +=
4 + u32::try_from(str.len()).expect("comment string length does not fit into u32");
}
for p in &self.pictures {
// Compute b64 len
let mut x = p.get_len();
if x % 3 != 0 {
x -= x % 3;
x += 3;
}
#[expect(clippy::integer_division)]
{
sum += 4 * (x / 3);
}
// Add "METADATA_BLOCK_PICTURE="
sum += 23;
// Add length bytes
sum += 4;
}
return sum;
}
/// Try to encode this vorbis comment
#[expect(clippy::expect_used)]
pub fn encode(&self, target: &mut impl Write) -> Result<(), FlacEncodeError> {
target.write_all(
&u32::try_from(self.vendor.len())
.expect("vendor length does not fit into u32")
.to_le_bytes(),
)?;
target.write_all(self.vendor.as_bytes())?;
target.write_all(
&u32::try_from(self.comments.len() + self.pictures.len())
.expect("total comment count does not fit into u32")
.to_le_bytes(),
)?;
for (tagtype, value) in &self.comments {
let tagtype_str = tagtype.to_vorbis_string();
let str = format!("{tagtype_str}={value}");
target.write_all(
&u32::try_from(str.len())
.expect("comment string length does not fit into u32")
.to_le_bytes(),
)?;
target.write_all(str.as_bytes())?;
}
for p in &self.pictures {
let mut pic_data = Vec::new();
p.encode(false, false, &mut pic_data)?;
let pic_string = format!(
"METADATA_BLOCK_PICTURE={}",
&base64::prelude::BASE64_STANDARD.encode(&pic_data)
);
target.write_all(
&u32::try_from(pic_string.len())
.expect("picture string length does not fit into u32")
.to_le_bytes(),
)?;
target.write_all(pic_string.as_bytes())?;
}
return Ok(());
}
}