1use crate::bytes::Reader;
47use crate::detect::Format;
48use crate::error::{MalformedDetail, ResourceLimit, Result, StryptError};
49use crate::formats::tags::{self, TagError};
50use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, vorbis, xmp};
51use crate::report::{
52 Finding, InspectOptions, MetadataKind, MetadataReport, MetadataValue, Note, Retained,
53 RetentionReason, StripReport,
54};
55
56#[derive(Debug, Clone, Copy, Default)]
58pub struct FlacHandler;
59
60impl MetadataHandler for FlacHandler {
61 fn name(&self) -> &'static str {
62 Format::Flac.id()
63 }
64
65 fn format(&self) -> Format {
66 Format::Flac
67 }
68
69 fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
70 let processed = process(input, options, &ParseLimits::default())?;
74 Ok(MetadataReport {
75 format: Format::Flac,
76 findings: processed.findings,
77 notes: processed.notes,
78 })
79 }
80
81 fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
82 let processed = process(input, &options.inspect, &options.limits)?;
83 Ok(Stripped {
84 report: StripReport {
85 format: Format::Flac,
86 removed: processed.findings,
87 retained: processed.retained,
88 notes: processed.notes,
89 input_bytes: as_u64(input.len()),
90 output_bytes: as_u64(processed.output.len()),
91 },
92 bytes: processed.output,
93 })
94 }
95}
96
97const MAGIC: &[u8; 4] = b"fLaC";
99
100const STREAMINFO: u8 = 0;
102const PADDING: u8 = 1;
103const APPLICATION: u8 = 2;
104const SEEKTABLE: u8 = 3;
105const VORBIS_COMMENT: u8 = 4;
106const CUESHEET: u8 = 5;
107const PICTURE: u8 = 6;
108const FORBIDDEN: u8 = 127;
110
111const STREAMINFO_LEN: usize = 34;
113const STREAMINFO_MD5_AT: usize = 18;
115
116const APPLICATION_ID_LEN: usize = 4;
118const CUESHEET_CATALOGUE_LEN: usize = 128;
120
121struct Block<'a> {
123 kind: u8,
124 payload: &'a [u8],
125}
126
127enum Payload<'a> {
130 Raw(&'a [u8]),
131 Zeros(usize),
132}
133
134impl Payload<'_> {
135 const fn len(&self) -> usize {
136 match self {
137 Self::Raw(bytes) => bytes.len(),
138 Self::Zeros(n) => *n,
139 }
140 }
141}
142
143struct Processed {
145 findings: Vec<Finding>,
146 retained: Vec<Retained>,
147 notes: Vec<Note>,
148 output: Vec<u8>,
149}
150
151fn walk<'a>(input: &'a [u8], limits: &ParseLimits) -> Result<(Vec<Block<'a>>, &'a [u8])> {
157 let mut r = Reader::new(input);
158 if r.take(MAGIC.len()) != Some(MAGIC.as_slice()) {
159 return Err(malformed(MalformedDetail::MissingMarker, Some(0)));
160 }
161
162 let mut blocks: Vec<Block<'a>> = Vec::new();
163 let mut budget = limits.max_items;
164 loop {
165 let at = r.position();
166 spend(&mut budget)?;
167 let header = r
168 .take(4)
169 .ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(at)))?;
170 let first = header.first().copied().unwrap_or_default();
171 let kind = first & 0x7F;
172 let last = first & 0x80 != 0;
173 let length = block_length(header)
174 .ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(at)))?;
175
176 if kind == FORBIDDEN {
177 return Err(malformed(MalformedDetail::UnexpectedMarker, as_offset(at)));
180 }
181 if blocks.is_empty() && kind != STREAMINFO {
182 return Err(malformed(MalformedDetail::MissingMarker, as_offset(at)));
183 }
184 if !blocks.is_empty() && kind == STREAMINFO {
185 return Err(malformed(MalformedDetail::UnexpectedMarker, as_offset(at)));
186 }
187
188 let payload = r
189 .take(length)
190 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange, as_offset(at)))?;
191 if kind == STREAMINFO && payload.len() != STREAMINFO_LEN {
192 return Err(malformed(MalformedDetail::LengthOutOfRange, as_offset(at)));
193 }
194 blocks.push(Block { kind, payload });
195 if last {
196 break;
197 }
198 }
199
200 let audio = r.take_rest();
201 let sync = matches!((audio.first(), audio.get(1)), (Some(0xFF), Some(second)) if second & 0xFE == 0xF8);
205 if !sync {
206 return Err(malformed(
207 MalformedDetail::MissingMarker,
208 as_offset(input.len().saturating_sub(audio.len())),
209 ));
210 }
211 Ok((blocks, audio))
212}
213
214fn block_length(header: &[u8]) -> Option<usize> {
216 match header.get(1..4)? {
217 [high, middle, low] => usize::try_from(u32::from_be_bytes([0, *high, *middle, *low])).ok(),
218 _ => None,
219 }
220}
221
222fn spend(budget: &mut u32) -> Result<()> {
224 if *budget == 0 {
225 return Err(StryptError::LimitExceeded {
226 format: Format::Flac,
227 limit: ResourceLimit::ItemCount,
228 });
229 }
230 *budget = budget.saturating_sub(1);
231 Ok(())
232}
233
234fn process(input: &[u8], options: &InspectOptions, limits: &ParseLimits) -> Result<Processed> {
236 let (head_tags, start) = tags::head(input).map_err(convert)?;
239 let (tail_tags, end) = tags::tail(input, start).map_err(convert)?;
240 let body = input
241 .get(start..end)
242 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange, as_offset(start)))?;
243
244 let (blocks, audio) = walk(body, limits)?;
245 let mut out = Processed {
246 findings: Vec::new(),
247 retained: Vec::new(),
248 notes: Vec::new(),
249 output: Vec::with_capacity(body.len()),
250 };
251 for tag in head_tags.iter().chain(tail_tags.iter()) {
252 out.findings.extend(tags::findings(tag, options));
253 }
254
255 let mut kept: Vec<(u8, Payload<'_>)> = Vec::new();
256 for block in &blocks {
257 let size = as_u64(block.payload.len());
258 match block.kind {
259 STREAMINFO => {
260 if !md5_is_absent(block.payload) {
262 out.retained.push(Retained {
263 location: "STREAMINFO (MD5 of the unencoded audio)".to_owned(),
264 reason: RetentionReason::DerivedFromPayload,
265 });
266 }
267 kept.push((block.kind, Payload::Raw(block.payload)));
268 }
269 SEEKTABLE => {
270 kept.push((block.kind, Payload::Raw(block.payload)));
273 }
274 PADDING => {
275 if block.payload.iter().any(|byte| *byte != 0) {
276 out.findings.push(
280 Finding::new(MetadataKind::Other, "PADDING", size)
281 .with_field("Padding")
282 .with_value(options, || MetadataValue::Opaque { bytes: size }),
283 );
284 }
285 kept.push((block.kind, Payload::Zeros(block.payload.len())));
286 }
287 APPLICATION => out.findings.push(application(block.payload, size, options)),
288 VORBIS_COMMENT => {
289 vorbis::comments(
290 block.payload,
291 "VORBIS_COMMENT",
292 size,
293 options,
294 &mut out.findings,
295 );
296 }
297 CUESHEET => {
298 out.findings.push(cuesheet(block.payload, size));
299 out.notes.push(Note::CapabilityRemoved {
300 location: "CUESHEET".to_owned(),
301 capability: "be split into the tracks of the disc it was ripped from"
302 .to_owned(),
303 });
304 }
305 PICTURE => out.findings.push(picture(block.payload, size, options)),
306 other => out.findings.push(
307 Finding::new(
311 MetadataKind::Other,
312 format!("Metadata block type {other}"),
313 size,
314 ),
315 ),
316 }
317 }
318
319 out.output.extend_from_slice(MAGIC);
320 let last_index = kept.len().saturating_sub(1);
321 for (index, (kind, payload)) in kept.iter().enumerate() {
322 let last = if index == last_index { 0x80 } else { 0x00 };
323 out.output.push(kind | last);
324 let length = u32::try_from(payload.len())
327 .unwrap_or(u32::MAX)
328 .to_be_bytes();
329 out.output
330 .extend_from_slice(length.get(1..4).unwrap_or_default());
331 match payload {
332 Payload::Raw(bytes) => out.output.extend_from_slice(bytes),
333 Payload::Zeros(n) => out.output.resize(out.output.len().saturating_add(*n), 0),
334 }
335 }
336 out.output.extend_from_slice(audio);
337
338 out.notes.push(Note::OutOfScopeContent {
342 location: "audio frames, which are copied without being decoded".to_owned(),
343 });
344 Ok(out)
345}
346
347fn convert(error: TagError) -> StryptError {
350 match error {
351 TagError::Malformed { detail, offset } => malformed(detail, as_offset(offset)),
352 TagError::Limit(limit) => StryptError::LimitExceeded {
353 format: Format::Flac,
354 limit,
355 },
356 }
357}
358
359fn md5_is_absent(payload: &[u8]) -> bool {
361 payload
362 .get(STREAMINFO_MD5_AT..)
363 .is_none_or(|md5| md5.iter().all(|byte| *byte == 0))
364}
365
366fn application(payload: &[u8], size: u64, options: &InspectOptions) -> Finding {
369 let id = payload.get(0..APPLICATION_ID_LEN).unwrap_or(payload);
370 Finding::new(MetadataKind::SoftwareFingerprint, "APPLICATION", size)
371 .with_field(xmp::name_of(id))
372 .with_value(options, || MetadataValue::Opaque { bytes: size })
373}
374
375fn cuesheet(payload: &[u8], size: u64) -> Finding {
378 let catalogue = payload
379 .get(0..CUESHEET_CATALOGUE_LEN)
380 .unwrap_or(payload)
381 .iter()
382 .any(|byte| *byte != 0);
383 let finding = Finding::new(MetadataKind::DocumentIdentifier, "CUESHEET", size);
384 if catalogue {
385 finding.with_field("MediaCatalogNumber")
386 } else {
387 finding
388 }
389}
390
391fn picture(payload: &[u8], size: u64, options: &InspectOptions) -> Finding {
394 let mut r = Reader::new(payload);
395 let kind = r.u32_be().unwrap_or_default();
396 let media_type = r
397 .u32_be()
398 .and_then(|n| usize::try_from(n).ok())
399 .and_then(|n| r.take(n))
400 .unwrap_or_default();
401 let description = r
402 .u32_be()
403 .and_then(|n| usize::try_from(n).ok())
404 .and_then(|n| r.take(n))
405 .unwrap_or_default();
406
407 let field = if media_type.is_empty() {
408 format!("PictureType {kind}")
409 } else {
410 format!("PictureType {kind} ({})", xmp::name_of(media_type))
411 };
412 Finding::new(MetadataKind::Thumbnail, "PICTURE", size)
413 .with_field(field)
414 .with_value(options, || {
415 if description.is_empty() {
416 MetadataValue::Opaque { bytes: size }
417 } else {
418 MetadataValue::Text(xmp::name_of(description))
419 }
420 })
421}
422
423fn malformed(detail: MalformedDetail, offset: Option<u64>) -> StryptError {
425 StryptError::Malformed {
426 format: Format::Flac,
427 offset,
428 detail,
429 }
430}
431
432fn as_offset(position: usize) -> Option<u64> {
434 u64::try_from(position).ok()
435}
436
437fn as_u64(value: usize) -> u64 {
439 u64::try_from(value).unwrap_or(u64::MAX)
440}
441
442#[cfg(test)]
443mod tests {
444 #![allow(
447 clippy::unwrap_used,
448 clippy::expect_used,
449 clippy::indexing_slicing,
450 clippy::arithmetic_side_effects
451 )]
452
453 use super::*;
454
455 fn block(kind: u8, payload: &[u8], last: bool) -> Vec<u8> {
457 let mut out = vec![kind | if last { 0x80 } else { 0 }];
458 let length = u32::try_from(payload.len()).unwrap().to_be_bytes();
459 out.extend_from_slice(&length[1..4]);
460 out.extend_from_slice(payload);
461 out
462 }
463
464 fn streaminfo(md5: u8) -> Vec<u8> {
466 let mut payload = vec![0u8; STREAMINFO_LEN];
467 for byte in payload.iter_mut().skip(STREAMINFO_MD5_AT) {
468 *byte = md5;
469 }
470 payload
471 }
472
473 fn frames() -> Vec<u8> {
475 let mut out = vec![0xFF, 0xF8];
476 out.extend_from_slice(b"SYNTHETIC-AUDIO-FRAMES");
477 out
478 }
479
480 fn flac(blocks: &[Vec<u8>]) -> Vec<u8> {
482 let mut out = MAGIC.to_vec();
483 out.extend_from_slice(&block(STREAMINFO, &streaminfo(0xAB), blocks.is_empty()));
484 for (index, payload) in blocks.iter().enumerate() {
485 let mut copy = payload.clone();
486 if index == blocks.len() - 1 {
487 copy[0] |= 0x80;
488 }
489 out.extend_from_slice(©);
490 }
491 out.extend_from_slice(&frames());
492 out
493 }
494
495 fn le32(n: usize) -> [u8; 4] {
496 u32::try_from(n)
497 .expect("test lengths are small")
498 .to_le_bytes()
499 }
500
501 fn comment_block(vendor: &[u8], items: &[&[u8]]) -> Vec<u8> {
502 let mut payload = le32(vendor.len()).to_vec();
503 payload.extend_from_slice(vendor);
504 payload.extend_from_slice(&le32(items.len()));
505 for item in items {
506 payload.extend_from_slice(&le32(item.len()));
507 payload.extend_from_slice(item);
508 }
509 block(VORBIS_COMMENT, &payload, false)
510 }
511
512 fn strip_ok(data: &[u8]) -> Stripped {
513 FlacHandler
514 .strip(data, &StripOptions::default())
515 .expect("strip failed")
516 }
517
518 fn findings(data: &[u8]) -> Vec<Finding> {
519 FlacHandler
520 .inspect(data, &InspectOptions::names_only())
521 .expect("inspect failed")
522 .findings
523 }
524
525 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
526 haystack.windows(needle.len()).any(|w| w == needle)
527 }
528
529 #[test]
530 fn a_clean_file_strips_to_a_byte_identical_copy() {
531 let input = flac(&[]);
534 let stripped = strip_ok(&input);
535 assert!(stripped.report.removed.is_empty());
536 assert_eq!(stripped.bytes, input);
537 }
538
539 #[test]
540 fn the_audio_is_never_touched() {
541 let input = flac(&[comment_block(
542 b"SYNTHETIC-ENCODER",
543 &[b"ARTIST=SYNTHETIC-0001"],
544 )]);
545 let output = strip_ok(&input).bytes;
546 assert!(
547 contains(&output, b"SYNTHETIC-AUDIO-FRAMES"),
548 "the audio frames did not survive byte for byte"
549 );
550 }
551
552 #[test]
553 fn a_vorbis_comment_is_itemised_by_field_and_removed() {
554 let input = flac(&[comment_block(
555 b"reference libFLAC SYNTHETIC-VENDOR-0002",
556 &[
557 b"ARTIST=SYNTHETIC-ARTIST-0003",
558 b"DATE=2026-09-01",
559 b"MUSICBRAINZ_TRACKID=SYNTHETIC-0004",
560 ],
561 )]);
562 let found = findings(&input);
563 let fields: Vec<&str> = found.iter().filter_map(|f| f.field.as_deref()).collect();
564 assert!(fields.contains(&"vendor"));
565 assert!(fields.contains(&"ARTIST"));
566 assert_eq!(
567 found
568 .iter()
569 .find(|f| f.field.as_deref() == Some("ARTIST"))
570 .unwrap()
571 .kind,
572 MetadataKind::PersonalIdentity
573 );
574 assert_eq!(
575 found
576 .iter()
577 .find(|f| f.field.as_deref() == Some("DATE"))
578 .unwrap()
579 .kind,
580 MetadataKind::Timestamp
581 );
582 assert_eq!(
583 found
584 .iter()
585 .find(|f| f.field.as_deref() == Some("MUSICBRAINZ_TRACKID"))
586 .unwrap()
587 .kind,
588 MetadataKind::DocumentIdentifier
589 );
590 assert!(
591 found.iter().all(|f| f.value.is_none()),
592 "values are withheld by default"
593 );
594 assert!(!contains(&strip_ok(&input).bytes, b"SYNTHETIC-ARTIST-0003"));
595 }
596
597 #[test]
598 fn a_comment_value_is_reported_only_when_the_caller_asks() {
599 let input = flac(&[comment_block(b"v", &[b"ARTIST=SYNTHETIC-ARTIST-0005"])]);
600 let report = FlacHandler
601 .inspect(&input, &InspectOptions::with_values())
602 .unwrap();
603 assert!(
604 report
605 .findings
606 .iter()
607 .any(|f| f.value == Some(MetadataValue::Text("SYNTHETIC-ARTIST-0005".to_owned())))
608 );
609 }
610
611 #[test]
612 fn cover_art_is_removed_and_ranked_as_a_picture() {
613 let mut payload = 3u32.to_be_bytes().to_vec(); payload.extend_from_slice(&(9u32).to_be_bytes());
617 payload.extend_from_slice(b"image/png");
618 payload.extend_from_slice(&(24u32).to_be_bytes());
619 payload.extend_from_slice(b"SYNTHETIC-DESCRIPTION-006");
620 let input = flac(&[block(PICTURE, &payload[0..payload.len()], false)]);
621
622 let found = findings(&input);
623 assert_eq!(found[0].kind, MetadataKind::Thumbnail);
624 assert_eq!(found[0].location, "PICTURE");
625 assert!(!contains(&strip_ok(&input).bytes, b"SYNTHETIC-DESCRIPTION"));
626 }
627
628 #[test]
629 fn a_cuesheet_is_removed_and_the_report_says_what_that_costs() {
630 let mut payload = vec![0u8; CUESHEET_CATALOGUE_LEN];
631 payload[0..9].copy_from_slice(b"012345678");
632 payload.extend_from_slice(&[0u8; 8]);
633 let input = flac(&[block(CUESHEET, &payload, false)]);
634
635 let stripped = strip_ok(&input);
636 assert_eq!(
637 stripped.report.removed[0].kind,
638 MetadataKind::DocumentIdentifier
639 );
640 assert_eq!(
641 stripped.report.removed[0].field.as_deref(),
642 Some("MediaCatalogNumber")
643 );
644 assert!(matches!(
645 stripped.report.notes.first(),
646 Some(Note::CapabilityRemoved { .. })
647 ));
648 assert!(!contains(&stripped.bytes, b"012345678"));
649 }
650
651 #[test]
652 fn padding_keeps_its_size_and_loses_its_contents() {
653 let mut payload = vec![0u8; 64];
656 payload[8..31].copy_from_slice(b"SYNTHETIC-IN-PADDING-07");
657 let input = flac(&[block(PADDING, &payload, false)]);
658
659 let stripped = strip_ok(&input);
660 assert_eq!(stripped.report.removed[0].location, "PADDING");
661 assert!(!contains(&stripped.bytes, b"SYNTHETIC-IN-PADDING-07"));
662 assert_eq!(
663 stripped.bytes.len(),
664 input.len(),
665 "the padding block changed size"
666 );
667 }
668
669 #[test]
670 fn zero_padding_is_left_exactly_as_it_was() {
671 let input = flac(&[block(PADDING, &[0u8; 32], false)]);
672 let stripped = strip_ok(&input);
673 assert!(stripped.report.removed.is_empty());
674 assert_eq!(stripped.bytes, input);
675 }
676
677 #[test]
678 fn a_seek_table_survives_because_removal_moves_no_offset() {
679 let seek = vec![0x11u8; 18];
682 let input = flac(&[
683 comment_block(b"v", &[b"ARTIST=SYNTHETIC-0008"]),
684 block(SEEKTABLE, &seek, false),
685 ]);
686 let output = strip_ok(&input).bytes;
687 assert!(contains(&output, &seek));
688 assert!(!contains(&output, b"SYNTHETIC-0008"));
689 }
690
691 #[test]
692 fn an_application_block_is_removed_and_named_by_its_registered_id() {
693 let mut payload = b"riff".to_vec();
694 payload.extend_from_slice(b"SYNTHETIC-APPLICATION-0009");
695 let input = flac(&[block(APPLICATION, &payload, false)]);
696 let found = findings(&input);
697 assert_eq!(found[0].field.as_deref(), Some("riff"));
698 assert!(!contains(
699 &strip_ok(&input).bytes,
700 b"SYNTHETIC-APPLICATION-0009"
701 ));
702 }
703
704 #[test]
705 fn a_reserved_block_type_does_not_survive_by_being_unknown() {
706 let input = flac(&[block(42, b"SYNTHETIC-RESERVED-0010", false)]);
707 let found = findings(&input);
708 assert_eq!(found[0].location, "Metadata block type 42");
709 assert!(!contains(
710 &strip_ok(&input).bytes,
711 b"SYNTHETIC-RESERVED-0010"
712 ));
713 }
714
715 #[test]
716 fn the_last_block_flag_moves_to_whatever_block_ends_up_last() {
717 let input = flac(&[comment_block(b"v", &[b"ARTIST=SYNTHETIC-0011"])]);
720 let output = strip_ok(&input).bytes;
721 assert_eq!(
722 output[4] & 0x80,
723 0x80,
724 "STREAMINFO was not marked as the last metadata block"
725 );
726 assert_eq!(output[4] & 0x7F, STREAMINFO);
727 }
728
729 #[test]
730 fn the_audio_md5_is_kept_and_declared() {
731 let stripped = strip_ok(&flac(&[]));
735 assert_eq!(
736 stripped.report.retained[0].reason,
737 RetentionReason::DerivedFromPayload
738 );
739 }
740
741 #[test]
742 fn a_file_whose_md5_is_already_absent_declares_nothing() {
743 let mut input = MAGIC.to_vec();
744 input.extend_from_slice(&block(STREAMINFO, &[0u8; STREAMINFO_LEN], true));
745 input.extend_from_slice(&frames());
746 assert!(strip_ok(&input).report.retained.is_empty());
747 }
748
749 #[test]
750 fn every_file_says_the_audio_was_not_examined() {
751 let notes = FlacHandler
752 .inspect(&flac(&[]), &InspectOptions::names_only())
753 .unwrap()
754 .notes;
755 assert!(matches!(
756 notes.last(),
757 Some(Note::OutOfScopeContent { location }) if location.starts_with("audio frames")
758 ));
759 }
760
761 #[test]
762 fn stripping_twice_changes_nothing() {
763 let input = flac(&[
764 comment_block(b"v", &[b"ARTIST=SYNTHETIC-0012"]),
765 block(PADDING, &[0x7Fu8; 16], false),
766 block(APPLICATION, b"riffSYNTHETIC-0013", false),
767 ]);
768 let once = strip_ok(&input).bytes;
769 let twice = strip_ok(&once).bytes;
770 assert_eq!(once, twice, "strip is not idempotent");
771 }
772
773 fn id3v2(id: &[u8], text: &[u8]) -> Vec<u8> {
775 let syncsafe = |n: usize| {
776 [
777 u8::try_from((n >> 21) & 0x7F).unwrap(),
778 u8::try_from((n >> 14) & 0x7F).unwrap(),
779 u8::try_from((n >> 7) & 0x7F).unwrap(),
780 u8::try_from(n & 0x7F).unwrap(),
781 ]
782 };
783 let mut payload = vec![0x03u8];
784 payload.extend_from_slice(text);
785 let mut body = id.to_vec();
786 body.extend_from_slice(&syncsafe(payload.len()));
787 body.extend_from_slice(&[0, 0]);
788 body.extend_from_slice(&payload);
789 let mut out = b"ID3\x04\x00\x00".to_vec();
790 out.extend_from_slice(&syncsafe(body.len()));
791 out.extend_from_slice(&body);
792 out
793 }
794
795 #[test]
796 fn a_prepended_id3v2_tag_is_read_and_removed_rather_than_refused() {
797 let clean = flac(&[]);
800 let mut input = id3v2(b"TPE1", b"SYNTHETIC-ARTIST-0101");
801 input.extend_from_slice(&clean);
802 let result = strip_ok(&input);
803 assert_eq!(result.bytes, clean, "the FLAC behind the tag moved");
804 assert!(!contains(&result.bytes, b"SYNTHETIC-ARTIST-0101"));
805 assert!(
806 result
807 .report
808 .removed
809 .iter()
810 .any(|f| f.location == "ID3v2.4" && f.field.as_deref() == Some("TPE1"))
811 );
812 }
813
814 #[test]
815 fn tags_appended_past_the_last_frame_are_removed_too() {
816 let clean = flac(&[]);
817 let mut input = clean.clone();
818 let mut v1 = vec![0u8; 128];
819 v1[0..3].copy_from_slice(b"TAG");
820 v1[33..54].copy_from_slice(b"SYNTHETIC-ARTIST-0102");
821 input.extend_from_slice(&v1);
822 let result = strip_ok(&input);
823 assert_eq!(result.bytes, clean);
824 assert!(
825 result
826 .report
827 .removed
828 .iter()
829 .any(|f| f.location == "ID3v1" && f.field.as_deref() == Some("Artist"))
830 );
831 }
832
833 #[test]
834 fn a_tag_at_each_end_leaves_the_stream_between_them_untouched() {
835 let clean = flac(&[comment_block(b"SYNTHETIC-VENDOR-0103", &[])]);
836 let mut input = id3v2(b"TIT2", b"SYNTHETIC-TITLE-0104");
837 input.extend_from_slice(&clean);
838 input.extend_from_slice(b"LYRICSBEGINSYNTHETIC-LYRIC-0105");
839 input.extend_from_slice(b"LYRICSEND");
840 let result = strip_ok(&input);
841 assert_eq!(result.bytes, strip_ok(&clean).bytes);
842 for secret in [
843 &b"SYNTHETIC-TITLE-0104"[..],
844 &b"SYNTHETIC-LYRIC-0105"[..],
845 &b"SYNTHETIC-VENDOR-0103"[..],
846 ] {
847 assert!(!contains(&result.bytes, secret));
848 }
849 }
850
851 #[test]
852 fn a_file_that_is_not_flac_is_refused() {
853 assert!(matches!(
854 FlacHandler.inspect(b"fLaD\x00\x00\x00\x22", &InspectOptions::names_only()),
855 Err(StryptError::Malformed {
856 detail: MalformedDetail::MissingMarker,
857 ..
858 })
859 ));
860 }
861
862 #[test]
863 fn a_first_block_that_is_not_streaminfo_is_refused() {
864 let mut input = MAGIC.to_vec();
865 input.extend_from_slice(&block(PADDING, &[0u8; 4], true));
866 input.extend_from_slice(&frames());
867 assert!(matches!(
868 FlacHandler.inspect(&input, &InspectOptions::names_only()),
869 Err(StryptError::Malformed {
870 detail: MalformedDetail::MissingMarker,
871 ..
872 })
873 ));
874 }
875
876 #[test]
877 fn the_forbidden_block_type_is_refused() {
878 let mut input = MAGIC.to_vec();
879 input.extend_from_slice(&block(STREAMINFO, &streaminfo(1), false));
880 input.extend_from_slice(&block(FORBIDDEN, &[0u8; 2], true));
881 input.extend_from_slice(&frames());
882 assert!(matches!(
883 FlacHandler.inspect(&input, &InspectOptions::names_only()),
884 Err(StryptError::Malformed {
885 detail: MalformedDetail::UnexpectedMarker,
886 ..
887 })
888 ));
889 }
890
891 #[test]
892 fn a_block_length_running_past_the_end_of_the_file_is_refused() {
893 let mut input = flac(&[]);
894 input[5] = 0xFF;
895 assert!(matches!(
896 FlacHandler.inspect(&input, &InspectOptions::names_only()),
897 Err(StryptError::Malformed {
898 detail: MalformedDetail::LengthOutOfRange,
899 ..
900 })
901 ));
902 }
903
904 #[test]
905 fn a_file_with_no_frame_sync_after_its_blocks_is_refused() {
906 let mut input = MAGIC.to_vec();
909 input.extend_from_slice(&block(STREAMINFO, &streaminfo(1), true));
910 input.extend_from_slice(b"NOT-A-FRAME");
911 assert!(matches!(
912 FlacHandler.strip(&input, &StripOptions::default()),
913 Err(StryptError::Malformed {
914 detail: MalformedDetail::MissingMarker,
915 ..
916 })
917 ));
918 }
919
920 #[test]
921 fn a_second_streaminfo_is_refused() {
922 let mut input = MAGIC.to_vec();
923 input.extend_from_slice(&block(STREAMINFO, &streaminfo(1), false));
924 input.extend_from_slice(&block(STREAMINFO, &streaminfo(2), true));
925 input.extend_from_slice(&frames());
926 assert!(matches!(
927 FlacHandler.inspect(&input, &InspectOptions::names_only()),
928 Err(StryptError::Malformed {
929 detail: MalformedDetail::UnexpectedMarker,
930 ..
931 })
932 ));
933 }
934
935 #[test]
936 fn a_streaminfo_of_the_wrong_length_is_refused() {
937 let mut input = MAGIC.to_vec();
938 input.extend_from_slice(&block(STREAMINFO, &[0u8; 20], true));
939 input.extend_from_slice(&frames());
940 assert!(matches!(
941 FlacHandler.inspect(&input, &InspectOptions::names_only()),
942 Err(StryptError::Malformed {
943 detail: MalformedDetail::LengthOutOfRange,
944 ..
945 })
946 ));
947 }
948
949 #[test]
950 fn a_comment_block_whose_lengths_do_not_add_up_is_reported_and_deleted() {
951 let mut payload = 0xFFFF_FFFFu32.to_le_bytes().to_vec();
954 payload.extend_from_slice(b"SYNTHETIC-UNREADABLE-0014");
955 let input = flac(&[block(VORBIS_COMMENT, &payload, false)]);
956
957 let stripped = strip_ok(&input);
958 assert_eq!(stripped.report.removed[0].location, "VORBIS_COMMENT");
959 assert!(!contains(&stripped.bytes, b"SYNTHETIC-UNREADABLE-0014"));
960 }
961
962 #[test]
963 fn a_block_count_beyond_the_limit_is_refused() {
964 let blocks: Vec<Vec<u8>> = (0..64).map(|_| block(PADDING, &[0u8; 1], false)).collect();
965 let input = flac(&blocks);
966 let options = StripOptions {
967 limits: ParseLimits {
968 max_items: 8,
969 ..ParseLimits::default()
970 },
971 ..StripOptions::default()
972 };
973 assert!(matches!(
974 FlacHandler.strip(&input, &options),
975 Err(StryptError::LimitExceeded { .. })
976 ));
977 }
978
979 #[test]
980 fn truncation_at_every_length_is_refused_or_survived_but_never_panics() {
981 let input = flac(&[
982 comment_block(b"vendor", &[b"ARTIST=SYNTHETIC-0015"]),
983 block(PICTURE, b"\0\0\0\x03\0\0\0\x09image/pngSYNTHETIC", false),
984 block(PADDING, &[0u8; 8], false),
985 ]);
986 for n in 0..=input.len() {
987 let prefix = &input[0..n];
988 let _ = FlacHandler.inspect(prefix, &InspectOptions::names_only());
989 let _ = FlacHandler.strip(prefix, &StripOptions::default());
990 }
991 }
992}