1fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, String> {
6 bytes
7 .get(offset..offset + 2)
8 .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
9 .ok_or_else(|| "FIB: unexpected end of WordDocument stream".to_string())
10}
11
12fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, String> {
13 bytes
14 .get(offset..offset + 4)
15 .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
16 .ok_or_else(|| "FIB: unexpected end of WordDocument stream".to_string())
17}
18
19pub mod fcidx {
21 pub const STSHF: usize = 1;
22 pub const PLCFFND_REF: usize = 2;
23 pub const PLCFFND_TXT: usize = 3;
24 pub const PLCFAND_REF: usize = 4;
25 pub const PLCFAND_TXT: usize = 5;
26 pub const PLCF_SED: usize = 6;
27 pub const PLCF_HDD: usize = 11;
28 pub const PLCF_BTE_CHPX: usize = 12;
29 pub const PLCF_BTE_PAPX: usize = 13;
30 pub const STTBF_FFN: usize = 15;
31 pub const PLCF_FLD_MOM: usize = 16;
32 pub const DOP: usize = 31;
33 pub const CLX: usize = 33;
34 pub const GRP_XST_ATN_OWNERS: usize = 36;
35 pub const PLCF_SPA_MOM: usize = 40;
36 pub const PLCF_SPA_HDR: usize = 41;
37 pub const PLCFEND_REF: usize = 46;
38 pub const PLCFEND_TXT: usize = 47;
39 pub const DGG_INFO: usize = 50;
40 pub const STTBF_RMARK: usize = 51;
41 pub const PLCF_TXBX_TXT: usize = 56;
42 pub const PLCF_HDR_TXBX_TXT: usize = 58;
43 pub const PLF_LST: usize = 73;
44 pub const PLF_LFO: usize = 74;
45 pub const ATRD_EXTRA: usize = 112;
46}
47
48#[derive(Debug, Default, Clone, Copy)]
49pub struct CcpCounts {
50 pub text: u32,
51 pub ftn: u32,
52 pub hdd: u32,
53 pub mcr: u32,
54 pub atn: u32,
55 pub edn: u32,
56 pub txbx: u32,
57 pub hdr_txbx: u32,
58}
59
60impl CcpCounts {
61 pub fn checked_total(&self) -> Option<u32> {
62 [
63 self.text,
64 self.ftn,
65 self.hdd,
66 self.mcr,
67 self.atn,
68 self.edn,
69 self.txbx,
70 self.hdr_txbx,
71 ]
72 .into_iter()
73 .try_fold(0u32, u32::checked_add)
74 }
75
76 pub fn total(&self) -> u32 {
77 self.checked_total().unwrap_or(u32::MAX)
78 }
79
80 pub fn ftn_start(&self) -> u32 {
82 self.text
83 }
84
85 pub fn hdd_start(&self) -> u32 {
87 self.text.saturating_add(self.ftn)
88 }
89
90 pub fn edn_start(&self) -> u32 {
92 [self.text, self.ftn, self.hdd, self.mcr, self.atn]
93 .into_iter()
94 .fold(0u32, u32::saturating_add)
95 }
96
97 pub fn txbx_start(&self) -> u32 {
99 [self.text, self.ftn, self.hdd, self.mcr, self.atn, self.edn]
100 .into_iter()
101 .fold(0u32, u32::saturating_add)
102 }
103
104 pub fn hdr_txbx_start(&self) -> u32 {
106 self.txbx_start().saturating_add(self.txbx)
107 }
108}
109
110#[derive(Debug)]
111pub struct Fib {
112 pub n_fib: u16,
113 pub f_complex: bool,
114 pub f_which_tbl_stm: bool,
115 pub ccp: CcpCounts,
116 rg_fc_lcb: Vec<(u32, u32)>,
117}
118
119impl Fib {
120 pub fn parse(word_stream: &[u8]) -> Result<Fib, String> {
121 let w_ident = read_u16(word_stream, 0)?;
122 if w_ident != 0xA5EC {
123 return Err(format!(
124 "Not a Word binary document (wIdent 0x{w_ident:04X}, expected 0xA5EC)"
125 ));
126 }
127 let n_fib = read_u16(word_stream, 2)?;
128 if n_fib < 0x00C1 {
129 return Err(format!(
130 "Unsupported legacy Word version (nFib 0x{n_fib:04X}); only Word 97-2003 binary files are supported"
131 ));
132 }
133
134 let flags = read_u16(word_stream, 0x0A)?;
135 let f_complex = flags & 0x0004 != 0;
136 let f_encrypted = flags & 0x0100 != 0;
137 let f_which_tbl_stm = flags & 0x0200 != 0;
138 let f_obfuscated = flags & 0x8000 != 0;
139 if f_encrypted {
140 return Err(if f_obfuscated {
141 "Encrypted .doc files (XOR obfuscation) are not supported".to_string()
142 } else {
143 "Encrypted .doc files are not supported".to_string()
144 });
145 }
146
147 let csw = read_u16(word_stream, 0x20)? as usize;
148 let rg_lw_offset = 0x22 + csw * 2 + 2;
149 let cslw = read_u16(word_stream, 0x22 + csw * 2)? as usize;
150 let lw = |index: usize| read_u32(word_stream, rg_lw_offset + index * 4);
151 let ccp = CcpCounts {
152 text: lw(3)?,
153 ftn: lw(4)?,
154 hdd: lw(5)?,
155 mcr: lw(6)?,
156 atn: lw(7)?,
157 edn: lw(8)?,
158 txbx: lw(9)?,
159 hdr_txbx: lw(10)?,
160 };
161
162 let cb_rg_fc_lcb_offset = rg_lw_offset + cslw * 4;
163 let pair_count = read_u16(word_stream, cb_rg_fc_lcb_offset)? as usize;
164 let pairs_offset = cb_rg_fc_lcb_offset + 2;
165 let mut rg_fc_lcb = Vec::with_capacity(pair_count);
166 for index in 0..pair_count {
167 let fc = read_u32(word_stream, pairs_offset + index * 8)?;
168 let lcb = read_u32(word_stream, pairs_offset + index * 8 + 4)?;
169 rg_fc_lcb.push((fc, lcb));
170 }
171
172 Ok(Fib {
173 n_fib,
174 f_complex,
175 f_which_tbl_stm,
176 ccp,
177 rg_fc_lcb,
178 })
179 }
180
181 pub fn table_slice<'a>(&self, table_stream: &'a [u8], index: usize) -> Option<&'a [u8]> {
184 let (fc, lcb) = *self.rg_fc_lcb.get(index)?;
185 if lcb == 0 {
186 return None;
187 }
188 table_stream.get(fc as usize..(fc as usize).checked_add(lcb as usize)?)
189 }
190
191 pub fn fc_lcb(&self, index: usize) -> Option<(u32, u32)> {
192 self.rg_fc_lcb.get(index).copied()
193 }
194
195 pub fn table_stream_name(&self) -> &'static str {
196 if self.f_which_tbl_stm {
197 "1Table"
198 } else {
199 "0Table"
200 }
201 }
202}