Skip to main content

extend_docx/doc/
fkp.rs

1//! Formatted disk pages ([MS-DOC] 2.9.74-2.9.79): 512-byte pages holding
2//! character (CHPX) and paragraph (PAPX) property deltas keyed by FC ranges,
3//! located through the PlcfBteChpx / PlcfBtePapx bin tables.
4
5const FKP_SIZE: usize = 512;
6
7/// One FC interval of a CHPX FKP with its grpprl (empty = inherit from style).
8#[derive(Debug, Clone)]
9pub struct ChpxRun {
10    pub fc_start: u32,
11    pub fc_end: u32,
12    pub grpprl: Vec<u8>,
13}
14
15/// One paragraph interval of a PAPX FKP.
16#[derive(Debug, Clone)]
17pub struct PapxRun {
18    pub fc_start: u32,
19    pub fc_end: u32,
20    pub istd: u16,
21    pub grpprl: Vec<u8>,
22}
23
24fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
25    bytes
26        .get(offset..offset + 4)
27        .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
28}
29
30/// Parses a bin table (PlcfBteChpx/PlcfBtePapx): FC boundaries plus the FKP
31/// page number for each interval. Returns (fc_start, fc_end, page_number).
32pub fn parse_bin_table(plc: &[u8]) -> Vec<(u32, u32, u32)> {
33    if plc.len() < 4 || !(plc.len() - 4).is_multiple_of(8) {
34        return Vec::new();
35    }
36    let count = (plc.len() - 4) / 8;
37    let mut entries = Vec::with_capacity(count);
38    for index in 0..count {
39        let Some(fc_start) = read_u32(plc, index * 4) else { break };
40        let Some(fc_end) = read_u32(plc, (index + 1) * 4) else { break };
41        let Some(page) = read_u32(plc, (count + 1) * 4 + index * 4) else { break };
42        entries.push((fc_start, fc_end, page));
43    }
44    entries
45}
46
47fn fkp_page(word_stream: &[u8], page_number: u32) -> Option<&[u8]> {
48    let start = page_number as usize * FKP_SIZE;
49    word_stream.get(start..start + FKP_SIZE)
50}
51
52/// Parses every CHPX run in the FKP page `page_number`.
53pub fn parse_chpx_fkp(word_stream: &[u8], page_number: u32) -> Vec<ChpxRun> {
54    let Some(page) = fkp_page(word_stream, page_number) else {
55        return Vec::new();
56    };
57    let crun = page[FKP_SIZE - 1] as usize;
58    if crun == 0 || (crun + 1) * 4 + crun > FKP_SIZE - 1 {
59        return Vec::new();
60    }
61    let mut runs = Vec::with_capacity(crun);
62    for index in 0..crun {
63        let Some(fc_start) = read_u32(page, index * 4) else { break };
64        let Some(fc_end) = read_u32(page, (index + 1) * 4) else { break };
65        let offset_byte = page[(crun + 1) * 4 + index] as usize;
66        let grpprl = if offset_byte == 0 {
67            Vec::new()
68        } else {
69            let chpx_offset = offset_byte * 2;
70            let cb = page.get(chpx_offset).copied().unwrap_or(0) as usize;
71            page.get(chpx_offset + 1..chpx_offset + 1 + cb)
72                .map(|slice| slice.to_vec())
73                .unwrap_or_default()
74        };
75        runs.push(ChpxRun {
76            fc_start,
77            fc_end,
78            grpprl,
79        });
80    }
81    runs
82}
83
84/// Parses every PAPX run in the FKP page `page_number`. Each run is one
85/// paragraph; the grpprl carries paragraph and table sprms, prefixed in the
86/// file by the paragraph's style index (istd).
87pub fn parse_papx_fkp(word_stream: &[u8], page_number: u32) -> Vec<PapxRun> {
88    const BX_SIZE: usize = 13;
89    let Some(page) = fkp_page(word_stream, page_number) else {
90        return Vec::new();
91    };
92    let crun = page[FKP_SIZE - 1] as usize;
93    if crun == 0 || (crun + 1) * 4 + crun * BX_SIZE > FKP_SIZE - 1 {
94        return Vec::new();
95    }
96    let mut runs = Vec::with_capacity(crun);
97    for index in 0..crun {
98        let Some(fc_start) = read_u32(page, index * 4) else { break };
99        let Some(fc_end) = read_u32(page, (index + 1) * 4) else { break };
100        let bx_offset = (crun + 1) * 4 + index * BX_SIZE;
101        let offset_byte = page[bx_offset] as usize;
102        let (istd, grpprl) = if offset_byte == 0 {
103            (0u16, Vec::new())
104        } else {
105            parse_papx_in_fkp(page, offset_byte * 2)
106        };
107        runs.push(PapxRun {
108            fc_start,
109            fc_end,
110            istd,
111            grpprl,
112        });
113    }
114    runs
115}
116
117/// PapxInFkp ([MS-DOC] 2.9.175): cb byte (with a 0 escape to a second byte for
118/// large grpprls), then GrpPrlAndIstd = istd (u16) + paragraph/table sprms.
119fn parse_papx_in_fkp(page: &[u8], offset: usize) -> (u16, Vec<u8>) {
120    let Some(&cb) = page.get(offset) else {
121        return (0, Vec::new());
122    };
123    let (payload_start, payload_len) = if cb == 0 {
124        let Some(&cb2) = page.get(offset + 1) else {
125            return (0, Vec::new());
126        };
127        (offset + 2, cb2 as usize * 2)
128    } else {
129        (offset + 1, cb as usize * 2 - 1)
130    };
131    let Some(payload) = page.get(payload_start..payload_start + payload_len) else {
132        return (0, Vec::new());
133    };
134    if payload.len() < 2 {
135        return (0, Vec::new());
136    }
137    let istd = u16::from_le_bytes([payload[0], payload[1]]);
138    (istd, payload[2..].to_vec())
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn parses_chpx_fkp_page() {
147        let mut page = vec![0u8; FKP_SIZE];
148        // 2 runs: fc 100..110 with grpprl, 110..120 default.
149        page[FKP_SIZE - 1] = 2;
150        page[0..4].copy_from_slice(&100u32.to_le_bytes());
151        page[4..8].copy_from_slice(&110u32.to_le_bytes());
152        page[8..12].copy_from_slice(&120u32.to_le_bytes());
153        // run offsets (word offsets): run 0 -> chpx at byte 100, run 1 -> 0.
154        page[12] = 50;
155        page[13] = 0;
156        page[100] = 3; // cb
157        page[101..104].copy_from_slice(&[0x35, 0x08, 0x01]); // sprmCFBold on
158
159        let mut stream = vec![0u8; FKP_SIZE * 2];
160        stream[FKP_SIZE..].copy_from_slice(&page);
161        let runs = parse_chpx_fkp(&stream, 1);
162        assert_eq!(runs.len(), 2);
163        assert_eq!(runs[0].fc_start, 100);
164        assert_eq!(runs[0].grpprl, vec![0x35, 0x08, 0x01]);
165        assert!(runs[1].grpprl.is_empty());
166    }
167
168    #[test]
169    fn parses_papx_fkp_page() {
170        let mut page = vec![0u8; FKP_SIZE];
171        page[FKP_SIZE - 1] = 1;
172        page[0..4].copy_from_slice(&200u32.to_le_bytes());
173        page[4..8].copy_from_slice(&230u32.to_le_bytes());
174        let bx_offset = 8;
175        page[bx_offset] = 60; // word offset -> byte 120
176        // PapxInFkp at 120: cb=3 -> payload 5 bytes = istd(2) + 3-byte sprm.
177        page[120] = 3;
178        page[121..123].copy_from_slice(&1u16.to_le_bytes()); // istd 1
179        page[123..126].copy_from_slice(&[0x05, 0x24, 0x01]); // sprmPFKeep on
180
181        let mut stream = vec![0u8; FKP_SIZE * 2];
182        stream[FKP_SIZE..].copy_from_slice(&page);
183        let runs = parse_papx_fkp(&stream, 1);
184        assert_eq!(runs.len(), 1);
185        assert_eq!(runs[0].istd, 1);
186        assert_eq!(runs[0].grpprl, vec![0x05, 0x24, 0x01]);
187    }
188
189    #[test]
190    fn parses_papx_large_grpprl_escape() {
191        let mut page = vec![0u8; FKP_SIZE];
192        page[FKP_SIZE - 1] = 1;
193        page[0..4].copy_from_slice(&10u32.to_le_bytes());
194        page[4..8].copy_from_slice(&20u32.to_le_bytes());
195        page[8] = 60;
196        // cb=0 escape: second byte cb2=3 -> payload 6 bytes.
197        page[120] = 0;
198        page[121] = 3;
199        page[122..124].copy_from_slice(&5u16.to_le_bytes());
200        page[124..128].copy_from_slice(&[0x03, 0x24, 0x01, 0x00]);
201
202        let mut stream = vec![0u8; FKP_SIZE * 2];
203        stream[FKP_SIZE..].copy_from_slice(&page);
204        let runs = parse_papx_fkp(&stream, 1);
205        assert_eq!(runs[0].istd, 5);
206        assert_eq!(runs[0].grpprl.len(), 4);
207    }
208}