use crate::bytes::Reader;
use crate::detect::Format;
use crate::error::{MalformedDetail, ResourceLimit, Result, StryptError};
use crate::formats::tags::{self, TagError};
use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, vorbis, xmp};
use crate::report::{
Finding, InspectOptions, MetadataKind, MetadataReport, MetadataValue, Note, Retained,
RetentionReason, StripReport,
};
#[derive(Debug, Clone, Copy, Default)]
pub struct FlacHandler;
impl MetadataHandler for FlacHandler {
fn name(&self) -> &'static str {
Format::Flac.id()
}
fn format(&self) -> Format {
Format::Flac
}
fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
let processed = process(input, options, &ParseLimits::default())?;
Ok(MetadataReport {
format: Format::Flac,
findings: processed.findings,
notes: processed.notes,
})
}
fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
let processed = process(input, &options.inspect, &options.limits)?;
Ok(Stripped {
report: StripReport {
format: Format::Flac,
removed: processed.findings,
retained: processed.retained,
notes: processed.notes,
input_bytes: as_u64(input.len()),
output_bytes: as_u64(processed.output.len()),
},
bytes: processed.output,
})
}
}
const MAGIC: &[u8; 4] = b"fLaC";
const STREAMINFO: u8 = 0;
const PADDING: u8 = 1;
const APPLICATION: u8 = 2;
const SEEKTABLE: u8 = 3;
const VORBIS_COMMENT: u8 = 4;
const CUESHEET: u8 = 5;
const PICTURE: u8 = 6;
const FORBIDDEN: u8 = 127;
const STREAMINFO_LEN: usize = 34;
const STREAMINFO_MD5_AT: usize = 18;
const APPLICATION_ID_LEN: usize = 4;
const CUESHEET_CATALOGUE_LEN: usize = 128;
struct Block<'a> {
kind: u8,
payload: &'a [u8],
}
enum Payload<'a> {
Raw(&'a [u8]),
Zeros(usize),
}
impl Payload<'_> {
const fn len(&self) -> usize {
match self {
Self::Raw(bytes) => bytes.len(),
Self::Zeros(n) => *n,
}
}
}
struct Processed {
findings: Vec<Finding>,
retained: Vec<Retained>,
notes: Vec<Note>,
output: Vec<u8>,
}
fn walk<'a>(input: &'a [u8], limits: &ParseLimits) -> Result<(Vec<Block<'a>>, &'a [u8])> {
let mut r = Reader::new(input);
if r.take(MAGIC.len()) != Some(MAGIC.as_slice()) {
return Err(malformed(MalformedDetail::MissingMarker, Some(0)));
}
let mut blocks: Vec<Block<'a>> = Vec::new();
let mut budget = limits.max_items;
loop {
let at = r.position();
spend(&mut budget)?;
let header = r
.take(4)
.ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(at)))?;
let first = header.first().copied().unwrap_or_default();
let kind = first & 0x7F;
let last = first & 0x80 != 0;
let length = block_length(header)
.ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(at)))?;
if kind == FORBIDDEN {
return Err(malformed(MalformedDetail::UnexpectedMarker, as_offset(at)));
}
if blocks.is_empty() && kind != STREAMINFO {
return Err(malformed(MalformedDetail::MissingMarker, as_offset(at)));
}
if !blocks.is_empty() && kind == STREAMINFO {
return Err(malformed(MalformedDetail::UnexpectedMarker, as_offset(at)));
}
let payload = r
.take(length)
.ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange, as_offset(at)))?;
if kind == STREAMINFO && payload.len() != STREAMINFO_LEN {
return Err(malformed(MalformedDetail::LengthOutOfRange, as_offset(at)));
}
blocks.push(Block { kind, payload });
if last {
break;
}
}
let audio = r.take_rest();
let sync = matches!((audio.first(), audio.get(1)), (Some(0xFF), Some(second)) if second & 0xFE == 0xF8);
if !sync {
return Err(malformed(
MalformedDetail::MissingMarker,
as_offset(input.len().saturating_sub(audio.len())),
));
}
Ok((blocks, audio))
}
fn block_length(header: &[u8]) -> Option<usize> {
match header.get(1..4)? {
[high, middle, low] => usize::try_from(u32::from_be_bytes([0, *high, *middle, *low])).ok(),
_ => None,
}
}
fn spend(budget: &mut u32) -> Result<()> {
if *budget == 0 {
return Err(StryptError::LimitExceeded {
format: Format::Flac,
limit: ResourceLimit::ItemCount,
});
}
*budget = budget.saturating_sub(1);
Ok(())
}
fn process(input: &[u8], options: &InspectOptions, limits: &ParseLimits) -> Result<Processed> {
let (head_tags, start) = tags::head(input).map_err(convert)?;
let (tail_tags, end) = tags::tail(input, start).map_err(convert)?;
let body = input
.get(start..end)
.ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange, as_offset(start)))?;
let (blocks, audio) = walk(body, limits)?;
let mut out = Processed {
findings: Vec::new(),
retained: Vec::new(),
notes: Vec::new(),
output: Vec::with_capacity(body.len()),
};
for tag in head_tags.iter().chain(tail_tags.iter()) {
out.findings.extend(tags::findings(tag, options));
}
let mut kept: Vec<(u8, Payload<'_>)> = Vec::new();
for block in &blocks {
let size = as_u64(block.payload.len());
match block.kind {
STREAMINFO => {
if !md5_is_absent(block.payload) {
out.retained.push(Retained {
location: "STREAMINFO (MD5 of the unencoded audio)".to_owned(),
reason: RetentionReason::DerivedFromPayload,
});
}
kept.push((block.kind, Payload::Raw(block.payload)));
}
SEEKTABLE => {
kept.push((block.kind, Payload::Raw(block.payload)));
}
PADDING => {
if block.payload.iter().any(|byte| *byte != 0) {
out.findings.push(
Finding::new(MetadataKind::Other, "PADDING", size)
.with_field("Padding")
.with_value(options, || MetadataValue::Opaque { bytes: size }),
);
}
kept.push((block.kind, Payload::Zeros(block.payload.len())));
}
APPLICATION => out.findings.push(application(block.payload, size, options)),
VORBIS_COMMENT => {
vorbis::comments(
block.payload,
"VORBIS_COMMENT",
size,
options,
&mut out.findings,
);
}
CUESHEET => {
out.findings.push(cuesheet(block.payload, size));
out.notes.push(Note::CapabilityRemoved {
location: "CUESHEET".to_owned(),
capability: "be split into the tracks of the disc it was ripped from"
.to_owned(),
});
}
PICTURE => out.findings.push(picture(block.payload, size, options)),
other => out.findings.push(
Finding::new(
MetadataKind::Other,
format!("Metadata block type {other}"),
size,
),
),
}
}
out.output.extend_from_slice(MAGIC);
let last_index = kept.len().saturating_sub(1);
for (index, (kind, payload)) in kept.iter().enumerate() {
let last = if index == last_index { 0x80 } else { 0x00 };
out.output.push(kind | last);
let length = u32::try_from(payload.len())
.unwrap_or(u32::MAX)
.to_be_bytes();
out.output
.extend_from_slice(length.get(1..4).unwrap_or_default());
match payload {
Payload::Raw(bytes) => out.output.extend_from_slice(bytes),
Payload::Zeros(n) => out.output.resize(out.output.len().saturating_add(*n), 0),
}
}
out.output.extend_from_slice(audio);
out.notes.push(Note::OutOfScopeContent {
location: "audio frames, which are copied without being decoded".to_owned(),
});
Ok(out)
}
fn convert(error: TagError) -> StryptError {
match error {
TagError::Malformed { detail, offset } => malformed(detail, as_offset(offset)),
TagError::Limit(limit) => StryptError::LimitExceeded {
format: Format::Flac,
limit,
},
}
}
fn md5_is_absent(payload: &[u8]) -> bool {
payload
.get(STREAMINFO_MD5_AT..)
.is_none_or(|md5| md5.iter().all(|byte| *byte == 0))
}
fn application(payload: &[u8], size: u64, options: &InspectOptions) -> Finding {
let id = payload.get(0..APPLICATION_ID_LEN).unwrap_or(payload);
Finding::new(MetadataKind::SoftwareFingerprint, "APPLICATION", size)
.with_field(xmp::name_of(id))
.with_value(options, || MetadataValue::Opaque { bytes: size })
}
fn cuesheet(payload: &[u8], size: u64) -> Finding {
let catalogue = payload
.get(0..CUESHEET_CATALOGUE_LEN)
.unwrap_or(payload)
.iter()
.any(|byte| *byte != 0);
let finding = Finding::new(MetadataKind::DocumentIdentifier, "CUESHEET", size);
if catalogue {
finding.with_field("MediaCatalogNumber")
} else {
finding
}
}
fn picture(payload: &[u8], size: u64, options: &InspectOptions) -> Finding {
let mut r = Reader::new(payload);
let kind = r.u32_be().unwrap_or_default();
let media_type = r
.u32_be()
.and_then(|n| usize::try_from(n).ok())
.and_then(|n| r.take(n))
.unwrap_or_default();
let description = r
.u32_be()
.and_then(|n| usize::try_from(n).ok())
.and_then(|n| r.take(n))
.unwrap_or_default();
let field = if media_type.is_empty() {
format!("PictureType {kind}")
} else {
format!("PictureType {kind} ({})", xmp::name_of(media_type))
};
Finding::new(MetadataKind::Thumbnail, "PICTURE", size)
.with_field(field)
.with_value(options, || {
if description.is_empty() {
MetadataValue::Opaque { bytes: size }
} else {
MetadataValue::Text(xmp::name_of(description))
}
})
}
fn malformed(detail: MalformedDetail, offset: Option<u64>) -> StryptError {
StryptError::Malformed {
format: Format::Flac,
offset,
detail,
}
}
fn as_offset(position: usize) -> Option<u64> {
u64::try_from(position).ok()
}
fn as_u64(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
use super::*;
fn block(kind: u8, payload: &[u8], last: bool) -> Vec<u8> {
let mut out = vec![kind | if last { 0x80 } else { 0 }];
let length = u32::try_from(payload.len()).unwrap().to_be_bytes();
out.extend_from_slice(&length[1..4]);
out.extend_from_slice(payload);
out
}
fn streaminfo(md5: u8) -> Vec<u8> {
let mut payload = vec![0u8; STREAMINFO_LEN];
for byte in payload.iter_mut().skip(STREAMINFO_MD5_AT) {
*byte = md5;
}
payload
}
fn frames() -> Vec<u8> {
let mut out = vec![0xFF, 0xF8];
out.extend_from_slice(b"SYNTHETIC-AUDIO-FRAMES");
out
}
fn flac(blocks: &[Vec<u8>]) -> Vec<u8> {
let mut out = MAGIC.to_vec();
out.extend_from_slice(&block(STREAMINFO, &streaminfo(0xAB), blocks.is_empty()));
for (index, payload) in blocks.iter().enumerate() {
let mut copy = payload.clone();
if index == blocks.len() - 1 {
copy[0] |= 0x80;
}
out.extend_from_slice(©);
}
out.extend_from_slice(&frames());
out
}
fn le32(n: usize) -> [u8; 4] {
u32::try_from(n)
.expect("test lengths are small")
.to_le_bytes()
}
fn comment_block(vendor: &[u8], items: &[&[u8]]) -> Vec<u8> {
let mut payload = le32(vendor.len()).to_vec();
payload.extend_from_slice(vendor);
payload.extend_from_slice(&le32(items.len()));
for item in items {
payload.extend_from_slice(&le32(item.len()));
payload.extend_from_slice(item);
}
block(VORBIS_COMMENT, &payload, false)
}
fn strip_ok(data: &[u8]) -> Stripped {
FlacHandler
.strip(data, &StripOptions::default())
.expect("strip failed")
}
fn findings(data: &[u8]) -> Vec<Finding> {
FlacHandler
.inspect(data, &InspectOptions::names_only())
.expect("inspect failed")
.findings
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
#[test]
fn a_clean_file_strips_to_a_byte_identical_copy() {
let input = flac(&[]);
let stripped = strip_ok(&input);
assert!(stripped.report.removed.is_empty());
assert_eq!(stripped.bytes, input);
}
#[test]
fn the_audio_is_never_touched() {
let input = flac(&[comment_block(
b"SYNTHETIC-ENCODER",
&[b"ARTIST=SYNTHETIC-0001"],
)]);
let output = strip_ok(&input).bytes;
assert!(
contains(&output, b"SYNTHETIC-AUDIO-FRAMES"),
"the audio frames did not survive byte for byte"
);
}
#[test]
fn a_vorbis_comment_is_itemised_by_field_and_removed() {
let input = flac(&[comment_block(
b"reference libFLAC SYNTHETIC-VENDOR-0002",
&[
b"ARTIST=SYNTHETIC-ARTIST-0003",
b"DATE=2026-09-01",
b"MUSICBRAINZ_TRACKID=SYNTHETIC-0004",
],
)]);
let found = findings(&input);
let fields: Vec<&str> = found.iter().filter_map(|f| f.field.as_deref()).collect();
assert!(fields.contains(&"vendor"));
assert!(fields.contains(&"ARTIST"));
assert_eq!(
found
.iter()
.find(|f| f.field.as_deref() == Some("ARTIST"))
.unwrap()
.kind,
MetadataKind::PersonalIdentity
);
assert_eq!(
found
.iter()
.find(|f| f.field.as_deref() == Some("DATE"))
.unwrap()
.kind,
MetadataKind::Timestamp
);
assert_eq!(
found
.iter()
.find(|f| f.field.as_deref() == Some("MUSICBRAINZ_TRACKID"))
.unwrap()
.kind,
MetadataKind::DocumentIdentifier
);
assert!(
found.iter().all(|f| f.value.is_none()),
"values are withheld by default"
);
assert!(!contains(&strip_ok(&input).bytes, b"SYNTHETIC-ARTIST-0003"));
}
#[test]
fn a_comment_value_is_reported_only_when_the_caller_asks() {
let input = flac(&[comment_block(b"v", &[b"ARTIST=SYNTHETIC-ARTIST-0005"])]);
let report = FlacHandler
.inspect(&input, &InspectOptions::with_values())
.unwrap();
assert!(
report
.findings
.iter()
.any(|f| f.value == Some(MetadataValue::Text("SYNTHETIC-ARTIST-0005".to_owned())))
);
}
#[test]
fn cover_art_is_removed_and_ranked_as_a_picture() {
let mut payload = 3u32.to_be_bytes().to_vec(); payload.extend_from_slice(&(9u32).to_be_bytes());
payload.extend_from_slice(b"image/png");
payload.extend_from_slice(&(24u32).to_be_bytes());
payload.extend_from_slice(b"SYNTHETIC-DESCRIPTION-006");
let input = flac(&[block(PICTURE, &payload[0..payload.len()], false)]);
let found = findings(&input);
assert_eq!(found[0].kind, MetadataKind::Thumbnail);
assert_eq!(found[0].location, "PICTURE");
assert!(!contains(&strip_ok(&input).bytes, b"SYNTHETIC-DESCRIPTION"));
}
#[test]
fn a_cuesheet_is_removed_and_the_report_says_what_that_costs() {
let mut payload = vec![0u8; CUESHEET_CATALOGUE_LEN];
payload[0..9].copy_from_slice(b"012345678");
payload.extend_from_slice(&[0u8; 8]);
let input = flac(&[block(CUESHEET, &payload, false)]);
let stripped = strip_ok(&input);
assert_eq!(
stripped.report.removed[0].kind,
MetadataKind::DocumentIdentifier
);
assert_eq!(
stripped.report.removed[0].field.as_deref(),
Some("MediaCatalogNumber")
);
assert!(matches!(
stripped.report.notes.first(),
Some(Note::CapabilityRemoved { .. })
));
assert!(!contains(&stripped.bytes, b"012345678"));
}
#[test]
fn padding_keeps_its_size_and_loses_its_contents() {
let mut payload = vec![0u8; 64];
payload[8..31].copy_from_slice(b"SYNTHETIC-IN-PADDING-07");
let input = flac(&[block(PADDING, &payload, false)]);
let stripped = strip_ok(&input);
assert_eq!(stripped.report.removed[0].location, "PADDING");
assert!(!contains(&stripped.bytes, b"SYNTHETIC-IN-PADDING-07"));
assert_eq!(
stripped.bytes.len(),
input.len(),
"the padding block changed size"
);
}
#[test]
fn zero_padding_is_left_exactly_as_it_was() {
let input = flac(&[block(PADDING, &[0u8; 32], false)]);
let stripped = strip_ok(&input);
assert!(stripped.report.removed.is_empty());
assert_eq!(stripped.bytes, input);
}
#[test]
fn a_seek_table_survives_because_removal_moves_no_offset() {
let seek = vec![0x11u8; 18];
let input = flac(&[
comment_block(b"v", &[b"ARTIST=SYNTHETIC-0008"]),
block(SEEKTABLE, &seek, false),
]);
let output = strip_ok(&input).bytes;
assert!(contains(&output, &seek));
assert!(!contains(&output, b"SYNTHETIC-0008"));
}
#[test]
fn an_application_block_is_removed_and_named_by_its_registered_id() {
let mut payload = b"riff".to_vec();
payload.extend_from_slice(b"SYNTHETIC-APPLICATION-0009");
let input = flac(&[block(APPLICATION, &payload, false)]);
let found = findings(&input);
assert_eq!(found[0].field.as_deref(), Some("riff"));
assert!(!contains(
&strip_ok(&input).bytes,
b"SYNTHETIC-APPLICATION-0009"
));
}
#[test]
fn a_reserved_block_type_does_not_survive_by_being_unknown() {
let input = flac(&[block(42, b"SYNTHETIC-RESERVED-0010", false)]);
let found = findings(&input);
assert_eq!(found[0].location, "Metadata block type 42");
assert!(!contains(
&strip_ok(&input).bytes,
b"SYNTHETIC-RESERVED-0010"
));
}
#[test]
fn the_last_block_flag_moves_to_whatever_block_ends_up_last() {
let input = flac(&[comment_block(b"v", &[b"ARTIST=SYNTHETIC-0011"])]);
let output = strip_ok(&input).bytes;
assert_eq!(
output[4] & 0x80,
0x80,
"STREAMINFO was not marked as the last metadata block"
);
assert_eq!(output[4] & 0x7F, STREAMINFO);
}
#[test]
fn the_audio_md5_is_kept_and_declared() {
let stripped = strip_ok(&flac(&[]));
assert_eq!(
stripped.report.retained[0].reason,
RetentionReason::DerivedFromPayload
);
}
#[test]
fn a_file_whose_md5_is_already_absent_declares_nothing() {
let mut input = MAGIC.to_vec();
input.extend_from_slice(&block(STREAMINFO, &[0u8; STREAMINFO_LEN], true));
input.extend_from_slice(&frames());
assert!(strip_ok(&input).report.retained.is_empty());
}
#[test]
fn every_file_says_the_audio_was_not_examined() {
let notes = FlacHandler
.inspect(&flac(&[]), &InspectOptions::names_only())
.unwrap()
.notes;
assert!(matches!(
notes.last(),
Some(Note::OutOfScopeContent { location }) if location.starts_with("audio frames")
));
}
#[test]
fn stripping_twice_changes_nothing() {
let input = flac(&[
comment_block(b"v", &[b"ARTIST=SYNTHETIC-0012"]),
block(PADDING, &[0x7Fu8; 16], false),
block(APPLICATION, b"riffSYNTHETIC-0013", false),
]);
let once = strip_ok(&input).bytes;
let twice = strip_ok(&once).bytes;
assert_eq!(once, twice, "strip is not idempotent");
}
fn id3v2(id: &[u8], text: &[u8]) -> Vec<u8> {
let syncsafe = |n: usize| {
[
u8::try_from((n >> 21) & 0x7F).unwrap(),
u8::try_from((n >> 14) & 0x7F).unwrap(),
u8::try_from((n >> 7) & 0x7F).unwrap(),
u8::try_from(n & 0x7F).unwrap(),
]
};
let mut payload = vec![0x03u8];
payload.extend_from_slice(text);
let mut body = id.to_vec();
body.extend_from_slice(&syncsafe(payload.len()));
body.extend_from_slice(&[0, 0]);
body.extend_from_slice(&payload);
let mut out = b"ID3\x04\x00\x00".to_vec();
out.extend_from_slice(&syncsafe(body.len()));
out.extend_from_slice(&body);
out
}
#[test]
fn a_prepended_id3v2_tag_is_read_and_removed_rather_than_refused() {
let clean = flac(&[]);
let mut input = id3v2(b"TPE1", b"SYNTHETIC-ARTIST-0101");
input.extend_from_slice(&clean);
let result = strip_ok(&input);
assert_eq!(result.bytes, clean, "the FLAC behind the tag moved");
assert!(!contains(&result.bytes, b"SYNTHETIC-ARTIST-0101"));
assert!(
result
.report
.removed
.iter()
.any(|f| f.location == "ID3v2.4" && f.field.as_deref() == Some("TPE1"))
);
}
#[test]
fn tags_appended_past_the_last_frame_are_removed_too() {
let clean = flac(&[]);
let mut input = clean.clone();
let mut v1 = vec![0u8; 128];
v1[0..3].copy_from_slice(b"TAG");
v1[33..54].copy_from_slice(b"SYNTHETIC-ARTIST-0102");
input.extend_from_slice(&v1);
let result = strip_ok(&input);
assert_eq!(result.bytes, clean);
assert!(
result
.report
.removed
.iter()
.any(|f| f.location == "ID3v1" && f.field.as_deref() == Some("Artist"))
);
}
#[test]
fn a_tag_at_each_end_leaves_the_stream_between_them_untouched() {
let clean = flac(&[comment_block(b"SYNTHETIC-VENDOR-0103", &[])]);
let mut input = id3v2(b"TIT2", b"SYNTHETIC-TITLE-0104");
input.extend_from_slice(&clean);
input.extend_from_slice(b"LYRICSBEGINSYNTHETIC-LYRIC-0105");
input.extend_from_slice(b"LYRICSEND");
let result = strip_ok(&input);
assert_eq!(result.bytes, strip_ok(&clean).bytes);
for secret in [
&b"SYNTHETIC-TITLE-0104"[..],
&b"SYNTHETIC-LYRIC-0105"[..],
&b"SYNTHETIC-VENDOR-0103"[..],
] {
assert!(!contains(&result.bytes, secret));
}
}
#[test]
fn a_file_that_is_not_flac_is_refused() {
assert!(matches!(
FlacHandler.inspect(b"fLaD\x00\x00\x00\x22", &InspectOptions::names_only()),
Err(StryptError::Malformed {
detail: MalformedDetail::MissingMarker,
..
})
));
}
#[test]
fn a_first_block_that_is_not_streaminfo_is_refused() {
let mut input = MAGIC.to_vec();
input.extend_from_slice(&block(PADDING, &[0u8; 4], true));
input.extend_from_slice(&frames());
assert!(matches!(
FlacHandler.inspect(&input, &InspectOptions::names_only()),
Err(StryptError::Malformed {
detail: MalformedDetail::MissingMarker,
..
})
));
}
#[test]
fn the_forbidden_block_type_is_refused() {
let mut input = MAGIC.to_vec();
input.extend_from_slice(&block(STREAMINFO, &streaminfo(1), false));
input.extend_from_slice(&block(FORBIDDEN, &[0u8; 2], true));
input.extend_from_slice(&frames());
assert!(matches!(
FlacHandler.inspect(&input, &InspectOptions::names_only()),
Err(StryptError::Malformed {
detail: MalformedDetail::UnexpectedMarker,
..
})
));
}
#[test]
fn a_block_length_running_past_the_end_of_the_file_is_refused() {
let mut input = flac(&[]);
input[5] = 0xFF;
assert!(matches!(
FlacHandler.inspect(&input, &InspectOptions::names_only()),
Err(StryptError::Malformed {
detail: MalformedDetail::LengthOutOfRange,
..
})
));
}
#[test]
fn a_file_with_no_frame_sync_after_its_blocks_is_refused() {
let mut input = MAGIC.to_vec();
input.extend_from_slice(&block(STREAMINFO, &streaminfo(1), true));
input.extend_from_slice(b"NOT-A-FRAME");
assert!(matches!(
FlacHandler.strip(&input, &StripOptions::default()),
Err(StryptError::Malformed {
detail: MalformedDetail::MissingMarker,
..
})
));
}
#[test]
fn a_second_streaminfo_is_refused() {
let mut input = MAGIC.to_vec();
input.extend_from_slice(&block(STREAMINFO, &streaminfo(1), false));
input.extend_from_slice(&block(STREAMINFO, &streaminfo(2), true));
input.extend_from_slice(&frames());
assert!(matches!(
FlacHandler.inspect(&input, &InspectOptions::names_only()),
Err(StryptError::Malformed {
detail: MalformedDetail::UnexpectedMarker,
..
})
));
}
#[test]
fn a_streaminfo_of_the_wrong_length_is_refused() {
let mut input = MAGIC.to_vec();
input.extend_from_slice(&block(STREAMINFO, &[0u8; 20], true));
input.extend_from_slice(&frames());
assert!(matches!(
FlacHandler.inspect(&input, &InspectOptions::names_only()),
Err(StryptError::Malformed {
detail: MalformedDetail::LengthOutOfRange,
..
})
));
}
#[test]
fn a_comment_block_whose_lengths_do_not_add_up_is_reported_and_deleted() {
let mut payload = 0xFFFF_FFFFu32.to_le_bytes().to_vec();
payload.extend_from_slice(b"SYNTHETIC-UNREADABLE-0014");
let input = flac(&[block(VORBIS_COMMENT, &payload, false)]);
let stripped = strip_ok(&input);
assert_eq!(stripped.report.removed[0].location, "VORBIS_COMMENT");
assert!(!contains(&stripped.bytes, b"SYNTHETIC-UNREADABLE-0014"));
}
#[test]
fn a_block_count_beyond_the_limit_is_refused() {
let blocks: Vec<Vec<u8>> = (0..64).map(|_| block(PADDING, &[0u8; 1], false)).collect();
let input = flac(&blocks);
let options = StripOptions {
limits: ParseLimits {
max_items: 8,
..ParseLimits::default()
},
..StripOptions::default()
};
assert!(matches!(
FlacHandler.strip(&input, &options),
Err(StryptError::LimitExceeded { .. })
));
}
#[test]
fn truncation_at_every_length_is_refused_or_survived_but_never_panics() {
let input = flac(&[
comment_block(b"vendor", &[b"ARTIST=SYNTHETIC-0015"]),
block(PICTURE, b"\0\0\0\x03\0\0\0\x09image/pngSYNTHETIC", false),
block(PADDING, &[0u8; 8], false),
]);
for n in 0..=input.len() {
let prefix = &input[0..n];
let _ = FlacHandler.inspect(prefix, &InspectOptions::names_only());
let _ = FlacHandler.strip(prefix, &StripOptions::default());
}
}
}