Skip to main content

ravenrs_sheet/xlsx/
xlsb.rs

1//! Data-only XLSB import, implemented from Microsoft's public MS-XLSB specification:
2//! <https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xlsb/acc8aa92-1f02-4167-99f5-84f9f676b95a>.
3//! Formula caches become constants. Styles, macros and other workbook features are not run or imported.
4
5use std::sync::Arc;
6
7use crate::base::{CellError, CellRef, DateSystem, MAX_COLS, MAX_ROWS, RangeRef, Value};
8use crate::model::{Cell, Sheet, Visibility, Workbook};
9use quick_xml::NsReader;
10use quick_xml::XmlVersion;
11use quick_xml::events::Event;
12use quick_xml::name::ResolveResult;
13
14use crate::xlsx::package::{Package, Rel, normalize, resolve, split_dir};
15use crate::xlsx::{IoError, ReadReport};
16
17const MAIN: &str = "application/vnd.ms-excel.main";
18const BINARY_MAIN: &str = "application/vnd.ms-excel.sheet.binary.macroEnabled.main";
19const WORKSHEET: &str = "application/vnd.ms-excel.worksheet";
20const STRINGS: &str = "application/vnd.ms-excel.sharedStrings";
21const CT_NS: &[u8] = b"http://schemas.openxmlformats.org/package/2006/content-types";
22const REL_NS: &[u8] = b"http://schemas.openxmlformats.org/package/2006/relationships";
23const REL_TYPE: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/";
24const STRICT_REL_TYPE: &str = "http://purl.oclc.org/ooxml/officeDocument/relationships/";
25const MS_REL_TYPE: &str = "http://schemas.microsoft.com/office/2006/relationships/";
26const MAX_META: u64 = 1024 * 1024;
27const MAX_TEXT: usize = 64 * 1024 * 1024;
28const MAX_CELLS: usize = 1_000_000;
29const MAX_STRINGS: usize = 1_000_000;
30const MAX_SHEETS: usize = 1024;
31const MAX_RECORDS: usize = 5_000_000;
32
33fn invalid(msg: impl Into<String>) -> IoError {
34    IoError::Format(format!("XLSB: {}", msg.into()))
35}
36
37#[derive(Default)]
38struct Budget {
39    cells: usize,
40    text: usize,
41}
42impl Budget {
43    fn text(&mut self, len: usize) -> Result<(), IoError> {
44        self.text = self.text.saturating_add(len);
45        if self.text > MAX_TEXT {
46            return Err(IoError::TooLarge("XLSB decoded text exceeds 64 MiB".into()));
47        }
48        Ok(())
49    }
50    fn cell(&mut self) -> Result<(), IoError> {
51        self.cells += 1;
52        if self.cells > MAX_CELLS {
53            return Err(IoError::TooLarge("XLSB exceeds 1 million imported cells".into()));
54        }
55        Ok(())
56    }
57}
58
59/// Imports worksheet scalar values and saved formula results. Number formats are omitted;
60/// dates remain serial numbers with the workbook's original date system. Inspect warnings.
61pub fn read_xlsb(bytes: &[u8]) -> Result<(Workbook, ReadReport), IoError> {
62    let mut package = Package::open(bytes)?;
63    let types = content_types(&mut package)?;
64    let roots = relationships(&mut package, "")?;
65    let roots: Vec<_> = roots.iter().filter(|r| r.kind == "officeDocument").collect();
66    if roots.len() != 1 {
67        return Err(invalid("expected one workbook relationship"));
68    }
69    let root = roots.first().ok_or_else(|| invalid("missing workbook relationship"))?;
70    if root.external {
71        return Err(invalid("external workbook relationship"));
72    }
73    check_type(&types, &root.target, MAIN)?;
74    let book = required(&mut package, &root.target)?;
75    let mut wb = Workbook::new();
76    wb.sheets.clear();
77    let mut report = ReadReport::default();
78    report.warn("XLSB data-only import: formulas become their saved results and will not recalculate. Formatting, charts, images, macros and other features are not imported; dates, currency and percentages may display as numbers. Save a new .xlsx file; XLSB saving is not supported.");
79    let sheets = workbook(&book, &mut wb, &mut report)?;
80    let rels = relationships(&mut package, &root.target)?;
81    let mut budget = Budget::default();
82    let sst_rels: Vec<_> = rels.iter().filter(|r| r.kind == "sharedStrings").collect();
83    if sst_rels.len() > 1 {
84        return Err(invalid("multiple shared-string relationships"));
85    }
86    let strings = if let Some(rel) = sst_rels.first() {
87        if rel.external {
88            return Err(invalid("external shared-string relationship"));
89        }
90        check_type(&types, &rel.target, STRINGS)?;
91        shared_strings(&required(&mut package, &rel.target)?, &mut budget, &mut report)?
92    } else {
93        vec![]
94    };
95    for info in sheets {
96        let Some(id) = info.rel else {
97            if info.visibility != Visibility::VeryHidden {
98                return Err(invalid("worksheet relationship is null"));
99            }
100            report.warn("XLSB module sheets were skipped.");
101            continue;
102        };
103        let rel = rels.iter().find(|r| r.id == id).ok_or_else(|| invalid("worksheet relationship is missing"))?;
104        if rel.external {
105            return Err(invalid("external sheet relationship"));
106        }
107        if rel.kind != "worksheet" {
108            if !matches!(rel.kind.as_str(), "chartsheet" | "dialogsheet" | "xlMacrosheet" | "xlIntlMacrosheet") {
109                return Err(invalid("unknown sheet relationship type"));
110            }
111            report.warn("XLSB non-worksheet sheets were skipped.");
112            continue;
113        }
114        check_type(&types, &rel.target, WORKSHEET)?;
115        wb.check_sheet_name(&info.name, None).map_err(invalid)?;
116        let mut sheet = worksheet(&required(&mut package, &rel.target)?, &info.name, &strings, &mut budget, &mut report)?;
117        sheet.visibility = info.visibility;
118        wb.sheets.push(Arc::new(sheet));
119    }
120    if wb.sheets.is_empty() {
121        return Err(invalid("no supported worksheets"));
122    }
123    wb.set_active_sheet(wb.sheets.iter().position(|s| s.visibility == Visibility::Visible).unwrap_or(0));
124    Ok((wb, report))
125}
126
127pub(crate) fn is_xlsb(bytes: &[u8]) -> bool {
128    let inspect = || -> Result<bool, IoError> {
129        let mut package = Package::open(bytes)?;
130        let types = content_types(&mut package)?;
131        let roots = relationships(&mut package, "")?;
132        let mut books = roots.iter().filter(|r| r.kind == "officeDocument");
133        let Some(root) = books.next() else { return Ok(false) };
134        Ok(books.next().is_none() && !root.external && check_type(&types, &root.target, MAIN).is_ok())
135    };
136    inspect().unwrap_or(false)
137}
138
139fn required(package: &mut Package<'_>, path: &str) -> Result<Vec<u8>, IoError> {
140    package.read(path)?.ok_or_else(|| invalid(format!("missing part {path}")))
141}
142
143struct Entry {
144    name: String,
145    attrs: Vec<(String, String)>,
146}
147impl Entry {
148    fn attr(&self, name: &str) -> Option<&str> {
149        self.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
150    }
151}
152
153/// The two package metadata parts are flat, namespace-qualified XML documents. Keep only
154/// direct child entries, and reject truncated XML instead of guessing a partial relationship.
155fn metadata(bytes: &[u8], ns: &[u8], root_name: &[u8]) -> Result<Vec<Entry>, IoError> {
156    let mut reader = NsReader::from_reader(bytes);
157    let mut depth = 0usize;
158    let mut seen = false;
159    let mut entries = vec![];
160    loop {
161        let event = reader.read_event().map_err(|e| IoError::Xml(e.to_string()))?;
162        match &event {
163            Event::Start(e) | Event::Empty(e) => {
164                let (namespace, local) = reader.resolver_mut().resolve_element(e.name());
165                let valid_ns = matches!(namespace, ResolveResult::Bound(n) if n.as_ref() == ns);
166                if depth == 0 {
167                    if seen || !valid_ns || local.as_ref() != root_name {
168                        return Err(invalid("invalid package metadata root"));
169                    }
170                    seen = true;
171                } else if depth == 1 && valid_ns {
172                    let mut attrs = vec![];
173                    for a in e.attributes() {
174                        let a = a.map_err(|e| IoError::Xml(e.to_string()))?;
175                        if !a.key.as_ref().contains(&b':') {
176                            attrs.push((
177                                String::from_utf8_lossy(a.key.as_ref()).into_owned(),
178                                a.normalized_value(XmlVersion::Explicit1_0).map_err(|e| IoError::Xml(e.to_string()))?.into_owned(),
179                            ));
180                        }
181                    }
182                    entries.push(Entry { name: String::from_utf8_lossy(local.as_ref()).into_owned(), attrs });
183                }
184                if matches!(event, Event::Start(_)) {
185                    depth += 1;
186                    if depth > crate::xlsx::xml::MAX_DEPTH {
187                        return Err(invalid("package metadata is nested too deeply"));
188                    }
189                }
190            }
191            Event::End(_) => depth = depth.checked_sub(1).ok_or_else(|| invalid("unexpected metadata end"))?,
192            Event::Text(t) if depth == 0 && !t.xml_content(XmlVersion::Explicit1_0).map_err(|e| IoError::Xml(e.to_string()))?.trim().is_empty() => {
193                return Err(invalid("text outside package metadata"));
194            }
195            Event::DocType(_) => return Err(invalid("DOCTYPE in package metadata is unsupported")),
196            Event::Eof => break,
197            _ => {}
198        }
199    }
200    if !seen || depth != 0 {
201        return Err(invalid("incomplete package metadata"));
202    }
203    Ok(entries)
204}
205
206fn content_types(package: &mut Package<'_>) -> Result<Vec<Entry>, IoError> {
207    let data = package.read_limited("[Content_Types].xml", MAX_META)?.ok_or_else(|| invalid("missing content types"))?;
208    metadata(&data, CT_NS, b"Types")
209}
210
211fn check_type(types: &[Entry], path: &str, expected: &str) -> Result<(), IoError> {
212    let found = types.iter().find(|e| e.name == "Override" && e.attr("PartName").is_some_and(|p| normalize(p) == normalize(path))).or_else(|| {
213        types.iter().find(|e| {
214            e.name == "Default" && e.attr("Extension").is_some_and(|ext| path.rsplit('.').next().is_some_and(|p| p.eq_ignore_ascii_case(ext)))
215        })
216    });
217    let actual = found.and_then(|e| e.attr("ContentType"));
218    if actual != Some(expected) && !(expected == MAIN && actual == Some(BINARY_MAIN)) {
219        return Err(invalid(format!("wrong or missing content type for {path}")));
220    }
221    Ok(())
222}
223
224fn relationships(package: &mut Package<'_>, path: &str) -> Result<Vec<Rel>, IoError> {
225    let (dir, file) = split_dir(path);
226    let relpath = if dir.is_empty() { format!("_rels/{file}.rels") } else { format!("{dir}/_rels/{file}.rels") };
227    let data = package.read_limited(&relpath, MAX_META)?.ok_or_else(|| invalid(format!("missing relationships for {path}")))?;
228    let mut out: Vec<Rel> = vec![];
229    for e in metadata(&data, REL_NS, b"Relationships")? {
230        if e.name != "Relationship" {
231            continue;
232        }
233        let id = e.attr("Id").filter(|s| !s.is_empty()).ok_or_else(|| invalid("relationship has no ID"))?;
234        if out.iter().any(|r| r.id == id) {
235            return Err(invalid("duplicate relationship ID"));
236        }
237        let ty = e.attr("Type").ok_or_else(|| invalid("relationship has no type"))?;
238        let kind = ty
239            .strip_prefix(REL_TYPE)
240            .or_else(|| ty.strip_prefix(STRICT_REL_TYPE))
241            .or_else(|| ty.strip_prefix(MS_REL_TYPE))
242            .map(str::to_owned)
243            .unwrap_or_else(|| format!("unsupported:{ty}"));
244        let target = e.attr("Target").ok_or_else(|| invalid("relationship has no target"))?;
245        let external = e.attr("TargetMode") == Some("External");
246        out.push(Rel { id: id.into(), kind, target: if external { target.into() } else { resolve(dir, target) }, external });
247    }
248    Ok(out)
249}
250
251struct Bytes<'a> {
252    rest: &'a [u8],
253}
254impl<'a> Bytes<'a> {
255    fn new(bytes: &'a [u8]) -> Self {
256        Self { rest: bytes }
257    }
258    fn take(&mut self, n: usize) -> Result<&'a [u8], IoError> {
259        let out = self.rest.get(..n).ok_or_else(|| invalid("truncated record or string"))?;
260        self.rest = self.rest.get(n..).ok_or_else(|| invalid("truncated record"))?;
261        Ok(out)
262    }
263    fn u8(&mut self) -> Result<u8, IoError> {
264        self.take(1)?.first().copied().ok_or_else(|| invalid("truncated byte"))
265    }
266    fn u16(&mut self) -> Result<u16, IoError> {
267        Ok(u16::from_le_bytes(self.take(2)?.try_into().map_err(|_| invalid("truncated u16"))?))
268    }
269    fn u32(&mut self) -> Result<u32, IoError> {
270        Ok(u32::from_le_bytes(self.take(4)?.try_into().map_err(|_| invalid("truncated u32"))?))
271    }
272    fn f64(&mut self) -> Result<f64, IoError> {
273        let n = f64::from_le_bytes(self.take(8)?.try_into().map_err(|_| invalid("truncated number"))?);
274        if !n.is_finite() {
275            return Err(invalid("nonfinite numeric value"));
276        }
277        Ok(n)
278    }
279    fn string(&mut self, max: u32, nullable: bool) -> Result<Option<String>, IoError> {
280        let count = self.u32()?;
281        if nullable && count == u32::MAX {
282            return Ok(None);
283        }
284        if count > max {
285            return Err(IoError::TooLarge("XLSB string length exceeds its limit".into()));
286        }
287        let size = (count as usize).checked_mul(2).ok_or_else(|| invalid("string length overflow"))?;
288        let bytes = self.take(size)?;
289        let mut text = String::new();
290        for ch in char::decode_utf16(bytes.as_chunks::<2>().0.iter().copied().map(u16::from_le_bytes)) {
291            text.push(ch.map_err(|_| invalid("invalid UTF-16 string"))?);
292        }
293        Ok(Some(text))
294    }
295    fn text(&mut self, max: u32) -> Result<String, IoError> {
296        self.string(max, false)?.ok_or_else(|| invalid("null string"))
297    }
298    fn finish(self) -> Result<(), IoError> {
299        if !self.rest.is_empty() {
300            return Err(invalid("unexpected bytes after record payload"));
301        }
302        Ok(())
303    }
304}
305
306struct Records<'a> {
307    bytes: Bytes<'a>,
308    count: usize,
309    blocks: Vec<u16>,
310}
311impl<'a> Records<'a> {
312    fn new(bytes: &'a [u8]) -> Self {
313        Self { bytes: Bytes::new(bytes), count: 0, blocks: vec![] }
314    }
315    fn next(&mut self, report: &mut ReadReport) -> Result<Option<(u16, &'a [u8])>, IoError> {
316        while !self.bytes.rest.is_empty() {
317            self.count += 1;
318            if self.count > MAX_RECORDS {
319                return Err(IoError::TooLarge("XLSB part contains too many records".into()));
320            }
321            let first = self.bytes.u8()?;
322            let id = if first & 0x80 == 0 {
323                first as u16
324            } else {
325                let id = u16::from(first & 0x7f) | (u16::from(self.bytes.u8()? & 0x7f) << 7);
326                if id < 128 {
327                    return Err(invalid("invalid two-byte record type"));
328                }
329                id
330            };
331            let mut size = 0usize;
332            for shift in [0, 7, 14, 21] {
333                let b = self.bytes.u8()?;
334                size |= usize::from(b & 0x7f) << shift;
335                // The high bit of the fourth length byte is ignored by MS-XLSB.
336                if b & 0x80 == 0 || shift == 21 {
337                    break;
338                }
339            }
340            let payload = self.bytes.take(size)?;
341            match id {
342                35 | 37 => {
343                    let mut b = Bytes::new(payload);
344                    if id == 35 {
345                        b.take(4)?;
346                    } else {
347                        let count = b.u16()?;
348                        if count == 0 {
349                            return Err(invalid("empty alternate-content version list"));
350                        }
351                        b.take(usize::from(count) * 4)?;
352                    }
353                    b.finish()?;
354                    if self.blocks.len() >= 64 {
355                        return Err(invalid("extension blocks nested too deeply"));
356                    }
357                    self.blocks.push(id + 1);
358                    report.warn("Unsupported XLSB future-record and alternate-content blocks were skipped.");
359                }
360                36 | 38 => {
361                    if self.blocks.pop() != Some(id) || !payload.is_empty() {
362                        return Err(invalid("mismatched extension block end"));
363                    }
364                }
365                _ if !self.blocks.is_empty() => {}
366                _ => return Ok(Some((id, payload))),
367            }
368        }
369        if !self.blocks.is_empty() {
370            return Err(invalid("unterminated extension block"));
371        }
372        Ok(None)
373    }
374    fn first(&mut self, id: u16, report: &mut ReadReport) -> Result<(), IoError> {
375        if self.next(report)? != Some((id, &[])) {
376            return Err(invalid("missing part begin record"));
377        }
378        Ok(())
379    }
380    fn end(&mut self, payload: &[u8], report: &mut ReadReport) -> Result<(), IoError> {
381        if !payload.is_empty() || self.next(report)?.is_some() {
382            return Err(invalid("records after part end"));
383        }
384        Ok(())
385    }
386}
387
388struct SheetInfo {
389    name: String,
390    rel: Option<String>,
391    visibility: Visibility,
392}
393fn workbook(bytes: &[u8], wb: &mut Workbook, report: &mut ReadReport) -> Result<Vec<SheetInfo>, IoError> {
394    let mut records = Records::new(bytes);
395    records.first(131, report)?;
396    let mut sheets = vec![];
397    let mut bundle = false;
398    let mut bundle_seen = false;
399    while let Some((id, payload)) = records.next(report)? {
400        let mut b = Bytes::new(payload);
401        match id {
402            132 => {
403                if bundle || !bundle_seen {
404                    return Err(invalid("incomplete worksheet list"));
405                }
406                records.end(payload, report)?;
407                return Ok(sheets);
408            }
409            143 => {
410                if bundle_seen {
411                    return Err(invalid("duplicate worksheet list"));
412                }
413                bundle_seen = true;
414                bundle = true;
415                b.finish()?;
416            }
417            144 => {
418                if !bundle {
419                    return Err(invalid("unexpected worksheet list end"));
420                }
421                bundle = false;
422                b.finish()?;
423            }
424            153 => {
425                wb.date_system = if b.u32()? & 1 != 0 { DateSystem::D1904 } else { DateSystem::D1900 };
426                b.u32()?;
427                b.text(31)?;
428                b.finish()?;
429            }
430            156 => {
431                if !bundle {
432                    return Err(invalid("worksheet outside the worksheet list"));
433                }
434                if sheets.len() >= MAX_SHEETS {
435                    return Err(IoError::TooLarge("XLSB exceeds 1024 sheets".into()));
436                }
437                let visibility = match b.u32()? {
438                    0 => Visibility::Visible,
439                    1 => Visibility::Hidden,
440                    2 => Visibility::VeryHidden,
441                    _ => return Err(invalid("invalid sheet visibility")),
442                };
443                let sheet_id = b.u32()?;
444                if !(1..=65535).contains(&sheet_id) {
445                    return Err(invalid("invalid worksheet ID"));
446                }
447                let rel = b.string(255, true)?;
448                let name = b.text(31)?;
449                b.finish()?;
450                sheets.push(SheetInfo { name, rel, visibility });
451            }
452            _ => {}
453        }
454    }
455    Err(invalid("missing workbook end record"))
456}
457
458fn rich_string(b: &mut Bytes<'_>) -> Result<String, IoError> {
459    let flags = b.u8()?;
460    let text = b.text(32767)?;
461    if flags & 1 != 0 {
462        let count = b.u32()?;
463        if count > 32767 {
464            return Err(invalid("too many rich-text runs"));
465        }
466        b.take(count as usize * 4)?;
467    }
468    if flags & 2 != 0 {
469        b.text(32767)?;
470        let count = b.u32()?;
471        if count > 32767 {
472            return Err(invalid("too many phonetic runs"));
473        }
474        // [MS-XLSB] 2.5.103 PhRun: ichFirst, ichMom, cchMom, ifnt (u16 each)
475        // then phType/alcH/unused1 (16 bits) = 10 bytes. A RichStr is always
476        // the last field of its record (BrtSSTItem, BrtCellRichSt) and
477        // phonetic data is not imported, so tolerate writers that pad runs:
478        // require the documented size, then drop whatever follows.
479        b.take(count as usize * 10)?;
480        b.rest = &[];
481    }
482    Ok(text)
483}
484
485fn shared_strings(bytes: &[u8], budget: &mut Budget, report: &mut ReadReport) -> Result<Vec<Arc<str>>, IoError> {
486    let mut records = Records::new(bytes);
487    let Some((159, payload)) = records.next(report)? else {
488        return Err(invalid("missing shared-string begin record"));
489    };
490    let mut header = Bytes::new(payload);
491    let total = header.u32()?;
492    let unique = header.u32()?;
493    header.finish()?;
494    if unique > total || unique as usize > MAX_STRINGS {
495        return Err(IoError::TooLarge("invalid or oversized shared-string count".into()));
496    }
497    let mut strings = vec![];
498    while let Some((id, payload)) = records.next(report)? {
499        match id {
500            160 => {
501                records.end(payload, report)?;
502                if strings.len() != unique as usize {
503                    return Err(invalid("shared-string count does not match records"));
504                }
505                return Ok(strings);
506            }
507            19 => {
508                if strings.len() >= unique as usize {
509                    return Err(invalid("too many shared-string items"));
510                }
511                let mut b = Bytes::new(payload);
512                let text = rich_string(&mut b)?;
513                b.finish()?;
514                budget.text(text.len())?;
515                strings.push(Arc::from(text));
516            }
517            _ => return Err(invalid("unexpected shared-string record")),
518        }
519    }
520    Err(invalid("missing shared-string end record"))
521}
522
523fn error(code: u8) -> Result<CellError, IoError> {
524    Ok(match code {
525        0x00 => CellError::Null,
526        0x07 => CellError::Div0,
527        0x0f => CellError::Value,
528        0x17 => CellError::Ref,
529        0x1d => CellError::Name,
530        0x24 => CellError::Num,
531        0x2a => CellError::NA,
532        0x2b => CellError::GettingData,
533        _ => return Err(invalid("unknown cell error code")),
534    })
535}
536fn boolean(b: &mut Bytes<'_>) -> Result<bool, IoError> {
537    match b.u8()? {
538        0 => Ok(false),
539        1 => Ok(true),
540        _ => Err(invalid("invalid boolean value")),
541    }
542}
543fn cached_formula(b: &mut Bytes<'_>) -> Result<(), IoError> {
544    b.take(2)?;
545    let cce = b.u32()?;
546    if !(1..=16384).contains(&cce) {
547        return Err(invalid("invalid formula token length"));
548    }
549    b.take(cce as usize)?;
550    let extra = b.u32()?;
551    b.take(extra as usize)?;
552    Ok(())
553}
554fn value(id: u16, b: &mut Bytes<'_>, strings: &[Arc<str>], budget: &mut Budget) -> Result<Value, IoError> {
555    let value = match id {
556        1 => Value::Empty,
557        2 => {
558            let rk = b.u32()?;
559            let n = if rk & 2 != 0 { ((rk as i32) >> 2) as f64 } else { f64::from_bits(u64::from(rk & !3) << 32) };
560            let n = if rk & 1 != 0 { n / 100.0 } else { n };
561            if !n.is_finite() {
562                return Err(invalid("nonfinite RK number"));
563            }
564            Value::Number(n)
565        }
566        3 | 11 => Value::Error(error(b.u8()?)?),
567        4 | 10 => Value::Bool(boolean(b)?),
568        5 | 9 => Value::Number(b.f64()?),
569        6 | 8 | 62 => {
570            let s = if id == 62 { rich_string(b)? } else { b.text(32767)? };
571            budget.text(s.len())?;
572            Value::Text(Arc::from(s))
573        }
574        7 => Value::Text(strings.get(b.u32()? as usize).cloned().ok_or_else(|| invalid("shared-string index is out of bounds"))?),
575        _ => return Err(invalid("unsupported cell record")),
576    };
577    if (8..=11).contains(&id) {
578        cached_formula(b)?;
579    }
580    Ok(value)
581}
582
583fn worksheet(bytes: &[u8], name: &str, strings: &[Arc<str>], budget: &mut Budget, report: &mut ReadReport) -> Result<Sheet, IoError> {
584    let mut records = Records::new(bytes);
585    records.first(129, report)?;
586    let mut sheet = Sheet::new(name);
587    let mut data = false;
588    let mut data_seen = false;
589    let mut row = None;
590    let mut col = None;
591    while let Some((id, payload)) = records.next(report)? {
592        let mut b = Bytes::new(payload);
593        match id {
594            130 => {
595                if data || !data_seen {
596                    return Err(invalid("incomplete worksheet data"));
597                }
598                records.end(payload, report)?;
599                return Ok(sheet);
600            }
601            145 => {
602                if data_seen {
603                    return Err(invalid("duplicate worksheet data"));
604                }
605                data_seen = true;
606                data = true;
607                b.finish()?;
608            }
609            146 => {
610                if !data {
611                    return Err(invalid("unexpected worksheet data end"));
612                }
613                data = false;
614                b.finish()?;
615            }
616            0 => {
617                if !data {
618                    return Err(invalid("row outside worksheet data"));
619                }
620                let r = b.u32()?;
621                if r >= MAX_ROWS || row.is_some_and(|previous| r <= previous) {
622                    return Err(invalid("row is out of bounds or unordered"));
623                }
624                b.take(9)?;
625                let spans = b.u32()?;
626                if spans > 16 {
627                    return Err(invalid("too many row spans"));
628                }
629                b.take(spans as usize * 8)?;
630                b.finish()?;
631                row = Some(r);
632                col = None;
633            }
634            1..=11 | 62 => {
635                if !data {
636                    return Err(invalid("cell outside worksheet data"));
637                }
638                let r = row.ok_or_else(|| invalid("cell before row header"))?;
639                let c = b.u32()?;
640                if c >= MAX_COLS || col.is_some_and(|previous| c <= previous) {
641                    return Err(invalid("column is out of bounds or unordered"));
642                }
643                b.u32()?; // Style index and phonetic flag; formatting is not imported.
644                let value = value(id, &mut b, strings, budget)?;
645                b.finish()?;
646                if !value.is_empty() {
647                    budget.cell()?;
648                    sheet.cells.set(CellRef::new(r, c), Cell::value(value));
649                }
650                col = Some(c);
651            }
652            176 => {
653                if data {
654                    return Err(invalid("merge inside worksheet data"));
655                }
656                let r0 = b.u32()?;
657                let r1 = b.u32()?;
658                let c0 = b.u32()?;
659                let c1 = b.u32()?;
660                b.finish()?;
661                if r0 > r1 || c0 > c1 || r1 >= MAX_ROWS || c1 >= MAX_COLS {
662                    report.warn("Invalid XLSB merged ranges were skipped.");
663                    continue;
664                }
665                if sheet.merges.len() >= 100_000 {
666                    return Err(IoError::TooLarge("XLSB exceeds 100000 merges".into()));
667                }
668                sheet.merges.push(RangeRef::new(CellRef::new(r0, c0), CellRef::new(r1, c1)));
669            }
670            _ => {}
671        }
672    }
673    Err(invalid("missing worksheet end record"))
674}