1mod formula;
15mod ftab;
16mod records;
17mod styles;
18
19use std::collections::{HashMap, HashSet};
20use std::sync::Arc;
21
22use crate::base::{CellRef, DateSystem, MAX_COLS, MAX_ROWS, RangeRef, Value};
23use crate::model::{CalcMode, CalcSettings, Cell, DefinedName, Formula, LineInfo, Sheet, StyleId, Visibility, Workbook};
24
25use crate::xlsx::cfb::Compound;
26use crate::xlsx::tables::{col_width_to_px, pt_to_px};
27use crate::xlsx::{IoError, ReadReport};
28use formula::{Links, Mode, SupBook, Unsupported, Xti, cell_error, decode};
29use records::{Cursor, Record, Records};
30
31const MAX_CELLS: usize = 4_000_000;
32const MAX_STRINGS: usize = 4_000_000;
33const MAX_TEXT: usize = 256 * 1024 * 1024;
34const MAX_SHEETS: usize = 4096;
35const MAX_NAMES: usize = 65_536;
36const MAX_XFS: usize = 65_536;
37const MAX_MERGES: usize = 100_000;
38const MAX_RECORDS: usize = 40_000_000;
42
43const BOF: u16 = 0x0809;
44const EOF: u16 = 0x000A;
45const BIFF8: u16 = 0x0600;
46const WSBOOL: u16 = 0x0081;
48const WSBOOL_DIALOG: u8 = 0x10;
49
50pub(crate) fn invalid(msg: impl Into<String>) -> IoError {
51 IoError::Format(format!("XLS: {}", msg.into()))
52}
53
54const ENCRYPTED: &str =
56 "this .xls workbook is password-protected (encrypted). GridCraft can't open encrypted .xls files; remove the password in Excel and try again.";
57
58pub(crate) fn is_xls(bytes: &[u8]) -> bool {
61 let Ok(cf) = Compound::open(bytes) else { return false };
62 cf.has_root_stream("Workbook") || cf.has_root_stream("Book")
63}
64
65const MAX_SHARED_ATTACHES: usize = 262_144;
71const MAX_SHARED_TEMPLATE: usize = 8192;
72
73pub(crate) struct Budget {
74 cells: usize,
75 text: usize,
76 records: usize,
77 max_records: usize,
78}
79
80impl Default for Budget {
81 fn default() -> Self {
82 Budget { cells: 0, text: 0, records: 0, max_records: MAX_RECORDS }
83 }
84}
85
86impl Budget {
87 #[cfg(test)]
89 pub(crate) fn with_max_records(max_records: usize) -> Self {
90 Budget { max_records, ..Budget::default() }
91 }
92 fn record(&mut self) -> Result<(), IoError> {
93 self.records = self.records.saturating_add(1);
94 if self.records > self.max_records {
95 return Err(IoError::TooLarge("XLS: too many records".into()));
96 }
97 Ok(())
98 }
99 fn cell(&mut self) -> Result<(), IoError> {
100 self.cells += 1;
101 if self.cells > MAX_CELLS {
102 return Err(IoError::TooLarge("XLS: more than 4 million cells".into()));
103 }
104 Ok(())
105 }
106 fn text(&mut self, len: usize) -> Result<(), IoError> {
107 self.text = self.text.saturating_add(len);
108 if self.text > MAX_TEXT {
109 return Err(IoError::TooLarge("XLS: more than 256 MB of text".into()));
110 }
111 Ok(())
112 }
113}
114
115struct BoundSheet {
116 pos: usize,
117 name: String,
118 visibility: Visibility,
119 kind: u8,
120}
121
122struct Lbl {
123 name: String,
124 builtin: Option<u8>,
125 hidden: bool,
126 macro_name: bool,
127 itab: u16,
128 rgce: Vec<u8>,
129 rgcb: Vec<u8>,
130}
131
132#[derive(Default)]
133struct Globals {
134 sheets: Vec<BoundSheet>,
135 date1904: bool,
136 formats: HashMap<u16, String>,
137 styles: styles::StyleRecords,
138 sst: Vec<Arc<str>>,
139 links: Links,
140 names: Vec<Lbl>,
141 precision_as_displayed: bool,
142}
143
144pub fn read_xls(bytes: &[u8]) -> Result<(Workbook, ReadReport), IoError> {
146 read_xls_with(bytes, Budget::default())
147}
148
149pub(crate) fn read_xls_with(bytes: &[u8], mut budget: Budget) -> Result<(Workbook, ReadReport), IoError> {
151 let cf = Compound::open(bytes).map_err(invalid)?;
152 let stream = match cf.root_stream("Workbook").map_err(invalid)? {
153 Some(s) => s,
154 None => {
155 if cf.has_root_stream("Book") {
156 return Err(invalid("Excel 5.0/95 workbooks (BIFF5) are not supported; save the file as .xlsx in Excel and try again."));
157 }
158 return Err(invalid("the file has no Workbook stream, so it is not an Excel workbook"));
159 }
160 };
161 let mut report = ReadReport::default();
162 let g = globals(&stream, &mut budget, &mut report)?;
163
164 let mut wb = Workbook::new();
165 wb.sheets.clear();
166 wb.date_system = if g.date1904 { DateSystem::D1904 } else { DateSystem::D1900 };
167 let styles = styles::build(&g.styles, &g.formats, &mut wb);
169 if g.styles.short {
170 report.warn("Some cell formatting records were shorter than expected; the formatting present was kept.");
171 }
172
173 let mut model_index: Vec<Option<usize>> = Vec::with_capacity(g.sheets.len());
175 for bs in &g.sheets {
176 if bs.kind != 0 {
177 report.warn(match bs.kind {
178 2 => "Chart sheets in .xls files are not imported yet and were skipped.",
179 1 => "Excel 4.0 macro sheets were skipped.",
180 _ => "VBA modules and other non-worksheet sheets were skipped.",
181 });
182 model_index.push(None);
183 continue;
184 }
185 wb.check_sheet_name(&bs.name, None).map_err(invalid)?;
186 let Some((mut sheet, calc)) = worksheet(&stream, bs, &g, &styles, &mut budget, &mut report)? else {
187 report.warn("Dialog sheets and unreadable sheets were skipped.");
188 model_index.push(None);
189 continue;
190 };
191 sheet.visibility = bs.visibility;
192 if wb.sheets.is_empty() {
194 wb.calc = calc;
195 }
196 model_index.push(Some(wb.sheets.len()));
197 wb.sheets.push(Arc::new(sheet));
198 }
199 if wb.sheets.is_empty() {
200 return Err(invalid("the workbook has no worksheets GridCraft can read"));
201 }
202 wb.calc.precision_as_displayed = g.precision_as_displayed;
203 defined_names(&g, &model_index, &mut wb, &mut report);
204 wb.set_active_sheet(wb.sheets.iter().position(|s| s.visibility == Visibility::Visible).unwrap_or(0));
205 report.warnings.insert(
207 0,
208 "Imported from an Excel 97-2003 (.xls) workbook: values, formulas, cell formatting, merged cells, column widths, row heights and defined names were read. Charts, pictures, comments and other features are not imported yet. Save as .xlsx; saving .xls is not supported.".into(),
209 );
210 Ok((wb, report))
211}
212
213fn bof(rec: &Record<'_>) -> Result<(u16, u16), IoError> {
214 if rec.id != BOF {
215 return Err(invalid("a substream does not start with a BOF record"));
216 }
217 let mut c = Cursor::new(&rec.parts);
218 Ok((c.u16()?, c.u16()?))
219}
220
221fn globals(stream: &[u8], budget: &mut Budget, report: &mut ReadReport) -> Result<Globals, IoError> {
222 let mut recs = Records::new(stream, 0);
223 let first = recs.next_record()?.ok_or_else(|| invalid("empty Workbook stream"))?;
224 let (vers, dt) = bof(&first)?;
225 if vers != BIFF8 {
226 return Err(invalid(if vers == 0x0500 {
227 "Excel 5.0/95 workbooks (BIFF5) are not supported; save the file as .xlsx in Excel and try again."
228 } else {
229 "unsupported BIFF version (only Excel 97-2003 BIFF8 files are read)"
230 }));
231 }
232 if dt != 0x0005 {
233 return Err(invalid("the Workbook stream does not start with the workbook globals"));
234 }
235 let mut g = Globals::default();
236 let mut offsets = std::collections::HashSet::new();
237 while let Some(rec) = recs.next_record()? {
238 budget.record()?;
239 let mut c = Cursor::new(&rec.parts);
240 match rec.id {
241 EOF => break,
242 0x002F => return Err(invalid(ENCRYPTED)),
243 0x0085 => {
244 if g.sheets.len() >= MAX_SHEETS {
245 return Err(IoError::TooLarge("XLS: too many sheets".into()));
246 }
247 let pos = c.u32()? as usize;
248 if !offsets.insert(pos) {
249 return Err(invalid("two sheets point at the same substream"));
250 }
251 let state = c.u8()?;
252 let kind = c.u8()?;
253 let name = c.short_string()?;
254 let visibility = match state & 3 {
255 0 => Visibility::Visible,
256 1 => Visibility::Hidden,
257 _ => Visibility::VeryHidden,
258 };
259 g.links.sheets.push(name.clone());
260 g.sheets.push(BoundSheet { pos, name, visibility, kind });
261 }
262 0x0022 => g.date1904 = c.u16()? == 1,
263 0x000E => g.precision_as_displayed = c.u16()? == 0,
264 0x041E => {
265 let ifmt = c.u16()?;
266 let cch = usize::from(c.u16()?);
267 let high = c.u8()? & 1 != 0;
268 let fit = c.remaining() / if high { 2 } else { 1 };
271 if cch > fit {
272 report.warn("Some number formats were shorter than their declared length; the characters present were kept.");
273 }
274 let code = c.chars(cch.min(fit), high)?;
275 budget.text(code.len())?;
276 g.formats.insert(ifmt, code);
277 }
278 0x00E0 => g.styles.xf(&mut c, MAX_XFS),
279 0x0031 => g.styles.font(&mut c),
280 0x0092 => g.styles.palette(&mut c),
281 0x0293 => g.styles.style(&mut c),
282 0x00FC => g.sst = shared_strings(&mut c, budget)?,
283 0x01AE => {
284 let _ctab = c.u16()?;
285 let cch = c.u16()?;
286 g.links.books.push(SupBook { internal: cch == 0x0401, addin: cch == 0x3A01, names: vec![] });
287 }
288 0x0023 => {
289 c.u16()?;
292 c.skip(4)?;
293 let name = c.short_string()?;
294 if let Some(book) = g.links.books.last_mut()
295 && book.names.len() < MAX_NAMES
296 {
297 book.names.push(name);
298 }
299 }
300 0x0017 => {
301 let n = usize::from(c.u16()?);
302 for _ in 0..n.min(c.remaining() / 6) {
303 let book = c.u16()?;
304 let first = c.u16()? as i16;
305 let last = c.u16()? as i16;
306 g.links.xti.push(Xti { book, first, last });
307 }
308 }
309 0x0018 => {
310 if g.names.len() >= MAX_NAMES {
311 continue;
312 }
313 let lbl = label(&mut c)?;
314 g.links.names.push(lbl.name.clone());
315 g.names.push(lbl);
316 }
317 _ => {}
318 }
319 }
320 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();
322 g.links.skipped = skipped;
323 Ok(g)
324}
325
326const BUILTIN_NAMES: [&str; 14] = [
328 "Consolidate_Area",
329 "Auto_Open",
330 "Auto_Close",
331 "Extract",
332 "Database",
333 "Criteria",
334 "Print_Area",
335 "Print_Titles",
336 "Recorder",
337 "Data_Form",
338 "Auto_Activate",
339 "Auto_Deactivate",
340 "Sheet_Title",
341 "_FilterDatabase",
342];
343
344fn label(c: &mut Cursor<'_, '_>) -> Result<Lbl, IoError> {
345 let flags = c.u16()?;
346 let _key = c.u8()?;
347 let cch = usize::from(c.u8()?);
348 let cce = usize::from(c.u16()?);
349 c.skip(2)?;
350 let itab = c.u16()?;
351 c.skip(4)?;
352 let high = c.u8()? & 1 != 0;
353 let raw = c.chars(cch, high)?;
354 let builtin = flags & 0x0020 != 0;
355 let (name, builtin) = if builtin {
356 let code = raw.chars().next().map_or(0, |ch| ch as u32);
357 let code = u8::try_from(code).unwrap_or(u8::MAX);
358 (BUILTIN_NAMES.get(usize::from(code)).map_or_else(|| raw.clone(), |n| (*n).to_string()), Some(code))
359 } else {
360 (raw, None)
361 };
362 let rest = c.rest();
363 let rgce = rest.get(..cce).unwrap_or(&rest).to_vec();
364 let rgcb = rest.get(cce..).unwrap_or(&[]).to_vec();
365 Ok(Lbl { name, builtin, hidden: flags & 1 != 0, macro_name: flags & 0x000A != 0, itab, rgce, rgcb })
366}
367
368fn shared_strings(c: &mut Cursor<'_, '_>, budget: &mut Budget) -> Result<Vec<Arc<str>>, IoError> {
369 let _total = c.u32()?;
370 let unique = c.u32()? as usize;
371 if unique > MAX_STRINGS {
372 return Err(IoError::TooLarge("XLS: too many shared strings".into()));
373 }
374 let mut out = Vec::with_capacity(unique.min(c.remaining() / 3));
376 for _ in 0..unique {
377 if c.remaining() == 0 {
378 break;
379 }
380 let s = c.rich_string()?;
381 budget.text(s.len())?;
382 out.push(Arc::from(s));
383 }
384 Ok(out)
385}
386
387struct Pending {
389 pos: CellRef,
390 anchor: CellRef,
391}
392
393struct Parsed {
395 rgce: Vec<u8>,
396 rgcb: Vec<u8>,
397}
398
399fn parsed(c: &mut Cursor<'_, '_>) -> Result<Parsed, IoError> {
400 let cce = usize::from(c.u16()?);
401 let rest = c.rest();
402 if cce > rest.len() {
403 return Err(invalid("formula runs past the end of its record"));
404 }
405 Ok(Parsed { rgce: rest.get(..cce).unwrap_or(&[]).to_vec(), rgcb: rest.get(cce..).unwrap_or(&[]).to_vec() })
406}
407
408struct SheetState<'g> {
409 g: &'g Globals,
410 styles: &'g [StyleId],
411 sheet: Sheet,
412 pending: Vec<Pending>,
413 shared: HashMap<CellRef, Parsed>,
414 arrays: HashMap<CellRef, (RangeRef, Parsed)>,
415 last_formula: Option<CellRef>,
416 pending_string: Option<(CellRef, StyleId)>,
419 dropped: usize,
420 unsupported: Vec<&'static str>,
421 calc: CalcSettings,
422}
423
424impl SheetState<'_> {
425 fn unsupported(&mut self, why: &'static str) {
427 if !self.unsupported.contains(&why) {
428 self.unsupported.push(why);
429 }
430 }
431
432 fn style(&self, ixfe: u16) -> StyleId {
433 self.styles.get(usize::from(ixfe)).copied().unwrap_or(StyleId::DEFAULT)
434 }
435
436 fn pos(&mut self, row: u16, col: u16) -> Option<CellRef> {
437 let (r, c) = (u32::from(row), u32::from(col));
438 if r >= MAX_ROWS || c >= MAX_COLS.min(256) {
439 self.dropped += 1;
440 return None;
441 }
442 Some(CellRef::new(r, c))
443 }
444
445 fn put(&mut self, budget: &mut Budget, row: u16, col: u16, ixfe: u16, value: Value) -> Result<Option<CellRef>, IoError> {
446 let Some(pos) = self.pos(row, col) else { return Ok(None) };
447 let style = self.style(ixfe);
448 if value.is_empty() && style == StyleId::DEFAULT {
449 return Ok(Some(pos));
450 }
451 budget.cell()?;
452 self.sheet.cells.set(pos, Cell { value, formula: None, style, py: None });
453 Ok(Some(pos))
454 }
455
456 fn attach(&mut self, pos: CellRef, p: &Parsed, mode: Mode, array: Option<RangeRef>) {
458 match decode(&p.rgce, &p.rgcb, pos, mode, &self.g.links) {
459 Ok(e) => {
460 let text = crate::formula::print(&e);
461 let mut f = Formula::new(&text);
462 if f.expr.is_none() {
463 self.unsupported("syntax GridCraft can't parse yet");
464 return;
465 }
466 f.array = array.filter(|r| !r.is_single());
467 if let Some(cell) = self.sheet.cells.get_mut(pos) {
468 cell.formula = Some(Arc::new(f));
469 } else {
470 self.sheet.cells.set(pos, Cell { value: Value::Empty, formula: Some(Arc::new(f)), style: StyleId::DEFAULT, py: None });
471 }
472 }
473 Err(Unsupported(why)) => self.unsupported(why),
474 }
475 }
476}
477
478fn rk(v: u32) -> Value {
480 let n = if v & 2 != 0 { f64::from((v as i32) >> 2) } else { f64::from_bits(u64::from(v & !3) << 32) };
481 Value::number(if v & 1 != 0 { n / 100.0 } else { n })
482}
483
484fn resolve_pending(
489 st: &mut SheetState<'_>,
490 arrays: &HashMap<CellRef, (RangeRef, Parsed)>,
491 shared: &HashMap<CellRef, Parsed>,
492 pending: &[Pending],
493) -> usize {
494 let mut failed: HashSet<CellRef> = HashSet::new();
495 let mut shared_attaches = 0usize;
496 for p in pending {
497 if let Some((range, f)) = arrays.get(&p.anchor) {
498 if p.pos == p.anchor {
499 st.attach(p.pos, f, Mode::Cell, Some(*range));
500 }
501 } else if let Some(f) = shared.get(&p.anchor) {
502 if failed.contains(&p.anchor) || shared_attaches >= MAX_SHARED_ATTACHES {
503 continue;
504 }
505 shared_attaches += 1;
506 st.attach(p.pos, f, Mode::Shared, None);
507 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);
508 if !imported {
509 failed.insert(p.anchor);
510 }
511 } else {
512 st.unsupported("a shared formula that is missing from the file");
513 }
514 }
515 if shared_attaches >= MAX_SHARED_ATTACHES {
516 st.unsupported("more shared formulas than the expansion cap; some formulas kept their values");
517 }
518 shared_attaches
519}
520
521fn blank_array_followers(st: &mut SheetState<'_>, pending: &[Pending]) {
525 for p in pending {
526 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));
527 if p.pos != p.anchor && filled {
528 let style = st.sheet.style_id(p.pos);
529 st.sheet.cells.set(p.pos, Cell { value: Value::Empty, formula: None, style, py: None });
530 }
531 }
532}
533
534fn worksheet(
535 stream: &[u8],
536 bs: &BoundSheet,
537 g: &Globals,
538 styles: &[StyleId],
539 budget: &mut Budget,
540 report: &mut ReadReport,
541) -> Result<Option<(Sheet, CalcSettings)>, IoError> {
542 if bs.pos >= stream.len() {
543 return Err(invalid(format!("sheet '{}' points outside the Workbook stream", bs.name)));
544 }
545 let mut recs = Records::new(stream, bs.pos);
546 let first = recs.next_record()?.ok_or_else(|| invalid("missing sheet substream"))?;
547 let (vers, dt) = bof(&first)?;
548 if vers != BIFF8 || dt != 0x0010 {
549 return Ok(None);
550 }
551 let mut st = SheetState {
552 g,
553 styles,
554 sheet: Sheet::new(&bs.name),
555 pending: vec![],
556 shared: HashMap::new(),
557 arrays: HashMap::new(),
558 last_formula: None,
559 pending_string: None,
560 dropped: 0,
561 unsupported: vec![],
562 calc: CalcSettings::default(),
563 };
564 let mut depth = 1usize;
565 while let Some(rec) = recs.next_record()? {
566 budget.record()?;
567 match rec.id {
568 BOF => {
569 depth += 1;
571 continue;
572 }
573 EOF => {
574 depth -= 1;
575 if depth == 0 {
576 break;
577 }
578 continue;
579 }
580 _ if depth > 1 => continue,
581 WSBOOL if rec.parts.first().and_then(|p| p.first()).is_some_and(|b| b & WSBOOL_DIALOG != 0) => return Ok(None),
582 _ => {}
583 }
584 sheet_record(&rec, &mut st, budget, report)?;
585 }
586 let (arrays, shared) = (std::mem::take(&mut st.arrays), std::mem::take(&mut st.shared));
588 let pending = std::mem::take(&mut st.pending);
589 resolve_pending(&mut st, &arrays, &shared, &pending);
590 blank_array_followers(&mut st, &pending);
591 if st.dropped > 0 {
592 report.warn(format!("{} cells outside the grid were skipped.", st.dropped));
593 }
594 for why in &st.unsupported {
595 report.warn(format!("Some formulas use {why}; their saved results were kept as values."));
596 }
597 Ok(Some((st.sheet, st.calc)))
598}
599
600const WINDOW2_MIN_LEN: usize = 10;
602
603fn sheet_record(rec: &Record<'_>, st: &mut SheetState<'_>, budget: &mut Budget, report: &mut ReadReport) -> Result<(), IoError> {
604 let mut c = Cursor::new(&rec.parts);
605 if !matches!(rec.id, 0x0207 | 0x04BC | 0x0221 | 0x0236) {
607 st.pending_string = None;
608 }
609 match rec.id {
610 0x0203 => {
611 let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
612 let v = Value::number(c.f64()?);
613 st.put(budget, r, col, x, v)?;
614 }
615 0x027E => {
616 let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
617 let v = rk(c.u32()?);
618 st.put(budget, r, col, x, v)?;
619 }
620 0x00BD => {
621 let r = c.u16()?;
622 let first = c.u16()?;
623 let n = c.remaining().saturating_sub(2) / 6;
624 for i in 0..n {
625 let x = c.u16()?;
626 let v = rk(c.u32()?);
627 let col = u16::try_from(usize::from(first) + i).unwrap_or(u16::MAX);
628 st.put(budget, r, col, x, v)?;
629 }
630 }
631 0x00FD => {
632 let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
633 let i = c.u32()? as usize;
634 match st.g.sst.get(i) {
635 Some(s) => {
636 st.put(budget, r, col, x, Value::Text(s.clone()))?;
637 }
638 None => report.warn("Some cells refer to missing shared strings and were left empty."),
639 }
640 }
641 0x0204 => {
642 let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
643 let s = c.string()?;
644 budget.text(s.len())?;
645 st.put(budget, r, col, x, Value::from(s))?;
646 }
647 0x0205 => {
648 let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
649 let (v, is_err) = (c.u8()?, c.u8()?);
650 let v = if is_err != 0 { Value::Error(cell_error(v)) } else { Value::Bool(v != 0) };
651 st.put(budget, r, col, x, v)?;
652 }
653 0x0201 => {
654 let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
655 st.put(budget, r, col, x, Value::Empty)?;
656 }
657 0x00BE => {
658 let r = c.u16()?;
659 let first = c.u16()?;
660 let n = c.remaining().saturating_sub(2) / 2;
661 for i in 0..n {
662 let x = c.u16()?;
663 let col = u16::try_from(usize::from(first) + i).unwrap_or(u16::MAX);
664 st.put(budget, r, col, x, Value::Empty)?;
665 }
666 }
667 0x0006 => formula_cell(&mut c, st, budget)?,
668 0x0207 => {
669 if let Some((pos, style)) = st.pending_string.take() {
670 let s = c.string()?;
671 budget.text(s.len())?;
672 if let Some(cell) = st.sheet.cells.get_mut(pos) {
673 cell.value = Value::from(s);
674 } else {
675 st.sheet.cells.set(pos, Cell { value: Value::from(s), formula: None, style, py: None });
676 }
677 }
678 }
679 0x04BC => {
680 c.skip(8)?;
682 let f = parsed(&mut c)?;
683 if let Some(at) = st.last_formula {
684 st.shared.insert(at, f);
685 }
686 }
687 0x0221 => {
688 let (r0, r1) = (c.u16()?, c.u16()?);
689 let (c0, c1) = (c.u8()?, c.u8()?);
690 c.skip(6)?;
691 let f = parsed(&mut c)?;
692 let range = RangeRef::new(CellRef::new(u32::from(r0), u32::from(c0)), CellRef::new(u32::from(r1.max(r0)), u32::from(c1.max(c0))));
693 if let Some(at) = st.last_formula {
694 st.arrays.insert(at, (range, f));
695 }
696 }
697 0x000D => {
698 st.calc.mode = match c.u16()? as i16 {
699 0 => CalcMode::Manual,
700 -1 => CalcMode::AutomaticExceptTables,
701 _ => CalcMode::Automatic,
702 }
703 }
704 0x000C => st.calc.set_max_iterations(u32::from(c.u16()?)),
705 0x0011 => st.calc.iterative = c.u16()? == 1,
706 0x0010 => st.calc.set_max_change(c.f64()?),
707 0x0236 => report.warn("What-if data tables are not supported; their values were kept."),
708 0x00E5 => {
709 let n = usize::from(c.u16()?);
710 for _ in 0..n.min(c.remaining() / 8) {
711 let (r0, r1, c0, c1) = (c.u16()?, c.u16()?, c.u16()?, c.u16()?);
712 if r1 < r0 || c1 < c0 || u32::from(c1) >= 256 {
713 continue;
714 }
715 if st.sheet.merges.len() >= MAX_MERGES {
716 return Err(IoError::TooLarge("XLS: too many merged ranges".into()));
717 }
718 let range = RangeRef::new(CellRef::new(u32::from(r0), u32::from(c0)), CellRef::new(u32::from(r1), u32::from(c1)));
719 if !range.is_single() {
720 st.sheet.merges.push(range);
721 }
722 }
723 }
724 0x007D => {
725 let (first, last) = (c.u16()?, c.u16()?);
726 let width = c.u16()?;
727 let x = c.u16()?;
728 let flags = c.u16()?;
729 let last = last.min(255);
730 if first > last {
731 return Ok(());
732 }
733 let mut info = LineInfo::default();
734 let px = col_width_to_px(f64::from(width) / 256.0);
735 if flags & 0x0002 != 0 || px != st.sheet.default_col_width {
736 info.size = Some(px);
737 }
738 info.hidden = flags & 0x0001 != 0;
739 info.outline = ((flags >> 8) & 7) as u8;
740 info.collapsed = flags & 0x1000 != 0;
741 let style = st.style(x);
742 if style != StyleId::DEFAULT {
743 info.style = Some(style);
744 }
745 if info != LineInfo::default() {
746 for col in first..=last {
747 st.sheet.cols.insert(u32::from(col), info);
748 }
749 }
750 }
751 0x0208 => {
752 let r = c.u16()?;
753 c.skip(4)?;
754 let height = c.u16()?;
755 c.skip(4)?;
756 let flags = c.u32()?;
757 let mut info = LineInfo { custom: flags & 0x40 != 0, hidden: flags & 0x20 != 0, ..LineInfo::default() };
758 let px = pt_to_px(f64::from(height & 0x7FFF) / 20.0);
759 if info.custom || (px - st.sheet.default_row_height).abs() > 0.5 {
760 info.size = Some(px);
761 }
762 info.outline = (flags & 7) as u8;
763 info.collapsed = flags & 0x10 != 0;
764 if flags & 0x80 != 0 {
765 let style = st.style(((flags >> 16) & 0x0FFF) as u16);
766 if style != StyleId::DEFAULT {
767 info.style = Some(style);
768 }
769 }
770 if info != LineInfo::default() && u32::from(r) < MAX_ROWS {
771 st.sheet.rows.insert(u32::from(r), info);
772 }
773 }
774 0x0225 => {
775 let flags = c.u16()?;
776 let height = c.u16()?;
777 if flags & 0x0002 == 0 && height > 0 {
778 st.sheet.default_row_height = pt_to_px(f64::from(height) / 20.0);
779 }
780 }
781 0x023E => {
782 let flags = if c.remaining() >= WINDOW2_MIN_LEN { c.u16().ok() } else { None };
783 let Some(flags) = flags else { return Ok(()) };
784 let on = |bit: u16| flags & bit != 0;
785 st.sheet.show_formulas = on(0x0001);
786 st.sheet.show_gridlines = on(0x0002);
787 st.sheet.show_headings = on(0x0004);
788 st.sheet.show_zeros = on(0x0010);
789 st.sheet.right_to_left = on(0x0040);
790 }
791 0x001C => report.warn("Cell comments in .xls files are not imported yet."),
792 0x00EC => report.warn("Pictures, charts and shapes in .xls files are not imported yet."),
793 0x01B0 | 0x0879 => report.warn("Conditional formatting in .xls files is not imported yet."),
794 0x01B2 => report.warn("Data validation in .xls files is not imported yet."),
795 0x01B8 => report.warn("Hyperlinks in .xls files are not imported yet."),
796 0x00B0 => report.warn("PivotTables in .xls files are not imported yet; their values were kept."),
797 _ => {}
798 }
799 Ok(())
800}
801
802fn formula_cell(c: &mut Cursor<'_, '_>, st: &mut SheetState<'_>, budget: &mut Budget) -> Result<(), IoError> {
803 let (r, col, x) = (c.u16()?, c.u16()?, c.u16()?);
804 let mut val = [0u8; 8];
805 for b in val.iter_mut() {
806 *b = c.u8()?;
807 }
808 let _flags = c.u16()?;
809 c.skip(4)?;
810 let f = parsed(c)?;
811 let [b0, _, b2, _, _, _, e0, e1] = val;
812 let mut string_result = false;
813 let value = if [e0, e1] == [0xFF, 0xFF] {
814 match b0 {
815 0x00 => {
816 string_result = true;
817 Value::Empty
818 }
819 0x01 => Value::Bool(b2 != 0),
820 0x02 => Value::Error(cell_error(b2)),
821 0x03 => Value::from(""),
822 _ => Value::Empty,
823 }
824 } else {
825 Value::number(f64::from_le_bytes(val))
826 };
827 let Some(pos) = st.pos(r, col) else { return Ok(()) };
828 budget.cell()?;
829 let style = st.style(x);
830 st.sheet.cells.set(pos, Cell { value, formula: None, style, py: None });
831 st.last_formula = Some(pos);
832 if string_result {
833 st.pending_string = Some((pos, style));
834 }
835 if f.rgce.len() == 5 && f.rgce.first() == Some(&0x01) {
837 let get = |i: usize| f.rgce.get(i).copied().unwrap_or(0);
838 let anchor = CellRef::new(u32::from(u16::from_le_bytes([get(1), get(2)])), u32::from(u16::from_le_bytes([get(3), get(4)])));
839 st.pending.push(Pending { pos, anchor });
840 return Ok(());
841 }
842 st.attach(pos, &f, Mode::Cell, None);
843 Ok(())
844}
845
846fn defined_names(g: &Globals, model_index: &[Option<usize>], wb: &mut Workbook, report: &mut ReadReport) {
847 let scope_of = |itab: u16| -> Option<Option<usize>> {
848 if itab == 0 {
849 return Some(None);
850 }
851 model_index.get(usize::from(itab) - 1).copied().flatten().map(Some)
852 };
853 for lbl in &g.names {
854 let Some(scope) = scope_of(lbl.itab) else { continue };
855 let text = match decode(&lbl.rgce, &lbl.rgcb, CellRef::new(0, 0), Mode::Name, &g.links) {
856 Ok(e) => crate::formula::print(&e),
857 Err(_) if lbl.rgce.is_empty() => continue,
858 Err(Unsupported(why)) => {
859 report.warn(format!("Some defined names use {why} and were skipped."));
860 continue;
861 }
862 };
863 match lbl.builtin {
864 Some(6 | 7) => {
865 let Some(si) = scope else { continue };
866 let Some(sheet) = wb.sheet_mut(si) else { continue };
867 if lbl.builtin == Some(6) {
868 sheet.print.print_area = crate::xlsx::read::parse_print_area(&text);
869 } else {
870 let (rows, cols) = crate::xlsx::read::parse_print_titles(&text);
871 sheet.print.title_rows = rows;
872 sheet.print.title_cols = cols;
873 }
874 }
875 Some(_) => {}
876 None if lbl.macro_name => report.warn("Macro names were skipped."),
877 None => wb.names.push(DefinedName { name: lbl.name.clone(), scope, formula: text, comment: String::new(), hidden: lbl.hidden }),
878 }
879 }
880}