1use crate::bytes::Reader;
19use crate::container::bmff::{self, Box as Bmff, BoxType, WalkError};
20use crate::detect::Format;
21use crate::error::{MalformedDetail, Result, StryptError};
22use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, exif, xmp};
23use crate::report::{
24 Finding, InspectOptions, MetadataKind, MetadataReport, MetadataValue, Note, Retained,
25 RetentionReason, StripReport,
26};
27
28pub(crate) mod boxes;
29
30#[derive(Debug, Clone, Copy)]
32#[non_exhaustive]
33pub struct HeifHandler {
34 format: Format,
35}
36
37impl HeifHandler {
38 pub const HEIF: Self = Self {
40 format: Format::Heif,
41 };
42 pub const AVIF: Self = Self {
44 format: Format::Avif,
45 };
46}
47
48impl MetadataHandler for HeifHandler {
49 fn name(&self) -> &'static str {
50 self.format.id()
51 }
52
53 fn format(&self) -> Format {
54 self.format
55 }
56
57 fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
58 let processed = process(self.format, input, options, &ParseLimits::default())?;
60 Ok(MetadataReport {
61 format: self.format,
62 findings: processed.findings,
63 notes: processed.notes,
64 })
65 }
66
67 fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
68 let processed = process(self.format, input, &options.inspect, &options.limits)?;
69 Ok(Stripped {
70 report: StripReport {
71 format: self.format,
72 removed: processed.findings,
73 retained: processed.retained,
74 notes: processed.notes,
75 input_bytes: as_u64(input.len()),
76 output_bytes: as_u64(processed.output.len()),
77 },
78 bytes: processed.output,
79 })
80 }
81}
82
83struct Processed {
85 output: Vec<u8>,
86 findings: Vec<Finding>,
87 retained: Vec<Retained>,
88 notes: Vec<Note>,
89}
90
91struct Item<'a> {
93 id: u32,
95 kind: BoxType,
96 name: &'a [u8],
98 content_type: &'a [u8],
100 data: Vec<&'a [u8]>,
102 bytes: u64,
103}
104
105struct Reference {
107 kind: BoxType,
108 from: u32,
109 to: Vec<u32>,
110}
111
112struct Property<'a> {
114 kind: BoxType,
115 payload: &'a [u8],
116}
117
118#[derive(Clone, Copy)]
120struct Association {
121 index: u16,
123 essential: bool,
125}
126
127struct Parsed<'a> {
129 ftyp: &'a [u8],
130 primary: u32,
131 items: Vec<Item<'a>>,
132 references: Vec<Reference>,
133 properties: Vec<Property<'a>>,
134 associations: Vec<(u32, Vec<Association>)>,
135 handler_name: &'a [u8],
137 stray_meta: Vec<(BoxType, u64)>,
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 let mut findings = Vec::new();
153 let mut notes = Vec::new();
154
155 for b in &top {
157 if boxes::MOTION_BOXES.contains(&b.kind) {
158 return Err(StryptError::UnsupportedFormat {
159 format: crate::error::UnsupportedKind::MotionHeif,
160 });
161 }
162 }
163
164 let parsed = parse(format, input, &top, &mut budget, limits)?;
165
166 for b in &top {
167 if boxes::top_level_kept(b.kind) {
168 continue;
169 }
170 if b.is(*b"mdat") {
173 continue;
174 }
175 findings.extend(report_stray_box(b, options));
176 }
177
178 for (kind, size) in &parsed.stray_meta {
179 let location = match kind {
180 b"xml " | b"bxml" => "meta/xml",
181 b"uuid" => "meta/uuid",
182 _ => "meta",
183 };
184 findings.push(
185 Finding::new(MetadataKind::Other, location, *size).with_field(xmp::name_of(kind)),
186 );
187 }
188
189 if !trailing.is_empty() {
190 findings.push(Finding::new(
192 MetadataKind::Other,
193 "trailing data",
194 as_u64(trailing.len()),
195 ));
196 }
197
198 for property in &parsed.properties {
201 if boxes::property_kept(property.kind, property.payload) {
202 continue;
203 }
204 let kind = if property.kind == *b"colr" {
205 MetadataKind::ColourProfile
206 } else {
207 MetadataKind::Other
208 };
209 findings.push(
210 Finding::new(kind, "iprp/ipco", as_u64(property.payload.len()))
211 .with_field(xmp::name_of(&property.kind)),
212 );
213 }
214
215 let removed_ids = decide(&parsed, &mut findings, options);
216
217 let primary_kept = parsed
220 .items
221 .iter()
222 .any(|i| i.id == parsed.primary && !removed_ids.contains(&i.id));
223 if !primary_kept {
224 return Err(malformed(format, MalformedDetail::MissingMarker));
225 }
226
227 if !parsed.handler_name.is_empty() {
228 findings.push(
229 Finding::new(
230 MetadataKind::SoftwareFingerprint,
231 "meta/hdlr",
232 as_u64(parsed.handler_name.len()),
233 )
234 .with_field("name")
235 .with_value(options, || {
236 MetadataValue::Text(xmp::name_of(parsed.handler_name))
237 }),
238 );
239 }
240
241 let output = write(&parsed, &removed_ids, format)?;
242
243 let retained = if parsed
244 .properties
245 .iter()
246 .any(|p| p.kind == *b"colr" && boxes::property_kept(p.kind, p.payload))
247 {
248 vec![Retained {
249 location: "iprp/ipco (colr nclx)".to_owned(),
250 reason: RetentionReason::RemovalWouldAlterPayload,
253 }]
254 } else {
255 Vec::new()
256 };
257
258 notes.extend(parsed_notes(&parsed));
259 Ok(Processed {
260 output,
261 findings,
262 retained,
263 notes,
264 })
265}
266
267fn parsed_notes(parsed: &Parsed<'_>) -> Vec<Note> {
269 let mut notes = Vec::new();
270 if parsed.items.iter().any(|i| i.kind == *b"grid") {
271 notes.push(Note::OutOfScopeContent {
272 location: "grid (tiled image; tiles are moved, never decoded)".to_owned(),
273 });
274 }
275 notes
276}
277
278const XMP_UUID: [u8; 16] = [
280 0xBE, 0x7A, 0xCF, 0xCB, 0x97, 0xA9, 0x42, 0xE8, 0x9C, 0x71, 0x99, 0x94, 0x91, 0xE3, 0xAF, 0xAC,
281];
282
283fn report_stray_box(b: &Bmff<'_>, options: &InspectOptions) -> Vec<Finding> {
285 let size = b.size;
286 if b.is(*b"uuid")
287 && b.payload.get(..16) == Some(&XMP_UUID)
288 && let Some(packet) = b.payload.get(16..)
289 {
290 return xmp::scan(packet, "uuid (XMP)", options);
291 }
292 let location = match &b.kind {
293 b"free" | b"skip" => "free space",
294 b"uuid" => "uuid",
295 _ => "box",
296 };
297 vec![Finding::new(MetadataKind::Other, location, size).with_field(xmp::name_of(&b.kind))]
298}
299
300fn decide(parsed: &Parsed<'_>, findings: &mut Vec<Finding>, options: &InspectOptions) -> Vec<u32> {
302 let thumbnails: Vec<u32> = parsed
306 .references
307 .iter()
308 .filter(|r| r.kind == boxes::REFERENCE_THUMBNAIL)
309 .map(|r| r.from)
310 .collect();
311
312 let mut removed = Vec::new();
313 for item in &parsed.items {
314 let is_thumbnail = thumbnails.contains(&item.id);
315 if boxes::is_image_item(item.kind) && !is_thumbnail {
316 if !item.name.is_empty() {
317 findings.push(
318 Finding::new(MetadataKind::Other, "iinf/infe", as_u64(item.name.len()))
319 .with_field("item_name")
320 .with_value(options, || MetadataValue::Text(xmp::name_of(item.name))),
321 );
322 }
323 continue;
324 }
325 removed.push(item.id);
326 findings.extend(report_item(item, is_thumbnail, options));
327 }
328 removed
329}
330
331fn report_item(item: &Item<'_>, is_thumbnail: bool, options: &InspectOptions) -> Vec<Finding> {
333 let joined: Vec<u8> = item.data.iter().flat_map(|s| s.iter().copied()).collect();
334
335 if is_thumbnail {
336 return vec![
337 Finding::new(MetadataKind::Thumbnail, "item (thmb)", item.bytes)
338 .with_field(xmp::name_of(&item.kind)),
339 ];
340 }
341
342 if item.kind == *b"Exif" {
343 let start = u32::from_be_bytes([
346 joined.first().copied().unwrap_or(0),
347 joined.get(1).copied().unwrap_or(0),
348 joined.get(2).copied().unwrap_or(0),
349 joined.get(3).copied().unwrap_or(0),
350 ]);
351 let at = usize::try_from(start)
352 .unwrap_or(usize::MAX)
353 .saturating_add(4);
354 if let Some(tiff) = joined.get(at..) {
355 let scanned = exif::scan(tiff, "item (Exif)", options, &ParseLimits::default());
356 if !scanned.findings.is_empty() {
357 return scanned.findings;
358 }
359 }
360 return vec![Finding::new(MetadataKind::Other, "item (Exif)", item.bytes)];
361 }
362
363 if item.kind == *b"mime" {
364 if xmp::contains(item.content_type, b"rdf+xml") || xmp::contains(&joined, b"<x:xmpmeta") {
366 let scanned = xmp::scan(&joined, "item (XMP)", options);
367 if !scanned.is_empty() {
368 return scanned;
369 }
370 }
371 return vec![
372 Finding::new(MetadataKind::Other, "item (mime)", item.bytes)
373 .with_field(xmp::name_of(item.content_type)),
374 ];
375 }
376
377 vec![
378 Finding::new(
379 MetadataKind::Other,
380 format!("item ({})", xmp::name_of(&item.kind)),
381 item.bytes,
382 )
383 .with_field(xmp::name_of(&item.kind)),
384 ]
385}
386
387fn parse<'a>(
393 format: Format,
394 input: &'a [u8],
395 top: &[Bmff<'a>],
396 budget: &mut u32,
397 limits: &ParseLimits,
398) -> Result<Parsed<'a>> {
399 let ftyp = bmff::find(top, *b"ftyp")
400 .ok_or_else(|| malformed(format, MalformedDetail::MissingMarker))?;
401
402 for brand in ftyp.payload.chunks_exact(4).skip(2) {
404 if let Ok(b) = <[u8; 4]>::try_from(brand)
405 && boxes::SEQUENCE_BRANDS.contains(&b)
406 {
407 return Err(StryptError::UnsupportedFormat {
408 format: crate::error::UnsupportedKind::MotionHeif,
409 });
410 }
411 }
412
413 let meta = bmff::find(top, *b"meta")
414 .ok_or_else(|| malformed(format, MalformedDetail::MissingMarker))?;
415 let (_, _, meta_body) = meta
416 .full()
417 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
418 let children = bmff::children_at(meta, meta_body, limits.max_depth, budget)
419 .map_err(|e| from_walk(format, e))?;
420
421 if bmff::find(&children, *b"ipro").is_some() {
424 return Err(malformed(format, MalformedDetail::UnsupportedFeature));
425 }
426
427 let stray_meta: Vec<(BoxType, u64)> = children
429 .iter()
430 .filter(|c| !boxes::meta_kept(c.kind) && !c.is(*b"idat") && !c.is(*b"ipro"))
431 .map(|c| (c.kind, c.size))
432 .collect();
433
434 let handler_name = bmff::find(&children, *b"hdlr")
435 .and_then(|b| handler_name(b))
436 .unwrap_or_default();
437
438 let primary = bmff::find(&children, *b"pitm")
439 .and_then(|b| primary_item(b))
440 .ok_or_else(|| malformed(format, MalformedDetail::MissingMarker))?;
441
442 let infos = bmff::find(&children, *b"iinf")
443 .ok_or_else(|| malformed(format, MalformedDetail::MissingMarker))
444 .and_then(|b| item_infos(format, b, limits.max_depth, budget))?;
445
446 let idat = bmff::find(&children, *b"idat").map(|b| b.payload);
447 let locations = bmff::find(&children, *b"iloc")
448 .ok_or_else(|| malformed(format, MalformedDetail::MissingMarker))
449 .and_then(|b| item_locations(format, b))?;
450
451 let references = bmff::find(&children, *b"iref")
452 .map(|b| item_references(format, b, limits.max_depth, budget))
453 .transpose()?
454 .unwrap_or_default();
455
456 let (properties, associations) = bmff::find(&children, *b"iprp")
457 .map(|b| item_properties(format, b, limits.max_depth, budget))
458 .transpose()?
459 .unwrap_or_default();
460
461 let mut items = Vec::with_capacity(infos.len());
463 for info in infos {
464 let (kind, name, content_type, protection, id) = info;
465 if protection != 0 {
466 return Err(malformed(format, MalformedDetail::UnsupportedFeature));
467 }
468 let extents = locations
469 .iter()
470 .find(|(item_id, _)| *item_id == id)
471 .map(|(_, e)| e.clone())
472 .unwrap_or_default();
473 let data = resolve(format, input, idat, &extents)?;
474 let bytes = data.iter().map(|s| as_u64(s.len())).sum();
475 items.push(Item {
476 id,
477 kind,
478 name,
479 content_type,
480 data,
481 bytes,
482 });
483 }
484
485 Ok(Parsed {
486 ftyp: ftyp.payload,
487 primary,
488 items,
489 references,
490 properties,
491 associations,
492 handler_name,
493 stray_meta,
494 })
495}
496
497#[derive(Clone, Copy)]
499struct Extent {
500 construction: u8,
502 offset: u64,
503 length: u64,
504}
505
506fn resolve<'a>(
508 format: Format,
509 input: &'a [u8],
510 idat: Option<&'a [u8]>,
511 extents: &[Extent],
512) -> Result<Vec<&'a [u8]>> {
513 let mut out = Vec::with_capacity(extents.len());
514 for extent in extents {
515 let source: &[u8] = match extent.construction {
516 0 => input,
517 1 => idat.ok_or_else(|| malformed(format, MalformedDetail::MissingMarker))?,
518 _ => return Err(malformed(format, MalformedDetail::UnsupportedFeature)),
521 };
522 let start = usize::try_from(extent.offset)
523 .map_err(|_| malformed(format, MalformedDetail::LengthOutOfRange))?;
524 let len = usize::try_from(extent.length)
525 .map_err(|_| malformed(format, MalformedDetail::LengthOutOfRange))?;
526 let end = start
527 .checked_add(len)
528 .ok_or_else(|| malformed(format, MalformedDetail::LengthOutOfRange))?;
529 let slice = source
530 .get(start..end)
531 .ok_or_else(|| malformed(format, MalformedDetail::LengthOutOfRange))?;
532 out.push(slice);
533 }
534 Ok(out)
535}
536
537fn handler_name<'a>(b: &Bmff<'a>) -> Option<&'a [u8]> {
539 let (_, _, body) = b.full()?;
540 let rest = body.get(20..)?;
542 let end = rest.iter().position(|&c| c == 0).unwrap_or(rest.len());
543 rest.get(..end)
544}
545
546fn primary_item(b: &Bmff<'_>) -> Option<u32> {
548 let (version, _, body) = b.full()?;
549 let mut r = Reader::new(body);
550 if version == 0 {
551 r.u16_be().map(u32::from)
552 } else {
553 r.u32_be()
554 }
555}
556
557type ItemInfo<'a> = (BoxType, &'a [u8], &'a [u8], u16, u32);
559
560fn item_infos<'a>(
561 format: Format,
562 b: &Bmff<'a>,
563 depth: u32,
564 budget: &mut u32,
565) -> Result<Vec<ItemInfo<'a>>> {
566 let (version, _, body) = b
567 .full()
568 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
569 let mut r = Reader::new(body);
570 let _count = if version == 0 {
571 u32::from(
572 r.u16_be()
573 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?,
574 )
575 } else {
576 r.u32_be()
577 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?
578 };
579 let rest = r.take_rest();
581 let entries = bmff::children_at(b, rest, depth, budget).map_err(|e| from_walk(format, e))?;
582
583 let mut out = Vec::with_capacity(entries.len());
584 for entry in &entries {
585 if !entry.is(*b"infe") {
586 continue;
587 }
588 out.push(item_info(format, entry)?);
589 }
590 Ok(out)
591}
592
593fn item_info<'a>(format: Format, b: &Bmff<'a>) -> Result<ItemInfo<'a>> {
595 let (version, _, body) = b
596 .full()
597 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
598 if version < 2 {
600 return Err(malformed(format, MalformedDetail::UnsupportedFeature));
601 }
602 let mut r = Reader::new(body);
603 let id = if version == 2 {
604 u32::from(
605 r.u16_be()
606 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?,
607 )
608 } else {
609 r.u32_be()
610 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?
611 };
612 let protection = r
613 .u16_be()
614 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
615 let kind: BoxType = r
616 .take(4)
617 .and_then(|b| b.try_into().ok())
618 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
619 let rest = r.take_rest();
620 let (name, after) = c_string(rest);
621 let content_type = if kind == *b"mime" {
622 c_string(after).0
623 } else {
624 &[][..]
625 };
626 Ok((kind, name, content_type, protection, id))
627}
628
629fn c_string(data: &[u8]) -> (&[u8], &[u8]) {
631 let end = data.iter().position(|&c| c == 0).unwrap_or(data.len());
632 let text = data.get(..end).unwrap_or_default();
633 let rest = data.get(end.saturating_add(1)..).unwrap_or_default();
634 (text, rest)
635}
636
637fn item_locations(format: Format, b: &Bmff<'_>) -> Result<Vec<(u32, Vec<Extent>)>> {
639 let (version, _, body) = b
640 .full()
641 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
642 let mut r = Reader::new(body);
643 let sizes = r
644 .u16_be()
645 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
646 let offset_size = ((sizes >> 12) & 0xF) as u8;
647 let length_size = ((sizes >> 8) & 0xF) as u8;
648 let base_offset_size = ((sizes >> 4) & 0xF) as u8;
649 let index_size = (sizes & 0xF) as u8;
650
651 let count = if version < 2 {
652 u32::from(
653 r.u16_be()
654 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?,
655 )
656 } else {
657 r.u32_be()
658 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?
659 };
660
661 let mut out = Vec::new();
662 for _ in 0..count {
663 let id = if version < 2 {
664 u32::from(
665 r.u16_be()
666 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?,
667 )
668 } else {
669 r.u32_be()
670 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?
671 };
672 let construction = if version == 0 {
673 0
674 } else {
675 let word = r
676 .u16_be()
677 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
678 (word & 0xF) as u8
679 };
680 let data_reference = r
682 .u16_be()
683 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
684 if data_reference != 0 {
685 return Err(malformed(format, MalformedDetail::UnsupportedFeature));
686 }
687 let base = read_uint(&mut r, base_offset_size)
688 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
689 let extent_count = r
690 .u16_be()
691 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
692
693 let mut extents = Vec::new();
694 for _ in 0..extent_count {
695 if version > 0 && index_size > 0 {
696 read_uint(&mut r, index_size)
697 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
698 }
699 let offset = read_uint(&mut r, offset_size)
700 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
701 let length = read_uint(&mut r, length_size)
702 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
703 extents.push(Extent {
704 construction,
705 offset: base.saturating_add(offset),
706 length,
707 });
708 }
709 out.push((id, extents));
710 }
711 Ok(out)
712}
713
714fn read_uint(r: &mut Reader<'_>, size: u8) -> Option<u64> {
716 match size {
717 0 => Some(0),
718 4 => r.u32_be().map(u64::from),
719 8 => {
720 let b: [u8; 8] = r.take(8)?.try_into().ok()?;
721 Some(u64::from_be_bytes(b))
722 }
723 _ => None,
725 }
726}
727
728fn item_references(
730 format: Format,
731 b: &Bmff<'_>,
732 depth: u32,
733 budget: &mut u32,
734) -> Result<Vec<Reference>> {
735 let (version, _, body) = b
736 .full()
737 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
738 let entries = bmff::children_at(b, body, depth, budget).map_err(|e| from_walk(format, e))?;
739
740 let mut out = Vec::with_capacity(entries.len());
741 for entry in &entries {
742 let mut r = Reader::new(entry.payload);
743 let wide = version != 0;
744 let from = if wide {
745 r.u32_be()
746 } else {
747 r.u16_be().map(u32::from)
748 }
749 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
750 let count = r
751 .u16_be()
752 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
753 let mut to = Vec::with_capacity(usize::from(count));
754 for _ in 0..count {
755 let id = if wide {
756 r.u32_be()
757 } else {
758 r.u16_be().map(u32::from)
759 }
760 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
761 to.push(id);
762 }
763 out.push(Reference {
764 kind: entry.kind,
765 from,
766 to,
767 });
768 }
769 Ok(out)
770}
771
772type Properties<'a> = (Vec<Property<'a>>, Vec<(u32, Vec<Association>)>);
774
775fn item_properties<'a>(
776 format: Format,
777 b: &Bmff<'a>,
778 depth: u32,
779 budget: &mut u32,
780) -> Result<Properties<'a>> {
781 let children =
782 bmff::children_at(b, b.payload, depth, budget).map_err(|e| from_walk(format, e))?;
783
784 let properties = match bmff::find(&children, *b"ipco") {
785 Some(ipco) => bmff::children_at(ipco, ipco.payload, depth, budget)
786 .map_err(|e| from_walk(format, e))?
787 .iter()
788 .map(|p| Property {
789 kind: p.kind,
790 payload: p.payload,
791 })
792 .collect(),
793 None => Vec::new(),
794 };
795
796 let mut associations = Vec::new();
797 for ipma in children.iter().filter(|c| c.is(*b"ipma")) {
798 let (version, flags, body) = ipma
799 .full()
800 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
801 let mut r = Reader::new(body);
802 let count = r
803 .u32_be()
804 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
805 for _ in 0..count {
806 let id = if version == 0 {
807 r.u16_be().map(u32::from)
808 } else {
809 r.u32_be()
810 }
811 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
812 let n = r
813 .u8()
814 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
815 let mut list = Vec::with_capacity(usize::from(n));
816 for _ in 0..n {
817 let (essential, index) = if flags & 1 == 1 {
819 let word = r
820 .u16_be()
821 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
822 ((word & 0x8000) != 0, word & 0x7FFF)
823 } else {
824 let byte = r
825 .u8()
826 .ok_or_else(|| malformed(format, MalformedDetail::Truncated))?;
827 ((byte & 0x80) != 0, u16::from(byte & 0x7F))
828 };
829 list.push(Association { index, essential });
830 }
831 associations.push((id, list));
832 }
833 }
834
835 Ok((properties, associations))
836}
837
838const ILOC_OFFSET_SIZE: u8 = 4;
845const ILOC_LENGTH_SIZE: u8 = 4;
846
847fn write(parsed: &Parsed<'_>, removed: &[u32], format: Format) -> Result<Vec<u8>> {
852 let kept: Vec<&Item<'_>> = parsed
853 .items
854 .iter()
855 .filter(|i| !removed.contains(&i.id))
856 .collect();
857
858 let mut ftyp = Vec::new();
859 bmff::write_box(&mut ftyp, *b"ftyp", |out| {
860 out.extend_from_slice(parsed.ftyp);
863 Ok(())
864 })
865 .map_err(|d| malformed(format, d))?;
866
867 let probe = write_meta(parsed, &kept, 0, format)?;
869 let mdat_base = as_u64(ftyp.len())
870 .checked_add(as_u64(probe.len()))
871 .and_then(|n| n.checked_add(8))
872 .ok_or_else(|| malformed(format, MalformedDetail::LengthOutOfRange))?;
873 let meta = write_meta(parsed, &kept, mdat_base, format)?;
874
875 if meta.len() != probe.len() {
878 return Err(malformed(format, MalformedDetail::NotRoundTrippable));
879 }
880
881 let mut out = Vec::new();
882 out.extend_from_slice(&ftyp);
883 out.extend_from_slice(&meta);
884 bmff::write_box(&mut out, *b"mdat", |out| {
885 for item in &kept {
886 for chunk in &item.data {
887 out.extend_from_slice(chunk);
888 }
889 }
890 Ok(())
891 })
892 .map_err(|d| malformed(format, d))?;
893
894 Ok(out)
895}
896
897fn write_meta(
899 parsed: &Parsed<'_>,
900 kept: &[&Item<'_>],
901 mdat_base: u64,
902 format: Format,
903) -> Result<Vec<u8>> {
904 let renumber = |old: u32| -> Option<u16> {
907 kept.iter()
908 .position(|i| i.id == old)
909 .and_then(|p| u16::try_from(p.saturating_add(1)).ok())
910 };
911
912 let mut kept_properties: Vec<usize> = Vec::new();
915 for (i, property) in parsed.properties.iter().enumerate() {
916 if boxes::property_kept(property.kind, property.payload) {
917 kept_properties.push(i);
918 }
919 }
920 let reindex = |old: u16| -> Option<u16> {
921 let zero_based = usize::from(old).checked_sub(1)?;
922 kept_properties
923 .iter()
924 .position(|&i| i == zero_based)
925 .and_then(|p| u16::try_from(p.saturating_add(1)).ok())
926 };
927
928 let mut out = Vec::new();
929 bmff::write_full_box(&mut out, *b"meta", 0, 0, |out| {
930 bmff::write_full_box(out, *b"hdlr", 0, 0, |out| {
932 out.extend_from_slice(&[0; 4]);
933 out.extend_from_slice(b"pict");
934 out.extend_from_slice(&[0; 12]);
935 out.push(0);
936 Ok(())
937 })?;
938
939 let primary = renumber(parsed.primary).unwrap_or(1);
940 bmff::write_full_box(out, *b"pitm", 0, 0, |out| {
941 out.extend_from_slice(&primary.to_be_bytes());
942 Ok(())
943 })?;
944
945 bmff::write_box(out, *b"dinf", |out| {
947 bmff::write_full_box(out, *b"dref", 0, 0, |out| {
948 out.extend_from_slice(&1_u32.to_be_bytes());
949 bmff::write_full_box(out, *b"url ", 0, 1, |_| Ok(()))
951 })
952 })?;
953
954 write_iinf(out, kept)?;
955 write_iloc(out, kept, mdat_base)?;
956 write_iref(out, parsed, &renumber)?;
957 write_iprp(out, parsed, kept, &kept_properties, &reindex)
958 })
959 .map_err(|d| malformed(format, d))?;
960 Ok(out)
961}
962
963fn write_iinf(out: &mut Vec<u8>, kept: &[&Item<'_>]) -> std::result::Result<(), MalformedDetail> {
965 bmff::write_full_box(out, *b"iinf", 0, 0, |out| {
966 let count = u16::try_from(kept.len()).map_err(|_| MalformedDetail::LengthOutOfRange)?;
967 out.extend_from_slice(&count.to_be_bytes());
968 for (i, item) in kept.iter().enumerate() {
969 let id = u16::try_from(i.saturating_add(1))
970 .map_err(|_| MalformedDetail::LengthOutOfRange)?;
971 bmff::write_full_box(out, *b"infe", 2, 0, |out| {
972 out.extend_from_slice(&id.to_be_bytes());
973 out.extend_from_slice(&0_u16.to_be_bytes());
974 out.extend_from_slice(&item.kind);
975 out.push(0);
977 Ok(())
978 })?;
979 }
980 Ok(())
981 })
982}
983
984fn write_iloc(
986 out: &mut Vec<u8>,
987 kept: &[&Item<'_>],
988 mdat_base: u64,
989) -> std::result::Result<(), MalformedDetail> {
990 bmff::write_full_box(out, *b"iloc", 1, 0, |out| {
993 let widths = (u16::from(ILOC_OFFSET_SIZE) << 12) | (u16::from(ILOC_LENGTH_SIZE) << 8);
994 out.extend_from_slice(&widths.to_be_bytes());
995 let count = u16::try_from(kept.len()).map_err(|_| MalformedDetail::LengthOutOfRange)?;
996 out.extend_from_slice(&count.to_be_bytes());
997
998 let mut cursor = mdat_base;
999 for (i, item) in kept.iter().enumerate() {
1000 let id = u16::try_from(i.saturating_add(1))
1001 .map_err(|_| MalformedDetail::LengthOutOfRange)?;
1002 out.extend_from_slice(&id.to_be_bytes());
1003 out.extend_from_slice(&0_u16.to_be_bytes());
1004 out.extend_from_slice(&0_u16.to_be_bytes());
1005 out.extend_from_slice(&1_u16.to_be_bytes());
1007 let offset = u32::try_from(cursor).map_err(|_| MalformedDetail::LengthOutOfRange)?;
1008 let length =
1009 u32::try_from(item.bytes).map_err(|_| MalformedDetail::LengthOutOfRange)?;
1010 out.extend_from_slice(&offset.to_be_bytes());
1011 out.extend_from_slice(&length.to_be_bytes());
1012 cursor = cursor
1013 .checked_add(item.bytes)
1014 .ok_or(MalformedDetail::LengthOutOfRange)?;
1015 }
1016 Ok(())
1017 })
1018}
1019
1020fn write_iref<F>(
1022 out: &mut Vec<u8>,
1023 parsed: &Parsed<'_>,
1024 renumber: &F,
1025) -> std::result::Result<(), MalformedDetail>
1026where
1027 F: Fn(u32) -> Option<u16>,
1028{
1029 let usable: Vec<&Reference> = parsed
1030 .references
1031 .iter()
1032 .filter(|r| boxes::REFERENCE_TYPES_KEPT.contains(&r.kind))
1033 .filter(|r| renumber(r.from).is_some())
1034 .collect();
1035 if usable.is_empty() {
1036 return Ok(());
1037 }
1038 bmff::write_full_box(out, *b"iref", 0, 0, |out| {
1039 for reference in usable {
1040 let targets: Vec<u16> = reference.to.iter().filter_map(|t| renumber(*t)).collect();
1042 if targets.is_empty() {
1043 continue;
1044 }
1045 let from = renumber(reference.from).ok_or(MalformedDetail::BrokenIndex)?;
1046 bmff::write_box(out, reference.kind, |out| {
1047 out.extend_from_slice(&from.to_be_bytes());
1048 let count =
1049 u16::try_from(targets.len()).map_err(|_| MalformedDetail::LengthOutOfRange)?;
1050 out.extend_from_slice(&count.to_be_bytes());
1051 for target in &targets {
1052 out.extend_from_slice(&target.to_be_bytes());
1053 }
1054 Ok(())
1055 })?;
1056 }
1057 Ok(())
1058 })
1059}
1060
1061fn write_iprp<F>(
1063 out: &mut Vec<u8>,
1064 parsed: &Parsed<'_>,
1065 kept: &[&Item<'_>],
1066 kept_properties: &[usize],
1067 reindex: &F,
1068) -> std::result::Result<(), MalformedDetail>
1069where
1070 F: Fn(u16) -> Option<u16>,
1071{
1072 if kept_properties.is_empty() {
1073 return Ok(());
1074 }
1075 bmff::write_box(out, *b"iprp", |out| {
1076 bmff::write_box(out, *b"ipco", |out| {
1077 for &i in kept_properties {
1078 let property = parsed
1079 .properties
1080 .get(i)
1081 .ok_or(MalformedDetail::BrokenIndex)?;
1082 bmff::write_box(out, property.kind, |out| {
1083 out.extend_from_slice(property.payload);
1084 Ok(())
1085 })?;
1086 }
1087 Ok(())
1088 })?;
1089
1090 let wide = kept_properties.len() > 0x7F;
1092 let flags = u32::from(wide);
1093 bmff::write_full_box(out, *b"ipma", 0, flags, |out| {
1094 let mut entries: Vec<(u16, Vec<Association>)> = Vec::new();
1095 for (i, item) in kept.iter().enumerate() {
1096 let id = u16::try_from(i.saturating_add(1))
1097 .map_err(|_| MalformedDetail::LengthOutOfRange)?;
1098 let mut list = Vec::new();
1099 for (owner, associations) in &parsed.associations {
1100 if *owner != item.id {
1101 continue;
1102 }
1103 for association in associations {
1104 if let Some(index) = reindex(association.index) {
1105 list.push(Association {
1106 index,
1107 essential: association.essential,
1108 });
1109 }
1110 }
1111 }
1112 if !list.is_empty() {
1113 entries.push((id, list));
1114 }
1115 }
1116
1117 let count =
1118 u32::try_from(entries.len()).map_err(|_| MalformedDetail::LengthOutOfRange)?;
1119 out.extend_from_slice(&count.to_be_bytes());
1120 for (id, list) in entries {
1121 out.extend_from_slice(&id.to_be_bytes());
1122 let n = u8::try_from(list.len()).map_err(|_| MalformedDetail::LengthOutOfRange)?;
1123 out.push(n);
1124 for association in list {
1125 if wide {
1126 let mut word = association.index & 0x7FFF;
1127 if association.essential {
1128 word |= 0x8000;
1129 }
1130 out.extend_from_slice(&word.to_be_bytes());
1131 } else {
1132 let mut byte = u8::try_from(association.index & 0x7F).unwrap_or_default();
1133 if association.essential {
1134 byte |= 0x80;
1135 }
1136 out.push(byte);
1137 }
1138 }
1139 }
1140 Ok(())
1141 })
1142 })
1143}
1144
1145fn from_walk(format: Format, e: WalkError) -> StryptError {
1149 match e {
1150 WalkError::Malformed(detail) => malformed(format, detail),
1151 WalkError::Limit(limit) => StryptError::LimitExceeded { format, limit },
1152 }
1153}
1154
1155fn malformed(format: Format, detail: MalformedDetail) -> StryptError {
1157 StryptError::Malformed {
1158 format,
1159 offset: None,
1160 detail,
1161 }
1162}
1163
1164fn as_u64(value: usize) -> u64 {
1166 u64::try_from(value).unwrap_or(u64::MAX)
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171 #![allow(
1173 clippy::unwrap_used,
1174 clippy::indexing_slicing,
1175 clippy::arithmetic_side_effects
1176 )]
1177
1178 use super::*;
1179
1180 fn boxed(kind: [u8; 4], payload: &[u8]) -> Vec<u8> {
1181 let mut out = u32::try_from(payload.len() + 8)
1182 .unwrap()
1183 .to_be_bytes()
1184 .to_vec();
1185 out.extend_from_slice(&kind);
1186 out.extend_from_slice(payload);
1187 out
1188 }
1189
1190 fn full_boxed(kind: [u8; 4], version: u8, flags: u32, payload: &[u8]) -> Vec<u8> {
1191 let mut body = vec![version];
1192 body.extend_from_slice(&flags.to_be_bytes()[1..4]);
1193 body.extend_from_slice(payload);
1194 boxed(kind, &body)
1195 }
1196
1197 fn minimal(codestream: &[u8]) -> Vec<u8> {
1199 let mut meta = Vec::new();
1200 meta.extend_from_slice(&full_boxed(
1201 *b"hdlr",
1202 0,
1203 0,
1204 &[&[0u8; 4][..], b"pict", &[0u8; 13]].concat(),
1205 ));
1206 meta.extend_from_slice(&full_boxed(*b"pitm", 0, 0, &1_u16.to_be_bytes()));
1207 let infe = full_boxed(
1208 *b"infe",
1209 2,
1210 0,
1211 &[
1212 &1_u16.to_be_bytes()[..],
1213 &0_u16.to_be_bytes(),
1214 b"av01",
1215 &[0],
1216 ]
1217 .concat(),
1218 );
1219 meta.extend_from_slice(&full_boxed(
1220 *b"iinf",
1221 0,
1222 0,
1223 &[&1_u16.to_be_bytes()[..], &infe].concat(),
1224 ));
1225
1226 let ftyp = boxed(*b"ftyp", b"avif\x00\x00\x00\x00mif1avif");
1227 let with = |offset: u32| {
1229 let mut body = (((4_u16) << 12) | ((4_u16) << 8)).to_be_bytes().to_vec();
1230 body.extend_from_slice(&1_u16.to_be_bytes());
1231 body.extend_from_slice(&1_u16.to_be_bytes());
1232 body.extend_from_slice(&0_u16.to_be_bytes());
1233 body.extend_from_slice(&0_u16.to_be_bytes());
1234 body.extend_from_slice(&1_u16.to_be_bytes());
1235 body.extend_from_slice(&offset.to_be_bytes());
1236 body.extend_from_slice(&u32::try_from(codestream.len()).unwrap().to_be_bytes());
1237 let mut m = meta.clone();
1238 m.extend_from_slice(&full_boxed(*b"iloc", 1, 0, &body));
1239 full_boxed(*b"meta", 0, 0, &m)
1240 };
1241 let probe = with(0);
1242 let base = u32::try_from(ftyp.len() + probe.len() + 8).unwrap();
1243 let mut out = ftyp.clone();
1244 out.extend_from_slice(&with(base));
1245 out.extend_from_slice(&boxed(*b"mdat", codestream));
1246 out
1247 }
1248
1249 fn strip_ok(data: &[u8]) -> Stripped {
1250 HeifHandler::AVIF
1251 .strip(data, &StripOptions::default())
1252 .unwrap()
1253 }
1254
1255 #[test]
1256 fn a_minimal_file_round_trips_and_keeps_its_picture() {
1257 let out = strip_ok(&minimal(b"PICTURE-BYTES")).bytes;
1258 assert!(out.windows(13).any(|w| w == b"PICTURE-BYTES"));
1259 }
1260
1261 #[test]
1262 fn the_output_reads_back_clean() {
1263 let out = strip_ok(&minimal(b"PICTURE-BYTES")).bytes;
1264 let report = HeifHandler::AVIF
1265 .inspect(&out, &InspectOptions::names_only())
1266 .unwrap();
1267 assert!(report.findings.is_empty(), "{:?}", report.findings);
1268 }
1269
1270 #[test]
1271 fn stripping_twice_is_byte_identical() {
1272 let once = strip_ok(&minimal(b"PICTURE-BYTES")).bytes;
1273 let twice = strip_ok(&once).bytes;
1274 assert_eq!(once, twice);
1275 }
1276
1277 #[test]
1278 fn a_file_with_no_meta_is_refused() {
1279 let mut data = boxed(*b"ftyp", b"avif\x00\x00\x00\x00mif1avif");
1280 data.extend_from_slice(&boxed(*b"mdat", b"picture"));
1281 assert!(
1282 HeifHandler::AVIF
1283 .strip(&data, &StripOptions::default())
1284 .is_err()
1285 );
1286 }
1287
1288 #[test]
1289 fn a_moov_box_is_refused_as_a_motion_file() {
1290 let mut data = minimal(b"PICTURE");
1291 data.extend_from_slice(&boxed(*b"moov", b""));
1292 assert!(matches!(
1293 HeifHandler::HEIF.strip(&data, &StripOptions::default()),
1294 Err(StryptError::UnsupportedFormat {
1295 format: crate::error::UnsupportedKind::MotionHeif
1296 })
1297 ));
1298 }
1299
1300 #[test]
1301 fn an_iloc_width_the_format_does_not_permit_is_refused() {
1302 let mut data = minimal(b"PICTURE");
1305 let at = data.windows(4).position(|w| w == b"iloc").unwrap();
1306 data[at + 8] = 0x20;
1307 assert!(
1308 HeifHandler::AVIF
1309 .strip(&data, &StripOptions::default())
1310 .is_err()
1311 );
1312 }
1313
1314 #[test]
1315 fn read_uint_rejects_widths_outside_the_specification() {
1316 let mut r = Reader::new(&[0xFF; 16]);
1317 assert_eq!(read_uint(&mut r, 0), Some(0));
1318 assert!(read_uint(&mut r, 4).is_some());
1319 assert!(read_uint(&mut r, 8).is_some());
1320 assert!(read_uint(&mut r, 2).is_none());
1321 assert!(read_uint(&mut r, 3).is_none());
1322 }
1323
1324 #[test]
1325 fn a_c_string_stops_at_its_terminator() {
1326 assert_eq!(c_string(b"name\x00rest"), (&b"name"[..], &b"rest"[..]));
1327 assert_eq!(c_string(b"name"), (&b"name"[..], &b""[..]));
1329 assert_eq!(c_string(b""), (&b""[..], &b""[..]));
1330 }
1331
1332 #[test]
1333 fn the_report_counts_bytes_without_naming_values_by_default() {
1334 let mut data = minimal(b"PICTURE");
1336 data.extend_from_slice(&boxed(*b"free", b"SECRET"));
1337 let report = strip_ok(&data).report;
1338 assert!(report.removed.iter().all(|f| f.value.is_none()));
1339 assert!(report.removed.iter().any(|f| f.bytes > 0));
1340 }
1341}