Skip to main content

extend_docx/doc/
fib.rs

1//! File Information Block ([MS-DOC] 2.5.1) — the versioned header at offset 0
2//! of the WordDocument stream. All other structures are located through its
3//! (fc, lcb) pointer pairs into the Table stream.
4
5fn 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
19/// Indices into [`Fib::rg_fc_lcb`] for the FibRgFcLcb97 pointer pairs we consume.
20pub 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    /// CP at which the footnote subdocument starts.
81    pub fn ftn_start(&self) -> u32 {
82        self.text
83    }
84
85    /// CP at which the header/footer subdocument starts.
86    pub fn hdd_start(&self) -> u32 {
87        self.text.saturating_add(self.ftn)
88    }
89
90    /// CP at which the endnote subdocument starts.
91    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    /// CP at which the main-textbox subdocument starts.
98    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    /// CP at which the header-textbox subdocument starts.
105    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    /// Returns the Table-stream slice referenced by pointer pair `index`,
182    /// or None when absent/empty.
183    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}