1use crate::bytes::Reader;
34use crate::container::ogg::{self as page, Emit, Packet, WalkError};
35use crate::detect::Format;
36use crate::error::{MalformedDetail, Result, StryptError, UnsupportedKind};
37use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, vorbis, xmp};
38use crate::report::{
39 Finding, InspectOptions, MetadataKind, MetadataReport, MetadataValue, Note, Retained,
40 RetentionReason, StripReport,
41};
42
43#[derive(Debug, Clone, Copy)]
45pub struct OggHandler {
46 format: Format,
47}
48
49impl OggHandler {
50 pub const VORBIS: Self = Self {
52 format: Format::Ogg,
53 };
54 pub const OPUS: Self = Self {
56 format: Format::Opus,
57 };
58 pub const FLAC: Self = Self {
60 format: Format::OggFlac,
61 };
62}
63
64impl MetadataHandler for OggHandler {
65 fn name(&self) -> &'static str {
66 self.format.id()
67 }
68
69 fn format(&self) -> Format {
70 self.format
71 }
72
73 fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
74 let processed = process(input, self.format, options, &ParseLimits::default())?;
78 Ok(MetadataReport {
79 format: self.format,
80 findings: processed.findings,
81 notes: processed.notes,
82 })
83 }
84
85 fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
86 let processed = process(input, self.format, &options.inspect, &options.limits)?;
87 Ok(Stripped {
88 report: StripReport {
89 format: self.format,
90 removed: processed.findings,
91 retained: processed.retained,
92 notes: processed.notes,
93 input_bytes: as_u64(input.len()),
94 output_bytes: as_u64(processed.output.len()),
95 },
96 bytes: processed.output,
97 })
98 }
99}
100
101const VORBIS_ID: &[u8] = b"\x01vorbis";
103const VORBIS_COMMENT_ID: &[u8] = b"\x03vorbis";
104const VORBIS_SETUP_ID: &[u8] = b"\x05vorbis";
105const OPUS_HEAD: &[u8] = b"OpusHead";
106const OPUS_TAGS: &[u8] = b"OpusTags";
107const FLAC_MAPPING: &[u8] = b"\x7fFLAC";
108const THEORA_ID: &[u8] = b"\x80theora";
109const SPEEX_ID: &[u8] = b"Speex ";
110const SKELETON_ID: &[u8] = b"fishead\0";
111
112const VORBIS_FRAMING_BIT: u8 = 0x01;
114
115const FLAC_MAPPING_MAJOR: u8 = 1;
118const FLAC_MAPPING_HEADER_LEN: usize = 9;
119
120const FLAC_STREAMINFO: u8 = 0;
122const FLAC_PADDING: u8 = 1;
123const FLAC_SEEKTABLE: u8 = 3;
124const FLAC_VORBIS_COMMENT: u8 = 4;
125const FLAC_FORBIDDEN: u8 = 127;
126const FLAC_BLOCK_HEADER_LEN: usize = 4;
127const FLAC_STREAMINFO_LEN: usize = 34;
128const FLAC_STREAMINFO_MD5_AT: usize = 18;
129
130pub(crate) enum Sniff {
132 Supported(Format),
134 Refused(UnsupportedKind),
136}
137
138pub(crate) fn sniff(data: &[u8]) -> Option<Sniff> {
144 let first = page_at(data, 0)?;
145 if first.flags & page::BOS == 0 {
146 return Some(Sniff::Refused(UnsupportedKind::OtherOggCodec));
149 }
150 if let Some(second) = page_at(data, first.end)
153 && second.flags & page::BOS != 0
154 {
155 return Some(Sniff::Refused(UnsupportedKind::MultiplexedOgg));
156 }
157
158 let body = data.get(first.body..first.end).unwrap_or_default();
159 Some(match () {
160 () if body.starts_with(VORBIS_ID) => Sniff::Supported(Format::Ogg),
161 () if body.starts_with(OPUS_HEAD) => Sniff::Supported(Format::Opus),
162 () if body.starts_with(FLAC_MAPPING) => Sniff::Supported(Format::OggFlac),
163 () if body.starts_with(THEORA_ID) => Sniff::Refused(UnsupportedKind::OggTheora),
164 () if body.starts_with(SPEEX_ID) || body.starts_with(SKELETON_ID) => {
165 Sniff::Refused(UnsupportedKind::OtherOggCodec)
166 }
167 () => Sniff::Refused(UnsupportedKind::OtherOggCodec),
168 })
169}
170
171struct Header {
173 flags: u8,
174 body: usize,
176 end: usize,
178}
179
180fn page_at(data: &[u8], at: usize) -> Option<Header> {
182 let rest = data.get(at..)?;
183 if !rest.starts_with(page::MAGIC) {
184 return None;
185 }
186 let segments = usize::from(*rest.get(26)?);
187 let body = at.checked_add(27)?.checked_add(segments)?;
188 let lacing = rest.get(27..27usize.checked_add(segments)?)?;
189 let length = lacing
190 .iter()
191 .fold(0usize, |sum, n| sum.saturating_add(usize::from(*n)));
192 Some(Header {
193 flags: *rest.get(5)?,
194 body,
195 end: body.checked_add(length)?,
196 })
197}
198
199struct Processed {
201 findings: Vec<Finding>,
202 retained: Vec<Retained>,
203 notes: Vec<Note>,
204 output: Vec<u8>,
205}
206
207fn process(
209 input: &[u8],
210 format: Format,
211 options: &InspectOptions,
212 limits: &ParseLimits,
213) -> Result<Processed> {
214 let mut budget = limits.max_items;
215 let pages = page::pages(input, &mut budget).map_err(|e| convert(e, format))?;
216 single_bitstream(&pages, format)?;
217 let packets = page::packets(&pages).map_err(|e| convert(e, format))?;
218
219 let mut out = Processed {
220 findings: Vec::new(),
221 retained: Vec::new(),
222 notes: Vec::new(),
223 output: Vec::new(),
224 };
225 let mut replacements: Vec<Option<Vec<u8>>> = vec![None; packets.len()];
228 let headers = match format {
229 Format::Opus => opus(&packets, options, &mut replacements, &mut out)?,
230 Format::OggFlac => flac(&packets, options, &mut replacements, &mut out)?,
231 _ => vorbis_stream(&packets, options, &mut replacements, &mut out)?,
233 };
234 if packets.len() <= headers {
235 return Err(malformed(format, MalformedDetail::Truncated, None));
238 }
239
240 let emits: Vec<Emit<'_>> = packets
241 .iter()
242 .enumerate()
243 .map(
244 |(index, packet)| match replacements.get(index).and_then(|slot| slot.as_deref()) {
245 Some(bytes) => Emit::replacing(packet, bytes),
246 None => Emit::copied(packet),
247 },
248 )
249 .collect();
250 out.output = page::write(0, &emits).map_err(|detail| malformed(format, detail, None))?;
251
252 out.notes.push(Note::OutOfScopeContent {
255 location: "audio packets, which are copied without being decoded".to_owned(),
256 });
257 Ok(out)
258}
259
260fn single_bitstream(pages: &[page::Page<'_>], format: Format) -> Result<()> {
262 let Some(first) = pages.first() else {
263 return Err(malformed(format, MalformedDetail::MissingMarker, None));
264 };
265 if first.flags & page::BOS == 0 {
266 return Err(malformed(
267 format,
268 MalformedDetail::MissingMarker,
269 as_offset(first.offset),
270 ));
271 }
272 for (index, current) in pages.iter().enumerate() {
273 if current.serial != first.serial || (index > 0 && current.flags & page::BOS != 0) {
274 return Err(StryptError::UnsupportedFormat {
275 format: UnsupportedKind::MultiplexedOgg,
276 });
277 }
278 let last = index.saturating_add(1) == pages.len();
279 if (current.flags & page::EOS != 0) != last {
280 return Err(StryptError::UnsupportedFormat {
283 format: UnsupportedKind::MultiplexedOgg,
284 });
285 }
286 }
287 Ok(())
288}
289
290fn vorbis_stream(
292 packets: &[Packet<'_>],
293 options: &InspectOptions,
294 replacements: &mut [Option<Vec<u8>>],
295 out: &mut Processed,
296) -> Result<usize> {
297 let format = Format::Ogg;
298 expect(packets, 0, VORBIS_ID, format)?;
299 let comment = expect(packets, 1, VORBIS_COMMENT_ID, format)?;
300 expect(packets, 2, VORBIS_SETUP_ID, format)?;
301
302 let body = comment.get(VORBIS_COMMENT_ID.len()..).unwrap_or_default();
303 vorbis::comments(
304 body,
305 "VORBIS_COMMENT",
306 as_u64(body.len()),
307 options,
308 &mut out.findings,
309 );
310
311 let mut empty = VORBIS_COMMENT_ID.to_vec();
312 empty.extend_from_slice(&vorbis::EMPTY);
313 empty.push(VORBIS_FRAMING_BIT);
314 set(replacements, 1, empty);
315 Ok(3)
316}
317
318fn opus(
320 packets: &[Packet<'_>],
321 options: &InspectOptions,
322 replacements: &mut [Option<Vec<u8>>],
323 out: &mut Processed,
324) -> Result<usize> {
325 let format = Format::Opus;
326 expect(packets, 0, OPUS_HEAD, format)?;
327 let comment = expect(packets, 1, OPUS_TAGS, format)?;
328
329 let body = comment.get(OPUS_TAGS.len()..).unwrap_or_default();
330 vorbis::comments(
331 body,
332 "OpusTags",
333 as_u64(body.len()),
334 options,
335 &mut out.findings,
336 );
337
338 let mut empty = OPUS_TAGS.to_vec();
339 empty.extend_from_slice(&vorbis::EMPTY);
340 set(replacements, 1, empty);
341 Ok(2)
342}
343
344fn flac(
350 packets: &[Packet<'_>],
351 options: &InspectOptions,
352 replacements: &mut [Option<Vec<u8>>],
353 out: &mut Processed,
354) -> Result<usize> {
355 let format = Format::OggFlac;
356 let mapping = expect(packets, 0, FLAC_MAPPING, format)?;
357 let mut r = Reader::new(&mapping);
358 r.skip(FLAC_MAPPING.len())
359 .ok_or_else(|| malformed(format, MalformedDetail::Truncated, None))?;
360 if r.u8() != Some(FLAC_MAPPING_MAJOR) {
361 return Err(malformed(format, MalformedDetail::UnexpectedMarker, None));
363 }
364 let _minor = r.u8();
365 let declared = r
366 .u16_be()
367 .ok_or_else(|| malformed(format, MalformedDetail::Truncated, None))?;
368 if r.take(4) != Some(b"fLaC") {
369 return Err(malformed(
370 format,
371 MalformedDetail::MissingMarker,
372 as_offset(FLAC_MAPPING_HEADER_LEN),
373 ));
374 }
375 let (kind, last, payload) = block(r.take_rest(), format)?;
376 if kind != FLAC_STREAMINFO || payload.len() != FLAC_STREAMINFO_LEN {
377 return Err(malformed(format, MalformedDetail::MissingMarker, None));
378 }
379 if !md5_is_absent(payload) {
380 out.retained.push(Retained {
384 location: "STREAMINFO (MD5 of the unencoded audio)".to_owned(),
385 reason: RetentionReason::DerivedFromPayload,
386 });
387 }
388
389 let mut index = 1usize;
390 let mut done = last;
391 while !done {
392 let Some(packet) = packets.get(index) else {
393 return Err(malformed(format, MalformedDetail::Truncated, None));
394 };
395 let bytes = packet.bytes();
396 let (kind, last, payload) = block(&bytes, format)?;
397 done = last;
398 let flag = if last { 0x80 } else { 0x00 };
399 let size = as_u64(payload.len());
400 match kind {
401 FLAC_STREAMINFO | FLAC_FORBIDDEN => {
402 return Err(malformed(format, MalformedDetail::UnexpectedMarker, None));
405 }
406 FLAC_SEEKTABLE => {}
407 FLAC_VORBIS_COMMENT => {
408 vorbis::comments(payload, "VORBIS_COMMENT", size, options, &mut out.findings);
409 set(
410 replacements,
411 index,
412 flac_block(FLAC_VORBIS_COMMENT, flag, &vorbis::EMPTY),
413 );
414 }
415 FLAC_PADDING => {
416 if payload.iter().any(|byte| *byte != 0) {
417 out.findings.push(
419 Finding::new(MetadataKind::Other, "PADDING", size)
420 .with_field("Padding")
421 .with_value(options, || MetadataValue::Opaque { bytes: size }),
422 );
423 set(
424 replacements,
425 index,
426 flac_block(FLAC_PADDING, flag, &vec![0u8; payload.len()]),
427 );
428 }
429 }
430 other => {
431 out.findings
432 .push(finding_for(other, payload, size, options));
433 set(replacements, index, flac_block(FLAC_PADDING, flag, &[]));
434 }
435 }
436 index = index.saturating_add(1);
437 }
438
439 let counted = u16::try_from(index.saturating_sub(1)).unwrap_or(u16::MAX);
440 if declared != 0 && declared != counted {
441 return Err(malformed(format, MalformedDetail::LengthOutOfRange, None));
444 }
445 Ok(index)
446}
447
448fn block(data: &[u8], format: Format) -> Result<(u8, bool, &[u8])> {
450 let header = data
451 .get(..FLAC_BLOCK_HEADER_LEN)
452 .ok_or_else(|| malformed(format, MalformedDetail::Truncated, None))?;
453 let first = header.first().copied().unwrap_or_default();
454 let length = match header.get(1..4) {
455 Some([high, middle, low]) => {
456 usize::try_from(u32::from_be_bytes([0, *high, *middle, *low])).ok()
457 }
458 _ => None,
459 }
460 .ok_or_else(|| malformed(format, MalformedDetail::LengthOutOfRange, None))?;
461 let payload = data
462 .get(FLAC_BLOCK_HEADER_LEN..)
463 .ok_or_else(|| malformed(format, MalformedDetail::Truncated, None))?;
464 if payload.len() != length {
465 return Err(malformed(format, MalformedDetail::LengthOutOfRange, None));
468 }
469 Ok((first & 0x7F, first & 0x80 != 0, payload))
470}
471
472fn flac_block(kind: u8, last: u8, payload: &[u8]) -> Vec<u8> {
474 let mut out = vec![kind | last];
475 let length = u32::try_from(payload.len())
476 .unwrap_or(u32::MAX)
477 .to_be_bytes();
478 out.extend_from_slice(length.get(1..4).unwrap_or_default());
479 out.extend_from_slice(payload);
480 out
481}
482
483fn finding_for(kind: u8, payload: &[u8], size: u64, options: &InspectOptions) -> Finding {
486 match kind {
487 2 => Finding::new(MetadataKind::SoftwareFingerprint, "APPLICATION", size)
488 .with_field(xmp::name_of(payload.get(0..4).unwrap_or(payload)))
489 .with_value(options, || MetadataValue::Opaque { bytes: size }),
490 5 => Finding::new(MetadataKind::DocumentIdentifier, "CUESHEET", size)
491 .with_field("MediaCatalogNumber"),
492 6 => Finding::new(MetadataKind::Thumbnail, "PICTURE", size)
493 .with_value(options, || MetadataValue::Opaque { bytes: size }),
494 other => Finding::new(
495 MetadataKind::Other,
496 format!("Metadata block type {other}"),
497 size,
498 ),
499 }
500}
501
502fn md5_is_absent(payload: &[u8]) -> bool {
504 payload
505 .get(FLAC_STREAMINFO_MD5_AT..)
506 .is_none_or(|md5| md5.iter().all(|byte| *byte == 0))
507}
508
509fn expect<'a>(
511 packets: &'a [Packet<'_>],
512 index: usize,
513 marker: &[u8],
514 format: Format,
515) -> Result<std::borrow::Cow<'a, [u8]>> {
516 let packet = packets
517 .get(index)
518 .ok_or_else(|| malformed(format, MalformedDetail::Truncated, None))?;
519 let bytes = packet.bytes();
520 if !bytes.starts_with(marker) {
521 return Err(malformed(format, MalformedDetail::MissingMarker, None));
522 }
523 Ok(bytes)
524}
525
526fn set(replacements: &mut [Option<Vec<u8>>], index: usize, bytes: Vec<u8>) {
528 if let Some(slot) = replacements.get_mut(index) {
529 *slot = Some(bytes);
530 }
531}
532
533fn convert(error: WalkError, format: Format) -> StryptError {
535 match error {
536 WalkError::Malformed { detail, offset } => malformed(format, detail, as_offset(offset)),
537 WalkError::Limit(limit) => StryptError::LimitExceeded { format, limit },
538 }
539}
540
541fn malformed(format: Format, detail: MalformedDetail, offset: Option<u64>) -> StryptError {
543 StryptError::Malformed {
544 format,
545 offset,
546 detail,
547 }
548}
549
550fn as_offset(position: usize) -> Option<u64> {
552 u64::try_from(position).ok()
553}
554
555fn as_u64(value: usize) -> u64 {
557 u64::try_from(value).unwrap_or(u64::MAX)
558}
559
560#[cfg(test)]
561mod tests {
562 #![allow(
565 clippy::unwrap_used,
566 clippy::expect_used,
567 clippy::indexing_slicing,
568 clippy::arithmetic_side_effects
569 )]
570
571 use super::*;
572
573 fn le32(n: usize) -> [u8; 4] {
574 u32::try_from(n).unwrap().to_le_bytes()
575 }
576
577 fn comment_body(vendor: &[u8], items: &[&[u8]]) -> Vec<u8> {
579 let mut out = le32(vendor.len()).to_vec();
580 out.extend_from_slice(vendor);
581 out.extend_from_slice(&le32(items.len()));
582 for item in items {
583 out.extend_from_slice(&le32(item.len()));
584 out.extend_from_slice(item);
585 }
586 out
587 }
588
589 fn build_page(
591 flags: u8,
592 granule: u64,
593 serial: u32,
594 sequence: u32,
595 packets: &[&[u8]],
596 ) -> Vec<u8> {
597 let mut lacing = Vec::new();
598 let mut body = Vec::new();
599 for packet in packets {
600 let mut written = 0;
601 loop {
602 let take = 255.min(packet.len() - written);
603 lacing.push(u8::try_from(take).unwrap());
604 body.extend_from_slice(&packet[written..written + take]);
605 written += take;
606 if take < 255 {
607 break;
608 }
609 }
610 }
611 let mut out = page::MAGIC.to_vec();
612 out.push(0);
613 out.push(flags);
614 out.extend_from_slice(&granule.to_le_bytes());
615 out.extend_from_slice(&serial.to_le_bytes());
616 out.extend_from_slice(&sequence.to_le_bytes());
617 out.extend_from_slice(&[0u8; 4]);
618 out.push(u8::try_from(lacing.len()).unwrap());
619 out.extend_from_slice(&lacing);
620 out.extend_from_slice(&body);
621 let crc = page::crc(&out);
622 out[22..26].copy_from_slice(&crc.to_le_bytes());
623 out
624 }
625
626 fn vorbis_file(vendor: &[u8], items: &[&[u8]]) -> Vec<u8> {
628 let mut comment = VORBIS_COMMENT_ID.to_vec();
629 comment.extend_from_slice(&comment_body(vendor, items));
630 comment.push(VORBIS_FRAMING_BIT);
631 let mut id = VORBIS_ID.to_vec();
632 id.extend_from_slice(&[0u8; 23]);
633 let mut setup = VORBIS_SETUP_ID.to_vec();
634 setup.extend_from_slice(b"SETUP");
635
636 let mut out = build_page(page::BOS, 0, 0x1234_5678, 0, &[&id]);
637 out.extend_from_slice(&build_page(0, 0, 0x1234_5678, 1, &[&comment, &setup]));
638 out.extend_from_slice(&build_page(
639 page::EOS,
640 1024,
641 0x1234_5678,
642 2,
643 &[b"AUDIO-PACKET-PAYLOAD"],
644 ));
645 out
646 }
647
648 fn opus_file(vendor: &[u8], items: &[&[u8]]) -> Vec<u8> {
650 let mut head = OPUS_HEAD.to_vec();
651 head.extend_from_slice(&[1, 1, 0x38, 1, 0x80, 0xBB, 0, 0, 0, 0, 0]);
652 let mut tags = OPUS_TAGS.to_vec();
653 tags.extend_from_slice(&comment_body(vendor, items));
654
655 let mut out = build_page(page::BOS, 0, 99, 0, &[&head]);
656 out.extend_from_slice(&build_page(0, 0, 99, 1, &[&tags]));
657 out.extend_from_slice(&build_page(page::EOS, 960, 99, 2, &[b"AUDIO-PAYLOAD"]));
658 out
659 }
660
661 fn flac_mapping(headers: u16, md5: u8) -> Vec<u8> {
662 let mut out = FLAC_MAPPING.to_vec();
663 out.extend_from_slice(&[FLAC_MAPPING_MAJOR, 0]);
664 out.extend_from_slice(&headers.to_be_bytes());
665 out.extend_from_slice(b"fLaC");
666 let mut streaminfo = vec![0u8; FLAC_STREAMINFO_LEN];
667 for byte in streaminfo.iter_mut().skip(FLAC_STREAMINFO_MD5_AT) {
668 *byte = md5;
669 }
670 out.extend_from_slice(&flac_block(FLAC_STREAMINFO, 0, &streaminfo));
671 out
672 }
673
674 fn flac_file(blocks: &[(u8, Vec<u8>)]) -> Vec<u8> {
676 let mapping = flac_mapping(u16::try_from(blocks.len()).unwrap(), 0xAB);
677 let mut packets: Vec<Vec<u8>> = vec![mapping];
678 for (index, (kind, payload)) in blocks.iter().enumerate() {
679 let last = if index + 1 == blocks.len() { 0x80 } else { 0 };
680 packets.push(flac_block(*kind, last, payload));
681 }
682 let refs: Vec<&[u8]> = packets.iter().skip(1).map(Vec::as_slice).collect();
683 let mut out = build_page(page::BOS, 0, 7, 0, &[&packets[0]]);
684 out.extend_from_slice(&build_page(0, 0, 7, 1, &refs));
685 out.extend_from_slice(&build_page(page::EOS, 4096, 7, 2, &[b"\xff\xf8AUDIO"]));
686 out
687 }
688
689 fn strip_ok(handler: OggHandler, data: &[u8]) -> Stripped {
690 handler
691 .strip(data, &StripOptions::default())
692 .expect("strip failed")
693 }
694
695 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
696 haystack.windows(needle.len()).any(|w| w == needle)
697 }
698
699 #[test]
700 fn a_vorbis_comment_is_itemised_and_emptied() {
701 let input = vorbis_file(
702 b"SYNTHETIC-VENDOR-0001",
703 &[b"ARTIST=SYNTHETIC-ARTIST-0002", b"DATE=2026-09-03"],
704 );
705 let stripped = strip_ok(OggHandler::VORBIS, &input);
706 let fields: Vec<&str> = stripped
707 .report
708 .removed
709 .iter()
710 .filter_map(|f| f.field.as_deref())
711 .collect();
712 assert!(fields.contains(&"vendor"));
713 assert!(fields.contains(&"ARTIST"));
714 assert!(!contains(&stripped.bytes, b"SYNTHETIC-"));
715 }
716
717 #[test]
718 fn the_audio_packets_cross_byte_for_byte() {
719 let input = vorbis_file(b"v", &[b"ARTIST=SYNTHETIC-0003"]);
721 let output = strip_ok(OggHandler::VORBIS, &input).bytes;
722 assert!(contains(&output, b"AUDIO-PACKET-PAYLOAD"));
723 assert!(contains(&output, b"SETUP"), "the setup header moved");
724 }
725
726 #[test]
727 fn the_comment_packet_is_emptied_rather_than_dropped() {
728 let input = opus_file(b"libopus SYNTHETIC-0004", &[b"TITLE=SYNTHETIC-0005"]);
730 let output = strip_ok(OggHandler::OPUS, &input).bytes;
731 assert!(contains(&output, OPUS_TAGS));
732 assert!(!contains(&output, b"SYNTHETIC-"));
733 }
734
735 #[test]
736 fn the_serial_number_is_rewritten() {
737 for (handler, input) in [
739 (OggHandler::VORBIS, vorbis_file(b"v", &[])),
740 (OggHandler::OPUS, opus_file(b"v", &[])),
741 ] {
742 let output = strip_ok(handler, &input).bytes;
743 assert_eq!(output.get(14..18), Some(&0u32.to_le_bytes()[..]));
744 }
745 }
746
747 #[test]
748 fn the_granule_positions_are_carried_verbatim() {
749 let input = opus_file(b"v", &[b"ARTIST=SYNTHETIC-0006"]);
751 let output = strip_ok(OggHandler::OPUS, &input).bytes;
752 let mut budget = 4096;
753 let pages = page::pages(&output, &mut budget).unwrap();
754 let granules: Vec<u64> = pages.iter().map(|p| p.granule).collect();
755 assert_eq!(granules, vec![0, 0, 960]);
756 }
757
758 #[test]
759 fn stripping_twice_is_byte_exact() {
760 for (handler, input) in [
761 (
762 OggHandler::VORBIS,
763 vorbis_file(b"v", &[b"A=SYNTHETIC-0007"]),
764 ),
765 (OggHandler::OPUS, opus_file(b"v", &[b"A=SYNTHETIC-0008"])),
766 (
767 OggHandler::FLAC,
768 flac_file(&[(FLAC_VORBIS_COMMENT, comment_body(b"v", &[b"A=SYN-9"]))]),
769 ),
770 ] {
771 let once = strip_ok(handler, &input).bytes;
772 let twice = strip_ok(handler, &once).bytes;
773 assert_eq!(once, twice, "strip is not idempotent");
774 }
775 }
776
777 #[test]
778 fn a_stripped_stream_re_inspects_clean() {
779 for (handler, input) in [
780 (
781 OggHandler::VORBIS,
782 vorbis_file(b"v", &[b"A=SYNTHETIC-0010"]),
783 ),
784 (OggHandler::OPUS, opus_file(b"v", &[b"A=SYNTHETIC-0011"])),
785 (
786 OggHandler::FLAC,
787 flac_file(&[(FLAC_VORBIS_COMMENT, comment_body(b"v", &[b"A=SYN-12"]))]),
788 ),
789 ] {
790 let output = strip_ok(handler, &input).bytes;
791 let report = handler
792 .inspect(&output, &InspectOptions::names_only())
793 .unwrap();
794 assert!(report.findings.is_empty(), "{:?}", report.findings);
795 }
796 }
797
798 #[test]
799 fn an_ogg_flac_keeps_its_packet_count_when_a_block_goes() {
800 let mut picture = 3u32.to_be_bytes().to_vec();
803 picture.extend_from_slice(&9u32.to_be_bytes());
804 picture.extend_from_slice(b"image/png");
805 picture.extend_from_slice(&21u32.to_be_bytes());
806 picture.extend_from_slice(b"SYNTHETIC-PICTURE-013");
807 let input = flac_file(&[
808 (
809 FLAC_VORBIS_COMMENT,
810 comment_body(b"v", &[b"A=SYNTHETIC-14"]),
811 ),
812 (6, picture),
813 ]);
814
815 let stripped = strip_ok(OggHandler::FLAC, &input);
816 assert!(!contains(&stripped.bytes, b"SYNTHETIC-"));
817 let mut budget = 4096;
818 let before = page::packets(&page::pages(&input, &mut budget).unwrap())
819 .unwrap()
820 .len();
821 let mut budget = 4096;
822 let after = page::packets(&page::pages(&stripped.bytes, &mut budget).unwrap())
823 .unwrap()
824 .len();
825 assert_eq!(before, after, "a header packet was dropped");
826 }
827
828 #[test]
829 fn an_ogg_flac_declares_the_audio_md5_it_keeps() {
830 let input = flac_file(&[(FLAC_PADDING, vec![0u8; 8])]);
831 let stripped = strip_ok(OggHandler::FLAC, &input);
832 assert_eq!(
833 stripped.report.retained.first().map(|r| r.reason),
834 Some(RetentionReason::DerivedFromPayload)
835 );
836 }
837
838 #[test]
839 fn ogg_flac_padding_keeps_its_size_and_loses_its_contents() {
840 let mut payload = vec![0u8; 32];
841 payload[4..27].copy_from_slice(b"SYNTHETIC-IN-PADDING-15");
842 let input = flac_file(&[(FLAC_PADDING, payload)]);
843 let stripped = strip_ok(OggHandler::FLAC, &input);
844 assert!(!contains(&stripped.bytes, b"SYNTHETIC-"));
845 assert_eq!(stripped.report.removed[0].location, "PADDING");
846 }
847
848 #[test]
849 fn a_second_bitstream_is_refused_by_name() {
850 let mut input = vorbis_file(b"v", &[]);
851 let mut second = build_page(page::BOS | page::EOS, 0, 4242, 0, &[b"\x80theora"]);
852 std::mem::swap(&mut input, &mut second);
853 input.extend_from_slice(&second);
854 assert!(matches!(
855 OggHandler::VORBIS.strip(&input, &StripOptions::default()),
856 Err(StryptError::UnsupportedFormat {
857 format: UnsupportedKind::MultiplexedOgg
858 })
859 ));
860 }
861
862 #[test]
863 fn a_sniff_names_what_it_will_not_handle() {
864 let theora = build_page(page::BOS | page::EOS, 0, 1, 0, &[b"\x80theora"]);
865 assert!(matches!(
866 sniff(&theora),
867 Some(Sniff::Refused(UnsupportedKind::OggTheora))
868 ));
869 let speex = build_page(page::BOS | page::EOS, 0, 1, 0, &[b"Speex "]);
870 assert!(matches!(
871 sniff(&speex),
872 Some(Sniff::Refused(UnsupportedKind::OtherOggCodec))
873 ));
874 assert!(matches!(
875 sniff(&vorbis_file(b"v", &[])),
876 Some(Sniff::Supported(Format::Ogg))
877 ));
878 assert!(matches!(
879 sniff(&opus_file(b"v", &[])),
880 Some(Sniff::Supported(Format::Opus))
881 ));
882 assert!(matches!(
883 sniff(&flac_file(&[(FLAC_PADDING, vec![0u8; 4])])),
884 Some(Sniff::Supported(Format::OggFlac))
885 ));
886 }
887
888 #[test]
889 fn a_page_whose_crc_does_not_match_is_refused() {
890 let mut input = vorbis_file(b"v", &[]);
891 let at = input.len() - 1;
892 input[at] ^= 0xFF;
893 assert!(matches!(
894 OggHandler::VORBIS.strip(&input, &StripOptions::default()),
895 Err(StryptError::Malformed { .. })
896 ));
897 }
898
899 #[test]
900 fn a_stream_that_is_only_headers_is_refused() {
901 let mut comment = VORBIS_COMMENT_ID.to_vec();
902 comment.extend_from_slice(&comment_body(b"v", &[]));
903 comment.push(VORBIS_FRAMING_BIT);
904 let mut id = VORBIS_ID.to_vec();
905 id.extend_from_slice(&[0u8; 23]);
906 let mut setup = VORBIS_SETUP_ID.to_vec();
907 setup.extend_from_slice(b"SETUP");
908 let mut input = build_page(page::BOS, 0, 1, 0, &[&id]);
909 input.extend_from_slice(&build_page(page::EOS, 0, 1, 1, &[&comment, &setup]));
910 assert!(matches!(
911 OggHandler::VORBIS.strip(&input, &StripOptions::default()),
912 Err(StryptError::Malformed { .. })
913 ));
914 }
915
916 #[test]
917 fn a_missing_comment_header_is_refused() {
918 let mut id = VORBIS_ID.to_vec();
919 id.extend_from_slice(&[0u8; 23]);
920 let mut input = build_page(page::BOS, 0, 1, 0, &[&id]);
921 input.extend_from_slice(&build_page(
922 page::EOS,
923 1,
924 1,
925 1,
926 &[b"NOT-A-HEADER", b"AUDIO"],
927 ));
928 assert!(matches!(
929 OggHandler::VORBIS.strip(&input, &StripOptions::default()),
930 Err(StryptError::Malformed {
931 detail: MalformedDetail::MissingMarker,
932 ..
933 })
934 ));
935 }
936
937 #[test]
938 fn an_ogg_flac_block_that_does_not_fill_its_packet_is_refused() {
939 let mut input = flac_file(&[(FLAC_PADDING, vec![0u8; 8])]);
941 let at = input
943 .windows(4)
944 .position(|w| w == b"OggS")
945 .and_then(|_| input.windows(2).position(|w| w == [0x81, 0x00]))
946 .unwrap();
947 input[at + 3] = 0x40;
948 let mut budget = 4096;
949 if page::pages(&input, &mut budget).is_ok() {
950 }
953 assert!(
954 OggHandler::FLAC
955 .strip(&input, &StripOptions::default())
956 .is_err()
957 );
958 }
959
960 #[test]
961 fn truncation_at_every_length_is_refused_but_never_panics() {
962 let input = vorbis_file(b"vendor", &[b"ARTIST=SYNTHETIC-0016"]);
963 for n in 0..=input.len() {
964 let prefix = input.get(0..n).unwrap();
965 let _ = OggHandler::VORBIS.inspect(prefix, &InspectOptions::names_only());
966 let _ = OggHandler::VORBIS.strip(prefix, &StripOptions::default());
967 }
968 }
969}