//! 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, /// List of (tag, value) /// Repeated tags are allowed! pub comments: Vec<(TagType, SmartString)>, /// A list of pictures found in this comment pub pictures: Vec, } impl VorbisComment { /// Try to decode the given data as a vorbis comment block pub fn decode(data: &[u8]) -> Result { 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(()); } }