Initial pile-audio

This commit is contained in:
2026-01-06 23:04:28 -08:00
parent 8611109f0f
commit 1e0a9309ac
120 changed files with 4438 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,102 @@
//! 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
#[allow(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) -> Result<Self, PictureTypeError> {
Ok(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 Err(PictureTypeError { idx }),
})
}
/// 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,40 @@
//! 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)]
pub enum TagType {
/// A tag we didn't recognize
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,
}

View File

@@ -0,0 +1,327 @@
//! 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)]
#[allow(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)]
#[allow(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 = {
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| VorbisCommentDecodeError::MalformedData)?;
let length = u32::from_le_bytes(block);
let mut text = vec![0u8; length.try_into().unwrap()];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut text)
.map_err(|_| VorbisCommentDecodeError::MalformedData)?;
String::from_utf8(text)?
};
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| VorbisCommentDecodeError::MalformedData)?;
let n_comments: usize = u32::from_le_bytes(block).try_into().unwrap();
let mut comments = Vec::new();
let mut pictures = Vec::new();
for _ in 0..n_comments {
let comment = {
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| VorbisCommentDecodeError::MalformedData)?;
let length = u32::from_le_bytes(block);
let mut text = vec![0u8; length.try_into().unwrap()];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut text)
.map_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.
pub fn get_len(&self) -> u32 {
let mut sum: u32 = 0;
sum += u32::try_from(self.vendor.len()).unwrap() + 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()).unwrap();
}
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
pub fn encode(&self, target: &mut impl Write) -> Result<(), VorbisCommentEncodeError> {
target.write_all(&u32::try_from(self.vendor.len()).unwrap().to_le_bytes())?;
target.write_all(self.vendor.as_bytes())?;
target.write_all(
&u32::try_from(self.comments.len() + self.pictures.len())
.unwrap()
.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()).unwrap().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()).unwrap().to_le_bytes())?;
target.write_all(pic_string.as_bytes())?;
}
return Ok(());
}
}

View File

@@ -0,0 +1,840 @@
//! 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)]
#[allow(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 {
match self.current_block.as_mut().unwrap() {
FlacBlockType::MagicBits { data, left_to_read } => {
last_read_size = buf.read(&mut data[4 - *left_to_read..4]).unwrap();
*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]).unwrap();
*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 } => {
last_read_size = buf
.by_ref()
.take(u64::from(header.length) - u64::try_from(data.len()).unwrap())
.read_to_end(data)
.unwrap();
if data.len() == usize::try_from(header.length).unwrap() {
// 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).unwrap();
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).unwrap(),
))
.unwrap();
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},
};
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);
}
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

@@ -0,0 +1,76 @@
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use std::{
fmt::Debug,
io::{Cursor, Read},
};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
/// 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 {
fn get_len(&self) -> u32 {
(self.data.len() + 4).try_into().unwrap()
}
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,43 @@
use std::fmt::Debug;
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
/// 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,54 @@
use std::fmt::Debug;
use crate::{
common::vorbiscomment::VorbisComment,
flac::errors::{FlacDecodeError, FlacEncodeError},
};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
/// 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,50 @@
use std::fmt::Debug;
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
/// 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 {
fn get_len(&self) -> u32 {
self.data.len().try_into().unwrap()
}
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::flac::errors::{FlacDecodeError, FlacEncodeError};
/// A type of flac metadata block
#[allow(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,58 @@
//! Read and write impelementations 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;
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;
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>;
}
/// 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 Write,
) -> Result<(), FlacEncodeError>;
}

View File

@@ -0,0 +1,50 @@
use std::{fmt::Debug, io::Read};
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
/// 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 {
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().unwrap(),
})
}
}
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,197 @@
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};
/// 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 = PictureType::from_idx(u32::from_be_bytes(block))?;
// Image format
let mime = {
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let mime_length = u32::from_be_bytes(block).try_into().unwrap();
let mut mime = vec![0u8; mime_length];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut mime)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
String::from_utf8(mime)
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(mime::APPLICATION_OCTET_STREAM)
};
// Image description
let description = {
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let desc_length = u32::from_be_bytes(block).try_into().unwrap();
let mut desc = vec![0u8; desc_length];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut desc)
.map_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 = {
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut block)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
let data_length = u32::from_be_bytes(block).try_into().unwrap();
let mut img_data = vec![0u8; data_length];
#[expect(clippy::map_err_ignore)]
d.read_exact(&mut img_data)
.map_err(|_| FlacDecodeError::MalformedBlock)?;
img_data
};
Ok(Self {
picture_type,
mime,
description,
width,
height,
bit_depth: depth,
color_count,
img_data,
})
}
}
impl FlacMetablockEncode for FlacPictureBlock {
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()
.unwrap()
}
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())?;
let mime = self.mime.to_string();
target.write_all(&u32::try_from(mime.len()).unwrap().to_be_bytes())?;
target.write_all(self.mime.to_string().as_bytes())?;
drop(mime);
target.write_all(&u32::try_from(self.description.len()).unwrap().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()).unwrap().to_be_bytes())?;
target.write_all(&self.img_data)?;
return Ok(());
}
}

View File

@@ -0,0 +1,50 @@
use std::fmt::Debug;
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
/// 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 {
fn get_len(&self) -> u32 {
self.data.len().try_into().unwrap()
}
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,218 @@
use std::io::{Cursor, Read};
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
/// 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,68 @@
//! FLAC errors
use crate::common::{
picturetype::PictureTypeError,
vorbiscomment::{VorbisCommentDecodeError, VorbisCommentEncodeError},
};
use std::string::FromUtf8Error;
use thiserror::Error;
#[allow(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("io error while reading flac")]
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")]
FailedStringDecode(#[from] FromUtf8Error),
/// We tried to read a block, but it was out of spec.
#[error("malformed flac block")]
MalformedBlock,
/// 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),
}
#[allow(missing_docs)]
#[derive(Debug, Error)]
pub enum FlacEncodeError {
/// We encountered an i/o error while processing
#[error("io error while encoding block")]
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,942 @@
//! 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
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

@@ -0,0 +1,208 @@
//! 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(_))) {
let x = self.last_block.take().unwrap();
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,
};
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

@@ -0,0 +1,5 @@
//! 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

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

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