1use 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 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 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(); 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 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 let fat_entry_count = fat.len() + 8; 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); 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 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(§or);
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}