Skip to main content

extend_docx/doc/
cfb.rs

1//! Read-only parser for OLE2 / Compound File Binary (CFB) containers ([MS-CFB]).
2//! Legacy .doc files store their streams (`WordDocument`, `0Table`/`1Table`, `Data`)
3//! inside this container format.
4
5use std::collections::{HashMap, HashSet};
6
7pub const CFB_SIGNATURE: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
8
9const SECT_END_OF_CHAIN: u32 = 0xFFFF_FFFE;
10const SECT_FREE: u32 = 0xFFFF_FFFF;
11const DIR_ENTRY_SIZE: usize = 128;
12const OBJECT_TYPE_STREAM: u8 = 2;
13const OBJECT_TYPE_ROOT: u8 = 5;
14const MAX_CHAIN_SECTORS: usize = 1 << 22;
15
16pub struct CompoundFile {
17    streams: HashMap<String, Result<Vec<u8>, String>>,
18}
19
20struct DirEntry {
21    name: String,
22    object_type: u8,
23    left: u32,
24    right: u32,
25    child: u32,
26    start_sector: u32,
27    size: u64,
28}
29
30fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, String> {
31    bytes
32        .get(offset..offset + 2)
33        .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
34        .ok_or_else(|| "CFB: unexpected end of data reading u16".to_string())
35}
36
37fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, String> {
38    bytes
39        .get(offset..offset + 4)
40        .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
41        .ok_or_else(|| "CFB: unexpected end of data reading u32".to_string())
42}
43
44fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, String> {
45    bytes
46        .get(offset..offset + 8)
47        .map(|chunk| {
48            u64::from_le_bytes([
49                chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
50            ])
51        })
52        .ok_or_else(|| "CFB: unexpected end of data reading u64".to_string())
53}
54
55pub fn is_cfb(bytes: &[u8]) -> bool {
56    bytes.len() >= 8 && bytes[..8] == CFB_SIGNATURE
57}
58
59impl CompoundFile {
60    pub fn parse(bytes: &[u8]) -> Result<CompoundFile, String> {
61        Self::parse_selected(bytes, None)
62    }
63
64    pub(crate) fn parse_selected(
65        bytes: &[u8],
66        selected: Option<&[&str]>,
67    ) -> Result<CompoundFile, String> {
68        if !is_cfb(bytes) {
69            return Err("CFB: missing compound file signature".to_string());
70        }
71        if bytes.len() < 512 {
72            return Err("CFB: file too small for header".to_string());
73        }
74
75        let major_version = read_u16(bytes, 26)?;
76        let sector_shift = read_u16(bytes, 30)? as u32;
77        let expected_shift = if major_version == 4 { 12 } else { 9 };
78        if sector_shift != expected_shift {
79            return Err(format!(
80                "CFB: unexpected sector shift {sector_shift} for version {major_version}"
81            ));
82        }
83        let sector_size = 1usize << sector_shift;
84
85        let dir_start = read_u32(bytes, 48)?;
86        let mini_cutoff = read_u32(bytes, 56)? as u64;
87        let mini_fat_start = read_u32(bytes, 60)?;
88        let difat_start = read_u32(bytes, 68)?;
89
90        let fat = build_fat(bytes, sector_size, difat_start)?;
91
92        let directory = read_chain(bytes, sector_size, &fat, dir_start)?;
93        let entries = parse_directory(&directory)?;
94        if entries.is_empty() {
95            return Err("CFB: empty directory".to_string());
96        }
97
98        let root = entries
99            .iter()
100            .find(|entry| entry.object_type == OBJECT_TYPE_ROOT)
101            .ok_or_else(|| "CFB: missing root directory entry".to_string())?;
102
103        let mini_stream =
104            read_sized_chain(bytes, sector_size, &fat, root.start_sector, Some(root.size));
105        let mini_fat = read_sector_table(bytes, sector_size, &fat, mini_fat_start);
106
107        let mut streams = HashMap::new();
108        let mut visited = vec![false; entries.len()];
109        let mut stack = vec![root.child];
110        while let Some(index) = stack.pop() {
111            let index = index as usize;
112            if index >= entries.len() || visited[index] {
113                continue;
114            }
115            visited[index] = true;
116            let entry = &entries[index];
117            stack.push(entry.left);
118            stack.push(entry.right);
119
120            if entry.object_type != OBJECT_TYPE_STREAM {
121                continue;
122            }
123            if selected.is_some_and(|names| {
124                !names
125                    .iter()
126                    .any(|name| entry.name.eq_ignore_ascii_case(name))
127            }) {
128                continue;
129            }
130
131            let data = if entry.size < mini_cutoff {
132                match (&mini_stream, &mini_fat) {
133                    (Ok(stream), Ok(fat)) => {
134                        read_mini_chain(stream, fat, entry.start_sector, entry.size)
135                    }
136                    (Err(error), _) | (_, Err(error)) => Err(error.clone()),
137                }
138            } else {
139                read_sized_chain(
140                    bytes,
141                    sector_size,
142                    &fat,
143                    entry.start_sector,
144                    Some(entry.size),
145                )
146            };
147            streams
148                .entry(entry.name.to_ascii_lowercase())
149                .or_insert(data);
150        }
151
152        Ok(CompoundFile { streams })
153    }
154
155    pub fn stream(&self, name: &str) -> Option<&[u8]> {
156        self.try_stream(name).ok().flatten()
157    }
158
159    pub(crate) fn take_stream(&mut self, name: &str) -> Result<Option<Vec<u8>>, String> {
160        self.streams.remove(&name.to_ascii_lowercase()).transpose()
161    }
162
163    pub fn try_stream(&self, name: &str) -> Result<Option<&[u8]>, String> {
164        self.streams
165            .get(&name.to_ascii_lowercase())
166            .map(|data| data.as_ref().map(Vec::as_slice).map_err(Clone::clone))
167            .transpose()
168    }
169}
170
171fn build_fat(bytes: &[u8], sector_size: usize, difat_start: u32) -> Result<Vec<u32>, String> {
172    let entries_per_sector = sector_size / 4;
173    let fat_count = read_u32(bytes, 44)? as usize;
174    if fat_count > bytes.len() / sector_size {
175        return Err("CFB: FAT size exceeds container".to_string());
176    }
177    let mut fat_sectors = Vec::new();
178
179    for index in 0..109 {
180        if fat_sectors.len() == fat_count {
181            break;
182        }
183        let sector = read_u32(bytes, 76 + index * 4)?;
184        if sector != SECT_FREE && sector != SECT_END_OF_CHAIN {
185            fat_sectors.push(sector);
186        }
187    }
188
189    let mut difat_sector = difat_start;
190    let mut difat_guard = 0usize;
191    while fat_sectors.len() < fat_count
192        && difat_sector != SECT_END_OF_CHAIN
193        && difat_sector != SECT_FREE
194    {
195        difat_guard += 1;
196        if difat_guard > MAX_CHAIN_SECTORS {
197            return Err("CFB: DIFAT chain too long".to_string());
198        }
199        let offset = sector_offset(difat_sector, sector_size);
200        for index in 0..entries_per_sector - 1 {
201            if fat_sectors.len() == fat_count {
202                break;
203            }
204            let sector = read_u32(bytes, offset + index * 4)?;
205            if sector != SECT_FREE && sector != SECT_END_OF_CHAIN {
206                fat_sectors.push(sector);
207            }
208        }
209        difat_sector = read_u32(bytes, offset + (entries_per_sector - 1) * 4)?;
210    }
211    if fat_sectors.len() != fat_count {
212        return Err("CFB: incomplete FAT sector list".to_string());
213    }
214
215    let mut fat = Vec::with_capacity(fat_sectors.len() * entries_per_sector);
216    for sector in fat_sectors {
217        let offset = sector_offset(sector, sector_size);
218        for index in 0..entries_per_sector {
219            fat.push(read_u32(bytes, offset + index * 4)?);
220        }
221    }
222    Ok(fat)
223}
224
225fn sector_offset(sector: u32, sector_size: usize) -> usize {
226    (sector as usize + 1) * sector_size
227}
228
229fn read_chain(
230    bytes: &[u8],
231    sector_size: usize,
232    fat: &[u32],
233    start_sector: u32,
234) -> Result<Vec<u8>, String> {
235    read_sized_chain(bytes, sector_size, fat, start_sector, None)
236}
237
238fn read_sized_chain(
239    bytes: &[u8],
240    sector_size: usize,
241    fat: &[u32],
242    start_sector: u32,
243    size: Option<u64>,
244) -> Result<Vec<u8>, String> {
245    let limit = size.map(|size| usize::try_from(size).unwrap_or(usize::MAX));
246    if limit.is_some_and(|size| size > bytes.len()) {
247        return Err("CFB: stream size exceeds container".to_string());
248    }
249    let mut data = Vec::new();
250    let mut sector = start_sector;
251    let mut visited = HashSet::new();
252    while sector != SECT_END_OF_CHAIN && sector != SECT_FREE {
253        if limit == Some(data.len()) {
254            break;
255        }
256        if !visited.insert(sector) || visited.len() > MAX_CHAIN_SECTORS {
257            return Err("CFB: sector chain too long (cycle?)".to_string());
258        }
259        let offset = sector_offset(sector, sector_size);
260        let remaining = limit.map(|size| size - data.len()).unwrap_or(sector_size);
261        let end = offset + sector_size.min(remaining);
262        if end > bytes.len() {
263            return Err("CFB: sector chain points past end of file".to_string());
264        }
265        data.extend_from_slice(&bytes[offset..end]);
266        sector = *fat
267            .get(sector as usize)
268            .ok_or_else(|| "CFB: sector index outside FAT".to_string())?;
269    }
270    if limit.is_some_and(|size| size != data.len()) {
271        return Err("CFB: stream chain shorter than declared size".to_string());
272    }
273    Ok(data)
274}
275
276fn read_sector_table(
277    bytes: &[u8],
278    sector_size: usize,
279    fat: &[u32],
280    start_sector: u32,
281) -> Result<Vec<u32>, String> {
282    if start_sector == SECT_END_OF_CHAIN || start_sector == SECT_FREE {
283        return Ok(Vec::new());
284    }
285    let raw = read_chain(bytes, sector_size, fat, start_sector)?;
286    Ok(raw
287        .chunks_exact(4)
288        .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
289        .collect())
290}
291
292fn read_mini_chain(
293    mini_stream: &[u8],
294    mini_fat: &[u32],
295    start_sector: u32,
296    size: u64,
297) -> Result<Vec<u8>, String> {
298    const MINI_SECTOR_SIZE: usize = 64;
299    let size = usize::try_from(size).map_err(|_| "CFB: mini stream size overflow".to_string())?;
300    if size > mini_stream.len() {
301        return Err("CFB: mini stream size exceeds root stream".to_string());
302    }
303    let mut data = Vec::with_capacity(size);
304    let mut sector = start_sector;
305    let mut visited = HashSet::new();
306    while sector != SECT_END_OF_CHAIN && sector != SECT_FREE {
307        if data.len() == size {
308            break;
309        }
310        if !visited.insert(sector) || visited.len() > MAX_CHAIN_SECTORS {
311            return Err("CFB: mini sector chain too long (cycle?)".to_string());
312        }
313        let offset = sector as usize * MINI_SECTOR_SIZE;
314        let end = offset + MINI_SECTOR_SIZE.min(size - data.len());
315        if end > mini_stream.len() {
316            return Err("CFB: mini sector outside mini stream".to_string());
317        }
318        data.extend_from_slice(&mini_stream[offset..end]);
319        sector = *mini_fat
320            .get(sector as usize)
321            .ok_or_else(|| "CFB: mini sector index outside mini FAT".to_string())?;
322    }
323    if data.len() != size {
324        return Err("CFB: mini chain shorter than declared size".to_string());
325    }
326    Ok(data)
327}
328
329fn parse_directory(directory: &[u8]) -> Result<Vec<DirEntry>, String> {
330    let mut entries = Vec::new();
331    for chunk in directory.chunks_exact(DIR_ENTRY_SIZE) {
332        let name_length = read_u16(chunk, 64)? as usize;
333        let object_type = chunk[66];
334        let name = if (2..=64).contains(&name_length) {
335            let units: Vec<u16> = chunk[..name_length - 2]
336                .chunks_exact(2)
337                .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
338                .collect();
339            String::from_utf16_lossy(&units)
340        } else {
341            String::new()
342        };
343        entries.push(DirEntry {
344            name,
345            object_type,
346            left: read_u32(chunk, 68)?,
347            right: read_u32(chunk, 72)?,
348            child: read_u32(chunk, 76)?,
349            start_sector: read_u32(chunk, 116)?,
350            size: read_u64(chunk, 120)? & 0xFFFF_FFFF,
351        });
352    }
353    Ok(entries)
354}
355
356#[cfg(test)]
357pub(crate) mod tests {
358    use super::*;
359
360    /// Builds a minimal valid CFB v3 container holding the given streams,
361    /// exercising both the regular FAT (>= 4096 bytes) and the mini stream path.
362    pub(crate) fn build_cfb(streams: &[(&str, &[u8])]) -> Vec<u8> {
363        const SECTOR: usize = 512;
364        let mut sectors: Vec<[u8; SECTOR]> = Vec::new();
365        let mut fat: Vec<u32> = Vec::new();
366
367        fn append_data(sectors: &mut Vec<[u8; SECTOR]>, fat: &mut Vec<u32>, data: &[u8]) -> u32 {
368            let start = sectors.len() as u32;
369            let count = data.len().div_ceil(SECTOR).max(1);
370            for index in 0..count {
371                let mut sector = [0u8; SECTOR];
372                let begin = index * SECTOR;
373                let end = (begin + SECTOR).min(data.len());
374                if begin < data.len() {
375                    sector[..end - begin].copy_from_slice(&data[begin..end]);
376                }
377                sectors.push(sector);
378                fat.push(if index + 1 == count {
379                    SECT_END_OF_CHAIN
380                } else {
381                    (start as usize + index + 1) as u32
382                });
383            }
384            start
385        }
386
387        // Mini stream: streams smaller than 4096 bytes.
388        let mut mini_stream: Vec<u8> = Vec::new();
389        let mut mini_fat: Vec<u32> = Vec::new();
390        let mut placements: Vec<(usize, u32, u64)> = Vec::new(); // (stream index, start sector, size)
391
392        for (index, (_, data)) in streams.iter().enumerate() {
393            if data.len() < 4096 {
394                let start = (mini_stream.len() / 64) as u32;
395                let count = data.len().div_ceil(64).max(1);
396                mini_stream.extend_from_slice(data);
397                let padded = count * 64;
398                mini_stream.resize(mini_stream.len() + padded - data.len(), 0);
399                for sector_index in 0..count {
400                    mini_fat.push(if sector_index + 1 == count {
401                        SECT_END_OF_CHAIN
402                    } else {
403                        start + sector_index as u32 + 1
404                    });
405                }
406                placements.push((index, start, data.len() as u64));
407            }
408        }
409
410        let mini_stream_start = append_data(&mut sectors, &mut fat, &mini_stream);
411        let mini_fat_bytes: Vec<u8> = mini_fat
412            .iter()
413            .flat_map(|value| value.to_le_bytes())
414            .collect();
415        let mini_fat_start = append_data(&mut sectors, &mut fat, &mini_fat_bytes);
416
417        for (index, (_, data)) in streams.iter().enumerate() {
418            if data.len() >= 4096 {
419                let start = append_data(&mut sectors, &mut fat, data);
420                placements.push((index, start, data.len() as u64));
421            }
422        }
423
424        // Directory: root + one entry per stream, linked as a simple chain via `right`.
425        let mut directory = Vec::new();
426        let entry_count = streams.len() + 1;
427        let write_entry = |directory: &mut Vec<u8>,
428                           name: &str,
429                           object_type: u8,
430                           right: u32,
431                           child: u32,
432                           start: u32,
433                           size: u64| {
434            let mut entry = [0u8; DIR_ENTRY_SIZE];
435            let units: Vec<u16> = name.encode_utf16().collect();
436            for (index, unit) in units.iter().enumerate() {
437                entry[index * 2..index * 2 + 2].copy_from_slice(&unit.to_le_bytes());
438            }
439            entry[64..66].copy_from_slice(&(((units.len() + 1) * 2) as u16).to_le_bytes());
440            entry[66] = object_type;
441            entry[68..72].copy_from_slice(&SECT_FREE.to_le_bytes());
442            entry[72..76].copy_from_slice(&right.to_le_bytes());
443            entry[76..80].copy_from_slice(&child.to_le_bytes());
444            entry[116..120].copy_from_slice(&start.to_le_bytes());
445            entry[120..128].copy_from_slice(&size.to_le_bytes());
446            directory.extend_from_slice(&entry);
447        };
448
449        write_entry(
450            &mut directory,
451            "Root Entry",
452            OBJECT_TYPE_ROOT,
453            SECT_FREE,
454            if streams.is_empty() { SECT_FREE } else { 1 },
455            mini_stream_start,
456            mini_stream.len() as u64,
457        );
458        for (index, (name, data)) in streams.iter().enumerate() {
459            let placement = placements
460                .iter()
461                .find(|(stream_index, _, _)| *stream_index == index)
462                .expect("placement");
463            let right = if index + 1 < streams.len() {
464                index as u32 + 2
465            } else {
466                SECT_FREE
467            };
468            write_entry(
469                &mut directory,
470                name,
471                OBJECT_TYPE_STREAM,
472                right,
473                SECT_FREE,
474                placement.1,
475                data.len() as u64,
476            );
477        }
478        while directory.len() % SECTOR != 0 {
479            directory.push(0);
480        }
481        let _ = entry_count;
482        let dir_start = append_data(&mut sectors, &mut fat, &directory);
483
484        // FAT sectors themselves.
485        let fat_entry_count = fat.len() + 8; // room for FAT sectors marked below
486        let fat_sector_count = (fat_entry_count * 4).div_ceil(SECTOR);
487        let mut fat_full = fat.clone();
488        let fat_start = sectors.len() as u32;
489        for index in 0..fat_sector_count {
490            fat_full.push(0xFFFF_FFFD); // FATSECT marker
491            let _ = index;
492        }
493        while fat_full.len() * 4 % SECTOR != 0 {
494            fat_full.push(SECT_FREE);
495        }
496        let fat_bytes: Vec<u8> = fat_full
497            .iter()
498            .flat_map(|value| value.to_le_bytes())
499            .collect();
500        for chunk in fat_bytes.chunks(SECTOR) {
501            let mut sector = [0u8; SECTOR];
502            sector[..chunk.len()].copy_from_slice(chunk);
503            sectors.push(sector);
504        }
505
506        // Header.
507        let mut header = [0u8; SECTOR];
508        header[..8].copy_from_slice(&CFB_SIGNATURE);
509        header[24..26].copy_from_slice(&0x003Eu16.to_le_bytes());
510        header[26..28].copy_from_slice(&3u16.to_le_bytes());
511        header[28..30].copy_from_slice(&0xFFFEu16.to_le_bytes());
512        header[30..32].copy_from_slice(&9u16.to_le_bytes());
513        header[32..34].copy_from_slice(&6u16.to_le_bytes());
514        header[44..48].copy_from_slice(&(fat_sector_count as u32).to_le_bytes());
515        header[48..52].copy_from_slice(&dir_start.to_le_bytes());
516        header[56..60].copy_from_slice(&4096u32.to_le_bytes());
517        header[60..64].copy_from_slice(&mini_fat_start.to_le_bytes());
518        header[64..68].copy_from_slice(&1u32.to_le_bytes());
519        header[68..72].copy_from_slice(&SECT_END_OF_CHAIN.to_le_bytes());
520        for index in 0..109 {
521            let value = if index < fat_sector_count {
522                fat_start + index as u32
523            } else {
524                SECT_FREE
525            };
526            header[76 + index * 4..80 + index * 4].copy_from_slice(&value.to_le_bytes());
527        }
528
529        let mut file = header.to_vec();
530        for sector in sectors {
531            file.extend_from_slice(&sector);
532        }
533        file
534    }
535
536    #[test]
537    fn rejects_non_cfb_input() {
538        assert!(CompoundFile::parse(b"PK\x03\x04not a cfb").is_err());
539    }
540
541    #[test]
542    fn reads_mini_stream_entries() {
543        let file = build_cfb(&[("WordDocument", b"hello mini stream".as_slice())]);
544        let cfb = CompoundFile::parse(&file).expect("parse cfb");
545        assert_eq!(
546            cfb.stream("WordDocument").expect("stream"),
547            b"hello mini stream"
548        );
549        assert_eq!(cfb.stream("WORDDOCUMENT"), cfb.stream("worddocument"));
550    }
551
552    #[test]
553    fn reads_regular_fat_entries() {
554        let big: Vec<u8> = (0..10_000u32).map(|value| (value % 251) as u8).collect();
555        let file = build_cfb(&[
556            ("WordDocument", big.as_slice()),
557            ("1Table", b"table data".as_slice()),
558        ]);
559        let cfb = CompoundFile::parse(&file).expect("parse cfb");
560        assert_eq!(cfb.stream("WordDocument").expect("stream"), big.as_slice());
561        assert_eq!(cfb.stream("1Table").expect("table"), b"table data");
562    }
563}