mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-28 03:59:26 -06:00
Writer::add rejected a duplicate name by comparing the new name with every sibling through compare_names, which allocates two Vec<u16> per call, so n children in one storage cost O(n^2) allocating comparisons. An AAF export puts every marker and component in one storage: 20 000 markers took 58 s, 20 000 clips 56 s, 20 000 connected clips 87 s and 100 000 markers never finished (an FCPXML of 1 MB is enough). Doubling the input quadrupled the time; every other exporter stayed linear. Each storage keeps a set of its children's names as sibling order compares them (UTF-16 code units, upper-cased), so the duplicate check is a hash lookup. Same result: case-insensitive, also beyond ASCII, per storage. 20 000 markers now export to AAF in 0.38 s, 100 000 in 2.0 s, 20 000 clips in 0.23 s and 20 000 connected clips in 0.45 s. Tests: adding 40 000 children must take under 3 s (0.02 s now, 21 s before) and the duplicate rules are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
56 lines
2.8 KiB
Markdown
56 lines
2.8 KiB
Markdown
# filmcraft-cfb
|
||
|
||
A clean-room reader and writer for the Compound File Binary format (Microsoft "structured
|
||
storage"), the low-level container of AAF files. Layer L0: no dependencies beyond `std`, no
|
||
`unsafe`, builds for `wasm32-unknown-unknown`. No third-party implementation (the AAF SDK's
|
||
structured storage code, libgsf, olefile, POI…) was consulted.
|
||
|
||
## Specification
|
||
|
||
| Document | Edition | Used for |
|
||
|---|---|---|
|
||
| [MS-CFB] Compound File Binary File Format (Microsoft Open Specifications) | revision 10.0 (the format is unchanged since 1.0) | header, sector chains, FAT / DIFAT / mini FAT, directory entries, red-black sibling trees, name comparison, version 3 and 4 files |
|
||
|
||
## Reading
|
||
|
||
```rust
|
||
let bytes = std::fs::read("sequence.aaf")?;
|
||
let cf = filmcraft_cfb::CompoundFile::open(&bytes)?;
|
||
let header = cf.find("Header-2")?; // a storage
|
||
println!("{:02x?}", cf.entries[header].clsid); // its class id
|
||
let props = cf.read_path("Header-2/properties")?; // a stream
|
||
for &child in &cf.entries[header].children { /* sorted siblings */ }
|
||
```
|
||
|
||
- Accepts version 3 (512-byte sectors) and version 4 (4096-byte sectors) files; other sector
|
||
shifts between 7 and 16 are tolerated.
|
||
- Every chain (DIFAT, FAT, mini FAT, directory, streams) is bounds-checked and cycle-checked; a
|
||
directory tree that references an entry twice is rejected. Damaged or truncated files return
|
||
`Error::Invalid`, never panic or loop.
|
||
- Version 3 stream sizes ignore the high 32 bits (§2.6.3).
|
||
|
||
## Writing
|
||
|
||
```rust
|
||
let mut w = filmcraft_cfb::Writer::new(filmcraft_cfb::Version::V4);
|
||
w.set_clsid(filmcraft_cfb::Writer::ROOT, root_class);
|
||
let h = w.storage(filmcraft_cfb::Writer::ROOT, "Header-2", header_class)?;
|
||
w.stream(h, "properties", bytes)?;
|
||
let file: Vec<u8> = w.finish();
|
||
```
|
||
|
||
Layout: stream sectors (streams ≥ 4096 bytes), mini stream, mini FAT, directory, FAT, DIFAT. Each
|
||
chain is contiguous. Sibling trees are balanced binary search trees in [MS-CFB] name order
|
||
(length first, then upper-cased code units) coloured as valid red-black trees. Names are checked
|
||
(1–31 UTF-16 code units, none of `/ \ : !`, unique per storage, case-insensitively).
|
||
|
||
## Tests
|
||
|
||
`src/tests.rs`: round trips for both versions with empty, mini, boundary (63/64/65, 4095/4096/4097
|
||
bytes) and multi-sector streams and up to 40 siblings; red-black invariants of every sibling tree;
|
||
a 7.5 MB stream that needs DIFAT sectors; Unicode and case-insensitive names; truncation at every
|
||
29 bytes and 1500 random mutations never panic and only return complete streams.
|
||
The writer rejects a duplicate sibling name with an O(1) hash lookup on the upper-cased name (a scan over the
|
||
siblings made n children cost O(n^2): 20 000 AAF markers took a minute); `adding_many_children_is_not_quadratic`
|
||
and `duplicate_names_are_still_rejected` (`tests/writer_scale.rs`) cover it.
|