use crate::error::{Error, Result};
use crate::limits::Limits;
pub const MODEL_VERSION: u8 = 1;
pub const MAX_MODEL_PIECES: u32 = 1 << 24;
pub const MAX_MODEL_TEXT_UNITS: u32 = 1 << 30;
pub const FIB_MAGIC: u16 = 0xA5EC;
pub const NFIB_MIN: u16 = 0x00C1;
pub const NFIB_MAX: u16 = 0x010C;
pub const F_WHICH_TBL_STM: u16 = 0x0200;
pub const CLXT_PRC: u8 = 0x01;
pub const CLXT_PCDT: u8 = 0x02;
pub const PCDT_BASE: u32 = 4;
pub const PCD_LEN: usize = 8;
pub const PCD_FCOMPRESSED: u32 = 0x4000_0000;
pub const PCD_FCMASK: u32 = 0x3FFF_FFFF;
pub const CLX_PAIR_INDEX: usize = 33;
pub const FC_LCB_PAIR_LEN: usize = 8;
pub const FIB_MIN_LEN: usize = 154;
pub const FIB_BASE_LEN: usize = 32;
pub const STREAM_WORD_DOCUMENT: &str = "WordDocument";
pub const STREAM_TABLE_0: &str = "0Table";
pub const STREAM_TABLE_1: &str = "1Table";
pub const FIELD_TEXT: u8 = 0;
pub const fn field_name(field: u8) -> &'static str {
let _ = field;
"text"
}
pub const fn n_fib_plausible(n_fib: u16) -> bool {
n_fib >= NFIB_MIN && n_fib <= NFIB_MAX
}
pub const fn version_name(n_fib: u16) -> &'static str {
match n_fib {
0x00C1 | 0x00C2 => "word97",
0x00D9 | 0x00DA => "word2000",
0x0101 | 0x0102 => "word2002",
0x010C | 0x010D => "word2003",
_ => "word97-2003",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocFib {
pub w_ident: u16,
pub n_fib: u16,
pub n_product: u16,
pub lid: u16,
pub pn_next: i16,
pub flags: u16,
pub n_fib_back: u16,
pub l_key: u32,
pub envr: u8,
pub flags2: u8,
pub fc_min: u32,
pub fc_mac: u32,
pub cb_mac: u32,
pub ccp_text: u32,
pub ccp_ftn: u32,
pub ccp_hdd: u32,
pub ccp_atn: u32,
pub ccp_edn: u32,
pub ccp_txbx: u32,
pub ccp_hdr_txbx: u32,
pub fc_clx: u32,
pub lcb_clx: u32,
pub csw: u16,
pub cslw: u16,
pub cb_rg_fc_lcb: u16,
}
impl DocFib {
pub const fn table_1(&self) -> bool {
self.flags & F_WHICH_TBL_STM != 0
}
pub const fn table_stream(&self) -> &'static str {
if self.table_1() {
STREAM_TABLE_1
} else {
STREAM_TABLE_0
}
}
pub fn total_cp(&self) -> u64 {
let mut total = 0u64;
for c in [
self.ccp_text,
self.ccp_ftn,
self.ccp_hdd,
self.ccp_atn,
self.ccp_edn,
self.ccp_txbx,
self.ccp_hdr_txbx,
] {
total = total.saturating_add(c as u64);
}
total
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocPiece {
pub cp_start: u32,
pub cp_end: u32,
pub fc: u32,
pub compressed: bool,
pub span: (u64, u64),
pub text: Vec<u16>,
}
impl DocPiece {
pub const fn cp_len(&self) -> u64 {
(self.cp_end - self.cp_start) as u64
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocModel {
pub doc_len: u64,
pub wd_span: (u64, u64),
pub tbl_span: (u64, u64),
pub fib: DocFib,
pub fib_span: (u64, u64),
pub clx_span: (u64, u64),
pub pieces: Vec<DocPiece>,
}
impl DocModel {
pub fn piece(&self, index: u32) -> Option<&DocPiece> {
self.pieces.get(index as usize)
}
pub fn piece_source_span(&self, p: &DocPiece) -> (u64, u64) {
let base = self.wd_span.0;
(base.saturating_add(p.span.0), base.saturating_add(p.span.1))
}
pub fn encode(&self) -> Vec<u8> {
let text_units: usize = self.pieces.iter().map(|p| p.text.len()).sum();
let mut out = Vec::with_capacity(256 + self.pieces.len() * 64 + text_units * 2);
out.extend_from_slice(b"DOCM");
out.push(MODEL_VERSION);
out.extend_from_slice(&self.doc_len.to_le_bytes());
out.extend_from_slice(&self.wd_span.0.to_le_bytes());
out.extend_from_slice(&self.wd_span.1.to_le_bytes());
out.extend_from_slice(&self.tbl_span.0.to_le_bytes());
out.extend_from_slice(&self.tbl_span.1.to_le_bytes());
let f = &self.fib;
out.extend_from_slice(&f.w_ident.to_le_bytes());
out.extend_from_slice(&f.n_fib.to_le_bytes());
out.extend_from_slice(&f.n_product.to_le_bytes());
out.extend_from_slice(&f.lid.to_le_bytes());
out.extend_from_slice(&f.pn_next.to_le_bytes());
out.extend_from_slice(&f.flags.to_le_bytes());
out.extend_from_slice(&f.n_fib_back.to_le_bytes());
out.extend_from_slice(&f.l_key.to_le_bytes());
out.push(f.envr);
out.push(f.flags2);
out.extend_from_slice(&f.fc_min.to_le_bytes());
out.extend_from_slice(&f.fc_mac.to_le_bytes());
out.extend_from_slice(&f.cb_mac.to_le_bytes());
out.extend_from_slice(&f.ccp_text.to_le_bytes());
out.extend_from_slice(&f.ccp_ftn.to_le_bytes());
out.extend_from_slice(&f.ccp_hdd.to_le_bytes());
out.extend_from_slice(&f.ccp_atn.to_le_bytes());
out.extend_from_slice(&f.ccp_edn.to_le_bytes());
out.extend_from_slice(&f.ccp_txbx.to_le_bytes());
out.extend_from_slice(&f.ccp_hdr_txbx.to_le_bytes());
out.extend_from_slice(&f.fc_clx.to_le_bytes());
out.extend_from_slice(&f.lcb_clx.to_le_bytes());
out.extend_from_slice(&f.csw.to_le_bytes());
out.extend_from_slice(&f.cslw.to_le_bytes());
out.extend_from_slice(&f.cb_rg_fc_lcb.to_le_bytes());
out.extend_from_slice(&self.fib_span.0.to_le_bytes());
out.extend_from_slice(&self.fib_span.1.to_le_bytes());
out.extend_from_slice(&self.clx_span.0.to_le_bytes());
out.extend_from_slice(&self.clx_span.1.to_le_bytes());
out.extend_from_slice(&(self.pieces.len() as u32).to_le_bytes());
for p in &self.pieces {
out.extend_from_slice(&p.cp_start.to_le_bytes());
out.extend_from_slice(&p.cp_end.to_le_bytes());
out.extend_from_slice(&p.fc.to_le_bytes());
out.push(p.compressed as u8);
out.extend_from_slice(&p.span.0.to_le_bytes());
out.extend_from_slice(&p.span.1.to_le_bytes());
out.extend_from_slice(&(p.text.len() as u32).to_le_bytes());
for u in &p.text {
out.extend_from_slice(&u.to_le_bytes());
}
}
out
}
pub fn decode(bytes: &[u8]) -> Result<DocModel> {
let mut r = BinReader::new(bytes);
if r.bytes(4)? != b"DOCM" {
return Err(corrupt("bad model magic"));
}
if r.u8()? != MODEL_VERSION {
return Err(corrupt("unsupported model version"));
}
let doc_len = r.u64()?;
let wd_span = (r.u64()?, r.u64()?);
let tbl_span = (r.u64()?, r.u64()?);
check_span(wd_span, doc_len, "WordDocument span")?;
check_span(tbl_span, doc_len, "table span")?;
let w_ident = r.u16()?;
let n_fib = r.u16()?;
let n_product = r.u16()?;
let lid = r.u16()?;
let pn_next = r.u16()? as i16;
let flags = r.u16()?;
let n_fib_back = r.u16()?;
let l_key = r.u32()?;
let envr = r.u8()?;
let flags2 = r.u8()?;
let fc_min = r.u32()?;
let fc_mac = r.u32()?;
let cb_mac = r.u32()?;
let ccp_text = r.u32()?;
let ccp_ftn = r.u32()?;
let ccp_hdd = r.u32()?;
let ccp_atn = r.u32()?;
let ccp_edn = r.u32()?;
let ccp_txbx = r.u32()?;
let ccp_hdr_txbx = r.u32()?;
let fc_clx = r.u32()?;
let lcb_clx = r.u32()?;
let csw = r.u16()?;
let cslw = r.u16()?;
let cb_rg_fc_lcb = r.u16()?;
if w_ident != FIB_MAGIC || !n_fib_plausible(n_fib) {
return Err(corrupt("model FIB magic or nFib is implausible"));
}
let fib = DocFib {
w_ident,
n_fib,
n_product,
lid,
pn_next,
flags,
n_fib_back,
l_key,
envr,
flags2,
fc_min,
fc_mac,
cb_mac,
ccp_text,
ccp_ftn,
ccp_hdd,
ccp_atn,
ccp_edn,
ccp_txbx,
ccp_hdr_txbx,
fc_clx,
lcb_clx,
csw,
cslw,
cb_rg_fc_lcb,
};
let fib_span = (r.u64()?, r.u64()?);
let clx_span = (r.u64()?, r.u64()?);
check_span(fib_span, wd_span.1, "FIB span")?;
check_span(clx_span, tbl_span.1, "CLX span")?;
let count = r.u32()?;
if count > MAX_MODEL_PIECES {
return Err(corrupt("model piece count is implausible"));
}
let mut pieces = Vec::with_capacity(count as usize);
let mut text_units: u64 = 0;
let mut prev_cp: u32 = 0;
for i in 0..count {
let cp_start = r.u32()?;
let cp_end = r.u32()?;
let fc = r.u32()?;
let compressed = r.u8()? != 0;
let span = (r.u64()?, r.u64()?);
check_span(span, wd_span.1, "piece span")?;
let units = r.u32()?;
if units as u64 > MAX_MODEL_TEXT_UNITS as u64 {
return Err(corrupt("piece text is implausible"));
}
if cp_start > cp_end || (i > 0 && cp_start < prev_cp) {
return Err(corrupt("piece CPs are not monotonic"));
}
let expected_units = if compressed {
span.1 - span.0
} else {
(span.1 - span.0) / 2
};
if units as u64 != expected_units || (!compressed && (span.1 - span.0) % 2 != 0) {
return Err(corrupt("piece text length disagrees with its span"));
}
let mut text = Vec::with_capacity(units as usize);
for _ in 0..units {
text.push(r.u16()?);
}
text_units = text_units.saturating_add(units as u64);
if text_units > MAX_MODEL_TEXT_UNITS as u64 {
return Err(corrupt("model text is implausible"));
}
prev_cp = cp_end;
pieces.push(DocPiece {
cp_start,
cp_end,
fc,
compressed,
span,
text,
});
}
if !r.at_end() {
return Err(corrupt("model has trailing bytes"));
}
Ok(DocModel {
doc_len,
wd_span,
tbl_span,
fib,
fib_span,
clx_span,
pieces,
})
}
}
fn check_span(span: (u64, u64), bound: u64, what: &'static str) -> Result<()> {
if span.0 > span.1 || span.1 > bound {
return Err(corrupt(format!("{what} is outside its container")));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocMatch {
pub piece: u32,
pub field: u8,
pub start: u64,
pub end: u64,
pub text: String,
}
pub fn piece_bytes<'a>(source: &'a [u8], model: &DocModel, p: &DocPiece) -> Result<&'a [u8]> {
let (start, end) = model.piece_source_span(p);
slice(source, start, end, "piece span is outside the source")
}
pub fn piece_text(p: &DocPiece) -> String {
String::from_utf16_lossy(&p.text)
}
pub fn main_text(model: &DocModel) -> String {
let ccp = model.fib.ccp_text as u64;
let mut units: Vec<u16> = Vec::new();
for p in &model.pieces {
if p.cp_start as u64 >= ccp {
break;
}
let end = (p.cp_end as u64).min(ccp);
let take = (end - p.cp_start as u64) as usize;
units.extend_from_slice(&p.text[..take.min(p.text.len())]);
if end >= ccp {
break;
}
}
String::from_utf16_lossy(&units)
}
pub fn canonical_text(model: &DocModel, _source: &[u8], max_bytes: u64) -> Result<String> {
let text = main_text(model);
if text.len() as u64 > max_bytes {
return Err(Error::resource_limit(format!(
"doc canonical text exceeded the {max_bytes}-byte budget"
)));
}
Ok(text)
}
pub fn find(model: &DocModel, pattern: &str, max_bytes: u64) -> Result<Vec<DocMatch>> {
if pattern.is_empty() {
return Ok(Vec::new());
}
let needle = pattern.as_bytes();
let plen_units = pattern.encode_utf16().count() as u64;
let mut out: Vec<DocMatch> = Vec::new();
let mut estimated: u64 = 0;
for (i, p) in model.pieces.iter().enumerate() {
let text = piece_text(p);
let bytes = text.as_bytes();
let unit: u64 = if p.compressed { 1 } else { 2 };
let base = p.fc as u64;
let mut at = 0usize;
while at + needle.len() <= bytes.len() {
if &bytes[at..at + needle.len()] == needle {
estimated = estimated.saturating_add(64 + needle.len() as u64);
if estimated > max_bytes {
return Err(Error::resource_limit(format!(
"doc find exceeded the {max_bytes}-byte budget"
)));
}
let units = text[..at].encode_utf16().count() as u64;
let start = base.saturating_add(units.saturating_mul(unit));
let end = start.saturating_add(plen_units.saturating_mul(unit));
out.push(DocMatch {
piece: i as u32,
field: FIELD_TEXT,
start,
end,
text: pattern.to_string(),
});
at += needle.len();
} else {
at += 1;
}
}
}
Ok(out)
}
pub fn detect(source: &[u8], limits: Limits) -> bool {
if source.len() as u64 > limits.max_doc_document_bytes {
return false;
}
if !crate::adapter::cfbf::detect(source, limits) {
return false;
}
let Ok(model) = crate::adapter::cfbf::parse(source, limits) else {
return false;
};
let Some(wd_entry) = find_named(&model, STREAM_WORD_DOCUMENT) else {
return false;
};
let Ok(wd) = crate::adapter::cfbf::stream_bytes(source, wd_entry) else {
return false;
};
if wd.len() < FIB_BASE_LEN {
return false;
}
let w_ident = u16::from_le_bytes([wd[0], wd[1]]);
if w_ident != FIB_MAGIC {
return false;
}
let n_fib = u16::from_le_bytes([wd[2], wd[3]]);
if !n_fib_plausible(n_fib) {
return false;
}
let flags = u16::from_le_bytes([wd[10], wd[11]]);
let table = if flags & F_WHICH_TBL_STM != 0 {
STREAM_TABLE_1
} else {
STREAM_TABLE_0
};
find_named(&model, table).is_some()
}
pub fn parse(source: &[u8], limits: Limits) -> Result<DocModel> {
let len = source.len() as u64;
if len > limits.max_doc_document_bytes {
return Err(Error::resource_limit(format!(
"doc source is {len} bytes, above the {}-byte document cap",
limits.max_doc_document_bytes
)));
}
let cfbf = crate::adapter::cfbf::parse(source, limits)?;
let wd_entry = find_named(&cfbf, STREAM_WORD_DOCUMENT)
.ok_or_else(|| invalid("the compound file has no WordDocument stream"))?;
let wd = crate::adapter::cfbf::stream_bytes(source, wd_entry)?;
if wd.len() as u64 > limits.max_doc_stream_bytes {
return Err(Error::resource_limit(format!(
"the WordDocument stream is {} bytes, above the {}-byte stream cap",
wd.len(),
limits.max_doc_stream_bytes
)));
}
let fib = parse_fib(wd, limits)?;
let table = fib.table_stream();
let tbl_entry = find_named(&cfbf, table)
.ok_or_else(|| invalid(format!("the compound file has no {table} stream")))?;
let tbl = crate::adapter::cfbf::stream_bytes(source, tbl_entry)?;
if tbl.len() as u64 > limits.max_doc_stream_bytes {
return Err(Error::resource_limit(format!(
"the {table} stream is {} bytes, above the {}-byte stream cap",
tbl.len(),
limits.max_doc_stream_bytes
)));
}
let fc_clx = fib.fc_clx as u64;
let lcb_clx = fib.lcb_clx as u64;
let clx_end = fc_clx
.checked_add(lcb_clx)
.ok_or_else(|| invalid("the CLX span overflows"))?;
if clx_end > tbl.len() as u64 {
return Err(invalid("the CLX lies outside the table stream"));
}
let clx = &tbl[fc_clx as usize..clx_end as usize];
let (raw_pieces, pcdt_off, pcdt_len) = parse_clx(clx, limits)?;
let mut pieces = Vec::with_capacity(raw_pieces.len());
let mut text_bytes: u64 = 0;
for rp in &raw_pieces {
let cp_len = (rp.cp_end - rp.cp_start) as u64;
let byte_len = if rp.compressed { cp_len } else { cp_len * 2 };
let start = rp.fc as u64;
let end = start
.checked_add(byte_len)
.ok_or_else(|| invalid("a piece span overflows"))?;
if end > wd.len() as u64 {
return Err(invalid("a piece falls outside the WordDocument stream"));
}
text_bytes = text_bytes.saturating_add(byte_len);
if text_bytes > limits.max_doc_text_bytes {
return Err(Error::resource_limit(format!(
"doc text is above the {}-byte text cap",
limits.max_doc_text_bytes
)));
}
let bytes = &wd[start as usize..end as usize];
let text = decode_piece(bytes, rp.compressed);
pieces.push(DocPiece {
cp_start: rp.cp_start,
cp_end: rp.cp_end,
fc: rp.fc,
compressed: rp.compressed,
span: (start, end),
text,
});
}
let total_cp = pieces.last().map(|p| p.cp_end as u64).unwrap_or(0);
if total_cp != fib.total_cp() {
return Err(invalid(
"the piece table's last CP disagrees with the declared section totals",
));
}
let fib_span = (0, fib_declared_len(&fib));
let clx_start = fc_clx.saturating_add(pcdt_off);
let clx_span = (clx_start, clx_start.saturating_add(pcdt_len));
Ok(DocModel {
doc_len: len,
wd_span: wd_entry.data_span,
tbl_span: tbl_entry.data_span,
fib,
fib_span,
clx_span,
pieces,
})
}
pub fn build_doc_model(source: &[u8], limits: Limits) -> Result<Vec<u8>> {
Ok(parse(source, limits)?.encode())
}
fn fib_declared_len(fib: &DocFib) -> u64 {
let mut len = 38u64;
len = len.saturating_add(fib.csw as u64 * 2);
len = len.saturating_add(fib.cslw as u64 * 4);
len = len.saturating_add(fib.cb_rg_fc_lcb as u64 * FC_LCB_PAIR_LEN as u64);
len
}
fn parse_fib(wd: &[u8], limits: Limits) -> Result<DocFib> {
if wd.len() < FIB_MIN_LEN {
return Err(invalid("the FIB is truncated"));
}
let get16 = |off: usize| -> Result<u16> {
rd_u16(wd, off).ok_or_else(|| invalid("the FIB is truncated"))
};
let get32 = |off: usize| -> Result<u32> {
rd_u32(wd, off).ok_or_else(|| invalid("the FIB is truncated"))
};
let w_ident = get16(0)?;
if w_ident != FIB_MAGIC {
return Err(invalid(format!("bad FIB wIdent 0x{w_ident:04X}")));
}
let n_fib = get16(2)?;
if !n_fib_plausible(n_fib) {
return Err(invalid(format!("implausible nFib {n_fib}")));
}
let n_product = get16(4)?;
let lid = get16(6)?;
let pn_next = get16(8)? as i16;
let flags = get16(10)?;
let n_fib_back = get16(12)?;
let l_key = get32(14)?;
let envr = *wd.get(18).ok_or_else(|| invalid("the FIB is truncated"))?;
let flags2 = *wd.get(19).ok_or_else(|| invalid("the FIB is truncated"))?;
let fc_min = get32(24)?;
let fc_mac = get32(28)?;
if fc_min as u64 > wd.len() as u64 || fc_mac as u64 > wd.len() as u64 {
return Err(invalid("fcMin or fcMac is outside the WordDocument stream"));
}
let mut off = FIB_BASE_LEN;
let csw = get16(off)? as u64;
if csw > limits.max_doc_fib_fields {
return Err(Error::resource_limit(format!(
"FIB csw {csw} exceeds max_doc_fib_fields {}",
limits.max_doc_fib_fields
)));
}
off = off
.checked_add(2)
.and_then(|o| o.checked_add(csw as usize * 2))
.ok_or_else(|| invalid("the FIB is truncated"))?;
let cslw = get16(off)? as u64;
if cslw > limits.max_doc_fib_fields {
return Err(Error::resource_limit(format!(
"FIB cslw {cslw} exceeds max_doc_fib_fields {}",
limits.max_doc_fib_fields
)));
}
let fib_rglw_off = off
.checked_add(2)
.ok_or_else(|| invalid("the FIB is truncated"))?;
off = fib_rglw_off
.checked_add(cslw as usize * 4)
.ok_or_else(|| invalid("the FIB is truncated"))?;
let cb_rg_fc_lcb = get16(off)? as u64;
if cb_rg_fc_lcb > limits.max_doc_fib_fields {
return Err(Error::resource_limit(format!(
"FIB cbRgFcLcb {cb_rg_fc_lcb} exceeds max_doc_fib_fields {}",
limits.max_doc_fib_fields
)));
}
let fib_rg_fclcb_off = off
.checked_add(2)
.ok_or_else(|| invalid("the FIB is truncated"))?;
off = fib_rg_fclcb_off
.checked_add(cb_rg_fc_lcb as usize * FC_LCB_PAIR_LEN)
.ok_or_else(|| invalid("the FIB is truncated"))?;
if off > wd.len() {
return Err(invalid("the FIB extends past the WordDocument stream"));
}
if cslw < 11 {
return Err(invalid(
"the FIB fibRgLw is too short for the section counts",
));
}
if cb_rg_fc_lcb <= CLX_PAIR_INDEX as u64 {
return Err(invalid("the FIB has no fcClx pair"));
}
let rl = fib_rglw_off;
let cb_mac = get32(rl)?;
let ccp = |delta: usize| -> Result<u32> {
let v = get32(rl + delta)?;
if v > i32::MAX as u32 {
Err(invalid("a FIB ccp count is negative"))
} else {
Ok(v)
}
};
let ccp_text = ccp(12)?;
let ccp_ftn = ccp(16)?;
let ccp_hdd = ccp(20)?;
let ccp_atn = ccp(28)?;
let ccp_edn = ccp(32)?;
let ccp_txbx = ccp(36)?;
let ccp_hdr_txbx = ccp(40)?;
let clx_pair = fib_rg_fclcb_off + CLX_PAIR_INDEX * FC_LCB_PAIR_LEN;
let fc_clx = get32(clx_pair)?;
let lcb_clx = get32(clx_pair + 4)?;
Ok(DocFib {
w_ident,
n_fib,
n_product,
lid,
pn_next,
flags,
n_fib_back,
l_key,
envr,
flags2,
fc_min,
fc_mac,
cb_mac,
ccp_text,
ccp_ftn,
ccp_hdd,
ccp_atn,
ccp_edn,
ccp_txbx,
ccp_hdr_txbx,
fc_clx,
lcb_clx,
csw: csw as u16,
cslw: cslw as u16,
cb_rg_fc_lcb: cb_rg_fc_lcb as u16,
})
}
struct RawPiece {
cp_start: u32,
cp_end: u32,
fc: u32,
compressed: bool,
}
fn parse_clx(clx: &[u8], limits: Limits) -> Result<(Vec<RawPiece>, u64, u64)> {
let mut pos = 0usize;
loop {
let clxt = *clx
.get(pos)
.ok_or_else(|| invalid("the CLX has no Pcdt record"))?;
pos += 1;
match clxt {
CLXT_PRC => {
let b = clx
.get(pos..pos + 2)
.ok_or_else(|| invalid("a CLX Prc record is truncated"))?;
let cb = i16::from_le_bytes([b[0], b[1]]);
pos += 2;
if cb < 0 {
return Err(invalid("a CLX Prc record has a negative length"));
}
pos = pos
.checked_add(cb as usize)
.ok_or_else(|| invalid("a CLX Prc record overflows"))?;
if pos > clx.len() {
return Err(invalid("a CLX Prc record extends past the CLX"));
}
}
CLXT_PCDT => {
let pcdt_off = (pos - 1) as u64;
let b = clx
.get(pos..pos + 4)
.ok_or_else(|| invalid("the CLX Pcdt is truncated"))?;
let lcb = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
pos += 4;
if lcb < PCDT_BASE || !(lcb - PCDT_BASE).is_multiple_of(12) {
return Err(invalid("the CLX Pcdt lcb is not 12n+4"));
}
let n = (lcb - PCDT_BASE) / 12;
if n as u64 > limits.max_doc_pieces {
return Err(Error::resource_limit(format!(
"the piece table has {n} pieces, exceeding max_doc_pieces {}",
limits.max_doc_pieces
)));
}
let plc_end = pos
.checked_add(lcb as usize)
.ok_or_else(|| invalid("the CLX PlcPcd overflows"))?;
if plc_end > clx.len() {
return Err(invalid("the CLX Pcdt extends past the CLX"));
}
let plc = &clx[pos..plc_end];
let pieces = parse_plcpcd(plc, n as usize)?;
return Ok((pieces, pcdt_off, 5 + lcb as u64));
}
other => {
return Err(invalid(format!("unknown CLX record tag 0x{other:02X}")));
}
}
}
}
fn parse_plcpcd(plc: &[u8], n: usize) -> Result<Vec<RawPiece>> {
let cp_bytes = (n + 1)
.checked_mul(4)
.ok_or_else(|| invalid("the piece CP array overflows"))?;
let pcd_bytes = n
.checked_mul(PCD_LEN)
.ok_or_else(|| invalid("the PCD array overflows"))?;
if cp_bytes + pcd_bytes != plc.len() {
return Err(invalid(
"the PlcPcd length is inconsistent with its piece count",
));
}
let mut cps: Vec<u32> = Vec::with_capacity(n + 1);
for i in 0..=n {
let off = i * 4;
cps.push(u32::from_le_bytes([
plc[off],
plc[off + 1],
plc[off + 2],
plc[off + 3],
]));
}
if cps[0] != 0 {
return Err(invalid("the piece CP array does not start at 0"));
}
for w in cps.windows(2) {
if w[1] < w[0] {
return Err(invalid("the piece CP array is not monotonic"));
}
}
let pcd_base = cp_bytes;
let mut pieces = Vec::with_capacity(n);
for i in 0..n {
let off = pcd_base + i * PCD_LEN;
let fc_raw = u32::from_le_bytes([plc[off + 2], plc[off + 3], plc[off + 4], plc[off + 5]]);
let compressed = fc_raw & PCD_FCOMPRESSED != 0;
let fc = if compressed {
(fc_raw & PCD_FCMASK) >> 1
} else {
fc_raw
};
pieces.push(RawPiece {
cp_start: cps[i],
cp_end: cps[i + 1],
fc,
compressed,
});
}
Ok(pieces)
}
fn decode_piece(bytes: &[u8], compressed: bool) -> Vec<u16> {
if compressed {
bytes.iter().map(|&b| cp1252(b)).collect()
} else {
bytes
.as_chunks::<2>()
.0
.iter()
.map(|c| u16::from_le_bytes(*c))
.collect()
}
}
fn cp1252(b: u8) -> u16 {
const HIGH: [u16; 32] = [
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFD, 0x017D, 0xFFFD, 0xFFFD, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFD, 0x017E, 0x0178, ];
match b {
0x80..=0x9F => HIGH[(b - 0x80) as usize],
_ => b as u16,
}
}
fn find_named<'a>(
model: &'a crate::adapter::cfbf::CfbfModel,
name: &str,
) -> Option<&'a crate::adapter::cfbf::CfbfEntry> {
model
.entries
.iter()
.find(|e| e.obj_type == crate::adapter::cfbf::OBJ_STREAM && entry_name_is(e, name))
}
fn entry_name_is(e: &crate::adapter::cfbf::CfbfEntry, name: &str) -> bool {
crate::adapter::cfbf::entry_name_text(e) == name
}
fn rd_u16(b: &[u8], off: usize) -> Option<u16> {
let s = b.get(off..off + 2)?;
Some(u16::from_le_bytes([s[0], s[1]]))
}
fn rd_u32(b: &[u8], off: usize) -> Option<u32> {
let s = b.get(off..off + 4)?;
Some(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
fn slice<'a>(source: &'a [u8], start: u64, end: u64, why: &'static str) -> Result<&'a [u8]> {
let s = usize::try_from(start).map_err(|_| invalid(why))?;
let e = usize::try_from(end).map_err(|_| invalid(why))?;
source.get(s..e).ok_or_else(|| invalid(why))
}
fn invalid(message: impl Into<String>) -> Error {
Error::invalid_doc_structure(format!("doc: {}", message.into()))
}
fn corrupt(message: impl Into<String>) -> Error {
Error::invalid_doc_structure(format!("doc model: {}", message.into()))
}
struct BinReader<'a> {
b: &'a [u8],
at: usize,
}
impl<'a> BinReader<'a> {
fn new(b: &'a [u8]) -> Self {
BinReader { b, at: 0 }
}
fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.at
.checked_add(n)
.ok_or_else(|| corrupt("length overflow"))?;
let out = self
.b
.get(self.at..end)
.ok_or_else(|| corrupt("unexpected end of model"))?;
self.at = end;
Ok(out)
}
fn u8(&mut self) -> Result<u8> {
Ok(self.bytes(1)?[0])
}
fn u16(&mut self) -> Result<u16> {
let b = self.bytes(2)?;
Ok(u16::from_le_bytes([b[0], b[1]]))
}
fn u32(&mut self) -> Result<u32> {
let b = self.bytes(4)?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn u64(&mut self) -> Result<u64> {
let b = self.bytes(8)?;
Ok(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn at_end(&self) -> bool {
self.at == self.b.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_names_are_stable() {
assert_eq!(version_name(0x00C1), "word97");
assert_eq!(version_name(0x00D9), "word2000");
assert_eq!(version_name(0x0101), "word2002");
assert_eq!(version_name(0x010C), "word2003");
assert_eq!(version_name(0x0065), "word97-2003");
}
#[test]
fn nfib_range_is_word_97_to_2003() {
assert!(!n_fib_plausible(0x0065)); assert!(n_fib_plausible(0x00C1));
assert!(n_fib_plausible(0x010C));
assert!(!n_fib_plausible(0x010D));
}
#[test]
fn cp1252_maps_the_high_range() {
assert_eq!(cp1252(0x41), 0x0041);
assert_eq!(cp1252(0x80), 0x20AC);
assert_eq!(cp1252(0xE9), 0x00E9);
assert_eq!(cp1252(0xFF), 0x00FF);
assert_eq!(cp1252(0x81), 0xFFFD);
}
#[test]
fn prose_is_not_a_doc() {
assert!(!detect(
b"The quick brown fox jumps over the lazy dog.\nPlain prose.\n",
Limits::DEFAULT
));
}
#[test]
fn random_bytes_never_panic() {
let mut seed: u64 = 0x1234_5678_9abc_def0;
for _ in 0..1024 {
let mut buf = [0u8; 1024];
for b in buf.iter_mut() {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*b = (seed >> 33) as u8;
}
let _ = detect(&buf, Limits::DEFAULT);
let _ = parse(&buf, Limits::STRICT);
}
let mut buf = vec![0u8; 2048];
buf[..8].copy_from_slice(&crate::adapter::cfbf::SIGNATURE);
for _ in 0..256 {
for b in buf[8..].iter_mut() {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*b = (seed >> 33) as u8;
}
let _ = detect(&buf, Limits::STRICT);
let _ = parse(&buf, Limits::STRICT);
}
}
}