M11.4: filmcraft-cfb, a Compound File Binary ([MS-CFB]) reader and writer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
30d6e3e950
commit
02a7039ac5
|
|
@ -1738,6 +1738,10 @@ dependencies = [
|
|||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-cfb"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-cli"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ filmcraft-bitstream = { path = "crates/bitstream" }
|
|||
filmcraft-isobmff = { path = "crates/isobmff" }
|
||||
filmcraft-matroska = { path = "crates/matroska" }
|
||||
filmcraft-mxf = { path = "crates/mxf" }
|
||||
filmcraft-cfb = { path = "crates/cfb" }
|
||||
filmcraft-ogg = { path = "crates/ogg" }
|
||||
filmcraft-h264 = { path = "crates/h264" }
|
||||
filmcraft-hevc = { path = "crates/hevc" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "filmcraft-cfb"
|
||||
description = "Clean-room Compound File Binary (Microsoft structured storage, [MS-CFB]) reader and writer: the container of AAF files"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# 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.
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
//! Clean-room Compound File Binary format reader and writer ("structured storage", the container
|
||||
//! of AAF files), written from the public Microsoft Open Specification **[MS-CFB]: Compound File
|
||||
//! Binary File Format** (protocol revision 10.0 and earlier editions; the on-disk format has not
|
||||
//! changed since 1.0).
|
||||
//!
|
||||
//! A compound file is a small FAT file system inside one file: fixed-size sectors (512 bytes in
|
||||
//! version 3, 4096 bytes in version 4) chained by a file allocation table, a directory of 128-byte
|
||||
//! entries arranged as one red-black tree of siblings per storage, and a "mini stream" with a mini
|
||||
//! FAT for streams smaller than 4096 bytes.
|
||||
//!
|
||||
//! - [`CompoundFile::open`] parses a file held in memory (no copies of large streams; every chain
|
||||
//! is bounds- and cycle-checked, so damaged files give errors, never panics or endless loops).
|
||||
//! - [`Writer`] builds a tree of storages and streams and lays it out in one pass
|
||||
//! ([`Writer::finish`]): stream sectors, mini stream, mini FAT, directory, FAT and DIFAT.
|
||||
//!
|
||||
//! Layer L0: no dependencies beyond `std`, no `unsafe`, builds for `wasm32-unknown-unknown`.
|
||||
|
||||
mod read;
|
||||
mod write;
|
||||
|
||||
pub use read::{CompoundFile, Entry, EntryKind};
|
||||
pub use write::{NodeId, Version, Writer};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
|
||||
/// The 8-byte header signature (§2.2).
|
||||
pub const SIGNATURE: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
|
||||
/// Largest regular sector number (§2.1).
|
||||
pub const MAXREGSECT: u32 = 0xFFFF_FFFA;
|
||||
/// Sector used by the DIFAT.
|
||||
pub const DIFSECT: u32 = 0xFFFF_FFFC;
|
||||
/// Sector used by the FAT.
|
||||
pub const FATSECT: u32 = 0xFFFF_FFFD;
|
||||
/// End of a sector chain.
|
||||
pub const ENDOFCHAIN: u32 = 0xFFFF_FFFE;
|
||||
/// Unallocated sector.
|
||||
pub const FREESECT: u32 = 0xFFFF_FFFF;
|
||||
/// No sibling / child directory entry.
|
||||
pub const NOSTREAM: u32 = 0xFFFF_FFFF;
|
||||
/// Streams shorter than this live in the mini stream (§2.2 `Mini Stream Cutoff Size`).
|
||||
pub const MINI_STREAM_CUTOFF: u32 = 4096;
|
||||
/// Mini sector size (§2.2: mini sector shift 6).
|
||||
pub const MINI_SECTOR_SIZE: usize = 64;
|
||||
/// Longest directory entry name in UTF-16 code units, without the terminating NUL (§2.6.1).
|
||||
pub const MAX_NAME_LEN: usize = 31;
|
||||
|
||||
/// Errors from the reader and writer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// Not a compound file (bad signature or byte order mark).
|
||||
NotCfb,
|
||||
/// Structurally invalid or truncated.
|
||||
Invalid(String),
|
||||
/// No entry with that name / path.
|
||||
NotFound(String),
|
||||
/// A name the format cannot store (too long, illegal characters, duplicate).
|
||||
BadName(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::NotCfb => f.write_str("not a compound file"),
|
||||
Error::Invalid(s) => write!(f, "invalid compound file: {s}"),
|
||||
Error::NotFound(s) => write!(f, "no entry {s:?}"),
|
||||
Error::BadName(s) => write!(f, "invalid entry name {s:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Whether `head` starts with the compound file signature.
|
||||
pub fn sniff(head: &[u8]) -> bool {
|
||||
head.len() >= 8 && head[..8] == SIGNATURE
|
||||
}
|
||||
|
||||
fn upper(u: u16) -> u16 {
|
||||
if (b'a' as u16..=b'z' as u16).contains(&u) {
|
||||
return u - 32;
|
||||
}
|
||||
if u < 0x80 {
|
||||
return u;
|
||||
}
|
||||
match char::from_u32(u as u32) {
|
||||
Some(c) => {
|
||||
let mut up = c.to_uppercase();
|
||||
match (up.next(), up.next()) {
|
||||
(Some(x), None) if (x as u32) <= 0xFFFF => x as u32 as u16,
|
||||
_ => u,
|
||||
}
|
||||
}
|
||||
None => u,
|
||||
}
|
||||
}
|
||||
|
||||
/// The sibling order of directory entries (§2.6.4): shorter names first, then a code-unit
|
||||
/// comparison of the upper-cased names.
|
||||
pub fn compare_names(a: &str, b: &str) -> Ordering {
|
||||
let ua: Vec<u16> = a.encode_utf16().collect();
|
||||
let ub: Vec<u16> = b.encode_utf16().collect();
|
||||
ua.len().cmp(&ub.len()).then_with(|| ua.iter().map(|&u| upper(u)).cmp(ub.iter().map(|&u| upper(u))))
|
||||
}
|
||||
|
||||
/// Check a directory entry name (§2.6.1): 1–31 UTF-16 code units, none of `/ \ : !`.
|
||||
pub fn validate_name(name: &str) -> Result<()> {
|
||||
let n = name.encode_utf16().count();
|
||||
if n == 0 || n > MAX_NAME_LEN || name.contains(['/', '\\', ':', '!']) {
|
||||
return Err(Error::BadName(name.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
//! Reading compound files ([MS-CFB] §2.2–2.6).
|
||||
|
||||
use crate::{ENDOFCHAIN, Error, FREESECT, MAXREGSECT, MINI_SECTOR_SIZE, NOSTREAM, Result, SIGNATURE, compare_names};
|
||||
|
||||
/// Directory entry object type (§2.6.1).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EntryKind {
|
||||
Root,
|
||||
Storage,
|
||||
Stream,
|
||||
/// Unallocated (type 0) or an unknown type.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// One directory entry.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
pub kind: EntryKind,
|
||||
/// Class id of a storage (AAF: the class of the object stored in it).
|
||||
pub clsid: [u8; 16],
|
||||
pub state_bits: u32,
|
||||
/// FILETIME stamps (0 when not set).
|
||||
pub created: u64,
|
||||
pub modified: u64,
|
||||
/// First sector (regular or mini) of a stream; the mini stream's for the root.
|
||||
pub start: u32,
|
||||
pub size: u64,
|
||||
/// Child entries of a storage, in sibling-tree order (sorted by [`compare_names`]).
|
||||
pub children: Vec<usize>,
|
||||
left: u32,
|
||||
right: u32,
|
||||
child: u32,
|
||||
}
|
||||
|
||||
/// A parsed compound file over borrowed bytes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompoundFile<'a> {
|
||||
data: &'a [u8],
|
||||
/// Major version: 3 (512-byte sectors) or 4 (4096-byte sectors).
|
||||
pub version: u16,
|
||||
/// Header CLSID (normally zero).
|
||||
pub header_clsid: [u8; 16],
|
||||
sector_size: usize,
|
||||
mini_cutoff: u64,
|
||||
fat: Vec<u32>,
|
||||
minifat: Vec<u32>,
|
||||
mini_stream: Vec<u8>,
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
fn u16_at(b: &[u8], at: usize) -> u16 {
|
||||
u16::from_le_bytes([b[at], b[at + 1]])
|
||||
}
|
||||
fn u32_at(b: &[u8], at: usize) -> u32 {
|
||||
u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
|
||||
}
|
||||
fn u64_at(b: &[u8], at: usize) -> u64 {
|
||||
u64::from_le_bytes(b[at..at + 8].try_into().unwrap_or([0; 8]))
|
||||
}
|
||||
|
||||
fn invalid(s: impl Into<String>) -> Error {
|
||||
Error::Invalid(s.into())
|
||||
}
|
||||
|
||||
impl<'a> CompoundFile<'a> {
|
||||
/// Parse the header, FAT, mini FAT, directory and mini stream of `data`.
|
||||
pub fn open(data: &'a [u8]) -> Result<CompoundFile<'a>> {
|
||||
if data.len() < 512 || data[..8] != SIGNATURE || u16_at(data, 28) != 0xFFFE {
|
||||
return Err(Error::NotCfb);
|
||||
}
|
||||
let version = u16_at(data, 26);
|
||||
let shift = u16_at(data, 30);
|
||||
if !(7..=16).contains(&shift) {
|
||||
return Err(invalid(format!("sector shift {shift}")));
|
||||
}
|
||||
let mini_shift = u16_at(data, 32);
|
||||
if 1usize.checked_shl(mini_shift as u32) != Some(MINI_SECTOR_SIZE) {
|
||||
return Err(invalid(format!("mini sector shift {mini_shift}")));
|
||||
}
|
||||
let sector_size = 1usize << shift;
|
||||
let n_fat = u32_at(data, 44) as usize;
|
||||
let first_dir = u32_at(data, 48);
|
||||
let mini_cutoff = u32_at(data, 56) as u64;
|
||||
let first_minifat = u32_at(data, 60);
|
||||
let first_difat = u32_at(data, 68);
|
||||
let n_difat = u32_at(data, 72) as usize;
|
||||
let mut header_clsid = [0u8; 16];
|
||||
header_clsid.copy_from_slice(&data[8..24]);
|
||||
let max_sectors = data.len() / sector_size + 1;
|
||||
if n_fat > max_sectors {
|
||||
return Err(invalid(format!("{n_fat} FAT sectors in a {}-byte file", data.len())));
|
||||
}
|
||||
let mut cf = CompoundFile {
|
||||
data,
|
||||
version,
|
||||
header_clsid,
|
||||
sector_size,
|
||||
mini_cutoff,
|
||||
fat: Vec::new(),
|
||||
minifat: Vec::new(),
|
||||
mini_stream: Vec::new(),
|
||||
entries: Vec::new(),
|
||||
};
|
||||
// FAT sector numbers: 109 in the header, then the DIFAT chain.
|
||||
let mut fat_sectors: Vec<u32> = (0..109).map(|i| u32_at(data, 76 + i * 4)).filter(|&s| s <= MAXREGSECT).collect();
|
||||
let mut d = first_difat;
|
||||
let mut seen = 0;
|
||||
while d <= MAXREGSECT && fat_sectors.len() < n_fat {
|
||||
seen += 1;
|
||||
if seen > n_difat.max(1) + max_sectors {
|
||||
return Err(invalid("DIFAT chain loops"));
|
||||
}
|
||||
let s = cf.sector(d)?;
|
||||
let per = sector_size / 4 - 1;
|
||||
for i in 0..per {
|
||||
let v = u32_at(s, i * 4);
|
||||
if v <= MAXREGSECT {
|
||||
fat_sectors.push(v);
|
||||
}
|
||||
}
|
||||
d = u32_at(s, per * 4);
|
||||
}
|
||||
fat_sectors.truncate(n_fat);
|
||||
let mut fat = Vec::with_capacity(fat_sectors.len() * sector_size / 4);
|
||||
for s in fat_sectors {
|
||||
let b = cf.sector(s)?;
|
||||
fat.extend((0..sector_size / 4).map(|i| u32_at(b, i * 4)));
|
||||
}
|
||||
cf.fat = fat;
|
||||
// Directory.
|
||||
let dir = cf.read_chain(first_dir, None)?;
|
||||
let n_entries = dir.len() / 128;
|
||||
if n_entries == 0 {
|
||||
return Err(invalid("empty directory"));
|
||||
}
|
||||
for i in 0..n_entries {
|
||||
cf.entries.push(parse_entry(&dir[i * 128..(i + 1) * 128], version));
|
||||
}
|
||||
if cf.entries[0].kind != EntryKind::Root {
|
||||
return Err(invalid("first directory entry is not the root"));
|
||||
}
|
||||
// Mini FAT and mini stream.
|
||||
if first_minifat <= MAXREGSECT {
|
||||
let mf = cf.read_chain(first_minifat, None)?;
|
||||
cf.minifat = mf.chunks_exact(4).map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
|
||||
}
|
||||
let root = &cf.entries[0];
|
||||
if root.start <= MAXREGSECT && root.size > 0 {
|
||||
let size = root.size.min(data.len() as u64) as usize;
|
||||
cf.mini_stream = cf.read_chain(root.start, Some(size))?;
|
||||
}
|
||||
cf.build_tree()?;
|
||||
Ok(cf)
|
||||
}
|
||||
|
||||
/// Sector `n` (a whole sector; a truncated last sector is an error).
|
||||
fn sector(&self, n: u32) -> Result<&'a [u8]> {
|
||||
let start = (n as usize + 1).checked_mul(self.sector_size).ok_or_else(|| invalid("sector number overflow"))?;
|
||||
let end = start.checked_add(self.sector_size).ok_or_else(|| invalid("sector number overflow"))?;
|
||||
if n > MAXREGSECT || end > self.data.len() {
|
||||
return Err(invalid(format!("sector {n} is outside the file")));
|
||||
}
|
||||
Ok(&self.data[start..end])
|
||||
}
|
||||
|
||||
/// Concatenate a regular sector chain; `limit` stops once that many bytes are read.
|
||||
fn read_chain(&self, start: u32, limit: Option<usize>) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::new();
|
||||
let mut s = start;
|
||||
let mut steps = 0usize;
|
||||
while s != ENDOFCHAIN {
|
||||
if s > MAXREGSECT {
|
||||
return Err(invalid(format!("bad sector {s:#x} in a chain")));
|
||||
}
|
||||
steps += 1;
|
||||
if steps > self.fat.len().max(1) {
|
||||
return Err(invalid("sector chain loops"));
|
||||
}
|
||||
let b = self.sector(s)?;
|
||||
out.extend_from_slice(b);
|
||||
if limit.is_some_and(|l| out.len() >= l) {
|
||||
break;
|
||||
}
|
||||
s = *self.fat.get(s as usize).ok_or_else(|| invalid(format!("sector {s} has no FAT entry")))?;
|
||||
if s == FREESECT {
|
||||
return Err(invalid("chain runs into a free sector"));
|
||||
}
|
||||
}
|
||||
if let Some(l) = limit {
|
||||
if out.len() < l {
|
||||
return Err(invalid("stream is truncated"));
|
||||
}
|
||||
out.truncate(l);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn read_mini_chain(&self, start: u32, size: usize) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::with_capacity(size);
|
||||
let mut s = start;
|
||||
let mut steps = 0usize;
|
||||
while out.len() < size {
|
||||
if s > MAXREGSECT {
|
||||
return Err(invalid("mini stream chain ends early"));
|
||||
}
|
||||
steps += 1;
|
||||
if steps > self.minifat.len().max(1) {
|
||||
return Err(invalid("mini sector chain loops"));
|
||||
}
|
||||
let at = s as usize * MINI_SECTOR_SIZE;
|
||||
let b = self.mini_stream.get(at..at + MINI_SECTOR_SIZE).ok_or_else(|| invalid("mini sector outside the mini stream"))?;
|
||||
out.extend_from_slice(b);
|
||||
s = *self.minifat.get(s as usize).ok_or_else(|| invalid("mini sector has no mini FAT entry"))?;
|
||||
}
|
||||
out.truncate(size);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn build_tree(&mut self) -> Result<()> {
|
||||
let n = self.entries.len();
|
||||
let mut owner = vec![false; n];
|
||||
owner[0] = true;
|
||||
for parent in 0..n {
|
||||
if !matches!(self.entries[parent].kind, EntryKind::Root | EntryKind::Storage) {
|
||||
continue;
|
||||
}
|
||||
let mut kids = Vec::new();
|
||||
// in-order walk of the sibling tree, iterative, cycle-safe
|
||||
let mut stack: Vec<u32> = Vec::new();
|
||||
let mut cur = self.entries[parent].child;
|
||||
let mut steps = 0;
|
||||
while cur != NOSTREAM || !stack.is_empty() {
|
||||
while cur != NOSTREAM {
|
||||
let i = cur as usize;
|
||||
if i >= n || owner[i] {
|
||||
return Err(invalid(format!("directory entry {cur} is referenced twice or does not exist")));
|
||||
}
|
||||
owner[i] = true;
|
||||
stack.push(cur);
|
||||
cur = self.entries[i].left;
|
||||
steps += 1;
|
||||
if steps > n {
|
||||
return Err(invalid("directory tree loops"));
|
||||
}
|
||||
}
|
||||
if let Some(top) = stack.pop() {
|
||||
kids.push(top as usize);
|
||||
cur = self.entries[top as usize].right;
|
||||
}
|
||||
}
|
||||
self.entries[parent].children = kids;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &Entry {
|
||||
&self.entries[0]
|
||||
}
|
||||
|
||||
pub fn entry(&self, id: usize) -> Option<&Entry> {
|
||||
self.entries.get(id)
|
||||
}
|
||||
|
||||
/// Child of storage `parent` named `name` (case-insensitive as in §2.6.4).
|
||||
pub fn child(&self, parent: usize, name: &str) -> Option<usize> {
|
||||
self.entries.get(parent)?.children.iter().copied().find(|&c| compare_names(&self.entries[c].name, name).is_eq())
|
||||
}
|
||||
|
||||
/// Entry at a `/`-separated path below the root (`""` is the root).
|
||||
pub fn find(&self, path: &str) -> Result<usize> {
|
||||
let mut cur = 0;
|
||||
for part in path.split('/').filter(|p| !p.is_empty()) {
|
||||
cur = self.child(cur, part).ok_or_else(|| Error::NotFound(path.to_string()))?;
|
||||
}
|
||||
Ok(cur)
|
||||
}
|
||||
|
||||
/// Contents of stream `id`.
|
||||
pub fn read(&self, id: usize) -> Result<Vec<u8>> {
|
||||
let e = self.entries.get(id).ok_or_else(|| Error::NotFound(format!("entry {id}")))?;
|
||||
if e.kind != EntryKind::Stream {
|
||||
return Err(invalid(format!("{:?} is not a stream", e.name)));
|
||||
}
|
||||
let size = usize::try_from(e.size).map_err(|_| invalid("stream too large"))?;
|
||||
if size == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if size as u64 > self.data.len() as u64 {
|
||||
return Err(invalid(format!("stream {:?} is larger than the file", e.name)));
|
||||
}
|
||||
if e.size < self.mini_cutoff { self.read_mini_chain(e.start, size) } else { self.read_chain(e.start, Some(size)) }
|
||||
}
|
||||
|
||||
/// Contents of the stream at `path`.
|
||||
pub fn read_path(&self, path: &str) -> Result<Vec<u8>> {
|
||||
self.read(self.find(path)?)
|
||||
}
|
||||
|
||||
/// Sector size in bytes (512 or 4096).
|
||||
pub fn sector_size(&self) -> usize {
|
||||
self.sector_size
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_entry(b: &[u8], version: u16) -> Entry {
|
||||
let name_len = (u16_at(b, 64) as usize).min(64);
|
||||
let units: Vec<u16> = (0..name_len / 2).map(|i| u16_at(b, i * 2)).take_while(|&u| u != 0).collect();
|
||||
let kind = match b[66] {
|
||||
5 => EntryKind::Root,
|
||||
1 => EntryKind::Storage,
|
||||
2 => EntryKind::Stream,
|
||||
_ => EntryKind::Unknown,
|
||||
};
|
||||
let mut clsid = [0u8; 16];
|
||||
clsid.copy_from_slice(&b[80..96]);
|
||||
let size = u64_at(b, 120);
|
||||
Entry {
|
||||
name: String::from_utf16_lossy(&units),
|
||||
kind,
|
||||
clsid,
|
||||
state_bits: u32_at(b, 96),
|
||||
created: u64_at(b, 100),
|
||||
modified: u64_at(b, 108),
|
||||
start: u32_at(b, 116),
|
||||
// version 3 files may leave garbage in the high 32 bits (§2.6.3)
|
||||
size: if version == 3 { size & 0xFFFF_FFFF } else { size },
|
||||
children: Vec::new(),
|
||||
left: u32_at(b, 68),
|
||||
right: u32_at(b, 72),
|
||||
child: u32_at(b, 76),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
use crate::*;
|
||||
|
||||
fn pattern(n: usize, seed: u8) -> Vec<u8> {
|
||||
(0..n).map(|i| (i as u32).wrapping_mul(31).wrapping_add(seed as u32) as u8).collect()
|
||||
}
|
||||
|
||||
/// A tree with mini and regular streams, nested storages and many siblings.
|
||||
fn sample(version: Version, siblings: usize) -> (Vec<u8>, Vec<(String, Vec<u8>)>) {
|
||||
let mut w = Writer::new(version);
|
||||
w.set_clsid(Writer::ROOT, [7; 16]);
|
||||
let mut expect = Vec::new();
|
||||
let a = w.storage(Writer::ROOT, "Header-2", [1; 16]).unwrap();
|
||||
let b = w.storage(a, "Content-3b03", [2; 16]).unwrap();
|
||||
for (i, size) in [0usize, 1, 63, 64, 65, 4095, 4096, 4097, 20_000].iter().enumerate() {
|
||||
let name = format!("s{i}");
|
||||
let data = pattern(*size, i as u8);
|
||||
w.stream(b, &name, data.clone()).unwrap();
|
||||
expect.push((format!("Header-2/Content-3b03/{name}"), data));
|
||||
}
|
||||
for i in 0..siblings {
|
||||
let name = format!("Mobs-1901{{{i:x}}}");
|
||||
let s = w.storage(Writer::ROOT, &name, [3; 16]).unwrap();
|
||||
let data = pattern(10 + i * 7, 99);
|
||||
w.stream(s, "properties", data.clone()).unwrap();
|
||||
expect.push((format!("{name}/properties"), data));
|
||||
}
|
||||
(w.finish(), expect)
|
||||
}
|
||||
|
||||
fn check(bytes: &[u8], expect: &[(String, Vec<u8>)]) {
|
||||
let cf = CompoundFile::open(bytes).unwrap();
|
||||
assert_eq!(cf.root().clsid, [7; 16]);
|
||||
for (path, data) in expect {
|
||||
assert_eq!(&cf.read_path(path).unwrap(), data, "{path}");
|
||||
}
|
||||
let h = cf.find("Header-2").unwrap();
|
||||
assert_eq!(cf.entries[h].clsid, [1; 16]);
|
||||
assert_eq!(cf.entries[h].kind, EntryKind::Storage);
|
||||
// children come back sorted in CFB order
|
||||
for e in &cf.entries {
|
||||
for w in e.children.windows(2) {
|
||||
assert!(compare_names(&cf.entries[w[0]].name, &cf.entries[w[1]].name).is_lt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every path from a node to a nil leaf has the same number of black nodes, and no red node has
|
||||
/// a red child.
|
||||
fn check_red_black(bytes: &[u8]) {
|
||||
let cf = CompoundFile::open(bytes).unwrap();
|
||||
let raw = |i: u32| -> (u32, u32, u8) {
|
||||
let ss = cf.sector_size();
|
||||
let dir_start = u32::from_le_bytes(bytes[48..52].try_into().unwrap());
|
||||
// the writer stores the directory contiguously
|
||||
let at = (dir_start as usize + 1) * ss + i as usize * 128;
|
||||
let e = &bytes[at..at + 128];
|
||||
(u32::from_le_bytes(e[68..72].try_into().unwrap()), u32::from_le_bytes(e[72..76].try_into().unwrap()), e[67])
|
||||
};
|
||||
fn black_height(raw: &dyn Fn(u32) -> (u32, u32, u8), n: u32, parent_red: bool) -> usize {
|
||||
if n == NOSTREAM {
|
||||
return 1;
|
||||
}
|
||||
let (l, r, c) = raw(n);
|
||||
let red = c == 0;
|
||||
assert!(!(red && parent_red), "red node with a red parent");
|
||||
let hl = black_height(raw, l, red);
|
||||
let hr = black_height(raw, r, red);
|
||||
assert_eq!(hl, hr, "unbalanced black height");
|
||||
hl + usize::from(!red)
|
||||
}
|
||||
let dir_start = u32::from_le_bytes(bytes[48..52].try_into().unwrap());
|
||||
let ss = cf.sector_size();
|
||||
for i in 0..cf.entries.len() {
|
||||
let at = (dir_start as usize + 1) * ss + i * 128;
|
||||
let child = u32::from_le_bytes(bytes[at + 76..at + 80].try_into().unwrap());
|
||||
if child != NOSTREAM {
|
||||
let (_, _, c) = raw(child);
|
||||
assert_eq!(c, 1, "tree roots are black");
|
||||
black_height(&raw, child, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_v3_and_v4() {
|
||||
for v in [Version::V3, Version::V4] {
|
||||
for n in [0, 1, 2, 3, 7, 8, 40] {
|
||||
let (bytes, expect) = sample(v, n);
|
||||
assert_eq!(bytes.len() % v_sector(v), 0);
|
||||
check(&bytes, &expect);
|
||||
check_red_black(&bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn v_sector(v: Version) -> usize {
|
||||
if v == Version::V3 { 512 } else { 4096 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_fields() {
|
||||
let (b, _) = sample(Version::V4, 3);
|
||||
assert!(sniff(&b));
|
||||
assert_eq!(&b[24..26], &0x3Eu16.to_le_bytes());
|
||||
assert_eq!(&b[26..28], &4u16.to_le_bytes());
|
||||
assert_eq!(&b[30..32], &12u16.to_le_bytes());
|
||||
let (b, _) = sample(Version::V3, 3);
|
||||
assert_eq!(&b[26..28], &3u16.to_le_bytes());
|
||||
assert_eq!(&b[30..32], &9u16.to_le_bytes());
|
||||
assert_eq!(&b[40..44], &0u32.to_le_bytes(), "version 3 leaves the directory sector count 0");
|
||||
assert_eq!(CompoundFile::open(&b).unwrap().version, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_file_needs_difat() {
|
||||
// > 109 FAT sectors with 512-byte sectors: 109 * 128 sectors * 512 bytes ≈ 7.1 MB
|
||||
let mut w = Writer::new(Version::V3);
|
||||
let big = pattern(7_500_000, 5);
|
||||
w.stream(Writer::ROOT, "big", big.clone()).unwrap();
|
||||
w.stream(Writer::ROOT, "small", vec![1, 2, 3]).unwrap();
|
||||
let b = w.finish();
|
||||
let n_difat = u32::from_le_bytes(b[72..76].try_into().unwrap());
|
||||
assert!(n_difat >= 1);
|
||||
let cf = CompoundFile::open(&b).unwrap();
|
||||
assert_eq!(cf.read_path("big").unwrap(), big);
|
||||
assert_eq!(cf.read_path("small").unwrap(), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names() {
|
||||
assert!(compare_names("b", "aa").is_lt(), "shorter first");
|
||||
assert!(compare_names("abc", "ABD").is_lt());
|
||||
assert!(compare_names("Abc", "aBC").is_eq());
|
||||
assert!(validate_name("a/b").is_err());
|
||||
assert!(validate_name(&"x".repeat(32)).is_err());
|
||||
assert!(validate_name(&"x".repeat(31)).is_ok());
|
||||
let mut w = Writer::default();
|
||||
w.stream(Writer::ROOT, "Name", vec![]).unwrap();
|
||||
assert!(matches!(w.stream(Writer::ROOT, "NAME", vec![]), Err(Error::BadName(_))));
|
||||
let s = w.stream(Writer::ROOT, "x", vec![]).unwrap();
|
||||
assert!(w.storage(s, "y", [0; 16]).is_err(), "streams have no children");
|
||||
let cf_bytes = w.finish();
|
||||
let cf = CompoundFile::open(&cf_bytes).unwrap();
|
||||
assert!(cf.find("name").is_ok(), "lookups are case-insensitive");
|
||||
assert!(matches!(cf.find("nope"), Err(Error::NotFound(_))));
|
||||
assert!(cf.read(0).is_err(), "the root is not a stream");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_names() {
|
||||
let mut w = Writer::default();
|
||||
w.stream(Writer::ROOT, "Ünïcødé ストリーム", vec![9; 10]).unwrap();
|
||||
let b = w.finish();
|
||||
let cf = CompoundFile::open(&b).unwrap();
|
||||
assert_eq!(cf.read_path("üNÏCØDÉ ストリーム").unwrap(), vec![9; 10]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_cfb() {
|
||||
assert_eq!(CompoundFile::open(b"hello").unwrap_err(), Error::NotCfb);
|
||||
assert_eq!(CompoundFile::open(&[0u8; 600]).unwrap_err(), Error::NotCfb);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncation_never_panics() {
|
||||
let (b, expect) = sample(Version::V3, 20);
|
||||
let mut ok = 0;
|
||||
for cut in (0..b.len()).step_by(29) {
|
||||
if let Ok(cf) = CompoundFile::open(&b[..cut]) {
|
||||
ok += 1;
|
||||
for (path, data) in &expect {
|
||||
if let Ok(d) = cf.read_path(path) {
|
||||
assert_eq!(&d, data, "a stream that reads back is complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = ok;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_never_panics_or_hangs() {
|
||||
let (b, _) = sample(Version::V3, 12);
|
||||
let mut seed = 0x9E37_79B9_7F4A_7C15u64;
|
||||
for _ in 0..1500 {
|
||||
let mut g = b.clone();
|
||||
for _ in 0..6 {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
// bias towards the header, FAT and directory (the end of the file)
|
||||
let at = if seed & 1 == 0 { (seed >> 8) as usize % 512 } else { g.len() - 1 - (seed >> 8) as usize % 2048.min(g.len()) };
|
||||
g[at] = (seed >> 40) as u8;
|
||||
}
|
||||
if let Ok(cf) = CompoundFile::open(&g) {
|
||||
for i in 0..cf.entries.len() {
|
||||
let _ = cf.read(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
//! Writing compound files ([MS-CFB] §2.2–2.6).
|
||||
//!
|
||||
//! Layout written by [`Writer::finish`] (sector numbers ascending): the sectors of every stream of
|
||||
//! 4096 bytes or more, the mini stream, the mini FAT, the directory, the FAT, then DIFAT sectors
|
||||
//! when the FAT needs more than the 109 header slots. Every chain is contiguous. Sibling trees are
|
||||
//! balanced binary search trees coloured as valid red-black trees (§2.6.4).
|
||||
|
||||
use crate::{DIFSECT, ENDOFCHAIN, FATSECT, FREESECT, MINI_SECTOR_SIZE, MINI_STREAM_CUTOFF, NOSTREAM, Result, SIGNATURE, compare_names, validate_name};
|
||||
|
||||
/// File format version.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Version {
|
||||
/// Version 3: 512-byte sectors.
|
||||
V3,
|
||||
/// Version 4: 4096-byte sectors (AAF's "4K" files).
|
||||
#[default]
|
||||
V4,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
fn sector_size(self) -> usize {
|
||||
match self {
|
||||
Version::V3 => 512,
|
||||
Version::V4 => 4096,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A storage or stream added to a [`Writer`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct NodeId(pub usize);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Kind {
|
||||
Root,
|
||||
Storage,
|
||||
Stream,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Node {
|
||||
name: String,
|
||||
kind: Kind,
|
||||
clsid: [u8; 16],
|
||||
data: Vec<u8>,
|
||||
children: Vec<usize>,
|
||||
}
|
||||
|
||||
/// Builds a compound file in memory.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Writer {
|
||||
version: Version,
|
||||
nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
impl Default for Writer {
|
||||
fn default() -> Self {
|
||||
Writer::new(Version::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
/// The root storage.
|
||||
pub const ROOT: NodeId = NodeId(0);
|
||||
|
||||
pub fn new(version: Version) -> Self {
|
||||
Writer { version, nodes: vec![Node { name: "Root Entry".into(), kind: Kind::Root, clsid: [0; 16], data: Vec::new(), children: Vec::new() }] }
|
||||
}
|
||||
|
||||
fn add(&mut self, parent: NodeId, name: &str, kind: Kind, clsid: [u8; 16], data: Vec<u8>) -> Result<NodeId> {
|
||||
validate_name(name)?;
|
||||
let p = self.nodes.get(parent.0).ok_or_else(|| crate::Error::NotFound(format!("node {}", parent.0)))?;
|
||||
if p.kind == Kind::Stream {
|
||||
return Err(crate::Error::Invalid(format!("{:?} is a stream, not a storage", p.name)));
|
||||
}
|
||||
if p.children.iter().any(|&c| compare_names(&self.nodes[c].name, name).is_eq()) {
|
||||
return Err(crate::Error::BadName(format!("duplicate name {name}")));
|
||||
}
|
||||
let id = self.nodes.len();
|
||||
self.nodes.push(Node { name: name.to_string(), kind, clsid, data, children: Vec::new() });
|
||||
self.nodes[parent.0].children.push(id);
|
||||
Ok(NodeId(id))
|
||||
}
|
||||
|
||||
/// Add a storage below `parent`.
|
||||
pub fn storage(&mut self, parent: NodeId, name: &str, clsid: [u8; 16]) -> Result<NodeId> {
|
||||
self.add(parent, name, Kind::Storage, clsid, Vec::new())
|
||||
}
|
||||
|
||||
/// Add a stream below `parent`.
|
||||
pub fn stream(&mut self, parent: NodeId, name: &str, data: Vec<u8>) -> Result<NodeId> {
|
||||
self.add(parent, name, Kind::Stream, [0; 16], data)
|
||||
}
|
||||
|
||||
/// Set the class id of a storage (or the root).
|
||||
pub fn set_clsid(&mut self, node: NodeId, clsid: [u8; 16]) {
|
||||
if let Some(n) = self.nodes.get_mut(node.0) {
|
||||
n.clsid = clsid;
|
||||
}
|
||||
}
|
||||
|
||||
/// Lay the file out and return its bytes.
|
||||
pub fn finish(self) -> Vec<u8> {
|
||||
let ss = self.version.sector_size();
|
||||
let per_sector = ss / 4;
|
||||
// Directory order: depth-first pre-order from the root.
|
||||
let mut order = Vec::with_capacity(self.nodes.len());
|
||||
let mut stack = vec![0usize];
|
||||
while let Some(n) = stack.pop() {
|
||||
order.push(n);
|
||||
for &c in self.nodes[n].children.iter().rev() {
|
||||
stack.push(c);
|
||||
}
|
||||
}
|
||||
let mut dir_index = vec![0u32; self.nodes.len()];
|
||||
for (i, &n) in order.iter().enumerate() {
|
||||
dir_index[n] = i as u32;
|
||||
}
|
||||
|
||||
// Stream placement.
|
||||
let mut start = vec![ENDOFCHAIN; self.nodes.len()];
|
||||
let mut next_sector = 0u32;
|
||||
let mut big_chains: Vec<(u32, u32)> = Vec::new(); // (first, count)
|
||||
for &n in &order {
|
||||
let node = &self.nodes[n];
|
||||
if node.kind == Kind::Stream && node.data.len() as u64 >= MINI_STREAM_CUTOFF as u64 {
|
||||
let count = node.data.len().div_ceil(ss) as u32;
|
||||
start[n] = next_sector;
|
||||
big_chains.push((next_sector, count));
|
||||
next_sector += count;
|
||||
}
|
||||
}
|
||||
let mut next_mini = 0u32;
|
||||
let mut mini_chains: Vec<(u32, u32)> = Vec::new();
|
||||
for &n in &order {
|
||||
let node = &self.nodes[n];
|
||||
if node.kind == Kind::Stream && !node.data.is_empty() && (node.data.len() as u64) < MINI_STREAM_CUTOFF as u64 {
|
||||
let count = node.data.len().div_ceil(MINI_SECTOR_SIZE) as u32;
|
||||
start[n] = next_mini;
|
||||
mini_chains.push((next_mini, count));
|
||||
next_mini += count;
|
||||
}
|
||||
}
|
||||
let mini_bytes = next_mini as usize * MINI_SECTOR_SIZE;
|
||||
let mini_stream_sectors = mini_bytes.div_ceil(ss) as u32;
|
||||
let mini_stream_start = if mini_stream_sectors > 0 { next_sector } else { ENDOFCHAIN };
|
||||
next_sector += mini_stream_sectors;
|
||||
let minifat_sectors = (next_mini as usize * 4).div_ceil(ss) as u32;
|
||||
let minifat_start = if minifat_sectors > 0 { next_sector } else { ENDOFCHAIN };
|
||||
next_sector += minifat_sectors;
|
||||
let dir_sectors = (order.len() * 128).div_ceil(ss) as u32;
|
||||
let dir_start = next_sector;
|
||||
next_sector += dir_sectors;
|
||||
let base = next_sector as usize;
|
||||
let (mut fat_n, mut difat_n) = (1usize, 0usize);
|
||||
loop {
|
||||
let total = base + fat_n + difat_n;
|
||||
let need_fat = total.div_ceil(per_sector);
|
||||
let need_difat = if need_fat > 109 { (need_fat - 109).div_ceil(per_sector - 1) } else { 0 };
|
||||
if need_fat == fat_n && need_difat == difat_n {
|
||||
break;
|
||||
}
|
||||
fat_n = need_fat.max(fat_n);
|
||||
difat_n = need_difat.max(difat_n);
|
||||
}
|
||||
let fat_start = next_sector;
|
||||
let difat_start = fat_start + fat_n as u32;
|
||||
let total = base + fat_n + difat_n;
|
||||
|
||||
// FAT.
|
||||
let mut fat = vec![FREESECT; fat_n * per_sector];
|
||||
let chain = |first: u32, count: u32, fat: &mut [u32]| {
|
||||
for i in 0..count {
|
||||
fat[(first + i) as usize] = if i + 1 == count { ENDOFCHAIN } else { first + i + 1 };
|
||||
}
|
||||
};
|
||||
for &(f, c) in &big_chains {
|
||||
chain(f, c, &mut fat);
|
||||
}
|
||||
if mini_stream_sectors > 0 {
|
||||
chain(mini_stream_start, mini_stream_sectors, &mut fat);
|
||||
}
|
||||
if minifat_sectors > 0 {
|
||||
chain(minifat_start, minifat_sectors, &mut fat);
|
||||
}
|
||||
chain(dir_start, dir_sectors, &mut fat);
|
||||
for i in 0..fat_n {
|
||||
fat[fat_start as usize + i] = FATSECT;
|
||||
}
|
||||
for i in 0..difat_n {
|
||||
fat[difat_start as usize + i] = DIFSECT;
|
||||
}
|
||||
debug_assert!(total <= fat.len());
|
||||
|
||||
// Mini FAT.
|
||||
let mut minifat = vec![FREESECT; minifat_sectors as usize * per_sector];
|
||||
for &(f, c) in &mini_chains {
|
||||
for i in 0..c {
|
||||
minifat[(f + i) as usize] = if i + 1 == c { ENDOFCHAIN } else { f + i + 1 };
|
||||
}
|
||||
}
|
||||
|
||||
// Directory.
|
||||
let mut tree = vec![(NOSTREAM, NOSTREAM, NOSTREAM, 1u8); self.nodes.len()]; // left, right, child, colour
|
||||
for (p, node) in self.nodes.iter().enumerate() {
|
||||
if node.kind == Kind::Stream || node.children.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut kids = node.children.clone();
|
||||
kids.sort_by(|&a, &b| compare_names(&self.nodes[a].name, &self.nodes[b].name));
|
||||
let height = usize::BITS - (kids.len() + 1).leading_zeros() - 1; // floor(log2(n + 1))
|
||||
let perfect = (1usize << height) - 1 == kids.len();
|
||||
let root = build_tree(&kids, 0, perfect, height as usize, &mut tree, &dir_index);
|
||||
tree[p].2 = root;
|
||||
}
|
||||
let mut dir = Vec::with_capacity(dir_sectors as usize * ss);
|
||||
for &n in &order {
|
||||
let node = &self.nodes[n];
|
||||
let mut e = [0u8; 128];
|
||||
let units: Vec<u16> = node.name.encode_utf16().collect();
|
||||
for (i, u) in units.iter().enumerate() {
|
||||
e[i * 2..i * 2 + 2].copy_from_slice(&u.to_le_bytes());
|
||||
}
|
||||
e[64..66].copy_from_slice(&(((units.len() + 1) * 2) as u16).to_le_bytes());
|
||||
e[66] = match node.kind {
|
||||
Kind::Root => 5,
|
||||
Kind::Storage => 1,
|
||||
Kind::Stream => 2,
|
||||
};
|
||||
let (l, r, c, colour) = tree[n];
|
||||
e[67] = if node.kind == Kind::Root { 1 } else { colour };
|
||||
e[68..72].copy_from_slice(&l.to_le_bytes());
|
||||
e[72..76].copy_from_slice(&r.to_le_bytes());
|
||||
e[76..80].copy_from_slice(&c.to_le_bytes());
|
||||
e[80..96].copy_from_slice(&node.clsid);
|
||||
let (st, size) = match node.kind {
|
||||
Kind::Root => (mini_stream_start, mini_bytes as u64),
|
||||
Kind::Storage => (0, 0),
|
||||
Kind::Stream => (if node.data.is_empty() { ENDOFCHAIN } else { start[n] }, node.data.len() as u64),
|
||||
};
|
||||
e[116..120].copy_from_slice(&st.to_le_bytes());
|
||||
e[120..128].copy_from_slice(&size.to_le_bytes());
|
||||
dir.extend_from_slice(&e);
|
||||
}
|
||||
while dir.len() < dir_sectors as usize * ss {
|
||||
let mut e = [0u8; 128];
|
||||
e[68..80].copy_from_slice(&[0xFF; 12]);
|
||||
dir.extend_from_slice(&e);
|
||||
}
|
||||
|
||||
// Header.
|
||||
let mut out = Vec::with_capacity((total + 1) * ss);
|
||||
out.extend_from_slice(&SIGNATURE);
|
||||
out.extend_from_slice(&[0; 16]);
|
||||
out.extend_from_slice(&0x003Eu16.to_le_bytes());
|
||||
out.extend_from_slice(&(if self.version == Version::V3 { 3u16 } else { 4u16 }).to_le_bytes());
|
||||
out.extend_from_slice(&0xFFFEu16.to_le_bytes());
|
||||
out.extend_from_slice(&(if self.version == Version::V3 { 9u16 } else { 12u16 }).to_le_bytes());
|
||||
out.extend_from_slice(&6u16.to_le_bytes());
|
||||
out.extend_from_slice(&[0; 6]);
|
||||
out.extend_from_slice(&(if self.version == Version::V3 { 0 } else { dir_sectors }).to_le_bytes());
|
||||
out.extend_from_slice(&(fat_n as u32).to_le_bytes());
|
||||
out.extend_from_slice(&dir_start.to_le_bytes());
|
||||
out.extend_from_slice(&0u32.to_le_bytes());
|
||||
out.extend_from_slice(&MINI_STREAM_CUTOFF.to_le_bytes());
|
||||
out.extend_from_slice(&minifat_start.to_le_bytes());
|
||||
out.extend_from_slice(&minifat_sectors.to_le_bytes());
|
||||
out.extend_from_slice(&(if difat_n > 0 { difat_start } else { ENDOFCHAIN }).to_le_bytes());
|
||||
out.extend_from_slice(&(difat_n as u32).to_le_bytes());
|
||||
for i in 0..109 {
|
||||
let v = if i < fat_n { fat_start + i as u32 } else { FREESECT };
|
||||
out.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
out.resize(ss, 0);
|
||||
|
||||
// Sectors.
|
||||
let pad = |out: &mut Vec<u8>| {
|
||||
let r = out.len() % ss;
|
||||
if r != 0 {
|
||||
out.resize(out.len() + ss - r, 0);
|
||||
}
|
||||
};
|
||||
for &n in &order {
|
||||
let node = &self.nodes[n];
|
||||
if node.kind == Kind::Stream && node.data.len() as u64 >= MINI_STREAM_CUTOFF as u64 {
|
||||
out.extend_from_slice(&node.data);
|
||||
pad(&mut out);
|
||||
}
|
||||
}
|
||||
for &n in &order {
|
||||
let node = &self.nodes[n];
|
||||
if node.kind == Kind::Stream && !node.data.is_empty() && (node.data.len() as u64) < MINI_STREAM_CUTOFF as u64 {
|
||||
out.extend_from_slice(&node.data);
|
||||
let r = node.data.len() % MINI_SECTOR_SIZE;
|
||||
if r != 0 {
|
||||
out.resize(out.len() + MINI_SECTOR_SIZE - r, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
pad(&mut out);
|
||||
for v in &minifat {
|
||||
out.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
out.extend_from_slice(&dir);
|
||||
for v in &fat {
|
||||
out.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
let mut remaining: Vec<u32> = (109..fat_n).map(|i| fat_start + i as u32).collect();
|
||||
for i in 0..difat_n {
|
||||
let mut s = Vec::with_capacity(ss);
|
||||
let take = remaining.len().min(per_sector - 1);
|
||||
for v in remaining.drain(..take) {
|
||||
s.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
while s.len() < ss - 4 {
|
||||
s.extend_from_slice(&FREESECT.to_le_bytes());
|
||||
}
|
||||
let next = if i + 1 < difat_n { difat_start + i as u32 + 1 } else { ENDOFCHAIN };
|
||||
s.extend_from_slice(&next.to_le_bytes());
|
||||
out.extend_from_slice(&s);
|
||||
}
|
||||
debug_assert_eq!(out.len(), (total + 1) * ss);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Balanced BST of `kids[..]` (sorted). Nodes on the bottom level of an imperfect tree are red,
|
||||
/// so every root-to-leaf path has the same number of black nodes.
|
||||
fn build_tree(kids: &[usize], depth: usize, perfect: bool, height: usize, tree: &mut [(u32, u32, u32, u8)], dir_index: &[u32]) -> u32 {
|
||||
if kids.is_empty() {
|
||||
return NOSTREAM;
|
||||
}
|
||||
let mid = kids.len() / 2;
|
||||
let n = kids[mid];
|
||||
let left = build_tree(&kids[..mid], depth + 1, perfect, height, tree, dir_index);
|
||||
let right = build_tree(&kids[mid + 1..], depth + 1, perfect, height, tree, dir_index);
|
||||
tree[n].0 = left;
|
||||
tree[n].1 = right;
|
||||
tree[n].3 = if !perfect && depth == height { 0 } else { 1 };
|
||||
dir_index[n]
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ Design principles:
|
|||
L2 edit · codecs · interchange · captions · speech
|
||||
L1 frame · media · project · audio-dsp · text
|
||||
L0 foundation: time · geom · color · bitstream · testkit (dev-dependency only)
|
||||
codecs/containers: isobmff · matroska · mxf · ogg · h264 · h264enc · hevc · vp9 · av1 · prores · dnx · aac · opus
|
||||
codecs/containers: isobmff · matroska · mxf · cfb · ogg · h264 · h264enc · hevc · vp9 · av1 · prores · dnx · aac · opus
|
||||
```
|
||||
|
||||
Crates are named `filmcraft-<dir>` (`crates/time` is `filmcraft-time`). The apps are `filmcraft`
|
||||
|
|
@ -42,6 +42,7 @@ and `filmcraft-cli`.
|
|||
| `isobmff` | L0 | MP4/MOV demux and mux |
|
||||
| `matroska` | L0 | MKV/WebM demux |
|
||||
| `mxf` | L0 | MXF demux (SMPTE ST 377-1): OP1a / OP-Atom, index tables, AVC / VC-3 / ProRes / MPEG-2 identification, PCM / AES3 sound, timecode ([README](../crates/mxf/README.md)) |
|
||||
| `cfb` | L0 | Compound File Binary ([MS-CFB] structured storage) reader and writer, the container of AAF ([README](../crates/cfb/README.md)) |
|
||||
| `ogg` | L0 | Ogg demux (RFC 3533), Ogg Opus timing (RFC 7845 granules, pre-skip, end trimming), Vorbis headers |
|
||||
| `h264`, `h264enc` | L0 | H.264 decoder; H.264 encoder |
|
||||
| `hevc` | L0 | H.265 Main/Main 10 decoder |
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const LAYERS: &[(&str, u8)] = &[
|
|||
("isobmff", 0),
|
||||
("matroska", 0),
|
||||
("mxf", 0),
|
||||
("cfb", 0),
|
||||
("ogg", 0),
|
||||
("riff", 0),
|
||||
("h264", 0),
|
||||
|
|
|
|||
Loading…
Reference in New Issue