pile-audio refactor
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
//! Components shared between many different formats
|
||||
|
||||
pub mod picturetype;
|
||||
pub mod tagtype;
|
||||
pub mod vorbiscomment;
|
||||
@@ -1,355 +0,0 @@
|
||||
//! Decode and write Vorbis comment blocks
|
||||
|
||||
use base64::Engine;
|
||||
use smartstring::{LazyCompact, SmartString};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{Cursor, Read, Write},
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
|
||||
use crate::flac::blocks::{FlacMetablockDecode, FlacMetablockEncode, FlacPictureBlock};
|
||||
|
||||
use super::tagtype::TagType;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[expect(missing_docs)]
|
||||
pub enum VorbisCommentDecodeError {
|
||||
/// We encountered an IoError while processing a block
|
||||
IoError(std::io::Error),
|
||||
|
||||
/// We tried to decode a string, but got invalid data
|
||||
FailedStringDecode(FromUtf8Error),
|
||||
|
||||
/// The given comment string isn't within spec
|
||||
MalformedCommentString(String),
|
||||
|
||||
/// The comment we're reading is invalid
|
||||
MalformedData,
|
||||
|
||||
/// We tried to decode picture data, but it was malformed.
|
||||
MalformedPicture,
|
||||
}
|
||||
|
||||
impl Display for VorbisCommentDecodeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::IoError(_) => write!(f, "io error while reading vorbis comments"),
|
||||
Self::FailedStringDecode(_) => {
|
||||
write!(f, "string decode error while reading vorbis comments")
|
||||
}
|
||||
Self::MalformedCommentString(x) => {
|
||||
write!(f, "malformed comment string `{x}`")
|
||||
}
|
||||
Self::MalformedData => {
|
||||
write!(f, "malformed comment data")
|
||||
}
|
||||
Self::MalformedPicture => {
|
||||
write!(f, "malformed picture data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VorbisCommentDecodeError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::IoError(x) => Some(x),
|
||||
Self::FailedStringDecode(x) => Some(x),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for VorbisCommentDecodeError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for VorbisCommentDecodeError {
|
||||
fn from(value: FromUtf8Error) -> Self {
|
||||
Self::FailedStringDecode(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[expect(missing_docs)]
|
||||
pub enum VorbisCommentEncodeError {
|
||||
/// We encountered an IoError while processing a block
|
||||
IoError(std::io::Error),
|
||||
|
||||
/// We could not encode picture data
|
||||
PictureEncodeError,
|
||||
}
|
||||
|
||||
impl Display for VorbisCommentEncodeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::IoError(_) => write!(f, "io error while reading vorbis comments"),
|
||||
Self::PictureEncodeError => {
|
||||
write!(f, "could not encode picture")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VorbisCommentEncodeError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::IoError(x) => Some(x),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for VorbisCommentEncodeError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded vorbis comment block
|
||||
#[derive(Debug)]
|
||||
pub struct VorbisComment {
|
||||
/// This comment's vendor string
|
||||
pub vendor: SmartString<LazyCompact>,
|
||||
|
||||
/// List of (tag, value)
|
||||
/// Repeated tags are allowed!
|
||||
pub comments: Vec<(TagType, SmartString<LazyCompact>)>,
|
||||
|
||||
/// A list of pictures found in this comment
|
||||
pub pictures: Vec<FlacPictureBlock>,
|
||||
}
|
||||
|
||||
impl VorbisComment {
|
||||
/// Try to decode the given data as a vorbis comment block
|
||||
pub fn decode(data: &[u8]) -> Result<Self, VorbisCommentDecodeError> {
|
||||
let mut d = Cursor::new(data);
|
||||
|
||||
// This is re-used whenever we need to read four bytes
|
||||
let mut block = [0u8; 4];
|
||||
|
||||
let vendor = {
|
||||
d.read_exact(&mut block)
|
||||
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
|
||||
|
||||
let length = u32::from_le_bytes(block);
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut text = vec![
|
||||
0u8;
|
||||
length
|
||||
.try_into()
|
||||
.expect("vendor length does not fit into usize")
|
||||
];
|
||||
|
||||
d.read_exact(&mut text)
|
||||
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
|
||||
|
||||
String::from_utf8(text)?
|
||||
};
|
||||
|
||||
d.read_exact(&mut block)
|
||||
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let n_comments: usize = u32::from_le_bytes(block)
|
||||
.try_into()
|
||||
.expect("comment count does not fit into usize");
|
||||
|
||||
let mut comments = Vec::new();
|
||||
let mut pictures = Vec::new();
|
||||
for _ in 0..n_comments {
|
||||
let comment = {
|
||||
d.read_exact(&mut block)
|
||||
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
|
||||
|
||||
let length = u32::from_le_bytes(block);
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut text = vec![
|
||||
0u8;
|
||||
length
|
||||
.try_into()
|
||||
.expect("comment length does not fit into usize")
|
||||
];
|
||||
|
||||
d.read_exact(&mut text)
|
||||
.map_err(|_err| VorbisCommentDecodeError::MalformedData)?;
|
||||
|
||||
String::from_utf8(text)?
|
||||
};
|
||||
let (var, val) =
|
||||
comment
|
||||
.split_once('=')
|
||||
.ok_or(VorbisCommentDecodeError::MalformedCommentString(
|
||||
comment.clone(),
|
||||
))?;
|
||||
|
||||
if !val.is_empty() {
|
||||
if var.to_uppercase() == "METADATA_BLOCK_PICTURE" {
|
||||
#[expect(clippy::map_err_ignore)]
|
||||
pictures.push(
|
||||
FlacPictureBlock::decode(
|
||||
&base64::prelude::BASE64_STANDARD
|
||||
.decode(val)
|
||||
.map_err(|_| VorbisCommentDecodeError::MalformedPicture)?,
|
||||
)
|
||||
.map_err(|_| VorbisCommentDecodeError::MalformedPicture)?,
|
||||
);
|
||||
} else {
|
||||
// Make sure empty strings are saved as "None"
|
||||
comments.push((
|
||||
match &var.to_uppercase()[..] {
|
||||
"TITLE" => TagType::TrackTitle,
|
||||
"ALBUM" => TagType::Album,
|
||||
"TRACKNUMBER" => TagType::TrackNumber,
|
||||
"ARTIST" => TagType::TrackArtist,
|
||||
"ALBUMARTIST" => TagType::AlbumArtist,
|
||||
"GENRE" => TagType::Genre,
|
||||
"ISRC" => TagType::Isrc,
|
||||
"DATE" => TagType::ReleaseDate,
|
||||
"TOTALTRACKS" => TagType::TrackTotal,
|
||||
"LYRICS" => TagType::Lyrics,
|
||||
x => TagType::Other(x.into()),
|
||||
},
|
||||
val.into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
vendor: vendor.into(),
|
||||
comments,
|
||||
pictures,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VorbisComment {
|
||||
/// Get the number of bytes that `encode()` will write.
|
||||
#[expect(clippy::expect_used)]
|
||||
pub fn get_len(&self) -> u32 {
|
||||
let mut sum: u32 = 0;
|
||||
sum += u32::try_from(self.vendor.len()).expect("vendor length does not fit into u32") + 4;
|
||||
sum += 4;
|
||||
|
||||
for (tagtype, value) in &self.comments {
|
||||
let tagtype_str = match tagtype {
|
||||
TagType::TrackTitle => "TITLE",
|
||||
TagType::Album => "ALBUM",
|
||||
TagType::TrackNumber => "TRACKNUMBER",
|
||||
TagType::TrackArtist => "ARTIST",
|
||||
TagType::AlbumArtist => "ALBUMARTIST",
|
||||
TagType::Genre => "GENRE",
|
||||
TagType::Isrc => "ISRC",
|
||||
TagType::ReleaseDate => "DATE",
|
||||
TagType::TrackTotal => "TOTALTRACKS",
|
||||
TagType::Lyrics => "LYRICS",
|
||||
TagType::Comment => "COMMENT",
|
||||
TagType::DiskNumber => "DISKNUMBER",
|
||||
TagType::DiskTotal => "DISKTOTAL",
|
||||
TagType::Year => "YEAR",
|
||||
TagType::Other(x) => x,
|
||||
}
|
||||
.to_uppercase();
|
||||
|
||||
let str = format!("{tagtype_str}={value}");
|
||||
sum +=
|
||||
4 + u32::try_from(str.len()).expect("comment string length does not fit into u32");
|
||||
}
|
||||
|
||||
for p in &self.pictures {
|
||||
// Compute b64 len
|
||||
let mut x = p.get_len();
|
||||
if x % 3 != 0 {
|
||||
x -= x % 3;
|
||||
x += 3;
|
||||
}
|
||||
|
||||
#[expect(clippy::integer_division)]
|
||||
{
|
||||
sum += 4 * (x / 3);
|
||||
}
|
||||
|
||||
// Add "METADATA_BLOCK_PICTURE="
|
||||
sum += 23;
|
||||
|
||||
// Add length bytes
|
||||
sum += 4;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// Try to encode this vorbis comment
|
||||
#[expect(clippy::expect_used)]
|
||||
pub fn encode(&self, target: &mut impl Write) -> Result<(), VorbisCommentEncodeError> {
|
||||
target.write_all(
|
||||
&u32::try_from(self.vendor.len())
|
||||
.expect("vendor length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(self.vendor.as_bytes())?;
|
||||
|
||||
target.write_all(
|
||||
&u32::try_from(self.comments.len() + self.pictures.len())
|
||||
.expect("total comment count does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
|
||||
for (tagtype, value) in &self.comments {
|
||||
let tagtype_str = match tagtype {
|
||||
TagType::TrackTitle => "TITLE",
|
||||
TagType::Album => "ALBUM",
|
||||
TagType::TrackNumber => "TRACKNUMBER",
|
||||
TagType::TrackArtist => "ARTIST",
|
||||
TagType::AlbumArtist => "ALBUMARTIST",
|
||||
TagType::Genre => "GENRE",
|
||||
TagType::Isrc => "ISRC",
|
||||
TagType::ReleaseDate => "DATE",
|
||||
TagType::TrackTotal => "TOTALTRACKS",
|
||||
TagType::Lyrics => "LYRICS",
|
||||
TagType::Comment => "COMMENT",
|
||||
TagType::DiskNumber => "DISKNUMBER",
|
||||
TagType::DiskTotal => "DISKTOTAL",
|
||||
TagType::Year => "YEAR",
|
||||
TagType::Other(x) => x,
|
||||
}
|
||||
.to_uppercase();
|
||||
|
||||
let str = format!("{tagtype_str}={value}");
|
||||
target.write_all(
|
||||
&u32::try_from(str.len())
|
||||
.expect("comment string length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(str.as_bytes())?;
|
||||
}
|
||||
|
||||
for p in &self.pictures {
|
||||
let mut pic_data = Vec::new();
|
||||
|
||||
#[expect(clippy::map_err_ignore)]
|
||||
p.encode(false, false, &mut pic_data)
|
||||
.map_err(|_| VorbisCommentEncodeError::PictureEncodeError)?;
|
||||
|
||||
let pic_string = format!(
|
||||
"METADATA_BLOCK_PICTURE={}",
|
||||
&base64::prelude::BASE64_STANDARD.encode(&pic_data)
|
||||
);
|
||||
|
||||
target.write_all(
|
||||
&u32::try_from(pic_string.len())
|
||||
.expect("picture string length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(pic_string.as_bytes())?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -1,867 +0,0 @@
|
||||
//! Strip metadata from a FLAC file without loading the whole thing into memory.
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{Cursor, Read, Seek, Write},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{
|
||||
blocks::{
|
||||
FlacAudioFrame, FlacCommentBlock, FlacMetablockDecode, FlacMetablockEncode,
|
||||
FlacMetablockHeader, FlacMetablockType,
|
||||
},
|
||||
errors::{FlacDecodeError, FlacEncodeError},
|
||||
};
|
||||
use crate::flac::blocks::{
|
||||
FlacApplicationBlock, FlacCuesheetBlock, FlacPaddingBlock, FlacPictureBlock,
|
||||
FlacSeektableBlock, FlacStreaminfoBlock,
|
||||
};
|
||||
|
||||
const MIN_AUDIO_FRAME_LEN: usize = 5000;
|
||||
|
||||
/// Select which blocks we want to keep.
|
||||
/// All values are `false` by default.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct FlacBlockSelector {
|
||||
/// Select `FlacMetablockType::Streaminfo` blocks.
|
||||
pub pick_streaminfo: bool,
|
||||
|
||||
/// Select `FlacMetablockType::Padding` blocks.
|
||||
pub pick_padding: bool,
|
||||
|
||||
/// Select `FlacMetablockType::Application` blocks.
|
||||
pub pick_application: bool,
|
||||
|
||||
/// Select `FlacMetablockType::SeekTable` blocks.
|
||||
pub pick_seektable: bool,
|
||||
|
||||
/// Select `FlacMetablockType::VorbisComment` blocks.
|
||||
pub pick_vorbiscomment: bool,
|
||||
|
||||
/// Select `FlacMetablockType::CueSheet` blocks.
|
||||
pub pick_cuesheet: bool,
|
||||
|
||||
/// Select `FlacMetablockType::Picture` blocks.
|
||||
pub pick_picture: bool,
|
||||
|
||||
/// Select audio frames.
|
||||
pub pick_audio: bool,
|
||||
}
|
||||
|
||||
impl FlacBlockSelector {
|
||||
/// Make a new [`FlacBlockSelector`]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn should_pick_meta(&self, block_type: FlacMetablockType) -> bool {
|
||||
match block_type {
|
||||
FlacMetablockType::Streaminfo => self.pick_streaminfo,
|
||||
FlacMetablockType::Padding => self.pick_padding,
|
||||
FlacMetablockType::Application => self.pick_application,
|
||||
FlacMetablockType::Seektable => self.pick_seektable,
|
||||
FlacMetablockType::VorbisComment => self.pick_vorbiscomment,
|
||||
FlacMetablockType::Cuesheet => self.pick_cuesheet,
|
||||
FlacMetablockType::Picture => self.pick_picture,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FlacBlockType {
|
||||
MagicBits {
|
||||
data: [u8; 4],
|
||||
left_to_read: usize,
|
||||
},
|
||||
MetablockHeader {
|
||||
is_first: bool,
|
||||
data: [u8; 4],
|
||||
left_to_read: usize,
|
||||
},
|
||||
MetaBlock {
|
||||
header: FlacMetablockHeader,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
AudioData {
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[expect(missing_docs)]
|
||||
pub enum FlacBlock {
|
||||
Streaminfo(FlacStreaminfoBlock),
|
||||
Picture(FlacPictureBlock),
|
||||
Padding(FlacPaddingBlock),
|
||||
Application(FlacApplicationBlock),
|
||||
SeekTable(FlacSeektableBlock),
|
||||
VorbisComment(FlacCommentBlock),
|
||||
CueSheet(FlacCuesheetBlock),
|
||||
AudioFrame(FlacAudioFrame),
|
||||
}
|
||||
|
||||
impl FlacBlock {
|
||||
/// Encode this block
|
||||
pub fn encode(
|
||||
&self,
|
||||
is_last: bool,
|
||||
with_header: bool,
|
||||
target: &mut impl Write,
|
||||
) -> Result<(), FlacEncodeError> {
|
||||
match self {
|
||||
Self::Streaminfo(b) => b.encode(is_last, with_header, target),
|
||||
Self::SeekTable(b) => b.encode(is_last, with_header, target),
|
||||
Self::Picture(b) => b.encode(is_last, with_header, target),
|
||||
Self::Padding(b) => b.encode(is_last, with_header, target),
|
||||
Self::Application(b) => b.encode(is_last, with_header, target),
|
||||
Self::VorbisComment(b) => b.encode(is_last, with_header, target),
|
||||
Self::CueSheet(b) => b.encode(is_last, with_header, target),
|
||||
Self::AudioFrame(b) => b.encode(target),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to decode the given data as a block
|
||||
pub fn decode(block_type: FlacMetablockType, data: &[u8]) -> Result<Self, FlacDecodeError> {
|
||||
Ok(match block_type {
|
||||
FlacMetablockType::Streaminfo => {
|
||||
FlacBlock::Streaminfo(FlacStreaminfoBlock::decode(data)?)
|
||||
}
|
||||
FlacMetablockType::Application => {
|
||||
FlacBlock::Application(FlacApplicationBlock::decode(data)?)
|
||||
}
|
||||
FlacMetablockType::Cuesheet => FlacBlock::CueSheet(FlacCuesheetBlock::decode(data)?),
|
||||
FlacMetablockType::Padding => FlacBlock::Padding(FlacPaddingBlock::decode(data)?),
|
||||
FlacMetablockType::Picture => FlacBlock::Picture(FlacPictureBlock::decode(data)?),
|
||||
FlacMetablockType::Seektable => FlacBlock::SeekTable(FlacSeektableBlock::decode(data)?),
|
||||
FlacMetablockType::VorbisComment => {
|
||||
FlacBlock::VorbisComment(FlacCommentBlock::decode(data)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An error produced by a [`FlacBlockReader`]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FlacBlockReaderError {
|
||||
/// Could not decode flac data
|
||||
#[error("decode error while reading flac blocks")]
|
||||
DecodeError(#[from] FlacDecodeError),
|
||||
|
||||
/// Tried to finish or push data to a finished reader.
|
||||
#[error("flac block reader is already finished")]
|
||||
AlreadyFinished,
|
||||
}
|
||||
|
||||
/// A buffered flac block reader.
|
||||
/// Use `push_data` to add flac data into this struct,
|
||||
/// use `pop_block` to read flac blocks.
|
||||
///
|
||||
/// This is the foundation of all other flac processors
|
||||
/// we offer in this crate.
|
||||
pub struct FlacBlockReader {
|
||||
// Which blocks should we return?
|
||||
selector: FlacBlockSelector,
|
||||
|
||||
// The block we're currently reading.
|
||||
// If this is `None`, we've called `finish()`.
|
||||
current_block: Option<FlacBlockType>,
|
||||
|
||||
// Blocks we pick go here
|
||||
output_blocks: VecDeque<FlacBlock>,
|
||||
}
|
||||
|
||||
impl FlacBlockReader {
|
||||
/// Pop the next block we've read, if any.
|
||||
pub fn pop_block(&mut self) -> Option<FlacBlock> {
|
||||
self.output_blocks.pop_front()
|
||||
}
|
||||
|
||||
/// If true, this reader has received all the data it needs.
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.current_block.is_none()
|
||||
}
|
||||
|
||||
/// If true, this reader has at least one block ready to pop.
|
||||
/// Calling `pop_block` will return `Some(_)` if this is true.
|
||||
pub fn has_block(&self) -> bool {
|
||||
!self.output_blocks.is_empty()
|
||||
}
|
||||
|
||||
/// Make a new [`FlacBlockReader`].
|
||||
pub fn new(selector: FlacBlockSelector) -> Self {
|
||||
Self {
|
||||
selector,
|
||||
current_block: Some(FlacBlockType::MagicBits {
|
||||
data: [0; 4],
|
||||
left_to_read: 4,
|
||||
}),
|
||||
|
||||
output_blocks: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pass the given data through this block extractor.
|
||||
/// Output data is stored in an internal buffer, and should be accessed
|
||||
/// through `Read`.
|
||||
pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> {
|
||||
let mut buf = Cursor::new(buf);
|
||||
let mut last_read_size = 1;
|
||||
|
||||
if self.current_block.is_none() {
|
||||
return Err(FlacBlockReaderError::AlreadyFinished);
|
||||
}
|
||||
|
||||
'outer: while last_read_size != 0 {
|
||||
#[expect(clippy::expect_used)]
|
||||
match self
|
||||
.current_block
|
||||
.as_mut()
|
||||
.expect("current_block is Some, checked above")
|
||||
{
|
||||
FlacBlockType::MagicBits { data, left_to_read } => {
|
||||
last_read_size = buf
|
||||
.read(&mut data[4 - *left_to_read..4])
|
||||
.map_err(FlacDecodeError::from)?;
|
||||
*left_to_read -= last_read_size;
|
||||
|
||||
if *left_to_read == 0 {
|
||||
if *data != [0x66, 0x4C, 0x61, 0x43] {
|
||||
return Err(FlacDecodeError::BadMagicBytes.into());
|
||||
};
|
||||
|
||||
self.current_block = Some(FlacBlockType::MetablockHeader {
|
||||
is_first: true,
|
||||
data: [0; 4],
|
||||
left_to_read: 4,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
FlacBlockType::MetablockHeader {
|
||||
is_first,
|
||||
data,
|
||||
left_to_read,
|
||||
} => {
|
||||
last_read_size = buf
|
||||
.read(&mut data[4 - *left_to_read..4])
|
||||
.map_err(FlacDecodeError::from)?;
|
||||
*left_to_read -= last_read_size;
|
||||
|
||||
if *left_to_read == 0 {
|
||||
let header = FlacMetablockHeader::decode(data)?;
|
||||
if *is_first && !matches!(header.block_type, FlacMetablockType::Streaminfo)
|
||||
{
|
||||
return Err(FlacDecodeError::BadFirstBlock.into());
|
||||
}
|
||||
|
||||
self.current_block = Some(FlacBlockType::MetaBlock {
|
||||
header,
|
||||
data: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
FlacBlockType::MetaBlock { header, data } => {
|
||||
#[expect(clippy::expect_used)]
|
||||
{
|
||||
last_read_size = buf
|
||||
.by_ref()
|
||||
.take(
|
||||
u64::from(header.length)
|
||||
- u64::try_from(data.len())
|
||||
.expect("data length does not fit into u64"),
|
||||
)
|
||||
.read_to_end(data)
|
||||
.map_err(FlacDecodeError::from)?;
|
||||
}
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
if data.len()
|
||||
== usize::try_from(header.length)
|
||||
.expect("header length does not fit into usize")
|
||||
{
|
||||
// If we picked this block type, add it to the queue
|
||||
if self.selector.should_pick_meta(header.block_type) {
|
||||
let b = FlacBlock::decode(header.block_type, data)?;
|
||||
self.output_blocks.push_back(b);
|
||||
}
|
||||
|
||||
// Start next block
|
||||
if header.is_last {
|
||||
self.current_block = Some(FlacBlockType::AudioData { data: Vec::new() })
|
||||
} else {
|
||||
self.current_block = Some(FlacBlockType::MetablockHeader {
|
||||
is_first: false,
|
||||
data: [0; 4],
|
||||
left_to_read: 4,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlacBlockType::AudioData { data } => {
|
||||
// Limit the number of bytes we read at once, so we don't re-clone
|
||||
// large amounts of data if `buf` contains multiple sync sequences.
|
||||
// 5kb is a pretty reasonable frame size.
|
||||
last_read_size = buf
|
||||
.by_ref()
|
||||
.take(5_000)
|
||||
.read_to_end(data)
|
||||
.map_err(FlacDecodeError::from)?;
|
||||
if last_read_size == 0 {
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
// We can't run checks if we don't have enough data.
|
||||
if data.len() <= 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check frame sync header
|
||||
// (`if` makes sure we only do this once)
|
||||
if data.len() - last_read_size <= 2
|
||||
&& !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000)
|
||||
{
|
||||
return Err(FlacDecodeError::BadSyncBytes.into());
|
||||
}
|
||||
|
||||
if data.len() >= MIN_AUDIO_FRAME_LEN {
|
||||
// Look for a frame sync header in the data we read
|
||||
//
|
||||
// This isn't the *correct* way to split audio frames (false sync bytes can occur in audio data),
|
||||
// but it's good enough for now---we don't decode audio data anyway.
|
||||
//
|
||||
// We could split on every sequence of sync bytes, but that's not any less wrong than the approach here.
|
||||
// Also, it's slower---we'd rather have few large frames than many small ones.
|
||||
|
||||
let first_byte = if data.len() - last_read_size < MIN_AUDIO_FRAME_LEN {
|
||||
MIN_AUDIO_FRAME_LEN + 1
|
||||
} else {
|
||||
data.len() - last_read_size + MIN_AUDIO_FRAME_LEN + 1
|
||||
};
|
||||
|
||||
// `i` is the index of the first byte *after* the sync sequence.
|
||||
//
|
||||
// This may seem odd, but it makes the odd edge case easier to handle:
|
||||
// If we instead have `i` be the index of the first byte *of* the frame sequence,
|
||||
// dealing with the case where `data` contained half the sync sequence before
|
||||
// reading is tricky.
|
||||
for i in first_byte..data.len() {
|
||||
if data[i - 2] == 0b1111_1111
|
||||
&& data[i - 1] & 0b1111_1100 == 0b1111_1000
|
||||
{
|
||||
// We found another frame sync header. Split at this index.
|
||||
if self.selector.pick_audio {
|
||||
self.output_blocks.push_back(FlacBlock::AudioFrame(
|
||||
FlacAudioFrame::decode(&data[0..i - 2])?,
|
||||
));
|
||||
}
|
||||
|
||||
// Backtrack to the first bit AFTER this new sync sequence
|
||||
buf.seek(std::io::SeekFrom::Current(
|
||||
-i64::try_from(data.len() - i)
|
||||
.expect("seek offset does not fit into i64"),
|
||||
))
|
||||
.map_err(FlacDecodeError::from)?;
|
||||
|
||||
self.current_block = Some(FlacBlockType::AudioData {
|
||||
data: {
|
||||
let mut v = Vec::with_capacity(MIN_AUDIO_FRAME_LEN);
|
||||
v.extend(&data[i - 2..i]);
|
||||
v
|
||||
},
|
||||
});
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Finish reading data.
|
||||
/// This tells the reader that it has received the entire stream.
|
||||
///
|
||||
/// `finish()` should be called exactly once once we have finished each stream.
|
||||
/// Finishing twice or pushing data to a finished reader results in a panic.
|
||||
pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> {
|
||||
match self.current_block.take() {
|
||||
None => return Err(FlacBlockReaderError::AlreadyFinished),
|
||||
|
||||
Some(FlacBlockType::AudioData { data }) => {
|
||||
// We can't run checks if we don't have enough data.
|
||||
if data.len() <= 2 {
|
||||
return Err(FlacDecodeError::MalformedBlock.into());
|
||||
}
|
||||
|
||||
if !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000) {
|
||||
return Err(FlacDecodeError::BadSyncBytes.into());
|
||||
}
|
||||
|
||||
if self.selector.pick_audio {
|
||||
self.output_blocks
|
||||
.push_back(FlacBlock::AudioFrame(FlacAudioFrame::decode(&data)?));
|
||||
}
|
||||
|
||||
self.current_block = None;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// All other blocks have a known length and
|
||||
// are finished automatically.
|
||||
_ => return Err(FlacDecodeError::MalformedBlock.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
use paste::paste;
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{io::Write, ops::Range, str::FromStr};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::tagtype::TagType,
|
||||
flac::tests::{FlacBlockOutput, FlacTestCase, VorbisCommentTestValue, manifest},
|
||||
};
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn read_file(
|
||||
test_case: &FlacTestCase,
|
||||
fragment_size_range: Option<Range<usize>>,
|
||||
selector: FlacBlockSelector,
|
||||
) -> Result<Vec<FlacBlock>, FlacBlockReaderError> {
|
||||
let file_data = std::fs::read(test_case.get_path()).unwrap();
|
||||
|
||||
// Make sure input file is correct
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&file_data);
|
||||
assert_eq!(
|
||||
test_case.get_in_hash(),
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
);
|
||||
|
||||
let mut reader = FlacBlockReader::new(selector);
|
||||
let mut out_blocks = Vec::new();
|
||||
|
||||
// Push file data to the reader, in parts or as a whole.
|
||||
if let Some(fragment_size_range) = fragment_size_range {
|
||||
let mut head = 0;
|
||||
while head < file_data.len() {
|
||||
let mut frag_size = rand::rng().random_range(fragment_size_range.clone());
|
||||
if head + frag_size > file_data.len() {
|
||||
frag_size = file_data.len() - head;
|
||||
}
|
||||
reader.push_data(&file_data[head..head + frag_size])?;
|
||||
head += frag_size;
|
||||
}
|
||||
} else {
|
||||
reader.push_data(&file_data)?;
|
||||
}
|
||||
|
||||
reader.finish()?;
|
||||
while let Some(b) = reader.pop_block() {
|
||||
out_blocks.push(b)
|
||||
}
|
||||
|
||||
return Ok(out_blocks);
|
||||
}
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn test_identical(
|
||||
test_case: &FlacTestCase,
|
||||
fragment_size_range: Option<Range<usize>>,
|
||||
) -> Result<(), FlacBlockReaderError> {
|
||||
let out_blocks = read_file(
|
||||
test_case,
|
||||
fragment_size_range,
|
||||
FlacBlockSelector {
|
||||
pick_streaminfo: true,
|
||||
pick_padding: true,
|
||||
pick_application: true,
|
||||
pick_seektable: true,
|
||||
pick_vorbiscomment: true,
|
||||
pick_cuesheet: true,
|
||||
pick_picture: true,
|
||||
pick_audio: true,
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
out.write_all(&[0x66, 0x4C, 0x61, 0x43]).unwrap();
|
||||
|
||||
for i in 0..out_blocks.len() {
|
||||
let b = &out_blocks[i];
|
||||
let is_last = if i == out_blocks.len() - 1 {
|
||||
false
|
||||
} else {
|
||||
!matches!(b, FlacBlock::AudioFrame(_))
|
||||
&& matches!(&out_blocks[i + 1], FlacBlock::AudioFrame(_))
|
||||
};
|
||||
|
||||
b.encode(is_last, true, &mut out).unwrap();
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&out);
|
||||
let result = hasher.finalize().map(|x| format!("{x:02x}")).join("");
|
||||
assert_eq!(result, test_case.get_in_hash(), "Output hash doesn't match");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn test_blockread(
|
||||
test_case: &FlacTestCase,
|
||||
fragment_size_range: Option<Range<usize>>,
|
||||
) -> Result<(), FlacBlockReaderError> {
|
||||
let out_blocks = read_file(
|
||||
test_case,
|
||||
fragment_size_range,
|
||||
FlacBlockSelector {
|
||||
pick_streaminfo: true,
|
||||
pick_padding: true,
|
||||
pick_application: true,
|
||||
pick_seektable: true,
|
||||
pick_vorbiscomment: true,
|
||||
pick_cuesheet: true,
|
||||
pick_picture: true,
|
||||
pick_audio: true,
|
||||
},
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
test_case.get_blocks().unwrap().len(),
|
||||
out_blocks
|
||||
.iter()
|
||||
.filter(|x| !matches!(*x, FlacBlock::AudioFrame(_)))
|
||||
.count(),
|
||||
"Number of blocks didn't match"
|
||||
);
|
||||
|
||||
let mut audio_data_hasher = Sha256::new();
|
||||
let mut result_i = 0;
|
||||
|
||||
for b in out_blocks {
|
||||
match b {
|
||||
FlacBlock::Streaminfo(s) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size,
|
||||
max_block_size,
|
||||
min_frame_size,
|
||||
max_frame_size,
|
||||
sample_rate,
|
||||
channels,
|
||||
bits_per_sample,
|
||||
total_samples,
|
||||
md5_signature,
|
||||
} => {
|
||||
assert_eq!(*min_block_size, s.min_block_size,);
|
||||
assert_eq!(*max_block_size, s.max_block_size);
|
||||
assert_eq!(*min_frame_size, s.min_frame_size);
|
||||
assert_eq!(*max_frame_size, s.max_frame_size);
|
||||
assert_eq!(*sample_rate, s.sample_rate);
|
||||
assert_eq!(*channels, s.channels);
|
||||
assert_eq!(*bits_per_sample, s.bits_per_sample);
|
||||
assert_eq!(*total_samples, s.total_samples);
|
||||
assert_eq!(
|
||||
*md5_signature,
|
||||
s.md5_signature.iter().map(|x| format!("{x:02x}")).join("")
|
||||
);
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::Application(a) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Application {
|
||||
application_id,
|
||||
hash,
|
||||
} => {
|
||||
assert_eq!(
|
||||
*application_id, a.application_id,
|
||||
"Application id doesn't match"
|
||||
);
|
||||
assert_eq!(
|
||||
*hash,
|
||||
{
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(&a.data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
},
|
||||
"Application content hash doesn't match"
|
||||
);
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::CueSheet(c) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::CueSheet { hash } => {
|
||||
assert_eq!(*hash, {
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(&c.data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
});
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::Padding(p) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Padding { size } => {
|
||||
assert_eq!(p.size, *size);
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::SeekTable(t) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Seektable { hash } => {
|
||||
assert_eq!(*hash, {
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(&t.data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
});
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::Picture(p) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type,
|
||||
mime,
|
||||
description,
|
||||
width,
|
||||
height,
|
||||
bit_depth,
|
||||
color_count,
|
||||
img_data,
|
||||
} => {
|
||||
assert_eq!(*picture_type, p.picture_type, "{}", test_case.get_name());
|
||||
assert_eq!(*mime, p.mime, "{}", test_case.get_name());
|
||||
assert_eq!(*description, p.description, "{}", test_case.get_name());
|
||||
assert_eq!(*width, p.width, "{}", test_case.get_name());
|
||||
assert_eq!(*height, p.height, "{}", test_case.get_name());
|
||||
assert_eq!(*bit_depth, p.bit_depth, "{}", test_case.get_name());
|
||||
assert_eq!(*color_count, p.color_count, "{}", test_case.get_name());
|
||||
assert_eq!(
|
||||
*img_data,
|
||||
{
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(&p.img_data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
},
|
||||
"{}",
|
||||
test_case.get_name()
|
||||
);
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::VorbisComment(v) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor,
|
||||
comments,
|
||||
pictures,
|
||||
} => {
|
||||
assert_eq!(*vendor, v.comment.vendor, "Comment vendor doesn't match");
|
||||
|
||||
assert_eq!(
|
||||
v.comment.pictures.len(),
|
||||
pictures.len(),
|
||||
"Number of pictures doesn't match"
|
||||
);
|
||||
|
||||
for (p, e) in v.comment.pictures.iter().zip(*pictures) {
|
||||
match e {
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type,
|
||||
mime,
|
||||
description,
|
||||
width,
|
||||
height,
|
||||
bit_depth,
|
||||
color_count,
|
||||
img_data,
|
||||
} => {
|
||||
assert_eq!(*picture_type, p.picture_type);
|
||||
assert_eq!(*mime, p.mime);
|
||||
assert_eq!(*description, p.description);
|
||||
assert_eq!(*width, p.width);
|
||||
assert_eq!(*height, p.height);
|
||||
assert_eq!(*bit_depth, p.bit_depth);
|
||||
assert_eq!(*color_count, p.color_count);
|
||||
assert_eq!(*img_data, {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&p.img_data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
});
|
||||
}
|
||||
_ => panic!("Bad test data: expected only Picture blocks."),
|
||||
}
|
||||
}
|
||||
|
||||
match comments {
|
||||
VorbisCommentTestValue::Raw { tags } => {
|
||||
assert_eq!(
|
||||
v.comment.comments.len(),
|
||||
tags.len(),
|
||||
"Number of comments doesn't match"
|
||||
);
|
||||
|
||||
for ((got_tag, got_val), (exp_tag, exp_val)) in
|
||||
v.comment.comments.iter().zip(*tags)
|
||||
{
|
||||
assert_eq!(
|
||||
*got_tag,
|
||||
TagType::from_str(exp_tag).unwrap(),
|
||||
"Tag key doesn't match"
|
||||
);
|
||||
assert_eq!(
|
||||
got_val, exp_val,
|
||||
"Tag value of {exp_tag} doesn't match"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VorbisCommentTestValue::Hash { n_comments, hash } => {
|
||||
assert_eq!(
|
||||
v.comment.comments.len(),
|
||||
*n_comments,
|
||||
"Number of comments doesn't match"
|
||||
);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
for (got_tag, got_val) in v.comment.comments {
|
||||
hasher.update(format!("{got_tag}={got_val};").as_bytes());
|
||||
}
|
||||
assert_eq!(
|
||||
&hasher.finalize().map(|x| format!("{x:02x}")).join(""),
|
||||
hash,
|
||||
"Comment hash doesn't match"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::AudioFrame(data) => {
|
||||
let mut vec = Vec::new();
|
||||
data.encode(&mut vec).unwrap();
|
||||
audio_data_hasher.update(&vec);
|
||||
|
||||
if result_i != test_case.get_blocks().unwrap().len() {
|
||||
panic!("There are metadata blocks between audio frames!")
|
||||
}
|
||||
|
||||
// Don't increment result_i
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result_i += 1;
|
||||
}
|
||||
|
||||
// Check audio data hash
|
||||
assert_eq!(
|
||||
test_case.get_audio_hash().unwrap(),
|
||||
audio_data_hasher
|
||||
.finalize()
|
||||
.map(|x| format!("{x:02x}"))
|
||||
.join("")
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Helper macros to generate tests
|
||||
macro_rules! gen_tests {
|
||||
( $test_name:ident ) => {
|
||||
paste! {
|
||||
#[test]
|
||||
pub fn [<blockread_small_ $test_name>]() {
|
||||
let manifest = manifest();
|
||||
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
|
||||
|
||||
match test_case {
|
||||
FlacTestCase::Success { .. } => {
|
||||
for _ in 0..5 {
|
||||
test_blockread(
|
||||
test_case,
|
||||
Some(1..256),
|
||||
).unwrap()
|
||||
}
|
||||
},
|
||||
|
||||
FlacTestCase::Error { check_error, .. } => {
|
||||
let e = test_blockread(test_case, Some(1..256)).unwrap_err();
|
||||
match e {
|
||||
FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
|
||||
_ => panic!("Unexpected error {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn [<identical_small_ $test_name>]() {
|
||||
let manifest = manifest();
|
||||
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
|
||||
|
||||
match test_case {
|
||||
FlacTestCase::Success { .. } => {
|
||||
for _ in 0..5 {
|
||||
test_identical(
|
||||
test_case,
|
||||
Some(1..256),
|
||||
).unwrap()
|
||||
}
|
||||
},
|
||||
|
||||
FlacTestCase::Error { check_error, .. } => {
|
||||
let e = test_identical(test_case, Some(1..256)).unwrap_err();
|
||||
match e {
|
||||
FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
|
||||
_ => panic!("Unexpected error {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
gen_tests!(custom_01);
|
||||
gen_tests!(custom_02);
|
||||
gen_tests!(custom_03);
|
||||
|
||||
gen_tests!(uncommon_10);
|
||||
|
||||
gen_tests!(faulty_06);
|
||||
gen_tests!(faulty_07);
|
||||
gen_tests!(faulty_10);
|
||||
gen_tests!(faulty_11);
|
||||
|
||||
gen_tests!(subset_45);
|
||||
gen_tests!(subset_46);
|
||||
gen_tests!(subset_47);
|
||||
gen_tests!(subset_48);
|
||||
gen_tests!(subset_49);
|
||||
gen_tests!(subset_50);
|
||||
gen_tests!(subset_51);
|
||||
gen_tests!(subset_52);
|
||||
gen_tests!(subset_53);
|
||||
gen_tests!(subset_54);
|
||||
gen_tests!(subset_55);
|
||||
gen_tests!(subset_56);
|
||||
gen_tests!(subset_57);
|
||||
gen_tests!(subset_58);
|
||||
gen_tests!(subset_59);
|
||||
}
|
||||
@@ -1,943 +0,0 @@
|
||||
//! Parse FLAC metadata.
|
||||
|
||||
pub mod blockread;
|
||||
pub mod blocks;
|
||||
pub mod errors;
|
||||
pub mod proc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use itertools::Itertools;
|
||||
use mime::Mime;
|
||||
|
||||
use super::errors::FlacDecodeError;
|
||||
use crate::common::{picturetype::PictureType, vorbiscomment::VorbisCommentDecodeError};
|
||||
|
||||
/// The value of a vorbis comment.
|
||||
///
|
||||
/// Some files have VERY large comments, and providing them
|
||||
/// explicitly here doesn't make sense.
|
||||
#[derive(Clone)]
|
||||
pub enum VorbisCommentTestValue {
|
||||
/// The comments, in order
|
||||
Raw {
|
||||
tags: &'static [(&'static str, &'static str)],
|
||||
},
|
||||
/// The hash of all comments concatenated together,
|
||||
/// stringified as `{key}={value};`
|
||||
Hash {
|
||||
n_comments: usize,
|
||||
hash: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum FlacBlockOutput {
|
||||
Application {
|
||||
application_id: u32,
|
||||
hash: &'static str,
|
||||
},
|
||||
Streaminfo {
|
||||
min_block_size: u32,
|
||||
max_block_size: u32,
|
||||
min_frame_size: u32,
|
||||
max_frame_size: u32,
|
||||
sample_rate: u32,
|
||||
channels: u8,
|
||||
bits_per_sample: u8,
|
||||
total_samples: u128,
|
||||
md5_signature: &'static str,
|
||||
},
|
||||
CueSheet {
|
||||
// Hash of this block's data, without the header.
|
||||
// This is easy to get with
|
||||
//
|
||||
// ```notrust
|
||||
// metaflac \
|
||||
// --list \
|
||||
// --block-number=<n> \
|
||||
// --data-format=binary-headerless \
|
||||
// <file> \
|
||||
// | sha256sum
|
||||
//```
|
||||
hash: &'static str,
|
||||
},
|
||||
Seektable {
|
||||
hash: &'static str,
|
||||
},
|
||||
Padding {
|
||||
size: u32,
|
||||
},
|
||||
Picture {
|
||||
picture_type: PictureType,
|
||||
mime: Mime,
|
||||
description: &'static str,
|
||||
width: u32,
|
||||
height: u32,
|
||||
bit_depth: u32,
|
||||
color_count: u32,
|
||||
img_data: &'static str,
|
||||
},
|
||||
VorbisComment {
|
||||
vendor: &'static str,
|
||||
comments: VorbisCommentTestValue,
|
||||
pictures: &'static [FlacBlockOutput],
|
||||
},
|
||||
}
|
||||
|
||||
pub enum FlacTestCase {
|
||||
Success {
|
||||
/// This test's name
|
||||
test_name: &'static str,
|
||||
|
||||
/// The file to use for this test
|
||||
file_path: &'static str,
|
||||
|
||||
/// The hash of the input files
|
||||
in_hash: &'static str,
|
||||
|
||||
/// The flac metablocks we expect to find in this file, in order.
|
||||
blocks: Vec<FlacBlockOutput>,
|
||||
|
||||
/// The hash of the audio frames in this file
|
||||
///
|
||||
/// Get this hash by running `metaflac --remove-all --dont-use-padding`,
|
||||
/// then by manually deleting remaining headers in a hex editor
|
||||
/// (Remember that the sync sequence is 0xFF 0xF8)
|
||||
audio_hash: &'static str,
|
||||
|
||||
/// The hash we should get when we strip this file's tags.
|
||||
///
|
||||
/// A stripped flac file has unmodified STREAMINFO, SEEKTABLE,
|
||||
/// CUESHEET, and audio data blocks; and nothing else (not even padding).
|
||||
///
|
||||
/// Reference implementation:
|
||||
/// ```notrust
|
||||
/// metaflac \
|
||||
/// --remove \
|
||||
/// --block-type=PADDING,APPLICATION,VORBIS_COMMENT,PICTURE \
|
||||
/// --dont-use-padding \
|
||||
/// <file>
|
||||
/// ```
|
||||
stripped_hash: &'static str,
|
||||
},
|
||||
Error {
|
||||
/// This test's name
|
||||
test_name: &'static str,
|
||||
|
||||
/// The file to use for this test
|
||||
file_path: &'static str,
|
||||
|
||||
/// The hash of the input files
|
||||
in_hash: &'static str,
|
||||
|
||||
/// The error we should encounter while reading this file
|
||||
check_error: &'static dyn Fn(&FlacDecodeError) -> bool,
|
||||
|
||||
/// If some, stripping this file's metadata should produce the given hash.
|
||||
/// If none, trying to strip metadata should produce `check_error`
|
||||
stripped_hash: Option<&'static str>,
|
||||
|
||||
/// If some, the following images should be extracted from this file
|
||||
/// If none, trying to strip images should produce `check_error`
|
||||
pictures: Option<Vec<FlacBlockOutput>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl FlacTestCase {
|
||||
pub fn get_name(&self) -> &str {
|
||||
match self {
|
||||
Self::Error { test_name, .. } | Self::Success { test_name, .. } => test_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_path(&self) -> &str {
|
||||
match self {
|
||||
Self::Success { file_path, .. } | Self::Error { file_path, .. } => file_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_in_hash(&self) -> &str {
|
||||
match self {
|
||||
Self::Success { in_hash, .. } | Self::Error { in_hash, .. } => in_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_stripped_hash(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Success { stripped_hash, .. } => Some(stripped_hash),
|
||||
Self::Error { stripped_hash, .. } => *stripped_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_audio_hash(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Success { audio_hash, .. } => Some(audio_hash),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_blocks(&self) -> Option<&[FlacBlockOutput]> {
|
||||
match self {
|
||||
Self::Success { blocks, .. } => Some(blocks),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_pictures(&self) -> Option<Vec<FlacBlockOutput>> {
|
||||
match self {
|
||||
Self::Success { blocks, .. } => {
|
||||
let mut out = Vec::new();
|
||||
for b in blocks {
|
||||
match b {
|
||||
FlacBlockOutput::Picture { .. } => out.push(b.clone()),
|
||||
FlacBlockOutput::VorbisComment { pictures, .. } => {
|
||||
for p in *pictures {
|
||||
out.push(p.clone())
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
return Some(out);
|
||||
}
|
||||
|
||||
Self::Error { pictures, .. } => {
|
||||
pictures.as_ref().map(|x| x.iter().cloned().collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of test files and their expected output
|
||||
#[expect(clippy::unwrap_used)]
|
||||
pub fn manifest() -> [FlacTestCase; 23] {
|
||||
[
|
||||
FlacTestCase::Error {
|
||||
test_name: "uncommon_10",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_uncommon/10 - file starting at frame header.flac"
|
||||
),
|
||||
in_hash: "d95f63e8101320f5ac7ffe249bc429a209eb0e10996a987301eaa63386a8faa1",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::BadMagicBytes),
|
||||
stripped_hash: None,
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "faulty_06",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac"
|
||||
),
|
||||
in_hash: "53aed5e7fde7a652b82ba06a8382b2612b02ebbde7b0d2016276644d17cc76cd",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock),
|
||||
stripped_hash: None,
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "faulty_07",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac"
|
||||
),
|
||||
in_hash: "6d46725991ba5da477187fde7709ea201c399d00027257c365d7301226d851ea",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock),
|
||||
stripped_hash: None,
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "faulty_10",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac"
|
||||
),
|
||||
in_hash: "c79b0514a61634035a5653c5493797bbd1fcc78982116e4d429630e9e462d29b",
|
||||
check_error: &|x| {
|
||||
matches!(
|
||||
x,
|
||||
FlacDecodeError::VorbisComment(VorbisCommentDecodeError::MalformedData)
|
||||
)
|
||||
},
|
||||
// This file's vorbis comment is invalid, but that shouldn't stop us from removing it.
|
||||
// As a general rule, we should NOT encounter an error when stripping invalid blocks.
|
||||
//
|
||||
// We should, however, get errors when we try to strip flac files with invalid *structure*
|
||||
// (For example, the out-of-order streaminfo test in faulty_07).
|
||||
stripped_hash: Some(
|
||||
"4b994f82dc1699a58e2b127058b37374220ee41dc294d4887ac14f056291a1b0",
|
||||
),
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "faulty_11",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_faulty/11 - incorrect metadata block length.flac"
|
||||
),
|
||||
in_hash: "3732151ba8c4e66a785165aa75a444aad814c16807ddc97b793811376acacfd6",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::BadMetablockType(127)),
|
||||
stripped_hash: None,
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_45",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/45 - no total number of samples set.flac"
|
||||
),
|
||||
in_hash: "336a18eb7a78f7fc0ab34980348e2895bc3f82db440a2430d9f92e996f889f9a",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 907,
|
||||
max_frame_size: 8053,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 0,
|
||||
md5_signature: "c41ae3b82c35d8f5c3dab1729f948fde",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "3fb3482ebc1724559bdd57f34de472458563d78a676029614e76e32b5d2b8816",
|
||||
stripped_hash: "31631ac227ebe2689bac7caa1fa964b47e71a9f1c9c583a04ea8ebd9371508d0",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_46",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/46 - no min-max framesize set.flac"
|
||||
),
|
||||
in_hash: "9dc39732ce17815832790901b768bb50cd5ff0cd21b28a123c1cabc16ed776cc",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 0,
|
||||
max_frame_size: 0,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 282866,
|
||||
md5_signature: "fd131e6ebc75251ed83f8f4c07df36a4",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "a1eed422462b386a932b9eb3dff3aea3687b41eca919624fb574aadb7eb50040",
|
||||
stripped_hash: "9e57cd77f285fc31f87fa4e3a31ab8395d68d5482e174c8e0d0bba9a0c20ba27",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_47",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/47 - only STREAMINFO.flac"
|
||||
),
|
||||
in_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85",
|
||||
blocks: vec![FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 4747,
|
||||
max_frame_size: 7034,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 232608,
|
||||
md5_signature: "bba30c5f70789910e404b7ac727c3853",
|
||||
}],
|
||||
audio_hash: "5ee1450058254087f58c91baf0f70d14bde8782cf2dc23c741272177fe0fce6e",
|
||||
stripped_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_48",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac"
|
||||
),
|
||||
in_hash: "4417aca6b5f90971c50c28766d2f32b3acaa7f9f9667bd313336242dae8b2531",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 2445,
|
||||
max_frame_size: 7364,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 259884,
|
||||
md5_signature: "97a0574290237563fbaa788ad77d2cdf",
|
||||
},
|
||||
FlacBlockOutput::Seektable {
|
||||
hash: "21ca2184ae22fe26b690fd7cbd8d25fcde1d830ff6e5796ced4107bab219d7c0",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "c2d691f2c4c986fe3cd5fd7864d9ba9ce6dd68a4ffc670447f008434b13102c2",
|
||||
stripped_hash: "abc9a0c40a29c896bc6e1cc0b374db1c8e157af716a5a3c43b7db1591a74c4e8",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_49",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/49 - Extremely large PADDING.flac",
|
||||
),
|
||||
in_hash: "7bc44fa2754536279fde4f8fb31d824f43b8d0b3f93d27d055d209682914f20e",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 1353,
|
||||
max_frame_size: 7117,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 258939,
|
||||
md5_signature: "6e78f221caaaa5d570a53f1714d84ded",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Padding { size: 16777215 },
|
||||
],
|
||||
audio_hash: "5007be7109b28b0149d1b929d2a0e93a087381bd3e68cf2a3ef78ea265ea20c3",
|
||||
stripped_hash: "a2283bbacbc4905ad3df1bf9f43a0ea7aa65cf69523d84a7dd8eb54553cc437e",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_50",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/50 - Extremely large PICTURE.flac"
|
||||
),
|
||||
in_hash: "1f04f237d74836104993a8072d4223e84a5d3bd76fbc44555c221c7e69a23594",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 5099,
|
||||
max_frame_size: 7126,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 265617,
|
||||
md5_signature: "82164e4da30ed43b47e6027cef050648",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "",
|
||||
width: 3200,
|
||||
height: 2252,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
|
||||
},
|
||||
],
|
||||
audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c",
|
||||
stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_51",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac"
|
||||
),
|
||||
in_hash: "033160e8124ed287b0b5d615c94ac4139477e47d6e4059b1c19b7141566f5ef9",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 4531,
|
||||
max_frame_size: 7528,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 289972,
|
||||
md5_signature: "5ff622c88f8dd9bc201a6a541f3890d3",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Hash {
|
||||
n_comments: 39,
|
||||
hash: "01984e9ec0cfad41f27b3b4e84184966f6725ead84b7815bd0b3313549ee4229",
|
||||
},
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "76419865d10eb22a74f020423a4e515e800f0177441676afd0418557c2d76c36",
|
||||
stripped_hash: "c0ca6c6099b5d9ec53d6bb370f339b2b1570055813a6cd3616fac2db83a2185e",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_52",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/52 - Extremely large APPLICATION.flac"
|
||||
),
|
||||
in_hash: "0e45a4f8dbef15cbebdd8dfe690d8ae60e0c6abb596db1270a9161b62a7a3f1c",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 3711,
|
||||
max_frame_size: 7056,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 317876,
|
||||
md5_signature: "eb7140266bc194527488c21ab49bc47b",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Application {
|
||||
application_id: 0x74657374,
|
||||
hash: "cfc0b8969e4ba6bd507999ba89dea2d274df69d94749d6ae3cf117a7780bba09",
|
||||
},
|
||||
],
|
||||
audio_hash: "89ad1a5c86a9ef35d33189c81c8a90285a23964a13f8325bf2c02043e8c83d63",
|
||||
stripped_hash: "cc4a0afb95ec9bcde8ee33f13951e494dc4126a9a3a668d79c80ce3c14a3acd9",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_53",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac"
|
||||
),
|
||||
in_hash: "513fad18578f3225fae5de1bda8f700415be6fd8aa1e7af533b5eb796ed2d461",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 2798,
|
||||
max_frame_size: 7408,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 2910025,
|
||||
md5_signature: "d11f3717d628cfe6a90a10facc478340",
|
||||
},
|
||||
FlacBlockOutput::Seektable {
|
||||
hash: "18629e1b874cb27e4364da72fb3fec2141eb0618baae4a1cee6ed09562aa00a8",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::CueSheet {
|
||||
hash: "70638a241ca06881a52c0a18258ea2d8946a830137a70479c49746d2a1344bdd",
|
||||
},
|
||||
],
|
||||
audio_hash: "e993070f2080f2c598be1d61d208e9187a55ddea4be1d2ed1f8043e7c03e97a5",
|
||||
stripped_hash: "57c5b945e14c6fcd06916d6a57e5b036d67ff35757893c24ed872007aabbcf4b",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_54",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac"
|
||||
),
|
||||
in_hash: "b68dc6644784fac35aa07581be8603a360d1697e07a2265d7eb24001936fd247",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 1694,
|
||||
max_frame_size: 7145,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 433151,
|
||||
md5_signature: "1d950e92b357dedbc5290a7f2210a2ef",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Hash {
|
||||
n_comments: 20000,
|
||||
hash: "433f34ae532d265835153139b1db79352a26ad0d3b03e2f1a1b88ada34abfc77",
|
||||
},
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "4721b784058410c6263f73680079e9a71aee914c499afcf5580c121fce00e874",
|
||||
stripped_hash: "5c8b92b83c0fa17821add38263fa323d1c66cfd2ee57aca054b50bd05b9df5c2",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_55",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/55 - file 48-53 combined.flac"
|
||||
),
|
||||
in_hash: "a756b460df79b7cc492223f80cda570e4511f2024e5fa0c4d505ba51b86191f6",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 3103,
|
||||
max_frame_size: 11306,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 2646000,
|
||||
md5_signature: "2c78978cbbff11daac296fee97c3e061",
|
||||
},
|
||||
FlacBlockOutput::Seektable {
|
||||
hash: "58dfa7bac4974edf1956b068f5aa72d1fbd9301c36a3085a8a57b9db11a2dbf0",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.3 20190804",
|
||||
comments: VorbisCommentTestValue::Hash {
|
||||
n_comments: 40036,
|
||||
hash: "66cac9f9c42f48128e9fc24e1e96b46a06e885d233155556da16d9b05a23486e",
|
||||
},
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::CueSheet {
|
||||
hash: "db11916c8f5f39648256f93f202e00ff8d73d7d96b62f749b4c77cf3ea744f90",
|
||||
},
|
||||
FlacBlockOutput::Application {
|
||||
application_id: 0x74657374,
|
||||
hash: "6088a557a1bad7bfa5ebf79a324669fbf4fa2f8e708f5487305dfc5b2ff2249a",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "",
|
||||
width: 3200,
|
||||
height: 2252,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
|
||||
},
|
||||
FlacBlockOutput::Padding { size: 16777215 },
|
||||
],
|
||||
audio_hash: "f1285b77cec7fa9a0979033244489a9d06b8515b2158e9270087a65a4007084d",
|
||||
stripped_hash: "401038fce06aff5ebdc7a5f2fc01fa491cbf32d5da9ec99086e414b2da3f8449",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_56",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/56 - JPG PICTURE.flac"
|
||||
),
|
||||
in_hash: "5cebe7a3710cf8924bd2913854e9ca60b4cd53cfee5a3af0c3c73fddc1888963",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 3014,
|
||||
max_frame_size: 7219,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 220026,
|
||||
md5_signature: "5b0e898d9c2626d0c28684f5a586813f",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6",
|
||||
},
|
||||
],
|
||||
audio_hash: "ccfe90b0f15cd9662f7a18f40cd4c347538cf8897a08228e75351206f7804573",
|
||||
stripped_hash: "31a38d59db2010790b7abf65ec0cc03f2bbe1fed5952bc72bee4ca4d0c92e79f",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_57",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/57 - PNG PICTURE.flac"
|
||||
),
|
||||
in_hash: "c6abff7f8bb63c2821bd21dd9052c543f10ba0be878e83cb419c248f14f72697",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 463,
|
||||
max_frame_size: 6770,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 221623,
|
||||
md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_PNG,
|
||||
description: "",
|
||||
width: 960,
|
||||
height: 540,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
|
||||
},
|
||||
],
|
||||
audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f",
|
||||
stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_58",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/58 - GIF PICTURE.flac"
|
||||
),
|
||||
in_hash: "7c2b1a963a665847167a7275f9924f65baeb85c21726c218f61bf3f803f301c8",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 2853,
|
||||
max_frame_size: 6683,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 219826,
|
||||
md5_signature: "7c1810602a7db96d7a48022ac4aa495c",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_GIF,
|
||||
description: "",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 32,
|
||||
img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1",
|
||||
},
|
||||
],
|
||||
audio_hash: "30e3292e9f56cf88658eeadfdec8ad3a440690ce6d813e1b3374f60518c8e0ae",
|
||||
stripped_hash: "4cd771e27870e2a586000f5b369e0426183a521b61212302a2f5802b046910b2",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_59",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/59 - AVIF PICTURE.flac"
|
||||
),
|
||||
in_hash: "7395d02bf8d9533dc554cce02dee9de98c77f8731a45f62d0a243bd0d6f9a45c",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 153,
|
||||
max_frame_size: 7041,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 221423,
|
||||
md5_signature: "d354246011ca204159c06f52cad5f634",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: Mime::from_str("image/avif").unwrap(),
|
||||
description: "",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc",
|
||||
},
|
||||
],
|
||||
audio_hash: "b208c73d274e65b27232bfffbfcbcf4805ee3cbc9cfbf7d2104db8f53370273b",
|
||||
stripped_hash: "d5215e16c6b978fc2c3e6809e1e78981497cb8514df297c5169f3b4a28fd875c",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "custom_01",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_custom/01 - many images.flac"
|
||||
),
|
||||
in_hash: "8a5df37488866cd91ac16773e549ef4e3a85d9f88a0d9d345f174807bb536b96",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 5099,
|
||||
max_frame_size: 7126,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 265617,
|
||||
md5_signature: "82164e4da30ed43b47e6027cef050648",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_PNG,
|
||||
description: "",
|
||||
width: 960,
|
||||
height: 540,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
|
||||
}],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "",
|
||||
width: 3200,
|
||||
height: 2252,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::ABrightColoredFish,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "lorem",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::OtherFileIcon,
|
||||
mime: mime::IMAGE_PNG,
|
||||
description: "ipsum",
|
||||
width: 960,
|
||||
height: 540,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::Lyricist,
|
||||
mime: mime::IMAGE_GIF,
|
||||
description: "dolor",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 32,
|
||||
img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::BackCover,
|
||||
mime: Mime::from_str("image/avif").unwrap(),
|
||||
description: "est",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc",
|
||||
},
|
||||
],
|
||||
audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c",
|
||||
stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "custom_02",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_custom/02 - picture in vorbis comment.flac"
|
||||
),
|
||||
in_hash: "f6bb1a726fe6a3e25a4337d36e29fdced8ff01a46d627b7c2e1988c88f461f8c",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 463,
|
||||
max_frame_size: 6770,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 221623,
|
||||
md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_PNG,
|
||||
description: "",
|
||||
width: 960,
|
||||
height: 540,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
|
||||
}],
|
||||
},
|
||||
],
|
||||
audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f",
|
||||
stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "custom_03",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac"
|
||||
),
|
||||
in_hash: "7177f0ae4f04a563292be286ec05967f81ab16eb0a28b70fc07a1e47da9cafd0",
|
||||
check_error: &|x| {
|
||||
matches!(
|
||||
x,
|
||||
FlacDecodeError::VorbisComment(VorbisCommentDecodeError::MalformedPicture)
|
||||
)
|
||||
},
|
||||
stripped_hash: Some(
|
||||
"3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
|
||||
),
|
||||
pictures: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_sanity_check() {
|
||||
assert!(manifest().iter().map(|x| x.get_name()).all_unique());
|
||||
assert!(manifest().iter().map(|x| x.get_path()).all_unique());
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
//! A flac processor that strips metadata blocks from flac files
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use super::super::{
|
||||
blockread::{FlacBlock, FlacBlockReader, FlacBlockReaderError, FlacBlockSelector},
|
||||
errors::FlacEncodeError,
|
||||
};
|
||||
|
||||
/// Removes all metadata from a flac file
|
||||
pub struct FlacMetaStrip {
|
||||
reader: FlacBlockReader,
|
||||
|
||||
/// The last block that `reader` produced.
|
||||
///
|
||||
/// We need this to detect the last metadata block
|
||||
/// that `reader` produces.
|
||||
last_block: Option<FlacBlock>,
|
||||
|
||||
/// Set to `false` on the first call to `self.write_data`.
|
||||
/// Used to write fLaC magic bytes.
|
||||
first_write: bool,
|
||||
}
|
||||
|
||||
impl FlacMetaStrip {
|
||||
/// Make a new [`FlacMetaStrip`]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
first_write: true,
|
||||
last_block: None,
|
||||
reader: FlacBlockReader::new(FlacBlockSelector {
|
||||
pick_streaminfo: true,
|
||||
pick_padding: false,
|
||||
pick_application: false,
|
||||
pick_seektable: true,
|
||||
pick_vorbiscomment: false,
|
||||
pick_cuesheet: true,
|
||||
pick_picture: false,
|
||||
pick_audio: true,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push some data to this flac processor
|
||||
pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> {
|
||||
self.reader.push_data(buf)
|
||||
}
|
||||
|
||||
/// Call after sending the entire flac file to this reader
|
||||
pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> {
|
||||
self.reader.finish()
|
||||
}
|
||||
|
||||
/// If true, we have received all the data we need
|
||||
pub fn is_done(&mut self) -> bool {
|
||||
self.reader.is_done()
|
||||
}
|
||||
|
||||
/// If false, this reader has sent all its data.
|
||||
///
|
||||
/// Note that `read_data` may write zero bytes if this method returns `true`.
|
||||
/// If `has_data` is false, we don't AND WON'T have data. If we're waiting
|
||||
/// for data, this is `true`.
|
||||
pub fn has_data(&self) -> bool {
|
||||
self.last_block.is_some() || !self.reader.is_done() || self.reader.has_block()
|
||||
}
|
||||
|
||||
/// Write available data from this struct into `target`
|
||||
pub fn read_data(&mut self, target: &mut impl Write) -> Result<(), FlacEncodeError> {
|
||||
if self.first_write {
|
||||
target.write_all(&[0x66, 0x4C, 0x61, 0x43])?;
|
||||
self.first_write = false;
|
||||
}
|
||||
|
||||
while let Some(block) = self.reader.pop_block() {
|
||||
if let Some(last_block) = self.last_block.take() {
|
||||
last_block.encode(
|
||||
// The last metadata block is the only one followed by an audio frame
|
||||
!matches!(last_block, FlacBlock::AudioFrame(_))
|
||||
&& matches!(block, FlacBlock::AudioFrame(_)),
|
||||
true,
|
||||
target,
|
||||
)?;
|
||||
}
|
||||
self.last_block = Some(block);
|
||||
}
|
||||
|
||||
// We don't need to store audioframes in our last_block buffer,
|
||||
// since they do not have an `is_last` flag.
|
||||
if matches!(self.last_block, Some(FlacBlock::AudioFrame(_))) {
|
||||
#[expect(clippy::expect_used)]
|
||||
let x = self
|
||||
.last_block
|
||||
.take()
|
||||
.expect("last_block is Some(AudioFrame), just matched");
|
||||
x.encode(false, true, target)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use paste::paste;
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::flac::{
|
||||
blockread::FlacBlockReaderError, proc::metastrip::FlacMetaStrip, tests::FlacTestCase,
|
||||
tests::manifest,
|
||||
};
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn test_strip(
|
||||
test_case: &FlacTestCase,
|
||||
fragment_size_range: Option<std::ops::Range<usize>>,
|
||||
) -> Result<(), FlacBlockReaderError> {
|
||||
let file_data = std::fs::read(test_case.get_path()).unwrap();
|
||||
|
||||
// Make sure input file is correct
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&file_data);
|
||||
assert_eq!(
|
||||
test_case.get_in_hash(),
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
);
|
||||
|
||||
let mut strip = FlacMetaStrip::new();
|
||||
|
||||
// Push file data to the reader, in parts or as a whole.
|
||||
if let Some(fragment_size_range) = fragment_size_range {
|
||||
let mut head = 0;
|
||||
while head < file_data.len() {
|
||||
let mut frag_size = rand::rng().random_range(fragment_size_range.clone());
|
||||
if head + frag_size > file_data.len() {
|
||||
frag_size = file_data.len() - head;
|
||||
}
|
||||
strip.push_data(&file_data[head..head + frag_size])?;
|
||||
head += frag_size;
|
||||
}
|
||||
} else {
|
||||
strip.push_data(&file_data)?;
|
||||
}
|
||||
|
||||
strip.finish()?;
|
||||
|
||||
let mut out_data = Vec::new();
|
||||
strip.read_data(&mut out_data).unwrap();
|
||||
assert!(!strip.has_data());
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&out_data);
|
||||
let result = hasher.finalize().map(|x| format!("{x:02x}")).join("");
|
||||
assert_eq!(
|
||||
result,
|
||||
test_case.get_stripped_hash().unwrap(),
|
||||
"Stripped FLAC hash doesn't match"
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
macro_rules! gen_tests {
|
||||
( $test_name:ident ) => {
|
||||
paste! {
|
||||
#[test]
|
||||
pub fn [<strip_ $test_name>]() {
|
||||
let manifest =manifest();
|
||||
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
|
||||
match test_case {
|
||||
FlacTestCase::Error { stripped_hash: Some(_), .. } |
|
||||
FlacTestCase::Success { .. } => {
|
||||
for _ in 0..5 {
|
||||
test_strip(
|
||||
test_case,
|
||||
Some(5_000..100_000),
|
||||
).unwrap()
|
||||
}
|
||||
},
|
||||
|
||||
FlacTestCase::Error { check_error, .. } => {
|
||||
let e = test_strip(test_case, Some(5_000..100_000)).unwrap_err();
|
||||
match e {
|
||||
FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
|
||||
_ => panic!("Unexpected error {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
gen_tests!(custom_01);
|
||||
gen_tests!(custom_02);
|
||||
gen_tests!(custom_03);
|
||||
|
||||
gen_tests!(uncommon_10);
|
||||
|
||||
gen_tests!(faulty_06);
|
||||
gen_tests!(faulty_07);
|
||||
gen_tests!(faulty_10);
|
||||
gen_tests!(faulty_11);
|
||||
|
||||
gen_tests!(subset_45);
|
||||
gen_tests!(subset_47);
|
||||
gen_tests!(subset_54);
|
||||
gen_tests!(subset_55);
|
||||
gen_tests!(subset_57);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
//! Flac processors. These are well-tested wrappers around [`crate::flac::blockread::FlacBlockReader`]
|
||||
//! that are specialized for specific tasks.
|
||||
|
||||
pub mod metastrip;
|
||||
pub mod pictures;
|
||||
@@ -1,229 +0,0 @@
|
||||
//! A flac processor that finds all images inside a flac file
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::super::{
|
||||
blockread::{FlacBlock, FlacBlockReader, FlacBlockReaderError, FlacBlockSelector},
|
||||
blocks::FlacPictureBlock,
|
||||
};
|
||||
|
||||
/// Find all pictures in a flac file,
|
||||
/// in both picture metablocks and vorbis comments.
|
||||
pub struct FlacPictureReader {
|
||||
reader: FlacBlockReader,
|
||||
pictures: VecDeque<FlacPictureBlock>,
|
||||
}
|
||||
|
||||
impl FlacPictureReader {
|
||||
/// Make a new [`FlacMetaStrip`]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pictures: VecDeque::new(),
|
||||
reader: FlacBlockReader::new(FlacBlockSelector {
|
||||
pick_streaminfo: false,
|
||||
pick_padding: false,
|
||||
pick_application: false,
|
||||
pick_seektable: false,
|
||||
pick_vorbiscomment: true,
|
||||
pick_cuesheet: false,
|
||||
pick_picture: true,
|
||||
pick_audio: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push some data to this flac processor
|
||||
pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> {
|
||||
self.reader.push_data(buf)?;
|
||||
|
||||
while let Some(b) = self.reader.pop_block() {
|
||||
match b {
|
||||
FlacBlock::Picture(p) => self.pictures.push_back(p),
|
||||
|
||||
FlacBlock::VorbisComment(c) => {
|
||||
for p in c.comment.pictures {
|
||||
self.pictures.push_back(p)
|
||||
}
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Call after sending the entire flac file to this reader
|
||||
pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> {
|
||||
self.reader.finish()
|
||||
}
|
||||
|
||||
/// If true, we have received all the data we need
|
||||
pub fn is_done(&mut self) -> bool {
|
||||
self.reader.is_done()
|
||||
}
|
||||
|
||||
/// If false, this reader has sent all its data.
|
||||
///
|
||||
/// Note that `read_data` may write zero bytes if this method returns `true`.
|
||||
/// If `has_data` is false, we don't AND WON'T have data. If we're waiting
|
||||
/// for data, this is `true`.
|
||||
pub fn has_data(&self) -> bool {
|
||||
!self.reader.is_done() || self.reader.has_block() || !self.pictures.is_empty()
|
||||
}
|
||||
|
||||
/// Pop the next picture we read from this file, if any.
|
||||
pub fn pop_picture(&mut self) -> Option<FlacPictureBlock> {
|
||||
self.pictures.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use paste::paste;
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::flac::{
|
||||
blockread::FlacBlockReaderError,
|
||||
proc::pictures::FlacPictureReader,
|
||||
tests::{FlacBlockOutput, FlacTestCase, manifest},
|
||||
};
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn test_pictures(
|
||||
test_case: &FlacTestCase,
|
||||
fragment_size_range: Option<std::ops::Range<usize>>,
|
||||
) -> Result<(), FlacBlockReaderError> {
|
||||
let file_data = std::fs::read(test_case.get_path()).unwrap();
|
||||
|
||||
// Make sure input file is correct
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&file_data);
|
||||
assert_eq!(
|
||||
test_case.get_in_hash(),
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
);
|
||||
|
||||
let mut pic = FlacPictureReader::new();
|
||||
|
||||
// Push file data to the reader, in parts or as a whole.
|
||||
if let Some(fragment_size_range) = fragment_size_range {
|
||||
let mut head = 0;
|
||||
while head < file_data.len() {
|
||||
let mut frag_size = rand::rng().random_range(fragment_size_range.clone());
|
||||
if head + frag_size > file_data.len() {
|
||||
frag_size = file_data.len() - head;
|
||||
}
|
||||
pic.push_data(&file_data[head..head + frag_size])?;
|
||||
head += frag_size;
|
||||
}
|
||||
} else {
|
||||
pic.push_data(&file_data)?;
|
||||
}
|
||||
|
||||
pic.finish()?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
while let Some(p) = pic.pop_picture() {
|
||||
out.push(p);
|
||||
}
|
||||
|
||||
let out_pictures = test_case.get_pictures().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
out.len(),
|
||||
out_pictures.len(),
|
||||
"Unexpected number of pictures"
|
||||
);
|
||||
|
||||
for (got, expected) in out.iter().zip(out_pictures) {
|
||||
let (picture_type, mime, description, width, height, bit_depth, color_count, img_data) =
|
||||
match expected {
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type,
|
||||
mime,
|
||||
description,
|
||||
width,
|
||||
height,
|
||||
bit_depth,
|
||||
color_count,
|
||||
img_data,
|
||||
} => (
|
||||
picture_type,
|
||||
mime,
|
||||
description,
|
||||
width,
|
||||
height,
|
||||
bit_depth,
|
||||
color_count,
|
||||
img_data,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
assert_eq!(picture_type, got.picture_type, "{}", test_case.get_name());
|
||||
assert_eq!(mime, got.mime, "{}", test_case.get_name());
|
||||
assert_eq!(*description, got.description, "{}", test_case.get_name());
|
||||
assert_eq!(width, got.width, "{}", test_case.get_name());
|
||||
assert_eq!(height, got.height, "{}", test_case.get_name());
|
||||
assert_eq!(bit_depth, got.bit_depth, "{}", test_case.get_name());
|
||||
assert_eq!(color_count, got.color_count, "{}", test_case.get_name());
|
||||
assert_eq!(
|
||||
*img_data,
|
||||
{
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&got.img_data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
},
|
||||
"{}",
|
||||
test_case.get_name()
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
macro_rules! gen_tests {
|
||||
( $test_name:ident ) => {
|
||||
paste! {
|
||||
#[test]
|
||||
pub fn [<pictures_ $test_name>]() {
|
||||
let manifest = manifest();
|
||||
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
|
||||
match test_case {
|
||||
FlacTestCase::Error { pictures: Some(_), .. } |
|
||||
FlacTestCase::Success { .. } => {
|
||||
for _ in 0..5 {
|
||||
test_pictures(
|
||||
test_case,
|
||||
Some(5_000..100_000),
|
||||
).unwrap()
|
||||
}
|
||||
},
|
||||
|
||||
FlacTestCase::Error { check_error, .. } => {
|
||||
let e = test_pictures(test_case, Some(5_000..100_000)).unwrap_err();
|
||||
match e {
|
||||
FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
|
||||
_ => panic!("Unexpected error {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
gen_tests!(custom_01);
|
||||
gen_tests!(custom_02);
|
||||
gen_tests!(custom_03);
|
||||
|
||||
gen_tests!(subset_47);
|
||||
gen_tests!(subset_50);
|
||||
gen_tests!(subset_55);
|
||||
gen_tests!(subset_56);
|
||||
gen_tests!(subset_57);
|
||||
gen_tests!(subset_58);
|
||||
gen_tests!(subset_59);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
//! Read and write audio file metadata.
|
||||
pub mod common;
|
||||
pub mod flac;
|
||||
@@ -1,112 +0,0 @@
|
||||
use crate::flac::proc::pictures::FlacPictureReader;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
pub struct ExtractCovers {}
|
||||
|
||||
impl NodeBuilder for ExtractCovers {
|
||||
fn build<'ctx>(&self) -> Box<dyn Node<'ctx>> {
|
||||
Box::new(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
// Inputs: "data" - Bytes
|
||||
#[async_trait]
|
||||
impl<'ctx> Node<'ctx> for ExtractCovers {
|
||||
async fn run(
|
||||
&self,
|
||||
ctx: &CopperContext<'ctx>,
|
||||
this_node: ThisNodeInfo,
|
||||
params: NodeParameters,
|
||||
mut input: BTreeMap<PortName, Option<PipeData>>,
|
||||
) -> Result<BTreeMap<PortName, PipeData>, RunNodeError> {
|
||||
//
|
||||
// Extract parameters
|
||||
//
|
||||
params.err_if_not_empty()?;
|
||||
|
||||
//
|
||||
// Extract arguments
|
||||
//
|
||||
let data = input.remove(&PortName::new("data"));
|
||||
if data.is_none() {
|
||||
return Err(RunNodeError::MissingInput {
|
||||
port: PortName::new("data"),
|
||||
});
|
||||
}
|
||||
if let Some((port, _)) = input.pop_first() {
|
||||
return Err(RunNodeError::UnrecognizedInput { port });
|
||||
}
|
||||
|
||||
trace!(
|
||||
message = "Inputs ready, preparing reader",
|
||||
node_id = ?this_node.id
|
||||
);
|
||||
|
||||
let mut reader = match data.unwrap() {
|
||||
None => {
|
||||
return Err(RunNodeError::RequiredInputNull {
|
||||
port: PortName::new("data"),
|
||||
});
|
||||
}
|
||||
|
||||
Some(PipeData::Blob { source, .. }) => source.build(ctx).await?,
|
||||
|
||||
_ => {
|
||||
return Err(RunNodeError::BadInputType {
|
||||
port: PortName::new("data"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Setup is done, extract covers
|
||||
//
|
||||
debug!(
|
||||
message = "Extracting covers",
|
||||
node_id = ?this_node.id
|
||||
);
|
||||
let mut picreader = FlacPictureReader::new();
|
||||
|
||||
while let Some(data) = reader.next_fragment().await? {
|
||||
picreader
|
||||
.push_data(&data)
|
||||
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
|
||||
}
|
||||
|
||||
picreader
|
||||
.finish()
|
||||
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
|
||||
|
||||
//
|
||||
// Send the first cover we find
|
||||
//
|
||||
|
||||
let mut output = BTreeMap::new();
|
||||
|
||||
if let Some(picture) = picreader.pop_picture() {
|
||||
debug!(
|
||||
message = "Found a cover, sending",
|
||||
node_id = ?this_node.id,
|
||||
picture = ?picture
|
||||
);
|
||||
|
||||
output.insert(
|
||||
PortName::new("cover_data"),
|
||||
PipeData::Blob {
|
||||
source: BytesProcessorBuilder::new(RawBytesSource::Array {
|
||||
mime: picture.mime.clone(),
|
||||
data: Arc::new(picture.img_data),
|
||||
}),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
message = "Did not find a cover, sending None",
|
||||
node_id = ?this_node.id
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(output);
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
use crate::{
|
||||
common::tagtype::TagType,
|
||||
flac::blockread::{FlacBlock, FlacBlockReader, FlacBlockSelector},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use copper_piper::{
|
||||
base::{Node, NodeBuilder, NodeParameterValue, PortName, RunNodeError, ThisNodeInfo},
|
||||
data::PipeData,
|
||||
helpers::NodeParameters,
|
||||
CopperContext,
|
||||
};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
/// Extract tags from audio metadata
|
||||
pub struct ExtractTags {}
|
||||
|
||||
impl NodeBuilder for ExtractTags {
|
||||
fn build<'ctx>(&self) -> Box<dyn Node<'ctx>> {
|
||||
Box::new(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
// Inputs: "data" - Bytes
|
||||
// Outputs: variable, depends on tags
|
||||
#[async_trait]
|
||||
impl<'ctx> Node<'ctx> for ExtractTags {
|
||||
async fn run(
|
||||
&self,
|
||||
ctx: &CopperContext<'ctx>,
|
||||
this_node: ThisNodeInfo,
|
||||
mut params: NodeParameters,
|
||||
mut input: BTreeMap<PortName, Option<PipeData>>,
|
||||
) -> Result<BTreeMap<PortName, PipeData>, RunNodeError> {
|
||||
//
|
||||
// Extract parameters
|
||||
//
|
||||
|
||||
let tags = {
|
||||
let mut tags: BTreeMap<PortName, TagType> = BTreeMap::new();
|
||||
let val = params.pop_val("tags")?;
|
||||
|
||||
match val {
|
||||
NodeParameterValue::List(list) => {
|
||||
for t in list {
|
||||
match t {
|
||||
NodeParameterValue::String(s) => {
|
||||
tags.insert(PortName::new(s.as_str()), s.as_str().into());
|
||||
}
|
||||
_ => {
|
||||
return Err(RunNodeError::BadParameterType {
|
||||
parameter: "tags".into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(RunNodeError::BadParameterType {
|
||||
parameter: "tags".into(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
tags
|
||||
};
|
||||
|
||||
params.err_if_not_empty()?;
|
||||
|
||||
//
|
||||
// Extract arguments
|
||||
//
|
||||
let data = input.remove(&PortName::new("data"));
|
||||
if data.is_none() {
|
||||
return Err(RunNodeError::MissingInput {
|
||||
port: PortName::new("data"),
|
||||
});
|
||||
}
|
||||
if let Some((port, _)) = input.pop_first() {
|
||||
return Err(RunNodeError::UnrecognizedInput { port });
|
||||
}
|
||||
|
||||
trace!(
|
||||
message = "Inputs ready, preparing reader",
|
||||
node_id = ?this_node.id
|
||||
);
|
||||
|
||||
let mut reader = match data.unwrap() {
|
||||
None => {
|
||||
return Err(RunNodeError::RequiredInputNull {
|
||||
port: PortName::new("data"),
|
||||
})
|
||||
}
|
||||
|
||||
Some(PipeData::Blob { source, .. }) => source.build(ctx).await?,
|
||||
|
||||
_ => {
|
||||
return Err(RunNodeError::BadInputType {
|
||||
port: PortName::new("data"),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Setup is done, extract tags
|
||||
//
|
||||
debug!(
|
||||
message = "Extracting tags",
|
||||
node_id = ?this_node.id
|
||||
);
|
||||
|
||||
let mut block_reader = FlacBlockReader::new(FlacBlockSelector {
|
||||
pick_vorbiscomment: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
while let Some(data) = reader.next_fragment().await? {
|
||||
block_reader
|
||||
.push_data(&data)
|
||||
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
|
||||
}
|
||||
|
||||
block_reader
|
||||
.finish()
|
||||
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
|
||||
|
||||
//
|
||||
// Return tags
|
||||
//
|
||||
|
||||
let mut output = BTreeMap::new();
|
||||
|
||||
while block_reader.has_block() {
|
||||
let b = block_reader.pop_block().unwrap();
|
||||
match b {
|
||||
FlacBlock::VorbisComment(comment) => {
|
||||
for (port, tag_type) in tags.iter() {
|
||||
if let Some((_, tag_value)) =
|
||||
comment.comment.comments.iter().find(|(t, _)| t == tag_type)
|
||||
{
|
||||
let x = output.insert(
|
||||
port.clone(),
|
||||
PipeData::Text {
|
||||
value: tag_value.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Each insertion should be new
|
||||
assert!(x.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `reader` filters blocks for us
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// We should only have one comment block
|
||||
assert!(!block_reader.has_block());
|
||||
}
|
||||
|
||||
return Ok(output);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
//! Pipeline nodes for processing audio files
|
||||
use copper_piper::base::{NodeDispatcher, RegisterNodeError};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod extractcovers;
|
||||
mod extracttags;
|
||||
mod striptags;
|
||||
|
||||
/// Register all nodes in this module into the given dispatcher
|
||||
pub fn register(dispatcher: &mut NodeDispatcher) -> Result<(), RegisterNodeError> {
|
||||
dispatcher
|
||||
.register_node(
|
||||
"StripTags",
|
||||
BTreeMap::new(),
|
||||
Box::new(striptags::StripTags {}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
dispatcher
|
||||
.register_node(
|
||||
"ExtractCovers",
|
||||
BTreeMap::new(),
|
||||
Box::new(extractcovers::ExtractCovers {}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
dispatcher
|
||||
.register_node(
|
||||
"ExtractTags",
|
||||
BTreeMap::new(),
|
||||
Box::new(extracttags::ExtractTags {}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
//! Strip all tags from an audio file
|
||||
|
||||
use crate::flac::proc::metastrip::FlacMetaStrip;
|
||||
use async_trait::async_trait;
|
||||
use copper_piper::{
|
||||
base::{Node, NodeBuilder, NodeId, PortName, RunNodeError, ThisNodeInfo},
|
||||
data::PipeData,
|
||||
helpers::{
|
||||
processor::{StreamProcessor, StreamProcessorBuilder},
|
||||
NodeParameters,
|
||||
},
|
||||
CopperContext,
|
||||
};
|
||||
use copper_util::MimeType;
|
||||
use smartstring::{LazyCompact, SmartString};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::debug;
|
||||
|
||||
/// Strip all metadata from an audio file
|
||||
pub struct StripTags {}
|
||||
|
||||
impl NodeBuilder for StripTags {
|
||||
fn build<'ctx>(&self) -> Box<dyn Node<'ctx>> {
|
||||
Box::new(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
// Input: "data" - Blob
|
||||
// Output: "out" - Blob
|
||||
#[async_trait]
|
||||
impl<'ctx> Node<'ctx> for StripTags {
|
||||
async fn run(
|
||||
&self,
|
||||
_ctx: &CopperContext<'ctx>,
|
||||
this_node: ThisNodeInfo,
|
||||
params: NodeParameters,
|
||||
mut input: BTreeMap<PortName, Option<PipeData>>,
|
||||
) -> Result<BTreeMap<PortName, PipeData>, RunNodeError> {
|
||||
//
|
||||
// Extract parameters
|
||||
//
|
||||
params.err_if_not_empty()?;
|
||||
|
||||
//
|
||||
// Extract arguments
|
||||
//
|
||||
let data = input.remove(&PortName::new("data"));
|
||||
if data.is_none() {
|
||||
return Err(RunNodeError::MissingInput {
|
||||
port: PortName::new("data"),
|
||||
});
|
||||
}
|
||||
if let Some((port, _)) = input.pop_first() {
|
||||
return Err(RunNodeError::UnrecognizedInput { port });
|
||||
}
|
||||
|
||||
let source = match data.unwrap() {
|
||||
None => {
|
||||
return Err(RunNodeError::RequiredInputNull {
|
||||
port: PortName::new("data"),
|
||||
})
|
||||
}
|
||||
|
||||
Some(PipeData::Blob { source, .. }) => source,
|
||||
|
||||
_ => {
|
||||
return Err(RunNodeError::BadInputType {
|
||||
port: PortName::new("data"),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
message = "Setup done, stripping tags",
|
||||
node_id = ?this_node.id
|
||||
);
|
||||
|
||||
let mut output = BTreeMap::new();
|
||||
|
||||
output.insert(
|
||||
PortName::new("out"),
|
||||
PipeData::Blob {
|
||||
source: source.add_processor(Arc::new(TagStripProcessor {
|
||||
node_id: this_node.id.clone(),
|
||||
node_type: this_node.node_type.clone(),
|
||||
})),
|
||||
},
|
||||
);
|
||||
|
||||
return Ok(output);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TagStripProcessor {
|
||||
node_id: NodeId,
|
||||
node_type: SmartString<LazyCompact>,
|
||||
}
|
||||
|
||||
impl StreamProcessorBuilder for TagStripProcessor {
|
||||
fn build(&self) -> Box<dyn StreamProcessor> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StreamProcessor for TagStripProcessor {
|
||||
fn mime(&self) -> &MimeType {
|
||||
return &MimeType::Flac;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TagStripProcessor"
|
||||
}
|
||||
|
||||
fn source_node_id(&self) -> &NodeId {
|
||||
&self.node_id
|
||||
}
|
||||
|
||||
/// Return the type of the node that created this processor
|
||||
fn source_node_type(&self) -> &str {
|
||||
&self.node_type
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
mut source: Receiver<Arc<Vec<u8>>>,
|
||||
sink: Sender<Arc<Vec<u8>>>,
|
||||
max_buffer_size: usize,
|
||||
) -> Result<(), RunNodeError> {
|
||||
//
|
||||
// Strip tags
|
||||
//
|
||||
|
||||
let mut strip = FlacMetaStrip::new();
|
||||
let mut out_bytes = Vec::new();
|
||||
|
||||
while let Some(data) = source.recv().await {
|
||||
strip
|
||||
.push_data(&data)
|
||||
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
|
||||
|
||||
strip
|
||||
.read_data(&mut out_bytes)
|
||||
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
|
||||
|
||||
if out_bytes.len() >= max_buffer_size {
|
||||
let x = std::mem::take(&mut out_bytes);
|
||||
|
||||
match sink.send(Arc::new(x)).await {
|
||||
Ok(()) => {}
|
||||
|
||||
// Not an error, our receiver was dropped.
|
||||
// Exit early if that happens!
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
strip
|
||||
.finish()
|
||||
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
|
||||
|
||||
while strip.has_data() {
|
||||
strip
|
||||
.read_data(&mut out_bytes)
|
||||
.map_err(|e| RunNodeError::Other(Arc::new(e)))?;
|
||||
}
|
||||
|
||||
match sink.send(Arc::new(out_bytes)).await {
|
||||
Ok(()) => {}
|
||||
|
||||
// Not an error, our receiver was dropped.
|
||||
// Exit early if that happens!
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,9 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
pile-config = { workspace = true }
|
||||
pile-audio = { workspace = true }
|
||||
pile-toolbox = { workspace = true }
|
||||
pile-flac = { workspace = true }
|
||||
|
||||
|
||||
serde_json = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
fs::File,
|
||||
io::{Read, Seek},
|
||||
path::PathBuf,
|
||||
};
|
||||
use std::{fmt::Debug, fs::File, io::BufReader, path::PathBuf};
|
||||
|
||||
use pile_audio::flac::blockread::{FlacBlock, FlacBlockReader, FlacBlockSelector};
|
||||
use pile_config::Label;
|
||||
use pile_flac::{FlacBlock, FlacReader};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::Item;
|
||||
@@ -36,56 +31,34 @@ impl Item for FlacItem {
|
||||
}
|
||||
|
||||
fn json(&self) -> Result<serde_json::Value, std::io::Error> {
|
||||
let mut block_reader = FlacBlockReader::new(FlacBlockSelector {
|
||||
pick_vorbiscomment: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut file = File::open(&self.path)?;
|
||||
|
||||
// TODO: do not read the whole file
|
||||
file.rewind()?;
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data)?;
|
||||
|
||||
block_reader
|
||||
.push_data(&data)
|
||||
.map_err(std::io::Error::other)?;
|
||||
block_reader.finish().map_err(std::io::Error::other)?;
|
||||
|
||||
//
|
||||
// Return tags
|
||||
//
|
||||
let file = File::open(&self.path)?;
|
||||
let reader = FlacReader::new(BufReader::new(file));
|
||||
|
||||
let mut output = Map::new();
|
||||
|
||||
while let Some(block) = block_reader.pop_block() {
|
||||
match block {
|
||||
FlacBlock::VorbisComment(comment) => {
|
||||
for (k, v) in comment.comment.comments {
|
||||
let k = k.to_string();
|
||||
let v = Value::String(v.into());
|
||||
let e = output.get_mut(&k);
|
||||
for block in reader {
|
||||
if let FlacBlock::VorbisComment(comment) = block.unwrap() {
|
||||
for (k, v) in comment.comment.comments {
|
||||
let k = k.to_string();
|
||||
let v = Value::String(v.into());
|
||||
let e = output.get_mut(&k);
|
||||
|
||||
match e {
|
||||
None => {
|
||||
output.insert(k.clone(), Value::Array(vec![v]));
|
||||
}
|
||||
Some(e) => {
|
||||
// We always insert an array
|
||||
#[expect(clippy::unwrap_used)]
|
||||
e.as_array_mut().unwrap().push(v);
|
||||
}
|
||||
match e {
|
||||
None => {
|
||||
output.insert(k.clone(), Value::Array(vec![v]));
|
||||
}
|
||||
Some(e) => {
|
||||
// We always insert an array
|
||||
#[expect(clippy::unwrap_used)]
|
||||
e.as_array_mut().unwrap().push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `reader` filters blocks for us
|
||||
_ => unreachable!(),
|
||||
// We should only have one comment block,
|
||||
// stop reading when we find it
|
||||
break;
|
||||
}
|
||||
|
||||
// We should only have one comment block
|
||||
assert!(!block_reader.has_block());
|
||||
}
|
||||
|
||||
return Ok(serde_json::Value::Object(output));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "pile-audio"
|
||||
name = "pile-flac"
|
||||
version = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -16,6 +16,5 @@ smartstring = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
paste = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
io::{Cursor, Read},
|
||||
};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// An application block in a flac file
|
||||
pub struct FlacApplicationBlock {
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// An audio frame in a flac file
|
||||
pub struct FlacAudioFrame {
|
||||
/// The audio frame
|
||||
@@ -1,11 +1,7 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::{
|
||||
common::vorbiscomment::VorbisComment,
|
||||
flac::errors::{FlacDecodeError, FlacEncodeError},
|
||||
};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError, VorbisComment};
|
||||
|
||||
/// A vorbis comment metablock in a flac file
|
||||
pub struct FlacCommentBlock {
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// A cuesheet meta in a flac file
|
||||
pub struct FlacCuesheetBlock {
|
||||
@@ -1,7 +1,7 @@
|
||||
//! FLAC metablock headers. See spec.
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// A type of flac metadata block
|
||||
#[expect(missing_docs)]
|
||||
@@ -1,14 +1,11 @@
|
||||
//! Read and write implementations for all flac block types
|
||||
|
||||
// Not metadata blocks
|
||||
mod header;
|
||||
pub use header::{FlacMetablockHeader, FlacMetablockType};
|
||||
|
||||
mod audiodata;
|
||||
pub use audiodata::FlacAudioFrame;
|
||||
|
||||
// Metadata blocks
|
||||
|
||||
mod streaminfo;
|
||||
pub use streaminfo::FlacStreaminfoBlock;
|
||||
|
||||
@@ -30,15 +27,12 @@ pub use cuesheet::FlacCuesheetBlock;
|
||||
mod comment;
|
||||
pub use comment::FlacCommentBlock;
|
||||
|
||||
use super::errors::{FlacDecodeError, FlacEncodeError};
|
||||
use std::io::Write;
|
||||
|
||||
/// A decode implementation for a
|
||||
/// flac metadata block
|
||||
pub trait FlacMetablockDecode: Sized {
|
||||
/// Try to decode this block from bytes.
|
||||
/// `data` should NOT include the metablock header.
|
||||
fn decode(data: &[u8]) -> Result<Self, FlacDecodeError>;
|
||||
fn decode(data: &[u8]) -> Result<Self, crate::FlacDecodeError>;
|
||||
}
|
||||
|
||||
/// A encode implementation for a
|
||||
@@ -53,6 +47,6 @@ pub trait FlacMetablockEncode: Sized {
|
||||
&self,
|
||||
is_last: bool,
|
||||
with_header: bool,
|
||||
target: &mut impl Write,
|
||||
) -> Result<(), FlacEncodeError>;
|
||||
target: &mut impl std::io::Write,
|
||||
) -> Result<(), crate::FlacEncodeError>;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::{fmt::Debug, io::Read};
|
||||
|
||||
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// A padding block in a FLAC file.
|
||||
#[derive(Debug)]
|
||||
@@ -1,16 +1,11 @@
|
||||
use mime::Mime;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
io::{Cursor, Read},
|
||||
};
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use crate::{
|
||||
common::picturetype::PictureType,
|
||||
flac::errors::{FlacDecodeError, FlacEncodeError},
|
||||
};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError, PictureType};
|
||||
|
||||
/// A picture metablock in a flac file
|
||||
pub struct FlacPictureBlock {
|
||||
@@ -59,7 +54,9 @@ impl FlacMetablockDecode for FlacPictureBlock {
|
||||
#[expect(clippy::map_err_ignore)]
|
||||
d.read_exact(&mut block)
|
||||
.map_err(|_| FlacDecodeError::MalformedBlock)?;
|
||||
let picture_type = PictureType::from_idx(u32::from_be_bytes(block))?;
|
||||
let picture_type = u32::from_be_bytes(block);
|
||||
let picture_type = PictureType::from_idx(picture_type)
|
||||
.ok_or(FlacDecodeError::InvalidPictureType(picture_type))?;
|
||||
|
||||
// Image format
|
||||
let mime = {
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// A seektable block in a flac file
|
||||
pub struct FlacSeektableBlock {
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use crate::flac::errors::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// A streaminfo block in a flac file
|
||||
#[derive(Debug)]
|
||||
@@ -1,8 +1,4 @@
|
||||
//! FLAC errors
|
||||
use crate::common::{
|
||||
picturetype::PictureTypeError,
|
||||
vorbiscomment::{VorbisCommentDecodeError, VorbisCommentEncodeError},
|
||||
};
|
||||
use std::string::FromUtf8Error;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -22,47 +18,37 @@ pub enum FlacDecodeError {
|
||||
BadMetablockType(u8),
|
||||
|
||||
/// We encountered an i/o error while processing
|
||||
#[error("io error while reading flac")]
|
||||
#[error("i/o error")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
/// We could not parse a vorbis comment
|
||||
#[error("error while decoding vorbis comment")]
|
||||
VorbisComment(#[from] VorbisCommentDecodeError),
|
||||
|
||||
/// We tried to decode a string, but found invalid UTF-8
|
||||
#[error("error while decoding string")]
|
||||
#[error("could not decode string")]
|
||||
FailedStringDecode(#[from] FromUtf8Error),
|
||||
|
||||
/// We tried to read a block, but it was out of spec.
|
||||
#[error("malformed flac block")]
|
||||
MalformedBlock,
|
||||
|
||||
/// We could not parse a vorbis comment string
|
||||
#[error("malformed vorbis comment string: {0:?}")]
|
||||
MalformedCommentString(String),
|
||||
|
||||
/// We could not parse a vorbis comment string
|
||||
#[error("malformed vorbis picture")]
|
||||
MalformedPicture(base64::DecodeError),
|
||||
|
||||
/// We didn't find frame sync bytes where we expected them
|
||||
#[error("bad frame sync bytes")]
|
||||
BadSyncBytes,
|
||||
|
||||
/// We tried to decode a bad picture type
|
||||
#[error("bad picture type")]
|
||||
PictureTypeError(#[from] PictureTypeError),
|
||||
#[error("invalid picture type {0}")]
|
||||
InvalidPictureType(u32),
|
||||
}
|
||||
|
||||
#[expect(missing_docs)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FlacEncodeError {
|
||||
/// We encountered an i/o error while processing
|
||||
#[error("io error while encoding block")]
|
||||
#[error("i/o error")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
/// We could not encode a picture inside a vorbis comment
|
||||
#[error("could not encode picture in vorbis comment")]
|
||||
VorbisPictureEncodeError,
|
||||
}
|
||||
|
||||
impl From<VorbisCommentEncodeError> for FlacEncodeError {
|
||||
fn from(value: VorbisCommentEncodeError) -> Self {
|
||||
match value {
|
||||
VorbisCommentEncodeError::IoError(e) => e.into(),
|
||||
VorbisCommentEncodeError::PictureEncodeError => Self::VorbisPictureEncodeError,
|
||||
}
|
||||
}
|
||||
}
|
||||
19
crates/pile-flac/src/lib.rs
Normal file
19
crates/pile-flac/src/lib.rs
Normal 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;
|
||||
@@ -1,21 +1,5 @@
|
||||
//! An audio picture type, according to the ID3v2 APIC frame
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
/// We failed to decode a picture type
|
||||
#[derive(Debug)]
|
||||
pub struct PictureTypeError {
|
||||
idx: u32,
|
||||
}
|
||||
|
||||
impl Display for PictureTypeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Bad picture type `{}`", self.idx)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PictureTypeError {}
|
||||
|
||||
/// A picture type according to the ID3v2 APIC frame
|
||||
#[expect(missing_docs)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
@@ -46,8 +30,8 @@ pub enum PictureType {
|
||||
impl PictureType {
|
||||
/// Try to decode a picture type from the given integer.
|
||||
/// Returns an error if `idx` is invalid.
|
||||
pub fn from_idx(idx: u32) -> Result<Self, PictureTypeError> {
|
||||
Ok(match idx {
|
||||
pub fn from_idx(idx: u32) -> Option<Self> {
|
||||
Some(match idx {
|
||||
0 => PictureType::Other,
|
||||
1 => PictureType::PngFileIcon,
|
||||
2 => PictureType::OtherFileIcon,
|
||||
@@ -69,7 +53,7 @@ impl PictureType {
|
||||
18 => PictureType::Illustration,
|
||||
19 => PictureType::ArtistLogotype,
|
||||
20 => PictureType::PublisherLogotype,
|
||||
_ => return Err(PictureTypeError { idx }),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
63
crates/pile-flac/src/reader/block.rs
Normal file
63
crates/pile-flac/src/reader/block.rs
Normal 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)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
9
crates/pile-flac/src/reader/mod.rs
Normal file
9
crates/pile-flac/src/reader/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod block;
|
||||
pub use block::*;
|
||||
|
||||
#[expect(clippy::module_inception)]
|
||||
mod reader;
|
||||
pub use reader::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
184
crates/pile-flac/src/reader/reader.rs
Normal file
184
crates/pile-flac/src/reader/reader.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
393
crates/pile-flac/src/reader/tests.rs
Normal file
393
crates/pile-flac/src/reader/tests.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
use itertools::Itertools;
|
||||
use paste::paste;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fs::File, io::Write, str::FromStr};
|
||||
|
||||
use crate::{
|
||||
FlacDecodeError, TagType,
|
||||
tests::{FlacBlockOutput, FlacTestCase, VorbisCommentTestValue},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn read_file(test_case: &FlacTestCase) -> Result<Vec<FlacBlock>, FlacDecodeError> {
|
||||
let file_data = std::fs::read(test_case.get_path()).unwrap();
|
||||
|
||||
// Make sure input file is correct
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&file_data);
|
||||
assert_eq!(
|
||||
test_case.get_in_hash(),
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
);
|
||||
|
||||
let file = File::open(test_case.get_path()).unwrap();
|
||||
|
||||
let mut reader = FlacReader::new(file);
|
||||
let mut out_blocks = Vec::new();
|
||||
|
||||
while let Some(b) = reader.next() {
|
||||
out_blocks.push(b?)
|
||||
}
|
||||
|
||||
return Ok(out_blocks);
|
||||
}
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn test_identical(test_case: &FlacTestCase) -> Result<(), FlacDecodeError> {
|
||||
let out_blocks = read_file(test_case)?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
out.write_all(&[0x66, 0x4C, 0x61, 0x43]).unwrap();
|
||||
|
||||
for i in 0..out_blocks.len() {
|
||||
let b = &out_blocks[i];
|
||||
let is_last = if i == out_blocks.len() - 1 {
|
||||
false
|
||||
} else {
|
||||
!matches!(b, FlacBlock::AudioFrame(_))
|
||||
&& matches!(&out_blocks[i + 1], FlacBlock::AudioFrame(_))
|
||||
};
|
||||
|
||||
b.encode(is_last, true, &mut out).unwrap();
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&out);
|
||||
let result = hasher.finalize().map(|x| format!("{x:02x}")).join("");
|
||||
assert_eq!(result, test_case.get_in_hash(), "Output hash doesn't match");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn test_blockread(test_case: &FlacTestCase) -> Result<(), FlacDecodeError> {
|
||||
let out_blocks = read_file(test_case)?;
|
||||
|
||||
assert_eq!(
|
||||
test_case.get_blocks().unwrap().len(),
|
||||
out_blocks
|
||||
.iter()
|
||||
.filter(|x| !matches!(*x, FlacBlock::AudioFrame(_)))
|
||||
.count(),
|
||||
"Number of blocks didn't match"
|
||||
);
|
||||
|
||||
let mut audio_data_hasher = Sha256::new();
|
||||
let mut result_i = 0;
|
||||
|
||||
for b in out_blocks {
|
||||
match b {
|
||||
FlacBlock::Streaminfo(s) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size,
|
||||
max_block_size,
|
||||
min_frame_size,
|
||||
max_frame_size,
|
||||
sample_rate,
|
||||
channels,
|
||||
bits_per_sample,
|
||||
total_samples,
|
||||
md5_signature,
|
||||
} => {
|
||||
assert_eq!(*min_block_size, s.min_block_size,);
|
||||
assert_eq!(*max_block_size, s.max_block_size);
|
||||
assert_eq!(*min_frame_size, s.min_frame_size);
|
||||
assert_eq!(*max_frame_size, s.max_frame_size);
|
||||
assert_eq!(*sample_rate, s.sample_rate);
|
||||
assert_eq!(*channels, s.channels);
|
||||
assert_eq!(*bits_per_sample, s.bits_per_sample);
|
||||
assert_eq!(*total_samples, s.total_samples);
|
||||
assert_eq!(
|
||||
*md5_signature,
|
||||
s.md5_signature.iter().map(|x| format!("{x:02x}")).join("")
|
||||
);
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::Application(a) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Application {
|
||||
application_id,
|
||||
hash,
|
||||
} => {
|
||||
assert_eq!(
|
||||
*application_id, a.application_id,
|
||||
"Application id doesn't match"
|
||||
);
|
||||
assert_eq!(
|
||||
*hash,
|
||||
{
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(&a.data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
},
|
||||
"Application content hash doesn't match"
|
||||
);
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::CueSheet(c) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::CueSheet { hash } => {
|
||||
assert_eq!(*hash, {
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(&c.data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
});
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::Padding(p) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Padding { size } => {
|
||||
assert_eq!(p.size, *size);
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::SeekTable(t) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Seektable { hash } => {
|
||||
assert_eq!(*hash, {
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(&t.data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
});
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::Picture(p) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type,
|
||||
mime,
|
||||
description,
|
||||
width,
|
||||
height,
|
||||
bit_depth,
|
||||
color_count,
|
||||
img_data,
|
||||
} => {
|
||||
assert_eq!(*picture_type, p.picture_type, "{}", test_case.get_name());
|
||||
assert_eq!(*mime, p.mime, "{}", test_case.get_name());
|
||||
assert_eq!(*description, p.description, "{}", test_case.get_name());
|
||||
assert_eq!(*width, p.width, "{}", test_case.get_name());
|
||||
assert_eq!(*height, p.height, "{}", test_case.get_name());
|
||||
assert_eq!(*bit_depth, p.bit_depth, "{}", test_case.get_name());
|
||||
assert_eq!(*color_count, p.color_count, "{}", test_case.get_name());
|
||||
assert_eq!(
|
||||
*img_data,
|
||||
{
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(&p.img_data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
},
|
||||
"{}",
|
||||
test_case.get_name()
|
||||
);
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::VorbisComment(v) => match &test_case.get_blocks().unwrap()[result_i] {
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor,
|
||||
comments,
|
||||
pictures,
|
||||
} => {
|
||||
assert_eq!(*vendor, v.comment.vendor, "Comment vendor doesn't match");
|
||||
|
||||
assert_eq!(
|
||||
v.comment.pictures.len(),
|
||||
pictures.len(),
|
||||
"Number of pictures doesn't match"
|
||||
);
|
||||
|
||||
for (p, e) in v.comment.pictures.iter().zip(*pictures) {
|
||||
match e {
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type,
|
||||
mime,
|
||||
description,
|
||||
width,
|
||||
height,
|
||||
bit_depth,
|
||||
color_count,
|
||||
img_data,
|
||||
} => {
|
||||
assert_eq!(*picture_type, p.picture_type);
|
||||
assert_eq!(*mime, p.mime);
|
||||
assert_eq!(*description, p.description);
|
||||
assert_eq!(*width, p.width);
|
||||
assert_eq!(*height, p.height);
|
||||
assert_eq!(*bit_depth, p.bit_depth);
|
||||
assert_eq!(*color_count, p.color_count);
|
||||
assert_eq!(*img_data, {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&p.img_data);
|
||||
hasher.finalize().map(|x| format!("{x:02x}")).join("")
|
||||
});
|
||||
}
|
||||
_ => panic!("Bad test data: expected only Picture blocks."),
|
||||
}
|
||||
}
|
||||
|
||||
match comments {
|
||||
VorbisCommentTestValue::Raw { tags } => {
|
||||
assert_eq!(
|
||||
v.comment.comments.len(),
|
||||
tags.len(),
|
||||
"Number of comments doesn't match"
|
||||
);
|
||||
|
||||
for ((got_tag, got_val), (exp_tag, exp_val)) in
|
||||
v.comment.comments.iter().zip(*tags)
|
||||
{
|
||||
assert_eq!(
|
||||
*got_tag,
|
||||
TagType::from_str(exp_tag).unwrap(),
|
||||
"Tag key doesn't match"
|
||||
);
|
||||
assert_eq!(
|
||||
got_val, exp_val,
|
||||
"Tag value of {exp_tag} doesn't match"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VorbisCommentTestValue::Hash { n_comments, hash } => {
|
||||
assert_eq!(
|
||||
v.comment.comments.len(),
|
||||
*n_comments,
|
||||
"Number of comments doesn't match"
|
||||
);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
for (got_tag, got_val) in &v.comment.comments {
|
||||
hasher.update(format!("{}={got_val};", got_tag.to_vorbis_string()).as_bytes());
|
||||
}
|
||||
assert_eq!(
|
||||
&hasher.finalize().map(|x| format!("{x:02x}")).join(""),
|
||||
hash,
|
||||
"Comment hash doesn't match"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected block type"),
|
||||
},
|
||||
|
||||
FlacBlock::AudioFrame(data) => {
|
||||
let mut vec = Vec::new();
|
||||
data.encode(&mut vec).unwrap();
|
||||
audio_data_hasher.update(&vec);
|
||||
|
||||
if result_i != test_case.get_blocks().unwrap().len() {
|
||||
panic!("There are metadata blocks between audio frames!")
|
||||
}
|
||||
|
||||
// Don't increment result_i
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result_i += 1;
|
||||
}
|
||||
|
||||
// Check audio data hash
|
||||
assert_eq!(
|
||||
test_case.get_audio_hash().unwrap(),
|
||||
audio_data_hasher
|
||||
.finalize()
|
||||
.map(|x| format!("{x:02x}"))
|
||||
.join("")
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Helper macros to generate tests
|
||||
macro_rules! gen_tests {
|
||||
( $test_name:ident ) => {
|
||||
paste! {
|
||||
#[test]
|
||||
pub fn [<blockread_small_ $test_name>]() {
|
||||
let manifest = crate::tests::manifest();
|
||||
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
|
||||
|
||||
match test_case {
|
||||
FlacTestCase::Success { .. } => {
|
||||
for _ in 0..5 {
|
||||
test_blockread(
|
||||
test_case,
|
||||
).unwrap()
|
||||
}
|
||||
},
|
||||
|
||||
FlacTestCase::Error { check_error, .. } => {
|
||||
let e = test_blockread(test_case);
|
||||
match e {
|
||||
Err(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
|
||||
_ => panic!("Unexpected error {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn [<identical_small_ $test_name>]() {
|
||||
let manifest = crate::tests::manifest();
|
||||
let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap();
|
||||
|
||||
match test_case {
|
||||
FlacTestCase::Success { .. } => {
|
||||
for _ in 0..5 {
|
||||
test_identical(
|
||||
test_case,
|
||||
).unwrap()
|
||||
}
|
||||
},
|
||||
|
||||
FlacTestCase::Error { check_error, .. } => {
|
||||
let e = test_identical(test_case);
|
||||
match e {
|
||||
Err(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
|
||||
_ => panic!("Unexpected error {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
gen_tests!(custom_01);
|
||||
gen_tests!(custom_02);
|
||||
gen_tests!(custom_03);
|
||||
|
||||
gen_tests!(uncommon_10);
|
||||
|
||||
gen_tests!(faulty_06);
|
||||
gen_tests!(faulty_07);
|
||||
gen_tests!(faulty_10);
|
||||
gen_tests!(faulty_11);
|
||||
|
||||
gen_tests!(subset_45);
|
||||
gen_tests!(subset_46);
|
||||
gen_tests!(subset_47);
|
||||
gen_tests!(subset_48);
|
||||
gen_tests!(subset_49);
|
||||
gen_tests!(subset_50);
|
||||
gen_tests!(subset_51);
|
||||
gen_tests!(subset_52);
|
||||
gen_tests!(subset_53);
|
||||
gen_tests!(subset_54);
|
||||
gen_tests!(subset_55);
|
||||
gen_tests!(subset_56);
|
||||
gen_tests!(subset_57);
|
||||
gen_tests!(subset_58);
|
||||
gen_tests!(subset_59);
|
||||
@@ -4,37 +4,75 @@ use strum::{Display, EnumString};
|
||||
|
||||
/// A universal tag type
|
||||
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, EnumString, Display)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
pub enum TagType {
|
||||
/// A tag we didn't recognize
|
||||
#[strum(default)]
|
||||
Other(SmartString<LazyCompact>),
|
||||
|
||||
/// Album name
|
||||
Album,
|
||||
|
||||
/// Album artist
|
||||
AlbumArtist,
|
||||
|
||||
/// Comment
|
||||
Comment,
|
||||
|
||||
/// Release date
|
||||
ReleaseDate,
|
||||
|
||||
/// Disk number
|
||||
DiskNumber,
|
||||
|
||||
/// Total disks in album
|
||||
DiskTotal,
|
||||
|
||||
/// Genre
|
||||
Genre,
|
||||
|
||||
/// International standard recording code
|
||||
Isrc,
|
||||
|
||||
/// Track lyrics, possibly time-coded
|
||||
Lyrics,
|
||||
|
||||
/// This track's number in its album
|
||||
TrackNumber,
|
||||
|
||||
/// The total number of tracks in this track's album
|
||||
TrackTotal,
|
||||
|
||||
/// The title of this track
|
||||
TrackTitle,
|
||||
|
||||
/// This track's artist (the usual `Artist`,
|
||||
/// compare to `AlbumArtist`)
|
||||
TrackArtist,
|
||||
|
||||
/// The year this track was released
|
||||
Year,
|
||||
}
|
||||
|
||||
impl TagType {
|
||||
pub fn to_vorbis_string(&self) -> String {
|
||||
match self {
|
||||
TagType::TrackTitle => "TITLE",
|
||||
TagType::Album => "ALBUM",
|
||||
TagType::TrackNumber => "TRACKNUMBER",
|
||||
TagType::TrackArtist => "ARTIST",
|
||||
TagType::AlbumArtist => "ALBUMARTIST",
|
||||
TagType::Genre => "GENRE",
|
||||
TagType::Isrc => "ISRC",
|
||||
TagType::ReleaseDate => "DATE",
|
||||
TagType::TrackTotal => "TOTALTRACKS",
|
||||
TagType::Lyrics => "LYRICS",
|
||||
TagType::Comment => "COMMENT",
|
||||
TagType::DiskNumber => "DISKNUMBER",
|
||||
TagType::DiskTotal => "DISKTOTAL",
|
||||
TagType::Year => "YEAR",
|
||||
TagType::Other(x) => x,
|
||||
}
|
||||
.to_uppercase()
|
||||
}
|
||||
}
|
||||
919
crates/pile-flac/src/tests.rs
Normal file
919
crates/pile-flac/src/tests.rs
Normal file
@@ -0,0 +1,919 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use itertools::Itertools;
|
||||
use mime::Mime;
|
||||
|
||||
use crate::PictureType;
|
||||
|
||||
use super::errors::FlacDecodeError;
|
||||
|
||||
/// The value of a vorbis comment.
|
||||
///
|
||||
/// Some files have VERY large comments, and providing them
|
||||
/// explicitly here doesn't make sense.
|
||||
#[derive(Clone)]
|
||||
pub enum VorbisCommentTestValue {
|
||||
/// The comments, in order
|
||||
Raw {
|
||||
tags: &'static [(&'static str, &'static str)],
|
||||
},
|
||||
/// The hash of all comments concatenated together,
|
||||
/// stringified as `{key}={value};`
|
||||
Hash {
|
||||
n_comments: usize,
|
||||
hash: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum FlacBlockOutput {
|
||||
Application {
|
||||
application_id: u32,
|
||||
hash: &'static str,
|
||||
},
|
||||
Streaminfo {
|
||||
min_block_size: u32,
|
||||
max_block_size: u32,
|
||||
min_frame_size: u32,
|
||||
max_frame_size: u32,
|
||||
sample_rate: u32,
|
||||
channels: u8,
|
||||
bits_per_sample: u8,
|
||||
total_samples: u128,
|
||||
md5_signature: &'static str,
|
||||
},
|
||||
CueSheet {
|
||||
// Hash of this block's data, without the header.
|
||||
// This is easy to get with
|
||||
//
|
||||
// ```notrust
|
||||
// metaflac \
|
||||
// --list \
|
||||
// --block-number=<n> \
|
||||
// --data-format=binary-headerless \
|
||||
// <file> \
|
||||
// | sha256sum
|
||||
//```
|
||||
hash: &'static str,
|
||||
},
|
||||
Seektable {
|
||||
hash: &'static str,
|
||||
},
|
||||
Padding {
|
||||
size: u32,
|
||||
},
|
||||
Picture {
|
||||
picture_type: PictureType,
|
||||
mime: Mime,
|
||||
description: &'static str,
|
||||
width: u32,
|
||||
height: u32,
|
||||
bit_depth: u32,
|
||||
color_count: u32,
|
||||
img_data: &'static str,
|
||||
},
|
||||
VorbisComment {
|
||||
vendor: &'static str,
|
||||
comments: VorbisCommentTestValue,
|
||||
pictures: &'static [FlacBlockOutput],
|
||||
},
|
||||
}
|
||||
|
||||
pub enum FlacTestCase {
|
||||
Success {
|
||||
/// This test's name
|
||||
test_name: &'static str,
|
||||
|
||||
/// The file to use for this test
|
||||
file_path: &'static str,
|
||||
|
||||
/// The hash of the input files
|
||||
in_hash: &'static str,
|
||||
|
||||
/// The flac metablocks we expect to find in this file, in order.
|
||||
blocks: Vec<FlacBlockOutput>,
|
||||
|
||||
/// The hash of the audio frames in this file
|
||||
///
|
||||
/// Get this hash by running `metaflac --remove-all --dont-use-padding`,
|
||||
/// then by manually deleting remaining headers in a hex editor
|
||||
/// (Remember that the sync sequence is 0xFF 0xF8)
|
||||
audio_hash: &'static str,
|
||||
|
||||
/// The hash we should get when we strip this file's tags.
|
||||
///
|
||||
/// A stripped flac file has unmodified STREAMINFO, SEEKTABLE,
|
||||
/// CUESHEET, and audio data blocks; and nothing else (not even padding).
|
||||
///
|
||||
/// Reference implementation:
|
||||
/// ```notrust
|
||||
/// metaflac \
|
||||
/// --remove \
|
||||
/// --block-type=PADDING,APPLICATION,VORBIS_COMMENT,PICTURE \
|
||||
/// --dont-use-padding \
|
||||
/// <file>
|
||||
/// ```
|
||||
stripped_hash: &'static str,
|
||||
},
|
||||
Error {
|
||||
/// This test's name
|
||||
test_name: &'static str,
|
||||
|
||||
/// The file to use for this test
|
||||
file_path: &'static str,
|
||||
|
||||
/// The hash of the input files
|
||||
in_hash: &'static str,
|
||||
|
||||
/// The error we should encounter while reading this file
|
||||
check_error: &'static dyn Fn(&FlacDecodeError) -> bool,
|
||||
|
||||
/// If some, stripping this file's metadata should produce the given hash.
|
||||
/// If none, trying to strip metadata should produce `check_error`
|
||||
stripped_hash: Option<&'static str>,
|
||||
|
||||
/// If some, the following images should be extracted from this file
|
||||
/// If none, trying to strip images should produce `check_error`
|
||||
pictures: Option<Vec<FlacBlockOutput>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl FlacTestCase {
|
||||
pub fn get_name(&self) -> &str {
|
||||
match self {
|
||||
Self::Error { test_name, .. } | Self::Success { test_name, .. } => test_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_path(&self) -> &str {
|
||||
match self {
|
||||
Self::Success { file_path, .. } | Self::Error { file_path, .. } => file_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_in_hash(&self) -> &str {
|
||||
match self {
|
||||
Self::Success { in_hash, .. } | Self::Error { in_hash, .. } => in_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_stripped_hash(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Success { stripped_hash, .. } => Some(stripped_hash),
|
||||
Self::Error { stripped_hash, .. } => *stripped_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_audio_hash(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Success { audio_hash, .. } => Some(audio_hash),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_blocks(&self) -> Option<&[FlacBlockOutput]> {
|
||||
match self {
|
||||
Self::Success { blocks, .. } => Some(blocks),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_pictures(&self) -> Option<Vec<FlacBlockOutput>> {
|
||||
match self {
|
||||
Self::Success { blocks, .. } => {
|
||||
let mut out = Vec::new();
|
||||
for b in blocks {
|
||||
match b {
|
||||
FlacBlockOutput::Picture { .. } => out.push(b.clone()),
|
||||
FlacBlockOutput::VorbisComment { pictures, .. } => {
|
||||
for p in *pictures {
|
||||
out.push(p.clone())
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
return Some(out);
|
||||
}
|
||||
|
||||
Self::Error { pictures, .. } => pictures.as_ref().map(|x| x.iter().cloned().collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of test files and their expected output
|
||||
#[expect(clippy::unwrap_used)]
|
||||
pub fn manifest() -> [FlacTestCase; 23] {
|
||||
[
|
||||
FlacTestCase::Error {
|
||||
test_name: "uncommon_10",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_uncommon/10 - file starting at frame header.flac"
|
||||
),
|
||||
in_hash: "d95f63e8101320f5ac7ffe249bc429a209eb0e10996a987301eaa63386a8faa1",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::BadMagicBytes),
|
||||
stripped_hash: None,
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "faulty_06",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac"
|
||||
),
|
||||
in_hash: "53aed5e7fde7a652b82ba06a8382b2612b02ebbde7b0d2016276644d17cc76cd",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock),
|
||||
stripped_hash: None,
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "faulty_07",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac"
|
||||
),
|
||||
in_hash: "6d46725991ba5da477187fde7709ea201c399d00027257c365d7301226d851ea",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock),
|
||||
stripped_hash: None,
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "faulty_10",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac"
|
||||
),
|
||||
in_hash: "c79b0514a61634035a5653c5493797bbd1fcc78982116e4d429630e9e462d29b",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::IoError(_)),
|
||||
// This file's vorbis comment is invalid, but that shouldn't stop us from removing it.
|
||||
// As a general rule, we should NOT encounter an error when stripping invalid blocks.
|
||||
//
|
||||
// We should, however, get errors when we try to strip flac files with invalid structure.
|
||||
// (For example, the out-of-order streaminfo test in faulty_07).
|
||||
stripped_hash: Some("4b994f82dc1699a58e2b127058b37374220ee41dc294d4887ac14f056291a1b0"),
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "faulty_11",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_faulty/11 - incorrect metadata block length.flac"
|
||||
),
|
||||
in_hash: "3732151ba8c4e66a785165aa75a444aad814c16807ddc97b793811376acacfd6",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::BadMetablockType(127)),
|
||||
stripped_hash: None,
|
||||
pictures: None,
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_45",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/45 - no total number of samples set.flac"
|
||||
),
|
||||
in_hash: "336a18eb7a78f7fc0ab34980348e2895bc3f82db440a2430d9f92e996f889f9a",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 907,
|
||||
max_frame_size: 8053,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 0,
|
||||
md5_signature: "c41ae3b82c35d8f5c3dab1729f948fde",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "3fb3482ebc1724559bdd57f34de472458563d78a676029614e76e32b5d2b8816",
|
||||
stripped_hash: "31631ac227ebe2689bac7caa1fa964b47e71a9f1c9c583a04ea8ebd9371508d0",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_46",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/46 - no min-max framesize set.flac"
|
||||
),
|
||||
in_hash: "9dc39732ce17815832790901b768bb50cd5ff0cd21b28a123c1cabc16ed776cc",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 0,
|
||||
max_frame_size: 0,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 282866,
|
||||
md5_signature: "fd131e6ebc75251ed83f8f4c07df36a4",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "a1eed422462b386a932b9eb3dff3aea3687b41eca919624fb574aadb7eb50040",
|
||||
stripped_hash: "9e57cd77f285fc31f87fa4e3a31ab8395d68d5482e174c8e0d0bba9a0c20ba27",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_47",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/47 - only STREAMINFO.flac"
|
||||
),
|
||||
in_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85",
|
||||
blocks: vec![FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 4747,
|
||||
max_frame_size: 7034,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 232608,
|
||||
md5_signature: "bba30c5f70789910e404b7ac727c3853",
|
||||
}],
|
||||
audio_hash: "5ee1450058254087f58c91baf0f70d14bde8782cf2dc23c741272177fe0fce6e",
|
||||
stripped_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_48",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac"
|
||||
),
|
||||
in_hash: "4417aca6b5f90971c50c28766d2f32b3acaa7f9f9667bd313336242dae8b2531",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 2445,
|
||||
max_frame_size: 7364,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 259884,
|
||||
md5_signature: "97a0574290237563fbaa788ad77d2cdf",
|
||||
},
|
||||
FlacBlockOutput::Seektable {
|
||||
hash: "21ca2184ae22fe26b690fd7cbd8d25fcde1d830ff6e5796ced4107bab219d7c0",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "c2d691f2c4c986fe3cd5fd7864d9ba9ce6dd68a4ffc670447f008434b13102c2",
|
||||
stripped_hash: "abc9a0c40a29c896bc6e1cc0b374db1c8e157af716a5a3c43b7db1591a74c4e8",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_49",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/49 - Extremely large PADDING.flac",
|
||||
),
|
||||
in_hash: "7bc44fa2754536279fde4f8fb31d824f43b8d0b3f93d27d055d209682914f20e",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 1353,
|
||||
max_frame_size: 7117,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 258939,
|
||||
md5_signature: "6e78f221caaaa5d570a53f1714d84ded",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Padding { size: 16777215 },
|
||||
],
|
||||
audio_hash: "5007be7109b28b0149d1b929d2a0e93a087381bd3e68cf2a3ef78ea265ea20c3",
|
||||
stripped_hash: "a2283bbacbc4905ad3df1bf9f43a0ea7aa65cf69523d84a7dd8eb54553cc437e",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_50",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/50 - Extremely large PICTURE.flac"
|
||||
),
|
||||
in_hash: "1f04f237d74836104993a8072d4223e84a5d3bd76fbc44555c221c7e69a23594",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 5099,
|
||||
max_frame_size: 7126,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 265617,
|
||||
md5_signature: "82164e4da30ed43b47e6027cef050648",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "",
|
||||
width: 3200,
|
||||
height: 2252,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
|
||||
},
|
||||
],
|
||||
audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c",
|
||||
stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_51",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac"
|
||||
),
|
||||
in_hash: "033160e8124ed287b0b5d615c94ac4139477e47d6e4059b1c19b7141566f5ef9",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 4531,
|
||||
max_frame_size: 7528,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 289972,
|
||||
md5_signature: "5ff622c88f8dd9bc201a6a541f3890d3",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Hash {
|
||||
n_comments: 39,
|
||||
hash: "01984e9ec0cfad41f27b3b4e84184966f6725ead84b7815bd0b3313549ee4229",
|
||||
},
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "76419865d10eb22a74f020423a4e515e800f0177441676afd0418557c2d76c36",
|
||||
stripped_hash: "c0ca6c6099b5d9ec53d6bb370f339b2b1570055813a6cd3616fac2db83a2185e",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_52",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/52 - Extremely large APPLICATION.flac"
|
||||
),
|
||||
in_hash: "0e45a4f8dbef15cbebdd8dfe690d8ae60e0c6abb596db1270a9161b62a7a3f1c",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 3711,
|
||||
max_frame_size: 7056,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 317876,
|
||||
md5_signature: "eb7140266bc194527488c21ab49bc47b",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Application {
|
||||
application_id: 0x74657374,
|
||||
hash: "cfc0b8969e4ba6bd507999ba89dea2d274df69d94749d6ae3cf117a7780bba09",
|
||||
},
|
||||
],
|
||||
audio_hash: "89ad1a5c86a9ef35d33189c81c8a90285a23964a13f8325bf2c02043e8c83d63",
|
||||
stripped_hash: "cc4a0afb95ec9bcde8ee33f13951e494dc4126a9a3a668d79c80ce3c14a3acd9",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_53",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac"
|
||||
),
|
||||
in_hash: "513fad18578f3225fae5de1bda8f700415be6fd8aa1e7af533b5eb796ed2d461",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 2798,
|
||||
max_frame_size: 7408,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 2910025,
|
||||
md5_signature: "d11f3717d628cfe6a90a10facc478340",
|
||||
},
|
||||
FlacBlockOutput::Seektable {
|
||||
hash: "18629e1b874cb27e4364da72fb3fec2141eb0618baae4a1cee6ed09562aa00a8",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::CueSheet {
|
||||
hash: "70638a241ca06881a52c0a18258ea2d8946a830137a70479c49746d2a1344bdd",
|
||||
},
|
||||
],
|
||||
audio_hash: "e993070f2080f2c598be1d61d208e9187a55ddea4be1d2ed1f8043e7c03e97a5",
|
||||
stripped_hash: "57c5b945e14c6fcd06916d6a57e5b036d67ff35757893c24ed872007aabbcf4b",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_54",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac"
|
||||
),
|
||||
in_hash: "b68dc6644784fac35aa07581be8603a360d1697e07a2265d7eb24001936fd247",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 1694,
|
||||
max_frame_size: 7145,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 433151,
|
||||
md5_signature: "1d950e92b357dedbc5290a7f2210a2ef",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Hash {
|
||||
n_comments: 20000,
|
||||
hash: "0371b6f158411f35121f1d62ccbc18c90c9b1b0263e51bfc1b8fc942892eaf12",
|
||||
},
|
||||
pictures: &[],
|
||||
},
|
||||
],
|
||||
audio_hash: "4721b784058410c6263f73680079e9a71aee914c499afcf5580c121fce00e874",
|
||||
stripped_hash: "5c8b92b83c0fa17821add38263fa323d1c66cfd2ee57aca054b50bd05b9df5c2",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_55",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/55 - file 48-53 combined.flac"
|
||||
),
|
||||
in_hash: "a756b460df79b7cc492223f80cda570e4511f2024e5fa0c4d505ba51b86191f6",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 3103,
|
||||
max_frame_size: 11306,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 2646000,
|
||||
md5_signature: "2c78978cbbff11daac296fee97c3e061",
|
||||
},
|
||||
FlacBlockOutput::Seektable {
|
||||
hash: "58dfa7bac4974edf1956b068f5aa72d1fbd9301c36a3085a8a57b9db11a2dbf0",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.3 20190804",
|
||||
comments: VorbisCommentTestValue::Hash {
|
||||
n_comments: 40036,
|
||||
hash: "8d8f21954b4aaee2c7ec92389125a9b28b7de5a8153c62abdd80330f445214df",
|
||||
},
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::CueSheet {
|
||||
hash: "db11916c8f5f39648256f93f202e00ff8d73d7d96b62f749b4c77cf3ea744f90",
|
||||
},
|
||||
FlacBlockOutput::Application {
|
||||
application_id: 0x74657374,
|
||||
hash: "6088a557a1bad7bfa5ebf79a324669fbf4fa2f8e708f5487305dfc5b2ff2249a",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "",
|
||||
width: 3200,
|
||||
height: 2252,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
|
||||
},
|
||||
FlacBlockOutput::Padding { size: 16777215 },
|
||||
],
|
||||
audio_hash: "f1285b77cec7fa9a0979033244489a9d06b8515b2158e9270087a65a4007084d",
|
||||
stripped_hash: "401038fce06aff5ebdc7a5f2fc01fa491cbf32d5da9ec99086e414b2da3f8449",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_56",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/56 - JPG PICTURE.flac"
|
||||
),
|
||||
in_hash: "5cebe7a3710cf8924bd2913854e9ca60b4cd53cfee5a3af0c3c73fddc1888963",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 3014,
|
||||
max_frame_size: 7219,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 220026,
|
||||
md5_signature: "5b0e898d9c2626d0c28684f5a586813f",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6",
|
||||
},
|
||||
],
|
||||
audio_hash: "ccfe90b0f15cd9662f7a18f40cd4c347538cf8897a08228e75351206f7804573",
|
||||
stripped_hash: "31a38d59db2010790b7abf65ec0cc03f2bbe1fed5952bc72bee4ca4d0c92e79f",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_57",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/57 - PNG PICTURE.flac"
|
||||
),
|
||||
in_hash: "c6abff7f8bb63c2821bd21dd9052c543f10ba0be878e83cb419c248f14f72697",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 463,
|
||||
max_frame_size: 6770,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 221623,
|
||||
md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_PNG,
|
||||
description: "",
|
||||
width: 960,
|
||||
height: 540,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
|
||||
},
|
||||
],
|
||||
audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f",
|
||||
stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_58",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/58 - GIF PICTURE.flac"
|
||||
),
|
||||
in_hash: "7c2b1a963a665847167a7275f9924f65baeb85c21726c218f61bf3f803f301c8",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 2853,
|
||||
max_frame_size: 6683,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 219826,
|
||||
md5_signature: "7c1810602a7db96d7a48022ac4aa495c",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_GIF,
|
||||
description: "",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 32,
|
||||
img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1",
|
||||
},
|
||||
],
|
||||
audio_hash: "30e3292e9f56cf88658eeadfdec8ad3a440690ce6d813e1b3374f60518c8e0ae",
|
||||
stripped_hash: "4cd771e27870e2a586000f5b369e0426183a521b61212302a2f5802b046910b2",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "subset_59",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_subset/59 - AVIF PICTURE.flac"
|
||||
),
|
||||
in_hash: "7395d02bf8d9533dc554cce02dee9de98c77f8731a45f62d0a243bd0d6f9a45c",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 153,
|
||||
max_frame_size: 7041,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 221423,
|
||||
md5_signature: "d354246011ca204159c06f52cad5f634",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: Mime::from_str("image/avif").unwrap(),
|
||||
description: "",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc",
|
||||
},
|
||||
],
|
||||
audio_hash: "b208c73d274e65b27232bfffbfcbcf4805ee3cbc9cfbf7d2104db8f53370273b",
|
||||
stripped_hash: "d5215e16c6b978fc2c3e6809e1e78981497cb8514df297c5169f3b4a28fd875c",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "custom_01",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_custom/01 - many images.flac"
|
||||
),
|
||||
in_hash: "8a5df37488866cd91ac16773e549ef4e3a85d9f88a0d9d345f174807bb536b96",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 5099,
|
||||
max_frame_size: 7126,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 265617,
|
||||
md5_signature: "82164e4da30ed43b47e6027cef050648",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_PNG,
|
||||
description: "",
|
||||
width: 960,
|
||||
height: 540,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
|
||||
}],
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "",
|
||||
width: 3200,
|
||||
height: 2252,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::ABrightColoredFish,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
description: "lorem",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::OtherFileIcon,
|
||||
mime: mime::IMAGE_PNG,
|
||||
description: "ipsum",
|
||||
width: 960,
|
||||
height: 540,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::Lyricist,
|
||||
mime: mime::IMAGE_GIF,
|
||||
description: "dolor",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 32,
|
||||
img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1",
|
||||
},
|
||||
FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::BackCover,
|
||||
mime: Mime::from_str("image/avif").unwrap(),
|
||||
description: "est",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc",
|
||||
},
|
||||
],
|
||||
audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c",
|
||||
stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c",
|
||||
},
|
||||
FlacTestCase::Success {
|
||||
test_name: "custom_02",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_custom/02 - picture in vorbis comment.flac"
|
||||
),
|
||||
in_hash: "f6bb1a726fe6a3e25a4337d36e29fdced8ff01a46d627b7c2e1988c88f461f8c",
|
||||
blocks: vec![
|
||||
FlacBlockOutput::Streaminfo {
|
||||
min_block_size: 4096,
|
||||
max_block_size: 4096,
|
||||
min_frame_size: 463,
|
||||
max_frame_size: 6770,
|
||||
sample_rate: 44100,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
total_samples: 221623,
|
||||
md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7",
|
||||
},
|
||||
FlacBlockOutput::VorbisComment {
|
||||
vendor: "reference libFLAC 1.3.2 20170101",
|
||||
comments: VorbisCommentTestValue::Raw { tags: &[] },
|
||||
pictures: &[FlacBlockOutput::Picture {
|
||||
picture_type: PictureType::FrontCover,
|
||||
mime: mime::IMAGE_PNG,
|
||||
description: "",
|
||||
width: 960,
|
||||
height: 540,
|
||||
bit_depth: 24,
|
||||
color_count: 0,
|
||||
img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2",
|
||||
}],
|
||||
},
|
||||
],
|
||||
audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f",
|
||||
stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1",
|
||||
},
|
||||
FlacTestCase::Error {
|
||||
test_name: "custom_03",
|
||||
file_path: concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac"
|
||||
),
|
||||
in_hash: "7177f0ae4f04a563292be286ec05967f81ab16eb0a28b70fc07a1e47da9cafd0",
|
||||
check_error: &|x| matches!(x, FlacDecodeError::MalformedPicture(_)),
|
||||
stripped_hash: Some("3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1"),
|
||||
pictures: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_sanity_check() {
|
||||
assert!(manifest().iter().map(|x| x.get_name()).all_unique());
|
||||
assert!(manifest().iter().map(|x| x.get_path()).all_unique());
|
||||
}
|
||||
194
crates/pile-flac/src/vorbiscomment.rs
Normal file
194
crates/pile-flac/src/vorbiscomment.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Decode and write Vorbis comment blocks
|
||||
|
||||
use base64::Engine;
|
||||
use smartstring::{LazyCompact, SmartString};
|
||||
use std::{
|
||||
io::{Cursor, Read, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use super::tagtype::TagType;
|
||||
use crate::{
|
||||
FlacDecodeError, FlacEncodeError,
|
||||
blocks::{FlacMetablockDecode, FlacMetablockEncode, FlacPictureBlock},
|
||||
};
|
||||
|
||||
/// A decoded vorbis comment block
|
||||
#[derive(Debug)]
|
||||
pub struct VorbisComment {
|
||||
/// This comment's vendor string
|
||||
pub vendor: SmartString<LazyCompact>,
|
||||
|
||||
/// List of (tag, value)
|
||||
/// Repeated tags are allowed!
|
||||
pub comments: Vec<(TagType, SmartString<LazyCompact>)>,
|
||||
|
||||
/// A list of pictures found in this comment
|
||||
pub pictures: Vec<FlacPictureBlock>,
|
||||
}
|
||||
|
||||
impl VorbisComment {
|
||||
/// Try to decode the given data as a vorbis comment block
|
||||
pub fn decode(data: &[u8]) -> Result<Self, FlacDecodeError> {
|
||||
let mut d = Cursor::new(data);
|
||||
|
||||
// This is re-used whenever we need to read four bytes
|
||||
let mut block = [0u8; 4];
|
||||
|
||||
let vendor = {
|
||||
d.read_exact(&mut block)?;
|
||||
let length = u32::from_le_bytes(block);
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut text = vec![
|
||||
0u8;
|
||||
length
|
||||
.try_into()
|
||||
.expect("vendor length does not fit into usize")
|
||||
];
|
||||
|
||||
d.read_exact(&mut text)?;
|
||||
String::from_utf8(text)?
|
||||
};
|
||||
|
||||
d.read_exact(&mut block)?;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let n_comments: usize = u32::from_le_bytes(block)
|
||||
.try_into()
|
||||
.expect("comment count does not fit into usize");
|
||||
|
||||
let mut comments = Vec::new();
|
||||
let mut pictures = Vec::new();
|
||||
for _ in 0..n_comments {
|
||||
let comment = {
|
||||
d.read_exact(&mut block)?;
|
||||
|
||||
let length = u32::from_le_bytes(block);
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut text = vec![
|
||||
0u8;
|
||||
length
|
||||
.try_into()
|
||||
.expect("comment length does not fit into usize")
|
||||
];
|
||||
|
||||
d.read_exact(&mut text)?;
|
||||
|
||||
String::from_utf8(text)?
|
||||
};
|
||||
let (var, val) = comment
|
||||
.split_once('=')
|
||||
.ok_or(FlacDecodeError::MalformedCommentString(comment.clone()))?;
|
||||
|
||||
if !val.is_empty() {
|
||||
if var.to_uppercase() == "METADATA_BLOCK_PICTURE" {
|
||||
pictures.push(FlacPictureBlock::decode(
|
||||
&base64::prelude::BASE64_STANDARD
|
||||
.decode(val)
|
||||
.map_err(FlacDecodeError::MalformedPicture)?,
|
||||
)?);
|
||||
} else {
|
||||
// Make sure empty strings are saved as "None"
|
||||
comments.push((
|
||||
TagType::from_str(var).unwrap_or(TagType::Other(var.into())),
|
||||
val.into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
vendor: vendor.into(),
|
||||
comments,
|
||||
pictures,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VorbisComment {
|
||||
/// Get the number of bytes that `encode()` will write.
|
||||
#[expect(clippy::expect_used)]
|
||||
pub fn get_len(&self) -> u32 {
|
||||
let mut sum: u32 = 0;
|
||||
sum += u32::try_from(self.vendor.len()).expect("vendor length does not fit into u32") + 4;
|
||||
sum += 4;
|
||||
|
||||
for (tagtype, value) in &self.comments {
|
||||
let tagtype_str = tagtype.to_vorbis_string();
|
||||
let str = format!("{tagtype_str}={value}");
|
||||
sum +=
|
||||
4 + u32::try_from(str.len()).expect("comment string length does not fit into u32");
|
||||
}
|
||||
|
||||
for p in &self.pictures {
|
||||
// Compute b64 len
|
||||
let mut x = p.get_len();
|
||||
if x % 3 != 0 {
|
||||
x -= x % 3;
|
||||
x += 3;
|
||||
}
|
||||
|
||||
#[expect(clippy::integer_division)]
|
||||
{
|
||||
sum += 4 * (x / 3);
|
||||
}
|
||||
|
||||
// Add "METADATA_BLOCK_PICTURE="
|
||||
sum += 23;
|
||||
|
||||
// Add length bytes
|
||||
sum += 4;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// Try to encode this vorbis comment
|
||||
#[expect(clippy::expect_used)]
|
||||
pub fn encode(&self, target: &mut impl Write) -> Result<(), FlacEncodeError> {
|
||||
target.write_all(
|
||||
&u32::try_from(self.vendor.len())
|
||||
.expect("vendor length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(self.vendor.as_bytes())?;
|
||||
|
||||
target.write_all(
|
||||
&u32::try_from(self.comments.len() + self.pictures.len())
|
||||
.expect("total comment count does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
|
||||
for (tagtype, value) in &self.comments {
|
||||
let tagtype_str = tagtype.to_vorbis_string();
|
||||
let str = format!("{tagtype_str}={value}");
|
||||
target.write_all(
|
||||
&u32::try_from(str.len())
|
||||
.expect("comment string length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(str.as_bytes())?;
|
||||
}
|
||||
|
||||
for p in &self.pictures {
|
||||
let mut pic_data = Vec::new();
|
||||
p.encode(false, false, &mut pic_data)?;
|
||||
|
||||
let pic_string = format!(
|
||||
"METADATA_BLOCK_PICTURE={}",
|
||||
&base64::prelude::BASE64_STANDARD.encode(&pic_data)
|
||||
);
|
||||
|
||||
target.write_all(
|
||||
&u32::try_from(pic_string.len())
|
||||
.expect("picture string length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(pic_string.as_bytes())?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user