Files
filmcraft/crates/cfb/README.md
WbunkerandClaude Sonnet 5.5 a711c61840 fix(cfb): adding a child is O(1), not a scan of every sibling
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>
2026-10-17 10:05:53 -04:00

56 lines
2.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.