195 lines
4.5 KiB
Rust
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(());
|
|
}
|
|
}
|