pile-audio refactor
All checks were successful
CI / Typos (push) Successful in 20s
CI / Clippy (push) Successful in 2m3s
CI / Build and test (push) Successful in 3m31s

This commit is contained in:
2026-02-21 19:19:41 -08:00
parent 5aab61bd1b
commit bf1241e0a5
136 changed files with 1991 additions and 3390 deletions

View File

@@ -1,5 +0,0 @@
//! Components shared between many different formats
pub mod picturetype;
pub mod tagtype;
pub mod vorbiscomment;

View File

@@ -1,355 +0,0 @@
//! Decode and write Vorbis comment blocks
use base64::Engine;
use smartstring::{LazyCompact, SmartString};
use std::{
fmt::Display,
io::{Cursor, Read, Write},
string::FromUtf8Error,
};
use crate::flac::blocks::{FlacMetablockDecode, FlacMetablockEncode, FlacPictureBlock};
use super::tagtype::TagType;
#[derive(Debug)]
#[expect(missing_docs)]
pub enum VorbisCommentDecodeError {
/// We encountered an IoError while processing a block
IoError(std::io::Error),
/// We tried to decode a string, but got invalid data
FailedStringDecode(FromUtf8Error),
/// The given comment string isn't within spec
MalformedCommentString(String),
/// The comment we're reading is invalid
MalformedData,
/// We tried to decode picture data, but it was malformed.
MalformedPicture,
}
impl Display for VorbisCommentDecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IoError(_) => write!(f, "io error while reading vorbis comments"),
Self::FailedStringDecode(_) => {
write!(f, "string decode error while reading vorbis comments")
}
Self::MalformedCommentString(x) => {
write!(f, "malformed comment string `{x}`")
}
Self::MalformedData => {
write!(f, "malformed comment data")
}
Self::MalformedPicture => {
write!(f, "malformed picture data")
}
}
}
}
impl std::error::Error for VorbisCommentDecodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::IoError(x) => Some(x),
Self::FailedStringDecode(x) => Some(x),
_ => None,
}
}
}
impl From<std::io::Error> for VorbisCommentDecodeError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<FromUtf8Error> for VorbisCommentDecodeError {
fn from(value: FromUtf8Error) -> Self {
Self::FailedStringDecode(value)
}
}
#[derive(Debug)]
#[expect(missing_docs)]
pub enum VorbisCommentEncodeError {
/// We encountered an IoError while processing a block
IoError(std::io::Error),
/// We could not encode picture data
PictureEncodeError,
}
impl Display for VorbisCommentEncodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IoError(_) => write!(f, "io error while reading vorbis comments"),
Self::PictureEncodeError => {
write!(f, "could not encode picture")
}
}
}
}
impl std::error::Error for VorbisCommentEncodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::IoError(x) => Some(x),
_ => None,
}
}
}
impl From<std::io::Error> for VorbisCommentEncodeError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
/// 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, VorbisCommentDecodeError> {
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)
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
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)
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
String::from_utf8(text)?
};
d.read_exact(&mut block)
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
#[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)
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
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)
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
String::from_utf8(text)?
};
let (var, val) =
comment
.split_once('=')
.ok_or(VorbisCommentDecodeError::MalformedCommentString(
comment.clone(),
))?;
if !val.is_empty() {
if var.to_uppercase() == "METADATA_BLOCK_PICTURE" {
#[expect(clippy::map_err_ignore)]
pictures.push(
FlacPictureBlock::decode(
&base64::prelude::BASE64_STANDARD
.decode(val)
.map_err(|_| VorbisCommentDecodeError::MalformedPicture)?,
)
.map_err(|_| VorbisCommentDecodeError::MalformedPicture)?,
);
} else {
// Make sure empty strings are saved as "None"
comments.push((
match &var.to_uppercase()[..] {
"TITLE" => TagType::TrackTitle,
"ALBUM" => TagType::Album,
"TRACKNUMBER" => TagType::TrackNumber,
"ARTIST" => TagType::TrackArtist,
"ALBUMARTIST" => TagType::AlbumArtist,
"GENRE" => TagType::Genre,
"ISRC" => TagType::Isrc,
"DATE" => TagType::ReleaseDate,
"TOTALTRACKS" => TagType::TrackTotal,
"LYRICS" => TagType::Lyrics,
x => TagType::Other(x.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 = match tagtype {
TagType::TrackTitle => "TITLE",
TagType::Album => "ALBUM",
TagType::TrackNumber => "TRACKNUMBER",
TagType::TrackArtist => "ARTIST",
TagType::AlbumArtist => "ALBUMARTIST",
TagType::Genre => "GENRE",
TagType::Isrc => "ISRC",
TagType::ReleaseDate => "DATE",
TagType::TrackTotal => "TOTALTRACKS",
TagType::Lyrics => "LYRICS",
TagType::Comment => "COMMENT",
TagType::DiskNumber => "DISKNUMBER",
TagType::DiskTotal => "DISKTOTAL",
TagType::Year => "YEAR",
TagType::Other(x) => x,
}
.to_uppercase();
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<(), VorbisCommentEncodeError> {
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 = match tagtype {
TagType::TrackTitle => "TITLE",
TagType::Album => "ALBUM",
TagType::TrackNumber => "TRACKNUMBER",
TagType::TrackArtist => "ARTIST",
TagType::AlbumArtist => "ALBUMARTIST",
TagType::Genre => "GENRE",
TagType::Isrc => "ISRC",
TagType::ReleaseDate => "DATE",
TagType::TrackTotal => "TOTALTRACKS",
TagType::Lyrics => "LYRICS",
TagType::Comment => "COMMENT",
TagType::DiskNumber => "DISKNUMBER",
TagType::DiskTotal => "DISKTOTAL",
TagType::Year => "YEAR",
TagType::Other(x) => x,
}
.to_uppercase();
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();
#[expect(clippy::map_err_ignore)]
p.encode(false, false, &mut pic_data)
.map_err(|_| VorbisCommentEncodeError::PictureEncodeError)?;
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(());
}
}

View File

@@ -1,867 +0,0 @@
//! Strip metadata from a FLAC file without loading the whole thing into memory.
use std::{
collections::VecDeque,
io::{Cursor, Read, Seek, Write},
};
use thiserror::Error;
use super::{
blocks::{
FlacAudioFrame, FlacCommentBlock, FlacMetablockDecode, FlacMetablockEncode,
FlacMetablockHeader, FlacMetablockType,
},
errors::{FlacDecodeError, FlacEncodeError},
};
use crate::flac::blocks::{
FlacApplicationBlock, FlacCuesheetBlock, FlacPaddingBlock, FlacPictureBlock,
FlacSeektableBlock, FlacStreaminfoBlock,
};
const MIN_AUDIO_FRAME_LEN: usize = 5000;
/// Select which blocks we want to keep.
/// All values are `false` by default.
#[derive(Debug, Default, Clone, Copy)]
pub struct FlacBlockSelector {
/// Select `FlacMetablockType::Streaminfo` blocks.
pub pick_streaminfo: bool,
/// Select `FlacMetablockType::Padding` blocks.
pub pick_padding: bool,
/// Select `FlacMetablockType::Application` blocks.
pub pick_application: bool,
/// Select `FlacMetablockType::SeekTable` blocks.
pub pick_seektable: bool,
/// Select `FlacMetablockType::VorbisComment` blocks.
pub pick_vorbiscomment: bool,
/// Select `FlacMetablockType::CueSheet` blocks.
pub pick_cuesheet: bool,
/// Select `FlacMetablockType::Picture` blocks.
pub pick_picture: bool,
/// Select audio frames.
pub pick_audio: bool,
}
impl FlacBlockSelector {
/// Make a new [`FlacBlockSelector`]
pub fn new() -> Self {
Self::default()
}
fn should_pick_meta(&self, block_type: FlacMetablockType) -> bool {
match block_type {
FlacMetablockType::Streaminfo => self.pick_streaminfo,
FlacMetablockType::Padding => self.pick_padding,
FlacMetablockType::Application => self.pick_application,
FlacMetablockType::Seektable => self.pick_seektable,
FlacMetablockType::VorbisComment => self.pick_vorbiscomment,
FlacMetablockType::Cuesheet => self.pick_cuesheet,
FlacMetablockType::Picture => self.pick_picture,
}
}
}
enum FlacBlockType {
MagicBits {
data: [u8; 4],
left_to_read: usize,
},
MetablockHeader {
is_first: bool,
data: [u8; 4],
left_to_read: usize,
},
MetaBlock {
header: FlacMetablockHeader,
data: Vec<u8>,
},
AudioData {
data: Vec<u8>,
},
}
#[derive(Debug)]
#[expect(missing_docs)]
pub enum FlacBlock {
Streaminfo(FlacStreaminfoBlock),
Picture(FlacPictureBlock),
Padding(FlacPaddingBlock),
Application(FlacApplicationBlock),
SeekTable(FlacSeektableBlock),
VorbisComment(FlacCommentBlock),
CueSheet(FlacCuesheetBlock),
AudioFrame(FlacAudioFrame),
}
impl FlacBlock {
/// Encode this block
pub fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl Write,
) -> Result<(), FlacEncodeError> {
match self {
Self::Streaminfo(b) => b.encode(is_last, with_header, target),
Self::SeekTable(b) => b.encode(is_last, with_header, target),
Self::Picture(b) => b.encode(is_last, with_header, target),
Self::Padding(b) => b.encode(is_last, with_header, target),
Self::Application(b) => b.encode(is_last, with_header, target),
Self::VorbisComment(b) => b.encode(is_last, with_header, target),
Self::CueSheet(b) => b.encode(is_last, with_header, target),
Self::AudioFrame(b) => b.encode(target),
}
}
/// Try to decode the given data as a block
pub fn decode(block_type: FlacMetablockType, data: &[u8]) -> Result<Self, FlacDecodeError> {
Ok(match block_type {
FlacMetablockType::Streaminfo => {
FlacBlock::Streaminfo(FlacStreaminfoBlock::decode(data)?)
}
FlacMetablockType::Application => {
FlacBlock::Application(FlacApplicationBlock::decode(data)?)
}
FlacMetablockType::Cuesheet => FlacBlock::CueSheet(FlacCuesheetBlock::decode(data)?),
FlacMetablockType::Padding => FlacBlock::Padding(FlacPaddingBlock::decode(data)?),
FlacMetablockType::Picture => FlacBlock::Picture(FlacPictureBlock::decode(data)?),
FlacMetablockType::Seektable => FlacBlock::SeekTable(FlacSeektableBlock::decode(data)?),
FlacMetablockType::VorbisComment => {
FlacBlock::VorbisComment(FlacCommentBlock::decode(data)?)
}
})
}
}
/// An error produced by a [`FlacBlockReader`]
#[derive(Debug, Error)]
pub enum FlacBlockReaderError {
/// Could not decode flac data
#[error("decode error while reading flac blocks")]
DecodeError(#[from] FlacDecodeError),
/// Tried to finish or push data to a finished reader.
#[error("flac block reader is already finished")]
AlreadyFinished,
}
/// A buffered flac block reader.
/// Use `push_data` to add flac data into this struct,
/// use `pop_block` to read flac blocks.
///
/// This is the foundation of all other flac processors
/// we offer in this crate.
pub struct FlacBlockReader {
// Which blocks should we return?
selector: FlacBlockSelector,
// The block we're currently reading.
// If this is `None`, we've called `finish()`.
current_block: Option<FlacBlockType>,
// Blocks we pick go here
output_blocks: VecDeque<FlacBlock>,
}
impl FlacBlockReader {
/// Pop the next block we've read, if any.
pub fn pop_block(&mut self) -> Option<FlacBlock> {
self.output_blocks.pop_front()
}
/// If true, this reader has received all the data it needs.
pub fn is_done(&self) -> bool {
self.current_block.is_none()
}
/// If true, this reader has at least one block ready to pop.
/// Calling `pop_block` will return `Some(_)` if this is true.
pub fn has_block(&self) -> bool {
!self.output_blocks.is_empty()
}
/// Make a new [`FlacBlockReader`].
pub fn new(selector: FlacBlockSelector) -> Self {
Self {
selector,
current_block: Some(FlacBlockType::MagicBits {
data: [0; 4],
left_to_read: 4,
}),
output_blocks: VecDeque::new(),
}
}
/// Pass the given data through this block extractor.
/// Output data is stored in an internal buffer, and should be accessed
/// through `Read`.
pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> {
let mut buf = Cursor::new(buf);
let mut last_read_size = 1;
if self.current_block.is_none() {
return Err(FlacBlockReaderError::AlreadyFinished);
}
'outer: while last_read_size != 0 {
#[expect(clippy::expect_used)]
match self
.current_block
.as_mut()
.expect("current_block is Some, checked above")
{
FlacBlockType::MagicBits { data, left_to_read } => {
last_read_size = buf
.read(&mut data[4 - *left_to_read..4])
.map_err(FlacDecodeError::from)?;
*left_to_read -= last_read_size;
if *left_to_read == 0 {
if *data != [0x66, 0x4C, 0x61, 0x43] {
return Err(FlacDecodeError::BadMagicBytes.into());
};
self.current_block = Some(FlacBlockType::MetablockHeader {
is_first: true,
data: [0; 4],
left_to_read: 4,
})
}
}
FlacBlockType::MetablockHeader {
is_first,
data,
left_to_read,
} => {
last_read_size = buf
.read(&mut data[4 - *left_to_read..4])
.map_err(FlacDecodeError::from)?;
*left_to_read -= last_read_size;
if *left_to_read == 0 {
let header = FlacMetablockHeader::decode(data)?;
if *is_first && !matches!(header.block_type, FlacMetablockType::Streaminfo)
{
return Err(FlacDecodeError::BadFirstBlock.into());
}
self.current_block = Some(FlacBlockType::MetaBlock {
header,
data: Vec::new(),
})
}
}
FlacBlockType::MetaBlock { header, data } => {
#[expect(clippy::expect_used)]
{
last_read_size = buf
.by_ref()
.take(
u64::from(header.length)
- u64::try_from(data.len())
.expect("data length does not fit into u64"),
)
.read_to_end(data)
.map_err(FlacDecodeError::from)?;
}
#[expect(clippy::expect_used)]
if data.len()
== usize::try_from(header.length)
.expect("header length does not fit into usize")
{
// If we picked this block type, add it to the queue
if self.selector.should_pick_meta(header.block_type) {
let b = FlacBlock::decode(header.block_type, data)?;
self.output_blocks.push_back(b);
}
// Start next block
if header.is_last {
self.current_block = Some(FlacBlockType::AudioData { data: Vec::new() })
} else {
self.current_block = Some(FlacBlockType::MetablockHeader {
is_first: false,
data: [0; 4],
left_to_read: 4,
})
}
}
}
FlacBlockType::AudioData { data } => {
// Limit the number of bytes we read at once, so we don't re-clone
// large amounts of data if `buf` contains multiple sync sequences.
// 5kb is a pretty reasonable frame size.
last_read_size = buf
.by_ref()
.take(5_000)
.read_to_end(data)
.map_err(FlacDecodeError::from)?;
if last_read_size == 0 {
continue 'outer;
}
// We can't run checks if we don't have enough data.
if data.len() <= 2 {
continue;
}
// Check frame sync header
// (`if` makes sure we only do this once)
if data.len() - last_read_size <= 2
&& !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000)
{
return Err(FlacDecodeError::BadSyncBytes.into());
}
if data.len() >= MIN_AUDIO_FRAME_LEN {
// Look for a frame sync header in the data we read
//
// This isn't the *correct* way to split audio frames (false sync bytes can occur in audio data),
// but it's good enough for now---we don't decode audio data anyway.
//
// We could split on every sequence of sync bytes, but that's not any less wrong than the approach here.
// Also, it's slower---we'd rather have few large frames than many small ones.
let first_byte = if data.len() - last_read_size < MIN_AUDIO_FRAME_LEN {
MIN_AUDIO_FRAME_LEN + 1
} else {
data.len() - last_read_size + MIN_AUDIO_FRAME_LEN + 1
};
// `i` is the index of the first byte *after* the sync sequence.
//
// This may seem odd, but it makes the odd edge case easier to handle:
// If we instead have `i` be the index of the first byte *of* the frame sequence,
// dealing with the case where `data` contained half the sync sequence before
// reading is tricky.
for i in first_byte..data.len() {
if data[i - 2] == 0b1111_1111
&& data[i - 1] & 0b1111_1100 == 0b1111_1000
{
// We found another frame sync header. Split at this index.
if self.selector.pick_audio {
self.output_blocks.push_back(FlacBlock::AudioFrame(
FlacAudioFrame::decode(&data[0..i - 2])?,
));
}
// Backtrack to the first bit AFTER this new sync sequence
buf.seek(std::io::SeekFrom::Current(
-i64::try_from(data.len() - i)
.expect("seek offset does not fit into i64"),
))
.map_err(FlacDecodeError::from)?;
self.current_block = Some(FlacBlockType::AudioData {
data: {
let mut v = Vec::with_capacity(MIN_AUDIO_FRAME_LEN);
v.extend(&data[i - 2..i]);
v
},
});
continue 'outer;
}
}
}
}
}
}
return Ok(());
}
/// Finish reading data.
/// This tells the reader that it has received the entire stream.
///
/// `finish()` should be called exactly once once we have finished each stream.
/// Finishing twice or pushing data to a finished reader results in a panic.
pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> {
match self.current_block.take() {
None => return Err(FlacBlockReaderError::AlreadyFinished),
Some(FlacBlockType::AudioData { data }) => {
// We can't run checks if we don't have enough data.
if data.len() <= 2 {
return Err(FlacDecodeError::MalformedBlock.into());
}
if !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000) {
return Err(FlacDecodeError::BadSyncBytes.into());
}
if self.selector.pick_audio {
self.output_blocks
.push_back(FlacBlock::AudioFrame(FlacAudioFrame::decode(&data)?));
}
self.current_block = None;
return Ok(());
}
// All other blocks have a known length and
// are finished automatically.
_ => return Err(FlacDecodeError::MalformedBlock.into()),
}
}
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use paste::paste;
use rand::Rng;
use sha2::{Digest, Sha256};
use std::{io::Write, ops::Range, str::FromStr};
use super::*;
use crate::{
common::tagtype::TagType,
flac::tests::{FlacBlockOutput, FlacTestCase, VorbisCommentTestValue, manifest},
};
#[expect(clippy::unwrap_used)]
fn read_file(
test_case: &FlacTestCase,
fragment_size_range: Option<Range<usize>>,
selector: FlacBlockSelector,
) -> Result<Vec<FlacBlock>, FlacBlockReaderError> {
let file_data = std::fs::read(test_case.get_path()).unwrap();
// Make sure input file is correct
let mut hasher = Sha256::new();
hasher.update(&file_data);
assert_eq!(
test_case.get_in_hash(),
hasher.finalize().map(|x| format!("{x:02x}")).join("")
);
let mut reader = FlacBlockReader::new(selector);
let mut out_blocks = Vec::new();
// Push file data to the reader, in parts or as a whole.
if let Some(fragment_size_range) = fragment_size_range {
let mut head = 0;
while head < file_data.len() {
let mut frag_size = rand::rng().random_range(fragment_size_range.clone());
if head + frag_size > file_data.len() {
frag_size = file_data.len() - head;
}
reader.push_data(&file_data[head..head + frag_size])?;
head += frag_size;
}
} else {
reader.push_data(&file_data)?;
}
reader.finish()?;
while let Some(b) = reader.pop_block() {
out_blocks.push(b)
}
return Ok(out_blocks);
}
#[expect(clippy::unwrap_used)]
fn test_identical(
test_case: &FlacTestCase,
fragment_size_range: Option<Range<usize>>,
) -> Result<(), FlacBlockReaderError> {
let out_blocks = read_file(
test_case,
fragment_size_range,
FlacBlockSelector {
pick_streaminfo: true,
pick_padding: true,
pick_application: true,
pick_seektable: true,
pick_vorbiscomment: true,
pick_cuesheet: true,
pick_picture: true,
pick_audio: true,
},
)?;
let mut out = Vec::new();
out.write_all(&[0x66, 0x4C, 0x61, 0x43]).unwrap();
for i in 0..out_blocks.len() {
let b = &out_blocks[i];
let is_last = if i == out_blocks.len() - 1 {
false
} else {
!matches!(b, FlacBlock::AudioFrame(_))
&& matches!(&out_blocks[i + 1], FlacBlock::AudioFrame(_))
};
b.encode(is_last, true, &mut out).unwrap();
}
let mut hasher = Sha256::new();
hasher.update(&out);
let result = hasher.finalize().map(|x| format!("{x:02x}")).join("");
assert_eq!(result, test_case.get_in_hash(), "Output hash doesn't match");
return Ok(());
}
fn test_blockread(
test_case: &FlacTestCase,
fragment_size_range: Option<Range<usize>>,
) -> Result<(), FlacBlockReaderError> {
let out_blocks = read_file(
test_case,
fragment_size_range,
FlacBlockSelector {
pick_streaminfo: true,
pick_padding: true,
pick_application: true,
pick_seektable: true,
pick_vorbiscomment: true,
pick_cuesheet: true,
pick_picture: true,
pick_audio: true,
},
)?;
assert_eq!(
test_case.get_blocks().unwrap().len(),
out_blocks
.iter()
.filter(|x| !matches!(*x, FlacBlock::AudioFrame(_)))
.count(),
"Number of blocks didn't match"
);
let mut audio_data_hasher = Sha256::new();
let mut result_i = 0;
for b in out_blocks {
match b {
FlacBlock::Streaminfo(s) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Streaminfo {
min_block_size,
max_block_size,
min_frame_size,
max_frame_size,
sample_rate,
channels,
bits_per_sample,
total_samples,
md5_signature,
} => {
assert_eq!(*min_block_size, s.min_block_size,);
assert_eq!(*max_block_size, s.max_block_size);
assert_eq!(*min_frame_size, s.min_frame_size);
assert_eq!(*max_frame_size, s.max_frame_size);
assert_eq!(*sample_rate, s.sample_rate);
assert_eq!(*channels, s.channels);
assert_eq!(*bits_per_sample, s.bits_per_sample);
assert_eq!(*total_samples, s.total_samples);
assert_eq!(
*md5_signature,
s.md5_signature.iter().map(|x| format!("{x:02x}")).join("")
);
}
_ => panic!("Unexpected block type"),
},
FlacBlock::Application(a) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Application {
application_id,
hash,
} => {
assert_eq!(
*application_id, a.application_id,
"Application id doesn't match"
);
assert_eq!(
*hash,
{
let mut hasher = Sha256::new();
hasher.update(&a.data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
},
"Application content hash doesn't match"
);
}
_ => panic!("Unexpected block type"),
},
FlacBlock::CueSheet(c) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::CueSheet { hash } => {
assert_eq!(*hash, {
let mut hasher = Sha256::new();
hasher.update(&c.data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
});
}
_ => panic!("Unexpected block type"),
},
FlacBlock::Padding(p) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Padding { size } => {
assert_eq!(p.size, *size);
}
_ => panic!("Unexpected block type"),
},
FlacBlock::SeekTable(t) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Seektable { hash } => {
assert_eq!(*hash, {
let mut hasher = Sha256::new();
hasher.update(&t.data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
});
}
_ => panic!("Unexpected block type"),
},
FlacBlock::Picture(p) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Picture {
picture_type,
mime,
description,
width,
height,
bit_depth,
color_count,
img_data,
} => {
assert_eq!(*picture_type, p.picture_type, "{}", test_case.get_name());
assert_eq!(*mime, p.mime, "{}", test_case.get_name());
assert_eq!(*description, p.description, "{}", test_case.get_name());
assert_eq!(*width, p.width, "{}", test_case.get_name());
assert_eq!(*height, p.height, "{}", test_case.get_name());
assert_eq!(*bit_depth, p.bit_depth, "{}", test_case.get_name());
assert_eq!(*color_count, p.color_count, "{}", test_case.get_name());
assert_eq!(
*img_data,
{
let mut hasher = Sha256::new();
hasher.update(&p.img_data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
},
"{}",
test_case.get_name()
);
}
_ => panic!("Unexpected block type"),
},
FlacBlock::VorbisComment(v) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::VorbisComment {
vendor,
comments,
pictures,
} => {
assert_eq!(*vendor, v.comment.vendor, "Comment vendor doesn't match");
assert_eq!(
v.comment.pictures.len(),
pictures.len(),
"Number of pictures doesn't match"
);
for (p, e) in v.comment.pictures.iter().zip(*pictures) {
match e {
FlacBlockOutput::Picture {
picture_type,
mime,
description,
width,
height,
bit_depth,
color_count,
img_data,
} => {
assert_eq!(*picture_type, p.picture_type);
assert_eq!(*mime, p.mime);
assert_eq!(*description, p.description);
assert_eq!(*width, p.width);
assert_eq!(*height, p.height);
assert_eq!(*bit_depth, p.bit_depth);
assert_eq!(*color_count, p.color_count);
assert_eq!(*img_data, {
let mut hasher = Sha256::new();
hasher.update(&p.img_data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
});
}
_ => panic!("Bad test data: expected only Picture blocks."),
}
}
match comments {
VorbisCommentTestValue::Raw { tags } => {
assert_eq!(
v.comment.comments.len(),
tags.len(),
"Number of comments doesn't match"
);
for ((got_tag, got_val), (exp_tag, exp_val)) in
v.comment.comments.iter().zip(*tags)
{
assert_eq!(
*got_tag,
TagType::from_str(exp_tag).unwrap(),
"Tag key doesn't match"
);
assert_eq!(
got_val, exp_val,
"Tag value of {exp_tag} doesn't match"
);
}
}
VorbisCommentTestValue::Hash { n_comments, hash } => {
assert_eq!(
v.comment.comments.len(),
*n_comments,
"Number of comments doesn't match"
);
let mut hasher = Sha256::new();
for (got_tag, got_val) in v.comment.comments {
hasher.update(format!("{got_tag}={got_val};").as_bytes());
}
assert_eq!(
&hasher.finalize().map(|x| format!("{x:02x}")).join(""),
hash,
"Comment hash doesn't match"
);
}
}
}
_ => panic!("Unexpected block type"),
},
FlacBlock::AudioFrame(data) => {
let mut vec = Vec::new();
data.encode(&mut vec).unwrap();
audio_data_hasher.update(&vec);
if result_i != test_case.get_blocks().unwrap().len() {
panic!("There are metadata blocks between audio frames!")
}
// Don't increment result_i
continue;
}
}
result_i += 1;
}
// Check audio data hash
assert_eq!(
test_case.get_audio_hash().unwrap(),
audio_data_hasher
.finalize()
.map(|x| format!("{x:02x}"))
.join("")
);
return Ok(());
}
// Helper macros to generate tests
macro_rules! gen_tests {
( $test_name:ident ) => {
paste! {
#[test]
pub fn [<blockread_small_ $test_name>]() {
let manifest = manifest();
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
match test_case {
FlacTestCase::Success { .. } => {
for _ in 0..5 {
test_blockread(
test_case,
Some(1..256),
).unwrap()
}
},
FlacTestCase::Error { check_error, .. } => {
let e = test_blockread(test_case, Some(1..256)).unwrap_err();
match e {
FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
_ => panic!("Unexpected error {e:?}")
}
}
}
}
#[test]
pub fn [<identical_small_ $test_name>]() {
let manifest = manifest();
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
match test_case {
FlacTestCase::Success { .. } => {
for _ in 0..5 {
test_identical(
test_case,
Some(1..256),
).unwrap()
}
},
FlacTestCase::Error { check_error, .. } => {
let e = test_identical(test_case, Some(1..256)).unwrap_err();
match e {
FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
_ => panic!("Unexpected error {e:?}")
}
}
}
}
}
};
}
gen_tests!(custom_01);
gen_tests!(custom_02);
gen_tests!(custom_03);
gen_tests!(uncommon_10);
gen_tests!(faulty_06);
gen_tests!(faulty_07);
gen_tests!(faulty_10);
gen_tests!(faulty_11);
gen_tests!(subset_45);
gen_tests!(subset_46);
gen_tests!(subset_47);
gen_tests!(subset_48);
gen_tests!(subset_49);
gen_tests!(subset_50);
gen_tests!(subset_51);
gen_tests!(subset_52);
gen_tests!(subset_53);
gen_tests!(subset_54);
gen_tests!(subset_55);
gen_tests!(subset_56);
gen_tests!(subset_57);
gen_tests!(subset_58);
gen_tests!(subset_59);
}

View File

@@ -1,943 +0,0 @@
//! Parse FLAC metadata.
pub mod blockread;
pub mod blocks;
pub mod errors;
pub mod proc;
#[cfg(test)]
mod tests {
use std::str::FromStr;
use itertools::Itertools;
use mime::Mime;
use super::errors::FlacDecodeError;
use crate::common::{picturetype::PictureType, vorbiscomment::VorbisCommentDecodeError};
/// The value of a vorbis comment.
///
/// Some files have VERY large comments, and providing them
/// explicitly here doesn't make sense.
#[derive(Clone)]
pub enum VorbisCommentTestValue {
/// The comments, in order
Raw {
tags: &'static [(&'static str, &'static str)],
},
/// The hash of all comments concatenated together,
/// stringified as `{key}={value};`
Hash {
n_comments: usize,
hash: &'static str,
},
}
#[derive(Clone)]
pub enum FlacBlockOutput {
Application {
application_id: u32,
hash: &'static str,
},
Streaminfo {
min_block_size: u32,
max_block_size: u32,
min_frame_size: u32,
max_frame_size: u32,
sample_rate: u32,
channels: u8,
bits_per_sample: u8,
total_samples: u128,
md5_signature: &'static str,
},
CueSheet {
// Hash of this block's data, without the header.
// This is easy to get with
//
// ```notrust
// metaflac \
// --list \
// --block-number=<n> \
// --data-format=binary-headerless \
// <file> \
// | sha256sum
//```
hash: &'static str,
},
Seektable {
hash: &'static str,
},
Padding {
size: u32,
},
Picture {
picture_type: PictureType,
mime: Mime,
description: &'static str,
width: u32,
height: u32,
bit_depth: u32,
color_count: u32,
img_data: &'static str,
},
VorbisComment {
vendor: &'static str,
comments: VorbisCommentTestValue,
pictures: &'static [FlacBlockOutput],
},
}
pub enum FlacTestCase {
Success {
/// This test's name
test_name: &'static str,
/// The file to use for this test
file_path: &'static str,
/// The hash of the input files
in_hash: &'static str,
/// The flac metablocks we expect to find in this file, in order.
blocks: Vec<FlacBlockOutput>,
/// The hash of the audio frames in this file
///
/// Get this hash by running `metaflac --remove-all --dont-use-padding`,
/// then by manually deleting remaining headers in a hex editor
/// (Remember that the sync sequence is 0xFF 0xF8)
audio_hash: &'static str,
/// The hash we should get when we strip this file's tags.
///
/// A stripped flac file has unmodified STREAMINFO, SEEKTABLE,
/// CUESHEET, and audio data blocks; and nothing else (not even padding).
///
/// Reference implementation:
/// ```notrust
/// metaflac \
/// --remove \
/// --block-type=PADDING,APPLICATION,VORBIS_COMMENT,PICTURE \
/// --dont-use-padding \
/// <file>
/// ```
stripped_hash: &'static str,
},
Error {
/// This test's name
test_name: &'static str,
/// The file to use for this test
file_path: &'static str,
/// The hash of the input files
in_hash: &'static str,
/// The error we should encounter while reading this file
check_error: &'static dyn Fn(&FlacDecodeError) -> bool,
/// If some, stripping this file's metadata should produce the given hash.
/// If none, trying to strip metadata should produce `check_error`
stripped_hash: Option<&'static str>,
/// If some, the following images should be extracted from this file
/// If none, trying to strip images should produce `check_error`
pictures: Option<Vec<FlacBlockOutput>>,
},
}
impl FlacTestCase {
pub fn get_name(&self) -> &str {
match self {
Self::Error { test_name, .. } | Self::Success { test_name, .. } => test_name,
}
}
pub fn get_path(&self) -> &str {
match self {
Self::Success { file_path, .. } | Self::Error { file_path, .. } => file_path,
}
}
pub fn get_in_hash(&self) -> &str {
match self {
Self::Success { in_hash, .. } | Self::Error { in_hash, .. } => in_hash,
}
}
pub fn get_stripped_hash(&self) -> Option<&str> {
match self {
Self::Success { stripped_hash, .. } => Some(stripped_hash),
Self::Error { stripped_hash, .. } => *stripped_hash,
}
}
pub fn get_audio_hash(&self) -> Option<&str> {
match self {
Self::Success { audio_hash, .. } => Some(audio_hash),
_ => None,
}
}
pub fn get_blocks(&self) -> Option<&[FlacBlockOutput]> {
match self {
Self::Success { blocks, .. } => Some(blocks),
_ => None,
}
}
pub fn get_pictures(&self) -> Option<Vec<FlacBlockOutput>> {
match self {
Self::Success { blocks, .. } => {
let mut out = Vec::new();
for b in blocks {
match b {
FlacBlockOutput::Picture { .. } => out.push(b.clone()),
FlacBlockOutput::VorbisComment { pictures, .. } => {
for p in *pictures {
out.push(p.clone())
}
}
_ => {}
}
}
return Some(out);
}
Self::Error { pictures, .. } => {
pictures.as_ref().map(|x| x.iter().cloned().collect())
}
}
}
}
/// A list of test files and their expected output
#[expect(clippy::unwrap_used)]
pub fn manifest() -> [FlacTestCase; 23] {
[
FlacTestCase::Error {
test_name: "uncommon_10",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_uncommon/10 - file starting at frame header.flac"
),
in_hash: "d95f63e8101320f5ac7ffe249bc429a209eb0e10996a987301eaa63386a8faa1",
check_error: &|x| matches!(x, FlacDecodeError::BadMagicBytes),
stripped_hash: None,
pictures: None,
},
FlacTestCase::Error {
test_name: "faulty_06",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac"
),
in_hash: "53aed5e7fde7a652b82ba06a8382b2612b02ebbde7b0d2016276644d17cc76cd",
check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock),
stripped_hash: None,
pictures: None,
},
FlacTestCase::Error {
test_name: "faulty_07",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac"
),
in_hash: "6d46725991ba5da477187fde7709ea201c399d00027257c365d7301226d851ea",
check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock),
stripped_hash: None,
pictures: None,
},
FlacTestCase::Error {
test_name: "faulty_10",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac"
),
in_hash: "c79b0514a61634035a5653c5493797bbd1fcc78982116e4d429630e9e462d29b",
check_error: &|x| {
matches!(
x,
FlacDecodeError::VorbisComment(VorbisCommentDecodeError::MalformedData)
)
},
// This file's vorbis comment is invalid, but that shouldn't stop us from removing it.
// As a general rule, we should NOT encounter an error when stripping invalid blocks.
//
// We should, however, get errors when we try to strip flac files with invalid *structure*
// (For example, the out-of-order streaminfo test in faulty_07).
stripped_hash: Some(
"4b994f82dc1699a58e2b127058b37374220ee41dc294d4887ac14f056291a1b0",
),
pictures: None,
},
FlacTestCase::Error {
test_name: "faulty_11",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_faulty/11 - incorrect metadata block length.flac"
),
in_hash: "3732151ba8c4e66a785165aa75a444aad814c16807ddc97b793811376acacfd6",
check_error: &|x| matches!(x, FlacDecodeError::BadMetablockType(127)),
stripped_hash: None,
pictures: None,
},
FlacTestCase::Success {
test_name: "subset_45",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/45 - no total number of samples set.flac"
),
in_hash: "336a18eb7a78f7fc0ab34980348e2895bc3f82db440a2430d9f92e996f889f9a",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 907,
max_frame_size: 8053,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 0,
md5_signature: "c41ae3b82c35d8f5c3dab1729f948fde",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
],
audio_hash: "3fb3482ebc1724559bdd57f34de472458563d78a676029614e76e32b5d2b8816",
stripped_hash: "31631ac227ebe2689bac7caa1fa964b47e71a9f1c9c583a04ea8ebd9371508d0",
},
FlacTestCase::Success {
test_name: "subset_46",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/46 - no min-max framesize set.flac"
),
in_hash: "9dc39732ce17815832790901b768bb50cd5ff0cd21b28a123c1cabc16ed776cc",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 0,
max_frame_size: 0,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 282866,
md5_signature: "fd131e6ebc75251ed83f8f4c07df36a4",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
],
audio_hash: "a1eed422462b386a932b9eb3dff3aea3687b41eca919624fb574aadb7eb50040",
stripped_hash: "9e57cd77f285fc31f87fa4e3a31ab8395d68d5482e174c8e0d0bba9a0c20ba27",
},
FlacTestCase::Success {
test_name: "subset_47",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/47 - only STREAMINFO.flac"
),
in_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85",
blocks: vec![FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 4747,
max_frame_size: 7034,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 232608,
md5_signature: "bba30c5f70789910e404b7ac727c3853",
}],
audio_hash: "5ee1450058254087f58c91baf0f70d14bde8782cf2dc23c741272177fe0fce6e",
stripped_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85",
},
FlacTestCase::Success {
test_name: "subset_48",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac"
),
in_hash: "4417aca6b5f90971c50c28766d2f32b3acaa7f9f9667bd313336242dae8b2531",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 2445,
max_frame_size: 7364,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 259884,
md5_signature: "97a0574290237563fbaa788ad77d2cdf",
},
FlacBlockOutput::Seektable {
hash: "21ca2184ae22fe26b690fd7cbd8d25fcde1d830ff6e5796ced4107bab219d7c0",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
],
audio_hash: "c2d691f2c4c986fe3cd5fd7864d9ba9ce6dd68a4ffc670447f008434b13102c2",
stripped_hash: "abc9a0c40a29c896bc6e1cc0b374db1c8e157af716a5a3c43b7db1591a74c4e8",
},
FlacTestCase::Success {
test_name: "subset_49",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/49 - Extremely large PADDING.flac",
),
in_hash: "7bc44fa2754536279fde4f8fb31d824f43b8d0b3f93d27d055d209682914f20e",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 1353,
max_frame_size: 7117,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 258939,
md5_signature: "6e78f221caaaa5d570a53f1714d84ded",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Padding { size: 16777215 },
],
audio_hash: "5007be7109b28b0149d1b929d2a0e93a087381bd3e68cf2a3ef78ea265ea20c3",
stripped_hash: "a2283bbacbc4905ad3df1bf9f43a0ea7aa65cf69523d84a7dd8eb54553cc437e",
},
FlacTestCase::Success {
test_name: "subset_50",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/50 - Extremely large PICTURE.flac"
),
in_hash: "1f04f237d74836104993a8072d4223e84a5d3bd76fbc44555c221c7e69a23594",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 5099,
max_frame_size: 7126,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 265617,
md5_signature: "82164e4da30ed43b47e6027cef050648",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_JPEG,
description: "",
width: 3200,
height: 2252,
bit_depth: 24,
color_count: 0,
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
},
],
audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c",
stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c",
},
FlacTestCase::Success {
test_name: "subset_51",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac"
),
in_hash: "033160e8124ed287b0b5d615c94ac4139477e47d6e4059b1c19b7141566f5ef9",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 4531,
max_frame_size: 7528,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 289972,
md5_signature: "5ff622c88f8dd9bc201a6a541f3890d3",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Hash {
n_comments: 39,
hash: "01984e9ec0cfad41f27b3b4e84184966f6725ead84b7815bd0b3313549ee4229",
},
pictures: &[],
},
],
audio_hash: "76419865d10eb22a74f020423a4e515e800f0177441676afd0418557c2d76c36",
stripped_hash: "c0ca6c6099b5d9ec53d6bb370f339b2b1570055813a6cd3616fac2db83a2185e",
},
FlacTestCase::Success {
test_name: "subset_52",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/52 - Extremely large APPLICATION.flac"
),
in_hash: "0e45a4f8dbef15cbebdd8dfe690d8ae60e0c6abb596db1270a9161b62a7a3f1c",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 3711,
max_frame_size: 7056,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 317876,
md5_signature: "eb7140266bc194527488c21ab49bc47b",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Application {
application_id: 0x74657374,
hash: "cfc0b8969e4ba6bd507999ba89dea2d274df69d94749d6ae3cf117a7780bba09",
},
],
audio_hash: "89ad1a5c86a9ef35d33189c81c8a90285a23964a13f8325bf2c02043e8c83d63",
stripped_hash: "cc4a0afb95ec9bcde8ee33f13951e494dc4126a9a3a668d79c80ce3c14a3acd9",
},
FlacTestCase::Success {
test_name: "subset_53",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac"
),
in_hash: "513fad18578f3225fae5de1bda8f700415be6fd8aa1e7af533b5eb796ed2d461",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 2798,
max_frame_size: 7408,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 2910025,
md5_signature: "d11f3717d628cfe6a90a10facc478340",
},
FlacBlockOutput::Seektable {
hash: "18629e1b874cb27e4364da72fb3fec2141eb0618baae4a1cee6ed09562aa00a8",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::CueSheet {
hash: "70638a241ca06881a52c0a18258ea2d8946a830137a70479c49746d2a1344bdd",
},
],
audio_hash: "e993070f2080f2c598be1d61d208e9187a55ddea4be1d2ed1f8043e7c03e97a5",
stripped_hash: "57c5b945e14c6fcd06916d6a57e5b036d67ff35757893c24ed872007aabbcf4b",
},
FlacTestCase::Success {
test_name: "subset_54",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac"
),
in_hash: "b68dc6644784fac35aa07581be8603a360d1697e07a2265d7eb24001936fd247",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 1694,
max_frame_size: 7145,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 433151,
md5_signature: "1d950e92b357dedbc5290a7f2210a2ef",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Hash {
n_comments: 20000,
hash: "433f34ae532d265835153139b1db79352a26ad0d3b03e2f1a1b88ada34abfc77",
},
pictures: &[],
},
],
audio_hash: "4721b784058410c6263f73680079e9a71aee914c499afcf5580c121fce00e874",
stripped_hash: "5c8b92b83c0fa17821add38263fa323d1c66cfd2ee57aca054b50bd05b9df5c2",
},
FlacTestCase::Success {
test_name: "subset_55",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/55 - file 48-53 combined.flac"
),
in_hash: "a756b460df79b7cc492223f80cda570e4511f2024e5fa0c4d505ba51b86191f6",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 3103,
max_frame_size: 11306,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 2646000,
md5_signature: "2c78978cbbff11daac296fee97c3e061",
},
FlacBlockOutput::Seektable {
hash: "58dfa7bac4974edf1956b068f5aa72d1fbd9301c36a3085a8a57b9db11a2dbf0",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.3 20190804",
comments: VorbisCommentTestValue::Hash {
n_comments: 40036,
hash: "66cac9f9c42f48128e9fc24e1e96b46a06e885d233155556da16d9b05a23486e",
},
pictures: &[],
},
FlacBlockOutput::CueSheet {
hash: "db11916c8f5f39648256f93f202e00ff8d73d7d96b62f749b4c77cf3ea744f90",
},
FlacBlockOutput::Application {
application_id: 0x74657374,
hash: "6088a557a1bad7bfa5ebf79a324669fbf4fa2f8e708f5487305dfc5b2ff2249a",
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_JPEG,
description: "",
width: 3200,
height: 2252,
bit_depth: 24,
color_count: 0,
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
},
FlacBlockOutput::Padding { size: 16777215 },
],
audio_hash: "f1285b77cec7fa9a0979033244489a9d06b8515b2158e9270087a65a4007084d",
stripped_hash: "401038fce06aff5ebdc7a5f2fc01fa491cbf32d5da9ec99086e414b2da3f8449",
},
FlacTestCase::Success {
test_name: "subset_56",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/56 - JPG PICTURE.flac"
),
in_hash: "5cebe7a3710cf8924bd2913854e9ca60b4cd53cfee5a3af0c3c73fddc1888963",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 3014,
max_frame_size: 7219,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 220026,
md5_signature: "5b0e898d9c2626d0c28684f5a586813f",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_JPEG,
description: "",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 0,
img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6",
},
],
audio_hash: "ccfe90b0f15cd9662f7a18f40cd4c347538cf8897a08228e75351206f7804573",
stripped_hash: "31a38d59db2010790b7abf65ec0cc03f2bbe1fed5952bc72bee4ca4d0c92e79f",
},
FlacTestCase::Success {
test_name: "subset_57",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/57 - PNG PICTURE.flac"
),
in_hash: "c6abff7f8bb63c2821bd21dd9052c543f10ba0be878e83cb419c248f14f72697",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 463,
max_frame_size: 6770,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 221623,
md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_PNG,
description: "",
width: 960,
height: 540,
bit_depth: 24,
color_count: 0,
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
},
],
audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f",
stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
},
FlacTestCase::Success {
test_name: "subset_58",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/58 - GIF PICTURE.flac"
),
in_hash: "7c2b1a963a665847167a7275f9924f65baeb85c21726c218f61bf3f803f301c8",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 2853,
max_frame_size: 6683,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 219826,
md5_signature: "7c1810602a7db96d7a48022ac4aa495c",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_GIF,
description: "",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 32,
img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1",
},
],
audio_hash: "30e3292e9f56cf88658eeadfdec8ad3a440690ce6d813e1b3374f60518c8e0ae",
stripped_hash: "4cd771e27870e2a586000f5b369e0426183a521b61212302a2f5802b046910b2",
},
FlacTestCase::Success {
test_name: "subset_59",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/59 - AVIF PICTURE.flac"
),
in_hash: "7395d02bf8d9533dc554cce02dee9de98c77f8731a45f62d0a243bd0d6f9a45c",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 153,
max_frame_size: 7041,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 221423,
md5_signature: "d354246011ca204159c06f52cad5f634",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: Mime::from_str("image/avif").unwrap(),
description: "",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 0,
img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc",
},
],
audio_hash: "b208c73d274e65b27232bfffbfcbcf4805ee3cbc9cfbf7d2104db8f53370273b",
stripped_hash: "d5215e16c6b978fc2c3e6809e1e78981497cb8514df297c5169f3b4a28fd875c",
},
FlacTestCase::Success {
test_name: "custom_01",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_custom/01 - many images.flac"
),
in_hash: "8a5df37488866cd91ac16773e549ef4e3a85d9f88a0d9d345f174807bb536b96",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 5099,
max_frame_size: 7126,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 265617,
md5_signature: "82164e4da30ed43b47e6027cef050648",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_PNG,
description: "",
width: 960,
height: 540,
bit_depth: 24,
color_count: 0,
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
}],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_JPEG,
description: "",
width: 3200,
height: 2252,
bit_depth: 24,
color_count: 0,
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
},
FlacBlockOutput::Picture {
picture_type: PictureType::ABrightColoredFish,
mime: mime::IMAGE_JPEG,
description: "lorem",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 0,
img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6",
},
FlacBlockOutput::Picture {
picture_type: PictureType::OtherFileIcon,
mime: mime::IMAGE_PNG,
description: "ipsum",
width: 960,
height: 540,
bit_depth: 24,
color_count: 0,
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
},
FlacBlockOutput::Picture {
picture_type: PictureType::Lyricist,
mime: mime::IMAGE_GIF,
description: "dolor",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 32,
img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1",
},
FlacBlockOutput::Picture {
picture_type: PictureType::BackCover,
mime: Mime::from_str("image/avif").unwrap(),
description: "est",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 0,
img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc",
},
],
audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c",
stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c",
},
FlacTestCase::Success {
test_name: "custom_02",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_custom/02 - picture in vorbis comment.flac"
),
in_hash: "f6bb1a726fe6a3e25a4337d36e29fdced8ff01a46d627b7c2e1988c88f461f8c",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 463,
max_frame_size: 6770,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 221623,
md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_PNG,
description: "",
width: 960,
height: 540,
bit_depth: 24,
color_count: 0,
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
}],
},
],
audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f",
stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
},
FlacTestCase::Error {
test_name: "custom_03",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac"
),
in_hash: "7177f0ae4f04a563292be286ec05967f81ab16eb0a28b70fc07a1e47da9cafd0",
check_error: &|x| {
matches!(
x,
FlacDecodeError::VorbisComment(VorbisCommentDecodeError::MalformedPicture)
)
},
stripped_hash: Some(
"3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
),
pictures: None,
},
]
}
#[test]
fn manifest_sanity_check() {
assert!(manifest().iter().map(|x| x.get_name()).all_unique());
assert!(manifest().iter().map(|x| x.get_path()).all_unique());
}
}

View File

@@ -1,213 +0,0 @@
//! A flac processor that strips metadata blocks from flac files
use std::io::Write;
use super::super::{
blockread::{FlacBlock, FlacBlockReader, FlacBlockReaderError, FlacBlockSelector},
errors::FlacEncodeError,
};
/// Removes all metadata from a flac file
pub struct FlacMetaStrip {
reader: FlacBlockReader,
/// The last block that `reader` produced.
///
/// We need this to detect the last metadata block
/// that `reader` produces.
last_block: Option<FlacBlock>,
/// Set to `false` on the first call to `self.write_data`.
/// Used to write fLaC magic bytes.
first_write: bool,
}
impl FlacMetaStrip {
/// Make a new [`FlacMetaStrip`]
pub fn new() -> Self {
Self {
first_write: true,
last_block: None,
reader: FlacBlockReader::new(FlacBlockSelector {
pick_streaminfo: true,
pick_padding: false,
pick_application: false,
pick_seektable: true,
pick_vorbiscomment: false,
pick_cuesheet: true,
pick_picture: false,
pick_audio: true,
}),
}
}
/// Push some data to this flac processor
pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> {
self.reader.push_data(buf)
}
/// Call after sending the entire flac file to this reader
pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> {
self.reader.finish()
}
/// If true, we have received all the data we need
pub fn is_done(&mut self) -> bool {
self.reader.is_done()
}
/// If false, this reader has sent all its data.
///
/// Note that `read_data` may write zero bytes if this method returns `true`.
/// If `has_data` is false, we don't AND WON'T have data. If we're waiting
/// for data, this is `true`.
pub fn has_data(&self) -> bool {
self.last_block.is_some() || !self.reader.is_done() || self.reader.has_block()
}
/// Write available data from this struct into `target`
pub fn read_data(&mut self, target: &mut impl Write) -> Result<(), FlacEncodeError> {
if self.first_write {
target.write_all(&[0x66, 0x4C, 0x61, 0x43])?;
self.first_write = false;
}
while let Some(block) = self.reader.pop_block() {
if let Some(last_block) = self.last_block.take() {
last_block.encode(
// The last metadata block is the only one followed by an audio frame
!matches!(last_block, FlacBlock::AudioFrame(_))
&& matches!(block, FlacBlock::AudioFrame(_)),
true,
target,
)?;
}
self.last_block = Some(block);
}
// We don't need to store audioframes in our last_block buffer,
// since they do not have an `is_last` flag.
if matches!(self.last_block, Some(FlacBlock::AudioFrame(_))) {
#[expect(clippy::expect_used)]
let x = self
.last_block
.take()
.expect("last_block is Some(AudioFrame), just matched");
x.encode(false, true, target)?;
}
return Ok(());
}
}
#[cfg(test)]
mod tests {
use paste::paste;
use rand::Rng;
use sha2::{Digest, Sha256};
use crate::flac::{
blockread::FlacBlockReaderError, proc::metastrip::FlacMetaStrip, tests::FlacTestCase,
tests::manifest,
};
#[expect(clippy::unwrap_used)]
fn test_strip(
test_case: &FlacTestCase,
fragment_size_range: Option<std::ops::Range<usize>>,
) -> Result<(), FlacBlockReaderError> {
let file_data = std::fs::read(test_case.get_path()).unwrap();
// Make sure input file is correct
let mut hasher = Sha256::new();
hasher.update(&file_data);
assert_eq!(
test_case.get_in_hash(),
hasher.finalize().map(|x| format!("{x:02x}")).join("")
);
let mut strip = FlacMetaStrip::new();
// Push file data to the reader, in parts or as a whole.
if let Some(fragment_size_range) = fragment_size_range {
let mut head = 0;
while head < file_data.len() {
let mut frag_size = rand::rng().random_range(fragment_size_range.clone());
if head + frag_size > file_data.len() {
frag_size = file_data.len() - head;
}
strip.push_data(&file_data[head..head + frag_size])?;
head += frag_size;
}
} else {
strip.push_data(&file_data)?;
}
strip.finish()?;
let mut out_data = Vec::new();
strip.read_data(&mut out_data).unwrap();
assert!(!strip.has_data());
let mut hasher = Sha256::new();
hasher.update(&out_data);
let result = hasher.finalize().map(|x| format!("{x:02x}")).join("");
assert_eq!(
result,
test_case.get_stripped_hash().unwrap(),
"Stripped FLAC hash doesn't match"
);
return Ok(());
}
macro_rules! gen_tests {
( $test_name:ident ) => {
paste! {
#[test]
pub fn [<strip_ $test_name>]() {
let manifest =manifest();
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
match test_case {
FlacTestCase::Error { stripped_hash: Some(_), .. } |
FlacTestCase::Success { .. } => {
for _ in 0..5 {
test_strip(
test_case,
Some(5_000..100_000),
).unwrap()
}
},
FlacTestCase::Error { check_error, .. } => {
let e = test_strip(test_case, Some(5_000..100_000)).unwrap_err();
match e {
FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
_ => panic!("Unexpected error {e:?}")
}
}
}
}
}
};
}
gen_tests!(custom_01);
gen_tests!(custom_02);
gen_tests!(custom_03);
gen_tests!(uncommon_10);
gen_tests!(faulty_06);
gen_tests!(faulty_07);
gen_tests!(faulty_10);
gen_tests!(faulty_11);
gen_tests!(subset_45);
gen_tests!(subset_47);
gen_tests!(subset_54);
gen_tests!(subset_55);
gen_tests!(subset_57);
}

View File

@@ -1,5 +0,0 @@
//! Flac processors. These are well-tested wrappers around [`crate::flac::blockread::FlacBlockReader`]
//! that are specialized for specific tasks.
pub mod metastrip;
pub mod pictures;

View File

@@ -1,229 +0,0 @@
//! A flac processor that finds all images inside a flac file
use std::collections::VecDeque;
use super::super::{
blockread::{FlacBlock, FlacBlockReader, FlacBlockReaderError, FlacBlockSelector},
blocks::FlacPictureBlock,
};
/// Find all pictures in a flac file,
/// in both picture metablocks and vorbis comments.
pub struct FlacPictureReader {
reader: FlacBlockReader,
pictures: VecDeque<FlacPictureBlock>,
}
impl FlacPictureReader {
/// Make a new [`FlacMetaStrip`]
pub fn new() -> Self {
Self {
pictures: VecDeque::new(),
reader: FlacBlockReader::new(FlacBlockSelector {
pick_streaminfo: false,
pick_padding: false,
pick_application: false,
pick_seektable: false,
pick_vorbiscomment: true,
pick_cuesheet: false,
pick_picture: true,
pick_audio: false,
}),
}
}
/// Push some data to this flac processor
pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> {
self.reader.push_data(buf)?;
while let Some(b) = self.reader.pop_block() {
match b {
FlacBlock::Picture(p) => self.pictures.push_back(p),
FlacBlock::VorbisComment(c) => {
for p in c.comment.pictures {
self.pictures.push_back(p)
}
}
_ => unreachable!(),
}
}
return Ok(());
}
/// Call after sending the entire flac file to this reader
pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> {
self.reader.finish()
}
/// If true, we have received all the data we need
pub fn is_done(&mut self) -> bool {
self.reader.is_done()
}
/// If false, this reader has sent all its data.
///
/// Note that `read_data` may write zero bytes if this method returns `true`.
/// If `has_data` is false, we don't AND WON'T have data. If we're waiting
/// for data, this is `true`.
pub fn has_data(&self) -> bool {
!self.reader.is_done() || self.reader.has_block() || !self.pictures.is_empty()
}
/// Pop the next picture we read from this file, if any.
pub fn pop_picture(&mut self) -> Option<FlacPictureBlock> {
self.pictures.pop_front()
}
}
#[cfg(test)]
mod tests {
use paste::paste;
use rand::Rng;
use sha2::{Digest, Sha256};
use crate::flac::{
blockread::FlacBlockReaderError,
proc::pictures::FlacPictureReader,
tests::{FlacBlockOutput, FlacTestCase, manifest},
};
#[expect(clippy::unwrap_used)]
fn test_pictures(
test_case: &FlacTestCase,
fragment_size_range: Option<std::ops::Range<usize>>,
) -> Result<(), FlacBlockReaderError> {
let file_data = std::fs::read(test_case.get_path()).unwrap();
// Make sure input file is correct
let mut hasher = Sha256::new();
hasher.update(&file_data);
assert_eq!(
test_case.get_in_hash(),
hasher.finalize().map(|x| format!("{x:02x}")).join("")
);
let mut pic = FlacPictureReader::new();
// Push file data to the reader, in parts or as a whole.
if let Some(fragment_size_range) = fragment_size_range {
let mut head = 0;
while head < file_data.len() {
let mut frag_size = rand::rng().random_range(fragment_size_range.clone());
if head + frag_size > file_data.len() {
frag_size = file_data.len() - head;
}
pic.push_data(&file_data[head..head + frag_size])?;
head += frag_size;
}
} else {
pic.push_data(&file_data)?;
}
pic.finish()?;
let mut out = Vec::new();
while let Some(p) = pic.pop_picture() {
out.push(p);
}
let out_pictures = test_case.get_pictures().unwrap();
assert_eq!(
out.len(),
out_pictures.len(),
"Unexpected number of pictures"
);
for (got, expected) in out.iter().zip(out_pictures) {
let (picture_type, mime, description, width, height, bit_depth, color_count, img_data) =
match expected {
FlacBlockOutput::Picture {
picture_type,
mime,
description,
width,
height,
bit_depth,
color_count,
img_data,
} => (
picture_type,
mime,
description,
width,
height,
bit_depth,
color_count,
img_data,
),
_ => unreachable!(),
};
assert_eq!(picture_type, got.picture_type, "{}", test_case.get_name());
assert_eq!(mime, got.mime, "{}", test_case.get_name());
assert_eq!(*description, got.description, "{}", test_case.get_name());
assert_eq!(width, got.width, "{}", test_case.get_name());
assert_eq!(height, got.height, "{}", test_case.get_name());
assert_eq!(bit_depth, got.bit_depth, "{}", test_case.get_name());
assert_eq!(color_count, got.color_count, "{}", test_case.get_name());
assert_eq!(
*img_data,
{
let mut hasher = Sha256::new();
hasher.update(&got.img_data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
},
"{}",
test_case.get_name()
);
}
return Ok(());
}
macro_rules! gen_tests {
( $test_name:ident ) => {
paste! {
#[test]
pub fn [<pictures_ $test_name>]() {
let manifest = manifest();
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
match test_case {
FlacTestCase::Error { pictures: Some(_), .. } |
FlacTestCase::Success { .. } => {
for _ in 0..5 {
test_pictures(
test_case,
Some(5_000..100_000),
).unwrap()
}
},
FlacTestCase::Error { check_error, .. } => {
let e = test_pictures(test_case, Some(5_000..100_000)).unwrap_err();
match e {
FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
_ => panic!("Unexpected error {e:?}")
}
}
}
}
}
};
}
gen_tests!(custom_01);
gen_tests!(custom_02);
gen_tests!(custom_03);
gen_tests!(subset_47);
gen_tests!(subset_50);
gen_tests!(subset_55);
gen_tests!(subset_56);
gen_tests!(subset_57);
gen_tests!(subset_58);
gen_tests!(subset_59);
}

View File

@@ -1,3 +0,0 @@
//! Read and write audio file metadata.
pub mod common;
pub mod flac;

View File

@@ -1,112 +0,0 @@
use crate::flac::proc::pictures::FlacPictureReader;
use std::{collections::BTreeMap, sync::Arc};
use tracing::{debug, trace};
pub struct ExtractCovers {}
impl NodeBuilder for ExtractCovers {
fn build<'ctx>(&self) -> Box<dyn Node<'ctx>> {
Box::new(Self {})
}
}
// Inputs: "data" - Bytes
#[async_trait]
impl<'ctx> Node<'ctx> for ExtractCovers {
async fn run(
&self,
ctx: &CopperContext<'ctx>,
this_node: ThisNodeInfo,
params: NodeParameters,
mut input: BTreeMap<PortName, Option<PipeData>>,
) -> Result<BTreeMap<PortName, PipeData>, RunNodeError> {
//
// Extract parameters
//
params.err_if_not_empty()?;
//
// Extract arguments
//
let data = input.remove(&PortName::new("data"));
if data.is_none() {
return Err(RunNodeError::MissingInput {
port: PortName::new("data"),
});
}
if let Some((port, _)) = input.pop_first() {
return Err(RunNodeError::UnrecognizedInput { port });
}
trace!(
message = "Inputs ready, preparing reader",
node_id = ?this_node.id
);
let mut reader = match data.unwrap() {
None => {
return Err(RunNodeError::RequiredInputNull {
port: PortName::new("data"),
});
}
Some(PipeData::Blob { source, .. }) => source.build(ctx).await?,
_ => {
return Err(RunNodeError::BadInputType {
port: PortName::new("data"),
});
}
};
//
// Setup is done, extract covers
//
debug!(
message = "Extracting covers",
node_id = ?this_node.id
);
let mut picreader = FlacPictureReader::new();
while let Some(data) = reader.next_fragment().await? {
picreader
.push_data(&data)
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
}
picreader
.finish()
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
//
// Send the first cover we find
//
let mut output = BTreeMap::new();
if let Some(picture) = picreader.pop_picture() {
debug!(
message = "Found a cover, sending",
node_id = ?this_node.id,
picture = ?picture
);
output.insert(
PortName::new("cover_data"),
PipeData::Blob {
source: BytesProcessorBuilder::new(RawBytesSource::Array {
mime: picture.mime.clone(),
data: Arc::new(picture.img_data),
}),
},
);
} else {
debug!(
message = "Did not find a cover, sending None",
node_id = ?this_node.id
);
}
return Ok(output);
}
}

View File

@@ -1,164 +0,0 @@
use crate::{
common::tagtype::TagType,
flac::blockread::{FlacBlock, FlacBlockReader, FlacBlockSelector},
};
use async_trait::async_trait;
use copper_piper::{
base::{Node, NodeBuilder, NodeParameterValue, PortName, RunNodeError, ThisNodeInfo},
data::PipeData,
helpers::NodeParameters,
CopperContext,
};
use std::{collections::BTreeMap, sync::Arc};
use tracing::{debug, trace};
/// Extract tags from audio metadata
pub struct ExtractTags {}
impl NodeBuilder for ExtractTags {
fn build<'ctx>(&self) -> Box<dyn Node<'ctx>> {
Box::new(Self {})
}
}
// Inputs: "data" - Bytes
// Outputs: variable, depends on tags
#[async_trait]
impl<'ctx> Node<'ctx> for ExtractTags {
async fn run(
&self,
ctx: &CopperContext<'ctx>,
this_node: ThisNodeInfo,
mut params: NodeParameters,
mut input: BTreeMap<PortName, Option<PipeData>>,
) -> Result<BTreeMap<PortName, PipeData>, RunNodeError> {
//
// Extract parameters
//
let tags = {
let mut tags: BTreeMap<PortName, TagType> = BTreeMap::new();
let val = params.pop_val("tags")?;
match val {
NodeParameterValue::List(list) => {
for t in list {
match t {
NodeParameterValue::String(s) => {
tags.insert(PortName::new(s.as_str()), s.as_str().into());
}
_ => {
return Err(RunNodeError::BadParameterType {
parameter: "tags".into(),
})
}
}
}
}
_ => {
return Err(RunNodeError::BadParameterType {
parameter: "tags".into(),
})
}
};
tags
};
params.err_if_not_empty()?;
//
// Extract arguments
//
let data = input.remove(&PortName::new("data"));
if data.is_none() {
return Err(RunNodeError::MissingInput {
port: PortName::new("data"),
});
}
if let Some((port, _)) = input.pop_first() {
return Err(RunNodeError::UnrecognizedInput { port });
}
trace!(
message = "Inputs ready, preparing reader",
node_id = ?this_node.id
);
let mut reader = match data.unwrap() {
None => {
return Err(RunNodeError::RequiredInputNull {
port: PortName::new("data"),
})
}
Some(PipeData::Blob { source, .. }) => source.build(ctx).await?,
_ => {
return Err(RunNodeError::BadInputType {
port: PortName::new("data"),
})
}
};
//
// Setup is done, extract tags
//
debug!(
message = "Extracting tags",
node_id = ?this_node.id
);
let mut block_reader = FlacBlockReader::new(FlacBlockSelector {
pick_vorbiscomment: true,
..Default::default()
});
while let Some(data) = reader.next_fragment().await? {
block_reader
.push_data(&data)
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
}
block_reader
.finish()
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
//
// Return tags
//
let mut output = BTreeMap::new();
while block_reader.has_block() {
let b = block_reader.pop_block().unwrap();
match b {
FlacBlock::VorbisComment(comment) => {
for (port, tag_type) in tags.iter() {
if let Some((_, tag_value)) =
comment.comment.comments.iter().find(|(t, _)| t == tag_type)
{
let x = output.insert(
port.clone(),
PipeData::Text {
value: tag_value.clone(),
},
);
// Each insertion should be new
assert!(x.is_none());
}
}
}
// `reader` filters blocks for us
_ => unreachable!(),
}
// We should only have one comment block
assert!(!block_reader.has_block());
}
return Ok(output);
}
}

View File

@@ -1,36 +0,0 @@
//! Pipeline nodes for processing audio files
use copper_piper::base::{NodeDispatcher, RegisterNodeError};
use std::collections::BTreeMap;
mod extractcovers;
mod extracttags;
mod striptags;
/// Register all nodes in this module into the given dispatcher
pub fn register(dispatcher: &mut NodeDispatcher) -> Result<(), RegisterNodeError> {
dispatcher
.register_node(
"StripTags",
BTreeMap::new(),
Box::new(striptags::StripTags {}),
)
.unwrap();
dispatcher
.register_node(
"ExtractCovers",
BTreeMap::new(),
Box::new(extractcovers::ExtractCovers {}),
)
.unwrap();
dispatcher
.register_node(
"ExtractTags",
BTreeMap::new(),
Box::new(extracttags::ExtractTags {}),
)
.unwrap();
return Ok(());
}

View File

@@ -1,181 +0,0 @@
//! Strip all tags from an audio file
use crate::flac::proc::metastrip::FlacMetaStrip;
use async_trait::async_trait;
use copper_piper::{
base::{Node, NodeBuilder, NodeId, PortName, RunNodeError, ThisNodeInfo},
data::PipeData,
helpers::{
processor::{StreamProcessor, StreamProcessorBuilder},
NodeParameters,
},
CopperContext,
};
use copper_util::MimeType;
use smartstring::{LazyCompact, SmartString};
use std::{collections::BTreeMap, sync::Arc};
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::debug;
/// Strip all metadata from an audio file
pub struct StripTags {}
impl NodeBuilder for StripTags {
fn build<'ctx>(&self) -> Box<dyn Node<'ctx>> {
Box::new(Self {})
}
}
// Input: "data" - Blob
// Output: "out" - Blob
#[async_trait]
impl<'ctx> Node<'ctx> for StripTags {
async fn run(
&self,
_ctx: &CopperContext<'ctx>,
this_node: ThisNodeInfo,
params: NodeParameters,
mut input: BTreeMap<PortName, Option<PipeData>>,
) -> Result<BTreeMap<PortName, PipeData>, RunNodeError> {
//
// Extract parameters
//
params.err_if_not_empty()?;
//
// Extract arguments
//
let data = input.remove(&PortName::new("data"));
if data.is_none() {
return Err(RunNodeError::MissingInput {
port: PortName::new("data"),
});
}
if let Some((port, _)) = input.pop_first() {
return Err(RunNodeError::UnrecognizedInput { port });
}
let source = match data.unwrap() {
None => {
return Err(RunNodeError::RequiredInputNull {
port: PortName::new("data"),
})
}
Some(PipeData::Blob { source, .. }) => source,
_ => {
return Err(RunNodeError::BadInputType {
port: PortName::new("data"),
})
}
};
debug!(
message = "Setup done, stripping tags",
node_id = ?this_node.id
);
let mut output = BTreeMap::new();
output.insert(
PortName::new("out"),
PipeData::Blob {
source: source.add_processor(Arc::new(TagStripProcessor {
node_id: this_node.id.clone(),
node_type: this_node.node_type.clone(),
})),
},
);
return Ok(output);
}
}
#[derive(Debug, Clone)]
struct TagStripProcessor {
node_id: NodeId,
node_type: SmartString<LazyCompact>,
}
impl StreamProcessorBuilder for TagStripProcessor {
fn build(&self) -> Box<dyn StreamProcessor> {
Box::new(self.clone())
}
}
#[async_trait]
impl StreamProcessor for TagStripProcessor {
fn mime(&self) -> &MimeType {
return &MimeType::Flac;
}
fn name(&self) -> &'static str {
"TagStripProcessor"
}
fn source_node_id(&self) -> &NodeId {
&self.node_id
}
/// Return the type of the node that created this processor
fn source_node_type(&self) -> &str {
&self.node_type
}
async fn run(
&self,
mut source: Receiver<Arc<Vec<u8>>>,
sink: Sender<Arc<Vec<u8>>>,
max_buffer_size: usize,
) -> Result<(), RunNodeError> {
//
// Strip tags
//
let mut strip = FlacMetaStrip::new();
let mut out_bytes = Vec::new();
while let Some(data) = source.recv().await {
strip
.push_data(&data)
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
strip
.read_data(&mut out_bytes)
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
if out_bytes.len() >= max_buffer_size {
let x = std::mem::take(&mut out_bytes);
match sink.send(Arc::new(x)).await {
Ok(()) => {}
// Not an error, our receiver was dropped.
// Exit early if that happens!
Err(_) => return Ok(()),
};
}
}
strip
.finish()
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
while strip.has_data() {
strip
.read_data(&mut out_bytes)
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
}
match sink.send(Arc::new(out_bytes)).await {
Ok(()) => {}
// Not an error, our receiver was dropped.
// Exit early if that happens!
Err(_) => return Ok(()),
};
return Ok(());
}
}

View File

@@ -9,8 +9,9 @@ workspace = true
[dependencies]
pile-config = { workspace = true }
pile-audio = { workspace = true }
pile-toolbox = { workspace = true }
pile-flac = { workspace = true }
serde_json = { workspace = true }
itertools = { workspace = true }

View File

@@ -1,12 +1,7 @@
use std::{
fmt::Debug,
fs::File,
io::{Read, Seek},
path::PathBuf,
};
use std::{fmt::Debug, fs::File, io::BufReader, path::PathBuf};
use pile_audio::flac::blockread::{FlacBlock, FlacBlockReader, FlacBlockSelector};
use pile_config::Label;
use pile_flac::{FlacBlock, FlacReader};
use serde_json::{Map, Value};
use crate::Item;
@@ -36,56 +31,34 @@ impl Item for FlacItem {
}
fn json(&self) -> Result<serde_json::Value, std::io::Error> {
let mut block_reader = FlacBlockReader::new(FlacBlockSelector {
pick_vorbiscomment: true,
..Default::default()
});
let mut file = File::open(&self.path)?;
// TODO: do not read the whole file
file.rewind()?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
block_reader
.push_data(&data)
.map_err(std::io::Error::other)?;
block_reader.finish().map_err(std::io::Error::other)?;
//
// Return tags
//
let file = File::open(&self.path)?;
let reader = FlacReader::new(BufReader::new(file));
let mut output = Map::new();
while let Some(block) = block_reader.pop_block() {
match block {
FlacBlock::VorbisComment(comment) => {
for (k, v) in comment.comment.comments {
let k = k.to_string();
let v = Value::String(v.into());
let e = output.get_mut(&k);
for block in reader {
if let FlacBlock::VorbisComment(comment) = block.unwrap() {
for (k, v) in comment.comment.comments {
let k = k.to_string();
let v = Value::String(v.into());
let e = output.get_mut(&k);
match e {
None => {
output.insert(k.clone(), Value::Array(vec![v]));
}
Some(e) => {
// We always insert an array
#[expect(clippy::unwrap_used)]
e.as_array_mut().unwrap().push(v);
}
match e {
None => {
output.insert(k.clone(), Value::Array(vec![v]));
}
Some(e) => {
// We always insert an array
#[expect(clippy::unwrap_used)]
e.as_array_mut().unwrap().push(v);
}
}
}
// `reader` filters blocks for us
_ => unreachable!(),
// We should only have one comment block,
// stop reading when we find it
break;
}
// We should only have one comment block
assert!(!block_reader.has_block());
}
return Ok(serde_json::Value::Object(output));

View File

@@ -1,5 +1,5 @@
[package]
name = "pile-audio"
name = "pile-flac"
version = { workspace = true }
rust-version = { workspace = true }
edition = { workspace = true }
@@ -16,6 +16,5 @@ smartstring = { workspace = true }
[dev-dependencies]
paste = { workspace = true }
rand = { workspace = true }
sha2 = { workspace = true }
itertools = { workspace = true }

View File

@@ -1,10 +1,10 @@
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use std::{
fmt::Debug,
io::{Cursor, Read},
};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// An application block in a flac file
pub struct FlacApplicationBlock {

View File

@@ -1,7 +1,6 @@
use crate::{FlacDecodeError, FlacEncodeError};
use std::fmt::Debug;
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
/// An audio frame in a flac file
pub struct FlacAudioFrame {
/// The audio frame

View File

@@ -1,11 +1,7 @@
use std::fmt::Debug;
use crate::{
common::vorbiscomment::VorbisComment,
flac::errors::{FlacDecodeError, FlacEncodeError},
};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError, VorbisComment};
/// A vorbis comment metablock in a flac file
pub struct FlacCommentBlock {

View File

@@ -1,8 +1,7 @@
use std::fmt::Debug;
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// A cuesheet meta in a flac file
pub struct FlacCuesheetBlock {

View File

@@ -1,7 +1,7 @@
//! FLAC metablock headers. See spec.
use std::fmt::Debug;
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use crate::{FlacDecodeError, FlacEncodeError};
/// A type of flac metadata block
#[expect(missing_docs)]

View File

@@ -1,14 +1,11 @@
//! Read and write implementations for all flac block types
// Not metadata blocks
mod header;
pub use header::{FlacMetablockHeader, FlacMetablockType};
mod audiodata;
pub use audiodata::FlacAudioFrame;
// Metadata blocks
mod streaminfo;
pub use streaminfo::FlacStreaminfoBlock;
@@ -30,15 +27,12 @@ pub use cuesheet::FlacCuesheetBlock;
mod comment;
pub use comment::FlacCommentBlock;
use super::errors::{FlacDecodeError, FlacEncodeError};
use std::io::Write;
/// A decode implementation for a
/// flac metadata block
pub trait FlacMetablockDecode: Sized {
/// Try to decode this block from bytes.
/// `data` should NOT include the metablock header.
fn decode(data: &[u8]) -> Result<Self, FlacDecodeError>;
fn decode(data: &[u8]) -> Result<Self, crate::FlacDecodeError>;
}
/// A encode implementation for a
@@ -53,6 +47,6 @@ pub trait FlacMetablockEncode: Sized {
&self,
is_last: bool,
with_header: bool,
target: &mut impl Write,
) -> Result<(), FlacEncodeError>;
target: &mut impl std::io::Write,
) -> Result<(), crate::FlacEncodeError>;
}

View File

@@ -1,8 +1,7 @@
use std::{fmt::Debug, io::Read};
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// A padding block in a FLAC file.
#[derive(Debug)]

View File

@@ -1,16 +1,11 @@
use mime::Mime;
use std::{
fmt::Debug,
io::{Cursor, Read},
};
use mime::Mime;
use crate::{
common::picturetype::PictureType,
flac::errors::{FlacDecodeError, FlacEncodeError},
};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError, PictureType};
/// A picture metablock in a flac file
pub struct FlacPictureBlock {
@@ -59,7 +54,9 @@ impl FlacMetablockDecode for FlacPictureBlock {
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let picture_type = PictureType::from_idx(u32::from_be_bytes(block))?;
let picture_type = u32::from_be_bytes(block);
let picture_type = PictureType::from_idx(picture_type)
.ok_or(FlacDecodeError::InvalidPictureType(picture_type))?;
// Image format
let mime = {

View File

@@ -1,8 +1,7 @@
use std::fmt::Debug;
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// A seektable block in a flac file
pub struct FlacSeektableBlock {

View File

@@ -1,8 +1,7 @@
use std::io::{Cursor, Read};
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// A streaminfo block in a flac file
#[derive(Debug)]

View File

@@ -1,8 +1,4 @@
//! FLAC errors
use crate::common::{
picturetype::PictureTypeError,
vorbiscomment::{VorbisCommentDecodeError, VorbisCommentEncodeError},
};
use std::string::FromUtf8Error;
use thiserror::Error;
@@ -22,47 +18,37 @@ pub enum FlacDecodeError {
BadMetablockType(u8),
/// We encountered an i/o error while processing
#[error("io error while reading flac")]
#[error("i/o error")]
IoError(#[from] std::io::Error),
/// We could not parse a vorbis comment
#[error("error while decoding vorbis comment")]
VorbisComment(#[from] VorbisCommentDecodeError),
/// We tried to decode a string, but found invalid UTF-8
#[error("error while decoding string")]
#[error("could not decode string")]
FailedStringDecode(#[from] FromUtf8Error),
/// We tried to read a block, but it was out of spec.
#[error("malformed flac block")]
MalformedBlock,
/// We could not parse a vorbis comment string
#[error("malformed vorbis comment string: {0:?}")]
MalformedCommentString(String),
/// We could not parse a vorbis comment string
#[error("malformed vorbis picture")]
MalformedPicture(base64::DecodeError),
/// We didn't find frame sync bytes where we expected them
#[error("bad frame sync bytes")]
BadSyncBytes,
/// We tried to decode a bad picture type
#[error("bad picture type")]
PictureTypeError(#[from] PictureTypeError),
#[error("invalid picture type {0}")]
InvalidPictureType(u32),
}
#[expect(missing_docs)]
#[derive(Debug, Error)]
pub enum FlacEncodeError {
/// We encountered an i/o error while processing
#[error("io error while encoding block")]
#[error("i/o error")]
IoError(#[from] std::io::Error),
/// We could not encode a picture inside a vorbis comment
#[error("could not encode picture in vorbis comment")]
VorbisPictureEncodeError,
}
impl From<VorbisCommentEncodeError> for FlacEncodeError {
fn from(value: VorbisCommentEncodeError) -> Self {
match value {
VorbisCommentEncodeError::IoError(e) => e.into(),
VorbisCommentEncodeError::PictureEncodeError => Self::VorbisPictureEncodeError,
}
}
}

View File

@@ -0,0 +1,19 @@
pub mod blocks;
mod tagtype;
pub use tagtype::*;
mod picturetype;
pub use picturetype::*;
mod errors;
pub use errors::*;
mod reader;
pub use reader::*;
mod vorbiscomment;
pub use vorbiscomment::*;
#[cfg(test)]
mod tests;

View File

@@ -1,21 +1,5 @@
//! An audio picture type, according to the ID3v2 APIC frame
use std::fmt::Display;
/// We failed to decode a picture type
#[derive(Debug)]
pub struct PictureTypeError {
idx: u32,
}
impl Display for PictureTypeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Bad picture type `{}`", self.idx)
}
}
impl std::error::Error for PictureTypeError {}
/// A picture type according to the ID3v2 APIC frame
#[expect(missing_docs)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -46,8 +30,8 @@ pub enum PictureType {
impl PictureType {
/// Try to decode a picture type from the given integer.
/// Returns an error if `idx` is invalid.
pub fn from_idx(idx: u32) -> Result<Self, PictureTypeError> {
Ok(match idx {
pub fn from_idx(idx: u32) -> Option<Self> {
Some(match idx {
0 => PictureType::Other,
1 => PictureType::PngFileIcon,
2 => PictureType::OtherFileIcon,
@@ -69,7 +53,7 @@ impl PictureType {
18 => PictureType::Illustration,
19 => PictureType::ArtistLogotype,
20 => PictureType::PublisherLogotype,
_ => return Err(PictureTypeError { idx }),
_ => return None,
})
}

View File

@@ -0,0 +1,63 @@
use std::io::Write;
use crate::{
FlacDecodeError, FlacEncodeError,
blocks::{
FlacApplicationBlock, FlacAudioFrame, FlacCommentBlock, FlacCuesheetBlock,
FlacMetablockDecode, FlacMetablockEncode, FlacMetablockType, FlacPaddingBlock,
FlacPictureBlock, FlacSeektableBlock, FlacStreaminfoBlock,
},
};
#[derive(Debug)]
#[expect(missing_docs)]
pub enum FlacBlock {
Streaminfo(FlacStreaminfoBlock),
Picture(FlacPictureBlock),
Padding(FlacPaddingBlock),
Application(FlacApplicationBlock),
SeekTable(FlacSeektableBlock),
VorbisComment(FlacCommentBlock),
CueSheet(FlacCuesheetBlock),
AudioFrame(FlacAudioFrame),
}
impl FlacBlock {
/// Encode this block
pub fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl Write,
) -> Result<(), FlacEncodeError> {
match self {
Self::Streaminfo(b) => b.encode(is_last, with_header, target),
Self::SeekTable(b) => b.encode(is_last, with_header, target),
Self::Picture(b) => b.encode(is_last, with_header, target),
Self::Padding(b) => b.encode(is_last, with_header, target),
Self::Application(b) => b.encode(is_last, with_header, target),
Self::VorbisComment(b) => b.encode(is_last, with_header, target),
Self::CueSheet(b) => b.encode(is_last, with_header, target),
Self::AudioFrame(b) => b.encode(target),
}
}
/// Try to decode the given data as a block
pub fn decode(block_type: FlacMetablockType, data: &[u8]) -> Result<Self, FlacDecodeError> {
Ok(match block_type {
FlacMetablockType::Streaminfo => {
FlacBlock::Streaminfo(FlacStreaminfoBlock::decode(data)?)
}
FlacMetablockType::Application => {
FlacBlock::Application(FlacApplicationBlock::decode(data)?)
}
FlacMetablockType::Cuesheet => FlacBlock::CueSheet(FlacCuesheetBlock::decode(data)?),
FlacMetablockType::Padding => FlacBlock::Padding(FlacPaddingBlock::decode(data)?),
FlacMetablockType::Picture => FlacBlock::Picture(FlacPictureBlock::decode(data)?),
FlacMetablockType::Seektable => FlacBlock::SeekTable(FlacSeektableBlock::decode(data)?),
FlacMetablockType::VorbisComment => {
FlacBlock::VorbisComment(FlacCommentBlock::decode(data)?)
}
})
}
}

View File

@@ -0,0 +1,9 @@
mod block;
pub use block::*;
#[expect(clippy::module_inception)]
mod reader;
pub use reader::*;
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,184 @@
use std::io::{Read, Seek, SeekFrom};
use crate::{
FlacBlock, FlacDecodeError,
blocks::{FlacAudioFrame, FlacMetablockHeader, FlacMetablockType},
};
// TODO: quickly skip blocks we do not need
/// The next block we expect to read
enum ReaderState {
MagicBits,
MetablockHeader { is_first: bool },
MetaBlock { header: FlacMetablockHeader },
AudioData,
Done,
}
pub struct FlacReader<R: Read + Seek> {
inner: R,
state: ReaderState,
}
impl<R: Read + Seek> FlacReader<R> {
const MIN_AUDIO_FRAME_LEN: usize = 5000;
pub fn new(inner: R) -> Self {
Self {
inner,
state: ReaderState::MagicBits,
}
}
}
impl<R: Read + Seek> Iterator for FlacReader<R> {
type Item = Result<FlacBlock, FlacDecodeError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match &mut self.state {
ReaderState::Done => return None,
ReaderState::MagicBits => {
let mut data = [0u8; 4];
if let Err(e) = self.inner.read_exact(&mut data[..4]) {
self.state = ReaderState::Done;
return Some(Err(e.into()));
}
if data != [0x66, 0x4C, 0x61, 0x43] {
self.state = ReaderState::Done;
return Some(Err(FlacDecodeError::BadMagicBytes));
}
self.state = ReaderState::MetablockHeader { is_first: true };
}
ReaderState::MetablockHeader { is_first } => {
let mut data = [0u8; 4];
if let Err(e) = self.inner.read_exact(&mut data[..]) {
self.state = ReaderState::Done;
return Some(Err(e.into()));
}
let header = match FlacMetablockHeader::decode(&data) {
Ok(h) => h,
Err(e) => {
self.state = ReaderState::Done;
return Some(Err(e));
}
};
if *is_first && !matches!(header.block_type, FlacMetablockType::Streaminfo) {
self.state = ReaderState::Done;
return Some(Err(FlacDecodeError::BadFirstBlock));
}
self.state = ReaderState::MetaBlock { header };
}
ReaderState::MetaBlock { header } => {
let mut data = vec![0u8; header.length as usize];
if let Err(e) = self.inner.read_exact(&mut data) {
self.state = ReaderState::Done;
return Some(Err(e.into()));
}
let block = match FlacBlock::decode(header.block_type, &data) {
Ok(b) => b,
Err(e) => {
self.state = ReaderState::Done;
return Some(Err(e));
}
};
if header.is_last {
self.state = ReaderState::AudioData;
} else {
self.state = ReaderState::MetablockHeader { is_first: false };
}
return Some(Ok(block));
}
ReaderState::AudioData => {
let mut data = Vec::new();
loop {
let mut byte = [0u8; 1];
match self.inner.read_exact(&mut byte) {
Ok(_) => {
data.push(byte[0]);
if data.len() >= Self::MIN_AUDIO_FRAME_LEN + 2 {
let len = data.len();
if data[len - 2] == 0b1111_1111
&& data[len - 1] & 0b1111_1100 == 0b1111_1000
{
let frame_data = data[..len - 2].to_vec();
if frame_data.len() < 2
|| frame_data[0] != 0b1111_1111 || frame_data[1]
& 0b1111_1100
!= 0b1111_1000
{
self.state = ReaderState::Done;
return Some(Err(FlacDecodeError::BadSyncBytes));
}
let audio_frame = match FlacAudioFrame::decode(&frame_data)
{
Ok(f) => f,
Err(e) => {
self.state = ReaderState::Done;
return Some(Err(e));
}
};
// Seek back 2 bytes so the next frame starts with the sync bytes
if let Err(e) = self.inner.seek(SeekFrom::Current(-2)) {
self.state = ReaderState::Done;
return Some(Err(e.into()));
}
self.state = ReaderState::AudioData;
return Some(Ok(FlacBlock::AudioFrame(audio_frame)));
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
if data.len() > 2 {
if data[0] != 0b1111_1111
|| data[1] & 0b1111_1100 != 0b1111_1000
{
self.state = ReaderState::Done;
return Some(Err(FlacDecodeError::BadSyncBytes));
}
let audio_frame = match FlacAudioFrame::decode(&data) {
Ok(f) => f,
Err(e) => {
self.state = ReaderState::Done;
return Some(Err(e));
}
};
self.state = ReaderState::Done;
return Some(Ok(FlacBlock::AudioFrame(audio_frame)));
} else {
self.state = ReaderState::Done;
return None;
}
}
Err(e) => {
self.state = ReaderState::Done;
return Some(Err(e.into()));
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,393 @@
use itertools::Itertools;
use paste::paste;
use sha2::{Digest, Sha256};
use std::{fs::File, io::Write, str::FromStr};
use crate::{
FlacDecodeError, TagType,
tests::{FlacBlockOutput, FlacTestCase, VorbisCommentTestValue},
};
use super::*;
#[expect(clippy::unwrap_used)]
fn read_file(test_case: &FlacTestCase) -> Result<Vec<FlacBlock>, FlacDecodeError> {
let file_data = std::fs::read(test_case.get_path()).unwrap();
// Make sure input file is correct
let mut hasher = Sha256::new();
hasher.update(&file_data);
assert_eq!(
test_case.get_in_hash(),
hasher.finalize().map(|x| format!("{x:02x}")).join("")
);
let file = File::open(test_case.get_path()).unwrap();
let mut reader = FlacReader::new(file);
let mut out_blocks = Vec::new();
while let Some(b) = reader.next() {
out_blocks.push(b?)
}
return Ok(out_blocks);
}
#[expect(clippy::unwrap_used)]
fn test_identical(test_case: &FlacTestCase) -> Result<(), FlacDecodeError> {
let out_blocks = read_file(test_case)?;
let mut out = Vec::new();
out.write_all(&[0x66, 0x4C, 0x61, 0x43]).unwrap();
for i in 0..out_blocks.len() {
let b = &out_blocks[i];
let is_last = if i == out_blocks.len() - 1 {
false
} else {
!matches!(b, FlacBlock::AudioFrame(_))
&& matches!(&out_blocks[i + 1], FlacBlock::AudioFrame(_))
};
b.encode(is_last, true, &mut out).unwrap();
}
let mut hasher = Sha256::new();
hasher.update(&out);
let result = hasher.finalize().map(|x| format!("{x:02x}")).join("");
assert_eq!(result, test_case.get_in_hash(), "Output hash doesn't match");
return Ok(());
}
fn test_blockread(test_case: &FlacTestCase) -> Result<(), FlacDecodeError> {
let out_blocks = read_file(test_case)?;
assert_eq!(
test_case.get_blocks().unwrap().len(),
out_blocks
.iter()
.filter(|x| !matches!(*x, FlacBlock::AudioFrame(_)))
.count(),
"Number of blocks didn't match"
);
let mut audio_data_hasher = Sha256::new();
let mut result_i = 0;
for b in out_blocks {
match b {
FlacBlock::Streaminfo(s) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Streaminfo {
min_block_size,
max_block_size,
min_frame_size,
max_frame_size,
sample_rate,
channels,
bits_per_sample,
total_samples,
md5_signature,
} => {
assert_eq!(*min_block_size, s.min_block_size,);
assert_eq!(*max_block_size, s.max_block_size);
assert_eq!(*min_frame_size, s.min_frame_size);
assert_eq!(*max_frame_size, s.max_frame_size);
assert_eq!(*sample_rate, s.sample_rate);
assert_eq!(*channels, s.channels);
assert_eq!(*bits_per_sample, s.bits_per_sample);
assert_eq!(*total_samples, s.total_samples);
assert_eq!(
*md5_signature,
s.md5_signature.iter().map(|x| format!("{x:02x}")).join("")
);
}
_ => panic!("Unexpected block type"),
},
FlacBlock::Application(a) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Application {
application_id,
hash,
} => {
assert_eq!(
*application_id, a.application_id,
"Application id doesn't match"
);
assert_eq!(
*hash,
{
let mut hasher = Sha256::new();
hasher.update(&a.data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
},
"Application content hash doesn't match"
);
}
_ => panic!("Unexpected block type"),
},
FlacBlock::CueSheet(c) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::CueSheet { hash } => {
assert_eq!(*hash, {
let mut hasher = Sha256::new();
hasher.update(&c.data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
});
}
_ => panic!("Unexpected block type"),
},
FlacBlock::Padding(p) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Padding { size } => {
assert_eq!(p.size, *size);
}
_ => panic!("Unexpected block type"),
},
FlacBlock::SeekTable(t) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Seektable { hash } => {
assert_eq!(*hash, {
let mut hasher = Sha256::new();
hasher.update(&t.data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
});
}
_ => panic!("Unexpected block type"),
},
FlacBlock::Picture(p) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::Picture {
picture_type,
mime,
description,
width,
height,
bit_depth,
color_count,
img_data,
} => {
assert_eq!(*picture_type, p.picture_type, "{}", test_case.get_name());
assert_eq!(*mime, p.mime, "{}", test_case.get_name());
assert_eq!(*description, p.description, "{}", test_case.get_name());
assert_eq!(*width, p.width, "{}", test_case.get_name());
assert_eq!(*height, p.height, "{}", test_case.get_name());
assert_eq!(*bit_depth, p.bit_depth, "{}", test_case.get_name());
assert_eq!(*color_count, p.color_count, "{}", test_case.get_name());
assert_eq!(
*img_data,
{
let mut hasher = Sha256::new();
hasher.update(&p.img_data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
},
"{}",
test_case.get_name()
);
}
_ => panic!("Unexpected block type"),
},
FlacBlock::VorbisComment(v) => match &test_case.get_blocks().unwrap()[result_i] {
FlacBlockOutput::VorbisComment {
vendor,
comments,
pictures,
} => {
assert_eq!(*vendor, v.comment.vendor, "Comment vendor doesn't match");
assert_eq!(
v.comment.pictures.len(),
pictures.len(),
"Number of pictures doesn't match"
);
for (p, e) in v.comment.pictures.iter().zip(*pictures) {
match e {
FlacBlockOutput::Picture {
picture_type,
mime,
description,
width,
height,
bit_depth,
color_count,
img_data,
} => {
assert_eq!(*picture_type, p.picture_type);
assert_eq!(*mime, p.mime);
assert_eq!(*description, p.description);
assert_eq!(*width, p.width);
assert_eq!(*height, p.height);
assert_eq!(*bit_depth, p.bit_depth);
assert_eq!(*color_count, p.color_count);
assert_eq!(*img_data, {
let mut hasher = Sha256::new();
hasher.update(&p.img_data);
hasher.finalize().map(|x| format!("{x:02x}")).join("")
});
}
_ => panic!("Bad test data: expected only Picture blocks."),
}
}
match comments {
VorbisCommentTestValue::Raw { tags } => {
assert_eq!(
v.comment.comments.len(),
tags.len(),
"Number of comments doesn't match"
);
for ((got_tag, got_val), (exp_tag, exp_val)) in
v.comment.comments.iter().zip(*tags)
{
assert_eq!(
*got_tag,
TagType::from_str(exp_tag).unwrap(),
"Tag key doesn't match"
);
assert_eq!(
got_val, exp_val,
"Tag value of {exp_tag} doesn't match"
);
}
}
VorbisCommentTestValue::Hash { n_comments, hash } => {
assert_eq!(
v.comment.comments.len(),
*n_comments,
"Number of comments doesn't match"
);
let mut hasher = Sha256::new();
for (got_tag, got_val) in &v.comment.comments {
hasher.update(format!("{}={got_val};", got_tag.to_vorbis_string()).as_bytes());
}
assert_eq!(
&hasher.finalize().map(|x| format!("{x:02x}")).join(""),
hash,
"Comment hash doesn't match"
);
}
}
}
_ => panic!("Unexpected block type"),
},
FlacBlock::AudioFrame(data) => {
let mut vec = Vec::new();
data.encode(&mut vec).unwrap();
audio_data_hasher.update(&vec);
if result_i != test_case.get_blocks().unwrap().len() {
panic!("There are metadata blocks between audio frames!")
}
// Don't increment result_i
continue;
}
}
result_i += 1;
}
// Check audio data hash
assert_eq!(
test_case.get_audio_hash().unwrap(),
audio_data_hasher
.finalize()
.map(|x| format!("{x:02x}"))
.join("")
);
return Ok(());
}
// Helper macros to generate tests
macro_rules! gen_tests {
( $test_name:ident ) => {
paste! {
#[test]
pub fn [<blockread_small_ $test_name>]() {
let manifest = crate::tests::manifest();
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
match test_case {
FlacTestCase::Success { .. } => {
for _ in 0..5 {
test_blockread(
test_case,
).unwrap()
}
},
FlacTestCase::Error { check_error, .. } => {
let e = test_blockread(test_case);
match e {
Err(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
_ => panic!("Unexpected error {e:?}")
}
}
}
}
#[test]
pub fn [<identical_small_ $test_name>]() {
let manifest = crate::tests::manifest();
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
match test_case {
FlacTestCase::Success { .. } => {
for _ in 0..5 {
test_identical(
test_case,
).unwrap()
}
},
FlacTestCase::Error { check_error, .. } => {
let e = test_identical(test_case);
match e {
Err(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
_ => panic!("Unexpected error {e:?}")
}
}
}
}
}
};
}
gen_tests!(custom_01);
gen_tests!(custom_02);
gen_tests!(custom_03);
gen_tests!(uncommon_10);
gen_tests!(faulty_06);
gen_tests!(faulty_07);
gen_tests!(faulty_10);
gen_tests!(faulty_11);
gen_tests!(subset_45);
gen_tests!(subset_46);
gen_tests!(subset_47);
gen_tests!(subset_48);
gen_tests!(subset_49);
gen_tests!(subset_50);
gen_tests!(subset_51);
gen_tests!(subset_52);
gen_tests!(subset_53);
gen_tests!(subset_54);
gen_tests!(subset_55);
gen_tests!(subset_56);
gen_tests!(subset_57);
gen_tests!(subset_58);
gen_tests!(subset_59);

View File

@@ -4,37 +4,75 @@ use strum::{Display, EnumString};
/// A universal tag type
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, EnumString, Display)]
#[strum(ascii_case_insensitive)]
pub enum TagType {
/// A tag we didn't recognize
#[strum(default)]
Other(SmartString<LazyCompact>),
/// Album name
Album,
/// Album artist
AlbumArtist,
/// Comment
Comment,
/// Release date
ReleaseDate,
/// Disk number
DiskNumber,
/// Total disks in album
DiskTotal,
/// Genre
Genre,
/// International standard recording code
Isrc,
/// Track lyrics, possibly time-coded
Lyrics,
/// This track's number in its album
TrackNumber,
/// The total number of tracks in this track's album
TrackTotal,
/// The title of this track
TrackTitle,
/// This track's artist (the usual `Artist`,
/// compare to `AlbumArtist`)
TrackArtist,
/// The year this track was released
Year,
}
impl TagType {
pub fn to_vorbis_string(&self) -> String {
match self {
TagType::TrackTitle => "TITLE",
TagType::Album => "ALBUM",
TagType::TrackNumber => "TRACKNUMBER",
TagType::TrackArtist => "ARTIST",
TagType::AlbumArtist => "ALBUMARTIST",
TagType::Genre => "GENRE",
TagType::Isrc => "ISRC",
TagType::ReleaseDate => "DATE",
TagType::TrackTotal => "TOTALTRACKS",
TagType::Lyrics => "LYRICS",
TagType::Comment => "COMMENT",
TagType::DiskNumber => "DISKNUMBER",
TagType::DiskTotal => "DISKTOTAL",
TagType::Year => "YEAR",
TagType::Other(x) => x,
}
.to_uppercase()
}
}

View File

@@ -0,0 +1,919 @@
use std::str::FromStr;
use itertools::Itertools;
use mime::Mime;
use crate::PictureType;
use super::errors::FlacDecodeError;
/// The value of a vorbis comment.
///
/// Some files have VERY large comments, and providing them
/// explicitly here doesn't make sense.
#[derive(Clone)]
pub enum VorbisCommentTestValue {
/// The comments, in order
Raw {
tags: &'static [(&'static str, &'static str)],
},
/// The hash of all comments concatenated together,
/// stringified as `{key}={value};`
Hash {
n_comments: usize,
hash: &'static str,
},
}
#[derive(Clone)]
pub enum FlacBlockOutput {
Application {
application_id: u32,
hash: &'static str,
},
Streaminfo {
min_block_size: u32,
max_block_size: u32,
min_frame_size: u32,
max_frame_size: u32,
sample_rate: u32,
channels: u8,
bits_per_sample: u8,
total_samples: u128,
md5_signature: &'static str,
},
CueSheet {
// Hash of this block's data, without the header.
// This is easy to get with
//
// ```notrust
// metaflac \
// --list \
// --block-number=<n> \
// --data-format=binary-headerless \
// <file> \
// | sha256sum
//```
hash: &'static str,
},
Seektable {
hash: &'static str,
},
Padding {
size: u32,
},
Picture {
picture_type: PictureType,
mime: Mime,
description: &'static str,
width: u32,
height: u32,
bit_depth: u32,
color_count: u32,
img_data: &'static str,
},
VorbisComment {
vendor: &'static str,
comments: VorbisCommentTestValue,
pictures: &'static [FlacBlockOutput],
},
}
pub enum FlacTestCase {
Success {
/// This test's name
test_name: &'static str,
/// The file to use for this test
file_path: &'static str,
/// The hash of the input files
in_hash: &'static str,
/// The flac metablocks we expect to find in this file, in order.
blocks: Vec<FlacBlockOutput>,
/// The hash of the audio frames in this file
///
/// Get this hash by running `metaflac --remove-all --dont-use-padding`,
/// then by manually deleting remaining headers in a hex editor
/// (Remember that the sync sequence is 0xFF 0xF8)
audio_hash: &'static str,
/// The hash we should get when we strip this file's tags.
///
/// A stripped flac file has unmodified STREAMINFO, SEEKTABLE,
/// CUESHEET, and audio data blocks; and nothing else (not even padding).
///
/// Reference implementation:
/// ```notrust
/// metaflac \
/// --remove \
/// --block-type=PADDING,APPLICATION,VORBIS_COMMENT,PICTURE \
/// --dont-use-padding \
/// <file>
/// ```
stripped_hash: &'static str,
},
Error {
/// This test's name
test_name: &'static str,
/// The file to use for this test
file_path: &'static str,
/// The hash of the input files
in_hash: &'static str,
/// The error we should encounter while reading this file
check_error: &'static dyn Fn(&FlacDecodeError) -> bool,
/// If some, stripping this file's metadata should produce the given hash.
/// If none, trying to strip metadata should produce `check_error`
stripped_hash: Option<&'static str>,
/// If some, the following images should be extracted from this file
/// If none, trying to strip images should produce `check_error`
pictures: Option<Vec<FlacBlockOutput>>,
},
}
#[expect(dead_code)]
impl FlacTestCase {
pub fn get_name(&self) -> &str {
match self {
Self::Error { test_name, .. } | Self::Success { test_name, .. } => test_name,
}
}
pub fn get_path(&self) -> &str {
match self {
Self::Success { file_path, .. } | Self::Error { file_path, .. } => file_path,
}
}
pub fn get_in_hash(&self) -> &str {
match self {
Self::Success { in_hash, .. } | Self::Error { in_hash, .. } => in_hash,
}
}
pub fn get_stripped_hash(&self) -> Option<&str> {
match self {
Self::Success { stripped_hash, .. } => Some(stripped_hash),
Self::Error { stripped_hash, .. } => *stripped_hash,
}
}
pub fn get_audio_hash(&self) -> Option<&str> {
match self {
Self::Success { audio_hash, .. } => Some(audio_hash),
_ => None,
}
}
pub fn get_blocks(&self) -> Option<&[FlacBlockOutput]> {
match self {
Self::Success { blocks, .. } => Some(blocks),
_ => None,
}
}
pub fn get_pictures(&self) -> Option<Vec<FlacBlockOutput>> {
match self {
Self::Success { blocks, .. } => {
let mut out = Vec::new();
for b in blocks {
match b {
FlacBlockOutput::Picture { .. } => out.push(b.clone()),
FlacBlockOutput::VorbisComment { pictures, .. } => {
for p in *pictures {
out.push(p.clone())
}
}
_ => {}
}
}
return Some(out);
}
Self::Error { pictures, .. } => pictures.as_ref().map(|x| x.iter().cloned().collect()),
}
}
}
/// A list of test files and their expected output
#[expect(clippy::unwrap_used)]
pub fn manifest() -> [FlacTestCase; 23] {
[
FlacTestCase::Error {
test_name: "uncommon_10",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_uncommon/10 - file starting at frame header.flac"
),
in_hash: "d95f63e8101320f5ac7ffe249bc429a209eb0e10996a987301eaa63386a8faa1",
check_error: &|x| matches!(x, FlacDecodeError::BadMagicBytes),
stripped_hash: None,
pictures: None,
},
FlacTestCase::Error {
test_name: "faulty_06",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac"
),
in_hash: "53aed5e7fde7a652b82ba06a8382b2612b02ebbde7b0d2016276644d17cc76cd",
check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock),
stripped_hash: None,
pictures: None,
},
FlacTestCase::Error {
test_name: "faulty_07",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac"
),
in_hash: "6d46725991ba5da477187fde7709ea201c399d00027257c365d7301226d851ea",
check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock),
stripped_hash: None,
pictures: None,
},
FlacTestCase::Error {
test_name: "faulty_10",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac"
),
in_hash: "c79b0514a61634035a5653c5493797bbd1fcc78982116e4d429630e9e462d29b",
check_error: &|x| matches!(x, FlacDecodeError::IoError(_)),
// This file's vorbis comment is invalid, but that shouldn't stop us from removing it.
// As a general rule, we should NOT encounter an error when stripping invalid blocks.
//
// We should, however, get errors when we try to strip flac files with invalid structure.
// (For example, the out-of-order streaminfo test in faulty_07).
stripped_hash: Some("4b994f82dc1699a58e2b127058b37374220ee41dc294d4887ac14f056291a1b0"),
pictures: None,
},
FlacTestCase::Error {
test_name: "faulty_11",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_faulty/11 - incorrect metadata block length.flac"
),
in_hash: "3732151ba8c4e66a785165aa75a444aad814c16807ddc97b793811376acacfd6",
check_error: &|x| matches!(x, FlacDecodeError::BadMetablockType(127)),
stripped_hash: None,
pictures: None,
},
FlacTestCase::Success {
test_name: "subset_45",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/45 - no total number of samples set.flac"
),
in_hash: "336a18eb7a78f7fc0ab34980348e2895bc3f82db440a2430d9f92e996f889f9a",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 907,
max_frame_size: 8053,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 0,
md5_signature: "c41ae3b82c35d8f5c3dab1729f948fde",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
],
audio_hash: "3fb3482ebc1724559bdd57f34de472458563d78a676029614e76e32b5d2b8816",
stripped_hash: "31631ac227ebe2689bac7caa1fa964b47e71a9f1c9c583a04ea8ebd9371508d0",
},
FlacTestCase::Success {
test_name: "subset_46",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/46 - no min-max framesize set.flac"
),
in_hash: "9dc39732ce17815832790901b768bb50cd5ff0cd21b28a123c1cabc16ed776cc",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 0,
max_frame_size: 0,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 282866,
md5_signature: "fd131e6ebc75251ed83f8f4c07df36a4",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
],
audio_hash: "a1eed422462b386a932b9eb3dff3aea3687b41eca919624fb574aadb7eb50040",
stripped_hash: "9e57cd77f285fc31f87fa4e3a31ab8395d68d5482e174c8e0d0bba9a0c20ba27",
},
FlacTestCase::Success {
test_name: "subset_47",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/47 - only STREAMINFO.flac"
),
in_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85",
blocks: vec![FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 4747,
max_frame_size: 7034,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 232608,
md5_signature: "bba30c5f70789910e404b7ac727c3853",
}],
audio_hash: "5ee1450058254087f58c91baf0f70d14bde8782cf2dc23c741272177fe0fce6e",
stripped_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85",
},
FlacTestCase::Success {
test_name: "subset_48",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac"
),
in_hash: "4417aca6b5f90971c50c28766d2f32b3acaa7f9f9667bd313336242dae8b2531",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 2445,
max_frame_size: 7364,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 259884,
md5_signature: "97a0574290237563fbaa788ad77d2cdf",
},
FlacBlockOutput::Seektable {
hash: "21ca2184ae22fe26b690fd7cbd8d25fcde1d830ff6e5796ced4107bab219d7c0",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
],
audio_hash: "c2d691f2c4c986fe3cd5fd7864d9ba9ce6dd68a4ffc670447f008434b13102c2",
stripped_hash: "abc9a0c40a29c896bc6e1cc0b374db1c8e157af716a5a3c43b7db1591a74c4e8",
},
FlacTestCase::Success {
test_name: "subset_49",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/49 - Extremely large PADDING.flac",
),
in_hash: "7bc44fa2754536279fde4f8fb31d824f43b8d0b3f93d27d055d209682914f20e",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 1353,
max_frame_size: 7117,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 258939,
md5_signature: "6e78f221caaaa5d570a53f1714d84ded",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Padding { size: 16777215 },
],
audio_hash: "5007be7109b28b0149d1b929d2a0e93a087381bd3e68cf2a3ef78ea265ea20c3",
stripped_hash: "a2283bbacbc4905ad3df1bf9f43a0ea7aa65cf69523d84a7dd8eb54553cc437e",
},
FlacTestCase::Success {
test_name: "subset_50",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/50 - Extremely large PICTURE.flac"
),
in_hash: "1f04f237d74836104993a8072d4223e84a5d3bd76fbc44555c221c7e69a23594",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 5099,
max_frame_size: 7126,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 265617,
md5_signature: "82164e4da30ed43b47e6027cef050648",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_JPEG,
description: "",
width: 3200,
height: 2252,
bit_depth: 24,
color_count: 0,
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
},
],
audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c",
stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c",
},
FlacTestCase::Success {
test_name: "subset_51",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac"
),
in_hash: "033160e8124ed287b0b5d615c94ac4139477e47d6e4059b1c19b7141566f5ef9",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 4531,
max_frame_size: 7528,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 289972,
md5_signature: "5ff622c88f8dd9bc201a6a541f3890d3",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Hash {
n_comments: 39,
hash: "01984e9ec0cfad41f27b3b4e84184966f6725ead84b7815bd0b3313549ee4229",
},
pictures: &[],
},
],
audio_hash: "76419865d10eb22a74f020423a4e515e800f0177441676afd0418557c2d76c36",
stripped_hash: "c0ca6c6099b5d9ec53d6bb370f339b2b1570055813a6cd3616fac2db83a2185e",
},
FlacTestCase::Success {
test_name: "subset_52",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/52 - Extremely large APPLICATION.flac"
),
in_hash: "0e45a4f8dbef15cbebdd8dfe690d8ae60e0c6abb596db1270a9161b62a7a3f1c",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 3711,
max_frame_size: 7056,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 317876,
md5_signature: "eb7140266bc194527488c21ab49bc47b",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Application {
application_id: 0x74657374,
hash: "cfc0b8969e4ba6bd507999ba89dea2d274df69d94749d6ae3cf117a7780bba09",
},
],
audio_hash: "89ad1a5c86a9ef35d33189c81c8a90285a23964a13f8325bf2c02043e8c83d63",
stripped_hash: "cc4a0afb95ec9bcde8ee33f13951e494dc4126a9a3a668d79c80ce3c14a3acd9",
},
FlacTestCase::Success {
test_name: "subset_53",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac"
),
in_hash: "513fad18578f3225fae5de1bda8f700415be6fd8aa1e7af533b5eb796ed2d461",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 2798,
max_frame_size: 7408,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 2910025,
md5_signature: "d11f3717d628cfe6a90a10facc478340",
},
FlacBlockOutput::Seektable {
hash: "18629e1b874cb27e4364da72fb3fec2141eb0618baae4a1cee6ed09562aa00a8",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::CueSheet {
hash: "70638a241ca06881a52c0a18258ea2d8946a830137a70479c49746d2a1344bdd",
},
],
audio_hash: "e993070f2080f2c598be1d61d208e9187a55ddea4be1d2ed1f8043e7c03e97a5",
stripped_hash: "57c5b945e14c6fcd06916d6a57e5b036d67ff35757893c24ed872007aabbcf4b",
},
FlacTestCase::Success {
test_name: "subset_54",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac"
),
in_hash: "b68dc6644784fac35aa07581be8603a360d1697e07a2265d7eb24001936fd247",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 1694,
max_frame_size: 7145,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 433151,
md5_signature: "1d950e92b357dedbc5290a7f2210a2ef",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Hash {
n_comments: 20000,
hash: "0371b6f158411f35121f1d62ccbc18c90c9b1b0263e51bfc1b8fc942892eaf12",
},
pictures: &[],
},
],
audio_hash: "4721b784058410c6263f73680079e9a71aee914c499afcf5580c121fce00e874",
stripped_hash: "5c8b92b83c0fa17821add38263fa323d1c66cfd2ee57aca054b50bd05b9df5c2",
},
FlacTestCase::Success {
test_name: "subset_55",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/55 - file 48-53 combined.flac"
),
in_hash: "a756b460df79b7cc492223f80cda570e4511f2024e5fa0c4d505ba51b86191f6",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 3103,
max_frame_size: 11306,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 2646000,
md5_signature: "2c78978cbbff11daac296fee97c3e061",
},
FlacBlockOutput::Seektable {
hash: "58dfa7bac4974edf1956b068f5aa72d1fbd9301c36a3085a8a57b9db11a2dbf0",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.3 20190804",
comments: VorbisCommentTestValue::Hash {
n_comments: 40036,
hash: "8d8f21954b4aaee2c7ec92389125a9b28b7de5a8153c62abdd80330f445214df",
},
pictures: &[],
},
FlacBlockOutput::CueSheet {
hash: "db11916c8f5f39648256f93f202e00ff8d73d7d96b62f749b4c77cf3ea744f90",
},
FlacBlockOutput::Application {
application_id: 0x74657374,
hash: "6088a557a1bad7bfa5ebf79a324669fbf4fa2f8e708f5487305dfc5b2ff2249a",
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_JPEG,
description: "",
width: 3200,
height: 2252,
bit_depth: 24,
color_count: 0,
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
},
FlacBlockOutput::Padding { size: 16777215 },
],
audio_hash: "f1285b77cec7fa9a0979033244489a9d06b8515b2158e9270087a65a4007084d",
stripped_hash: "401038fce06aff5ebdc7a5f2fc01fa491cbf32d5da9ec99086e414b2da3f8449",
},
FlacTestCase::Success {
test_name: "subset_56",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/56 - JPG PICTURE.flac"
),
in_hash: "5cebe7a3710cf8924bd2913854e9ca60b4cd53cfee5a3af0c3c73fddc1888963",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 3014,
max_frame_size: 7219,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 220026,
md5_signature: "5b0e898d9c2626d0c28684f5a586813f",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_JPEG,
description: "",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 0,
img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6",
},
],
audio_hash: "ccfe90b0f15cd9662f7a18f40cd4c347538cf8897a08228e75351206f7804573",
stripped_hash: "31a38d59db2010790b7abf65ec0cc03f2bbe1fed5952bc72bee4ca4d0c92e79f",
},
FlacTestCase::Success {
test_name: "subset_57",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/57 - PNG PICTURE.flac"
),
in_hash: "c6abff7f8bb63c2821bd21dd9052c543f10ba0be878e83cb419c248f14f72697",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 463,
max_frame_size: 6770,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 221623,
md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_PNG,
description: "",
width: 960,
height: 540,
bit_depth: 24,
color_count: 0,
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
},
],
audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f",
stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
},
FlacTestCase::Success {
test_name: "subset_58",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/58 - GIF PICTURE.flac"
),
in_hash: "7c2b1a963a665847167a7275f9924f65baeb85c21726c218f61bf3f803f301c8",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 2853,
max_frame_size: 6683,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 219826,
md5_signature: "7c1810602a7db96d7a48022ac4aa495c",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_GIF,
description: "",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 32,
img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1",
},
],
audio_hash: "30e3292e9f56cf88658eeadfdec8ad3a440690ce6d813e1b3374f60518c8e0ae",
stripped_hash: "4cd771e27870e2a586000f5b369e0426183a521b61212302a2f5802b046910b2",
},
FlacTestCase::Success {
test_name: "subset_59",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_subset/59 - AVIF PICTURE.flac"
),
in_hash: "7395d02bf8d9533dc554cce02dee9de98c77f8731a45f62d0a243bd0d6f9a45c",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 153,
max_frame_size: 7041,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 221423,
md5_signature: "d354246011ca204159c06f52cad5f634",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: Mime::from_str("image/avif").unwrap(),
description: "",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 0,
img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc",
},
],
audio_hash: "b208c73d274e65b27232bfffbfcbcf4805ee3cbc9cfbf7d2104db8f53370273b",
stripped_hash: "d5215e16c6b978fc2c3e6809e1e78981497cb8514df297c5169f3b4a28fd875c",
},
FlacTestCase::Success {
test_name: "custom_01",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_custom/01 - many images.flac"
),
in_hash: "8a5df37488866cd91ac16773e549ef4e3a85d9f88a0d9d345f174807bb536b96",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 5099,
max_frame_size: 7126,
sample_rate: 48000,
channels: 2,
bits_per_sample: 16,
total_samples: 265617,
md5_signature: "82164e4da30ed43b47e6027cef050648",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_PNG,
description: "",
width: 960,
height: 540,
bit_depth: 24,
color_count: 0,
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
}],
},
FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_JPEG,
description: "",
width: 3200,
height: 2252,
bit_depth: 24,
color_count: 0,
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
},
FlacBlockOutput::Picture {
picture_type: PictureType::ABrightColoredFish,
mime: mime::IMAGE_JPEG,
description: "lorem",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 0,
img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6",
},
FlacBlockOutput::Picture {
picture_type: PictureType::OtherFileIcon,
mime: mime::IMAGE_PNG,
description: "ipsum",
width: 960,
height: 540,
bit_depth: 24,
color_count: 0,
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
},
FlacBlockOutput::Picture {
picture_type: PictureType::Lyricist,
mime: mime::IMAGE_GIF,
description: "dolor",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 32,
img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1",
},
FlacBlockOutput::Picture {
picture_type: PictureType::BackCover,
mime: Mime::from_str("image/avif").unwrap(),
description: "est",
width: 1920,
height: 1080,
bit_depth: 24,
color_count: 0,
img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc",
},
],
audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c",
stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c",
},
FlacTestCase::Success {
test_name: "custom_02",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_custom/02 - picture in vorbis comment.flac"
),
in_hash: "f6bb1a726fe6a3e25a4337d36e29fdced8ff01a46d627b7c2e1988c88f461f8c",
blocks: vec![
FlacBlockOutput::Streaminfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 463,
max_frame_size: 6770,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 221623,
md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7",
},
FlacBlockOutput::VorbisComment {
vendor: "reference libFLAC 1.3.2 20170101",
comments: VorbisCommentTestValue::Raw { tags: &[] },
pictures: &[FlacBlockOutput::Picture {
picture_type: PictureType::FrontCover,
mime: mime::IMAGE_PNG,
description: "",
width: 960,
height: 540,
bit_depth: 24,
color_count: 0,
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
}],
},
],
audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f",
stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
},
FlacTestCase::Error {
test_name: "custom_03",
file_path: concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac"
),
in_hash: "7177f0ae4f04a563292be286ec05967f81ab16eb0a28b70fc07a1e47da9cafd0",
check_error: &|x| matches!(x, FlacDecodeError::MalformedPicture(_)),
stripped_hash: Some("3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1"),
pictures: None,
},
]
}
#[test]
fn manifest_sanity_check() {
assert!(manifest().iter().map(|x| x.get_name()).all_unique());
assert!(manifest().iter().map(|x| x.get_path()).all_unique());
}

View File

@@ -0,0 +1,194 @@
//! 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(());
}
}

Some files were not shown because too many files have changed in this diff Show More