use crate::bytes::read_u32_be;
use crate::error::{FormatError, Result};
use crate::probe::Extent;
use crate::size;
pub(crate) const FLAC_MARKER: &[u8; 4] = b"fLaC";
pub(crate) const BLOCK_STREAMINFO: u8 = 0;
pub(crate) const BLOCK_APPLICATION: u8 = 2;
pub(crate) const BLOCK_SEEKTABLE: u8 = 3;
pub(crate) const BLOCK_VORBIS_COMMENT: u8 = 4;
pub(crate) const BLOCK_CUESHEET: u8 = 5;
pub(crate) const BLOCK_PICTURE: u8 = 6;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataBlock {
pub block_type: u8,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlacScan {
pub audio_offset: u64,
pub audio_length: u64,
pub preserved: Vec<MetadataBlock>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlacMeta {
pub audio_offset: u64,
pub preserved: Vec<MetadataBlock>,
}
struct BlockHead {
index: usize,
block_type: u8,
len: usize,
}
enum BlockStep<'a> {
Ready(BlockHead, &'a [u8]),
Truncated(BlockHead, u64),
NeedHeader(u64),
}
struct BlockWalker<'a> {
data: &'a [u8],
pos: usize,
index: usize,
done: bool,
}
impl<'a> BlockWalker<'a> {
fn new(data: &'a [u8]) -> Result<Self> {
if data.len() < 4 || &data[0..4] != FLAC_MARKER {
return Err(FormatError::NotFlac);
}
Ok(Self {
data,
pos: 4,
index: 0,
done: false,
})
}
fn audio_offset(&self) -> u64 {
self.pos as u64
}
fn next_block(&mut self) -> Option<BlockStep<'a>> {
if self.done {
return None;
}
if self.pos + 4 > self.data.len() {
self.done = true;
return Some(BlockStep::NeedHeader((self.pos + 4) as u64));
}
let header = self.data[self.pos];
let is_last = (header & 0x80) != 0;
let block_type = header & 0x7F;
let len = u24_be(
self.data[self.pos + 1],
self.data[self.pos + 2],
self.data[self.pos + 3],
);
let head = BlockHead {
index: self.index,
block_type,
len,
};
let body_start = self.pos + 4;
let body_end = body_start + len;
if body_end > self.data.len() {
self.done = true;
return Some(BlockStep::Truncated(head, body_end as u64));
}
self.pos = body_end;
self.index += 1;
if is_last {
self.done = true;
}
Some(BlockStep::Ready(head, &self.data[body_start..body_end]))
}
}
fn parse_blocks(data: &[u8]) -> Result<FlacMeta> {
let mut walker = BlockWalker::new(data)?;
let mut preserved = Vec::new();
while let Some(step) = walker.next_block() {
match step {
BlockStep::Ready(head, body) => {
check_streaminfo_position(head.index, head.block_type, head.len)?;
if matches!(
head.block_type,
BLOCK_STREAMINFO | BLOCK_APPLICATION | BLOCK_SEEKTABLE | BLOCK_CUESHEET
) {
preserved.push(MetadataBlock {
block_type: head.block_type,
body: body.to_vec(),
});
}
}
BlockStep::Truncated(..) | BlockStep::NeedHeader(_) => {
return Err(FormatError::Malformed);
}
}
}
Ok(FlacMeta {
audio_offset: walker.audio_offset(),
preserved,
})
}
pub fn read_metadata(data: &[u8]) -> Result<FlacMeta> {
parse_blocks(data)
}
pub fn read_metadata_bounded(prefix: &[u8]) -> Result<Extent<FlacMeta>> {
let mut walker = BlockWalker::new(prefix)?;
let mut preserved = Vec::new();
while let Some(step) = walker.next_block() {
match step {
BlockStep::Ready(head, body) => {
check_streaminfo_position(head.index, head.block_type, head.len)?;
if matches!(
head.block_type,
BLOCK_STREAMINFO | BLOCK_APPLICATION | BLOCK_SEEKTABLE | BLOCK_CUESHEET
) {
preserved.push(MetadataBlock {
block_type: head.block_type,
body: body.to_vec(),
});
}
}
BlockStep::Truncated(head, up_to) => {
check_streaminfo_position(head.index, head.block_type, head.len)?;
return Ok(Extent::NeedMore { up_to });
}
BlockStep::NeedHeader(up_to) => {
return Ok(Extent::NeedMore { up_to });
}
}
}
Ok(Extent::Complete(FlacMeta {
audio_offset: walker.audio_offset(),
preserved,
}))
}
pub fn locate_audio(data: &[u8]) -> Result<FlacScan> {
let meta = parse_blocks(data)?;
Ok(FlacScan {
audio_offset: meta.audio_offset,
audio_length: data.len() as u64 - meta.audio_offset,
preserved: meta.preserved,
})
}
use crate::input::{
ArtInput, BinaryTagInput, EmbeddedBinaryTag, EmbeddedPicture, PictureType, TagInput,
};
use crate::layout::{RegionLayout, Segment};
pub const MAX_BLOCK_BODY: u64 = 0x00FF_FFFF;
const STREAMINFO_BODY_LEN: usize = 34;
fn check_streaminfo_position(index: usize, block_type: u8, body_len: usize) -> Result<()> {
let is_streaminfo = block_type == BLOCK_STREAMINFO;
if index == 0 {
if !is_streaminfo || body_len != STREAMINFO_BODY_LEN {
return Err(FormatError::Malformed);
}
} else if is_streaminfo {
return Err(FormatError::Malformed);
}
Ok(())
}
pub(crate) fn push_block_header(
out: &mut Vec<u8>,
block_type: u8,
body_len: usize,
is_last: bool,
) -> Result<()> {
let len = u32::try_from(body_len)
.ok()
.filter(|&v| u64::from(v) <= MAX_BLOCK_BODY)
.ok_or(FormatError::TooLarge)?;
let first = (if is_last { 0x80 } else { 0 }) | (block_type & 0x7F);
out.push(first);
out.extend_from_slice(&len.to_be_bytes()[1..]);
Ok(())
}
pub fn structural_block_type(kind: &str) -> Option<u8> {
match kind {
"STREAMINFO" => Some(BLOCK_STREAMINFO),
"SEEKTABLE" => Some(BLOCK_SEEKTABLE),
_ => None,
}
}
pub fn split_preserved(
blocks: &[MetadataBlock],
) -> (Vec<(String, Vec<u8>)>, Vec<EmbeddedBinaryTag>) {
let mut structural = Vec::new();
let mut binary = Vec::new();
for blk in blocks {
match blk.block_type {
BLOCK_STREAMINFO => structural.push(("STREAMINFO".to_string(), blk.body.clone())),
BLOCK_SEEKTABLE => structural.push(("SEEKTABLE".to_string(), blk.body.clone())),
BLOCK_APPLICATION => binary.push(EmbeddedBinaryTag {
key: "APPLICATION".to_string(),
payload: blk.body.clone(),
}),
BLOCK_CUESHEET => binary.push(EmbeddedBinaryTag {
key: "CUESHEET".to_string(),
payload: blk.body.clone(),
}),
_ => {}
}
}
(structural, binary)
}
pub(crate) fn picture_body_framing(art: &ArtInput, description: &str) -> Result<Vec<u8>> {
let mut out = Vec::new();
out.extend_from_slice(&art.picture_type.get().to_be_bytes());
out.extend_from_slice(
&u32::try_from(art.mime.len())
.map_err(|_| FormatError::TooLarge)?
.to_be_bytes(),
);
out.extend_from_slice(art.mime.as_bytes());
out.extend_from_slice(
&u32::try_from(description.len())
.map_err(|_| FormatError::TooLarge)?
.to_be_bytes(),
);
out.extend_from_slice(description.as_bytes());
out.extend_from_slice(&art.width.to_be_bytes());
out.extend_from_slice(&art.height.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(
&u32::try_from(art.data_len.get())
.map_err(|_| FormatError::TooLarge)?
.to_be_bytes(),
); Ok(out)
}
pub fn synthesize_layout(
structural: &[MetadataBlock],
audio_offset: u64,
audio_length: u64,
tags: &[TagInput],
binary_tags: &[BinaryTagInput],
arts: &[ArtInput],
) -> Result<RegionLayout> {
let streaminfo: Vec<&MetadataBlock> = structural
.iter()
.filter(|b| b.block_type == BLOCK_STREAMINFO)
.collect();
if streaminfo.len() != 1 || streaminfo[0].body.len() != STREAMINFO_BODY_LEN {
return Err(FormatError::Malformed);
}
let mut ordered: Vec<&MetadataBlock> = structural.iter().collect();
ordered.sort_by_key(|b| b.block_type);
let valid_binary: Vec<&BinaryTagInput> = binary_tags
.iter()
.filter(|bt| matches!(bt.key.as_str(), "APPLICATION" | "CUESHEET"))
.collect();
let nonempty_art = arts.len();
let num_blocks = ordered.len() + 1 + valid_binary.len() + nonempty_art;
let last_index = num_blocks - 1;
let mut segments: Vec<Segment> = Vec::new();
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(FLAC_MARKER);
let mut idx = 0usize;
for blk in &ordered {
push_block_header(&mut buf, blk.block_type, blk.body.len(), idx == last_index)?;
buf.extend_from_slice(&blk.body);
idx += 1;
}
let vc = crate::vorbiscomment::build(tags)?;
if vc.len() as u64 > MAX_BLOCK_BODY {
return Err(FormatError::TooLarge);
}
push_block_header(&mut buf, BLOCK_VORBIS_COMMENT, vc.len(), idx == last_index)?;
buf.extend_from_slice(&vc);
idx += 1;
for bt in valid_binary {
let block_type = match bt.key.as_str() {
"APPLICATION" => BLOCK_APPLICATION,
"CUESHEET" => BLOCK_CUESHEET,
_ => continue,
};
if bt.len.get() > MAX_BLOCK_BODY {
return Err(FormatError::TooLarge);
}
push_block_header(
&mut buf,
block_type,
crate::convert::usize_from(bt.len.get()),
idx == last_index,
)?;
segments.push(Segment::Inline(std::mem::take(&mut buf)));
segments.push(Segment::BinaryTag {
payload_id: bt.payload_id,
len: bt.len,
});
idx += 1;
}
for art in arts {
let framing = picture_body_framing(art, &art.description)?;
let body_len = size::checked_add(framing.len() as u64, art.data_len.get())?;
if body_len > MAX_BLOCK_BODY {
return Err(FormatError::TooLarge);
}
push_block_header(
&mut buf,
BLOCK_PICTURE,
crate::convert::usize_from(body_len),
idx == last_index,
)?;
buf.extend_from_slice(&framing);
segments.push(Segment::Inline(std::mem::take(&mut buf)));
segments.push(Segment::ArtImage {
art_id: art.art_id,
len: art.data_len,
});
idx += 1;
}
if !buf.is_empty() {
segments.push(Segment::Inline(buf));
}
segments.push(Segment::BackingAudio {
offset: audio_offset,
len: audio_length,
});
Ok(RegionLayout::validated(segments)?)
}
pub fn read_vorbis_comments(data: &[u8]) -> Result<Vec<(String, String)>> {
let mut walker = BlockWalker::new(data)?;
while let Some(step) = walker.next_block() {
match step {
BlockStep::Ready(head, body) => {
if head.block_type == BLOCK_VORBIS_COMMENT {
return crate::vorbiscomment::parse(body);
}
}
BlockStep::Truncated(..) | BlockStep::NeedHeader(_) => {
return Err(FormatError::Malformed);
}
}
}
Ok(Vec::new())
}
fn u24_be(b0: u8, b1: u8, b2: u8) -> usize {
u32::from_be_bytes([0, b0, b1, b2]) as usize
}
pub(crate) fn parse_picture_block(body: &[u8]) -> Result<EmbeddedPicture> {
let mut pos = 0usize;
let picture_type = read_u32_be(body, pos)?;
pos += 4;
let mime_len = read_u32_be(body, pos)? as usize;
pos += 4;
let mime_end = pos + mime_len;
if mime_end > body.len() {
return Err(FormatError::Malformed);
}
let mime = String::from_utf8_lossy(&body[pos..mime_end]).into_owned();
pos = mime_end;
let desc_len = read_u32_be(body, pos)? as usize;
pos += 4;
let desc_end = pos + desc_len;
if desc_end > body.len() {
return Err(FormatError::Malformed);
}
let description = String::from_utf8_lossy(&body[pos..desc_end]).into_owned();
pos = desc_end;
let width = read_u32_be(body, pos)?;
pos += 4;
let height = read_u32_be(body, pos)?;
pos += 4;
let _depth = read_u32_be(body, pos)?;
pos += 4;
let _colors = read_u32_be(body, pos)?;
pos += 4;
let data_len = read_u32_be(body, pos)? as usize;
pos += 4;
let data_end = pos + data_len;
if data_end > body.len() {
return Err(FormatError::Malformed);
}
Ok(EmbeddedPicture {
mime,
picture_type: PictureType::new(picture_type).unwrap_or(PictureType::ZERO),
description,
width,
height,
data: body[pos..data_end].to_vec(),
})
}
pub fn read_pictures(data: &[u8]) -> Result<Vec<EmbeddedPicture>> {
let mut walker = BlockWalker::new(data)?;
let mut out = Vec::new();
while let Some(step) = walker.next_block() {
match step {
BlockStep::Ready(head, body) => {
if head.block_type == BLOCK_PICTURE {
out.push(parse_picture_block(body)?);
}
}
BlockStep::Truncated(..) | BlockStep::NeedHeader(_) => {
return Err(FormatError::Malformed);
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{BlobLen, PictureType};
use crate::probe::Extent;
fn flac_with_streaminfo(audio: &[u8]) -> (Vec<u8>, u64) {
let mut v = b"fLaC".to_vec();
push_block_header(&mut v, BLOCK_STREAMINFO, 34, true).unwrap();
v.extend(std::iter::repeat_n(0u8, 34));
let audio_offset = v.len() as u64;
v.extend_from_slice(audio);
(v, audio_offset)
}
#[test]
fn read_metadata_bounded_complete_when_prefix_covers_blocks() {
let (full, audio_offset) = flac_with_streaminfo(b"AUDIOAUDIO");
let prefix = &full[..crate::convert::usize_from(audio_offset) + 2];
match read_metadata_bounded(prefix).unwrap() {
Extent::Complete(meta) => assert_eq!(meta.audio_offset, audio_offset),
other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
}
}
#[test]
fn read_metadata_bounded_needmore_when_block_body_truncated() {
let (full, audio_offset) = flac_with_streaminfo(b"AUDIO");
let prefix = &full[..8];
match read_metadata_bounded(prefix).unwrap() {
Extent::NeedMore { up_to } => assert_eq!(up_to, audio_offset),
other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
}
}
#[test]
fn read_u32_be_assembles_big_endian_and_guards_length() {
let data = [0x11u8, 0x22, 0x33, 0x44, 0x55];
assert_eq!(read_u32_be(&data, 0).unwrap(), 0x1122_3344);
assert_eq!(read_u32_be(&data, 1).unwrap(), 0x2233_4455);
assert_eq!(read_u32_be(&data, 2), Err(FormatError::Malformed));
}
#[test]
fn push_block_header_emits_24bit_length_big_endian() {
let mut out = Vec::new();
push_block_header(&mut out, BLOCK_PICTURE, 0x12_3456, false).unwrap();
assert_eq!(out, vec![BLOCK_PICTURE, 0x12, 0x34, 0x56]);
let mut last = Vec::new();
push_block_header(&mut last, BLOCK_VORBIS_COMMENT, 0, true).unwrap();
assert_eq!(last, vec![0x80 | BLOCK_VORBIS_COMMENT, 0x00, 0x00, 0x00]);
}
fn raw_block(block_type: u8, body: &[u8], last: bool, len_override: Option<usize>) -> Vec<u8> {
let n = len_override.unwrap_or(body.len());
let mut v = vec![(if last { 0x80 } else { 0 }) | (block_type & 0x7F)];
v.extend_from_slice(&u32::try_from(n).unwrap().to_be_bytes()[1..]);
v.extend_from_slice(body);
v
}
fn flac_with(blocks: &[Vec<u8>]) -> Vec<u8> {
let mut f = b"fLaC".to_vec();
for b in blocks {
f.extend_from_slice(b);
}
f
}
fn valid_streaminfo() -> MetadataBlock {
MetadataBlock {
block_type: BLOCK_STREAMINFO,
body: vec![0u8; 34],
}
}
#[test]
fn locate_audio_rejects_missing_streaminfo() {
let file = flac_with(&[raw_block(BLOCK_VORBIS_COMMENT, &[], true, None)]);
assert_eq!(locate_audio(&file), Err(FormatError::Malformed));
}
#[test]
fn locate_audio_rejects_streaminfo_wrong_body_len() {
let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[0u8; 10], true, None)]);
assert_eq!(locate_audio(&file), Err(FormatError::Malformed));
}
#[test]
fn locate_audio_rejects_duplicate_streaminfo() {
let file = flac_with(&[
raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None),
raw_block(BLOCK_STREAMINFO, &[0u8; 34], true, None),
]);
assert_eq!(locate_audio(&file), Err(FormatError::Malformed));
}
#[test]
fn read_metadata_bounded_rejects_duplicate_streaminfo() {
let file = flac_with(&[
raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None),
raw_block(BLOCK_STREAMINFO, &[0u8; 34], true, None),
]);
assert_eq!(read_metadata_bounded(&file), Err(FormatError::Malformed));
}
#[test]
fn bounded_fails_closed_on_bad_first_header_before_widening() {
let mut prefix = b"fLaC".to_vec();
prefix.push(BLOCK_STREAMINFO | 0x80); prefix.extend_from_slice(&[0xFF, 0xFF, 0xFF]); assert_eq!(read_metadata_bounded(&prefix), Err(FormatError::Malformed));
}
#[test]
fn synthesize_layout_rejects_structural_without_streaminfo() {
let structural = [MetadataBlock {
block_type: BLOCK_SEEKTABLE,
body: vec![0u8; 4],
}];
assert_eq!(
synthesize_layout(&structural, 0, 0, &[], &[], &[]),
Err(FormatError::Malformed)
);
}
#[test]
fn parse_blocks_rejects_short_and_wrong_marker() {
assert_eq!(parse_blocks(b"fLa"), Err(FormatError::NotFlac));
assert_eq!(parse_blocks(b"fLaC"), Err(FormatError::Malformed));
assert_eq!(parse_blocks(b"XXXX____"), Err(FormatError::NotFlac));
}
#[test]
fn parse_blocks_guards_truncated_block_header() {
assert_eq!(parse_blocks(b"fLaC\x80"), Err(FormatError::Malformed));
}
#[test]
fn parse_blocks_accepts_header_flush_with_end() {
let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[0u8; 34], true, None)]);
let meta = parse_blocks(&file).unwrap();
assert_eq!(meta.audio_offset, 42); }
#[test]
fn parse_blocks_decodes_24bit_length_high_byte() {
let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x01_0000))]);
assert_eq!(parse_blocks(&file), Err(FormatError::Malformed));
}
#[test]
fn parse_blocks_preserves_structural_blocks() {
let si = vec![0xAA; 34];
let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &si, true, None)]);
let meta = parse_blocks(&file).unwrap();
assert_eq!(meta.audio_offset, 4 + 4 + 34);
assert_eq!(meta.preserved.len(), 1);
assert_eq!(meta.preserved[0].block_type, BLOCK_STREAMINFO);
assert_eq!(meta.preserved[0].body, si);
}
fn vc_body(vendor: &str, comments: &[&str]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&u32::try_from(vendor.len()).unwrap().to_le_bytes());
v.extend_from_slice(vendor.as_bytes());
v.extend_from_slice(&u32::try_from(comments.len()).unwrap().to_le_bytes());
for c in comments {
v.extend_from_slice(&u32::try_from(c.len()).unwrap().to_le_bytes());
v.extend_from_slice(c.as_bytes());
}
v
}
#[test]
fn read_vorbis_comments_returns_pairs_and_guards_marker() {
let vc = vc_body("v", &["TITLE=Hi", "ARTIST=Me"]);
let file = flac_with(&[raw_block(BLOCK_VORBIS_COMMENT, &vc, true, None)]);
let got = read_vorbis_comments(&file).unwrap();
assert_eq!(
got,
vec![
("title".to_string(), "Hi".to_string()),
("artist".to_string(), "Me".to_string()),
]
);
assert_eq!(read_vorbis_comments(b"fLa"), Err(FormatError::NotFlac));
assert_eq!(read_vorbis_comments(b"fLaC"), Err(FormatError::Malformed));
}
#[test]
fn read_vorbis_comments_guards_block_walk() {
assert_eq!(
read_vorbis_comments(b"fLaC\x80"),
Err(FormatError::Malformed)
);
let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, None)]);
assert_eq!(read_vorbis_comments(&file).unwrap(), Vec::new());
}
#[test]
fn read_vorbis_comments_decodes_24bit_length() {
let hi = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x01_0000))]);
assert_eq!(read_vorbis_comments(&hi), Err(FormatError::Malformed));
let mid = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x00_0100))]);
assert_eq!(read_vorbis_comments(&mid), Err(FormatError::Malformed));
}
fn picture_body(ptype: u32, mime: &str, desc: &str, w: u32, h: u32, data: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&ptype.to_be_bytes());
v.extend_from_slice(&u32::try_from(mime.len()).unwrap().to_be_bytes());
v.extend_from_slice(mime.as_bytes());
v.extend_from_slice(&u32::try_from(desc.len()).unwrap().to_be_bytes());
v.extend_from_slice(desc.as_bytes());
v.extend_from_slice(&w.to_be_bytes());
v.extend_from_slice(&h.to_be_bytes());
v.extend_from_slice(&0u32.to_be_bytes()); v.extend_from_slice(&0u32.to_be_bytes()); v.extend_from_slice(&u32::try_from(data.len()).unwrap().to_be_bytes());
v.extend_from_slice(data);
v
}
#[test]
fn parse_picture_block_roundtrips_fields() {
let body = picture_body(3, "image/png", "desc", 4, 5, b"PIXELS");
let p = parse_picture_block(&body).unwrap();
assert_eq!(p.picture_type.get(), 3);
assert_eq!(p.mime, "image/png");
assert_eq!(p.description, "desc");
assert_eq!(p.width, 4);
assert_eq!(p.height, 5);
assert_eq!(p.data, b"PIXELS");
}
#[test]
fn read_picture_clamps_out_of_range_type() {
let body = picture_body(99, "png", "", 0, 0, &[0xAB]);
let pic = parse_picture_block(&body).unwrap();
assert_eq!(pic.picture_type.get(), 0, "out-of-range type clamps to 0");
}
#[test]
fn parse_picture_block_guards_field_bounds() {
let mut bad_mime = 3u32.to_be_bytes().to_vec();
bad_mime.extend_from_slice(&16u32.to_be_bytes()); bad_mime.extend_from_slice(b"ab"); assert_eq!(parse_picture_block(&bad_mime), Err(FormatError::Malformed));
let mut bad_desc = 3u32.to_be_bytes().to_vec();
bad_desc.extend_from_slice(&3u32.to_be_bytes()); bad_desc.extend_from_slice(b"png");
bad_desc.extend_from_slice(&16u32.to_be_bytes()); bad_desc.extend_from_slice(b"x"); assert_eq!(parse_picture_block(&bad_desc), Err(FormatError::Malformed));
let mut trailing = picture_body(3, "png", "", 1, 1, b"DA");
trailing.push(0xFF); assert!(parse_picture_block(&trailing).is_ok());
}
#[test]
fn read_pictures_extracts_and_guards_marker() {
let pic = picture_body(3, "image/jpeg", "front", 8, 8, b"IMG");
let file = flac_with(&[raw_block(BLOCK_PICTURE, &pic, true, None)]);
let pics = read_pictures(&file).unwrap();
assert_eq!(pics.len(), 1);
assert_eq!(pics[0].mime, "image/jpeg");
assert_eq!(pics[0].data, b"IMG");
assert_eq!(read_pictures(b"fLa"), Err(FormatError::NotFlac));
assert_eq!(read_pictures(b"fLaC"), Err(FormatError::Malformed));
}
#[test]
fn read_pictures_guards_block_walk_and_length() {
assert_eq!(read_pictures(b"fLaC\x80"), Err(FormatError::Malformed));
let none = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, None)]);
assert_eq!(read_pictures(&none).unwrap(), Vec::new());
let hi = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x01_0000))]);
assert_eq!(read_pictures(&hi), Err(FormatError::Malformed));
let mid = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x00_0100))]);
assert_eq!(read_pictures(&mid), Err(FormatError::Malformed));
}
#[test]
fn bounded_rejects_short_and_wrong_marker() {
assert_eq!(read_metadata_bounded(b"fLa"), Err(FormatError::NotFlac));
match read_metadata_bounded(b"fLaC").unwrap() {
Extent::NeedMore { up_to } => assert_eq!(up_to, 8),
other @ Extent::Complete(_) => panic!("expected NeedMore{{up_to:8}}, got {other:?}"),
}
assert_eq!(read_metadata_bounded(b"XXXX"), Err(FormatError::NotFlac));
}
#[test]
fn bounded_needmore_up_to_is_pos_plus_4_for_truncated_header() {
let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None)]);
assert_eq!(file.len(), 42);
match read_metadata_bounded(&file).unwrap() {
Extent::NeedMore { up_to } => assert_eq!(up_to, 46),
other @ Extent::Complete(_) => panic!("expected NeedMore{{up_to:46}}, got {other:?}"),
}
}
#[test]
fn bounded_is_last_flag_continues_past_nonlast_block() {
let b1 = raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None); let b2 = raw_block(BLOCK_SEEKTABLE, &[0xBB, 0xBB, 0xBB], true, None); let file = flac_with(&[b1, b2]);
let expected_offset = (4 + 38 + 7) as u64; match read_metadata_bounded(&file).unwrap() {
Extent::Complete(meta) => {
assert_eq!(meta.audio_offset, expected_offset);
assert_eq!(meta.preserved.len(), 2);
}
other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
}
}
#[test]
fn bounded_block_type_mask_preserves_streaminfo() {
let body = vec![0x5A; 34];
let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &body, true, None)]);
match read_metadata_bounded(&file).unwrap() {
Extent::Complete(meta) => {
assert_eq!(meta.preserved.len(), 1);
assert_eq!(meta.preserved[0].block_type, BLOCK_STREAMINFO);
assert_eq!(meta.preserved[0].body, body);
}
other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
}
}
#[test]
fn bounded_decodes_24bit_length_exactly() {
let b1 = raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None); let len = 0x01_0203usize;
let body = vec![0u8; len];
let b2 = raw_block(BLOCK_SEEKTABLE, &body, true, None); let file = flac_with(&[b1, b2]);
let expected_offset = (4 + 38 + 4 + len) as u64;
match read_metadata_bounded(&file).unwrap() {
Extent::Complete(meta) => {
assert_eq!(meta.audio_offset, expected_offset);
}
other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
}
}
#[test]
fn bounded_length_decodes_high_and_mid_bytes() {
let b1 = raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None); let b2 = raw_block(BLOCK_SEEKTABLE, &[], true, Some(0x01_0100)); let file = flac_with(&[b1, b2]);
match read_metadata_bounded(&file).unwrap() {
Extent::NeedMore { up_to } => {
assert_eq!(up_to, 46 + 0x01_0100);
}
other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
}
}
#[test]
fn bounded_body_end_equal_to_prefix_is_complete() {
let b1 = raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None); let b2 = raw_block(BLOCK_SEEKTABLE, &[0xCC; 6], true, None); let file = flac_with(&[b1, b2]);
let total = file.len() as u64; match read_metadata_bounded(&file).unwrap() {
Extent::Complete(meta) => assert_eq!(meta.audio_offset, total),
other @ Extent::NeedMore { .. } => {
panic!("expected Complete (exact fit), got {other:?}")
}
}
}
#[test]
fn bounded_preserves_all_structural_block_types() {
let b_si = raw_block(BLOCK_STREAMINFO, &[0x01; 34], false, None);
let b_app = raw_block(BLOCK_APPLICATION, &[0x02, 0x02], false, None);
let b_seek = raw_block(BLOCK_SEEKTABLE, &[0x03, 0x03, 0x03], false, None);
let b_cue = raw_block(BLOCK_CUESHEET, &[0x04], true, None);
let file = flac_with(&[b_si, b_app, b_seek, b_cue]);
match read_metadata_bounded(&file).unwrap() {
Extent::Complete(meta) => {
let types: Vec<u8> = meta.preserved.iter().map(|b| b.block_type).collect();
assert_eq!(
types,
vec![
BLOCK_STREAMINFO,
BLOCK_APPLICATION,
BLOCK_SEEKTABLE,
BLOCK_CUESHEET,
]
);
assert_eq!(meta.preserved[0].body, vec![0x01; 34]);
assert_eq!(meta.preserved[1].body, vec![0x02, 0x02]);
assert_eq!(meta.preserved[2].body, vec![0x03, 0x03, 0x03]);
assert_eq!(meta.preserved[3].body, vec![0x04]);
}
other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
}
}
#[test]
fn split_preserved_classifies_structural_and_binary() {
use super::{MetadataBlock, split_preserved, structural_block_type};
let blocks = vec![
MetadataBlock {
block_type: 0,
body: vec![0xAA],
},
MetadataBlock {
block_type: 2,
body: b"testDATA".to_vec(),
},
MetadataBlock {
block_type: 3,
body: vec![0xBB],
},
MetadataBlock {
block_type: 5,
body: vec![0xCC; 4],
},
];
let (structural, binary) = split_preserved(&blocks);
assert_eq!(
structural,
vec![
("STREAMINFO".to_string(), vec![0xAA]),
("SEEKTABLE".to_string(), vec![0xBB]),
]
);
assert_eq!(binary.len(), 2);
assert_eq!(binary[0].key, "APPLICATION");
assert_eq!(binary[0].payload, b"testDATA");
assert_eq!(binary[1].key, "CUESHEET");
assert_eq!(binary[1].payload, vec![0xCC; 4]);
assert_eq!(structural_block_type("STREAMINFO"), Some(0));
assert_eq!(structural_block_type("SEEKTABLE"), Some(3));
assert_eq!(structural_block_type("APPLICATION"), None);
assert_eq!(structural_block_type("bogus"), None);
}
#[test]
fn synthesize_layout_picture_block_size_boundary_is_inclusive() {
let mk = |data_len: u64| ArtInput {
art_id: 1,
mime: "image/png".to_string(),
description: String::new(),
picture_type: PictureType::new(3).unwrap(),
width: 0,
height: 0,
data_len: BlobLen::new(data_len).unwrap(),
};
let art = mk(1);
let framing_len = picture_body_framing(&art, &art.description).unwrap().len() as u64;
let at_limit = 0x00FF_FFFF - framing_len; assert!(synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[], &[mk(at_limit)]).is_ok());
assert_eq!(
synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[], &[mk(at_limit + 1)]),
Err(FormatError::TooLarge)
);
}
#[test]
fn synthesize_layout_vorbis_comment_block_size_boundary_is_inclusive() {
let overhead = crate::vorbiscomment::build(&[TagInput::new("title", "")])
.unwrap()
.len() as u64;
let at_limit = "x".repeat(crate::convert::usize_from(MAX_BLOCK_BODY - overhead));
let tags = [TagInput::new("title", at_limit.as_str())];
assert!(synthesize_layout(&[valid_streaminfo()], 0, 0, &tags, &[], &[]).is_ok());
let over = format!("{at_limit}x");
let tags = [TagInput::new("title", over.as_str())];
assert_eq!(
synthesize_layout(&[valid_streaminfo()], 0, 0, &tags, &[], &[]),
Err(FormatError::TooLarge)
);
}
#[test]
fn synthesize_layout_checked_picture_len_rejects_overflow() {
let mk = |data_len: u64| ArtInput {
art_id: 1,
mime: "image/png".to_string(),
description: String::new(),
picture_type: PictureType::new(3).unwrap(),
width: 0,
height: 0,
data_len: BlobLen::new(data_len).unwrap(),
};
assert_eq!(
synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[], &[mk(u64::MAX)]),
Err(FormatError::TooLarge)
);
}
#[test]
fn synthesize_layout_binary_tag_block_size_boundary_is_inclusive() {
let mk = |len: u64| BinaryTagInput {
key: "APPLICATION".to_string(),
payload_id: 1,
len: BlobLen::new(len).unwrap(),
};
assert!(
synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[mk(0x00FF_FFFF)], &[]).is_ok()
);
assert_eq!(
synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[mk(0x0100_0000)], &[]),
Err(FormatError::TooLarge)
);
}
}