Skip to main content

pptxboss_core/
cfb.rs

1//! Compound File Binary containers (MS-CFB): the OLE storage behind legacy
2//! `.ppt` files and encrypted packages. The reader materializes the FAT,
3//! directory and mini stream once and serves streams by path, tolerating
4//! the header and chain defects the specification tells readers to expect.
5//! The writer produces a minimal version-3 file for fixtures and encrypted
6//! output.
7
8use std::sync::Arc;
9
10use crate::error::{Error, Result};
11use crate::zip::Source;
12
13pub const SIGNATURE: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
14const MAXREGSECT: u32 = 0xffff_fffa;
15const FATSECT: u32 = 0xffff_fffd;
16const ENDOFCHAIN: u32 = 0xffff_fffe;
17const FREESECT: u32 = 0xffff_ffff;
18const NOSTREAM: u32 = 0xffff_ffff;
19const HEADER_LEN: usize = 512;
20const HEADER_DIFAT_ENTRIES: usize = 109;
21const DIRECTORY_ENTRY_LEN: usize = 128;
22const MINI_SECTOR_LEN: usize = 64;
23const DEFAULT_MINI_CUTOFF: u64 = 4096;
24/// Iteration cap for every chain walk, well above any real file's sector count.
25const MAX_CHAIN: usize = 1 << 24;
26
27/// Whether `bytes` start with the compound file signature.
28pub fn is_compound(bytes: &[u8]) -> bool {
29    bytes.starts_with(&SIGNATURE)
30}
31
32/// One directory entry: a storage, a stream, or the root.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Entry {
35    pub name: String,
36    pub kind: EntryKind,
37    left: u32,
38    right: u32,
39    child: u32,
40    start: u32,
41    size: u64,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum EntryKind {
46    Storage,
47    Stream,
48    Root,
49    Unallocated,
50}
51
52/// A parsed compound file.
53pub struct Compound {
54    data: Vec<u8>,
55    sector_len: usize,
56    fat: Vec<u32>,
57    mini_fat: Vec<u32>,
58    mini_stream: Vec<u8>,
59    mini_cutoff: u64,
60    entries: Vec<Entry>,
61}
62
63impl Compound {
64    /// Reads the whole source and parses its container structures.
65    pub fn open(source: &dyn Source) -> Result<Self> {
66        let len =
67            usize::try_from(source.len()).map_err(|_| Error::Other("file too large".into()))?;
68        let mut data = vec![0u8; len];
69        source.read_at(0, &mut data)?;
70        Self::from_bytes(data)
71    }
72
73    pub fn open_source(source: Arc<dyn Source>) -> Result<Self> {
74        Self::open(source.as_ref())
75    }
76
77    pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
78        if data.len() < HEADER_LEN || !is_compound(&data) {
79            return Err(cfb("missing compound file signature"));
80        }
81        let major = u16_at(&data, 0x1a);
82        let sector_shift = u16_at(&data, 0x1e);
83        if !matches!(major, 3 | 4) {
84            return Err(cfb(&format!("unsupported major version {major}")));
85        }
86        if !matches!(sector_shift, 9 | 12) {
87            return Err(cfb(&format!("unsupported sector shift {sector_shift}")));
88        }
89        let sector_len = 1usize << sector_shift;
90        if data.len() < sector_len * 3 {
91            return Err(cfb("file shorter than three sectors"));
92        }
93        let fat_sector_count = u32_at(&data, 0x2c) as usize;
94        let first_directory = u32_at(&data, 0x30);
95        let mut mini_cutoff = u64::from(u32_at(&data, 0x38));
96        if mini_cutoff == 0 {
97            mini_cutoff = DEFAULT_MINI_CUTOFF;
98        }
99        let first_mini_fat = u32_at(&data, 0x3c);
100        let mini_fat_count = u32_at(&data, 0x40) as usize;
101        let first_difat = u32_at(&data, 0x44);
102        let difat_count = u32_at(&data, 0x48) as usize;
103
104        let mut difat: Vec<u32> = (0..HEADER_DIFAT_ENTRIES)
105            .map(|i| u32_at(&data, 0x4c + i * 4))
106            .take_while(|&sector| sector <= MAXREGSECT)
107            .collect();
108        let mut next = first_difat;
109        let mut visited = 0usize;
110        while next <= MAXREGSECT && visited < difat_count.max(1) && visited < MAX_CHAIN {
111            let Some(sector) = sector_bytes(&data, sector_len, next) else {
112                break;
113            };
114            let entries = sector_len / 4 - 1;
115            difat.extend(
116                (0..entries)
117                    .map(|i| u32_at(sector, i * 4))
118                    .take_while(|&s| s <= MAXREGSECT),
119            );
120            next = u32_at(sector, entries * 4);
121            visited += 1;
122        }
123        if fat_sector_count > 0 {
124            difat.truncate(fat_sector_count);
125        }
126
127        let mut fat = Vec::with_capacity(difat.len() * (sector_len / 4));
128        for &sector in &difat {
129            let Some(bytes) = sector_bytes(&data, sector_len, sector) else {
130                break;
131            };
132            fat.extend((0..sector_len / 4).map(|i| u32_at(bytes, i * 4)));
133        }
134
135        let directory = read_chain(&data, sector_len, &fat, first_directory, u64::MAX);
136        let mut entries = Vec::with_capacity(directory.len() / DIRECTORY_ENTRY_LEN);
137        for raw in directory.as_chunks::<DIRECTORY_ENTRY_LEN>().0 {
138            entries.push(parse_entry(raw, major));
139        }
140        if entries.is_empty() {
141            return Err(cfb("empty directory"));
142        }
143
144        let root = &entries[0];
145        let mini_stream = match root.size {
146            0 => Vec::new(),
147            size => read_chain(&data, sector_len, &fat, root.start, size),
148        };
149        let mini_fat_bytes = match mini_fat_count {
150            0 => Vec::new(),
151            _ => read_chain(&data, sector_len, &fat, first_mini_fat, u64::MAX),
152        };
153        let mini_fat = mini_fat_bytes
154            .as_chunks::<4>()
155            .0
156            .iter()
157            .map(|c| u32::from_le_bytes(*c))
158            .collect();
159
160        Ok(Self {
161            data,
162            sector_len,
163            fat,
164            mini_fat,
165            mini_stream,
166            mini_cutoff,
167            entries,
168        })
169    }
170
171    pub fn entries(&self) -> &[Entry] {
172        &self.entries
173    }
174
175    /// Full paths (`storage/stream`) of every stream, in directory order.
176    pub fn stream_paths(&self) -> Vec<String> {
177        let mut paths = Vec::new();
178        self.collect_paths(0, "", &mut paths, &mut vec![false; self.entries.len()]);
179        paths
180    }
181
182    fn collect_paths(&self, id: u32, prefix: &str, out: &mut Vec<String>, seen: &mut Vec<bool>) {
183        let Some(entry) = self.entries.get(id as usize) else {
184            return;
185        };
186        let child = entry.child;
187        let mut stack = vec![child];
188        while let Some(current) = stack.pop() {
189            if current == NOSTREAM
190                || current as usize >= self.entries.len()
191                || seen[current as usize]
192            {
193                continue;
194            }
195            seen[current as usize] = true;
196            let node = &self.entries[current as usize];
197            let path = match prefix.is_empty() {
198                true => node.name.clone(),
199                false => format!("{prefix}/{}", node.name),
200            };
201            match node.kind {
202                EntryKind::Stream => out.push(path),
203                EntryKind::Storage => self.collect_paths(current, &path, out, seen),
204                _ => {}
205            }
206            stack.push(node.left);
207            stack.push(node.right);
208        }
209    }
210
211    /// The entry at `path` (components separated by `/`), matched ASCII
212    /// case-insensitively by a bounded search of each storage's tree.
213    pub fn entry(&self, path: &str) -> Option<&Entry> {
214        let mut current = self.entries.first()?;
215        for component in path.split('/').filter(|part| !part.is_empty()) {
216            current = self.find_child(current.child, component)?;
217        }
218        Some(current)
219    }
220
221    fn find_child(&self, root: u32, name: &str) -> Option<&Entry> {
222        let mut stack = vec![root];
223        let mut seen = vec![false; self.entries.len()];
224        while let Some(id) = stack.pop() {
225            if id == NOSTREAM || id as usize >= self.entries.len() || seen[id as usize] {
226                continue;
227            }
228            seen[id as usize] = true;
229            let entry = &self.entries[id as usize];
230            if entry.kind != EntryKind::Unallocated && entry.name.eq_ignore_ascii_case(name) {
231                return Some(entry);
232            }
233            stack.push(entry.left);
234            stack.push(entry.right);
235        }
236        None
237    }
238
239    pub fn has_stream(&self, path: &str) -> bool {
240        self.entry(path)
241            .is_some_and(|entry| entry.kind == EntryKind::Stream)
242    }
243
244    /// The bytes of the stream at `path`, or None when it does not exist.
245    pub fn stream(&self, path: &str) -> Option<Vec<u8>> {
246        let entry = self.entry(path)?;
247        if entry.kind != EntryKind::Stream {
248            return None;
249        }
250        Some(self.read_entry(entry))
251    }
252
253    fn read_entry(&self, entry: &Entry) -> Vec<u8> {
254        if entry.size == 0 {
255            return Vec::new();
256        }
257        if entry.size < self.mini_cutoff {
258            let mut out = Vec::with_capacity(entry.size as usize);
259            let mut sector = entry.start;
260            let mut steps = 0usize;
261            while sector <= MAXREGSECT && (out.len() as u64) < entry.size && steps < MAX_CHAIN {
262                let start = sector as usize * MINI_SECTOR_LEN;
263                let Some(bytes) = self.mini_stream.get(start..) else {
264                    break;
265                };
266                let take = bytes.len().min(MINI_SECTOR_LEN);
267                out.extend_from_slice(&bytes[..take]);
268                sector = self
269                    .mini_fat
270                    .get(sector as usize)
271                    .copied()
272                    .unwrap_or(ENDOFCHAIN);
273                steps += 1;
274            }
275            out.truncate(entry.size as usize);
276            return out;
277        }
278        read_chain(
279            &self.data,
280            self.sector_len,
281            &self.fat,
282            entry.start,
283            entry.size,
284        )
285    }
286}
287
288fn cfb(msg: &str) -> Error {
289    Error::Other(format!("compound file: {msg}"))
290}
291
292fn u16_at(bytes: &[u8], offset: usize) -> u16 {
293    u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
294}
295
296fn u32_at(bytes: &[u8], offset: usize) -> u32 {
297    u32::from_le_bytes([
298        bytes[offset],
299        bytes[offset + 1],
300        bytes[offset + 2],
301        bytes[offset + 3],
302    ])
303}
304
305/// The bytes of regular sector `sector`, or None past the end of the file.
306fn sector_bytes(data: &[u8], sector_len: usize, sector: u32) -> Option<&[u8]> {
307    let start = (sector as usize + 1).checked_mul(sector_len)?;
308    let end = start.checked_add(sector_len)?;
309    match end <= data.len() {
310        true => Some(&data[start..end]),
311        false => data.get(start..).filter(|rest| !rest.is_empty()),
312    }
313}
314
315/// Concatenates the FAT chain from `start`, up to `size` bytes (or the whole chain).
316fn read_chain(data: &[u8], sector_len: usize, fat: &[u32], start: u32, size: u64) -> Vec<u8> {
317    let mut out = Vec::new();
318    let mut sector = start;
319    let mut steps = 0usize;
320    while sector <= MAXREGSECT && (out.len() as u64) < size && steps < MAX_CHAIN {
321        let Some(bytes) = sector_bytes(data, sector_len, sector) else {
322            break;
323        };
324        out.extend_from_slice(bytes);
325        sector = fat.get(sector as usize).copied().unwrap_or(ENDOFCHAIN);
326        steps += 1;
327    }
328    if size != u64::MAX {
329        out.truncate(size as usize);
330    }
331    out
332}
333
334fn parse_entry(raw: &[u8], major: u16) -> Entry {
335    let declared = usize::from(u16_at(raw, 0x40)).min(64);
336    let name_bytes = &raw[..declared];
337    let units: Vec<u16> = name_bytes
338        .as_chunks::<2>()
339        .0
340        .iter()
341        .map(|c| u16::from_le_bytes(*c))
342        .take_while(|&unit| unit != 0)
343        .collect();
344    let name = String::from_utf16_lossy(&units);
345    let kind = match raw[0x42] {
346        1 => EntryKind::Storage,
347        2 => EntryKind::Stream,
348        5 => EntryKind::Root,
349        _ => EntryKind::Unallocated,
350    };
351    let mut size = u64::from(u32_at(raw, 0x78)) | (u64::from(u32_at(raw, 0x7c)) << 32);
352    if major == 3 {
353        size &= 0xffff_ffff;
354    }
355    Entry {
356        name,
357        kind,
358        left: u32_at(raw, 0x44),
359        right: u32_at(raw, 0x48),
360        child: u32_at(raw, 0x4c),
361        start: u32_at(raw, 0x74),
362        size,
363    }
364}
365
366/// Builds a minimal version-3 compound file: root storage with streams,
367/// small streams in the mini stream, every directory node black.
368#[derive(Default)]
369pub struct Writer {
370    streams: Vec<(String, Vec<u8>)>,
371}
372
373impl Writer {
374    pub fn new() -> Self {
375        Self::default()
376    }
377
378    /// Adds a stream at the root storage (nested storages are not written).
379    pub fn stream(mut self, name: &str, data: &[u8]) -> Self {
380        self.streams.push((name.to_string(), data.to_vec()));
381        self
382    }
383
384    pub fn build(&self) -> Vec<u8> {
385        const SECTOR: usize = 512;
386        let mut mini_stream: Vec<u8> = Vec::new();
387        let mut mini_fat: Vec<u32> = Vec::new();
388        // Regular streams are laid out after: FAT sectors, directory sectors, mini FAT sectors, mini stream.
389        let mut entries: Vec<(String, u32, u64, bool)> = Vec::new(); // name, start, size, in mini stream
390        let mut regular: Vec<Vec<u8>> = Vec::new();
391        for (name, data) in &self.streams {
392            if (data.len() as u64) < DEFAULT_MINI_CUTOFF && !data.is_empty() {
393                let first = (mini_stream.len() / MINI_SECTOR_LEN) as u32;
394                let sectors = data.len().div_ceil(MINI_SECTOR_LEN);
395                mini_stream.extend_from_slice(data);
396                mini_stream.resize(
397                    mini_stream.len().div_ceil(MINI_SECTOR_LEN) * MINI_SECTOR_LEN,
398                    0,
399                );
400                for i in 0..sectors {
401                    mini_fat.push(match i + 1 == sectors {
402                        true => ENDOFCHAIN,
403                        false => first + i as u32 + 1,
404                    });
405                }
406                entries.push((name.clone(), first, data.len() as u64, true));
407                continue;
408            }
409            entries.push((name.clone(), 0, data.len() as u64, false));
410            regular.push(data.clone());
411        }
412        let directory_entries = 1 + entries.len();
413        let directory_sectors = directory_entries
414            .div_ceil(SECTOR / DIRECTORY_ENTRY_LEN)
415            .max(1);
416        let mini_fat_sectors = match mini_fat.is_empty() {
417            true => 0,
418            false => (mini_fat.len() * 4).div_ceil(SECTOR),
419        };
420        let mini_stream_sectors = mini_stream.len().div_ceil(SECTOR);
421        let regular_sectors: usize = regular.iter().map(|d| d.len().div_ceil(SECTOR)).sum();
422        let mut fat_sectors = 1usize;
423        loop {
424            let total = fat_sectors
425                + directory_sectors
426                + mini_fat_sectors
427                + mini_stream_sectors
428                + regular_sectors;
429            if total <= fat_sectors * (SECTOR / 4) {
430                break;
431            }
432            fat_sectors += 1;
433        }
434        let total_sectors = fat_sectors
435            + directory_sectors
436            + mini_fat_sectors
437            + mini_stream_sectors
438            + regular_sectors;
439        let mut fat: Vec<u32> = vec![FREESECT; fat_sectors * (SECTOR / 4)];
440        let mut next_sector = 0u32;
441        let mut allocate = |count: usize, fat: &mut Vec<u32>| -> u32 {
442            let start = next_sector;
443            for i in 0..count {
444                let s = start as usize + i;
445                fat[s] = match i + 1 == count {
446                    true => ENDOFCHAIN,
447                    false => start + i as u32 + 1,
448                };
449            }
450            next_sector += count as u32;
451            start
452        };
453        let fat_start = allocate(fat_sectors, &mut fat);
454        for i in 0..fat_sectors {
455            fat[fat_start as usize + i] = FATSECT;
456        }
457        let directory_start = allocate(directory_sectors, &mut fat);
458        let mini_fat_start = match mini_fat_sectors {
459            0 => ENDOFCHAIN,
460            n => allocate(n, &mut fat),
461        };
462        let mini_stream_start = match mini_stream_sectors {
463            0 => ENDOFCHAIN,
464            n => allocate(n, &mut fat),
465        };
466        let mut regular_iter = regular.iter();
467        for entry in entries.iter_mut() {
468            if entry.3 {
469                continue;
470            }
471            let data = regular_iter.next().expect("one regular stream per entry");
472            entry.1 = match data.is_empty() {
473                true => ENDOFCHAIN,
474                false => allocate(data.len().div_ceil(SECTOR), &mut fat),
475            };
476        }
477
478        let mut out = Vec::with_capacity((total_sectors + 1) * SECTOR);
479        out.extend_from_slice(&SIGNATURE);
480        out.extend_from_slice(&[0u8; 16]);
481        out.extend_from_slice(&0x003eu16.to_le_bytes());
482        out.extend_from_slice(&3u16.to_le_bytes());
483        out.extend_from_slice(&0xfffeu16.to_le_bytes());
484        out.extend_from_slice(&9u16.to_le_bytes());
485        out.extend_from_slice(&6u16.to_le_bytes());
486        out.extend_from_slice(&[0u8; 6]);
487        out.extend_from_slice(&0u32.to_le_bytes());
488        out.extend_from_slice(&(fat_sectors as u32).to_le_bytes());
489        out.extend_from_slice(&directory_start.to_le_bytes());
490        out.extend_from_slice(&0u32.to_le_bytes());
491        out.extend_from_slice(&(DEFAULT_MINI_CUTOFF as u32).to_le_bytes());
492        out.extend_from_slice(&mini_fat_start.to_le_bytes());
493        out.extend_from_slice(&(mini_fat_sectors as u32).to_le_bytes());
494        out.extend_from_slice(&ENDOFCHAIN.to_le_bytes());
495        out.extend_from_slice(&0u32.to_le_bytes());
496        for i in 0..HEADER_DIFAT_ENTRIES {
497            let value = match i < fat_sectors {
498                true => fat_start + i as u32,
499                false => FREESECT,
500            };
501            out.extend_from_slice(&value.to_le_bytes());
502        }
503        debug_assert_eq!(out.len(), HEADER_LEN);
504        for value in &fat {
505            out.extend_from_slice(&value.to_le_bytes());
506        }
507
508        let order = sorted_order(&entries);
509        let tree = balanced_tree(&order);
510        let mut directory = Vec::with_capacity(directory_sectors * SECTOR);
511        let root_child = tree.root.map_or(NOSTREAM, |id| id as u32 + 1);
512        directory.extend(directory_entry(
513            "Root Entry",
514            5,
515            NOSTREAM,
516            NOSTREAM,
517            root_child,
518            match mini_stream.is_empty() {
519                true => ENDOFCHAIN,
520                false => mini_stream_start,
521            },
522            mini_stream.len() as u64,
523        ));
524        for (index, (name, start, size, _)) in entries.iter().enumerate() {
525            let (left, right) = tree.links[index];
526            directory.extend(directory_entry(
527                name,
528                2,
529                left.map_or(NOSTREAM, |id| id as u32 + 1),
530                right.map_or(NOSTREAM, |id| id as u32 + 1),
531                NOSTREAM,
532                *start,
533                *size,
534            ));
535        }
536        while directory.len() < directory_sectors * SECTOR {
537            directory.extend(directory_entry("", 0, NOSTREAM, NOSTREAM, NOSTREAM, 0, 0));
538        }
539        out.extend_from_slice(&directory);
540        if mini_fat_sectors > 0 {
541            let mut bytes = Vec::with_capacity(mini_fat_sectors * SECTOR);
542            for value in &mini_fat {
543                bytes.extend_from_slice(&value.to_le_bytes());
544            }
545            bytes.resize(mini_fat_sectors * SECTOR, 0xff);
546            out.extend_from_slice(&bytes);
547        }
548        if mini_stream_sectors > 0 {
549            let mut bytes = mini_stream.clone();
550            bytes.resize(mini_stream_sectors * SECTOR, 0);
551            out.extend_from_slice(&bytes);
552        }
553        for data in &regular {
554            let mut bytes = data.clone();
555            bytes.resize(data.len().div_ceil(SECTOR) * SECTOR, 0);
556            out.extend_from_slice(&bytes);
557        }
558        out
559    }
560}
561
562/// Entry indexes ordered as the directory tree requires: shorter names
563/// first, then by upper-cased UTF-16 code units.
564fn sorted_order(entries: &[(String, u32, u64, bool)]) -> Vec<usize> {
565    let key = |name: &str| -> (usize, Vec<u16>) {
566        let units: Vec<u16> = name
567            .encode_utf16()
568            .map(|unit| match char::from_u32(u32::from(unit)) {
569                Some(ch) if ch.is_ascii_lowercase() => unit - 32,
570                _ => unit,
571            })
572            .collect();
573        ((units.len() + 1) * 2, units)
574    };
575    let mut order: Vec<usize> = (0..entries.len()).collect();
576    order.sort_by_key(|&i| key(&entries[i].0));
577    order
578}
579
580struct Tree {
581    root: Option<usize>,
582    /// `(left, right)` per entry index.
583    links: Vec<(Option<usize>, Option<usize>)>,
584}
585
586fn balanced_tree(order: &[usize]) -> Tree {
587    let mut links = vec![(None, None); order.len()];
588    let root = build_subtree(order, &mut links);
589    Tree { root, links }
590}
591
592fn build_subtree(order: &[usize], links: &mut [(Option<usize>, Option<usize>)]) -> Option<usize> {
593    if order.is_empty() {
594        return None;
595    }
596    let middle = order.len() / 2;
597    let node = order[middle];
598    let left = build_subtree(&order[..middle], links);
599    let right = build_subtree(&order[middle + 1..], links);
600    links[node] = (left, right);
601    Some(node)
602}
603
604fn directory_entry(
605    name: &str,
606    kind: u8,
607    left: u32,
608    right: u32,
609    child: u32,
610    start: u32,
611    size: u64,
612) -> Vec<u8> {
613    let mut entry = vec![0u8; DIRECTORY_ENTRY_LEN];
614    let units: Vec<u16> = name.encode_utf16().take(31).collect();
615    for (i, unit) in units.iter().enumerate() {
616        entry[i * 2..i * 2 + 2].copy_from_slice(&unit.to_le_bytes());
617    }
618    let name_len = match name.is_empty() {
619        true => 0u16,
620        false => (units.len() as u16 + 1) * 2,
621    };
622    entry[0x40..0x42].copy_from_slice(&name_len.to_le_bytes());
623    entry[0x42] = kind;
624    entry[0x43] = 1;
625    entry[0x44..0x48].copy_from_slice(&left.to_le_bytes());
626    entry[0x48..0x4c].copy_from_slice(&right.to_le_bytes());
627    entry[0x4c..0x50].copy_from_slice(&child.to_le_bytes());
628    entry[0x74..0x78].copy_from_slice(&start.to_le_bytes());
629    entry[0x78..0x80].copy_from_slice(&size.to_le_bytes());
630    entry
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn written_files_read_back_with_mini_and_regular_streams() {
639        let big: Vec<u8> = (0..10_000u32).map(|i| (i % 251) as u8).collect();
640        let bytes = Writer::new()
641            .stream("Current User", b"tiny")
642            .stream("PowerPoint Document", &big)
643            .stream("Pictures", &[7u8; 100])
644            .stream("\u{5}SummaryInformation", b"")
645            .build();
646        assert!(is_compound(&bytes));
647        assert_eq!(bytes.len() % 512, 0);
648        let compound = Compound::from_bytes(bytes).unwrap();
649        assert_eq!(compound.stream("Current User").unwrap(), b"tiny");
650        assert_eq!(compound.stream("powerpoint document").unwrap(), big);
651        assert_eq!(compound.stream("Pictures").unwrap(), vec![7u8; 100]);
652        assert_eq!(compound.stream("\u{5}SummaryInformation").unwrap(), b"");
653        assert!(compound.stream("Missing").is_none());
654        let mut paths = compound.stream_paths();
655        paths.sort();
656        assert_eq!(
657            paths,
658            [
659                "\u{5}SummaryInformation",
660                "Current User",
661                "Pictures",
662                "PowerPoint Document"
663            ]
664        );
665    }
666
667    #[test]
668    fn many_small_streams_span_several_directory_sectors() {
669        let mut writer = Writer::new();
670        for i in 0..40 {
671            writer = writer.stream(&format!("s{i}"), format!("payload {i}").as_bytes());
672        }
673        let compound = Compound::from_bytes(writer.build()).unwrap();
674        for i in 0..40 {
675            assert_eq!(
676                compound.stream(&format!("s{i}")).unwrap(),
677                format!("payload {i}").as_bytes()
678            );
679        }
680        assert_eq!(compound.stream_paths().len(), 40);
681    }
682
683    #[test]
684    fn rejects_non_compound_and_truncated_input() {
685        assert!(Compound::from_bytes(b"PK\x03\x04".to_vec()).is_err());
686        let mut header = vec![0u8; 600];
687        header[..8].copy_from_slice(&SIGNATURE);
688        assert!(Compound::from_bytes(header).is_err());
689        let bytes = Writer::new().stream("a", b"x").build();
690        let cut = bytes[..bytes.len() - 700].to_vec();
691        let compound = Compound::from_bytes(cut);
692        if let Ok(compound) = compound {
693            let _ = compound.stream("a");
694        }
695    }
696}