pile-audio refactor

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

View File

@@ -0,0 +1,79 @@
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 {
/// Registered application ID
pub application_id: u32,
/// The application data
pub data: Vec<u8>,
}
impl Debug for FlacApplicationBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlacApplicationBlock")
.field("application_id", &self.application_id)
.field("data_len", &self.data.len())
.finish()
}
}
impl FlacMetablockDecode for FlacApplicationBlock {
fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
let mut d = Cursor::new(data);
let mut block = [0u8; 4];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let application_id = u32::from_be_bytes(block);
let data = {
let mut data = Vec::with_capacity(data.len());
d.read_to_end(&mut data)?;
data
};
Ok(Self {
application_id,
data,
})
}
}
impl FlacMetablockEncode for FlacApplicationBlock {
#[expect(clippy::expect_used)]
fn get_len(&self) -> u32 {
(self.data.len() + 4)
.try_into()
.expect("application block size does not fit into u32")
}
fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl std::io::Write,
) -> Result<(), FlacEncodeError> {
if with_header {
let header = FlacMetablockHeader {
block_type: FlacMetablockType::Application,
length: self.get_len(),
is_last,
};
header.encode(target)?;
}
target.write_all(&self.application_id.to_be_bytes())?;
target.write_all(&self.data)?;
return Ok(());
}
}

View File

@@ -0,0 +1,42 @@
use crate::{FlacDecodeError, FlacEncodeError};
use std::fmt::Debug;
/// An audio frame in a flac file
pub struct FlacAudioFrame {
/// The audio frame
pub data: Vec<u8>,
}
impl Debug for FlacAudioFrame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlacAudioFrame")
.field("data_len", &self.data.len())
.finish()
}
}
impl FlacAudioFrame {
/// Decode the given data as a flac audio frame.
/// This should start with a sync sequence.
pub fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
if data.len() <= 2 {
return Err(FlacDecodeError::MalformedBlock);
}
if !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000) {
return Err(FlacDecodeError::BadSyncBytes);
}
Ok(Self {
data: Vec::from(data),
})
}
}
impl FlacAudioFrame {
/// Encode this audio frame.
pub fn encode(&self, target: &mut impl std::io::Write) -> Result<(), FlacEncodeError> {
target.write_all(&self.data)?;
return Ok(());
}
}

View File

@@ -0,0 +1,50 @@
use std::fmt::Debug;
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError, VorbisComment};
/// A vorbis comment metablock in a flac file
pub struct FlacCommentBlock {
/// The vorbis comment stored inside this block
pub comment: VorbisComment,
}
impl Debug for FlacCommentBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlacCommentBlock")
.field("comment", &self.comment)
.finish()
}
}
impl FlacMetablockDecode for FlacCommentBlock {
fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
let comment = VorbisComment::decode(data)?;
Ok(Self { comment })
}
}
impl FlacMetablockEncode for FlacCommentBlock {
fn get_len(&self) -> u32 {
self.comment.get_len()
}
fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl std::io::Write,
) -> Result<(), FlacEncodeError> {
if with_header {
let header = FlacMetablockHeader {
block_type: FlacMetablockType::VorbisComment,
length: self.get_len(),
is_last,
};
header.encode(target)?;
}
self.comment.encode(target)?;
return Ok(());
}
}

View File

@@ -0,0 +1,53 @@
use std::fmt::Debug;
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// A cuesheet meta in a flac file
pub struct FlacCuesheetBlock {
/// The seek table
pub data: Vec<u8>,
}
impl Debug for FlacCuesheetBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlacAudioFrame")
.field("data_len", &self.data.len())
.finish()
}
}
impl FlacMetablockDecode for FlacCuesheetBlock {
fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
Ok(Self { data: data.into() })
}
}
impl FlacMetablockEncode for FlacCuesheetBlock {
#[expect(clippy::expect_used)]
fn get_len(&self) -> u32 {
self.data
.len()
.try_into()
.expect("cuesheet size does not fit into u32")
}
fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl std::io::Write,
) -> Result<(), FlacEncodeError> {
if with_header {
let header = FlacMetablockHeader {
block_type: FlacMetablockType::Cuesheet,
length: self.get_len(),
is_last,
};
header.encode(target)?;
}
target.write_all(&self.data)?;
return Ok(());
}
}

View File

@@ -0,0 +1,87 @@
//! FLAC metablock headers. See spec.
use std::fmt::Debug;
use crate::{FlacDecodeError, FlacEncodeError};
/// A type of flac metadata block
#[expect(missing_docs)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum FlacMetablockType {
Streaminfo,
Padding,
Application,
Seektable,
VorbisComment,
Cuesheet,
Picture,
}
impl FlacMetablockType {
/// Read and parse a metablock header from the given reader.
/// Returns (block_type, block_data_length, is_last)
pub(crate) fn from_id(id: u8) -> Result<Self, FlacDecodeError> {
return Ok(match id & 0b01111111 {
0 => FlacMetablockType::Streaminfo,
1 => FlacMetablockType::Padding,
2 => FlacMetablockType::Application,
3 => FlacMetablockType::Seektable,
4 => FlacMetablockType::VorbisComment,
5 => FlacMetablockType::Cuesheet,
6 => FlacMetablockType::Picture,
x => return Err(FlacDecodeError::BadMetablockType(x)),
});
}
}
/// The header of a flac metadata block
#[derive(Debug, Clone)]
pub struct FlacMetablockHeader {
/// The type of block this is
pub block_type: FlacMetablockType,
/// The length of this block, in bytes
/// (not including this header)
pub length: u32,
/// If true, this is the last metadata block
pub is_last: bool,
}
impl FlacMetablockHeader {
/// Try to decode the given bytes as a flac metablock header
pub fn decode(header: &[u8]) -> Result<Self, FlacDecodeError> {
if header.len() != 4 {
return Err(FlacDecodeError::MalformedBlock);
}
return Ok(Self {
block_type: FlacMetablockType::from_id(header[0])?,
length: u32::from_be_bytes([0, header[1], header[2], header[3]]),
is_last: header[0] & 0b10000000 == 0b10000000,
});
}
}
impl FlacMetablockHeader {
/// Try to encode this header
pub fn encode(&self, target: &mut impl std::io::Write) -> Result<(), FlacEncodeError> {
let mut block_type = match self.block_type {
FlacMetablockType::Streaminfo => 0,
FlacMetablockType::Padding => 1,
FlacMetablockType::Application => 2,
FlacMetablockType::Seektable => 3,
FlacMetablockType::VorbisComment => 4,
FlacMetablockType::Cuesheet => 5,
FlacMetablockType::Picture => 6,
};
if self.is_last {
block_type |= 0b1000_0000;
};
let x = self.length.to_be_bytes();
target.write_all(&[block_type, x[1], x[2], x[3]])?;
return Ok(());
}
}

View File

@@ -0,0 +1,52 @@
//! Read and write implementations for all flac block types
mod header;
pub use header::{FlacMetablockHeader, FlacMetablockType};
mod audiodata;
pub use audiodata::FlacAudioFrame;
mod streaminfo;
pub use streaminfo::FlacStreaminfoBlock;
mod picture;
pub use picture::FlacPictureBlock;
mod padding;
pub use padding::FlacPaddingBlock;
mod application;
pub use application::FlacApplicationBlock;
mod seektable;
pub use seektable::FlacSeektableBlock;
mod cuesheet;
pub use cuesheet::FlacCuesheetBlock;
mod comment;
pub use comment::FlacCommentBlock;
/// 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, crate::FlacDecodeError>;
}
/// A encode implementation for a
/// flac metadata block
pub trait FlacMetablockEncode: Sized {
/// Get the number of bytes that `encode()` will write.
/// This does NOT include header length.
fn get_len(&self) -> u32;
/// Try to encode this block as bytes.
fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl std::io::Write,
) -> Result<(), crate::FlacEncodeError>;
}

View File

@@ -0,0 +1,53 @@
use std::{fmt::Debug, io::Read};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// A padding block in a FLAC file.
#[derive(Debug)]
pub struct FlacPaddingBlock {
/// The length of this padding, in bytes.
pub size: u32,
}
impl FlacMetablockDecode for FlacPaddingBlock {
#[expect(clippy::expect_used)]
fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
if data.iter().any(|x| *x != 0u8) {
return Err(FlacDecodeError::MalformedBlock);
}
Ok(Self {
size: data
.len()
.try_into()
.expect("padding size does not fit into u32"),
})
}
}
impl FlacMetablockEncode for FlacPaddingBlock {
fn get_len(&self) -> u32 {
self.size
}
fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl std::io::Write,
) -> Result<(), FlacEncodeError> {
if with_header {
let header = FlacMetablockHeader {
block_type: FlacMetablockType::Padding,
length: self.get_len(),
is_last,
};
header.encode(target)?;
}
std::io::copy(&mut std::io::repeat(0u8).take(self.size.into()), target)?;
return Ok(());
}
}

View File

@@ -0,0 +1,213 @@
use mime::Mime;
use std::{
fmt::Debug,
io::{Cursor, Read},
};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError, PictureType};
/// A picture metablock in a flac file
pub struct FlacPictureBlock {
/// The type of this picture
pub picture_type: PictureType,
/// The format of this picture
pub mime: Mime,
/// The description of this picture
pub description: String,
/// The width of this picture, in px
pub width: u32,
/// The height of this picture, in px
pub height: u32,
/// The bit depth of this picture
pub bit_depth: u32,
/// The color count of this picture (if indexed)
pub color_count: u32,
/// The image data
pub img_data: Vec<u8>,
}
impl Debug for FlacPictureBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlacPicture")
.field("type", &self.picture_type)
.field("mime", &self.mime)
.field("img_data.len()", &self.img_data.len())
.finish()
}
}
impl FlacMetablockDecode for FlacPictureBlock {
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];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
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 = {
d.read_exact(&mut block)
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
#[expect(clippy::expect_used)]
let mime_length = u32::from_be_bytes(block)
.try_into()
.expect("mime length does not fit into usize");
let mut mime = vec![0u8; mime_length];
d.read_exact(&mut mime)
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
String::from_utf8(mime)
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(mime::APPLICATION_OCTET_STREAM)
};
// Image description
let description = {
d.read_exact(&mut block)
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
#[expect(clippy::expect_used)]
let desc_length = u32::from_be_bytes(block)
.try_into()
.expect("description length does not fit into usize");
let mut desc = vec![0u8; desc_length];
d.read_exact(&mut desc)
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
String::from_utf8(desc)?
};
// Image width
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let width = u32::from_be_bytes(block);
// Image height
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let height = u32::from_be_bytes(block);
// Image bit depth
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let depth = u32::from_be_bytes(block);
// Color count for indexed images
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let color_count = u32::from_be_bytes(block);
// Image data length
let img_data = {
d.read_exact(&mut block)
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
#[expect(clippy::expect_used)]
let data_length = u32::from_be_bytes(block)
.try_into()
.expect("image data length does not fit into usize");
let mut img_data = vec![0u8; data_length];
d.read_exact(&mut img_data)
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
img_data
};
Ok(Self {
picture_type,
mime,
description,
width,
height,
bit_depth: depth,
color_count,
img_data,
})
}
}
impl FlacMetablockEncode for FlacPictureBlock {
#[expect(clippy::expect_used)]
fn get_len(&self) -> u32 {
(4 + (4 + self.mime.to_string().len())
+ (4 + self.description.len())
+ 4 + 4 + 4
+ 4 + (4 + self.img_data.len()))
.try_into()
.expect("picture block size does not fit into u32")
}
fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl std::io::Write,
) -> Result<(), FlacEncodeError> {
if with_header {
let header = FlacMetablockHeader {
block_type: FlacMetablockType::Picture,
length: self.get_len(),
is_last,
};
header.encode(target)?;
}
target.write_all(&self.picture_type.to_idx().to_be_bytes())?;
#[expect(clippy::expect_used)]
{
let mime = self.mime.to_string();
target.write_all(
&u32::try_from(mime.len())
.expect("mime length does not fit into u32")
.to_be_bytes(),
)?;
target.write_all(self.mime.to_string().as_bytes())?;
drop(mime);
target.write_all(
&u32::try_from(self.description.len())
.expect("description length does not fit into u32")
.to_be_bytes(),
)?;
target.write_all(self.description.as_bytes())?;
target.write_all(&self.width.to_be_bytes())?;
target.write_all(&self.height.to_be_bytes())?;
target.write_all(&self.bit_depth.to_be_bytes())?;
target.write_all(&self.color_count.to_be_bytes())?;
target.write_all(
&u32::try_from(self.img_data.len())
.expect("image data length does not fit into u32")
.to_be_bytes(),
)?;
}
target.write_all(&self.img_data)?;
return Ok(());
}
}

View File

@@ -0,0 +1,53 @@
use std::fmt::Debug;
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// A seektable block in a flac file
pub struct FlacSeektableBlock {
/// The seek table
pub data: Vec<u8>,
}
impl Debug for FlacSeektableBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlacSeektableBlock")
.field("data_len", &self.data.len())
.finish()
}
}
impl FlacMetablockDecode for FlacSeektableBlock {
fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
Ok(Self { data: data.into() })
}
}
impl FlacMetablockEncode for FlacSeektableBlock {
#[expect(clippy::expect_used)]
fn get_len(&self) -> u32 {
self.data
.len()
.try_into()
.expect("seektable size does not fit into u32")
}
fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl std::io::Write,
) -> Result<(), FlacEncodeError> {
if with_header {
let header = FlacMetablockHeader {
block_type: FlacMetablockType::Seektable,
length: self.get_len(),
is_last,
};
header.encode(target)?;
}
target.write_all(&self.data)?;
return Ok(());
}
}

View File

@@ -0,0 +1,217 @@
use std::io::{Cursor, Read};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
use crate::{FlacDecodeError, FlacEncodeError};
/// A streaminfo block in a flac file
#[derive(Debug)]
pub struct FlacStreaminfoBlock {
/// The minimum block size (in samples) used in the stream.
pub min_block_size: u32,
/// The maximum block size (in samples) used in the stream.
/// (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream.
pub max_block_size: u32,
/// The minimum frame size (in bytes) used in the stream.
/// May be 0 to imply the value is not known.
pub min_frame_size: u32,
/// The minimum frame size (in bytes) used in the stream.
/// May be 0 to imply the value is not known.
pub max_frame_size: u32,
/// Sample rate in Hz. Though 20 bits are available,
/// the maximum sample rate is limited by the structure of frame headers to 655350Hz.
/// Also, a value of 0 is invalid.
pub sample_rate: u32,
/// (number of channels)-1. FLAC supports from 1 to 8 channels
pub channels: u8,
/// (bits per sample)-1. FLAC supports from 4 to 32 bits per sample.
pub bits_per_sample: u8,
/// Total samples in stream. 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels. A value of zero here means the number of total samples is unknown.
pub total_samples: u128,
/// MD5 signature of the unencoded audio data. This allows the decoder to determine if an error exists in the audio data even when the error does not result in an invalid bitstream.
pub md5_signature: [u8; 16],
}
impl FlacMetablockDecode for FlacStreaminfoBlock {
fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
let mut d = Cursor::new(data);
let min_block_size = {
let mut block = [0u8; 4];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block[2..])
.map_err(|_| FlacDecodeError::MalformedBlock)?;
u32::from_be_bytes(block)
};
let max_block_size = {
let mut block = [0u8; 4];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block[2..])
.map_err(|_| FlacDecodeError::MalformedBlock)?;
u32::from_be_bytes(block)
};
let min_frame_size = {
let mut block = [0u8; 4];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block[1..])
.map_err(|_| FlacDecodeError::MalformedBlock)?;
u32::from_be_bytes(block)
};
let max_frame_size = {
let mut block = [0u8; 4];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block[1..])
.map_err(|_| FlacDecodeError::MalformedBlock)?;
u32::from_be_bytes(block)
};
let (sample_rate, channels, bits_per_sample, total_samples) = {
let mut block = [0u8; 8];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
(
// 20 bits: sample rate in hz
u32::from_be_bytes([0, block[0], block[1], block[2]]) >> 4,
// 3 bits: number of channels - 1.
// FLAC supports 1 - 8 channels.
((u8::from_le_bytes([block[2]]) & 0b0000_1110) >> 1) + 1,
// 5 bits: bits per sample - 1.
// FLAC supports 4 - 32 bps.
((u8::from_le_bytes([block[2]]) & 0b0000_0001) << 4)
+ ((u8::from_le_bytes([block[3]]) & 0b1111_0000) >> 4)
+ 1,
// 36 bits: total "cross-channel" samples in the stream.
// (one second of 44.1Khz audio will have 44100 samples regardless of the number of channels)
// Zero means we don't know.
u128::from_be_bytes([
0,
0,
0,
0,
//
0,
0,
0,
0,
//
0,
0,
0,
block[3] & 0b0000_1111,
//
block[4],
block[5],
block[6],
block[7],
]),
)
};
let md5_signature = {
let mut block = [0u8; 16];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
block
};
Ok(Self {
min_block_size,
max_block_size,
min_frame_size,
max_frame_size,
sample_rate,
channels,
bits_per_sample,
total_samples,
md5_signature,
})
}
}
impl FlacMetablockEncode for FlacStreaminfoBlock {
fn get_len(&self) -> u32 {
34
}
fn encode(
&self,
is_last: bool,
with_header: bool,
target: &mut impl std::io::Write,
) -> Result<(), FlacEncodeError> {
if with_header {
let header = FlacMetablockHeader {
block_type: FlacMetablockType::Streaminfo,
length: self.get_len(),
is_last,
};
header.encode(target)?;
}
target.write_all(&self.min_block_size.to_be_bytes()[2..])?;
target.write_all(&self.max_block_size.to_be_bytes()[2..])?;
target.write_all(&self.min_frame_size.to_be_bytes()[1..])?;
target.write_all(&self.max_frame_size.to_be_bytes()[1..])?;
// Layout of the next 8 bytes:
// [8]: full bytes
// [4 ]: first 4 bits are from this
// [ 3]: next 3 bits are from this
//
// [8][8][4 ]: Sample rate
// [ ][ ][ 3 ]: channels
// [ ][ ][ 1][4 ]: bits per sample
// [ ][ ][ ][ 4][8 x 4]: total samples
let mut out = [0u8; 8];
let sample_rate = &self.sample_rate.to_be_bytes()[1..4];
out[0] = (sample_rate[0] << 4) & 0b1111_0000;
out[0] |= (sample_rate[1] >> 4) & 0b0000_1111;
out[1] = (sample_rate[1] << 4) & 0b1111_0000;
out[1] |= (sample_rate[2] >> 4) & 0b000_1111;
out[2] = (sample_rate[2] << 4) & 0b1111_0000;
let channels = self.channels - 1;
out[2] |= (channels << 1) & 0b0000_1110;
let bits_per_sample = self.bits_per_sample - 1;
out[2] |= (bits_per_sample >> 4) & 0b0000_0001;
out[3] |= (bits_per_sample << 4) & 0b1111_0000;
let total_samples = self.total_samples.to_be_bytes();
out[3] |= total_samples[10] & 0b0000_1111;
out[4] = total_samples[12];
out[5] = total_samples[13];
out[6] = total_samples[14];
out[7] = total_samples[15];
target.write_all(&out)?;
target.write_all(&self.md5_signature)?;
return Ok(());
}
}

View File

@@ -0,0 +1,54 @@
//! FLAC errors
use std::string::FromUtf8Error;
use thiserror::Error;
#[expect(missing_docs)]
#[derive(Debug, Error)]
pub enum FlacDecodeError {
/// FLAC does not start with 0x66 0x4C 0x61 0x43
#[error("flac signature is missing or malformed")]
BadMagicBytes,
/// The first metablock isn't StreamInfo
#[error("first metablock isn't streaminfo")]
BadFirstBlock,
/// We got an invalid metadata block type
#[error("invalid flac metablock type {0}")]
BadMetablockType(u8),
/// We encountered an i/o error while processing
#[error("i/o error")]
IoError(#[from] std::io::Error),
/// We tried to decode a string, but found invalid UTF-8
#[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,
#[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("i/o error")]
IoError(#[from] std::io::Error),
}

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

@@ -0,0 +1,86 @@
//! An audio picture type, according to the ID3v2 APIC frame
/// A picture type according to the ID3v2 APIC frame
#[expect(missing_docs)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum PictureType {
Other,
PngFileIcon,
OtherFileIcon,
FrontCover,
BackCover,
LeafletPage,
Media,
LeadArtist,
Artist,
Conductor,
BandOrchestra,
Composer,
Lyricist,
RecLocation,
DuringRecording,
DuringPerformance,
VideoScreenCapture,
ABrightColoredFish,
Illustration,
ArtistLogotype,
PublisherLogotype,
}
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) -> Option<Self> {
Some(match idx {
0 => PictureType::Other,
1 => PictureType::PngFileIcon,
2 => PictureType::OtherFileIcon,
3 => PictureType::FrontCover,
4 => PictureType::BackCover,
5 => PictureType::LeafletPage,
6 => PictureType::Media,
7 => PictureType::LeadArtist,
8 => PictureType::Artist,
9 => PictureType::Conductor,
10 => PictureType::BandOrchestra,
11 => PictureType::Composer,
12 => PictureType::Lyricist,
13 => PictureType::RecLocation,
14 => PictureType::DuringRecording,
15 => PictureType::DuringPerformance,
16 => PictureType::VideoScreenCapture,
17 => PictureType::ABrightColoredFish,
18 => PictureType::Illustration,
19 => PictureType::ArtistLogotype,
20 => PictureType::PublisherLogotype,
_ => return None,
})
}
/// Return the index of this picture type
pub fn to_idx(&self) -> u32 {
match self {
PictureType::Other => 0,
PictureType::PngFileIcon => 1,
PictureType::OtherFileIcon => 2,
PictureType::FrontCover => 3,
PictureType::BackCover => 4,
PictureType::LeafletPage => 5,
PictureType::Media => 6,
PictureType::LeadArtist => 7,
PictureType::Artist => 8,
PictureType::Conductor => 9,
PictureType::BandOrchestra => 10,
PictureType::Composer => 11,
PictureType::Lyricist => 12,
PictureType::RecLocation => 13,
PictureType::DuringRecording => 14,
PictureType::DuringPerformance => 15,
PictureType::VideoScreenCapture => 16,
PictureType::ABrightColoredFish => 17,
PictureType::Illustration => 18,
PictureType::ArtistLogotype => 19,
PictureType::PublisherLogotype => 20,
}
}
}

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,397 @@
#![expect(clippy::unwrap_used)]
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 reader = FlacReader::new(file);
let mut out_blocks = Vec::new();
for b in reader {
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

@@ -0,0 +1,78 @@
//! Cross-format normalized tag types
use smartstring::{LazyCompact, SmartString};
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.clone(),
}
}
}
/// 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(());
}
}