mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-14 22:33:09 +00:00
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)
This commit is contained in:
1 parent
b80ac9edb9
commit
2e24ae47a1
22 files changed
+136
-53
No files matched your search
@@ -658,6 +658,7 @@ mod tests {
|
||||
assert!(matches!(read_aepx(b"<AfterEffectsProject>\xff</AfterEffectsProject>"), 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<Vec<u8>, AepError> {
|
||||
if text.len() % 2 == 1 {
|
||||
return Err(xml(offset, "bdata has an odd number of hex digits"));
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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<Self::Item> {
|
||||
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<Self::Item> {
|
||||
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.
|
||||
|
||||
@@ -63,6 +63,7 @@ pub(super) fn container_end(data: &[u8]) -> Result<usize, AepError> {
|
||||
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<Chunk<'static>> = (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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -184,29 +184,35 @@ struct Parser<'b> {
|
||||
items: Vec<V<'b>>,
|
||||
}
|
||||
|
||||
/// 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<u8> {
|
||||
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<V<'b>> {
|
||||
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> {
|
||||
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<u8> {
|
||||
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<TextValue> {
|
||||
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<'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<u8>) {
|
||||
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 >> ] >> >> >> ] >>";
|
||||
|
||||
@@ -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<Self> {
|
||||
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] =
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -15,6 +15,7 @@ pub(crate) fn bytes(len: usize, fields: &[(usize, &[u8])]) -> Vec<u8> {
|
||||
layout(len, fields).finish(|| format!("base::bytes ({len} bytes, fields at {:?})", fields.iter().map(|(at, v)| (*at, v.len())).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
/// 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])]);
|
||||
|
||||
@@ -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<u8>, Vec<Chunk<'static>>)> = 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<Chu
|
||||
for k in &p.keys {
|
||||
ldat.extend(key_record(p.kind, k, size)?);
|
||||
}
|
||||
set_key_list(list_mut(&mut tdbs, b"list")?, ldat, p.keys.len(), size)?;
|
||||
fill_key_list(list_mut(&mut tdbs, b"list")?, ldat, p.keys.len(), size)?;
|
||||
}
|
||||
if let Some((text, _)) = &p.expr {
|
||||
let kids = kids_mut(&mut tdbs)?;
|
||||
@@ -776,25 +767,6 @@ fn build_prop(cx: &Cx<'_>, p: &PropPlan, lp: &LayerPlan, cp: &CompPlan) -> R<Chu
|
||||
Ok(tdbs)
|
||||
}
|
||||
|
||||
/// Writes `count` key records of `size` bytes into the key list `list`: the counts into `lhd3`,
|
||||
/// the records as its `ldat` (replaced, or added when missing).
|
||||
///
|
||||
/// The same as `masks::set_key_list`, which takes the property record (`tdbs`) instead of the
|
||||
/// list; the time remap fills a list before it is put into its record.
|
||||
fn set_key_list(list: &mut Chunk<'static>, records: Vec<u8>, 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<Vec<u8>> {
|
||||
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<String> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ pub(crate) fn pard(p: &PDef, popup_items: Option<u16>) -> Vec<u8> {
|
||||
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<u16>) -> Fixed {
|
||||
let mut b = Fixed::zeroed(148);
|
||||
b.put(12, &u32::from(p.code).to_be_bytes());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -368,6 +368,7 @@ pub(crate) fn shph() -> Vec<u8> {
|
||||
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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -126,6 +126,7 @@ pub(crate) fn tdb4(spec: &RecSpec, keyed: bool, time_scale: u32) -> Vec<u8> {
|
||||
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<u8>) -> 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");
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<u8> {
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user