//! Hostile and damaged input: nothing may panic; whatever parses must write back identically. use craft_aep::{AepError, parse, write}; /// Parses; on success, the invariant "parse then write is the identity" must hold. fn check(bytes: &[u8]) -> Result<(), AepError> { let file = parse(bytes)?; let back = write(&file).map_err(|_| AepError::TooLarge)?; assert!(back == bytes, "round trip of an accepted input differs ({} bytes)", bytes.len()); Ok(()) } /// A small deterministic generator (xorshift64*). struct Rng(u64); impl Rng { fn next(&mut self) -> u64 { self.0 ^= self.0 >> 12; self.0 ^= self.0 << 25; self.0 ^= self.0 >> 27; self.0.wrapping_mul(0x2545_F491_4F6C_DD1D) } fn below(&mut self, n: usize) -> usize { (self.next() % n.max(1) as u64) as usize } } #[test] fn empty_and_tiny_inputs() { assert_eq!(check(&[]), Err(AepError::NotRifx)); for n in 0..16 { let bytes = fixture!("001-empty.aep"); let _ = check(&bytes[..n]); } } #[test] fn truncated_at_every_length() { let bytes = fixture!("001-empty.aep"); let declared = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize + 8; for len in 0..=bytes.len() { let r = check(&bytes[..len]); if len < declared { assert!(r.is_err(), "length {len} of {declared} must be rejected"); } else { assert!(r.is_ok(), "length {len} (>= declared end) must parse: {r:?}"); } } } #[test] fn truncated_larger_fixture_at_sampled_lengths() { let bytes = fixture!("090-mask-rect.aep"); let declared = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize + 8; let mut rng = Rng(0x9e37_79b9_7f4a_7c15); for _ in 0..400 { let len = rng.below(bytes.len() + 1); let r = check(&bytes[..len]); assert_eq!(r.is_ok(), len >= declared, "len {len}"); } } #[test] fn random_byte_flips() { for (name, iterations) in [("001-empty.aep", 4000), ("000-base.aep", 1500), ("110-text-A.aep", 600)] { let original = fixture!(name); let mut rng = Rng(0x1234_5678_9abc_def1 ^ original.len() as u64); let (mut ok, mut err) = (0, 0); for _ in 0..iterations { let mut b = original.clone(); for _ in 0..=rng.below(8) { let at = rng.below(b.len()); b[at] = rng.next() as u8; } match check(&b) { Ok(()) => ok += 1, Err(_) => err += 1, } } assert_eq!(ok + err, iterations, "{name}"); } } #[test] fn a_whole_combined_file_survives_flips_and_truncation() { // The cases above are cut from the combined files by our writer; this one is After Effects' // own bytes (the smallest combined file). let original = skip!(crate::common::try_file("aepx-pair.aep")); let declared = u32::from_be_bytes([original[4], original[5], original[6], original[7]]) as usize + 8; let mut rng = Rng(0x0bad_5eed_1234_5678); for _ in 0..200 { let len = rng.below(original.len() + 1); assert_eq!(check(&original[..len]).is_ok(), len >= declared, "len {len}"); let mut b = original.clone(); for _ in 0..=rng.below(8) { let at = rng.below(b.len()); b[at] = rng.next() as u8; } let _ = check(&b); } } #[test] fn flips_in_the_headers_and_sizes() { // Aim at the bytes that matter most: the first 200 bytes, every value of every byte. let original = fixture!("001-empty.aep"); for at in 0..200 { for v in [0u8, 1, 0x7f, 0x80, 0xff] { let mut b = original.clone(); b[at] = v; let _ = check(&b); } } } #[test] fn oversized_chunk_sizes() { let original = fixture!("001-empty.aep"); // Every chunk header's size field set to huge values, one at a time, at every 4-byte aligned // offset of the first 2 KB. for at in (12..2048).step_by(2) { for v in [u32::MAX, 0x8000_0000, 0x0100_0000, original.len() as u32, 1 << 20] { let mut b = original.clone(); b[at..at + 4].copy_from_slice(&v.to_be_bytes()); let _ = check(&b); } } // The container size itself. for v in [0u32, 3, 4, 5, u32::MAX, original.len() as u32] { let mut b = original.clone(); b[4..8].copy_from_slice(&v.to_be_bytes()); let _ = check(&b); } } #[test] fn garbage_after_a_valid_header() { let mut rng = Rng(42); for _ in 0..2000 { let n = rng.below(200); let mut b = b"RIFX\0\0\0\0Egg!".to_vec(); for _ in 0..n { b.push(rng.next() as u8); } let size = (b.len() - 8) as u32; b[4..8].copy_from_slice(&size.to_be_bytes()); let _ = check(&b); } } // ----------------------------------------------------------------------------------- typed view use craft_aep::{AepFile, Body, Chunk, Comp, Layer, Project, PropGroup, PropNode, Property}; /// Touches the property accessors; none may panic on a damaged property. fn exercise_property(p: &Property<'_, '_>) { let _ = (p.header(), p.dims(), p.is_animated(), p.static_value(), p.time_scale(), p.flags()); let _ = (p.expression(), p.expression_flags().map(|f| f.disabled()), p.expression_enabled(), p.user_name(), p.first_key_value()); if let Some(keys) = p.keys() { let scale = p.time_scale().unwrap_or(0); for k in keys.keyframes() { let _ = (k.time_units(), k.raw().len(), k.kind(), k.seconds(scale), k.seconds(0)); let _ = (k.interpolation_in(), k.interpolation_out(), k.flags(), k.label(), k.is_roving()); let _ = (k.scalar_value(), k.ease(), k.spatial(), k.components(), k.value()); let _ = k.ease_components(); for component in [0, 1, 2, 3, 7, 255, usize::MAX] { let _ = k.ease_component(component); } let _ = k.spatial().map(|s| (s.value.len(), s.in_tangent.len(), s.out_tangent.len(), s.ease)); } } } fn exercise_group(g: &PropGroup<'_, '_>, depth: usize) { if depth > 24 { return; } let _ = (g.flags(), g.names(), g.user_name()); for e in g.entries() { match e.node { PropNode::Group(g) => exercise_group(&g, depth + 1), PropNode::Property(p) => exercise_property(&p), PropNode::Other(c) => { if let Some(g) = c.child_list(b"tdgp").and_then(PropGroup::from_chunk) { exercise_group(&g, depth + 1); } } } } } fn exercise_layer(project: &Project<'_, '_>, comp_size: (u32, u32), layer: &Layer<'_, '_>) { let _ = (layer.name(), layer.is_three_d(), layer.id(), layer.track_matte()); if let Some(h) = layer.header() { let _ = (h.in_point(), h.out_point(), h.stretch_ratio(), h.stretch_den); let _ = (h.is_shy(), h.is_locked(), h.is_solo(), h.is_three_d(), h.is_motion_blur(), h.is_adjustment(), h.is_guide()); let _ = (h.is_null_object(), h.collapses_transformations(), h.quality, h.blend_mode, h.layer_type); let _ = ( h.is_enabled(), h.is_video_enabled(), h.is_audio_enabled(), h.is_effects_enabled(), h.is_auto_oriented(), h.light_type(), h.blend_mode.code(), ); } let _ = (layer.is_enabled(), layer.is_audio_enabled(), layer.is_effects_enabled()); let _ = layer.camera_position().map(|p| (p.stored, p.auto_state)); let _ = project.layer_anchor_px(layer); let _ = layer .time_remap() .map(|t| (t.enabled, t.keys.iter().fold(0.0, |acc, k| acc + k.value_seconds + k.time_seconds.unwrap_or(0.0) + f64::from(k.time_units)))); for g in layer.shape_groups() { let _ = (g.name, g.position, g.details.len()); for d in &g.details { match d { craft_aep::ShapeDetail::Polystar { star_type, outer_radius, inner_radius, inner_roundness, item_index } => { let _ = (star_type.value, outer_radius.value, inner_radius.value, inner_roundness.map(|r| r.value), *item_index); } craft_aep::ShapeDetail::Gradient { gradient_type, highlight_length, highlight_angle, item_index } => { let _ = (gradient_type.value, highlight_length.map(|t| t.value), highlight_angle.map(|t| t.value), *item_index); } craft_aep::ShapeDetail::RectPosition { position, item_index } => { let _ = (position.value, *item_index); } } } for item in g.items { if let craft_aep::ShapeItem::Path { paths, .. } = item { for s in paths { let _ = s.vertices((1.0, 1.0)).iter().fold(0.0, |acc, v| acc + v.in_tangent()[0] + v.out_tangent()[1]); } } } } let _ = (project.layer_source(layer).map(|i| i.id()), project.layer_source(layer).and_then(|i| i.display_name()), project.layer_source_size(layer)); if let Some(t) = layer.transform(comp_size.0, comp_size.1) { let _ = (t.anchor_px(f64::from(comp_size.0), f64::from(comp_size.1)), t.position.value, t.opacity_percent.value); } if let Some(root) = layer.properties() { exercise_group(&root, 0); } // One index for the layer's effects: building it walks every layer of the file. let defs = project.effect_definition_index(); for e in layer.effects() { let _ = (e.match_name().len(), e.name(), e.declared_param_count(), e.has_own_definitions(), defs.source(&e)); for p in e.params().into_iter().chain(defs.params(&e)) { let _ = (p.index, p.kind, p.name.len(), p.items.as_ref().map(Vec::len), p.pard().len(), p.value(), p.stored_value(), p.pard_default()); let _ = (p.is_default(), p.popup_item_count(), p.popup_selected_index(), p.popup_selected_item(), p.valid_max(), p.is_animated()); let _ = (p.valid_min(), p.slider_range()); if let Some(keys) = p.keys() { for k in keys.keyframes() { let _ = (k.value(), k.spatial(), k.time_units(), k.ease()); } } for owner in [0, 1, 0x1b, u32::MAX] { let _ = p.layer_ref(owner); } for size in [0.0, 1920.0, f64::NAN, f64::INFINITY, f64::MAX] { let _ = p.point_px(size, size); } if let Some(s) = p.stored() { exercise_property(&s); } } } for m in layer.masks() { let _ = (m.name.clone(), m.mode, m.inverted, m.counter, m.feather, m.opacity_percent, m.expansion, m.path_animated, m.path_key_seconds.len()); for s in &m.shapes { let _ = (s.vertex_count(), s.vertices((1920.0, 1080.0)).len(), s.vertices((f64::NAN, f64::INFINITY)).len(), s.open); } } if let Some(t) = layer.text_document() { let _ = (t.fill_rgb(), t.fonts.len(), t.text.len(), t.raw_text.len(), t.font_size, t.font.clone(), t.paragraph_texts().len()); let _ = ( t.paragraphs.iter().fold(0, |acc, p| acc + p.length), t.style_runs.iter().fold(0.0, |acc, r| acc + r.tracking + r.fill_argb[0]), t.text_box, ); let _ = t.style_runs.iter().fold(0.0, |acc, r| { acc + r.leading.unwrap_or(0.0) + f64::from(u8::from(r.faux_bold)) + f64::from(u8::from(r.faux_italic)) + r.font_index as f64 }); let _ = t.style_runs.iter().map(|r| r.caps).collect::>(); let _ = t.paragraphs.iter().map(|p| p.justification).collect::>(); } for m in layer.markers() { let _ = (m.comment.len(), m.chapter.len(), m.url.len(), m.frame_target.len(), m.cue_point_name.len(), m.duration_seconds); let _ = (m.label, m.cue_type, m.cue_params.iter().map(|(k, v)| k.len() + v.len()).sum::()); } } fn exercise_comp(project: &Project<'_, '_>, comp: &Comp<'_, '_>) { let tb = comp.time_base(); let _ = tb.map(|t| (t.frame_rate(), t.seconds(i32::MIN), t.seconds(i32::MAX))); let _ = (comp.name(), comp.renderer(), comp.markers()); let mut size = (1920, 1080); if let Some(h) = comp.header() { size = (u32::from(h.width), u32::from(h.height)); let _ = (h.frame_rate(), h.frame_rate_fixed, h.duration_seconds(), h.work_area_start_units, h.work_area_end_units); let _ = (h.background, h.pixel_aspect.to_f64(), h.pixel_aspect.reduced(), h.shutter_angle, h.motion_blur); let _ = (h.shutter_phase, h.display_start.map(|r| r.to_f64())); } for layer in comp.layers() { exercise_layer(project, size, &layer); } } /// Touches every accessor of the typed view; none may panic on a damaged tree. fn exercise(file: &AepFile<'_>) { let Some(project) = Project::new(file) else { return }; for item in project.all_items() { let _ = (item.kind(), item.name(), item.id(), item.label(), item.footage(), item.display_name()); let _ = item.footage().map(|f| (f.duration, f.native_frame_rate, f.bits_per_pixel, f.file_size, f.audio_sample_rate, f.is_sequence)); let _ = item.id().and_then(|id| project.item_by_id(id)); if let Some(comp) = item.comp() { exercise_comp(&project, &comp); } } } /// The fixtures whose typed fields are decoded, one per kind of chunk body. const DECODED: &[&str] = &[ "000-base.aep", "010-layer-name.aep", "022-layer-parent-null.aep", "030-comp-size-1280x720.aep", "035-comp-workarea-1-2.aep", "039-comp-renderer-advanced3d.aep", "044-static-anchor-10-20.aep", "052-key-position-2key-ease.aep", "054-key-position-2key-spatial-bezier.aep", "056-key-position-3key-roving-key2.aep", "058-key-opacity-2key.aep", "060-expr-opacity-wiggle.aep", "071-fx-slider-value77.aep", "073-fx-color-0.2-0.6-1.aep", "077-fx-dropdown-default.aep", "080-fx-dropshadow-default.aep", "091-mask-rect-props.aep", "093-mask-path-2keys.aep", "113-text-Hello-size60-fillorange.aep", "122-marker-comp-1s.aep", "123-marker-layer-1s.aep", "124-precomp-Pre.aep", "126-footage-png-layer.aep", "202-key-out-hold-in-linear.aep", "204-key-ease-asymmetric.aep", "205-key-opacity-ease-asymmetric.aep", "206-key-scale-2key-linear.aep", "208-key-mask-feather-2key-linear.aep", "210-key-fill-color-2key-linear.aep", "211-anchor-100x100-solid-10-20.aep", "213-expr-opacity-wiggle-disabled.aep", "214-fx-gaussianblur-default.aep", "215-fx-fill-default.aep", "218-fx-point-111-222.aep", "220-fx-layercontrol-to-layer1.aep", "221-fx-slider-2key.aep", "222-layer-video-off.aep", "228-layer-blend-difference.aep", "229-comp-fps-29.97.aep", "232-mask-ellipse.aep", "233-mask-open-path.aep", "237-mask-mode-difference.aep", "239-shape-stroke-green-w8-round.aep", "240-shape-custom-path-tangents.aep", "241-shape-gradient-fill-linear.aep", "242-shape-group-position-50-50.aep", "245-text-box-400x200-Hello.aep", "248-text-range-fill-red-1-3.aep", "249-marker-layer-full.aep", "251-camera-one-node.aep", "252-light-point.aep", "253-camera-position-100-200-m1000.aep", "254-light-spot-intensity-50.aep", "259-solid-blue-0-0-1.aep", "300-fx-gaussianblur-0-explicit.aep", "305-key-scale-bezier-ease.aep", "306-key-maskfeather-bezier-ease.aep", "307-key-fillcolor-bezier.aep", "309-solid-100x100-mask-rect.aep", "310-solid-100x100-pointcontrol.aep", "317-layer-stretch150.aep", "318-layer-inpoint-0.3333.aep", "319-precomp-timeremap.aep", "321-fx-layercontrol-none.aep", "324-fx-dropdown-5items-select3.aep", "327-comp-displaystartframe-100.aep", "328-mask-two-ellipse-rect.aep", "329-mask-ellipse-mode-none.aep", "331-shape-polystar-polygon-5-100.aep", "332-shape-gradient-fill-radial.aep", "333-shape-group-two-rects.aep", "334-text-box-right-align.aep", "335-text-point-fauxbold.aep", "340-text-range-two-styles.aep", "342-camera-dof-800-30.aep", "343-light-spot-color-red.aep", "349-marker-layer-event-cue-params.aep", "351-footage-wav-as-layer.aep", "352-footage-pngseq-imported.aep", "354-footage-pngseq-timeremap.aep", "364-fx-tint-white-2key.aep", "382-fx-ramp-start-2key.aep", "383-fx-ramp-on-100x100-start-10-20.aep", "389-fx-exposure-default.aep", "395-fx-exposure-individual-red-1.aep", "397-fx-exposure-master-2key.aep", ]; #[test] fn typed_view_reads_every_undamaged_fixture() { for name in crate::container::helpers::FIXTURES { let bytes = fixture!(name); exercise(&parse(&bytes).unwrap()); } } #[test] fn typed_view_survives_damaged_files() { for (name, iterations) in [("000-base.aep", 1500), ("055-key-position-3key-inserted-t2.5.aep", 800), ("071-fx-slider-value77.aep", 800)] { let original = fixture!(name); let mut rng = Rng(0xfeed_beef_1234_5678 ^ original.len() as u64); for _ in 0..iterations { let mut b = original.clone(); for _ in 0..=rng.below(12) { let at = rng.below(b.len()); b[at] = rng.next() as u8; } if let Ok(file) = parse(&b) { exercise(&file); } } } } #[test] fn typed_view_survives_byte_flips_in_the_decoded_fixtures() { for name in DECODED { let original = fixture!(name); let mut rng = Rng(0x0dd_ba11_cafe_f00d ^ original.len() as u64); let (mut parsed, mut total) = (0, 0); for _ in 0..250 { let mut b = original.clone(); for _ in 0..=rng.below(16) { let at = rng.below(b.len()); b[at] = rng.next() as u8; } total += 1; if let Ok(file) = parse(&b) { parsed += 1; exercise(&file); } } // Flips that land inside bodies keep the file parseable; sizes that break the structure do not. assert!(parsed > 0 && parsed <= total, "{name}: {parsed} of {total}"); } } #[test] fn truncated_typed_files_never_panic() { // A cut inside the container is an error; a cut in the trailer still parses. Either way no panic. for name in DECODED { let original = fixture!(name); let declared = u32::from_be_bytes([original[4], original[5], original[6], original[7]]) as usize + 8; let mut rng = Rng(0x7e57_7e57 ^ original.len() as u64); for _ in 0..120 { let len = rng.below(original.len() + 1); if let Ok(file) = parse(&original[..len]) { assert!(len >= declared, "{name}: {len} < {declared}"); exercise(&file); } } // Also cut right before and after the declared end. for len in [declared - 1, declared, declared + 1] { if let Ok(file) = parse(&original[..len.min(original.len())]) { exercise(&file); } } } } /// The chunks whose bodies the typed view decodes. const DECODED_IDS: &[&[u8; 4]] = &[ b"ldta", b"cdta", b"prin", b"idta", b"idpi", b"tdb4", b"cdat", b"tdsb", b"lhd3", b"ldat", b"mkif", b"shph", b"parn", b"pard", b"sspc", b"opti", b"alas", b"Utf8", b"tdmn", b"NmHd", b"btdk", b"tdpi", ]; /// Applies a random mutation to a body. fn damage(body: &mut Vec, rng: &mut Rng) { match rng.below(7) { 0 => body.truncate(rng.below(body.len() + 1)), 1 => body.clear(), 2 => { for _ in 0..=rng.below(4) { if !body.is_empty() { let at = rng.below(body.len()); body[at] = rng.next() as u8; } } } 3 => { // A 4-byte window set to an extreme value (sizes, counts, offsets). if body.len() >= 4 { let at = rng.below(body.len() - 3); let v = [0xffff_ffffu32, 0x8000_0000, 0x7fff_ffff, 0, 1][rng.below(5)]; body[at..at + 4].copy_from_slice(&v.to_be_bytes()); } } 4 => body.extend((0..=rng.below(64)).map(|_| rng.next() as u8)), 5 => { // An 8-byte window set to NaN / infinity / a huge double. if body.len() >= 8 { let at = rng.below(body.len() - 7); let v = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e308, -1e308][rng.below(5)]; body[at..at + 8].copy_from_slice(&v.to_be_bytes()); } } _ => body.iter_mut().for_each(|b| *b = 0xff), } } fn damage_tree(chunks: &mut [Chunk<'static>], rng: &mut Rng, depth: usize) { if depth > 70 { return; } for chunk in chunks { let id = chunk.id; match &mut chunk.body { Body::Data(d) => { if DECODED_IDS.contains(&&id) && rng.below(12) == 0 { damage(d.to_mut(), rng); } } Body::OpaqueList { data, .. } => { if rng.below(3) == 0 { damage(data.to_mut(), rng); } } Body::Chunks(c) | Body::List { children: c, .. } => damage_tree(c, rng, depth + 1), } } } #[test] fn typed_view_survives_damaged_chunk_bodies() { // Damage the bodies the typed view decodes (not the container framing), directly in the tree: // wrong sizes, empty and truncated bodies, extreme counts, NaN doubles, unterminated text. for name in DECODED { let bytes = fixture!(name); let original = parse(&bytes).unwrap().into_owned(); let mut rng = Rng(0xbad_c0de_0001 ^ bytes.len() as u64); for _ in 0..120 { let mut file = original.clone(); damage_tree(&mut file.chunks, &mut rng, 0); exercise(&file); } } } #[test] fn extreme_values_in_the_decoded_headers() { use craft_aep::{CompHeader, LayerHeader, Rational, ShapePath}; // Every field at its maximum: no overflow, no division by zero, no panic. let h = LayerHeader::parse(&[0xff; 164]).unwrap(); let _ = (h.in_point(), h.out_point(), h.start.to_f64(), h.stretch_ratio()); let mut zero_den = [0u8; 164]; zero_den[8..12].copy_from_slice(&u32::MAX.to_be_bytes()); zero_den[12..16].copy_from_slice(&i32::MIN.to_be_bytes()); let h = LayerHeader::parse(&zero_den).unwrap(); assert_eq!((h.in_point(), h.out_point(), h.start.to_f64()), (None, None, None)); let mut wide = [0u8; 164]; wide[8..12].copy_from_slice(&u32::MAX.to_be_bytes()); wide[12..16].copy_from_slice(&i32::MIN.to_be_bytes()); wide[16..20].copy_from_slice(&u32::MAX.to_be_bytes()); wide[20..24].copy_from_slice(&i32::MAX.to_be_bytes()); wide[24..28].copy_from_slice(&u32::MAX.to_be_bytes()); let h = LayerHeader::parse(&wide).unwrap(); let _ = h.in_point(); let c = CompHeader::parse(&[0xff; 204]).unwrap(); assert_eq!((c.duration_seconds(), c.work_area_end_units), (None, None)); let _ = (c.frame_rate(), c.pixel_aspect.to_f64()); let zero = CompHeader::parse(&[0; 204]).unwrap(); assert_eq!((zero.frame_rate(), zero.pixel_aspect.to_f64(), zero.duration_seconds()), (None, None, None)); let all = LayerHeader::parse(&[0xff; 164]).unwrap(); let _ = (all.is_enabled(), all.is_auto_oriented(), all.light_type(), all.blend_mode.code()); assert_eq!(LayerHeader::parse(&[0u8; 164]).unwrap().light_type(), None); assert_eq!(Rational { num: i64::MAX, den: i64::MIN }.reduced(), None); assert_eq!(Rational { num: i64::MIN, den: i64::MIN }.reduced(), Rational::new(1, 1)); // A path whose point count is not a multiple of three, and non-finite box and scale. let path = ShapePath { bbox: [f32::NAN, f32::INFINITY, f32::MIN, f32::MAX], closed_flag: 9, open: true, points: vec![[f32::NAN, 0.0]; 7] }; assert_eq!(path.vertex_count(), 2); assert_eq!(path.vertices((f64::NAN, f64::MAX)).len(), 2); } /// Calls `f` on the body of every data chunk with this id, anywhere in the tree. fn for_each_body(chunks: &mut [Chunk<'static>], id: &[u8; 4], f: &mut dyn FnMut(&mut Vec), depth: usize) { if depth > 70 { return; } for chunk in chunks { let this = chunk.id; match &mut chunk.body { Body::Data(d) if this == *id => f(d.to_mut()), Body::Data(_) | Body::OpaqueList { .. } => {} Body::Chunks(c) | Body::List { children: c, .. } => for_each_body(c, id, f, depth + 1), } } } #[test] fn hostile_keyframe_layouts_never_panic() { // The property header selects the record template: every component count (0..=255) with the // template flag at 0, 1 and 0xff, over keyframe lists of every record size in these fixtures. for name in [ "204-key-ease-asymmetric.aep", "206-key-scale-2key-linear.aep", "208-key-mask-feather-2key-linear.aep", "210-key-fill-color-2key-linear.aep", "093-mask-path-2keys.aep", "122-marker-comp-1s.aep", "305-key-scale-bezier-ease.aep", "306-key-maskfeather-bezier-ease.aep", "307-key-fillcolor-bezier.aep", "319-precomp-timeremap.aep", ] { let bytes = fixture!(name); let original = parse(&bytes).unwrap().into_owned(); for components in 0..=255u8 { for template in [0u8, 1, 0xff] { let mut file = original.clone(); for_each_body( &mut file.chunks, b"tdb4", &mut |body| { if body.len() > 7 { body[3] = components; body[7] = template; } }, 0, ); exercise(&file); } } } } #[test] fn hostile_text_dictionaries_and_marker_headers_never_panic() { // Damage the marker header (NmHd), the strings and the layer reference in many ways, and the // opaque text lists. for name in [ "245-text-box-400x200-Hello.aep", "248-text-range-fill-red-1-3.aep", "249-marker-layer-full.aep", "220-fx-layercontrol-to-layer1.aep", "334-text-box-right-align.aep", "335-text-point-fauxbold.aep", "340-text-range-two-styles.aep", "349-marker-layer-event-cue-params.aep", "321-fx-layercontrol-none.aep", "324-fx-dropdown-5items-select3.aep", "352-footage-pngseq-imported.aep", "350-footage-wav-imported.aep", ] { let bytes = fixture!(name); let original = parse(&bytes).unwrap().into_owned(); let mut rng = Rng(0x07e4_70dd_ba11 ^ bytes.len() as u64); for _ in 0..300 { let mut file = original.clone(); for id in [b"NmHd", b"Utf8", b"tdpi"] { for_each_body(&mut file.chunks, id, &mut |body| damage(body, &mut rng), 0); } damage_tree(&mut file.chunks, &mut rng, 0); exercise(&file); } } } #[test] fn extreme_values_in_the_batch_3_fields() { use craft_aep::{CompHeader, LayerHeader}; // Time stretch: zero and maximal numerators and denominators (bytes 8 and 108), with extreme times. for (num, den) in [(0u32, 0u32), (1, 0), (0, 1), (u32::MAX, 1), (1, u32::MAX), (u32::MAX, u32::MAX), (3, 2)] { let mut ldta = [0u8; 164]; ldta[8..12].copy_from_slice(&num.to_be_bytes()); ldta[108..112].copy_from_slice(&den.to_be_bytes()); ldta[12..16].copy_from_slice(&i32::MIN.to_be_bytes()); ldta[16..20].copy_from_slice(&1u32.to_be_bytes()); ldta[20..24].copy_from_slice(&i32::MAX.to_be_bytes()); ldta[24..28].copy_from_slice(&u32::MAX.to_be_bytes()); ldta[28..32].copy_from_slice(&i32::MIN.to_be_bytes()); ldta[32..36].copy_from_slice(&1u32.to_be_bytes()); let h = LayerHeader::parse(&ldta).unwrap(); let _ = (h.stretch_ratio(), h.in_point(), h.out_point()); if den == 0 { assert_eq!((h.stretch_ratio(), h.in_point(), h.out_point()), (None, None, None)); } } // Display start and shutter phase from extreme bytes; a missing tail leaves them unknown. for fill in [0x00u8, 0x7f, 0x80, 0xff] { let c = CompHeader::parse(&[fill; 204]).unwrap(); let _ = (c.shutter_phase, c.display_start.map(|r| r.to_f64())); } // A zero denominator has no value. assert_eq!(CompHeader::parse(&[0; 204]).unwrap().display_start.and_then(|r| r.to_f64()), None); }