Files
craft-libs/crates/aep/tests/it/container/robustness.rs
Dylan M. Taylorandvangel76 4a7024cd69 craft-aep: flat read-only tree index and the Pard decoder (ports of effectcraft#3302 and #3319) (#948)
* craft-aep: a flat read-only tree index, borrowed text documents, fewer allocations

The container part of storytold/effectcraft#3302:

- `TreeIndex`: the chunk tree as one flat array in depth-first order,
  borrowed from the input, for readers that only look. Same chunks counted
  against the same limits and the same errors as `parse`. An `.aepx` is
  indexed by `index_aepx` into one buffer. The typed view reads through
  either tree (`Node`, `Root`, `Project::from_index`).
- Chunk parser: each list of siblings is counted, then reserved once.
  `.aepx`: hex decoded into an output sized once, attribute values without
  an entity borrowed. `write` reserves the exact output size.
- Text documents are parsed into a tree that borrows from the body; the
  style and paragraph defaults are built once.
- Layer markers and Source Text are read where they are stored instead of
  searched for.
- Container accessors that only tests used are removed.

What the importer and exporter in EffectCraft reach across the crate
boundary is public here: `Root`, `Draft`, `AepxDrafts`, `index_aepx`,
`TreeIndex::from_drafts`, `Project::of`, `ProjectSettings::of`,
`Node::marker_list`, `Root::file_version` and `saved_by_version`,
`StyleRun::SAMPLED`. `TimeBase::NTSC` holds the rates as plain
numerator and denominator instead of an EffectCraft type.

* craft-aep: one decoder for an effect parameter definition (`Pard`)

The container part of storytold/effectcraft#3319: the fields of a `pard`
record (type code, name, default, valid range, slider range, popup item
count) are read by one type, `Pard`, which `EffectParam` now reads
through. It is public so that an exporter's tests can read back what
they encode instead of keeping a second decoder.

The rest of that PR (import and export bounds, loss reports) is in
EffectCraft's importer and exporter.

---------

Co-authored-by: vangel76 <vangel1976@googlemail.com>
2026-11-11 19:01:12 -05:00

711 lines
28 KiB
Rust

//! 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::<Vec<_>>();
let _ = t.paragraphs.iter().map(|p| p.justification).collect::<Vec<_>>();
}
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::<usize>());
}
}
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<u8>, 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<u8>), 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);
}