Files
gridcraft/crates/xlsx/src/xls.rs
T

886 lines
34 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Legacy Excel 97-2003 `.xls` (BIFF8) import, implemented from Microsoft's public
//! specifications [MS-CFB] (the compound file container) and [MS-XLS] (the Workbook stream):
//! <https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/cd03cb5f-ca02-4934-a391-bb674cb8aa06>.
//!
//! Read: worksheets, shared strings, cell values, formulas (text and cached results, including
//! shared and array formulas), number formats, the 1904 date system, merged cells, column
//! widths, row heights, defined names, print areas, print titles and the calculation settings
//! (mode, iteration, precision as displayed). Everything else (fonts, fills, borders, charts,
//! pictures, comments, conditional formats, validation, macros) is skipped with a warning.
//! Encrypted workbooks and pre-BIFF8 versions are refused.
mod formula;
mod ftab;
mod records;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use gridcraft_core::{CellRef, DateSystem, MAX_COLS, MAX_ROWS, RangeRef, Value};
use gridcraft_model::{CalcMode, CalcSettings, Cell, DefinedName, Formula, LineInfo, NumFmt, Sheet, Style, StyleId, Visibility, Workbook};
use crate::cfb::Compound;
use crate::tables::{builtin_format, col_width_to_px, pt_to_px};
use crate::{IoError, ReadReport};
use formula::{Links, Mode, SupBook, Unsupported, Xti, cell_error, decode};
use records::{Cursor, Record, Records};
const MAX_CELLS: usize = 4_000_000;
const MAX_STRINGS: usize = 4_000_000;
const MAX_TEXT: usize = 256 * 1024 * 1024;
const MAX_SHEETS: usize = 4096;
const MAX_NAMES: usize = 65_536;
const MAX_XFS: usize = 65_536;
const MAX_MERGES: usize = 100_000;
/// Records read from the whole Workbook stream at most (a 65,536 × 256 sheet has fewer). One
/// allowance shared by every substream: many BoundSheet records pointing into one huge
/// substream must not each get their own.
const MAX_RECORDS: usize = 40_000_000;
const BOF: u16 = 0x0809;
const EOF: u16 = 0x000A;
const BIFF8: u16 = 0x0600;
/// WsBool (MS-XLS 2.4.351): its first byte's `fDialog` bit marks a dialog sheet, whose BOF says worksheet.
const WSBOOL: u16 = 0x0081;
const WSBOOL_DIALOG: u8 = 0x10;
pub(crate) fn invalid(msg: impl Into<String>) -> IoError {
IoError::Format(format!("XLS: {}", msg.into()))
}
/// The message for an encrypted workbook (a FilePass record).
const ENCRYPTED: &str =
"this .xls workbook is password-protected (encrypted). GridCraft can't open encrypted .xls files; remove the password in Excel and try again.";
/// Whether the bytes are a compound file holding an Excel workbook stream (BIFF8 `Workbook`
/// or BIFF5 `Book`).
pub(crate) fn is_xls(bytes: &[u8]) -> bool {
let Ok(cf) = Compound::open(bytes) else { return false };
cf.has_root_stream("Workbook") || cf.has_root_stream("Book")
}
/// Most shared formulas one sheet expands, and the longest template text one attach may
/// produce: a template shared by its followers was costing every one of them a full decode and
/// print however often that could never succeed, so each anchor is tried once and failed ones
/// are skipped for the rest of the sheet. Beyond the cap the remaining references keep their
/// cached values.
const MAX_SHARED_ATTACHES: usize = 262_144;
const MAX_SHARED_TEMPLATE: usize = 8192;
pub(crate) struct Budget {
cells: usize,
text: usize,
records: usize,
max_records: usize,
}
impl Default for Budget {
fn default() -> Self {
Budget { cells: 0, text: 0, records: 0, max_records: MAX_RECORDS }
}
}
impl Budget {
/// A budget with a smaller record allowance (tests).
#[cfg(test)]
pub(crate) fn with_max_records(max_records: usize) -> Self {
Budget { max_records, ..Budget::default() }
}
fn record(&mut self) -> Result<(), IoError> {
self.records = self.records.saturating_add(1);
if self.records > self.max_records {
return Err(IoError::TooLarge("XLS: too many records".into()));
}
Ok(())
}
fn cell(&mut self) -> Result<(), IoError> {
self.cells += 1;
if self.cells > MAX_CELLS {
return Err(IoError::TooLarge("XLS: more than 4 million cells".into()));
}
Ok(())
}
fn text(&mut self, len: usize) -> Result<(), IoError> {
self.text = self.text.saturating_add(len);
if self.text > MAX_TEXT {
return Err(IoError::TooLarge("XLS: more than 256 MB of text".into()));
}
Ok(())
}
}
struct BoundSheet {
pos: usize,
name: String,
visibility: Visibility,
kind: u8,
}
struct Lbl {
name: String,
builtin: Option<u8>,
hidden: bool,
macro_name: bool,
itab: u16,
rgce: Vec<u8>,
rgcb: Vec<u8>,
}
#[derive(Default)]
struct Globals {
sheets: Vec<BoundSheet>,
date1904: bool,
formats: HashMap<u16, String>,
xf_formats: Vec<u16>,
sst: Vec<Arc<str>>,
links: Links,
names: Vec<Lbl>,
precision_as_displayed: bool,
}
/// Reads an Excel 97-2003 workbook. Inspect the report's warnings for what was skipped.
pub fn read_xls(bytes: &[u8]) -> Result<(Workbook, ReadReport), IoError> {
read_xls_with(bytes, Budget::default())
}
/// [`read_xls`] with an explicit resource budget.
pub(crate) fn read_xls_with(bytes: &[u8], mut budget: Budget) -> Result<(Workbook, ReadReport), IoError> {
let cf = Compound::open(bytes).map_err(invalid)?;
let stream = match cf.root_stream("Workbook").map_err(invalid)? {
Some(s) => s,
None => {
if cf.has_root_stream("Book") {
return Err(invalid("Excel 5.0/95 workbooks (BIFF5) are not supported; save the file as .xlsx in Excel and try again."));
}
return Err(invalid("the file has no Workbook stream, so it is not an Excel workbook"));
}
};
let mut report = ReadReport::default();
let g = globals(&stream, &mut budget, &mut report)?;
let mut wb = Workbook::new();
wb.sheets.clear();
wb.date_system = if g.date1904 { DateSystem::D1904 } else { DateSystem::D1900 };
// One model style per XF: only the number format is imported.
let styles: Vec<StyleId> = g
.xf_formats
.iter()
.map(|ifmt| {
let code = g.formats.get(ifmt).map(String::as_str).or_else(|| builtin_format(u32::from(*ifmt))).unwrap_or("General");
if code.eq_ignore_ascii_case("General") {
StyleId::DEFAULT
} else {
wb.styles.intern(Style { num_fmt: NumFmt::new(code), ..Style::default() })
}
})
.collect();
// BoundSheet8 index → model sheet index.
let mut model_index: Vec<Option<usize>> = Vec::with_capacity(g.sheets.len());
for bs in &g.sheets {
if bs.kind != 0 {
report.warn(match bs.kind {
2 => "Chart sheets in .xls files are not imported yet and were skipped.",
1 => "Excel 4.0 macro sheets were skipped.",
_ => "VBA modules and other non-worksheet sheets were skipped.",
});
model_index.push(None);
continue;
}
wb.check_sheet_name(&bs.name, None).map_err(invalid)?;
let Some((mut sheet, calc)) = worksheet(&stream, bs, &g, &styles, &mut budget, &mut report)? else {
report.warn("Dialog sheets and unreadable sheets were skipped.");
model_index.push(None);
continue;
};
sheet.visibility = bs.visibility;
// Every worksheet repeats the workbook's calculation settings; Excel applies the first.
if wb.sheets.is_empty() {
wb.calc = calc;
}
model_index.push(Some(wb.sheets.len()));
wb.sheets.push(Arc::new(sheet));
}
if wb.sheets.is_empty() {
return Err(invalid("the workbook has no worksheets GridCraft can read"));
}
wb.calc.precision_as_displayed = g.precision_as_displayed;
defined_names(&g, &model_index, &mut wb, &mut report);
wb.set_active_sheet(wb.sheets.iter().position(|s| s.visibility == Visibility::Visible).unwrap_or(0));
// The overview comes first.
report.warnings.insert(
0,
"Imported from an Excel 97-2003 (.xls) workbook: values, formulas, number formats, merged cells, column widths, row heights and defined names were read. Fonts, fills, borders, charts, pictures, comments and other features are not imported yet. Save as .xlsx; saving .xls is not supported.".into(),
);
Ok((wb, report))
}
fn bof(rec: &Record<'_>) -> Result<(u16, u16), IoError> {
if rec.id != BOF {
return Err(invalid("a substream does not start with a BOF record"));
}
let mut c = Cursor::new(&rec.parts);
Ok((c.u16()?, c.u16()?))
}
fn globals(stream: &[u8], budget: &mut Budget, report: &mut ReadReport) -> Result<Globals, IoError> {
let mut recs = Records::new(stream, 0);
let first = recs.next_record()?.ok_or_else(|| invalid("empty Workbook stream"))?;
let (vers, dt) = bof(&first)?;
if vers != BIFF8 {
return Err(invalid(if vers == 0x0500 {
"Excel 5.0/95 workbooks (BIFF5) are not supported; save the file as .xlsx in Excel and try again."
} else {
"unsupported BIFF version (only Excel 97-2003 BIFF8 files are read)"
}));
}
if dt != 0x0005 {
return Err(invalid("the Workbook stream does not start with the workbook globals"));
}
let mut g = Globals::default();
let mut offsets = std::collections::HashSet::new();
while let Some(rec) = recs.next_record()? {
budget.record()?;
let mut c = Cursor::new(&rec.parts);
match rec.id {
EOF => break,
0x002F => return Err(invalid(ENCRYPTED)),
0x0085 => {
if g.sheets.len() >= MAX_SHEETS {
return Err(IoError::TooLarge("XLS: too many sheets".into()));
}
let pos = c.u32()? as usize;
if !offsets.insert(pos) {
return Err(invalid("two sheets point at the same substream"));
}
let state = c.u8()?;
let kind = c.u8()?;
let name = c.short_string()?;
let visibility = match state & 3 {
0 => Visibility::Visible,
1 => Visibility::Hidden,
_ => Visibility::VeryHidden,
};
g.links.sheets.push(name.clone());
g.sheets.push(BoundSheet { pos, name, visibility, kind });
}
0x0022 => g.date1904 = c.u16()? == 1,
0x000E => g.precision_as_displayed = c.u16()? == 0,
0x041E => {
let ifmt = c.u16()?;
let cch = usize::from(c.u16()?);
let high = c.u8()? & 1 != 0;
// Some writers count one character more than the record holds; Excel opens
// those files, so keep the characters that are there.
let fit = c.remaining() / if high { 2 } else { 1 };
if cch > fit {
report.warn("Some number formats were shorter than their declared length; the characters present were kept.");
}
let code = c.chars(cch.min(fit), high)?;
budget.text(code.len())?;
g.formats.insert(ifmt, code);
}
0x00E0 if g.xf_formats.len() < MAX_XFS => {
c.u16()?;
g.xf_formats.push(c.u16()?);
}
0x00FC => g.sst = shared_strings(&mut c, budget)?,
0x01AE => {
let _ctab = c.u16()?;
let cch = c.u16()?;
g.links.books.push(SupBook { internal: cch == 0x0401, addin: cch == 0x3A01, names: vec![] });
}
0x0023 => {
// ExternName: flags, then an AddinUdf or ExternDocName body; both start with
// four bytes before the name.
c.u16()?;
c.skip(4)?;
let name = c.short_string()?;
if let Some(book) = g.links.books.last_mut()
&& book.names.len() < MAX_NAMES
{
book.names.push(name);
}
}
0x0017 => {
let n = usize::from(c.u16()?);
for _ in 0..n.min(c.remaining() / 6) {
let book = c.u16()?;
let first = c.u16()? as i16;
let last = c.u16()? as i16;
g.links.xti.push(Xti { book, first, last });
}
}
0x0018 => {
if g.names.len() >= MAX_NAMES {
continue;
}
let lbl = label(&mut c)?;
g.links.names.push(lbl.name.clone());
g.names.push(lbl);
}
_ => {}
}
}
// The same test `defined_names` applies, before any cell formula is decoded.
let skipped = g.names.iter().map(|l| !l.rgce.is_empty() && decode(&l.rgce, &l.rgcb, CellRef::new(0, 0), Mode::Name, &g.links).is_err()).collect();
g.links.skipped = skipped;
Ok(g)
}
/// Built-in defined names ([MS-XLS] 2.4.150, Lbl.Name).
const BUILTIN_NAMES: [&str; 14] = [
"Consolidate_Area",
"Auto_Open",
"Auto_Close",
"Extract",
"Database",
"Criteria",
"Print_Area",
"Print_Titles",
"Recorder",
"Data_Form",
"Auto_Activate",
"Auto_Deactivate",
"Sheet_Title",
"_FilterDatabase",
];
fn label(c: &mut Cursor<'_, '_>) -> Result<Lbl, IoError> {
let flags = c.u16()?;
let _key = c.u8()?;
let cch = usize::from(c.u8()?);
let cce = usize::from(c.u16()?);
c.skip(2)?;
let itab = c.u16()?;
c.skip(4)?;
let high = c.u8()? & 1 != 0;
let raw = c.chars(cch, high)?;
let builtin = flags & 0x0020 != 0;
let (name, builtin) = if builtin {
let code = raw.chars().next().map_or(0, |ch| ch as u32);
let code = u8::try_from(code).unwrap_or(u8::MAX);
(BUILTIN_NAMES.get(usize::from(code)).map_or_else(|| raw.clone(), |n| (*n).to_string()), Some(code))
} else {
(raw, None)
};
let rest = c.rest();
let rgce = rest.get(..cce).unwrap_or(&rest).to_vec();
let rgcb = rest.get(cce..).unwrap_or(&[]).to_vec();
Ok(Lbl { name, builtin, hidden: flags & 1 != 0, macro_name: flags & 0x000A != 0, itab, rgce, rgcb })
}
fn shared_strings(c: &mut Cursor<'_, '_>, budget: &mut Budget) -> Result<Vec<Arc<str>>, IoError> {
let _total = c.u32()?;
let unique = c.u32()? as usize;
if unique > MAX_STRINGS {
return Err(IoError::TooLarge("XLS: too many shared strings".into()));
}
// Each string takes at least three bytes: the declared count can't exceed what's there.
let mut out = Vec::with_capacity(unique.min(c.remaining() / 3));
for _ in 0..unique {
if c.remaining() == 0 {
break;
}
let s = c.rich_string()?;
budget.text(s.len())?;
out.push(Arc::from(s));
}
Ok(out)
}
/// A formula cell waiting for its shared or array formula (which follows it in the file).
struct Pending {
pos: CellRef,
anchor: CellRef,
}
/// A parsed formula: tokens and extra data.
struct Parsed {
rgce: Vec<u8>,
rgcb: Vec<u8>,
}
fn parsed(c: &mut Cursor<'_, '_>) -> Result<Parsed, IoError> {
let cce = usize::from(c.u16()?);
let rest = c.rest();
if cce > rest.len() {
return Err(invalid("formula runs past the end of its record"));
}
Ok(Parsed { rgce: rest.get(..cce).unwrap_or(&[]).to_vec(), rgcb: rest.get(cce..).unwrap_or(&[]).to_vec() })
}
struct SheetState<'g> {
g: &'g Globals,
styles: &'g [StyleId],
sheet: Sheet,
pending: Vec<Pending>,
shared: HashMap<CellRef, Parsed>,
arrays: HashMap<CellRef, (RangeRef, Parsed)>,
last_formula: Option<CellRef>,
/// The cell waiting for its String record, with its style: a blank cell isn't stored, so a
/// formula kept as its value may have no cell to receive the text.
pending_string: Option<(CellRef, StyleId)>,
dropped: usize,
unsupported: Vec<&'static str>,
calc: CalcSettings,
}
impl SheetState<'_> {
/// Records why a formula was kept as its value (each reason once).
fn unsupported(&mut self, why: &'static str) {
if !self.unsupported.contains(&why) {
self.unsupported.push(why);
}
}
fn style(&self, ixfe: u16) -> StyleId {
self.styles.get(usize::from(ixfe)).copied().unwrap_or(StyleId::DEFAULT)
}
fn pos(&mut self, row: u16, col: u16) -> Option<CellRef> {
let (r, c) = (u32::from(row), u32::from(col));
if r >= MAX_ROWS || c >= MAX_COLS.min(256) {
self.dropped += 1;
return None;
}
Some(CellRef::new(r, c))
}
fn put(&mut self, budget: &mut Budget, row: u16, col: u16, ixfe: u16, value: Value) -> Result<Option<CellRef>, IoError> {
let Some(pos) = self.pos(row, col) else { return Ok(None) };
let style = self.style(ixfe);
if value.is_empty() && style == StyleId::DEFAULT {
return Ok(Some(pos));
}
budget.cell()?;
self.sheet.cells.set(pos, Cell { value, formula: None, style });
Ok(Some(pos))
}
/// Decodes a formula and attaches it to the cell at `pos`, or records why it can't be.
fn attach(&mut self, pos: CellRef, p: &Parsed, mode: Mode, array: Option<RangeRef>) {
match decode(&p.rgce, &p.rgcb, pos, mode, &self.g.links) {
Ok(e) => {
let text = gridcraft_formula::print(&e);
let mut f = Formula::new(&text);
if f.expr.is_none() {
self.unsupported("syntax GridCraft can't parse yet");
return;
}
f.array = array.filter(|r| !r.is_single());
if let Some(cell) = self.sheet.cells.get_mut(pos) {
cell.formula = Some(Arc::new(f));
} else {
self.sheet.cells.set(pos, Cell { value: Value::Empty, formula: Some(Arc::new(f)), style: StyleId::DEFAULT });
}
}
Err(Unsupported(why)) => self.unsupported(why),
}
}
}
/// RK number ([MS-XLS] 2.5.217): a 30-bit integer or the top of an IEEE double, maybe ×100.
fn rk(v: u32) -> Value {
let n = if v & 2 != 0 { f64::from((v as i32) >> 2) } else { f64::from_bits(u64::from(v & !3) << 32) };
Value::number(if v & 1 != 0 { n / 100.0 } else { n })
}
/// Attaches every pending formula to its anchor's template, returning how many shared
/// templates were expanded. An anchor whose template can't be imported is refused once:
/// its followers re-decode the same template for nothing, so they are skipped instead (they
/// keep their cached values).
fn resolve_pending(
st: &mut SheetState<'_>,
arrays: &HashMap<CellRef, (RangeRef, Parsed)>,
shared: &HashMap<CellRef, Parsed>,
pending: &[Pending],
) -> usize {
let mut failed: HashSet<CellRef> = HashSet::new();
let mut shared_attaches = 0usize;
for p in pending {
if let Some((range, f)) = arrays.get(&p.anchor) {
if p.pos == p.anchor {
st.attach(p.pos, f, Mode::Cell, Some(*range));
}
} else if let Some(f) = shared.get(&p.anchor) {
if failed.contains(&p.anchor) || shared_attaches >= MAX_SHARED_ATTACHES {
continue;
}
shared_attaches += 1;
st.attach(p.pos, f, Mode::Shared, None);
let imported = st.sheet.cell(p.pos).and_then(|c| c.formula.as_ref()).is_some_and(|f| f.text.chars().count() <= MAX_SHARED_TEMPLATE);
if !imported {
failed.insert(p.anchor);
}
} else {
st.unsupported("a shared formula that is missing from the file");
}
}
if shared_attaches >= MAX_SHARED_ATTACHES {
st.unsupported("more shared formulas than the expansion cap; some formulas kept their values");
}
shared_attaches
}
/// The other cells of a legacy array formula hold only cached results: dropped, as when an
/// .xlsx is read, so that the formula fills its range on recalculation. If the formula itself
/// couldn't be imported, they keep their values.
fn blank_array_followers(st: &mut SheetState<'_>, pending: &[Pending]) {
for p in pending {
let filled = st.sheet.cell(p.anchor).and_then(|c| c.formula.as_ref()).and_then(|f| f.array).is_some_and(|r| r.contains(p.pos));
if p.pos != p.anchor && filled {
let style = st.sheet.style_id(p.pos);
st.sheet.cells.set(p.pos, Cell { value: Value::Empty, formula: None, style });
}
}
}
fn worksheet(
stream: &[u8],
bs: &BoundSheet,
g: &Globals,
styles: &[StyleId],
budget: &mut Budget,
report: &mut ReadReport,
) -> Result<Option<(Sheet, CalcSettings)>, IoError> {
if bs.pos >= stream.len() {
return Err(invalid(format!("sheet '{}' points outside the Workbook stream", bs.name)));
}
let mut recs = Records::new(stream, bs.pos);
let first = recs.next_record()?.ok_or_else(|| invalid("missing sheet substream"))?;
let (vers, dt) = bof(&first)?;
if vers != BIFF8 || dt != 0x0010 {
return Ok(None);
}
let mut st = SheetState {
g,
styles,
sheet: Sheet::new(&bs.name),
pending: vec![],
shared: HashMap::new(),
arrays: HashMap::new(),
last_formula: None,
pending_string: None,
dropped: 0,
unsupported: vec![],
calc: CalcSettings::default(),
};
let mut depth = 1usize;
while let Some(rec) = recs.next_record()? {
budget.record()?;
match rec.id {
BOF => {
// An embedded chart's substream.
depth += 1;
continue;
}
EOF => {
depth -= 1;
if depth == 0 {
break;
}
continue;
}
_ if depth > 1 => continue,
WSBOOL if rec.parts.first().and_then(|p| p.first()).is_some_and(|b| b & WSBOOL_DIALOG != 0) => return Ok(None),
_ => {}
}
sheet_record(&rec, &mut st, budget, report)?;
}
// Shared and array formulas: their definitions follow the first cell that uses them.
let (arrays, shared) = (std::mem::take(&mut st.arrays), std::mem::take(&mut st.shared));
let pending = std::mem::take(&mut st.pending);
resolve_pending(&mut st, &arrays, &shared, &pending);
blank_array_followers(&mut st, &pending);
if st.dropped > 0 {
report.warn(format!("{} cells outside the grid were skipped.", st.dropped));
}
for why in &st.unsupported {
report.warn(format!("Some formulas use {why}; their saved results were kept as values."));
}
Ok(Some((st.sheet, st.calc)))
}
/// The shortest WINDOW2 a file holds (the chart-sheet form); a shorter one is truncated.
const WINDOW2_MIN_LEN: usize = 10;
fn sheet_record(rec: &Record<'_>, st: &mut SheetState<'_>, budget: &mut Budget, report: &mut ReadReport) -> Result<(), IoError> {
let mut c = Cursor::new(&rec.parts);
// A formula's string result is the String record right after it (shared/array records may sit between).
if !matches!(rec.id, 0x0207 | 0x04BC | 0x0221 | 0x0236) {
st.pending_string = None;
}
match rec.id {
0x0203 => {
let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
let v = Value::number(c.f64()?);
st.put(budget, r, col, x, v)?;
}
0x027E => {
let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
let v = rk(c.u32()?);
st.put(budget, r, col, x, v)?;
}
0x00BD => {
let r = c.u16()?;
let first = c.u16()?;
let n = c.remaining().saturating_sub(2) / 6;
for i in 0..n {
let x = c.u16()?;
let v = rk(c.u32()?);
let col = u16::try_from(usize::from(first) + i).unwrap_or(u16::MAX);
st.put(budget, r, col, x, v)?;
}
}
0x00FD => {
let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
let i = c.u32()? as usize;
match st.g.sst.get(i) {
Some(s) => {
st.put(budget, r, col, x, Value::Text(s.clone()))?;
}
None => report.warn("Some cells refer to missing shared strings and were left empty."),
}
}
0x0204 => {
let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
let s = c.string()?;
budget.text(s.len())?;
st.put(budget, r, col, x, Value::from(s))?;
}
0x0205 => {
let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
let (v, is_err) = (c.u8()?, c.u8()?);
let v = if is_err != 0 { Value::Error(cell_error(v)) } else { Value::Bool(v != 0) };
st.put(budget, r, col, x, v)?;
}
0x0201 => {
let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
st.put(budget, r, col, x, Value::Empty)?;
}
0x00BE => {
let r = c.u16()?;
let first = c.u16()?;
let n = c.remaining().saturating_sub(2) / 2;
for i in 0..n {
let x = c.u16()?;
let col = u16::try_from(usize::from(first) + i).unwrap_or(u16::MAX);
st.put(budget, r, col, x, Value::Empty)?;
}
}
0x0006 => formula_cell(&mut c, st, budget)?,
0x0207 => {
if let Some((pos, style)) = st.pending_string.take() {
let s = c.string()?;
budget.text(s.len())?;
if let Some(cell) = st.sheet.cells.get_mut(pos) {
cell.value = Value::from(s);
} else {
st.sheet.cells.set(pos, Cell { value: Value::from(s), formula: None, style });
}
}
}
0x04BC => {
// RefU (the cells that may use it), reserved, cUse.
c.skip(8)?;
let f = parsed(&mut c)?;
if let Some(at) = st.last_formula {
st.shared.insert(at, f);
}
}
0x0221 => {
let (r0, r1) = (c.u16()?, c.u16()?);
let (c0, c1) = (c.u8()?, c.u8()?);
c.skip(6)?;
let f = parsed(&mut c)?;
let range = RangeRef::new(CellRef::new(u32::from(r0), u32::from(c0)), CellRef::new(u32::from(r1.max(r0)), u32::from(c1.max(c0))));
if let Some(at) = st.last_formula {
st.arrays.insert(at, (range, f));
}
}
0x000D => {
st.calc.mode = match c.u16()? as i16 {
0 => CalcMode::Manual,
-1 => CalcMode::AutomaticExceptTables,
_ => CalcMode::Automatic,
}
}
0x000C => st.calc.set_max_iterations(u32::from(c.u16()?)),
0x0011 => st.calc.iterative = c.u16()? == 1,
0x0010 => st.calc.set_max_change(c.f64()?),
0x0236 => report.warn("What-if data tables are not supported; their values were kept."),
0x00E5 => {
let n = usize::from(c.u16()?);
for _ in 0..n.min(c.remaining() / 8) {
let (r0, r1, c0, c1) = (c.u16()?, c.u16()?, c.u16()?, c.u16()?);
if r1 < r0 || c1 < c0 || u32::from(c1) >= 256 {
continue;
}
if st.sheet.merges.len() >= MAX_MERGES {
return Err(IoError::TooLarge("XLS: too many merged ranges".into()));
}
let range = RangeRef::new(CellRef::new(u32::from(r0), u32::from(c0)), CellRef::new(u32::from(r1), u32::from(c1)));
if !range.is_single() {
st.sheet.merges.push(range);
}
}
}
0x007D => {
let (first, last) = (c.u16()?, c.u16()?);
let width = c.u16()?;
let x = c.u16()?;
let flags = c.u16()?;
let last = last.min(255);
if first > last {
return Ok(());
}
let mut info = LineInfo::default();
let px = col_width_to_px(f64::from(width) / 256.0);
if flags & 0x0002 != 0 || px != st.sheet.default_col_width {
info.size = Some(px);
}
info.hidden = flags & 0x0001 != 0;
info.outline = ((flags >> 8) & 7) as u8;
info.collapsed = flags & 0x1000 != 0;
let style = st.style(x);
if style != StyleId::DEFAULT {
info.style = Some(style);
}
if info != LineInfo::default() {
for col in first..=last {
st.sheet.cols.insert(u32::from(col), info);
}
}
}
0x0208 => {
let r = c.u16()?;
c.skip(4)?;
let height = c.u16()?;
c.skip(4)?;
let flags = c.u32()?;
let mut info = LineInfo { custom: flags & 0x40 != 0, hidden: flags & 0x20 != 0, ..LineInfo::default() };
let px = pt_to_px(f64::from(height & 0x7FFF) / 20.0);
if info.custom || (px - st.sheet.default_row_height).abs() > 0.5 {
info.size = Some(px);
}
info.outline = (flags & 7) as u8;
info.collapsed = flags & 0x10 != 0;
if flags & 0x80 != 0 {
let style = st.style(((flags >> 16) & 0x0FFF) as u16);
if style != StyleId::DEFAULT {
info.style = Some(style);
}
}
if info != LineInfo::default() && u32::from(r) < MAX_ROWS {
st.sheet.rows.insert(u32::from(r), info);
}
}
0x0225 => {
let flags = c.u16()?;
let height = c.u16()?;
if flags & 0x0002 == 0 && height > 0 {
st.sheet.default_row_height = pt_to_px(f64::from(height) / 20.0);
}
}
0x023E => {
let flags = if c.remaining() >= WINDOW2_MIN_LEN { c.u16().ok() } else { None };
let Some(flags) = flags else { return Ok(()) };
let on = |bit: u16| flags & bit != 0;
st.sheet.show_formulas = on(0x0001);
st.sheet.show_gridlines = on(0x0002);
st.sheet.show_headings = on(0x0004);
st.sheet.show_zeros = on(0x0010);
st.sheet.right_to_left = on(0x0040);
}
0x001C => report.warn("Cell comments in .xls files are not imported yet."),
0x00EC => report.warn("Pictures, charts and shapes in .xls files are not imported yet."),
0x01B0 | 0x0879 => report.warn("Conditional formatting in .xls files is not imported yet."),
0x01B2 => report.warn("Data validation in .xls files is not imported yet."),
0x01B8 => report.warn("Hyperlinks in .xls files are not imported yet."),
0x00B0 => report.warn("PivotTables in .xls files are not imported yet; their values were kept."),
_ => {}
}
Ok(())
}
fn formula_cell(c: &mut Cursor<'_, '_>, st: &mut SheetState<'_>, budget: &mut Budget) -> Result<(), IoError> {
let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
let mut val = [0u8; 8];
for b in val.iter_mut() {
*b = c.u8()?;
}
let _flags = c.u16()?;
c.skip(4)?;
let f = parsed(c)?;
let [b0, _, b2, _, _, _, e0, e1] = val;
let mut string_result = false;
let value = if [e0, e1] == [0xFF, 0xFF] {
match b0 {
0x00 => {
string_result = true;
Value::Empty
}
0x01 => Value::Bool(b2 != 0),
0x02 => Value::Error(cell_error(b2)),
0x03 => Value::from(""),
_ => Value::Empty,
}
} else {
Value::number(f64::from_le_bytes(val))
};
let Some(pos) = st.pos(r, col) else { return Ok(()) };
budget.cell()?;
let style = st.style(x);
st.sheet.cells.set(pos, Cell { value, formula: None, style });
st.last_formula = Some(pos);
if string_result {
st.pending_string = Some((pos, style));
}
// PtgExp: part of a shared or array formula anchored at another cell.
if f.rgce.len() == 5 && f.rgce.first() == Some(&0x01) {
let get = |i: usize| f.rgce.get(i).copied().unwrap_or(0);
let anchor = CellRef::new(u32::from(u16::from_le_bytes([get(1), get(2)])), u32::from(u16::from_le_bytes([get(3), get(4)])));
st.pending.push(Pending { pos, anchor });
return Ok(());
}
st.attach(pos, &f, Mode::Cell, None);
Ok(())
}
fn defined_names(g: &Globals, model_index: &[Option<usize>], wb: &mut Workbook, report: &mut ReadReport) {
let scope_of = |itab: u16| -> Option<Option<usize>> {
if itab == 0 {
return Some(None);
}
model_index.get(usize::from(itab) - 1).copied().flatten().map(Some)
};
for lbl in &g.names {
let Some(scope) = scope_of(lbl.itab) else { continue };
let text = match decode(&lbl.rgce, &lbl.rgcb, CellRef::new(0, 0), Mode::Name, &g.links) {
Ok(e) => gridcraft_formula::print(&e),
Err(_) if lbl.rgce.is_empty() => continue,
Err(Unsupported(why)) => {
report.warn(format!("Some defined names use {why} and were skipped."));
continue;
}
};
match lbl.builtin {
Some(6 | 7) => {
let Some(si) = scope else { continue };
let Some(sheet) = wb.sheet_mut(si) else { continue };
if lbl.builtin == Some(6) {
sheet.print.print_area = crate::read::parse_print_area(&text);
} else {
let (rows, cols) = crate::read::parse_print_titles(&text);
sheet.print.title_rows = rows;
sheet.print.title_cols = cols;
}
}
Some(_) => {}
None if lbl.macro_name => report.warn("Macro names were skipped."),
None => wb.names.push(DefinedName { name: lbl.name.clone(), scope, formula: text, comment: String::new(), hidden: lbl.hidden }),
}
}
}