1use crate::bytes::Reader;
20use crate::container::bmff::{self, Box as Bmff, BoxType, WalkError};
21use crate::detect::Format;
22use crate::error::{MalformedDetail, Result, StryptError, UnsupportedKind};
23use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, xmp};
24use crate::report::{
25 Finding, InspectOptions, MetadataKind, MetadataReport, MetadataValue, Note, Retained,
26 RetentionReason, StripReport,
27};
28
29pub(crate) mod boxes;
30
31#[derive(Debug, Clone, Copy)]
33#[non_exhaustive]
34pub struct Mp4Handler {
35 format: Format,
36}
37
38impl Mp4Handler {
39 pub const MP4: Self = Self {
41 format: Format::Mp4,
42 };
43 pub const M4A: Self = Self {
45 format: Format::M4a,
46 };
47}
48
49impl MetadataHandler for Mp4Handler {
50 fn name(&self) -> &'static str {
51 self.format.id()
52 }
53
54 fn format(&self) -> Format {
55 self.format
56 }
57
58 fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
59 let processed = process(self.format, input, options, &ParseLimits::default())?;
61 Ok(MetadataReport {
62 format: self.format,
63 findings: processed.findings,
64 notes: processed.notes,
65 })
66 }
67
68 fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
69 let processed = process(self.format, input, &options.inspect, &options.limits)?;
70 Ok(Stripped {
71 report: StripReport {
72 format: self.format,
73 removed: processed.findings,
74 retained: processed.retained,
75 notes: processed.notes,
76 input_bytes: as_u64(input.len()),
77 output_bytes: as_u64(processed.output.len()),
78 },
79 bytes: processed.output,
80 })
81 }
82}
83
84struct Processed {
86 output: Vec<u8>,
87 findings: Vec<Finding>,
88 retained: Vec<Retained>,
89 notes: Vec<Note>,
90}
91
92#[derive(Debug, Clone, Copy)]
94struct Extent {
95 old_start: u64,
96 old_end: u64,
97 new_start: u64,
98}
99
100fn relocate(extents: &[Extent], offset: u64) -> Option<u64> {
105 extents
106 .iter()
107 .find(|e| offset >= e.old_start && offset < e.old_end)
108 .and_then(|e| offset.checked_sub(e.old_start)?.checked_add(e.new_start))
109}
110
111enum Node<'a> {
113 Copy(&'a [u8]),
115 Container {
117 kind: BoxType,
118 children: Vec<Node<'a>>,
119 },
120 Patched { kind: BoxType, body: Vec<u8> },
122 Offsets {
124 kind: BoxType,
125 head: [u8; 4],
126 values: Vec<u64>,
127 },
128}
129
130enum Top<'a> {
132 Copy(&'a [u8]),
133 Mdat {
134 raw: &'a [u8],
135 header: u64,
136 old_start: u64,
137 len: u64,
138 },
139 Moov,
140}
141
142fn process(
144 format: Format,
145 input: &[u8],
146 options: &InspectOptions,
147 limits: &ParseLimits,
148) -> Result<Processed> {
149 let mut budget = limits.max_items;
150 let (top, trailing) = bmff::top_level(input, &mut budget).map_err(|e| from_walk(format, e))?;
151
152 for b in &top {
154 refuse_structural(b.kind)?;
155 }
156 let ftyp = bmff::find(&top, *b"ftyp")
157 .ok_or_else(|| malformed(format, MalformedDetail::MissingMarker))?;
158 refuse_brands(ftyp.payload)?;
159
160 match top.iter().filter(|b| b.is(*b"moov")).count() {
163 1 => {}
164 0 => return Err(malformed(format, MalformedDetail::MissingMarker)),
165 _ => return Err(malformed(format, MalformedDetail::UnexpectedMarker)),
166 }
167
168 let mut findings = Vec::new();
169 let mut tops = Vec::with_capacity(top.len());
170 let mut moov_node = None;
171
172 for b in &top {
173 let raw = bmff::raw(input, b)
174 .ok_or_else(|| malformed(format, MalformedDetail::LengthOutOfRange))?;
175 if b.is(*b"moov") {
176 moov_node = Some(container_node(
177 format,
178 input,
179 b,
180 b.payload,
181 limits.max_depth,
182 &mut budget,
183 &mut findings,
184 options,
185 None,
186 )?);
187 tops.push(Top::Moov);
188 } else if b.is(*b"mdat") {
189 let old_start = b.offset.saturating_add(b.header);
190 tops.push(Top::Mdat {
191 raw,
192 header: b.header,
193 old_start,
194 len: b.size.saturating_sub(b.header),
195 });
196 } else if boxes::TOP_LEVEL_COPIED.contains(&b.kind) {
197 tops.push(Top::Copy(raw));
198 } else {
199 findings.extend(report_dropped(
200 b,
201 "",
202 limits.max_depth,
203 &mut budget,
204 options,
205 ));
206 }
207 }
208
209 if !trailing.is_empty() {
210 findings.push(Finding::new(
212 MetadataKind::Other,
213 "trailing data",
214 as_u64(trailing.len()),
215 ));
216 }
217
218 let moov = moov_node.ok_or_else(|| malformed(format, MalformedDetail::MissingMarker))?;
219 require_playable(format, &moov)?;
220
221 let probe = write_moov(format, &moov, None)?;
225 let extents = layout(&tops, as_u64(probe.len()));
226 let written = write_moov(format, &moov, Some(&extents))?;
227 if written.len() != probe.len() {
228 return Err(malformed(format, MalformedDetail::NotRoundTrippable));
229 }
230
231 let mut output = Vec::with_capacity(input.len());
232 for t in &tops {
233 match t {
234 Top::Copy(raw) | Top::Mdat { raw, .. } => output.extend_from_slice(raw),
235 Top::Moov => output.extend_from_slice(&written),
236 }
237 }
238
239 Ok(Processed {
240 output,
241 findings,
242 retained: vec![Retained {
243 location: "moov/trak/mdia/minf/stbl/stsd (codec configuration)".to_owned(),
244 reason: RetentionReason::StructurallyRequired,
246 }],
247 notes: vec![Note::OutOfScopeContent {
248 location: "coded samples (SEI user data and in-band codec headers are not decoded)"
249 .to_owned(),
250 }],
251 })
252}
253
254fn layout(tops: &[Top<'_>], moov_len: u64) -> Vec<Extent> {
256 let mut at = 0u64;
257 let mut extents = Vec::new();
258 for t in tops {
259 match t {
260 Top::Copy(raw) => at = at.saturating_add(as_u64(raw.len())),
261 Top::Mdat {
262 raw,
263 header,
264 old_start,
265 len,
266 } => {
267 let new_start = at.saturating_add(*header);
268 extents.push(Extent {
269 old_start: *old_start,
270 old_end: old_start.saturating_add(*len),
271 new_start,
272 });
273 at = at.saturating_add(as_u64(raw.len()));
274 }
275 Top::Moov => at = at.saturating_add(moov_len),
276 }
277 }
278 extents
279}
280
281fn refuse_structural(kind: BoxType) -> Result<()> {
283 if boxes::FRAGMENT_BOXES.contains(&kind) {
284 return Err(StryptError::UnsupportedFormat {
285 format: UnsupportedKind::FragmentedMp4,
286 });
287 }
288 if boxes::PROTECTION_BOXES.contains(&kind) {
289 return Err(StryptError::UnsupportedFormat {
290 format: UnsupportedKind::ProtectedMedia,
291 });
292 }
293 Ok(())
294}
295
296fn refuse_brands(ftyp: &[u8]) -> Result<()> {
301 for brand in ftyp.chunks_exact(4) {
302 let Ok(b) = <[u8; 4]>::try_from(brand) else {
303 continue;
304 };
305 if boxes::FRAGMENT_BRANDS.contains(&b) {
309 return Err(StryptError::UnsupportedFormat {
310 format: UnsupportedKind::FragmentedMp4,
311 });
312 }
313 if b == boxes::PROTECTED_BRAND {
314 return Err(StryptError::UnsupportedFormat {
315 format: UnsupportedKind::ProtectedMedia,
316 });
317 }
318 if b == boxes::QUICKTIME_BRAND {
319 return Err(StryptError::UnsupportedFormat {
320 format: UnsupportedKind::QuickTimeMovie,
321 });
322 }
323 if b.get(..3) == Some(b"3gp") || b.get(..3) == Some(b"3g2") {
324 return Err(StryptError::UnsupportedFormat {
325 format: UnsupportedKind::ThirdGenerationPartnership,
326 });
327 }
328 }
329 Ok(())
330}
331
332fn require_playable(format: Format, moov: &Node<'_>) -> Result<()> {
337 let Node::Container { children, .. } = moov else {
338 return Err(malformed(format, MalformedDetail::MissingMarker));
339 };
340 let has = |kind: BoxType| {
341 children.iter().any(|c| match c {
342 Node::Container { kind: k, .. }
343 | Node::Patched { kind: k, .. }
344 | Node::Offsets { kind: k, .. } => *k == kind,
345 Node::Copy(_) => false,
346 })
347 };
348 if has(*b"mvhd") && has(*b"trak") {
349 Ok(())
350 } else {
351 Err(malformed(format, MalformedDetail::MissingMarker))
352 }
353}
354
355#[allow(clippy::too_many_arguments)]
361fn container_node<'a>(
362 format: Format,
363 input: &'a [u8],
364 parent: &Bmff<'a>,
365 payload: &'a [u8],
366 depth: u32,
367 budget: &mut u32,
368 findings: &mut Vec<Finding>,
369 options: &InspectOptions,
370 handler: Option<BoxType>,
371) -> Result<Node<'a>> {
372 let kids =
373 bmff::children_at(parent, payload, depth, budget).map_err(|e| from_walk(format, e))?;
374 let mut children = Vec::with_capacity(kids.len());
375
376 let handler = if parent.is(*b"mdia") {
379 bmff::find(&kids, *b"hdlr")
380 .and_then(Bmff::full)
381 .and_then(|(_, _, rest)| rest.get(4..8).and_then(|s| BoxType::try_from(s).ok()))
382 } else {
383 handler
384 };
385
386 for kid in &kids {
387 refuse_structural(kid.kind)?;
388 if !boxes::kept_in(parent.kind, kid.kind) {
389 findings.extend(report_dropped(
390 kid,
391 location_of(parent.kind),
392 depth,
393 budget,
394 options,
395 ));
396 continue;
397 }
398 let raw = bmff::raw(input, kid)
399 .ok_or_else(|| malformed(format, MalformedDetail::LengthOutOfRange))?;
400
401 if boxes::is_container(kid.kind) {
402 children.push(container_node(
403 format,
404 input,
405 kid,
406 kid.payload,
407 depth.saturating_sub(1),
408 budget,
409 findings,
410 options,
411 handler,
412 )?);
413 continue;
414 }
415
416 if kid.kind == boxes::STCO || kid.kind == boxes::CO64 {
417 children.push(offsets_node(format, kid)?);
418 continue;
419 }
420 match &kid.kind {
421 b"mvhd" | b"tkhd" | b"mdhd" => {
422 children.push(header_node(format, kid, raw, findings, options)?);
423 }
424 b"hdlr" => children.push(handler_node(kid, raw, findings, options)),
425 b"stsd" => {
426 children.push(sample_description_node(
427 format,
428 kid,
429 raw,
430 handler,
431 depth.saturating_sub(1),
432 budget,
433 findings,
434 options,
435 )?);
436 }
437 b"dinf" => {
438 check_data_reference(format, kid, depth.saturating_sub(1), budget)?;
439 children.push(Node::Copy(raw));
440 }
441 _ => children.push(Node::Copy(raw)),
442 }
443 }
444
445 Ok(Node::Container {
446 kind: parent.kind,
447 children,
448 })
449}
450
451fn location_of(parent: BoxType) -> &'static str {
453 match &parent {
454 b"moov" => "moov",
455 b"trak" => "moov/trak",
456 b"mdia" => "moov/trak/mdia",
457 b"minf" => "moov/trak/mdia/minf",
458 b"stbl" => "moov/trak/mdia/minf/stbl",
459 _ => "",
460 }
461}
462
463fn path_of(kind: BoxType) -> &'static str {
465 match &kind {
466 b"mvhd" => "moov/mvhd",
467 b"tkhd" => "moov/trak/tkhd",
468 _ => "moov/trak/mdia/mdhd",
469 }
470}
471
472const LANGUAGE_UNDETERMINED: [u8; 2] = [0x55, 0xC4];
475
476fn header_node<'a>(
478 format: Format,
479 b: &Bmff<'a>,
480 raw: &'a [u8],
481 findings: &mut Vec<Finding>,
482 options: &InspectOptions,
483) -> Result<Node<'a>> {
484 let (version, flags, rest) = b
485 .full()
486 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
487 let wide = version == 1;
488 let times = if wide { 16usize } else { 8 };
489 let Some(head) = rest.get(..times) else {
490 return Ok(Node::Copy(raw));
493 };
494
495 let mut body = Vec::with_capacity(rest.len().saturating_add(4));
496 if head.iter().any(|byte| *byte != 0) {
497 findings.push(
498 Finding::new(MetadataKind::Timestamp, path_of(b.kind), as_u64(times))
499 .with_field("creation_time, modification_time"),
500 );
501 }
502 body.push(version);
503 let f = flags.to_be_bytes();
504 body.extend_from_slice(f.get(1..4).unwrap_or(&[0, 0, 0]));
505 body.resize(body.len().saturating_add(times), 0);
506 body.extend_from_slice(rest.get(times..).unwrap_or_default());
507
508 if b.is(*b"mdhd") {
510 let at: usize = if wide { 32 } else { 20 };
511 if rest.get(at.saturating_sub(4)..at.saturating_sub(2)) != Some(&LANGUAGE_UNDETERMINED)
512 && set_bytes(&mut body, at, &LANGUAGE_UNDETERMINED)
513 {
514 findings.push(
515 Finding::new(MetadataKind::Other, "moov/trak/mdia/mdhd", 2).with_field("language"),
516 );
517 }
518 }
519 if b.is(*b"mvhd") {
520 let at: usize = if wide { 84 } else { 72 };
523 let already_zero = body
524 .get(at..at.saturating_add(24))
525 .is_none_or(|s| s.iter().all(|byte| *byte == 0));
526 if !already_zero && set_bytes(&mut body, at, &[0u8; 24]) {
527 findings.push(
528 Finding::new(MetadataKind::Other, "moov/mvhd", 24)
529 .with_field("pre_defined")
530 .with_value(options, || {
531 MetadataValue::Text("poster, preview and selection times".to_owned())
532 }),
533 );
534 }
535 }
536
537 Ok(Node::Patched { kind: b.kind, body })
538}
539
540fn set_bytes(body: &mut [u8], at: usize, value: &[u8]) -> bool {
542 let Some(end) = at.checked_add(value.len()) else {
543 return false;
544 };
545 match body.get_mut(at..end) {
546 Some(slot) => {
547 slot.copy_from_slice(value);
548 true
549 }
550 None => false,
551 }
552}
553
554const HDLR_NAME_OFFSET: usize = 20;
556
557fn handler_node<'a>(
559 b: &Bmff<'a>,
560 raw: &'a [u8],
561 findings: &mut Vec<Finding>,
562 options: &InspectOptions,
563) -> Node<'a> {
564 let Some((version, flags, rest)) = b.full() else {
565 return Node::Copy(raw);
566 };
567 let Some(name) = rest.get(HDLR_NAME_OFFSET..) else {
568 return Node::Copy(raw);
569 };
570 if name.is_empty() || name == b"\0" {
573 return Node::Copy(raw);
574 }
575
576 findings.push(
577 Finding::new(
578 MetadataKind::SoftwareFingerprint,
579 "moov/trak/mdia/hdlr",
580 as_u64(name.len()),
581 )
582 .with_field("name")
583 .with_value(options, || MetadataValue::Text(xmp::name_of(name))),
584 );
585
586 let mut body = Vec::with_capacity(HDLR_NAME_OFFSET.saturating_add(5));
587 body.push(version);
588 let f = flags.to_be_bytes();
589 body.extend_from_slice(f.get(1..4).unwrap_or(&[0, 0, 0]));
590 body.extend_from_slice(rest.get(..HDLR_NAME_OFFSET).unwrap_or_default());
591 body.push(0);
592 Node::Patched { kind: b.kind, body }
593}
594
595fn offsets_node<'a>(format: Format, b: &Bmff<'_>) -> Result<Node<'a>> {
597 let (version, flags, rest) = b
598 .full()
599 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
600 let wide = b.is(boxes::CO64);
601 let width = if wide { 8usize } else { 4 };
602
603 let mut r = Reader::new(rest);
604 let count = r
605 .u32_be()
606 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
607 let count =
608 usize::try_from(count).map_err(|_| malformed(format, MalformedDetail::BrokenIndex))?;
609 let declared = count
610 .checked_mul(width)
611 .ok_or_else(|| malformed(format, MalformedDetail::BrokenIndex))?;
612 if r.remaining() != declared {
615 return Err(malformed(format, MalformedDetail::BrokenIndex));
616 }
617
618 let mut values = Vec::with_capacity(count);
619 for _ in 0..count {
620 let value = if wide {
621 let hi = r
622 .u32_be()
623 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
624 let lo = r
625 .u32_be()
626 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
627 (u64::from(hi) << 32) | u64::from(lo)
628 } else {
629 u64::from(
630 r.u32_be()
631 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?,
632 )
633 };
634 values.push(value);
635 }
636
637 let f = flags.to_be_bytes();
638 let head = [
639 version,
640 f.get(1).copied().unwrap_or(0),
641 f.get(2).copied().unwrap_or(0),
642 f.get(3).copied().unwrap_or(0),
643 ];
644 Ok(Node::Offsets {
645 kind: b.kind,
646 head,
647 values,
648 })
649}
650
651#[allow(clippy::too_many_arguments)]
662fn sample_description_node<'a>(
663 format: Format,
664 b: &Bmff<'_>,
665 raw: &'a [u8],
666 handler: Option<BoxType>,
667 depth: u32,
668 budget: &mut u32,
669 findings: &mut Vec<Finding>,
670 options: &InspectOptions,
671) -> Result<Node<'a>> {
672 let (version, flags, rest) = b
673 .full()
674 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
675 let entries = rest.get(4..).unwrap_or_default();
676 let kids = bmff::children_at(b, entries, depth, budget).map_err(|e| from_walk(format, e))?;
679 for kid in &kids {
680 if boxes::PROTECTED_SAMPLE_ENTRIES.contains(&kid.kind) {
681 return Err(StryptError::UnsupportedFormat {
682 format: UnsupportedKind::ProtectedMedia,
683 });
684 }
685 }
686
687 if handler != Some(*b"vide") {
688 return Ok(Node::Copy(raw));
689 }
690
691 let mut body = Vec::with_capacity(rest.len().saturating_add(4));
693 body.push(version);
694 let f = flags.to_be_bytes();
695 body.extend_from_slice(f.get(1..4).unwrap_or(&[0, 0, 0]));
696 body.extend_from_slice(rest);
697
698 let mut touched = false;
699 for kid in &kids {
700 let Some(at) = kid
703 .offset
704 .checked_add(kid.header)
705 .and_then(|start| start.checked_sub(b.offset.checked_add(b.header)?))
706 .and_then(|within| usize::try_from(within).ok())
707 .and_then(|within| within.checked_add(COMPRESSOR_NAME_OFFSET))
708 else {
709 continue;
710 };
711 let Some(end) = at.checked_add(COMPRESSOR_NAME_LEN) else {
712 continue;
713 };
714 let Some(field) = body.get(at..end) else {
715 continue;
716 };
717 if field.iter().all(|byte| *byte == 0) {
718 continue;
719 }
720 findings.push(
721 Finding::new(
722 MetadataKind::SoftwareFingerprint,
723 "moov/trak/mdia/minf/stbl/stsd",
724 as_u64(COMPRESSOR_NAME_LEN),
725 )
726 .with_field("compressorname")
727 .with_value(options, || {
728 MetadataValue::Text(compressor_name(field).unwrap_or_default())
729 }),
730 );
731 set_bytes(&mut body, at, &[0u8; COMPRESSOR_NAME_LEN]);
732 touched = true;
733 }
734
735 if touched {
736 Ok(Node::Patched { kind: b.kind, body })
737 } else {
738 Ok(Node::Copy(raw))
739 }
740}
741
742const COMPRESSOR_NAME_OFFSET: usize = 42;
744const COMPRESSOR_NAME_LEN: usize = 32;
745
746fn compressor_name(field: &[u8]) -> Option<String> {
748 let len = usize::from(*field.first()?).min(COMPRESSOR_NAME_LEN.saturating_sub(1));
749 Some(xmp::name_of(field.get(1..1usize.checked_add(len)?)?))
750}
751
752fn check_data_reference(
757 format: Format,
758 dinf: &Bmff<'_>,
759 depth: u32,
760 budget: &mut u32,
761) -> Result<()> {
762 let kids =
763 bmff::children_at(dinf, dinf.payload, depth, budget).map_err(|e| from_walk(format, e))?;
764 let Some(dref) = bmff::find(&kids, *b"dref") else {
765 return Ok(());
766 };
767 let Some((_, _, rest)) = dref.full() else {
768 return Err(malformed(format, MalformedDetail::Truncated));
769 };
770 let entries = rest.get(4..).unwrap_or_default();
771 let refs = bmff::children_at(dref, entries, depth.saturating_sub(1), budget)
772 .map_err(|e| from_walk(format, e))?;
773 for entry in &refs {
774 let self_contained = entry.full().is_some_and(|(_, flags, _)| flags & 1 != 0);
775 if !self_contained {
776 return Err(malformed(format, MalformedDetail::UnsupportedFeature));
777 }
778 }
779 Ok(())
780}
781
782fn write_moov(format: Format, moov: &Node<'_>, extents: Option<&[Extent]>) -> Result<Vec<u8>> {
788 let mut out = Vec::new();
789 write_node(&mut out, moov, extents).map_err(|detail| malformed(format, detail))?;
790 Ok(out)
791}
792
793fn write_node(
794 out: &mut Vec<u8>,
795 node: &Node<'_>,
796 extents: Option<&[Extent]>,
797) -> std::result::Result<(), MalformedDetail> {
798 match node {
799 Node::Copy(raw) => {
800 out.extend_from_slice(raw);
801 Ok(())
802 }
803 Node::Container { kind, children } => bmff::write_box(out, *kind, |o| {
804 for child in children {
805 write_node(o, child, extents)?;
806 }
807 Ok(())
808 }),
809 Node::Patched { kind, body } => bmff::write_box(out, *kind, |o| {
810 o.extend_from_slice(body);
811 Ok(())
812 }),
813 Node::Offsets { kind, head, values } => bmff::write_box(out, *kind, |o| {
814 o.extend_from_slice(head);
815 let count =
816 u32::try_from(values.len()).map_err(|_| MalformedDetail::LengthOutOfRange)?;
817 o.extend_from_slice(&count.to_be_bytes());
818 let wide = kind == &boxes::CO64;
819 for value in values {
820 let mapped = match extents {
821 None => 0,
824 Some(extents) => {
825 relocate(extents, *value).ok_or(MalformedDetail::BrokenIndex)?
826 }
827 };
828 if wide {
829 o.extend_from_slice(&mapped.to_be_bytes());
830 } else {
831 let narrow =
832 u32::try_from(mapped).map_err(|_| MalformedDetail::LengthOutOfRange)?;
833 o.extend_from_slice(&narrow.to_be_bytes());
834 }
835 }
836 Ok(())
837 }),
838 }
839}
840
841const REPORT_DESCENT: u32 = 3;
847
848fn report_dropped(
850 b: &Bmff<'_>,
851 parent: &str,
852 depth: u32,
853 budget: &mut u32,
854 options: &InspectOptions,
855) -> Vec<Finding> {
856 let at = if parent.is_empty() {
857 xmp::name_of(&b.kind)
858 } else {
859 format!("{parent}/{}", xmp::name_of(&b.kind))
860 };
861
862 if b.is(*b"uuid") {
863 if b.payload.get(..16) == Some(&boxes::XMP_UUID)
864 && let Some(packet) = b.payload.get(16..)
865 {
866 let scanned = xmp::scan(packet, &format!("{at} (XMP)"), options);
867 if !scanned.is_empty() {
868 return scanned;
869 }
870 }
871 return vec![Finding::new(MetadataKind::Other, at, b.size)];
872 }
873 if matches!(&b.kind, b"free" | b"skip" | b"wide") {
874 return vec![Finding::new(MetadataKind::Other, "free space", b.size).with_field(at)];
875 }
876
877 if matches!(&b.kind, b"udta" | b"meta" | b"ilst" | b"keys") {
880 let body = if b.is(*b"meta") {
881 b.full().map_or(b.payload, |(_, _, rest)| rest)
882 } else {
883 b.payload
884 };
885 if depth > 0
886 && let Ok(kids) = bmff::children_at(b, body, REPORT_DESCENT.min(depth), budget)
887 && !kids.is_empty()
888 {
889 let mut out = Vec::new();
890 for kid in &kids {
891 out.extend(report_dropped(
892 kid,
893 &at,
894 depth.saturating_sub(1),
895 budget,
896 options,
897 ));
898 }
899 return out;
900 }
901 }
902
903 let kind = tag_kind(b.kind);
904 let mut finding = Finding::new(kind, at, b.size).with_field(xmp::name_of(&b.kind));
905 if let Some(text) = tag_text(b.payload) {
906 finding = finding.with_value(options, || MetadataValue::Text(text));
907 }
908 vec![finding]
909}
910
911fn tag_kind(name: BoxType) -> MetadataKind {
917 match &name {
918 b"\xA9xyz" | b"loci" | b"gps " | b"\xA9gps" => MetadataKind::Location,
919 b"\xA9day" | b"date" | b"\xA9dtm" => MetadataKind::Timestamp,
920 b"\xA9too" | b"\xA9swr" | b"\xA9enc" | b"\xA9req" => MetadataKind::SoftwareFingerprint,
921 b"\xA9mak" | b"\xA9mod" | b"mak " | b"mod " | b"\xA9xmk" | b"\xA9xmd" => {
922 MetadataKind::DeviceIdentity
923 }
924 b"\xA9ART" | b"aART" | b"\xA9wrt" | b"\xA9alb" | b"\xA9cpy" | b"cprt" | b"auth"
925 | b"perf" | b"\xA9prf" | b"\xA9ope" => MetadataKind::PersonalIdentity,
926 b"covr" | b"\xA9art" => MetadataKind::Thumbnail,
927 b"\xA9cmt" | b"desc" | b"ldes" | b"\xA9des" | b"\xA9lyr" => MetadataKind::Comment,
928 _ => MetadataKind::Other,
929 }
930}
931
932fn tag_text(payload: &[u8]) -> Option<String> {
938 let text = payload.get(8..).filter(|_| payload.len() > 8)?;
939 let printable = text.iter().all(|b| *b >= 0x20 || *b == b'\n');
940 if printable && std::str::from_utf8(text).is_ok() {
941 return Some(xmp::name_of(text));
942 }
943 let short = payload.get(4..)?;
944 if short.iter().all(|b| *b >= 0x20) && std::str::from_utf8(short).is_ok() {
945 return Some(xmp::name_of(short));
946 }
947 None
948}
949
950fn from_walk(format: Format, e: WalkError) -> StryptError {
953 match e {
954 WalkError::Malformed(detail) => malformed(format, detail),
955 WalkError::Limit(limit) => StryptError::LimitExceeded { format, limit },
956 }
957}
958
959fn malformed(format: Format, detail: MalformedDetail) -> StryptError {
960 StryptError::Malformed {
961 format,
962 offset: None,
963 detail,
964 }
965}
966
967fn as_u64(value: usize) -> u64 {
968 u64::try_from(value).unwrap_or(u64::MAX)
969}
970
971#[cfg(test)]
972mod tests {
973 #![allow(
976 clippy::unwrap_used,
977 clippy::indexing_slicing,
978 clippy::arithmetic_side_effects
979 )]
980
981 use super::*;
982
983 #[test]
984 fn an_offset_outside_every_mdat_does_not_resolve() {
985 let extents = [Extent {
988 old_start: 100,
989 old_end: 200,
990 new_start: 40,
991 }];
992 assert_eq!(relocate(&extents, 100), Some(40));
993 assert_eq!(relocate(&extents, 199), Some(139));
994 assert_eq!(relocate(&extents, 200), None);
995 assert_eq!(relocate(&extents, 99), None);
996 }
997
998 #[test]
999 fn several_mdats_each_translate_by_their_own_delta() {
1000 let extents = [
1001 Extent {
1002 old_start: 100,
1003 old_end: 200,
1004 new_start: 90,
1005 },
1006 Extent {
1007 old_start: 300,
1008 old_end: 400,
1009 new_start: 250,
1010 },
1011 ];
1012 assert_eq!(relocate(&extents, 150), Some(140));
1013 assert_eq!(relocate(&extents, 350), Some(300));
1014 assert_eq!(relocate(&extents, 250), None);
1015 }
1016
1017 #[test]
1018 fn the_gps_atom_is_named_as_a_location() {
1019 assert_eq!(tag_kind(*b"\xA9xyz"), MetadataKind::Location);
1020 assert_eq!(tag_kind(*b"\xA9too"), MetadataKind::SoftwareFingerprint);
1021 assert_eq!(tag_kind(*b"XPRV"), MetadataKind::Other);
1022 }
1023}