From 2e24ae47a1a72fa925fe6339d4317f7821f3650e Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 8 Nov 2026 16:21:57 +0100 Subject: [PATCH] aep export: the time remap and Transform keys use the shared key-list and match-name helpers; doc comments for the audit's functions (#3300) --- crates/aep/src/container/aepx.rs | 5 ++ crates/aep/src/container/effects.rs | 1 + crates/aep/src/container/index.rs | 23 ++++++ crates/aep/src/container/read.rs | 4 + crates/aep/src/container/settings.rs | 1 + crates/aep/src/container/textdoc.rs | 35 ++++++++ crates/aep/src/container/typed.rs | 2 + crates/aep/src/container/write.rs | 3 + crates/aep/src/export/base.rs | 2 + crates/aep/src/export/emit.rs | 82 +++++++------------ crates/aep/src/export/fxsynth.rs | 1 + crates/aep/src/export/keys.rs | 3 + crates/aep/src/export/masks.rs | 3 + crates/aep/src/export/report.rs | 3 + crates/aep/src/export/synth.rs | 4 + crates/aep/src/export/tree.rs | 4 + crates/aep/src/import.rs | 2 + crates/aep/src/import/report.rs | 3 + .../aep/tests/it/container/batch3_objects.rs | 1 + .../aep/tests/it/container/layer_switches.rs | 1 + crates/aep/tests/it/container/tree_index.rs | 5 ++ crates/aep/tests/it/export/built.rs | 1 + 22 files changed, 136 insertions(+), 53 deletions(-) diff --git a/crates/aep/src/container/aepx.rs b/crates/aep/src/container/aepx.rs index e79b904d1..6ca729807 100644 --- a/crates/aep/src/container/aepx.rs +++ b/crates/aep/src/container/aepx.rs @@ -658,6 +658,7 @@ mod tests { assert!(matches!(read_aepx(b"\xff"), Err(AepError::Xml { .. }))); } + /// Attribute values decode entities only where they have them. #[test] fn attribute_values_decode_entities_only_where_they_have_them() { // An entity inside `bdata` still decodes before the hex does. @@ -672,6 +673,7 @@ mod tests { assert_eq!(f.chunks[0].bytes(), Some(&[0x0a, 0xff][..])); } + /// A large bdata with a bad digit is refused before it is sized. #[test] fn a_large_bdata_with_a_bad_digit_is_refused_before_it_is_sized() { // 10 MB of hex that is bad from the first pair (and once more at the very end). @@ -685,6 +687,7 @@ mod tests { assert!(matches!(read_aepx(x.as_bytes()), Err(AepError::Xml { .. }))); } + /// Bdata decodes up to the size limit and not past it. #[test] fn bdata_decodes_up_to_the_size_limit_and_not_past_it() { assert_eq!(decode_hex_at_most(b"00ff10", 0, 3), Ok(vec![0x00, 0xff, 0x10])); @@ -697,8 +700,10 @@ mod tests { assert_eq!(MAX_BDATA as u64, u64::from(u32::MAX)); } + /// Decode hex matches the collecting version. #[test] fn decode_hex_matches_the_collecting_version() { + /// The collecting decoder this one replaced, as the reference. fn old(text: &[u8], offset: usize) -> Result, AepError> { if text.len() % 2 == 1 { return Err(xml(offset, "bdata has an odd number of hex digits")); diff --git a/crates/aep/src/container/effects.rs b/crates/aep/src/container/effects.rs index 09a5b1ed6..6ccfa3af6 100644 --- a/crates/aep/src/container/effects.rs +++ b/crates/aep/src/container/effects.rs @@ -293,6 +293,7 @@ impl<'f, 'a> Effect<'f, 'a> { } } +/// True when a `LIST parT` holds parameter definitions (a `pard`). fn has_pard(part: Node<'_, '_>) -> bool { part.children().any(|c| c.is(b"pard")) } diff --git a/crates/aep/src/container/index.rs b/crates/aep/src/container/index.rs index cb9621e94..30d7fcb9d 100644 --- a/crates/aep/src/container/index.rs +++ b/crates/aep/src/container/index.rs @@ -57,6 +57,7 @@ struct Entry<'a> { } impl<'a> Entry<'a> { + /// An entry for a chunk whose body is not read yet. fn placeholder(id: [u8; 4]) -> Self { Self { data: &[], id, list_type: [0; 4], end: 0, kind: Kind::Data, pad: None } } @@ -70,6 +71,7 @@ pub struct TreeIndex<'a> { } impl fmt::Debug for TreeIndex<'_> { + /// The sizes only: the chunk count and the trailer length. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TreeIndex").field("chunks", &self.nodes.len()).field("trailer", &self.trailer.len()).finish() } @@ -196,6 +198,7 @@ impl<'a> TreeIndex<'a> { AepFile { chunks: self.build(0, u32::try_from(self.nodes.len()).unwrap_or(u32::MAX)), trailer: Cow::Borrowed(self.trailer) } } + /// The entry at `at`, `None` past the end. fn entry(&self, at: u32) -> Option<&Entry<'a>> { self.nodes.get(usize::try_from(at).ok()?) } @@ -332,10 +335,12 @@ impl<'a> Builder<'a> { } } + /// The input bytes `start..end` (empty when out of range). fn slice(&self, start: usize, end: usize) -> &'a [u8] { self.data.get(start..end).unwrap_or(&[]) } + /// Fills in the body of the entry at `at` once the parser knows what it is. fn set(&mut self, at: usize, kind: Kind, data: &'a [u8], list_type: [u8; 4]) { if let Some(e) = self.nodes.get_mut(at) { e.kind = kind; @@ -382,6 +387,7 @@ enum NodeRepr<'f, 'a> { } impl<'f, 'a> From<&'f Chunk<'a>> for Node<'f, 'a> { + /// The handle for this value. fn from(chunk: &'f Chunk<'a>) -> Self { Node(NodeRepr::Chunk(chunk)) } @@ -390,6 +396,7 @@ impl<'f, 'a> From<&'f Chunk<'a>> for Node<'f, 'a> { /// Prints the chunk as [`Chunk`]'s `Debug` does, whichever tree it is in, without copying the /// subtree. impl fmt::Debug for Node<'_, '_> { + /// The chunk as `Chunk`'s `Debug` prints it. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { NodeRepr::Chunk(c) => fmt::Debug::fmt(c, f), @@ -405,6 +412,7 @@ struct BodyDebug<'f, 'a>(Node<'f, 'a>); struct ChildrenDebug<'f, 'a>(Node<'f, 'a>); impl fmt::Debug for BodyDebug<'_, '_> { + /// The body as `Body`'s `Debug` prints it. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Some(e) = self.0.entry() else { return f.write_str("?") }; match e.kind { @@ -417,12 +425,14 @@ impl fmt::Debug for BodyDebug<'_, '_> { } impl fmt::Debug for ChildrenDebug<'_, '_> { + /// The children as a list. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_list().entries(self.0.children()).finish() } } impl<'f, 'a> Node<'f, 'a> { + /// The entry at `at`, `None` past the end. fn entry(self) -> Option<&'f Entry<'a>> { match self.0 { NodeRepr::Index(tree, at) => tree.entry(at), @@ -570,6 +580,7 @@ enum NodesRepr<'f, 'a> { impl<'f, 'a> Iterator for Nodes<'f, 'a> { type Item = Node<'f, 'a>; + /// The next item, `None` at the end of the range. fn next(&mut self) -> Option { match &mut self.0 { NodesRepr::Slice(it) => it.next().map(Node::from), @@ -605,6 +616,7 @@ enum WalkRepr<'f, 'a> { impl<'f, 'a> Iterator for NodeWalk<'f, 'a> { type Item = (usize, Node<'f, 'a>); + /// The next item, `None` at the end of the range. fn next(&mut self) -> Option { match &mut self.0 { WalkRepr::Chunk(w) => w.next().map(|(d, c)| (d, Node::from(c))), @@ -637,12 +649,14 @@ pub(crate) enum Root<'f, 'a> { } impl<'f, 'a> From<&'f AepFile<'a>> for Root<'f, 'a> { + /// The handle for this value. fn from(file: &'f AepFile<'a>) -> Self { Root::File(file) } } impl<'f, 'a> From<&'f TreeIndex<'a>> for Root<'f, 'a> { + /// The handle for this value. fn from(index: &'f TreeIndex<'a>) -> Self { Root::Index(index) } @@ -677,6 +691,7 @@ mod tests { struct Rng(u64); impl Rng { + /// The next pseudo-random number (xorshift64*). fn next(&mut self) -> u64 { let mut x = self.0; x ^= x >> 12; @@ -686,6 +701,7 @@ mod tests { x.wrapping_mul(0x2545_f491_4f6c_dd1d) } + /// A number below `n` (0 for `n == 0`). fn below(&mut self, n: u64) -> u64 { if n == 0 { 0 } else { self.next() % n } } @@ -745,6 +761,7 @@ mod tests { } } + /// The default limits and four tight ones (depth 1 and 3, 7 and 40 chunks). fn limits() -> [Limits; 5] { [ Limits::default(), @@ -755,6 +772,7 @@ mod tests { ] } + /// The index matches the chunk parser on generated and damaged trees. #[test] fn the_index_matches_the_chunk_parser_on_generated_and_damaged_trees() { let mut rng = Rng(0x9e37_79b9_7f4a_7c15); @@ -785,6 +803,7 @@ mod tests { } } + /// Header errors are the same. #[test] fn header_errors_are_the_same() { for bytes in [&b""[..], b"RIFX", b"RIFX\0\0\0\x04Egg", b"RIFX\0\0\0\x04Fnord", b"RIFX\0\0\0\x08Egg!", b"RIFX\0\0\0\x03Egg!", b"RIFF\0\0\0\x04Egg!"] { @@ -824,6 +843,7 @@ mod tests { } } + /// The index never reserves more than the input or the limit allows. #[test] fn the_index_never_reserves_more_than_the_input_or_the_limit_allows() { // 1000 empty headers: the most chunks the bytes can hold. @@ -875,6 +895,7 @@ mod tests { data } + /// The index grows with the chunk count not the byte size. #[test] fn the_index_grows_with_the_chunk_count_not_the_byte_size() { // 4 MB in 4 chunks: a few slots, not one per 32 bytes (that would be 131 072 slots, 4 MB). @@ -903,6 +924,7 @@ mod tests { out } + /// Failed attempts count once and leave nothing behind. #[test] fn failed_attempts_count_once_and_leave_nothing_behind() { let mut wide = b"Fold".to_vec(); @@ -928,6 +950,7 @@ mod tests { same(&data, Limits::default()); } + /// Depth beyond the limit stays opaque without recursion past it. #[test] fn depth_beyond_the_limit_stays_opaque_without_recursion_past_it() { // 5000 nested lists: far deeper than the limit; everything below depth 64 stays bytes. diff --git a/crates/aep/src/container/read.rs b/crates/aep/src/container/read.rs index a47d06684..200b3d84a 100644 --- a/crates/aep/src/container/read.rs +++ b/crates/aep/src/container/read.rs @@ -63,6 +63,7 @@ pub(super) fn container_end(data: &[u8]) -> Result { usize::try_from(declared).map_err(|_| AepError::TooLarge)?.checked_add(8).ok_or(AepError::TooLarge) } +/// The 4-byte id at `at`, `None` when the input is too short. pub(super) fn id4(data: &[u8], at: usize) -> Option<[u8; 4]> { array::<4>(data, at) } @@ -183,6 +184,7 @@ mod tests { } } + /// Every sibling list is allocated once at its exact length. #[test] fn every_sibling_list_is_allocated_once_at_its_exact_length() { let inner: Vec> = (0..37u8).map(|i| Chunk::data(*b"cdat", vec![i; usize::from(i % 5)])).collect(); @@ -201,6 +203,7 @@ mod tests { } } + /// The preallocation is bounded by the input and the chunk limit. #[test] fn the_preallocation_is_bounded_by_the_input_and_the_chunk_limit() { // 1000 empty headers: the most chunks the bytes can hold. @@ -245,6 +248,7 @@ mod tests { out } + /// Nested lists with broken tails parse each header once. #[test] fn nested_lists_with_broken_tails_parse_each_header_once() { // A wide, well-formed list at the bottom, then 50 levels that each fail and fall back to diff --git a/crates/aep/src/container/settings.rs b/crates/aep/src/container/settings.rs index 3079d5c7d..a443df874 100644 --- a/crates/aep/src/container/settings.rs +++ b/crates/aep/src/container/settings.rs @@ -64,6 +64,7 @@ impl ProjectSettings { Self::of(Root::from(index)) } + /// Reads the settings from the top-level chunks of either tree. pub(crate) fn of(file: Root<'_, '_>) -> ProjectSettings { let nhed = file.chunk(b"nhed").and_then(Node::bytes); let byte = |i: usize| nhed.and_then(|b| b.get(i).copied()); diff --git a/crates/aep/src/container/textdoc.rs b/crates/aep/src/container/textdoc.rs index 2de5d8eb2..e673e63cf 100644 --- a/crates/aep/src/container/textdoc.rs +++ b/crates/aep/src/container/textdoc.rs @@ -184,29 +184,35 @@ struct Parser<'b> { items: Vec>, } +/// Whitespace between tokens. fn is_ws(c: u8) -> bool { matches!(c, b' ' | b'\t' | b'\r' | b'\n') } +/// A byte that ends a bare word. fn is_delimiter(c: u8) -> bool { is_ws(c) || matches!(c, b'/' | b'[' | b']' | b'<' | b'>' | b'(' | b')') } impl<'b> Parser<'b> { + /// The next byte, `None` at the end. fn peek(&self) -> Option { self.b.get(self.i).copied() } + /// Moves past whitespace. fn skip_ws(&mut self) { while self.peek().is_some_and(is_ws) { self.i = self.i.saturating_add(1); } } + /// True when the unread bytes start with `s`. fn starts_with(&self, s: &[u8]) -> bool { self.b.get(self.i..).is_some_and(|rest| rest.starts_with(s)) } + /// The bare word at the position (up to the next delimiter), borrowed from the body. fn word(&mut self) -> &'b [u8] { let start = self.i; while self.peek().is_some_and(|c| !is_delimiter(c)) { @@ -242,6 +248,7 @@ impl<'b> Parser<'b> { } } + /// The value at the position, `None` when it is malformed or nested deeper than the limit. fn parse(&mut self, depth: usize) -> Option> { self.skip_ws(); if depth > MAX_DEPTH { @@ -322,6 +329,7 @@ impl<'b> Parser<'b> { } } +/// A big-endian UTF-16 string as text (unpaired surrogates become U+FFFD). fn decode_utf16be(bytes: &[u8]) -> String { let (pairs, rest) = bytes.as_chunks::<2>(); let units = pairs.iter().map(|p| u16::from_be_bytes(*p)); @@ -369,10 +377,12 @@ fn val(v: f64) -> V<'static> { V::Float(v) } +/// A dictionary value with these keys, for the built-in defaults. fn dict(pairs: Vec<(&'static str, V<'static>)>) -> V<'static> { V::Dict(pairs.into_iter().map(|(k, v)| (Cow::Borrowed(k), v)).collect()) } +/// An array of numbers, for the built-in defaults. fn numbers(v: &[f64]) -> V<'static> { V::Array(v.iter().map(|x| val(*x)).collect()) } @@ -774,11 +784,13 @@ pub struct TextBox { pub height: f64, } +/// The RGBA colour in a style slot (`slot/0/1`), `None` when it is not four numbers. fn colour_of(style: &V<'_>, slot: &str) -> Option<[f64; 4]> { let c = style.get(slot)?.get("0")?.get("1")?; Some([c.at(0)?.as_f64()?, c.at(1)?.as_f64()?, c.at(2)?.as_f64()?, c.at(3)?.as_f64()?]) } +/// A non-negative integer value as `usize`. fn usize_of(v: &V<'_>) -> Option { usize::try_from(v.as_i64()?).ok() } @@ -1203,6 +1215,7 @@ mod oracle { } impl TextValue { + /// The canonical text of a key, as the owning parser built it. fn key_text(&self) -> String { match self { Self::Name(s) | Self::Str(s) | Self::Token(s) => s.clone(), @@ -1220,29 +1233,35 @@ mod oracle { i: usize, } + /// Whitespace between tokens. fn is_ws(c: u8) -> bool { matches!(c, b' ' | b'\t' | b'\r' | b'\n') } + /// A byte that ends a bare word. fn is_delimiter(c: u8) -> bool { is_ws(c) || matches!(c, b'/' | b'[' | b']' | b'<' | b'>' | b'(' | b')') } impl Parser<'_> { + /// The next byte, `None` at the end. fn peek(&self) -> Option { self.b.get(self.i).copied() } + /// Moves past whitespace. fn skip_ws(&mut self) { while self.peek().is_some_and(is_ws) { self.i = self.i.saturating_add(1); } } + /// True when the unread bytes start with `s`. fn starts_with(&self, s: &[u8]) -> bool { self.b.get(self.i..).is_some_and(|rest| rest.starts_with(s)) } + /// The bare word at the position (up to the next delimiter), borrowed from the body. fn word(&mut self) -> &[u8] { let start = self.i; while self.peek().is_some_and(|c| !is_delimiter(c)) { @@ -1251,6 +1270,7 @@ mod oracle { self.b.get(start..self.i).unwrap_or(&[]) } + /// The value at the position, `None` when it is malformed or nested deeper than the limit. fn parse(&mut self, depth: usize) -> Option { self.skip_ws(); if depth > MAX_DEPTH { @@ -1332,6 +1352,7 @@ mod oracle { } } + /// A big-endian UTF-16 string as text (unpaired surrogates become U+FFFD). fn decode_utf16be(bytes: &[u8]) -> String { let (pairs, rest) = bytes.as_chunks::<2>(); let units = pairs.iter().map(|p| u16::from_be_bytes(*p)); @@ -1364,10 +1385,12 @@ mod oracle { TextValue::Float(v) } + /// A dictionary value with these keys, for the built-in defaults. fn dict(pairs: Vec<(&str, TextValue)>) -> TextValue { TextValue::Dict(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect()) } + /// An array of numbers, for the built-in defaults. fn numbers(v: &[f64]) -> TextValue { TextValue::Array(v.iter().map(|x| val(*x)).collect()) } @@ -1434,6 +1457,7 @@ mod tests { use super::*; use crate::container::Chunk; + /// A `LIST` of this type holding `kids`. fn list(t: &[u8; 4], kids: Vec>) -> Chunk<'static> { Chunk::list(*t, kids) } @@ -1451,6 +1475,7 @@ mod tests { struct Rng(u64); impl Rng { + /// The next pseudo-random number (xorshift). fn next(&mut self) -> u64 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 7; @@ -1458,11 +1483,13 @@ mod tests { self.0 } + /// A number below `n` (0 for `n == 0`). fn below(&mut self, n: usize) -> usize { usize::try_from(self.next() % (n as u64)).unwrap_or(0) } } + /// The borrowing parser reads what the owning one read. #[test] fn the_borrowing_parser_reads_what_the_owning_one_read() { // Every key form the owned keys canonicalise (`06`, `+6`, `-0`, `1.0`, `1e2`, strings, @@ -1515,6 +1542,7 @@ mod tests { b")", ]; // A value: mostly well formed, with a random part in its place now and then. + /// Appends one generated value to `out`: mostly well formed, now and then a random part. fn value(rng: &mut Rng, parts: &[&[u8]], depth: usize, out: &mut Vec) { let pick = |rng: &mut Rng| parts.get(rng.below(parts.len())).copied().unwrap_or_default(); match rng.below(if depth > 6 { 5 } else { 10 }) { @@ -1597,6 +1625,7 @@ mod tests { } } + /// A realistic document is read in full. #[test] fn a_realistic_document_is_read_in_full() { let doc = TextDocument::parse(DOC).expect("a document"); @@ -1612,6 +1641,7 @@ mod tests { assert_eq!(doc.line_orientation, Some(2)); } + /// The defaults are the sampled ones and built once. #[test] fn the_defaults_are_the_sampled_ones_and_built_once() { assert_eq!(STYLE_DEFAULTS.clone().into_owned(), oracle::style_defaults()); @@ -1619,6 +1649,7 @@ mod tests { assert!(std::ptr::eq(&*STYLE_DEFAULTS, &*STYLE_DEFAULTS)); } + /// Names keys and strings are borrowed from the body. #[test] fn names_keys_and_strings_are_borrowed_from_the_body() { let V::Dict(pairs) = parse_dict(b"/a /b /c (\xfe\xff\0x) /d (plain) /e word /f 1.5 /g (\\(esc) 07 1") else { panic!("a dict") }; @@ -1637,6 +1668,7 @@ mod tests { assert!(matches!(outer.get(1).map(|p| &p.1), Some(V::Array(a)) if a.len() == 9 && a.capacity() >= a.len())); } + /// Slot checks agree with the owned ones. #[test] fn slot_checks_agree_with_the_owned_ones() { // Slots: defaults, decoded, neutral-listed and unknown ones. Values: each default as @@ -1710,6 +1742,7 @@ mod tests { assert!(listed > 10_000, "{listed} slots listed"); } + /// Values compare by their text. #[test] fn values_compare_by_their_text() { // Two names that are not UTF-8 read as the same text with replacement characters, as the @@ -1721,6 +1754,7 @@ mod tests { assert!(is_neutral(&V::Name(b"nil")) && !is_neutral(&V::Token(b"nil")) && is_neutral(&V::Float(0.0)) && !is_neutral(&V::Str(Cow::Borrowed(b"")))); } + /// A layers marker list is the one in its root group. #[test] fn a_layers_marker_list_is_the_one_in_its_root_group() { let marked = |tag: u8| list(b"mrst", vec![Chunk::data(*b"test", vec![tag])]); @@ -1737,6 +1771,7 @@ mod tests { assert_eq!(Node::from(&secl).marker_list().and_then(|m| m.child(b"test")).and_then(Node::bytes), Some(&[3u8][..])); } + /// The source text record is found in the text group. #[test] fn the_source_text_record_is_found_in_the_text_group() { let doc = b"/1 << /1 [ << /0 << /0 (\xfe\xff\0H\0\r) /6 << /0 [ << /0 << /0 << /6 << /0 0 /1 50 /53 << /0 << /1 [ 1 1 0 0 ] >> >> >> >> >> /1 2 >> ] >> >> >> ] >>"; diff --git a/crates/aep/src/container/typed.rs b/crates/aep/src/container/typed.rs index 8cd35aced..7e7bfd0ec 100644 --- a/crates/aep/src/container/typed.rs +++ b/crates/aep/src/container/typed.rs @@ -46,6 +46,7 @@ impl<'f, 'a> Project<'f, 'a> { Self::of(Root::from(index)) } + /// The project view of either tree: its `LIST Fold` and `LIST EfdG`. pub(crate) fn of(root: Root<'f, 'a>) -> Option { root.list(b"Fold").map(|fold| Self { fold, effect_defs: root.list(b"EfdG") }) } @@ -572,6 +573,7 @@ mod tests { assert_eq!(g.names(), ["A", "B", "A"]); } + /// Match name is agrees with comparing the decoded name. #[test] fn match_name_is_agrees_with_comparing_the_decoded_name() { let bodies: [&[u8]; 9] = diff --git a/crates/aep/src/container/write.rs b/crates/aep/src/container/write.rs index 35a5b6276..9f401b3d4 100644 --- a/crates/aep/src/container/write.rs +++ b/crates/aep/src/container/write.rs @@ -125,6 +125,7 @@ mod tests { use super::*; + /// The output is sized once to its exact length. #[test] fn the_output_is_sized_once_to_its_exact_length() { let odd = |n: u8| Chunk::data(*b"cdat", vec![n; usize::from(n)]); @@ -151,6 +152,7 @@ mod tests { } } + /// A tree past the container size fails before serialising. #[cfg(target_pointer_width = "64")] #[test] fn a_tree_past_the_container_size_fails_before_serialising() { @@ -163,6 +165,7 @@ mod tests { assert!(sequence_len(&file.chunks[..4000], 1).is_some_and(|n| u32::try_from(n + 4).is_ok())); } + /// A tree past the depth limit has no size. #[test] fn a_tree_past_the_depth_limit_has_no_size() { let mut deep = Chunk::data(*b"cdat", vec![0]); diff --git a/crates/aep/src/export/base.rs b/crates/aep/src/export/base.rs index dfaca7321..05aa15891 100644 --- a/crates/aep/src/export/base.rs +++ b/crates/aep/src/export/base.rs @@ -15,6 +15,7 @@ pub(crate) fn bytes(len: usize, fields: &[(usize, &[u8])]) -> Vec { layout(len, fields).finish(|| format!("base::bytes ({len} bytes, fields at {:?})", fields.iter().map(|(at, v)| (*at, v.len())).collect::>())) } +/// The fixed layout of `len` zero bytes with `fields` written at their offsets. fn layout(len: usize, fields: &[(usize, &[u8])]) -> Fixed { let mut b = Fixed::zeroed(len); for (at, v) in fields { @@ -280,6 +281,7 @@ pub(crate) fn solid_item() -> Chunk<'static> { mod tests { use super::*; + /// A field out of range is an error. #[test] fn a_field_out_of_range_is_an_error() { let ok = layout(4, &[(0, &[1]), (3, &[2])]); diff --git a/crates/aep/src/export/emit.rs b/crates/aep/src/export/emit.rs index 08ab82904..f7b0ec6b8 100644 --- a/crates/aep/src/export/emit.rs +++ b/crates/aep/src/export/emit.rs @@ -6,7 +6,9 @@ use std::collections::HashMap; use crate::{AepFile, Chunk}; use effectcraft_project::{ItemId, LayerId}; +use crate::export::keys::fill_key_list; use crate::export::plan::{CompPlan, Entry, ItemPlan, KeyPlan, LayerPlan, LayerSrc, ProjectPlan, PropKind, PropPlan, SolidPlan, natural_key}; +use crate::export::synth::{is_tdmn, tdmn_name}; use crate::export::template::{self, Template}; use crate::export::tree::{ R, child_data, data_mut, kids_mut, list_mut, put, put_bits, put_cstr, put_f32, put_f64, put_i32, put_u8, put_u16, put_u32, set_utf8, tpl, @@ -437,7 +439,7 @@ fn put_used_comps(cx: &Cx<'_>, item: &mut Chunk<'static>, cp: &CompPlan) -> R<() fn put_time_remap_max(layr: &mut Chunk<'static>, seconds: f64) -> R<()> { let root = list_mut(layr, b"tdgp")?; let kids = kids_mut(root)?; - let at = kids.iter().position(|c| c.is(b"tdmn") && tdmn_is(c, "ADBE Time Remapping")).ok_or_else(|| tpl("layer without Time Remapping"))?; + let at = kids.iter().position(|c| is_tdmn(c, "ADBE Time Remapping")).ok_or_else(|| tpl("layer without Time Remapping"))?; let tdbs = kids.get_mut(at + 1).ok_or_else(|| tpl("Time Remapping without a record"))?; put_f64(child_data(tdbs, b"tduM")?, 0, seconds) } @@ -452,7 +454,7 @@ fn put_time_remap_max(layr: &mut Chunk<'static>, seconds: f64) -> R<()> { fn put_time_remap(cx: &Cx<'_>, layr: &mut Chunk<'static>, remap: &PropPlan) -> R<()> { let root = list_mut(layr, b"tdgp")?; let kids = kids_mut(root)?; - let at = kids.iter().position(|c| c.is(b"tdmn") && tdmn_is(c, "ADBE Time Remapping")).ok_or_else(|| tpl("layer without Time Remapping"))?; + let at = kids.iter().position(|c| is_tdmn(c, "ADBE Time Remapping")).ok_or_else(|| tpl("layer without Time Remapping"))?; let tdbs = kids.get_mut(at + 1).ok_or_else(|| tpl("Time Remapping without a record"))?; put_u32(child_data(tdbs, b"tdsb")?, 0, 1)?; { @@ -490,7 +492,7 @@ fn put_time_remap(cx: &Cx<'_>, layr: &mut Chunk<'static>, remap: &PropPlan) -> R for k in &remap.keys { ldat.extend(plain_record(1, k, size)?); } - set_key_list(&mut list, ldat, remap.keys.len(), size)?; + fill_key_list(&mut list, ldat, remap.keys.len(), size)?; if let Some(slot) = rk.get_mut(cdat_at) { *slot = list; } @@ -638,7 +640,7 @@ fn put_masks(cx: &Cx<'_>, layr: &mut Chunk<'static>, lp: &LayerPlan) -> R<()> { let parade = crate::export::masks::build_parade(&cx.t.masks, &lp.masks, lp.source_size)?; let root = list_mut(layr, b"tdgp")?; let kids = kids_mut(root)?; - let at = kids.iter().position(|c| c.is(b"tdmn") && tdmn_is(c, "ADBE Transform Group")).ok_or_else(|| tpl("layer without a Transform group"))?; + let at = kids.iter().position(|c| is_tdmn(c, "ADBE Transform Group")).ok_or_else(|| tpl("layer without a Transform group"))?; kids.splice(at..at, parade); Ok(()) } @@ -652,7 +654,7 @@ fn put_effects(cx: &mut Cx<'_>, layr: &mut Chunk<'static>, lp: &LayerPlan, layer let parade = crate::export::effects::build_parade(&cx.t.effects, &lp.fx, layer_id, layer_ids, lp.source_size, &mut cx.fx_use)?; let root = list_mut(layr, b"tdgp")?; let kids = kids_mut(root)?; - let at = kids.iter().position(|c| c.is(b"tdmn") && tdmn_is(c, "ADBE Transform Group")).ok_or_else(|| tpl("layer without a Transform group"))?; + let at = kids.iter().position(|c| is_tdmn(c, "ADBE Transform Group")).ok_or_else(|| tpl("layer without a Transform group"))?; kids.splice(at..at, parade); Ok(()) } @@ -675,19 +677,6 @@ const GROUP_ORDER: [&str; 13] = [ "ADBE Group End", ]; -/// The match name of a `tdmn` chunk: its bytes up to the first NUL, not decoded (empty for a chunk -/// without data). Every name the exporter looks for is ASCII, so comparing bytes is the same as -/// comparing the Latin-1 text the other exporter modules decode. -fn tdmn_bytes<'c>(c: &'c Chunk<'_>) -> &'c [u8] { - let d = c.bytes().unwrap_or_default(); - d.split(|b| *b == 0).next().unwrap_or_default() -} - -/// True when `c` has data and names `name`. -fn tdmn_is(c: &Chunk<'_>, name: &str) -> bool { - c.bytes().is_some() && tdmn_bytes(c) == name.as_bytes() -} - /// Inserts the properties of `lp` into the layer's Transform group, in the order the file uses. fn put_properties(cx: &Cx<'_>, layr: &mut Chunk<'static>, lp: &LayerPlan, cp: &CompPlan) -> R<()> { if lp.props.is_empty() { @@ -695,7 +684,7 @@ fn put_properties(cx: &Cx<'_>, layr: &mut Chunk<'static>, lp: &LayerPlan, cp: &C } let root = list_mut(layr, b"tdgp")?; let kids = kids_mut(root)?; - let at = kids.iter().position(|c| c.is(b"tdmn") && tdmn_is(c, "ADBE Transform Group")).ok_or_else(|| tpl("layer without a Transform group"))?; + let at = kids.iter().position(|c| is_tdmn(c, "ADBE Transform Group")).ok_or_else(|| tpl("layer without a Transform group"))?; let group = kids.get_mut(at + 1).ok_or_else(|| tpl("Transform group without a list"))?; let gk = kids_mut(group)?; // Split the group into its header (up to the first match name) and (tdmn, node) pairs. @@ -704,7 +693,9 @@ fn put_properties(cx: &Cx<'_>, layr: &mut Chunk<'static>, lp: &LayerPlan, cp: &C let mut pairs: Vec<(Vec, Vec>)> = Vec::new(); let mut it = std::mem::take(gk).into_iter(); while let Some(m) = it.next() { - let name = tdmn_bytes(&m).to_vec(); + // The match name as the shared decoder reads it (lossy UTF-8 up to the first NUL). Every + // name looked up below is ASCII, so a name matches exactly when its bytes do. + let name = tdmn_name(&m).unwrap_or_default().into_bytes(); if name == b"ADBE Group End" { pairs.push((name, vec![m])); continue; @@ -716,7 +707,7 @@ fn put_properties(cx: &Cx<'_>, layr: &mut Chunk<'static>, lp: &LayerPlan, cp: &C let name = p.kind.match_name(); let built = build_prop(cx, p, lp, cp)?; let tdmn = pairs.iter().find(|(n, _)| n == name.as_bytes()).and_then(|(_, v)| v.first().cloned()).unwrap_or_else(|| { - cx.t.stat.iter().chain(cx.t.keyd.iter()).find(|c| c.is(b"tdmn") && tdmn_is(c, name)).cloned().unwrap_or_else(|| Chunk::data(*b"tdmn", Vec::new())) + cx.t.stat.iter().chain(cx.t.keyd.iter()).find(|c| is_tdmn(c, name)).cloned().unwrap_or_else(|| Chunk::data(*b"tdmn", Vec::new())) }); let entry = vec![tdmn, built]; if let Some(slot) = pairs.iter_mut().find(|(n, _)| n == name.as_bytes()) { @@ -766,7 +757,7 @@ fn build_prop(cx: &Cx<'_>, p: &PropPlan, lp: &LayerPlan, cp: &CompPlan) -> R, p: &PropPlan, lp: &LayerPlan, cp: &CompPlan) -> R, records: Vec, count: usize, size: usize) -> R<()> { - let n = u32::try_from(count).map_err(|_| tpl("too many keys"))?; - { - let lhd3 = child_data(list, b"lhd3")?; - crate::export::synth::put_key_counts(lhd3, n, u32::try_from(size).map_err(|_| tpl("record size"))?)?; - } - let ld = kids_mut(list)?; - match ld.iter_mut().find(|c| c.is(b"ldat")) { - Some(c) => *c = Chunk::data(*b"ldat", records), - None => ld.push(Chunk::data(*b"ldat", records)), - } - Ok(()) -} - /// One keyframe record (`docs/aep-format.md`, 3.5). fn key_record(kind: PropKind, k: &KeyPlan, size: usize) -> R> { let mut r = vec![0u8; size]; @@ -856,10 +828,12 @@ mod tests { use effectcraft_project::{Comp, Expression, ItemKind, LayerSource, Project, Property, Solid, build}; use effectcraft_time::{FrameRate, TICKS_PER_SECOND, Tick}; + /// Seconds as ticks. fn secs(s: f64) -> Tick { Tick((s * TICKS_PER_SECOND as f64).round() as i64) } + /// Adds a layer to `comp`, changed by `edit` first. fn add_layer(p: &mut Project, comp: ItemId, name: &str, source: LayerSource, edit: impl FnOnce(&mut effectcraft_project::Layer)) { let mut c = p.comp(comp).cloned().expect("comp"); let mut l = build::layer(p, &c, name, source, (640, 360), None); @@ -871,6 +845,7 @@ mod tests { } } + /// Gives the property two keys (0 s and 1.5 s) holding its value. fn two_keys(prop: Option<&mut Property>) { let prop = prop.expect("property"); let v = prop.value.clone(); @@ -912,6 +887,7 @@ mod tests { p } + /// The export plan of `p`. fn plan_of(p: &Project) -> ProjectPlan { let mut col = crate::export::report::Collector::default(); crate::export::plan::plan(p, &mut col) @@ -973,6 +949,7 @@ mod tests { /// The comparator `sort_named` used before its keys were cached: `natural_key` built on every /// comparison. fn old_sort_named(v: &mut [Sorted]) { + /// The natural name order the cached sort keys replaced, as the reference. fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering { let (ka, kb) = (natural_key(a), natural_key(b)); for (x, y) in ka.iter().zip(kb.iter()) { @@ -1072,10 +1049,11 @@ mod tests { assert!(natural_bytes(&worst).len() <= 1_275); } - /// `tdmn_is` and `tdmn_bytes` agree with the Latin-1 decoding they replaced for every name the - /// emitter looks up, which holds because those names are ASCII. + /// The shared `is_tdmn` / `tdmn_name` the emitter uses agree with the Latin-1 decoding it used + /// before for every name it looks up, which holds because those names are ASCII. #[test] fn match_names_compare_as_the_latin1_decoding_did() { + /// The Latin-1 decoding the emitter used before. fn old_tdmn_name(c: &Chunk<'_>) -> Option { let d = c.bytes()?; Some(d.iter().take_while(|b| **b != 0).map(|b| char::from(*b)).collect()) @@ -1096,28 +1074,26 @@ mod tests { ]); for c in &chunks { for n in &wanted { - assert_eq!(tdmn_is(c, n), old_tdmn_name(c).as_deref() == Some(*n), "{c:?} {n}"); - } - let old = old_tdmn_name(c).unwrap_or_default(); - if old.is_ascii() { - assert_eq!(tdmn_bytes(c), old.as_bytes()); + assert_eq!(is_tdmn(c, n), old_tdmn_name(c).as_deref() == Some(*n), "{c:?} {n}"); + let key = tdmn_name(c).unwrap_or_default().into_bytes(); + assert_eq!(key == n.as_bytes(), old_tdmn_name(c).as_deref() == Some(*n), "{c:?} {n}"); } } } - /// `set_key_list` writes the counts and replaces the records, or adds them when the list has - /// none. + /// `fill_key_list`, which the time remap and the Transform keys use, writes the counts and + /// replaces the records, or adds them when the list has none. #[test] - fn set_key_list_writes_counts_and_records() { + fn fill_key_list_writes_counts_and_records() { let lhd3 = || Chunk::data(*b"lhd3", vec![0; 52]); let mut want_lhd3 = vec![0; 52]; crate::export::synth::put_key_counts(&mut want_lhd3, 3, 48).expect("counts"); for start in [vec![lhd3(), Chunk::data(*b"ldat", vec![9; 4])], vec![lhd3()]] { let mut list = Chunk::list(*b"list", start); - set_key_list(&mut list, vec![7; 144], 3, 48).expect("set"); + fill_key_list(&mut list, vec![7; 144], 3, 48).expect("set"); assert_eq!(list.children(), &[Chunk::data(*b"lhd3", want_lhd3.clone()), Chunk::data(*b"ldat", vec![7; 144])]); } let mut no_header = Chunk::list(*b"list", Vec::new()); - assert!(set_key_list(&mut no_header, Vec::new(), 0, 48).is_err()); + assert!(fill_key_list(&mut no_header, Vec::new(), 0, 48).is_err()); } } diff --git a/crates/aep/src/export/fxsynth.rs b/crates/aep/src/export/fxsynth.rs index cce25b6f2..611ef7310 100644 --- a/crates/aep/src/export/fxsynth.rs +++ b/crates/aep/src/export/fxsynth.rs @@ -51,6 +51,7 @@ pub(crate) fn pard(p: &PDef, popup_items: Option) -> Vec { pard_layout(p, popup_items).finish(|| format!("pard (type {})", p.code)) } +/// The 148-byte `pard` layout of a parameter definition: type, name and defaults. fn pard_layout(p: &PDef, popup_items: Option) -> Fixed { let mut b = Fixed::zeroed(148); b.put(12, &u32::from(p.code).to_be_bytes()); diff --git a/crates/aep/src/export/keys.rs b/crates/aep/src/export/keys.rs index b3cd1158c..a19187418 100644 --- a/crates/aep/src/export/keys.rs +++ b/crates/aep/src/export/keys.rs @@ -90,6 +90,7 @@ mod tests { use super::*; use crate::export::synth::key_list; + /// Keys are timed sorted merged and reported. #[test] fn keys_are_timed_sorted_merged_and_reported() { use effectcraft_keyframe::Value; @@ -122,6 +123,7 @@ mod tests { assert!(col.finish().losses.is_empty()); } + /// A key list is filled and replaces the value. #[test] fn a_key_list_is_filled_and_replaces_the_value() { let mut list = Chunk::list(*b"list", vec![Chunk::data(*b"lhd3", vec![0; 52])]); @@ -142,6 +144,7 @@ mod tests { assert!(set_key_list(&mut Chunk::list(*b"tdbs", Vec::new()), Vec::new(), 0, 48).is_err()); } + /// A spatial record holds the point layout. #[test] fn a_spatial_record_holds_the_point_layout() { let k = KeyPlan { diff --git a/crates/aep/src/export/masks.rs b/crates/aep/src/export/masks.rs index b46811500..9b235eb28 100644 --- a/crates/aep/src/export/masks.rs +++ b/crates/aep/src/export/masks.rs @@ -368,6 +368,7 @@ pub(crate) fn shph() -> Vec { shph_layout().finish(|| "shph".to_string()) } +/// The 24-byte `shph` layout of a closed path with a unit box. fn shph_layout() -> Fixed { let mut b = Fixed::zeroed(24); b.put(0, &[0xb3, 0xde, 0x02, 0x01]); @@ -634,6 +635,7 @@ mod tests { use super::*; use crate::export::synth::is_tdmn; + /// A closed three-point mask. fn mask() -> MaskPlan { let shape = ShapePlan { bbox: [0.0, 0.0, 1.0, 1.0], open: false, points: vec![[0.0, 0.0]; 3] }; MaskPlan { @@ -649,6 +651,7 @@ mod tests { } } + /// An atom without its path is an error. #[test] fn an_atom_without_its_path_is_an_error() { let mut t = MaskProtos::build(); diff --git a/crates/aep/src/export/report.rs b/crates/aep/src/export/report.rs index 309f564e8..b6094e791 100644 --- a/crates/aep/src/export/report.rs +++ b/crates/aep/src/export/report.rs @@ -262,6 +262,7 @@ impl Collector { mod tests { use super::*; + /// A report with losses in two categories, as the collector finishes it. fn collected() -> ExportReport { let mut col = Collector::default(); col.exported.layers = 2; @@ -286,6 +287,7 @@ mod tests { assert_eq!(back, collected()); } + /// The queries read the losses. #[test] fn the_queries_read_the_losses() { let report = collected(); @@ -296,6 +298,7 @@ mod tests { assert_eq!((empty.total_losses(), empty.is_lossless(), empty.summary()), (0, true, String::new())); } + /// Details stop at the cap. #[test] fn details_stop_at_the_cap() { let mut col = Collector::default(); diff --git a/crates/aep/src/export/synth.rs b/crates/aep/src/export/synth.rs index 80678baa3..8af515f24 100644 --- a/crates/aep/src/export/synth.rs +++ b/crates/aep/src/export/synth.rs @@ -126,6 +126,7 @@ pub(crate) fn tdb4(spec: &RecSpec, keyed: bool, time_scale: u32) -> Vec { tdb4_layout(spec, keyed, time_scale).finish(|| "tdb4".to_string()) } +/// The 124-byte `tdb4` property header layout for a record spec. fn tdb4_layout(spec: &RecSpec, keyed: bool, time_scale: u32) -> Fixed { let mut b = Fixed::zeroed(124); b.put(0, &[0xdb, 0x99, 0x00, spec.dims]); @@ -161,6 +162,7 @@ pub(crate) fn key_list(size: u32, count: u32, records: Vec) -> Chunk<'static Chunk::list(*b"list", vec![Chunk::data(*b"lhd3", lhd3), Chunk::data(*b"ldat", records)]) } +/// The 52-byte `lhd3` key list header for `count` records of `size` bytes. fn key_list_header(size: u32, count: u32) -> Fixed { let mut lhd3 = Fixed::zeroed(52); lhd3.put(0, &0x00d0_0bee_u32.to_be_bytes()); @@ -229,6 +231,7 @@ mod tests { assert_eq!(rec.child(b"cdat").and_then(|c| c.bytes()).map(<[u8]>::len), Some(40)); } + /// Every record header fits. #[test] fn every_record_header_fits() { for dims in 0..=u8::MAX { @@ -246,6 +249,7 @@ mod tests { } } + /// A match name reads as the importer reads it. #[test] fn a_match_name_reads_as_the_importer_reads_it() { let m = tdmn("ADBE Group End"); diff --git a/crates/aep/src/export/tree.rs b/crates/aep/src/export/tree.rs index 9c71a27b8..9b06ddcba 100644 --- a/crates/aep/src/export/tree.rs +++ b/crates/aep/src/export/tree.rs @@ -188,6 +188,7 @@ pub(crate) fn set_utf8(c: &mut Chunk<'static>, text: &str) -> R<()> { mod tests { use super::*; + /// The writer errors instead of dropping. #[test] fn the_writer_errors_instead_of_dropping() { let mut d = [0u8; 4]; @@ -200,6 +201,7 @@ mod tests { assert_eq!(d, [0, 0, 1, 2]); } + /// A cut string keeps a nul and whole characters. #[test] fn a_cut_string_keeps_a_nul_and_whole_characters() { let mut d = [0xffu8; 6]; @@ -219,6 +221,7 @@ mod tests { assert_eq!(fixed_layout_errors(), before); } + /// A broken fixed layout writes what fits and is counted. #[test] fn a_broken_fixed_layout_writes_what_fits_and_is_counted() { let before = fixed_layout_errors(); @@ -231,6 +234,7 @@ mod tests { assert_eq!(fixed_layout_errors(), before + 1); } + /// A group splits into header and entries. #[test] fn a_group_splits_into_header_and_entries() { use crate::export::synth::{tdmn, tdsn}; diff --git a/crates/aep/src/import.rs b/crates/aep/src/import.rs index 8662e182c..79e416563 100644 --- a/crates/aep/src/import.rs +++ b/crates/aep/src/import.rs @@ -191,12 +191,14 @@ mod tests { } } + /// A 40-byte NUL-padded match name. fn tdmn(name: &str) -> Chunk<'static> { let mut b = name.as_bytes().to_vec(); b.resize(40, 0); Chunk::data(*b"tdmn", b) } + /// A synthetic project imports the same through the index. #[test] fn a_synthetic_project_imports_the_same_through_the_index() { // A composition with a layer whose effect stores arbitrary data and a named value group. diff --git a/crates/aep/src/import/report.rs b/crates/aep/src/import/report.rs index c72fb8528..bbf6b2724 100644 --- a/crates/aep/src/import/report.rs +++ b/crates/aep/src/import/report.rs @@ -251,6 +251,7 @@ impl AeVersionInfo { Self::of_root(Root::from(file)) } + /// Reads the versions from the top-level chunks of either tree. pub(crate) fn of_root(file: Root<'_, '_>) -> Self { let file_version = file.file_version(); Self { @@ -342,6 +343,7 @@ pub const LIMITATIONS: &[&str] = &[ mod tests { use super::*; + /// A report with losses in two categories and a missing file, as the collector finishes it. fn collected() -> ImportReport { let mut col = Collector::default(); col.imported.layers = 2; @@ -366,6 +368,7 @@ mod tests { assert_eq!(back, collected()); } + /// The queries read the losses. #[test] fn the_queries_read_the_losses() { let report = collected(); diff --git a/crates/aep/tests/it/container/batch3_objects.rs b/crates/aep/tests/it/container/batch3_objects.rs index ca24d385d..714938312 100644 --- a/crates/aep/tests/it/container/batch3_objects.rs +++ b/crates/aep/tests/it/container/batch3_objects.rs @@ -386,6 +386,7 @@ fn the_last_style_run_absorbs_the_final_carriage_return() { // ------------------------------------------------------------------ camera and light (341-345) +/// Camera zoom keeps the auto position. #[test] fn camera_zoom_keeps_the_auto_position() { // 341: Zoom 2000 is stored (checked on import); the auto Position keeps [0, 0, -2666.67]. diff --git a/crates/aep/tests/it/container/layer_switches.rs b/crates/aep/tests/it/container/layer_switches.rs index 1f8565752..3d1f764fb 100644 --- a/crates/aep/tests/it/container/layer_switches.rs +++ b/crates/aep/tests/it/container/layer_switches.rs @@ -170,6 +170,7 @@ fn cameras_auto_orient_and_position() { assert_eq!(pos.stored, [100.0, 200.0, -1000.0]); } +/// Lights type and position. #[test] fn lights_type_and_position() { // 254: a spot light (its Intensity 50 is checked on import). diff --git a/crates/aep/tests/it/container/tree_index.rs b/crates/aep/tests/it/container/tree_index.rs index e49d641a0..bd36493cd 100644 --- a/crates/aep/tests/it/container/tree_index.rs +++ b/crates/aep/tests/it/container/tree_index.rs @@ -74,6 +74,7 @@ fn arbitrary(fx: &Effect<'_, '_>, name: &str, s: &mut String) { let _ = writeln!(s, " {:?} {:?} {:?}", CurvesData::parse(data), MeshData::parse(data), CryptomatteData::parse(data)); } +/// Appends a group and everything below it to the dump, depth-limited. fn tree(group: &PropGroup<'_, '_>, depth: usize, s: &mut String) { if depth > 32 { return; @@ -93,6 +94,7 @@ fn tree(group: &PropGroup<'_, '_>, depth: usize, s: &mut String) { } } +/// The index is the parsed tree and the view reads the same on every file. #[test] fn the_index_is_the_parsed_tree_and_the_view_reads_the_same_on_every_file() { for path in every_file() { @@ -124,6 +126,7 @@ fn the_index_is_the_parsed_tree_and_the_view_reads_the_same_on_every_file() { } } +/// The index and the parser fail alike under tight limits. #[test] fn the_index_and_the_parser_fail_alike_under_tight_limits() { for path in every_file().into_iter().filter(|p| p.extension().is_some_and(|x| x == "aep")) { @@ -155,6 +158,7 @@ fn tdmn(name: &str) -> Chunk<'static> { Chunk::data(*b"tdmn", b) } +/// A nested chunk `id` holding one `Utf8` with `text`. fn utf8(id: [u8; 4], text: &str) -> Chunk<'static> { Chunk { id, body: effectcraft_aep::Body::Chunks(vec![Chunk::data(*b"Utf8", text.as_bytes().to_vec())]), pad: None } } @@ -196,6 +200,7 @@ fn synthetic() -> Vec { write(&file).expect("write") } +/// The view reads arbitrary effect data the same through the index. #[test] fn the_view_reads_arbitrary_effect_data_the_same_through_the_index() { let bytes = synthetic(); diff --git a/crates/aep/tests/it/export/built.rs b/crates/aep/tests/it/export/built.rs index dc4c8dc3c..0f9784428 100644 --- a/crates/aep/tests/it/export/built.rs +++ b/crates/aep/tests/it/export/built.rs @@ -83,6 +83,7 @@ fn ntsc_and_odd_frame_rates_use_the_time_bases_after_effects_writes() { } } +/// Every ntsc rate reads back as the rate it was written for. #[test] fn every_ntsc_rate_reads_back_as_the_rate_it_was_written_for() { // The export's time base for each NTSC rate and the import's rate for that time base use the