use crate::error::{Error, Result};
use crate::limits::Limits;
pub const MODEL_VERSION: u8 = 1;
pub const MAX_MODEL_RECORDS: u32 = 1 << 24;
pub const MAX_MODEL_SHEETS: u32 = 1 << 20;
pub const MAX_MODEL_STRINGS: u32 = 1 << 24;
pub const MAX_MODEL_CELLS: u32 = 1 << 26;
pub const MAX_MODEL_TEXT_UNITS: u32 = 1 << 30;
pub const REC_EOF: u16 = 0x000A;
pub const REC_FORMULA: u16 = 0x0006;
pub const REC_CONTINUE: u16 = 0x003C;
pub const REC_BOUNDSHEET: u16 = 0x0085;
pub const REC_MULBLANK: u16 = 0x00BE;
pub const REC_XF: u16 = 0x00E0;
pub const REC_SST: u16 = 0x00FC;
pub const REC_LABELSST: u16 = 0x00FD;
pub const REC_DIMENSIONS: u16 = 0x0200;
pub const REC_BLANK: u16 = 0x0201;
pub const REC_NUMBER: u16 = 0x0203;
pub const REC_BOOLERR: u16 = 0x0205;
pub const REC_STRING: u16 = 0x0207;
pub const REC_ROW: u16 = 0x0208;
pub const REC_RK: u16 = 0x027E;
pub const REC_FORMAT: u16 = 0x041E;
pub const REC_BOF: u16 = 0x0809;
pub const BIFF8_VERSION: u16 = 0x0600;
pub const ST_WORKBOOK_GLOBALS: u16 = 0x0005;
pub const ST_WORKSHEET: u16 = 0x0010;
pub const ST_CHART: u16 = 0x0020;
pub const ST_MACRO: u16 = 0x0040;
pub const STREAM_WORKBOOK: &str = "Workbook";
pub const STREAM_BOOK: &str = "Book";
pub const NAME_WORKBOOK: u8 = 0;
pub const NAME_BOOK: u8 = 1;
pub const CELL_LABELSST: u8 = 0;
pub const CELL_NUMBER: u8 = 1;
pub const CELL_RK: u8 = 2;
pub const CELL_BOOLERR: u8 = 3;
pub const CELL_FORMULA: u8 = 4;
pub const CELL_BLANK: u8 = 5;
pub const CELL_MULBLANK: u8 = 6;
pub const FORMULA_NUMERIC: u8 = 0xFF;
pub const FIELD_SHEET: u8 = 0;
pub const FIELD_STRING: u8 = 1;
pub const fn field_name(field: u8) -> &'static str {
match field {
FIELD_STRING => "string",
_ => "sheet",
}
}
pub const fn stream_name(name: u8) -> &'static str {
match name {
NAME_BOOK => STREAM_BOOK,
_ => STREAM_WORKBOOK,
}
}
pub const fn substream_name(t: u16) -> &'static str {
match t {
ST_WORKBOOK_GLOBALS => "globals",
ST_WORKSHEET => "worksheet",
ST_CHART => "chart",
ST_MACRO => "macro",
_ => "unknown",
}
}
pub const fn is_substream_type(t: u16) -> bool {
matches!(t, ST_WORKBOOK_GLOBALS | ST_WORKSHEET | ST_CHART | ST_MACRO)
}
pub const fn sheet_type_name(dt: u8) -> &'static str {
match dt {
0x00 => "worksheet",
0x01 => "macro",
0x02 => "chart",
0x06 => "vbmodule",
_ => "unknown",
}
}
pub const fn cell_kind_name(kind: u8) -> &'static str {
match kind {
CELL_LABELSST => "labelsst",
CELL_NUMBER => "number",
CELL_RK => "rk",
CELL_BOOLERR => "boolerr",
CELL_FORMULA => "formula",
CELL_BLANK => "blank",
CELL_MULBLANK => "mulblank",
_ => "unknown",
}
}
pub const fn version_name(version: u16) -> &'static str {
match version {
BIFF8_VERSION => "biff8",
0x0500 => "biff5",
0x0700 => "biff7",
_ => "biff",
}
}
pub const fn state_name(state: u8) -> &'static str {
match state & 0x03 {
0 => "visible",
1 => "hidden",
2 => "veryhidden",
_ => "unknown",
}
}
pub fn rk_value(rk: u32) -> f64 {
let f_x100 = rk & 0x0000_0001 != 0;
let f_int = rk & 0x0000_0002 != 0;
let mut v = if f_int {
((rk as i32) >> 2) as f64
} else {
f64::from_bits(((rk & 0xFFFF_FFFC) as u64) << 32)
};
if f_x100 {
v /= 100.0;
}
v
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XlsRecord {
pub id: u16,
pub span: (u64, u64),
pub stream_off: u64,
pub len: u32,
}
impl XlsRecord {
pub fn payload_span(&self) -> (u64, u64) {
(self.span.0 + 4, self.span.0 + 4 + self.len as u64)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XlsSstString {
pub flags: u8,
pub compressed: bool,
pub rich: bool,
pub ext: bool,
pub text: Vec<u16>,
pub rec_span: (u64, u64),
pub n_parts: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XlsSst {
pub cst_total: u32,
pub cst_unique: u32,
pub strings: Vec<XlsSstString>,
pub record_spans: Vec<(u64, u64)>,
}
impl XlsSst {
pub fn string(&self, index: u32) -> Option<&XlsSstString> {
self.strings.get(index as usize)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XlsSheet {
pub name: Vec<u16>,
pub name_flags: u8,
pub name_compressed: bool,
pub pos: u32,
pub dt: u8,
pub state: u8,
pub rec_span: (u64, u64),
pub bof_span: (u64, u64),
pub dim_span: (u64, u64),
pub row_spans: Vec<(u64, u64)>,
pub cell_start: u32,
pub cell_end: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XlsCell {
pub kind: u8,
pub row: u32,
pub col: u32,
pub rec_span: (u64, u64),
pub isst: u32,
pub rk_raw: u32,
pub num_bits: u64,
pub bool_err: u8,
pub is_error: bool,
pub formula_special: u8,
pub has_string: bool,
pub string_span: (u64, u64),
pub text: Vec<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XlsModel {
pub doc_len: u64,
pub wb_span: (u64, u64),
pub stream_name: u8,
pub biff_version: u16,
pub root_substream: u16,
pub records: Vec<XlsRecord>,
pub sst: Option<XlsSst>,
pub sheets: Vec<XlsSheet>,
pub cells: Vec<XlsCell>,
pub xf_spans: Vec<(u64, u64)>,
pub format_spans: Vec<(u64, u64)>,
}
impl XlsModel {
pub fn sheet(&self, index: u32) -> Option<&XlsSheet> {
self.sheets.get(index as usize)
}
pub fn cell(&self, index: u32) -> Option<&XlsCell> {
self.cells.get(index as usize)
}
pub fn sheet_cells(&self, sheet: &XlsSheet) -> &[XlsCell] {
let s = sheet.cell_start as usize;
let e = sheet.cell_end as usize;
self.cells.get(s..e).unwrap_or(&[])
}
pub fn label_text(&self, cell: &XlsCell) -> Option<String> {
if cell.kind != CELL_LABELSST {
return None;
}
let s = self.sst.as_ref()?.string(cell.isst)?;
Some(String::from_utf16_lossy(&s.text))
}
pub fn text_units(&self) -> u64 {
let mut n = 0u64;
if let Some(sst) = &self.sst {
n = n.saturating_add(sst.strings.iter().map(|s| s.text.len() as u64).sum());
}
n = n.saturating_add(self.sheets.iter().map(|s| s.name.len() as u64).sum());
n = n.saturating_add(self.cells.iter().map(|c| c.text.len() as u64).sum::<u64>());
n
}
pub fn encode(&self) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(b"XLSM");
out.push(MODEL_VERSION);
out.extend_from_slice(&self.doc_len.to_le_bytes());
w_span(&mut out, self.wb_span);
out.push(self.stream_name);
out.extend_from_slice(&self.biff_version.to_le_bytes());
out.extend_from_slice(&self.root_substream.to_le_bytes());
out.extend_from_slice(&(self.records.len() as u32).to_le_bytes());
for r in &self.records {
out.extend_from_slice(&r.id.to_le_bytes());
w_span(&mut out, r.span);
out.extend_from_slice(&r.stream_off.to_le_bytes());
out.extend_from_slice(&r.len.to_le_bytes());
}
match &self.sst {
None => out.push(0),
Some(sst) => {
out.push(1);
out.extend_from_slice(&sst.cst_total.to_le_bytes());
out.extend_from_slice(&sst.cst_unique.to_le_bytes());
out.extend_from_slice(&(sst.record_spans.len() as u32).to_le_bytes());
for s in &sst.record_spans {
w_span(&mut out, *s);
}
out.extend_from_slice(&(sst.strings.len() as u32).to_le_bytes());
for s in &sst.strings {
out.push(s.flags);
out.push(s.compressed as u8);
out.push(s.rich as u8);
out.push(s.ext as u8);
w_span(&mut out, s.rec_span);
out.extend_from_slice(&s.n_parts.to_le_bytes());
w_units(&mut out, &s.text);
}
}
}
out.extend_from_slice(&(self.sheets.len() as u32).to_le_bytes());
for s in &self.sheets {
out.push(s.name_flags);
out.push(s.name_compressed as u8);
w_units(&mut out, &s.name);
out.extend_from_slice(&s.pos.to_le_bytes());
out.push(s.dt);
out.push(s.state);
w_span(&mut out, s.rec_span);
w_span(&mut out, s.bof_span);
w_span(&mut out, s.dim_span);
out.extend_from_slice(&(s.row_spans.len() as u32).to_le_bytes());
for rs in &s.row_spans {
w_span(&mut out, *rs);
}
out.extend_from_slice(&s.cell_start.to_le_bytes());
out.extend_from_slice(&s.cell_end.to_le_bytes());
}
out.extend_from_slice(&(self.cells.len() as u32).to_le_bytes());
for c in &self.cells {
out.push(c.kind);
out.extend_from_slice(&c.row.to_le_bytes());
out.extend_from_slice(&c.col.to_le_bytes());
w_span(&mut out, c.rec_span);
out.extend_from_slice(&c.isst.to_le_bytes());
out.extend_from_slice(&c.rk_raw.to_le_bytes());
out.extend_from_slice(&c.num_bits.to_le_bytes());
out.push(c.bool_err);
out.push(c.is_error as u8);
out.push(c.formula_special);
out.push(c.has_string as u8);
w_span(&mut out, c.string_span);
w_units(&mut out, &c.text);
}
out.extend_from_slice(&(self.xf_spans.len() as u32).to_le_bytes());
for s in &self.xf_spans {
w_span(&mut out, *s);
}
out.extend_from_slice(&(self.format_spans.len() as u32).to_le_bytes());
for s in &self.format_spans {
w_span(&mut out, *s);
}
out
}
pub fn decode(bytes: &[u8]) -> Result<XlsModel> {
let mut r = BinReader::new(bytes);
if r.bytes(4)? != b"XLSM" {
return Err(corrupt("bad model magic"));
}
if r.u8()? != MODEL_VERSION {
return Err(corrupt("unsupported model version"));
}
let doc_len = r.u64()?;
let wb_span = r.span()?;
check_span(wb_span, doc_len, "Workbook stream span")?;
let stream_name = r.u8()?;
if stream_name != NAME_WORKBOOK && stream_name != NAME_BOOK {
return Err(corrupt("bad stream-name marker"));
}
let biff_version = r.u16()?;
if biff_version != BIFF8_VERSION {
return Err(corrupt("model BIFF version is not BIFF8"));
}
let root_substream = r.u16()?;
if !is_substream_type(root_substream) {
return Err(corrupt("model root substream type is unknown"));
}
let n_records = r.u32()?;
if n_records > MAX_MODEL_RECORDS {
return Err(corrupt("model record count is implausible"));
}
let mut records = Vec::new();
for _ in 0..n_records {
let id = r.u16()?;
let span = r.span()?;
check_span(span, wb_span.1, "record span")?;
let stream_off = r.u64()?;
let len = r.u32()?;
if span.1 - span.0 != 4 + len as u64 {
return Err(corrupt("record span disagrees with its length"));
}
if stream_off + span.1 - span.0 > wb_span.1 {
return Err(corrupt("record lies outside the workbook stream"));
}
records.push(XlsRecord {
id,
span,
stream_off,
len,
});
}
let sst = if r.u8()? != 0 {
let cst_total = r.u32()?;
let cst_unique = r.u32()?;
if cst_unique > MAX_MODEL_STRINGS {
return Err(corrupt("model SST count is implausible"));
}
let n_spans = r.u32()?;
if n_spans > MAX_MODEL_RECORDS {
return Err(corrupt("model SST record-span count is implausible"));
}
let mut record_spans = Vec::new();
for _ in 0..n_spans {
let s = r.span()?;
check_span(s, doc_len, "SST record span")?;
record_spans.push(s);
}
let n_strings = r.u32()?;
if n_strings > MAX_MODEL_STRINGS {
return Err(corrupt("model SST string count is implausible"));
}
let mut strings = Vec::new();
let mut units_total: u64 = 0;
for _ in 0..n_strings {
let flags = r.u8()?;
let compressed = r.u8()? != 0;
let rich = r.u8()? != 0;
let ext = r.u8()? != 0;
let rec_span = r.span()?;
check_span(rec_span, doc_len, "SST string record span")?;
let n_parts = r.u32()?;
let text = r.units()?;
units_total = units_total.saturating_add(text.len() as u64);
if units_total > MAX_MODEL_TEXT_UNITS as u64 {
return Err(corrupt("model text is implausible"));
}
strings.push(XlsSstString {
flags,
compressed,
rich,
ext,
text,
rec_span,
n_parts,
});
}
Some(XlsSst {
cst_total,
cst_unique,
strings,
record_spans,
})
} else {
None
};
let n_sheets = r.u32()?;
if n_sheets > MAX_MODEL_SHEETS {
return Err(corrupt("model sheet count is implausible"));
}
let mut sheets = Vec::new();
let mut units_total: u64 = 0;
for _ in 0..n_sheets {
let name_flags = r.u8()?;
let name_compressed = r.u8()? != 0;
let name = r.units()?;
units_total = units_total.saturating_add(name.len() as u64);
if units_total > MAX_MODEL_TEXT_UNITS as u64 {
return Err(corrupt("model text is implausible"));
}
let pos = r.u32()?;
let dt = r.u8()?;
let state = r.u8()?;
let rec_span = r.span()?;
check_span(rec_span, doc_len, "sheet record span")?;
let bof_span = r.span()?;
check_span(bof_span, doc_len, "sheet BOF span")?;
let dim_span = r.span()?;
check_span(dim_span, doc_len, "sheet DIMENSIONS span")?;
let n_rows = r.u32()?;
if n_rows > MAX_MODEL_RECORDS {
return Err(corrupt("model sheet ROW count is implausible"));
}
let mut row_spans = Vec::new();
for _ in 0..n_rows {
let s = r.span()?;
check_span(s, doc_len, "sheet ROW span")?;
row_spans.push(s);
}
let cell_start = r.u32()?;
let cell_end = r.u32()?;
sheets.push(XlsSheet {
name,
name_flags,
name_compressed,
pos,
dt,
state,
rec_span,
bof_span,
dim_span,
row_spans,
cell_start,
cell_end,
});
}
let n_cells = r.u32()?;
if n_cells > MAX_MODEL_CELLS {
return Err(corrupt("model cell count is implausible"));
}
let mut cells = Vec::new();
for _ in 0..n_cells {
let kind = r.u8()?;
let row = r.u32()?;
let col = r.u32()?;
let rec_span = r.span()?;
check_span(rec_span, doc_len, "cell record span")?;
let isst = r.u32()?;
let rk_raw = r.u32()?;
let num_bits = r.u64()?;
let bool_err = r.u8()?;
let is_error = r.u8()? != 0;
let formula_special = r.u8()?;
let has_string = r.u8()? != 0;
let string_span = r.span()?;
check_span(string_span, doc_len, "cell STRING span")?;
let text = r.units()?;
units_total = units_total.saturating_add(text.len() as u64);
if units_total > MAX_MODEL_TEXT_UNITS as u64 {
return Err(corrupt("model text is implausible"));
}
cells.push(XlsCell {
kind,
row,
col,
rec_span,
isst,
rk_raw,
num_bits,
bool_err,
is_error,
formula_special,
has_string,
string_span,
text,
});
}
for s in &sheets {
if s.cell_start > s.cell_end || s.cell_end as usize > cells.len() {
return Err(corrupt("a sheet cell range is out of bounds"));
}
}
let n_xf = r.u32()?;
if n_xf > MAX_MODEL_RECORDS {
return Err(corrupt("model XF count is implausible"));
}
let mut xf_spans = Vec::new();
for _ in 0..n_xf {
let s = r.span()?;
check_span(s, doc_len, "XF record span")?;
xf_spans.push(s);
}
let n_fmt = r.u32()?;
if n_fmt > MAX_MODEL_RECORDS {
return Err(corrupt("model FORMAT count is implausible"));
}
let mut format_spans = Vec::new();
for _ in 0..n_fmt {
let s = r.span()?;
check_span(s, doc_len, "FORMAT record span")?;
format_spans.push(s);
}
if !r.at_end() {
return Err(corrupt("model has trailing bytes"));
}
Ok(XlsModel {
doc_len,
wb_span,
stream_name,
biff_version,
root_substream,
records,
sst,
sheets,
cells,
xf_spans,
format_spans,
})
}
}
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 XlsMatch {
pub field: u8,
pub index: u32,
pub start: u64,
pub end: u64,
pub text: String,
}
pub fn find(model: &XlsModel, pattern: &str, max_bytes: u64) -> Result<Vec<XlsMatch>> {
if pattern.is_empty() {
return Ok(Vec::new());
}
let mut out: Vec<XlsMatch> = Vec::new();
let mut estimated: u64 = 0;
let mut push = |field: u8, index: u32, span: (u64, u64), text: String| -> Result<()> {
estimated = estimated.saturating_add(64 + text.len() as u64);
if estimated > max_bytes {
return Err(Error::resource_limit(format!(
"xls find exceeded the {max_bytes}-byte budget"
)));
}
out.push(XlsMatch {
field,
index,
start: span.0,
end: span.1,
text,
});
Ok(())
};
for (i, s) in model.sheets.iter().enumerate() {
let name = String::from_utf16_lossy(&s.name);
if name.contains(pattern) {
push(FIELD_SHEET, i as u32, s.rec_span, name)?;
}
}
if let Some(sst) = &model.sst {
for (i, s) in sst.strings.iter().enumerate() {
let text = String::from_utf16_lossy(&s.text);
if text.contains(pattern) {
push(FIELD_STRING, i as u32, s.rec_span, text)?;
}
}
}
Ok(out)
}
pub fn canonical_text(model: &XlsModel, _source: &[u8], max_bytes: u64) -> Result<String> {
let mut out = String::new();
for (i, sheet) in model.sheets.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&String::from_utf16_lossy(&sheet.name));
for c in model.sheet_cells(sheet) {
let text = match c.kind {
CELL_LABELSST => model.label_text(c),
CELL_FORMULA if c.has_string => Some(String::from_utf16_lossy(&c.text)),
_ => None,
};
if let Some(t) = text
&& !t.is_empty()
{
out.push('\t');
out.push_str(&t);
}
}
if out.len() as u64 > max_bytes {
return Err(Error::resource_limit(format!(
"xls canonical text exceeded the {max_bytes}-byte budget"
)));
}
}
Ok(out)
}
pub fn detect(source: &[u8], limits: Limits) -> bool {
if source.len() as u64 > limits.max_xls_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(entry) = find_workbook(&model) else {
return false;
};
let Ok(wb) = crate::adapter::cfbf::stream_leading(source, &model, entry, 8) else {
return false;
};
if wb.len() < 8 {
return false;
}
let id = u16::from_le_bytes([wb[0], wb[1]]);
if id != REC_BOF {
return false;
}
let len = u16::from_le_bytes([wb[2], wb[3]]);
if len < 4 {
return false;
}
let version = u16::from_le_bytes([wb[4], wb[5]]);
if version != BIFF8_VERSION {
return false;
}
let subtype = u16::from_le_bytes([wb[6], wb[7]]);
is_substream_type(subtype)
}
pub fn parse(source: &[u8], limits: Limits) -> Result<XlsModel> {
let len = source.len() as u64;
if len > limits.max_xls_document_bytes {
return Err(Error::resource_limit(format!(
"xls source is {len} bytes, above the {}-byte document cap",
limits.max_xls_document_bytes
)));
}
let cfbf = crate::adapter::cfbf::parse(source, limits)?;
let (entry, stream_name) = find_workbook_named(&cfbf)
.ok_or_else(|| invalid("the compound file has no Workbook or Book stream"))?;
let wb = crate::adapter::cfbf::stream_bytes(source, entry)?;
let base = entry.data_span.0;
let wb_span = (base, base + wb.len() as u64);
let records = scan_records(wb, base, limits)?;
let first = records
.first()
.ok_or_else(|| invalid("the workbook stream is empty"))?;
if first.id != REC_BOF {
return Err(invalid("the workbook stream does not begin with a BOF"));
}
let bof_pl = payload(wb, first);
let biff_version = rd_u16(bof_pl, 0).ok_or_else(|| invalid("the BOF is truncated"))?;
if biff_version != BIFF8_VERSION {
return Err(invalid(format!(
"unsupported BIFF version 0x{biff_version:04X}"
)));
}
let root_substream = rd_u16(bof_pl, 2).ok_or_else(|| invalid("the BOF is truncated"))?;
if !is_substream_type(root_substream) {
return Err(invalid(format!(
"unknown BOF substream type 0x{root_substream:04X}"
)));
}
if root_substream != ST_WORKBOOK_GLOBALS {
return Err(invalid(
"the workbook stream does not begin with a workbook-globals substream",
));
}
let g_eof = (1..records.len())
.find(|&i| records[i].id == REC_EOF)
.ok_or_else(|| invalid("the globals substream has no EOF"))?;
let mut sheets: Vec<XlsSheet> = Vec::new();
let mut sst: Option<XlsSst> = None;
let mut xf_spans: Vec<(u64, u64)> = Vec::new();
let mut format_spans: Vec<(u64, u64)> = Vec::new();
let mut i = 1usize;
while i < g_eof {
let r = &records[i];
match r.id {
REC_BOUNDSHEET => {
if sheets.len() as u64 >= limits.max_xls_sheets {
return Err(Error::resource_limit(format!(
"the workbook has more than {} sheets",
limits.max_xls_sheets
)));
}
let s = parse_boundsheet(wb, r)?;
sheets.push(s);
}
REC_SST => {
if sst.is_none() {
let mut parts: Vec<&[u8]> = Vec::new();
let mut spans: Vec<(u64, u64)> = Vec::new();
parts.push(payload(wb, r));
spans.push(r.span);
let mut j = i + 1;
while j < records.len() && records[j].id == REC_CONTINUE {
parts.push(payload(wb, &records[j]));
spans.push(records[j].span);
j += 1;
}
sst = Some(parse_sst(&parts, &spans, limits)?);
i = j;
continue;
}
}
REC_XF => xf_spans.push(r.span),
REC_FORMAT => format_spans.push(r.span),
_ => {}
}
i += 1;
}
let mut cells: Vec<XlsCell> = Vec::new();
for sheet in sheets.iter_mut() {
let pos = sheet.pos as u64;
let bof_idx = records
.iter()
.position(|rr| rr.id == REC_BOF && rr.stream_off == pos)
.ok_or_else(|| invalid(format!("BOUNDSHEET position {pos} does not address a BOF")))?;
if bof_idx == 0 {
return Err(invalid("a BOUNDSHEET addresses the workbook-globals BOF"));
}
let bof = &records[bof_idx];
let st = rd_u16(payload(wb, bof), 2).ok_or_else(|| invalid("a sheet BOF is truncated"))?;
if !matches!(st, ST_WORKSHEET | ST_CHART | ST_MACRO) {
return Err(invalid("a BOUNDSHEET does not address a sheet substream"));
}
sheet.bof_span = bof.span;
let eof_idx = (bof_idx + 1..records.len())
.find(|&j| records[j].id == REC_EOF)
.ok_or_else(|| invalid("a sheet substream has no EOF"))?;
let cell_start = cells.len() as u32;
for k in (bof_idx + 1)..eof_idx {
let r = &records[k];
match r.id {
REC_DIMENSIONS => sheet.dim_span = r.span,
REC_ROW => sheet.row_spans.push(r.span),
REC_LABELSST | REC_NUMBER | REC_RK | REC_BOOLERR | REC_FORMULA | REC_BLANK
| REC_MULBLANK => {
scan_cell(wb, &records, k, &mut cells, limits)?;
}
_ => {}
}
}
sheet.cell_start = cell_start;
sheet.cell_end = cells.len() as u32;
}
let text_units = aggregate_text_units(&sst, &sheets, &cells);
if text_units > limits.max_xls_text_bytes {
return Err(Error::resource_limit(format!(
"xls derived text is above the {}-byte text cap",
limits.max_xls_text_bytes
)));
}
Ok(XlsModel {
doc_len: len,
wb_span,
stream_name,
biff_version,
root_substream,
records,
sst,
sheets,
cells,
xf_spans,
format_spans,
})
}
pub fn build_xls_model(source: &[u8], limits: Limits) -> Result<Vec<u8>> {
Ok(parse(source, limits)?.encode())
}
fn scan_records(wb: &[u8], base: u64, limits: Limits) -> Result<Vec<XlsRecord>> {
let mut out: Vec<XlsRecord> = Vec::new();
let mut off = 0usize;
while off < wb.len() {
if wb.len() - off < 4 {
return Err(invalid("a BIFF record header is truncated"));
}
let id = u16::from_le_bytes([wb[off], wb[off + 1]]);
let payload_len = u16::from_le_bytes([wb[off + 2], wb[off + 3]]) as usize;
let rec_end = off
.checked_add(4)
.and_then(|o| o.checked_add(payload_len))
.ok_or_else(|| invalid("a BIFF record length overflows"))?;
if rec_end > wb.len() {
return Err(invalid("a BIFF record extends past the stream"));
}
if out.len() as u64 >= limits.max_xls_records {
return Err(Error::resource_limit(format!(
"the workbook has more than {} records",
limits.max_xls_records
)));
}
out.push(XlsRecord {
id,
span: (base + off as u64, base + rec_end as u64),
stream_off: off as u64,
len: payload_len as u32,
});
off = rec_end;
}
Ok(out)
}
fn parse_boundsheet(wb: &[u8], r: &XlsRecord) -> Result<XlsSheet> {
let pl = payload(wb, r);
if pl.len() < 8 {
return Err(invalid("a BOUNDSHEET is truncated"));
}
let pos = rd_u32(pl, 0).ok_or_else(|| invalid("a BOUNDSHEET is truncated"))?;
let state = pl[4];
let dt = pl[5];
let cch = pl[6] as usize;
let name_flags = pl[7];
let name_compressed = name_flags & 0x01 == 0;
let unit = if name_compressed { 1 } else { 2 };
let need = 8 + cch * unit;
if pl.len() < need {
return Err(invalid("a BOUNDSHEET name is truncated"));
}
let mut name = Vec::with_capacity(cch);
for k in 0..cch {
if name_compressed {
name.push(pl[8 + k] as u16);
} else {
let o = 8 + k * 2;
name.push(u16::from_le_bytes([pl[o], pl[o + 1]]));
}
}
Ok(XlsSheet {
name,
name_flags,
name_compressed,
pos,
dt,
state,
rec_span: r.span,
bof_span: (0, 0),
dim_span: (0, 0),
row_spans: Vec::new(),
cell_start: 0,
cell_end: 0,
})
}
fn scan_cell(
wb: &[u8],
records: &[XlsRecord],
index: usize,
cells: &mut Vec<XlsCell>,
limits: Limits,
) -> Result<()> {
let r = &records[index];
let pl = payload(wb, r);
let mut push = |cell: XlsCell| -> Result<()> {
if cells.len() as u64 >= limits.max_xls_cells {
return Err(Error::resource_limit(format!(
"the workbook has more than {} cells",
limits.max_xls_cells
)));
}
cells.push(cell);
Ok(())
};
let blank = XlsCell {
kind: CELL_BLANK,
row: 0,
col: 0,
rec_span: r.span,
isst: 0,
rk_raw: 0,
num_bits: 0,
bool_err: 0,
is_error: false,
formula_special: FORMULA_NUMERIC,
has_string: false,
string_span: (0, 0),
text: Vec::new(),
};
match r.id {
REC_LABELSST => {
if pl.len() < 10 {
return Err(invalid("a LABELSST is truncated"));
}
push(XlsCell {
kind: CELL_LABELSST,
row: rd_u16(pl, 0).unwrap() as u32,
col: rd_u16(pl, 2).unwrap() as u32,
isst: rd_u32(pl, 6).unwrap(),
..blank
})?;
}
REC_NUMBER => {
if pl.len() < 14 {
return Err(invalid("a NUMBER is truncated"));
}
push(XlsCell {
kind: CELL_NUMBER,
row: rd_u16(pl, 0).unwrap() as u32,
col: rd_u16(pl, 2).unwrap() as u32,
num_bits: rd_u64(pl, 6).unwrap(),
..blank
})?;
}
REC_RK => {
if pl.len() < 10 {
return Err(invalid("an RK is truncated"));
}
push(XlsCell {
kind: CELL_RK,
row: rd_u16(pl, 0).unwrap() as u32,
col: rd_u16(pl, 2).unwrap() as u32,
rk_raw: rd_u32(pl, 6).unwrap(),
..blank
})?;
}
REC_BOOLERR => {
if pl.len() < 8 {
return Err(invalid("a BOOLERR is truncated"));
}
push(XlsCell {
kind: CELL_BOOLERR,
row: rd_u16(pl, 0).unwrap() as u32,
col: rd_u16(pl, 2).unwrap() as u32,
bool_err: pl[6],
is_error: pl[7] != 0,
..blank
})?;
}
REC_BLANK => {
if pl.len() < 6 {
return Err(invalid("a BLANK is truncated"));
}
push(XlsCell {
kind: CELL_BLANK,
row: rd_u16(pl, 0).unwrap() as u32,
col: rd_u16(pl, 2).unwrap() as u32,
..blank
})?;
}
REC_MULBLANK => {
if pl.len() < 8 {
return Err(invalid("a MULBLANK is truncated"));
}
let row = rd_u16(pl, 0).unwrap() as u32;
let mut off = 6usize;
loop {
let v =
rd_u16(pl, off).ok_or_else(|| invalid("a MULBLANK run has no terminator"))?;
let col = (v & 0x00FF) as u32;
push(XlsCell {
kind: CELL_MULBLANK,
row,
col,
rec_span: r.span,
isst: 0,
rk_raw: 0,
num_bits: 0,
bool_err: 0,
is_error: false,
formula_special: FORMULA_NUMERIC,
has_string: false,
string_span: (0, 0),
text: Vec::new(),
})?;
off += 2;
if v & 0x0100 != 0 {
break;
}
}
}
REC_FORMULA => {
if pl.len() < 20 {
return Err(invalid("a FORMULA is truncated"));
}
let row = rd_u16(pl, 0).unwrap() as u32;
let col = rd_u16(pl, 2).unwrap() as u32;
let num = rd_u64(pl, 6).unwrap();
let num_bytes = num.to_le_bytes();
let special = if num_bytes[6] == 0xFF && num_bytes[7] == 0xFF {
num_bytes[0]
} else {
FORMULA_NUMERIC
};
if special == 0 {
let next = records
.get(index + 1)
.ok_or_else(|| invalid("a string FORMULA has no STRING record"))?;
if next.id != REC_STRING {
return Err(invalid("a string FORMULA has no STRING record"));
}
let text = decode_unicode_string(payload(wb, next))?;
push(XlsCell {
kind: CELL_FORMULA,
row,
col,
num_bits: num,
formula_special: special,
has_string: true,
string_span: next.span,
text,
..blank
})?;
} else {
push(XlsCell {
kind: CELL_FORMULA,
row,
col,
num_bits: num,
formula_special: special,
..blank
})?;
}
}
_ => {}
}
Ok(())
}
fn decode_unicode_string(pl: &[u8]) -> Result<Vec<u16>> {
if pl.len() < 3 {
return Err(invalid("a STRING is truncated"));
}
let cch = rd_u16(pl, 0).unwrap() as usize;
let flags = pl[2];
let compressed = flags & 0x01 == 0;
let unit = if compressed { 1 } else { 2 };
let need = 3 + cch * unit;
if pl.len() < need {
return Err(invalid("a STRING is truncated"));
}
let mut text = Vec::with_capacity(cch);
for k in 0..cch {
if compressed {
text.push(pl[3 + k] as u16);
} else {
let o = 3 + k * 2;
text.push(u16::from_le_bytes([pl[o], pl[o + 1]]));
}
}
Ok(text)
}
fn parse_sst(parts: &[&[u8]], spans: &[(u64, u64)], limits: Limits) -> Result<XlsSst> {
let mut r = SstReader::new(parts);
let cst_total = r
.u32()
.ok_or_else(|| invalid("the SST header is truncated"))?;
let cst_unique = r
.u32()
.ok_or_else(|| invalid("the SST header is truncated"))?;
if cst_unique as u64 > limits.max_xls_sst_strings {
return Err(Error::resource_limit(format!(
"the SST has {cst_unique} strings, above max_xls_sst_strings {}",
limits.max_xls_sst_strings
)));
}
let mut strings: Vec<XlsSstString> = Vec::new();
for _ in 0..cst_unique {
let cch = r
.u16()
.ok_or_else(|| invalid("an SST string header is truncated"))?;
let flags = r
.byte()
.ok_or_else(|| invalid("an SST string header is truncated"))?;
let compressed = flags & 0x01 == 0;
let rich = flags & 0x08 != 0;
let ext = flags & 0x04 != 0;
let c_run = if rich {
r.u16()
.ok_or_else(|| invalid("an SST rich-run count is truncated"))?
} else {
0
};
let cb_ext = if ext {
r.u32()
.ok_or_else(|| invalid("an SST extended-phonetic size is truncated"))?
} else {
0
};
let start_part = r.part;
let mut text: Vec<u16> = Vec::new();
r.char_array(cch, compressed, &mut text)
.ok_or_else(|| invalid("an SST string is truncated or its CONTINUE is malformed"))?;
if rich {
r.skip(c_run as usize * 4)
.ok_or_else(|| invalid("an SST rich-run array is truncated"))?;
}
if ext {
r.skip(cb_ext as usize)
.ok_or_else(|| invalid("an SST extended-phonetic data is truncated"))?;
}
let rec_span = *spans
.get(start_part)
.ok_or_else(|| invalid("SST part has no record span"))?;
let n_parts = (r.part - start_part + 1) as u32;
strings.push(XlsSstString {
flags,
compressed,
rich,
ext,
text,
rec_span,
n_parts,
});
}
if !r.at_end() {
return Err(invalid(
"the SST has trailing bytes (its string count disagrees with its data)",
));
}
Ok(XlsSst {
cst_total,
cst_unique,
strings,
record_spans: spans.to_vec(),
})
}
fn aggregate_text_units(sst: &Option<XlsSst>, sheets: &[XlsSheet], cells: &[XlsCell]) -> u64 {
let mut n = 0u64;
if let Some(sst) = sst {
n = n.saturating_add(sst.strings.iter().map(|s| s.text.len() as u64).sum());
}
n = n.saturating_add(sheets.iter().map(|s| s.name.len() as u64).sum());
n = n.saturating_add(cells.iter().map(|c| c.text.len() as u64).sum());
n
}
struct SstReader<'a> {
parts: &'a [&'a [u8]],
part: usize,
off: usize,
}
impl<'a> SstReader<'a> {
fn new(parts: &'a [&'a [u8]]) -> Self {
SstReader {
parts,
part: 0,
off: 0,
}
}
fn at_end(&self) -> bool {
if self.part >= self.parts.len() {
return true;
}
if self.off < self.parts[self.part].len() {
return false;
}
self.parts[self.part + 1..].iter().all(|p| p.is_empty())
}
fn byte(&mut self) -> Option<u8> {
while self.part < self.parts.len() && self.off >= self.parts[self.part].len() {
self.part += 1;
self.off = 0;
}
if self.part >= self.parts.len() {
return None;
}
let b = *self.parts[self.part].get(self.off)?;
self.off += 1;
Some(b)
}
fn u16(&mut self) -> Option<u16> {
let a = self.byte()?;
let b = self.byte()?;
Some(u16::from_le_bytes([a, b]))
}
fn u32(&mut self) -> Option<u32> {
let a = self.byte()?;
let b = self.byte()?;
let c = self.byte()?;
let d = self.byte()?;
Some(u32::from_le_bytes([a, b, c, d]))
}
fn skip(&mut self, mut n: usize) -> Option<()> {
while n > 0 {
while self.part < self.parts.len() && self.off >= self.parts[self.part].len() {
self.part += 1;
self.off = 0;
}
if self.part >= self.parts.len() {
return None;
}
let avail = self.parts[self.part].len() - self.off;
let take = avail.min(n);
self.off += take;
n -= take;
}
Some(())
}
fn char_array(&mut self, cch: u16, mut compressed: bool, out: &mut Vec<u16>) -> Option<()> {
let mut left = cch as usize;
while left > 0 {
if self.part >= self.parts.len() {
return None;
}
if self.off >= self.parts[self.part].len() {
if self.part + 1 >= self.parts.len() {
return None;
}
self.part += 1;
let flag = *self.parts[self.part].first()?;
compressed = flag & 0x01 == 0;
self.off = 1;
if self.off >= self.parts[self.part].len() {
return None;
}
}
let avail = self.parts[self.part].len() - self.off;
let unit = if compressed { 1 } else { 2 };
if avail < unit {
return None;
}
let can = (avail / unit).min(left);
let p = self.parts[self.part];
for _ in 0..can {
if compressed {
out.push(p[self.off] as u16);
self.off += 1;
} else {
out.push(u16::from_le_bytes([p[self.off], p[self.off + 1]]));
self.off += 2;
}
}
left -= can;
}
Some(())
}
}
fn find_workbook_named(
model: &crate::adapter::cfbf::CfbfModel,
) -> Option<(&crate::adapter::cfbf::CfbfEntry, u8)> {
if let Some(e) =
find_named(model, STREAM_WORKBOOK).or_else(|| find_named_ci(model, STREAM_WORKBOOK))
{
return Some((e, NAME_WORKBOOK));
}
find_named(model, STREAM_BOOK)
.or_else(|| find_named_ci(model, STREAM_BOOK))
.map(|e| (e, NAME_BOOK))
}
fn find_workbook(
model: &crate::adapter::cfbf::CfbfModel,
) -> Option<&crate::adapter::cfbf::CfbfEntry> {
find_workbook_named(model).map(|(e, _)| e)
}
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
&& crate::adapter::cfbf::entry_name_text(e) == name
})
}
fn find_named_ci<'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
&& crate::adapter::cfbf::entry_name_text(e).eq_ignore_ascii_case(name)
})
}
fn payload<'a>(wb: &'a [u8], r: &XlsRecord) -> &'a [u8] {
let s = r.stream_off as usize + 4;
let e = s + r.len as usize;
wb.get(s..e).unwrap_or(&[])
}
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 rd_u64(b: &[u8], off: usize) -> Option<u64> {
let s = b.get(off..off + 8)?;
Some(u64::from_le_bytes([
s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7],
]))
}
fn invalid(message: impl Into<String>) -> Error {
Error::invalid_xls_structure(format!("xls: {}", message.into()))
}
fn corrupt(message: impl Into<String>) -> Error {
Error::invalid_xls_structure(format!("xls model: {}", message.into()))
}
fn w_span(out: &mut Vec<u8>, s: (u64, u64)) {
out.extend_from_slice(&s.0.to_le_bytes());
out.extend_from_slice(&s.1.to_le_bytes());
}
fn w_units(out: &mut Vec<u8>, units: &[u16]) {
out.extend_from_slice(&(units.len() as u32).to_le_bytes());
for u in units {
out.extend_from_slice(&u.to_le_bytes());
}
}
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 span(&mut self) -> Result<(u64, u64)> {
Ok((self.u64()?, self.u64()?))
}
fn units(&mut self) -> Result<Vec<u16>> {
let n = self.u32()?;
if n as u64 > MAX_MODEL_TEXT_UNITS as u64 {
return Err(corrupt("text length is implausible"));
}
let bytes = self.bytes(n as usize * 2)?;
Ok(bytes
.as_chunks::<2>()
.0
.iter()
.map(|c| u16::from_le_bytes(*c))
.collect())
}
fn at_end(&self) -> bool {
self.at == self.b.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_and_name_tables_are_stable() {
assert_eq!(version_name(BIFF8_VERSION), "biff8");
assert_eq!(version_name(0x0500), "biff5");
assert_eq!(stream_name(NAME_WORKBOOK), "Workbook");
assert_eq!(stream_name(NAME_BOOK), "Book");
assert_eq!(substream_name(ST_WORKBOOK_GLOBALS), "globals");
assert!(is_substream_type(ST_WORKSHEET));
assert!(!is_substream_type(0x0001));
assert_eq!(cell_kind_name(CELL_LABELSST), "labelsst");
assert_eq!(sheet_type_name(0x00), "worksheet");
assert_eq!(state_name(1), "hidden");
}
#[test]
fn rk_decodes_int_x100_and_float() {
assert_eq!(rk_value((7u32 << 2) | 0x02), 7.0);
assert_eq!(rk_value((7u32 << 2) | 0x02 | 0x01), 0.07);
assert_eq!(rk_value(0x3FF0_0000), 1.0);
assert_eq!(rk_value(((-3i32 as u32) << 2) | 0x02), -3.0);
}
#[test]
fn prose_is_not_xls() {
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 = 0x2620_0000_5156_4c53;
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; 4096];
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);
}
}
}