mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-15 23:02:14 +00:00
aep export: review fixes for #3278
- tree::Fixed replaces tree::fixed: a fixed layout writes every field that fits (as the old code did) and keeps the first error; finish() logs it once with the layout's description and counts it per thread, so the every-layout test cannot race with other tests. - base::bytes logs one line naming its length and fields. - masks::build_atom errors when the prototype has no Mask Shape instead of dropping the path of every mask quietly. - log sorted into the dependency list.
This commit is contained in:
1 parent
a4f9a97525
commit
9e82a5e704
6 files changed
+191
-105
No files matched your search
@@ -15,9 +15,9 @@ effectcraft-color = { workspace = true }
|
||||
effectcraft-keyframe = { workspace = true }
|
||||
effectcraft-project = { workspace = true }
|
||||
effectcraft-effects = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -7,24 +7,20 @@
|
||||
|
||||
use crate::Chunk;
|
||||
use crate::export::synth::{GROUP_END, tdmn, tdsn};
|
||||
use crate::export::tree::{R, fixed, put};
|
||||
use crate::export::tree::Fixed;
|
||||
|
||||
/// A zero buffer of `len` bytes with `fields` (offset, bytes) written into it. The layouts are
|
||||
/// fixed in code ([`fixed`]).
|
||||
/// fixed in code ([`Fixed`]).
|
||||
pub(crate) fn bytes(len: usize, fields: &[(usize, &[u8])]) -> Vec<u8> {
|
||||
fixed("base::bytes", try_bytes(len, fields), || {
|
||||
// Which layout: its length and fields (the builders are named in the comments above).
|
||||
log::error!("aep export: base::bytes layout of {len} bytes, fields at {:?}", fields.iter().map(|(at, v)| (*at, v.len())).collect::<Vec<_>>());
|
||||
vec![0u8; len]
|
||||
})
|
||||
layout(len, fields).finish(|| format!("base::bytes ({len} bytes, fields at {:?})", fields.iter().map(|(at, v)| (*at, v.len())).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
fn try_bytes(len: usize, fields: &[(usize, &[u8])]) -> R<Vec<u8>> {
|
||||
let mut b = vec![0u8; len];
|
||||
fn layout(len: usize, fields: &[(usize, &[u8])]) -> Fixed {
|
||||
let mut b = Fixed::zeroed(len);
|
||||
for (at, v) in fields {
|
||||
put(&mut b, *at, v)?;
|
||||
b.put(*at, v);
|
||||
}
|
||||
Ok(b)
|
||||
b
|
||||
}
|
||||
|
||||
pub(crate) fn utf8(text: &str) -> Chunk<'static> {
|
||||
@@ -286,9 +282,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_field_out_of_range_is_an_error() {
|
||||
assert_eq!(try_bytes(4, &[(0, &[1]), (3, &[2])]).ok(), Some(vec![1, 0, 0, 2]));
|
||||
assert!(try_bytes(4, &[(3, &[1, 2])]).is_err());
|
||||
assert!(try_bytes(4, &[(usize::MAX, &[1])]).is_err());
|
||||
let ok = layout(4, &[(0, &[1]), (3, &[2])]);
|
||||
assert!(ok.error().is_none());
|
||||
assert_eq!(ok.finish(String::new), [1, 0, 0, 2]);
|
||||
assert!(layout(4, &[(3, &[1, 2])]).error().is_some());
|
||||
assert!(layout(4, &[(usize::MAX, &[1])]).error().is_some());
|
||||
}
|
||||
|
||||
/// The chunks After Effects refuses a file without, or crashes on (aep-export.md, "What After
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::Chunk;
|
||||
use crate::effect_map::{Conv, by_match_name};
|
||||
use crate::export::fx_defs::{BUILT_IN, FxDef, PDef};
|
||||
use crate::export::synth::{GROUP_END, KeyTemplate, NO_NAME, RecSpec, class, tdmn, tdsn, utf8_in};
|
||||
use crate::export::tree::{R, fixed as fixed_layout, put};
|
||||
use crate::export::tree::Fixed;
|
||||
use crate::{CURVE_IDENTITY, CurvesData, MeshData};
|
||||
|
||||
/// The layer a prototype's layer parameters name when their default is the owning layer; the
|
||||
@@ -48,91 +48,91 @@ fn fixed(v: f64) -> [u8; 4] {
|
||||
/// display 124; fixed and integer: 124 / 128 / 132 / 136, default 140, display 144). Every other
|
||||
/// byte is zero (the switches at 48 are not needed: s12-e2-nofl).
|
||||
pub(crate) fn pard(p: &PDef, popup_items: Option<u16>) -> Vec<u8> {
|
||||
fixed_layout("pard", try_pard(p, popup_items), || vec![0u8; 148])
|
||||
pard_layout(p, popup_items).finish(|| format!("pard (type {})", p.code))
|
||||
}
|
||||
|
||||
fn try_pard(p: &PDef, popup_items: Option<u16>) -> R<Vec<u8>> {
|
||||
let mut b = vec![0u8; 148];
|
||||
put(&mut b, 12, &u32::from(p.code).to_be_bytes())?;
|
||||
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());
|
||||
let d = |i: usize| p.default.get(i).copied().unwrap_or(0.0);
|
||||
// After Effects takes the default of a parameter without a value record from here (s8-e1:
|
||||
// the copies below are the ones it reads).
|
||||
match p.code {
|
||||
10 => {
|
||||
put(&mut b, 56, &d(0).to_be_bytes())?;
|
||||
b.put(56, &d(0).to_be_bytes());
|
||||
// The default again as `f32` at 120, the display word (decimals) at 124.
|
||||
put(&mut b, 120, &(d(0) as f32).to_be_bytes())?;
|
||||
put(&mut b, 124, &p.display.to_be_bytes())?;
|
||||
b.put(120, &(d(0) as f32).to_be_bytes());
|
||||
b.put(124, &p.display.to_be_bytes());
|
||||
}
|
||||
2 => put(&mut b, 56, &fixed(d(0)))?,
|
||||
2 => b.put(56, &fixed(d(0))),
|
||||
// An angle: the default again at 60.
|
||||
3 => {
|
||||
put(&mut b, 56, &fixed(d(0)))?;
|
||||
put(&mut b, 60, &fixed(d(0)))?;
|
||||
b.put(56, &fixed(d(0)));
|
||||
b.put(60, &fixed(d(0)));
|
||||
}
|
||||
1 => put(&mut b, 56, &(d(0).round() as i32).to_be_bytes())?,
|
||||
1 => b.put(56, &(d(0).round() as i32).to_be_bytes()),
|
||||
// A checkbox: the default again as a byte at 60.
|
||||
4 => {
|
||||
put(&mut b, 56, &(d(0).round().max(0.0) as u32).to_be_bytes())?;
|
||||
put(&mut b, 60, &[u8::from(d(0) != 0.0)])?;
|
||||
b.put(56, &(d(0).round().max(0.0) as u32).to_be_bytes());
|
||||
b.put(60, &[u8::from(d(0) != 0.0)]);
|
||||
}
|
||||
// A popup: the default again as `u16` at 62 (the item count is at 60).
|
||||
7 => {
|
||||
let sel = d(0).round().clamp(0.0, f64::from(u16::MAX));
|
||||
put(&mut b, 56, &(sel as u32).to_be_bytes())?;
|
||||
put(&mut b, 62, &(sel as u16).to_be_bytes())?;
|
||||
b.put(56, &(sel as u32).to_be_bytes());
|
||||
b.put(62, &(sel as u16).to_be_bytes());
|
||||
}
|
||||
6 => {
|
||||
// Fractions of the layer at 56 / 60 and the same in percent at 68 / 72.
|
||||
put(&mut b, 56, &fixed(d(0)))?;
|
||||
put(&mut b, 60, &fixed(d(1)))?;
|
||||
put(&mut b, 68, &fixed(d(0) * 100.0))?;
|
||||
put(&mut b, 72, &fixed(d(1) * 100.0))?;
|
||||
b.put(56, &fixed(d(0)));
|
||||
b.put(60, &fixed(d(1)));
|
||||
b.put(68, &fixed(d(0) * 100.0));
|
||||
b.put(72, &fixed(d(1) * 100.0));
|
||||
}
|
||||
5 => {
|
||||
// ARGB at 56 and the same at 60.
|
||||
let c: Vec<u8> = (0..4).map(|i| d(i).round().clamp(0.0, 255.0) as u8).collect();
|
||||
put(&mut b, 56, &c)?;
|
||||
put(&mut b, 60, &c)?;
|
||||
b.put(56, &c);
|
||||
b.put(60, &c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if p.code == 7 {
|
||||
let n = popup_items.unwrap_or_else(|| u16::try_from(p.display).unwrap_or(0));
|
||||
put(&mut b, 60, &n.to_be_bytes())?;
|
||||
b.put(60, &n.to_be_bytes());
|
||||
}
|
||||
match p.code {
|
||||
10 => {
|
||||
if let Some(m) = p.valid_min {
|
||||
put(&mut b, 104, &(m as f32).to_be_bytes())?;
|
||||
b.put(104, &(m as f32).to_be_bytes());
|
||||
}
|
||||
if let Some(m) = p.valid_max {
|
||||
put(&mut b, 108, &(m as f32).to_be_bytes())?;
|
||||
b.put(108, &(m as f32).to_be_bytes());
|
||||
}
|
||||
if let Some((lo, hi)) = p.range {
|
||||
put(&mut b, 112, &(lo as f32).to_be_bytes())?;
|
||||
put(&mut b, 116, &(hi as f32).to_be_bytes())?;
|
||||
b.put(112, &(lo as f32).to_be_bytes());
|
||||
b.put(116, &(hi as f32).to_be_bytes());
|
||||
}
|
||||
}
|
||||
1 | 2 => {
|
||||
let enc = |v: f64| if p.code == 2 { fixed(v) } else { (v.round() as i32).to_be_bytes() };
|
||||
if let Some(m) = p.valid_min {
|
||||
put(&mut b, 124, &enc(m))?;
|
||||
b.put(124, &enc(m));
|
||||
}
|
||||
if let Some(m) = p.valid_max {
|
||||
put(&mut b, 128, &enc(m))?;
|
||||
b.put(128, &enc(m));
|
||||
}
|
||||
if let Some((lo, hi)) = p.range {
|
||||
put(&mut b, 132, &enc(lo))?;
|
||||
put(&mut b, 136, &enc(hi))?;
|
||||
b.put(132, &enc(lo));
|
||||
b.put(136, &enc(hi));
|
||||
}
|
||||
// The default again at 140, the display word (decimals, percent) at 144.
|
||||
put(&mut b, 140, &enc(d(0)))?;
|
||||
put(&mut b, 144, &p.display.to_be_bytes())?;
|
||||
b.put(140, &enc(d(0)));
|
||||
b.put(144, &p.display.to_be_bytes());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(b)
|
||||
b
|
||||
}
|
||||
|
||||
/// The default arbitrary data of an effect (its type-11 parameter), from the decoded layouts
|
||||
@@ -278,7 +278,9 @@ mod tests {
|
||||
fn every_parameter_definition_fits() {
|
||||
for fx in FX_DEFS {
|
||||
for (i, p) in fx.params.iter().enumerate() {
|
||||
assert_eq!(try_pard(p, popup_items(fx, i)).map(|b| b.len()).ok(), Some(148), "{} {}", fx.match_name, i);
|
||||
let b = pard_layout(p, popup_items(fx, i));
|
||||
assert!(b.error().is_none(), "{} {}", fx.match_name, i);
|
||||
assert_eq!(b.finish(String::new).len(), 148);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::export::keys::{set_key_list, timed_keys};
|
||||
use crate::export::plan::{CompPlan, EaseSlot, KeyPlan, LIMIT, cut_name};
|
||||
use crate::export::report::{Collector, LossCategory};
|
||||
use crate::export::synth::GROUP_END;
|
||||
use crate::export::tree::{R, child_data, child_mut, fixed, kids_mut, list_mut, put, put_f32, put_f64, put_i32, put_u8, put_u16, put_u32, split_group, tpl};
|
||||
use crate::export::tree::{Fixed, R, child_data, child_mut, kids_mut, list_mut, put_f32, put_f64, put_i32, put_u8, put_u16, put_u32, split_group, tpl};
|
||||
|
||||
/// The most vertices one mask path may have (as the importer).
|
||||
pub(crate) const MAX_VERTICES: usize = 65_536;
|
||||
@@ -365,26 +365,25 @@ const MASK_LIMIT: f64 = 32_000.0;
|
||||
/// s13-m1), byte 3 `0x01` (`0x09` once the path is open), the box at 4..19 (written per shape),
|
||||
/// byte 20 1.
|
||||
pub(crate) fn shph() -> Vec<u8> {
|
||||
fixed("shph", try_shph(), || vec![0u8; 24])
|
||||
shph_layout().finish(|| "shph".to_string())
|
||||
}
|
||||
|
||||
fn try_shph() -> R<Vec<u8>> {
|
||||
let mut b = vec![0u8; 24];
|
||||
put(&mut b, 0, &[0xb3, 0xde, 0x02, 0x01])?;
|
||||
put_u8(&mut b, 20, 1)?;
|
||||
Ok(b)
|
||||
fn shph_layout() -> Fixed {
|
||||
let mut b = Fixed::zeroed(24);
|
||||
b.put(0, &[0xb3, 0xde, 0x02, 0x01]);
|
||||
b.put(20, &[1]);
|
||||
b
|
||||
}
|
||||
|
||||
/// The two prototype records of a keyed path (093): 64 bytes each, flags `0x0001`, the `f64` at
|
||||
/// 16 1 in the first (the rest is zero: its last bytes were uninitialised).
|
||||
fn path_key_records() -> R<Vec<u8>> {
|
||||
let mut first = vec![0u8; 64];
|
||||
let mut later = vec![0u8; 64];
|
||||
for r in [&mut first, &mut later] {
|
||||
put_u16(r, 6, 0x0001)?;
|
||||
fn path_key_records() -> Fixed {
|
||||
let mut b = Fixed::zeroed(128);
|
||||
for at in [6, 64 + 6] {
|
||||
b.put(at, &0x0001u16.to_be_bytes());
|
||||
}
|
||||
put_f64(&mut first, 16, 1.0)?;
|
||||
Ok([first, later].concat())
|
||||
b.put(16, &1.0f64.to_be_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
impl MaskProtos {
|
||||
@@ -400,7 +399,7 @@ impl MaskProtos {
|
||||
let path_rec = |keyed: bool| {
|
||||
let mut kids = vec![state(), tdsn(NO_NAME), Chunk::data(*b"tdb4", tdb4(&path, keyed, 25_600))];
|
||||
if keyed {
|
||||
kids.push(key_list(64, 2, fixed("keyed path", path_key_records(), || vec![0u8; 128])));
|
||||
kids.push(key_list(64, 2, path_key_records().finish(|| "the keyed path records".to_string())));
|
||||
} else {
|
||||
kids.push(Chunk::data(*b"cdat", vec![0u8; 4]));
|
||||
}
|
||||
@@ -465,6 +464,7 @@ fn build_atom(t: &MaskProtos, m: &MaskPlan, size: (u32, u32)) -> R<Chunk<'static
|
||||
crate::export::tree::set_utf8(tdsn, &m.name)?;
|
||||
}
|
||||
let (mut out, entries) = split_group(kids_mut(&mut atom)?)?;
|
||||
let mut has_shape = false;
|
||||
for (name, entry) in entries {
|
||||
// The closing Group End (no node) is kept as it is.
|
||||
let [tdmn, mut node]: [Chunk<'static>; 2] = match entry.try_into() {
|
||||
@@ -477,6 +477,7 @@ fn build_atom(t: &MaskProtos, m: &MaskPlan, size: (u32, u32)) -> R<Chunk<'static
|
||||
let keep = match name.as_str() {
|
||||
"ADBE Mask Shape" => {
|
||||
build_shape(t, &mut node, m, size)?;
|
||||
has_shape = true;
|
||||
true
|
||||
}
|
||||
"ADBE Mask Feather" => {
|
||||
@@ -511,6 +512,10 @@ fn build_atom(t: &MaskProtos, m: &MaskPlan, size: (u32, u32)) -> R<Chunk<'static
|
||||
out.push(node);
|
||||
}
|
||||
}
|
||||
// A prototype without the path would drop every mask's shape.
|
||||
if !has_shape {
|
||||
return Err(tpl("mask atom without its path"));
|
||||
}
|
||||
*kids_mut(&mut atom)? = out;
|
||||
Ok(atom)
|
||||
}
|
||||
@@ -623,3 +628,35 @@ pub(crate) fn build_shap(proto: &Chunk<'static>, s: &ShapePlan) -> R<Chunk<'stat
|
||||
}
|
||||
Ok(shap)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::export::synth::is_tdmn;
|
||||
|
||||
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 {
|
||||
name: "M".into(),
|
||||
mode: 1,
|
||||
inverted: false,
|
||||
shape,
|
||||
path_keys: Vec::new(),
|
||||
feather: None,
|
||||
feather_keys: Vec::new(),
|
||||
opacity: None,
|
||||
expansion: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_atom_without_its_path_is_an_error() {
|
||||
let mut t = MaskProtos::build();
|
||||
assert!(build_atom(&t, &mask(), (100, 100)).is_ok());
|
||||
// Without the Mask Shape pair the path of every mask would be dropped quietly.
|
||||
let kids = kids_mut(&mut t.atom).unwrap();
|
||||
let at = kids.iter().position(|c| is_tdmn(c, "ADBE Mask Shape")).unwrap();
|
||||
kids.drain(at..at + 2);
|
||||
assert!(build_atom(&t, &mask(), (100, 100)).is_err());
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
//! evidence, or zero where After Effects was shown to accept zero (the read-backs listed in
|
||||
//! `docs/aep-export.md`, "Built in code").
|
||||
|
||||
use crate::export::tree::{R, fixed, put, put_u32};
|
||||
use crate::export::tree::{Fixed, R, put_u32};
|
||||
use crate::{Body, Chunk};
|
||||
|
||||
/// `tdmn`: a match name padded to 40 bytes (at most 39 bytes of the name, so that a NUL remains).
|
||||
@@ -123,12 +123,12 @@ impl RecSpec {
|
||||
/// word and the constant doubles at 16..56 are written; After Effects reads the other bytes as
|
||||
/// zero (read-back e1-ztdb4d).
|
||||
pub(crate) fn tdb4(spec: &RecSpec, keyed: bool, time_scale: u32) -> Vec<u8> {
|
||||
fixed("tdb4", try_tdb4(spec, keyed, time_scale), || vec![0u8; 124])
|
||||
tdb4_layout(spec, keyed, time_scale).finish(|| "tdb4".to_string())
|
||||
}
|
||||
|
||||
fn try_tdb4(spec: &RecSpec, keyed: bool, time_scale: u32) -> R<Vec<u8>> {
|
||||
let mut b = vec![0u8; 124];
|
||||
put(&mut b, 0, &[0xdb, 0x99, 0x00, spec.dims])?;
|
||||
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]);
|
||||
let spatial = match spec.template {
|
||||
KeyTemplate::Plain => 0,
|
||||
KeyTemplate::Spatial(t) => t,
|
||||
@@ -137,36 +137,36 @@ fn try_tdb4(spec: &RecSpec, keyed: bool, time_scale: u32) -> R<Vec<u8>> {
|
||||
Some(b) => (b & !0x01) | u8::from(!keyed),
|
||||
None => (if spatial == 0 { 0 } else { spatial_byte5(spatial) }) | u8::from(!keyed),
|
||||
};
|
||||
put(&mut b, 5, &[byte5])?;
|
||||
put(&mut b, 7, &[spatial])?;
|
||||
put(&mut b, 8, &spec.class.to_be_bytes())?;
|
||||
put(&mut b, 12, &time_scale.to_be_bytes())?;
|
||||
b.put(5, &[byte5]);
|
||||
b.put(7, &[spatial]);
|
||||
b.put(8, &spec.class.to_be_bytes());
|
||||
b.put(12, &time_scale.to_be_bytes());
|
||||
// Every record After Effects writes carries 0.0001 and four 1.0 here; without them a roving
|
||||
// key reads back at time zero instead of its retimed place (read-back s14-re-keyframes).
|
||||
put(&mut b, 16, &1e-4f64.to_be_bytes())?;
|
||||
b.put(16, &1e-4f64.to_be_bytes());
|
||||
for at in [24, 32, 40, 48] {
|
||||
put(&mut b, at, &1.0f64.to_be_bytes())?;
|
||||
b.put(at, &1.0f64.to_be_bytes());
|
||||
}
|
||||
put(&mut b, 68, &[u8::from(keyed)])?;
|
||||
b.put(68, &[u8::from(keyed)]);
|
||||
// Set on a static spatial property, cleared once it is keyed (aep-format.md 3.4, byte 79).
|
||||
put(&mut b, 79, &[u8::from(spatial != 0 && !keyed)])?;
|
||||
Ok(b)
|
||||
b.put(79, &[u8::from(spatial != 0 && !keyed)]);
|
||||
b
|
||||
}
|
||||
|
||||
/// A key list (`LIST list`: `lhd3` + `ldat`) of `count` records of `size` bytes (aep-format.md
|
||||
/// 3.5): word 0 `0x00d00bee`, word 2 the count, word 3 the blocks of four (at least one), word 4
|
||||
/// the size, words 5 and 7 the capacity (the count rounded up to four, at least two), word 6 one.
|
||||
pub(crate) fn key_list(size: u32, count: u32, records: Vec<u8>) -> Chunk<'static> {
|
||||
let lhd3 = fixed("lhd3", key_list_header(size, count), || vec![0u8; 52]);
|
||||
let lhd3 = key_list_header(size, count).finish(|| "lhd3".to_string());
|
||||
Chunk::list(*b"list", vec![Chunk::data(*b"lhd3", lhd3), Chunk::data(*b"ldat", records)])
|
||||
}
|
||||
|
||||
fn key_list_header(size: u32, count: u32) -> R<Vec<u8>> {
|
||||
let mut lhd3 = vec![0u8; 52];
|
||||
put_u32(&mut lhd3, 0, 0x00d0_0bee)?;
|
||||
put_u32(&mut lhd3, 24, 1)?;
|
||||
put_key_counts(&mut lhd3, count, size)?;
|
||||
Ok(lhd3)
|
||||
fn key_list_header(size: u32, count: u32) -> Fixed {
|
||||
let mut lhd3 = Fixed::zeroed(52);
|
||||
lhd3.put(0, &0x00d0_0bee_u32.to_be_bytes());
|
||||
lhd3.put(24, &1u32.to_be_bytes());
|
||||
lhd3.apply(|d| put_key_counts(d, count, size));
|
||||
lhd3
|
||||
}
|
||||
|
||||
/// Sets the count words of a key list header for `count` keys of `size` bytes.
|
||||
@@ -235,12 +235,14 @@ mod tests {
|
||||
for template in [KeyTemplate::Plain, KeyTemplate::Spatial(1), KeyTemplate::Spatial(3), KeyTemplate::Spatial(u8::MAX)] {
|
||||
for byte5 in [None, Some(0xff)] {
|
||||
let spec = RecSpec { dims, template, class: u32::MAX, limits: None, byte5 };
|
||||
assert!(try_tdb4(&spec, false, u32::MAX).is_ok() && try_tdb4(&spec, true, 0).is_ok());
|
||||
assert!(tdb4_layout(&spec, false, u32::MAX).error().is_none() && tdb4_layout(&spec, true, 0).error().is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size, count) in [(0, 0), (u32::MAX, u32::MAX), (48, 5)] {
|
||||
assert_eq!(key_list_header(size, count).map(|h| h.len()).ok(), Some(52));
|
||||
let h = key_list_header(size, count);
|
||||
assert!(h.error().is_none());
|
||||
assert_eq!(h.finish(String::new).len(), 52);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,27 +97,61 @@ pub(crate) fn put_cstr(d: &mut [u8], at: usize, len: usize, text: &str) -> R<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How many fixed layouts did not fit their buffer (see [`fixed`]); the tests keep it at zero.
|
||||
static FIXED_LAYOUT_ERRORS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
|
||||
/// The result of a builder whose layout is fixed in code (constant sizes and offsets, no input):
|
||||
/// its writes go through [`put`], which errors when one does not fit. Such an error is a bug in
|
||||
/// the layout, never something a project can cause, and the tests build every one of these
|
||||
/// layouts and require no error. The builders keep their infallible signatures because files
|
||||
/// outside this module call them without a `Result`; if a layout ever broke, `fallback` is used
|
||||
/// and the error goes to the log.
|
||||
pub(crate) fn fixed<T>(builder: &'static str, built: R<T>, fallback: impl FnOnce() -> T) -> T {
|
||||
built.unwrap_or_else(|e| {
|
||||
FIXED_LAYOUT_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
log::error!("aep export: the fixed layout of {builder} does not fit: {e}");
|
||||
fallback()
|
||||
})
|
||||
thread_local! {
|
||||
/// How many fixed layouts on this thread did not fit their buffer (see [`Fixed`]); the tests
|
||||
/// keep it at zero.
|
||||
static FIXED_LAYOUT_ERRORS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// How many fixed layouts have failed in this process (for the tests).
|
||||
/// A buffer whose layout is fixed in code (constant size and offsets, no input from a project).
|
||||
/// Every write goes through [`put`]; a write that does not fit is skipped and its error kept, the
|
||||
/// others are written. Such an error is a bug in the layout, never something a project can cause:
|
||||
/// the tests build every one of these layouts and require no error. The builders keep their
|
||||
/// infallible signatures because files outside this module call them without a `Result`; if a
|
||||
/// layout ever broke, [`Fixed::finish`] logs it once and returns what fit.
|
||||
pub(crate) struct Fixed {
|
||||
bytes: Vec<u8>,
|
||||
error: Option<AepExportError>,
|
||||
}
|
||||
|
||||
impl Fixed {
|
||||
/// `len` zero bytes.
|
||||
pub(crate) fn zeroed(len: usize) -> Fixed {
|
||||
Fixed { bytes: vec![0u8; len], error: None }
|
||||
}
|
||||
|
||||
/// Writes `bytes` at `at`, or keeps the error (the first one) when they do not fit.
|
||||
pub(crate) fn put(&mut self, at: usize, bytes: &[u8]) {
|
||||
self.apply(|d| put(d, at, bytes));
|
||||
}
|
||||
|
||||
/// Runs a writer on the buffer, keeping its error (the first one).
|
||||
pub(crate) fn apply(&mut self, write: impl FnOnce(&mut [u8]) -> R<()>) {
|
||||
if let Err(e) = write(&mut self.bytes) {
|
||||
self.error.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// The error of the first write that did not fit (for the tests).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn error(&self) -> Option<&AepExportError> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
/// The bytes; a write that did not fit is logged once, with what `layout` says.
|
||||
pub(crate) fn finish(self, layout: impl FnOnce() -> String) -> Vec<u8> {
|
||||
if let Some(e) = self.error {
|
||||
FIXED_LAYOUT_ERRORS.with(|n| n.set(n.get().saturating_add(1)));
|
||||
log::error!("aep export: the fixed layout of {} does not fit: {e}", layout());
|
||||
}
|
||||
self.bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// How many fixed layouts have failed on this thread (for the tests).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn fixed_layout_errors() -> usize {
|
||||
FIXED_LAYOUT_ERRORS.load(std::sync::atomic::Ordering::Relaxed)
|
||||
FIXED_LAYOUT_ERRORS.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// The entries of a property group by match name (see [`split_group`]).
|
||||
@@ -179,10 +213,23 @@ mod tests {
|
||||
/// text, cameras, lights, shapes, styles) are built and no write went out of range.
|
||||
#[test]
|
||||
fn every_fixed_layout_fits() {
|
||||
assert_eq!(fixed_layout_errors(), 0);
|
||||
assert!(crate::export::template::Template::load().is_ok());
|
||||
assert_eq!(fixed_layout_errors(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_broken_fixed_layout_writes_what_fits_and_is_counted() {
|
||||
let before = fixed_layout_errors();
|
||||
let mut b = Fixed::zeroed(4);
|
||||
b.put(0, &[1]);
|
||||
b.put(3, &[2, 2]);
|
||||
b.put(2, &[3]);
|
||||
assert!(b.error().is_some());
|
||||
assert_eq!(b.finish(|| "a test layout".to_string()), [1, 0, 3, 0]);
|
||||
assert_eq!(fixed_layout_errors(), before + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_splits_into_header_and_entries() {
|
||||
use crate::export::synth::{tdmn, tdsn};
|
||||
|
||||
Reference in new issue
Block a user