Skip to main content

calamine/
xls.rs

1// SPDX-License-Identifier: MIT
2//
3// Copyright 2016-2026, Johann Tuffe.
4
5use std::cmp::min;
6use std::collections::BTreeMap;
7use std::fmt::{self, Write};
8use std::io::{Read, Seek, SeekFrom};
9
10use log::debug;
11
12use crate::cfb::{Cfb, XlsEncoding};
13use crate::formats::{
14    builtin_format_by_code, detect_custom_number_format, format_excel_f64, format_excel_i64,
15    CellFormat,
16};
17#[cfg(feature = "picture")]
18use crate::utils::read_usize;
19use crate::utils::{push_column, read_f64, read_i16, read_i32, read_u16, read_u32};
20use crate::vba::VbaProject;
21use crate::{
22    Cell, CellErrorType, Data, Dimensions, HeaderRow, Metadata, Range, Reader, Sheet, SheetType,
23    SheetVisible,
24};
25
26#[derive(Debug)]
27/// An enum to handle Xls specific errors
28pub enum XlsError {
29    /// Io error
30    Io(std::io::Error),
31    /// Cfb error
32    Cfb(crate::cfb::CfbError),
33    /// Vba error
34    Vba(crate::vba::VbaError),
35
36    /// Cannot parse formula, stack is too short
37    StackLen,
38    /// Unrecognized data
39    Unrecognized {
40        /// data type
41        typ: &'static str,
42        /// value found
43        val: u8,
44    },
45    /// Workbook is password protected
46    Password,
47    /// Invalid length
48    Len {
49        /// expected length
50        expected: usize,
51        /// found length
52        found: usize,
53        /// length type
54        typ: &'static str,
55    },
56    /// Continue Record is too short
57    ContinueRecordTooShort,
58    /// End of stream
59    EoStream(&'static str),
60
61    /// Invalid Formula
62    InvalidFormula {
63        /// stack size
64        stack_size: usize,
65    },
66    /// Invalid or unknown iftab
67    IfTab(usize),
68    /// Invalid etpg
69    Etpg(u8),
70    /// No vba project
71    NoVba,
72
73    /// Invalid OfficeArt Record.
74    #[cfg(feature = "picture")]
75    #[cfg_attr(docsrs, doc(cfg(feature = "picture")))]
76    Art(&'static str),
77
78    /// Worksheet not found
79    WorksheetNotFound(String),
80    /// Invalid iFmt value
81    InvalidFormat {
82        /// iFmt value, See 2.4.126 Format
83        ifmt: u16,
84    },
85}
86
87from_err!(std::io::Error, XlsError, Io);
88from_err!(crate::cfb::CfbError, XlsError, Cfb);
89from_err!(crate::vba::VbaError, XlsError, Vba);
90
91impl std::fmt::Display for XlsError {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        match self {
94            XlsError::Io(e) => write!(f, "I/O error: {e}"),
95            XlsError::Cfb(e) => write!(f, "Cfb error: {e}"),
96            XlsError::Vba(e) => write!(f, "Vba error: {e}"),
97            XlsError::StackLen => write!(f, "Invalid stack length"),
98            XlsError::Unrecognized { typ, val } => write!(f, "Unrecognized {typ}: 0x{val:0X}"),
99            XlsError::Password => write!(f, "Workbook is password protected"),
100            XlsError::Len {
101                expected,
102                found,
103                typ,
104            } => write!(
105                f,
106                "Invalid {typ} length, expected at least {expected}, found {found}",
107            ),
108            XlsError::ContinueRecordTooShort => write!(
109                f,
110                "Continued record too short while reading extended string"
111            ),
112            XlsError::EoStream(s) => write!(f, "End of stream '{s}'"),
113            XlsError::InvalidFormula { stack_size } => {
114                write!(f, "Invalid formula (stack size: {stack_size})")
115            }
116            XlsError::IfTab(iftab) => write!(f, "Invalid iftab {iftab:X}"),
117            XlsError::Etpg(etpg) => write!(f, "Invalid etpg {etpg:X}"),
118            XlsError::NoVba => write!(f, "No VBA project"),
119            #[cfg(feature = "picture")]
120            XlsError::Art(s) => write!(f, "Invalid art record '{s}'"),
121            XlsError::WorksheetNotFound(name) => write!(f, "Worksheet '{name}' not found"),
122            XlsError::InvalidFormat { ifmt } => write!(f, "Invalid ifmt value: '{ifmt}'"),
123        }
124    }
125}
126
127impl std::error::Error for XlsError {
128    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
129        match self {
130            XlsError::Io(e) => Some(e),
131            XlsError::Cfb(e) => Some(e),
132            XlsError::Vba(e) => Some(e),
133            _ => None,
134        }
135    }
136}
137
138/// Options to perform specialized parsing.
139#[derive(Debug, Clone, Default)]
140#[non_exhaustive]
141pub struct XlsOptions {
142    /// Force a spreadsheet to be interpreted using a particular code page.
143    ///
144    /// XLS files can contain [code page] identifiers. If this identifier is missing or incorrect,
145    /// strings in the parsed spreadsheet may be decoded incorrectly. Setting this field causes
146    /// `calamine::Xls` to interpret strings using the specified code page, which may allow such
147    /// spreadsheets to be decoded properly.
148    ///
149    /// [code page]: https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
150    pub force_codepage: Option<u16>,
151    /// Row to use as header
152    pub header_row: HeaderRow,
153}
154
155struct SheetData {
156    range: Range<Data>,
157    formula: Range<String>,
158    merge_cells: Vec<Dimensions>,
159}
160
161/// A struct representing an old xls format file (CFB)
162pub struct Xls<RS> {
163    sheets: BTreeMap<String, SheetData>,
164    metadata: Metadata,
165    cfb: Cfb,
166    reader: RS,
167    options: XlsOptions,
168    formats: Vec<CellFormat>,
169    is_1904: bool,
170    #[cfg(feature = "picture")]
171    pictures: Option<Vec<(String, Vec<u8>)>>,
172}
173
174fn cfb<RS: Seek + Read>(reader: &mut RS) -> Result<Cfb, XlsError> {
175    let offset_end = reader.seek(SeekFrom::End(0))? as usize;
176    reader.seek(SeekFrom::Start(0))?;
177    let cfb = Cfb::new(reader, offset_end)?;
178    Ok(cfb)
179}
180
181impl<RS: Read + Seek> Xls<RS> {
182    /// Creates a new instance using `Options` to inform parsing.
183    ///
184    /// ```
185    /// use calamine::{Xls,XlsOptions};
186    /// # use std::io::Cursor;
187    /// # const BYTES: &[u8] = b"";
188    ///
189    /// # fn run() -> Result<Xls<Cursor<&'static [u8]>>, calamine::XlsError> {
190    /// # let reader = std::io::Cursor::new(BYTES);
191    /// let mut options = XlsOptions::default();
192    /// // ...set options...
193    /// let workbook = Xls::new_with_options(reader, options)?;
194    /// # Ok(workbook) }
195    /// # fn main() { assert!(run().is_err()); }
196    /// ```
197    pub fn new_with_options(mut reader: RS, options: XlsOptions) -> Result<Self, XlsError> {
198        let cfb = cfb(&mut reader)?;
199
200        debug!("cfb loaded");
201
202        let mut xls = Xls {
203            sheets: BTreeMap::new(),
204            cfb,
205            reader,
206            metadata: Metadata::default(),
207            options,
208            is_1904: false,
209            formats: Vec::new(),
210            #[cfg(feature = "picture")]
211            pictures: None,
212        };
213
214        xls.parse_workbook()?;
215
216        debug!("xls parsed");
217
218        Ok(xls)
219    }
220
221    /// Gets the worksheet merge cell dimensions
222    #[doc(hidden)]
223    #[deprecated(
224        since = "0.36.0",
225        note = "use `merge_cells_by_sheet_name()` or `merge_cells_by_sheet_id()` instead"
226    )]
227    pub fn worksheet_merge_cells(&self, name: &str) -> Option<Vec<Dimensions>> {
228        self.sheets.get(name).map(|r| r.merge_cells.clone())
229    }
230
231    /// Get the nth worksheet. Shortcut for getting the nth
232    /// sheet name, then the corresponding worksheet.
233    #[doc(hidden)]
234    #[deprecated(
235        since = "0.36.0",
236        note = "use `merge_cells_by_sheet_name()` or `merge_cells_by_sheet_id()` instead"
237    )]
238    pub fn worksheet_merge_cells_at(&self, n: usize) -> Option<Vec<Dimensions>> {
239        let sheet = self.metadata().sheets.get(n)?;
240
241        #[allow(deprecated)]
242        self.worksheet_merge_cells(&sheet.name)
243    }
244
245    /// Get the merged cells/regions in a worksheet by sheet name.
246    ///
247    /// Merged cells in Excel are a range of cells that have been merged to act
248    /// as a single cell. It is often used to create headers or titles that span
249    /// multiple columns or rows.
250    ///
251    /// The function returns a vector of [`Dimensions`] for the merged cells in
252    /// the named worksheet.
253    ///
254    /// Note: unlike the equivalent [`crate::Xlsx`] method, this method takes
255    /// `&self` because XLS data is fully parsed at construction time and held
256    /// in memory.
257    ///
258    /// # Parameters
259    ///
260    /// - `name`: The name of the worksheet.
261    ///
262    /// # Errors
263    ///
264    /// - [`XlsError::WorksheetNotFound`] if no worksheet with the given name
265    ///   exists.
266    ///
267    /// # Examples
268    ///
269    /// ```
270    /// use calamine::{open_workbook, Error, Reader, Xls};
271    ///
272    /// fn main() -> Result<(), Error> {
273    ///     let path = "tests/merge_cells.xls";
274    ///
275    ///     // Open the workbook.
276    ///     let workbook: Xls<_> = open_workbook(path)?;
277    ///
278    ///     // Get the names of all the sheets in the workbook.
279    ///     let sheet_names = workbook.sheet_names();
280    ///
281    ///     for sheet_name in &sheet_names {
282    ///         println!("{sheet_name}:");
283    ///
284    ///         // Get the merged cells in the current sheet.
285    ///         let merge_cells = workbook.merge_cells_by_sheet_name(sheet_name)?;
286    ///
287    ///         // Print the dimensions of each merged region.
288    ///         for dimension in &merge_cells {
289    ///             println!("    {dimension:?}");
290    ///         }
291    ///     }
292    ///
293    ///     Ok(())
294    /// }
295    /// ```
296    ///
297    /// Output:
298    ///
299    /// ```text
300    /// Dimensions { start: (0, 0), end: (0, 1) }
301    /// Dimensions { start: (1, 0), end: (3, 0) }
302    /// Dimensions { start: (1, 1), end: (3, 3) }
303    /// ```
304    ///
305    pub fn merge_cells_by_sheet_name(&self, name: &str) -> Result<Vec<Dimensions>, XlsError> {
306        self.sheets
307            .get(name)
308            .map(|r| r.merge_cells.clone())
309            .ok_or_else(|| XlsError::WorksheetNotFound(name.into()))
310    }
311
312    /// Get the merged cells/regions in a worksheet by sheet index.
313    ///
314    /// Merged cells in Excel are a range of cells that have been merged to act
315    /// as a single cell. It is often used to create headers or titles that span
316    /// multiple columns or rows.
317    ///
318    /// The function returns a vector of [`Dimensions`] for the merged cells in
319    /// the worksheet at the given index.
320    ///
321    /// Note: unlike the equivalent [`crate::Xlsx`] method, this method takes
322    /// `&self` because XLS data is fully parsed at construction time and held
323    /// in memory.
324    ///
325    /// # Parameters
326    ///
327    /// - `id`: The zero-based index of the worksheet.
328    ///
329    /// # Errors
330    ///
331    /// - [`XlsError::WorksheetNotFound`] if `id` is out of range.
332    ///
333    /// # Examples
334    ///
335    /// ```
336    /// use calamine::{open_workbook, Error, Xls};
337    ///
338    /// fn main() -> Result<(), Error> {
339    ///     let path = "tests/merge_cells.xls";
340    ///
341    ///     // Open the workbook.
342    ///     let workbook: Xls<_> = open_workbook(path)?;
343    ///
344    ///     // Get the merged cells in the first worksheet.
345    ///     let merge_cells = workbook.merge_cells_by_sheet_id(0)?;
346    ///
347    ///     // Print the dimensions of each merged region.
348    ///     for dimension in &merge_cells {
349    ///         println!("{dimension:?}");
350    ///     }
351    ///
352    ///     Ok(())
353    /// }
354    /// ```
355    ///
356    /// Output:
357    ///
358    /// ```text
359    /// Dimensions { start: (0, 0), end: (0, 1) }
360    /// Dimensions { start: (1, 0), end: (3, 0) }
361    /// Dimensions { start: (1, 1), end: (3, 3) }
362    /// ```
363    ///
364    pub fn merge_cells_by_sheet_id(&self, id: usize) -> Result<Vec<Dimensions>, XlsError> {
365        let name = self
366            .metadata()
367            .sheets
368            .get(id)
369            .map(|sheet| sheet.name.clone())
370            .ok_or_else(|| XlsError::WorksheetNotFound(format!("Sheet index {id} out of range")))?;
371
372        self.merge_cells_by_sheet_name(&name)
373    }
374
375    /// Check if the workbook uses the 1904 date system.
376    ///
377    /// Returns `true` if the workbook uses the 1904 date epoch (used by some older
378    /// Mac versions of Excel), or `false` if it uses the standard 1900 date epoch.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use calamine::{open_workbook, Error, Xls};
384    ///
385    /// fn main() -> Result<(), Error> {
386    ///     let workbook: Xls<_> = open_workbook("tests/date_1904.xls")?;
387    ///     let is_1904 = workbook.has_1904_epoch();
388    ///     println!("Uses 1904 date system: {}", is_1904);
389    ///     Ok(())
390    /// }
391    /// ```
392    pub fn has_1904_epoch(&self) -> bool {
393        self.is_1904
394    }
395}
396
397impl<RS: Read + Seek> Reader<RS> for Xls<RS> {
398    type Error = XlsError;
399
400    fn new(reader: RS) -> Result<Self, XlsError> {
401        Self::new_with_options(reader, XlsOptions::default())
402    }
403
404    fn with_header_row(&mut self, header_row: HeaderRow) -> &mut Self {
405        self.options.header_row = header_row;
406        self
407    }
408
409    fn vba_project(&mut self) -> Result<Option<VbaProject>, XlsError> {
410        // Reads vba once for all (better than reading all worksheets once for all)
411        if !self.cfb.has_directory("_VBA_PROJECT_CUR") {
412            return Ok(None);
413        }
414        let vba = VbaProject::from_cfb(&mut self.reader, &mut self.cfb)?;
415        Ok(Some(vba))
416    }
417
418    /// Parses Workbook stream, no need for the relationships variable
419    fn metadata(&self) -> &Metadata {
420        &self.metadata
421    }
422
423    fn worksheet_range(&mut self, name: &str) -> Result<Range<Data>, XlsError> {
424        let sheet = self
425            .sheets
426            .get(name)
427            .map(|r| r.range.clone())
428            .ok_or_else(|| XlsError::WorksheetNotFound(name.into()))?;
429
430        match self.options.header_row {
431            HeaderRow::FirstNonEmptyRow => Ok(sheet),
432            HeaderRow::Row(header_row_idx) => {
433                // If `header_row` is a row index, adjust the range
434                if let (Some(start), Some(end)) = (sheet.start(), sheet.end()) {
435                    Ok(sheet.range((header_row_idx, start.1), end))
436                } else {
437                    Ok(sheet)
438                }
439            }
440        }
441    }
442
443    fn worksheets(&mut self) -> Vec<(String, Range<Data>)> {
444        self.sheets
445            .iter()
446            .map(|(name, sheet)| (name.to_owned(), sheet.range.clone()))
447            .collect()
448    }
449
450    fn worksheet_formula(&mut self, name: &str) -> Result<Range<String>, XlsError> {
451        self.sheets
452            .get(name)
453            .ok_or_else(|| XlsError::WorksheetNotFound(name.into()))
454            .map(|r| r.formula.clone())
455    }
456
457    #[cfg(feature = "picture")]
458    fn pictures(&self) -> Option<Vec<(String, Vec<u8>)>> {
459        self.pictures.to_owned()
460    }
461}
462
463#[derive(Debug, Clone, Copy)]
464struct Xti {
465    _isup_book: u16,
466    itab_first: i16,
467    _itab_last: i16,
468}
469
470impl<RS: Read + Seek> Xls<RS> {
471    fn parse_workbook(&mut self) -> Result<(), XlsError> {
472        use super::cfb::CfbError::StreamNotFound;
473        // gets workbook and worksheets stream, or early exit
474        let stream = match self
475            .cfb
476            .get_stream("Workbook", &mut self.reader)
477            .or_else(|_| self.cfb.get_stream("Book", &mut self.reader))
478            .or_else(|_| self.cfb.get_stream("WORKBOOK", &mut self.reader))
479            .or_else(|_| self.cfb.get_stream("BOOK", &mut self.reader))
480        {
481            Ok(s) => s,
482            Err(StreamNotFound(_)) => return Err(StreamNotFound("Workbook".to_string()).into()),
483            Err(e) => return Err(e.into()),
484        };
485
486        let mut sheet_names = Vec::new();
487        let mut strings = Vec::new();
488        let mut defined_names = Vec::new();
489        let mut xtis = Vec::new();
490        let mut formats = BTreeMap::new();
491        let mut xfs = Vec::new();
492        let mut biff = Biff::Biff8; // Binary Interchange File Format (BIFF) version
493        let codepage = self.options.force_codepage.unwrap_or(1200);
494        let mut encoding = XlsEncoding::from_codepage(codepage)?;
495        #[cfg(feature = "picture")]
496        let mut draw_group: Vec<u8> = Vec::new();
497        {
498            let wb = &stream;
499            let records = RecordIter { stream: wb };
500            for record in records {
501                // Record docs/specs -
502                // https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/6fba0383-0d7a-4c7a-afe9-642ff70cbd36
503                let mut r = record?;
504                match r.typ {
505                    // FilePass (MS-XLS 2.4.117)
506                    0x002F if read_u16(r.data) != 0 => return Err(XlsError::Password),
507                    // CodePage (MS-XLS 2.4.52)
508                    0x0042 if self.options.force_codepage.is_none() => {
509                        encoding = XlsEncoding::from_codepage(read_u16(r.data))?;
510                    }
511                    // RRTabId (MS-XLS 2.4.241)
512                    0x013D => {
513                        let sheet_len = r.data.len() / 2;
514                        sheet_names.reserve(sheet_len);
515                        self.metadata.sheets.reserve(sheet_len);
516                    }
517                    // DateMode (MS-XLS 2.4.77)
518                    0x0022 if read_u16(r.data) == 1 => {
519                        self.is_1904 = true;
520                    }
521                    // Format (MS-XLS 2.4.126)
522                    0x041E => match parse_format(&mut r, &encoding, biff) {
523                        Ok((idx, format)) => {
524                            formats.insert(idx, format);
525                        }
526                        Err(e) => log::warn!("{e}"),
527                    },
528                    // XF (MS-XLS 2.4.353)
529                    0x00E0 => {
530                        xfs.push(parse_xf(&r)?);
531                    }
532                    // BoundSheet8 (MS-XLS 2.4.28)
533                    0x0085 => {
534                        let (pos, sheet) = parse_sheet_metadata(&mut r, &encoding, biff)?;
535                        self.metadata.sheets.push(sheet.clone());
536                        sheet_names.push((pos, sheet.name));
537                    }
538                    // BOF (MS-XLS 2.4.21)
539                    0x0809 => {
540                        let bof = parse_bof(&mut r)?;
541                        biff = bof.biff;
542                    }
543                    // Lbl (MS-XLS 2.4.150)
544                    0x0018 => defined_names.push(parse_lbl(&r, &encoding, biff)?),
545                    // ExternSheet (MS-XLS 2.4.106)
546                    0x0017 => xtis.extend(parse_extern_sheet(&r, biff)),
547                    // SST (MS-XLS 2.4.265)
548                    0x00FC => strings = parse_sst(&mut r, &encoding)?,
549                    #[cfg(feature = "picture")]
550                    // MsoDrawingGroup (MS-XLS 2.4.171)
551                    0x00EB => {
552                        draw_group.extend(r.data);
553                        draw_group.extend(r.cont.iter().flat_map(|v| *v));
554                    }
555                    // EOF (MS-XLS 2.4.103)
556                    0x000A => break,
557                    _ => (),
558                }
559            }
560        }
561
562        // Before BIFF8, formula tokens embed sheet indices directly (itabFirst),
563        // so create identity-mapped XTI entries to reuse the BIFF8 lookup code.
564        if matches!(biff, Biff::Biff2 | Biff::Biff3 | Biff::Biff4 | Biff::Biff5) && xtis.is_empty()
565        {
566            xtis = (0..sheet_names.len())
567                .map(|i| Xti {
568                    _isup_book: 0,
569                    itab_first: i as i16,
570                    _itab_last: i as i16,
571                })
572                .collect();
573        }
574        self.formats = xfs
575            .into_iter()
576            .map(|fmt| match formats.get(&fmt) {
577                Some(s) => *s,
578                _ => builtin_format_by_code(fmt),
579            })
580            .collect();
581
582        debug!("formats: {:?}", self.formats);
583
584        let defined_names = defined_names
585            .into_iter()
586            .map(|(name, (i, mut f))| {
587                if let Some(i) = i {
588                    let sh = xtis
589                        .get(i)
590                        .and_then(|xti| sheet_names.get(xti.itab_first as usize))
591                        .map_or("#REF", |sh| &sh.1);
592                    f = format!("{sh}!{f}");
593                }
594                (name, f)
595            })
596            .collect::<Vec<_>>();
597
598        debug!("defined_names: {defined_names:?}");
599
600        let mut sheets = BTreeMap::new();
601        let fmla_sheet_names = sheet_names
602            .iter()
603            .map(|(_, n)| n.clone())
604            .collect::<Vec<_>>();
605        for (pos, name) in sheet_names {
606            let sh = &stream[pos..];
607            let records = RecordIter { stream: sh };
608            let mut cells = Vec::new();
609            let mut formulas = Vec::new();
610            let mut fmla_pos = (0, 0);
611            let mut merge_cells = Vec::new();
612            // Nested substreams (embedded charts) carry cell-like records; skip them.
613            // The first BOF belongs to the worksheet itself.
614            let mut seen_sheet_bof = false;
615            let mut substream_depth = 0usize;
616            for record in records {
617                let r = record?;
618                if substream_depth > 0 {
619                    match r.typ {
620                        0x0809 => substream_depth += 1,
621                        0x000A => substream_depth -= 1,
622                        _ => (),
623                    }
624                    continue;
625                }
626                match r.typ {
627                    // BOF of a nested substream (embedded chart, macro sheet, ...)
628                    0x0809 => {
629                        if seen_sheet_bof {
630                            substream_depth += 1;
631                        } else {
632                            seen_sheet_bof = true;
633                        }
634                    }
635                    // 512: Dimensions
636                    0x0200 => {
637                        let Dimensions { start, end } = parse_dimensions(r.data)?;
638                        let rows = (end.0 - start.0 + 1) as usize;
639                        let cols = (end.1 - start.1 + 1) as usize;
640                        cells.reserve(rows.saturating_mul(cols));
641                    }
642                    //0x0201 => cells.push(parse_blank(r.data)?), // 513: Blank
643                    0x0203 => cells.push(parse_number(r.data, &self.formats, self.is_1904)?), // 515: Number
644                    0x0204 => cells.push(parse_label(r.data, &encoding, biff)?), // 516: Label [MS-XLS 2.4.148]
645                    0x00D6 => cells.push(parse_label(r.data, &encoding, biff)?), // RString
646                    0x0205 => cells.push(parse_bool_err(r.data)?),               // 517: BoolErr
647                    0x0207 => {
648                        // 519 String (formula value)
649                        let val = Data::String(parse_string(r.data, &encoding, biff)?);
650                        cells.push(Cell::new(fmla_pos, val));
651                    }
652                    0x027E => cells.push(parse_rk(r.data, &self.formats, self.is_1904)?), // 638: Rk
653                    0x00FD => cells.extend(parse_label_sst(r.data, &strings)?), // LabelSst
654                    0x00BD => parse_mul_rk(r.data, &mut cells, &self.formats, self.is_1904)?, // 189: MulRk
655                    0x00E5 => parse_merge_cells(r.data, &mut merge_cells)?, // 229: Merge Cells
656                    0x000A => break,                                        // 10: EOF,
657                    0x0006 => {
658                        // 6: Formula
659                        if r.data.len() < 20 {
660                            return Err(XlsError::Len {
661                                expected: 20,
662                                found: r.data.len(),
663                                typ: "Formula",
664                            });
665                        }
666                        let row = read_u16(r.data);
667                        let col = read_u16(&r.data[2..]);
668                        let format = self.formats.get(read_u16(&r.data[4..]) as usize);
669                        fmla_pos = (row as u32, col as u32);
670                        if let Some(val) =
671                            parse_formula_value(&r.data[6..14], format, self.is_1904)?
672                        {
673                            // If the value is a string
674                            // it will appear in 0x0207 record coming next
675                            cells.push(Cell::new(fmla_pos, val));
676                        }
677                        let fmla = parse_formula(
678                            &r.data[20..],
679                            &fmla_sheet_names,
680                            &defined_names,
681                            &xtis,
682                            &encoding,
683                            biff,
684                        )
685                        .unwrap_or_else(|e| {
686                            debug!("{e}");
687                            format!(
688                                "Unrecognised formula \
689                                 for cell ({row}, {col}): {e:?}"
690                            )
691                        });
692                        formulas.push(Cell::new(fmla_pos, fmla));
693                    }
694                    _ => (),
695                }
696            }
697            let range = Range::from_sparse(cells);
698            let formula = Range::from_sparse(formulas);
699            sheets.insert(
700                name,
701                SheetData {
702                    range,
703                    formula,
704                    merge_cells,
705                },
706            );
707        }
708
709        self.sheets = sheets;
710        self.metadata.names = defined_names;
711
712        #[cfg(feature = "picture")]
713        if !draw_group.is_empty() {
714            let pics = parse_pictures(&draw_group)?;
715            if !pics.is_empty() {
716                self.pictures = Some(pics);
717            }
718        }
719
720        Ok(())
721    }
722}
723
724/// <https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/4d6a3d1e-d7c5-405f-bbae-d01e9cb79366>
725struct Bof {
726    /// Binary Interchange File Format
727    biff: Biff,
728}
729
730/// <https://www.loc.gov/preservation/digital/formats/fdd/fdd000510.shtml#notes>
731#[derive(Clone, Copy, Debug, PartialEq, Eq)]
732enum Biff {
733    Biff2,
734    Biff3,
735    Biff4,
736    Biff5,
737    Biff8,
738    // Used by MS-XLSB Workbook(2.1.7.61) or Worksheet(2.1.7.61) which are not supported yet.
739    // Biff12,
740}
741
742/// BOF [MS-XLS] 2.4.21
743fn parse_bof(r: &mut Record<'_>) -> Result<Bof, XlsError> {
744    let mut dt = 0;
745    let biff_version = read_u16(&r.data[..2]);
746
747    if r.data.len() >= 4 {
748        dt = read_u16(&r.data[2..]);
749    }
750
751    let biff = match biff_version {
752        0x0200 | 0x0002 | 0x0007 => Biff::Biff2,
753        0x0300 => Biff::Biff3,
754        0x0400 => Biff::Biff4,
755        0x0500 => Biff::Biff5,
756        0x0600 => Biff::Biff8,
757        0 => {
758            if dt == 0x1000 {
759                Biff::Biff5
760            } else {
761                Biff::Biff8
762            }
763        }
764        _ => Biff::Biff8,
765    };
766
767    Ok(Bof { biff })
768}
769
770/// `BoundSheet8` [MS-XLS 2.4.28]
771fn parse_sheet_metadata(
772    r: &mut Record<'_>,
773    encoding: &XlsEncoding,
774    biff: Biff,
775) -> Result<(usize, Sheet), XlsError> {
776    let pos = read_u32(r.data) as usize;
777    let visible = match r.data[4] & 0b0011_1111 {
778        0x00 => SheetVisible::Visible,
779        0x01 => SheetVisible::Hidden,
780        0x02 => SheetVisible::VeryHidden,
781        e => {
782            return Err(XlsError::Unrecognized {
783                typ: "BoundSheet8:hsState",
784                val: e,
785            });
786        }
787    };
788    let typ = match r.data[5] {
789        0x00 => SheetType::WorkSheet,
790        0x01 => SheetType::MacroSheet,
791        0x02 => SheetType::ChartSheet,
792        0x06 => SheetType::Vba,
793        e => {
794            return Err(XlsError::Unrecognized {
795                typ: "BoundSheet8:dt",
796                val: e,
797            });
798        }
799    };
800    r.data = &r.data[6..];
801    let mut name = parse_short_string(r, encoding, biff)?;
802    name.retain(|c| c != '\0');
803    Ok((pos, Sheet { name, typ, visible }))
804}
805
806fn parse_lbl(
807    r: &Record<'_>,
808    encoding: &XlsEncoding,
809    biff: Biff,
810) -> Result<(String, (Option<usize>, String)), XlsError> {
811    let cch = r.data[3] as usize;
812    let cce = read_u16(&r.data[4..]) as usize;
813    let mut name = String::with_capacity(cch);
814    match biff {
815        Biff::Biff2 | Biff::Biff3 | Biff::Biff4 | Biff::Biff5 => {
816            // BIFF5 and earlier: plain byte string, no flags byte
817            encoding.decode_to(&r.data[14..], cch, &mut name, None);
818        }
819        Biff::Biff8 => {
820            read_unicode_string_no_cch(encoding, &r.data[14..], cch, &mut name)?;
821        }
822    }
823    let rgce = &r.data[r.data.len() - cce..];
824    let formula = parse_defined_names(rgce, biff)?;
825    Ok((name, formula))
826}
827
828fn parse_extern_sheet(r: &Record<'_>, biff: Biff) -> Vec<Xti> {
829    match biff {
830        Biff::Biff8 => {
831            // Single record with cXTI count + array of 6-byte XTI structs
832            let cxti = read_u16(r.data) as usize;
833            r.data[2..]
834                .as_chunks::<6>()
835                .0
836                .iter()
837                .take(cxti)
838                .map(|xti| Xti {
839                    _isup_book: read_u16(&xti[..2]),
840                    itab_first: read_i16(&xti[2..4]),
841                    _itab_last: read_i16(&xti[4..]),
842                })
843                .collect()
844        }
845        // BIFF5 and earlier: individual sheet name references; formula
846        // tokens embed sheet indices directly, so no XTI table needed.
847        Biff::Biff2 | Biff::Biff3 | Biff::Biff4 | Biff::Biff5 => Vec::new(),
848    }
849}
850
851fn parse_number(r: &[u8], formats: &[CellFormat], is_1904: bool) -> Result<Cell<Data>, XlsError> {
852    if r.len() < 14 {
853        return Err(XlsError::Len {
854            typ: "number",
855            expected: 14,
856            found: r.len(),
857        });
858    }
859    let row = read_u16(r) as u32;
860    let col = read_u16(&r[2..]) as u32;
861    let v = read_f64(&r[6..]);
862    let format = formats.get(read_u16(&r[4..]) as usize);
863
864    Ok(Cell::new((row, col), format_excel_f64(v, format, is_1904)))
865}
866
867fn parse_bool_err(r: &[u8]) -> Result<Cell<Data>, XlsError> {
868    if r.len() < 8 {
869        return Err(XlsError::Len {
870            typ: "BoolErr",
871            expected: 8,
872            found: r.len(),
873        });
874    }
875    let row = read_u16(r);
876    let col = read_u16(&r[2..]);
877    let pos = (row as u32, col as u32);
878    match r[7] {
879        0x00 => Ok(Cell::new(pos, Data::Bool(r[6] != 0))),
880        0x01 => Ok(Cell::new(pos, parse_err(r[6])?)),
881        e => Err(XlsError::Unrecognized {
882            typ: "fError",
883            val: e,
884        }),
885    }
886}
887
888fn parse_err(e: u8) -> Result<Data, XlsError> {
889    match e {
890        0x00 => Ok(Data::Error(CellErrorType::Null)),
891        0x07 => Ok(Data::Error(CellErrorType::Div0)),
892        0x0F => Ok(Data::Error(CellErrorType::Value)),
893        0x17 => Ok(Data::Error(CellErrorType::Ref)),
894        0x1D => Ok(Data::Error(CellErrorType::Name)),
895        0x24 => Ok(Data::Error(CellErrorType::Num)),
896        0x2A => Ok(Data::Error(CellErrorType::NA)),
897        0x2B => Ok(Data::Error(CellErrorType::GettingData)),
898        e => Err(XlsError::Unrecognized {
899            typ: "error",
900            val: e,
901        }),
902    }
903}
904
905fn parse_rk(r: &[u8], formats: &[CellFormat], is_1904: bool) -> Result<Cell<Data>, XlsError> {
906    if r.len() < 10 {
907        return Err(XlsError::Len {
908            typ: "rk",
909            expected: 10,
910            found: r.len(),
911        });
912    }
913    let row = read_u16(r);
914    let col = read_u16(&r[2..]);
915
916    Ok(Cell::new(
917        (row as u32, col as u32),
918        rk_num(&r[4..10], formats, is_1904),
919    ))
920}
921
922fn parse_merge_cells(r: &[u8], merge_cells: &mut Vec<Dimensions>) -> Result<(), XlsError> {
923    let count = read_u16(r);
924
925    for i in 0..count {
926        let offset: usize = (2 + i * 8).into();
927
928        let rf = read_u16(&r[offset..]);
929        let rl = read_u16(&r[offset + 2..]);
930        let cf = read_u16(&r[offset + 4..]);
931        let cl = read_u16(&r[offset + 6..]);
932
933        merge_cells.push(Dimensions {
934            start: (rf.into(), cf.into()),
935            end: (rl.into(), cl.into()),
936        });
937    }
938
939    Ok(())
940}
941
942fn parse_mul_rk(
943    r: &[u8],
944    cells: &mut Vec<Cell<Data>>,
945    formats: &[CellFormat],
946    is_1904: bool,
947) -> Result<(), XlsError> {
948    if r.len() < 6 {
949        return Err(XlsError::Len {
950            typ: "rk",
951            expected: 6,
952            found: r.len(),
953        });
954    }
955
956    let row = read_u16(r);
957    let col_first = read_u16(&r[2..]);
958    let col_last = read_u16(&r[r.len() - 2..]);
959
960    if r.len() != 6 + 6 * (col_last - col_first + 1) as usize {
961        return Err(XlsError::Len {
962            typ: "rk",
963            expected: 6 + 6 * (col_last - col_first + 1) as usize,
964            found: r.len(),
965        });
966    }
967
968    for (col, rk) in (col_first as u32..).zip(r[4..r.len() - 2].chunks(6)) {
969        cells.push(Cell::new((row as u32, col), rk_num(rk, formats, is_1904)));
970    }
971    Ok(())
972}
973
974fn rk_num(rk: &[u8], formats: &[CellFormat], is_1904: bool) -> Data {
975    let d100 = (rk[2] & 1) != 0;
976    let is_int = (rk[2] & 2) != 0;
977    let format = formats.get(read_u16(rk) as usize);
978
979    let mut v = [0u8; 8];
980    v[4..].copy_from_slice(&rk[2..]);
981    v[4] &= 0xFC;
982    if is_int {
983        let v = (read_i32(&v[4..8]) >> 2) as i64;
984        if d100 && v % 100 != 0 {
985            format_excel_f64(v as f64 / 100.0, format, is_1904)
986        } else {
987            format_excel_i64(if d100 { v / 100 } else { v }, format, is_1904)
988        }
989    } else {
990        let v = read_f64(&v);
991        format_excel_f64(if d100 { v / 100.0 } else { v }, format, is_1904)
992    }
993}
994
995/// `ShortXLUnicodeString` [MS-XLS 2.5.240]
996fn parse_short_string(
997    r: &mut Record<'_>,
998    encoding: &XlsEncoding,
999    biff: Biff,
1000) -> Result<String, XlsError> {
1001    let biff8 = matches!(biff, Biff::Biff8);
1002    if r.data.is_empty() || r.data.len() < 2 && biff8 {
1003        return Err(XlsError::Len {
1004            typ: "short string",
1005            expected: if biff8 { 2 } else { 1 },
1006            found: r.data.len(),
1007        });
1008    }
1009
1010    let cch = r.data[0] as usize;
1011    r.data = &r.data[1..];
1012    let mut high_byte = None;
1013
1014    if biff8 {
1015        high_byte = Some(r.data[0] & 0x1 != 0);
1016        r.data = &r.data[1..];
1017    }
1018
1019    let mut s = String::with_capacity(cch);
1020    encoding.decode_to(r.data, cch, &mut s, high_byte);
1021    Ok(s)
1022}
1023
1024/// `XLUnicodeString` [MS-XLS 2.5.294]
1025fn parse_string(r: &[u8], encoding: &XlsEncoding, biff: Biff) -> Result<String, XlsError> {
1026    let (mut high_byte, expected) = match biff {
1027        Biff::Biff2 | Biff::Biff3 | Biff::Biff4 | Biff::Biff5 => (None, 2),
1028        Biff::Biff8 => (Some(false), 3),
1029    };
1030    if r.len() < expected {
1031        if 2 == r.len() && read_u16(r) == 0 {
1032            // tests/OOM_alloc2.xls
1033            return Ok(String::new());
1034        }
1035        return Err(XlsError::Len {
1036            typ: "string",
1037            expected,
1038            found: r.len(),
1039        });
1040    }
1041    // delay populating Some(_) variant until length checks guarantee r[2] can't crash
1042    high_byte = high_byte.map(|_| r[2] & 0x1 != 0);
1043
1044    let cch = read_u16(r) as usize;
1045    let mut s = String::with_capacity(cch);
1046    encoding.decode_to(&r[expected..], cch, &mut s, high_byte);
1047    Ok(s)
1048}
1049
1050fn parse_label(r: &[u8], encoding: &XlsEncoding, biff: Biff) -> Result<Cell<Data>, XlsError> {
1051    if r.len() < 6 {
1052        return Err(XlsError::Len {
1053            typ: "label",
1054            expected: 6,
1055            found: r.len(),
1056        });
1057    }
1058    let row = read_u16(r);
1059    let col = read_u16(&r[2..]);
1060    let _ixfe = read_u16(&r[4..]);
1061    Ok(Cell::new(
1062        (row as u32, col as u32),
1063        Data::String(parse_string(&r[6..], encoding, biff)?),
1064    ))
1065}
1066
1067fn parse_label_sst(r: &[u8], strings: &[String]) -> Result<Option<Cell<Data>>, XlsError> {
1068    if r.len() < 10 {
1069        return Err(XlsError::Len {
1070            typ: "label sst",
1071            expected: 10,
1072            found: r.len(),
1073        });
1074    }
1075    let row = read_u16(r);
1076    let col = read_u16(&r[2..]);
1077    let i = read_u32(&r[6..]) as usize;
1078    if let Some(s) = strings.get(i) {
1079        return Ok(Some(Cell::new(
1080            (row as u32, col as u32),
1081            Data::String(s.clone()),
1082        )));
1083    }
1084    Ok(None)
1085}
1086
1087fn parse_dimensions(r: &[u8]) -> Result<Dimensions, XlsError> {
1088    let (rf, rl, mut cf, cl) = match r.len() {
1089        10 => (
1090            read_u16(&r[0..2]) as u32,
1091            read_u16(&r[2..4]) as u32,
1092            read_u16(&r[4..6]) as u32,
1093            read_u16(&r[6..8]) as u32,
1094        ),
1095        // The BIFF8 record is 14 bytes but some writers add 2 more, which are
1096        // ignored.
1097        14 | 16 => (
1098            read_u32(&r[0..4]),
1099            read_u32(&r[4..8]),
1100            read_u16(&r[8..10]) as u32,
1101            read_u16(&r[10..12]) as u32,
1102        ),
1103        _ => {
1104            return Err(XlsError::Len {
1105                typ: "dimensions",
1106                expected: 14,
1107                found: r.len(),
1108            });
1109        }
1110    };
1111    // 2.5.53 ColU must be <= 0xFF, if larger, reasonable to assume
1112    // starts at 0
1113    // tests/OOM_alloc2.xls
1114    if 0xFF < cf || cl < cf {
1115        cf = 0;
1116    }
1117    if 1 <= rl && 1 <= cl {
1118        Ok(Dimensions {
1119            start: (rf, cf),
1120            end: (rl - 1, cl - 1),
1121        })
1122    } else {
1123        Ok(Dimensions {
1124            start: (rf, cf),
1125            end: (rf, cf),
1126        })
1127    }
1128}
1129
1130// Parse the Excel xls Shared String Table (SST). See [MS-XLS] 2.4.265.
1131//
1132// https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/b6231b92-d32e-4626-badd-c3310a672bab
1133fn parse_sst(r: &mut Record<'_>, encoding: &XlsEncoding) -> Result<Vec<String>, XlsError> {
1134    if r.data.len() < 8 {
1135        return Err(XlsError::Len {
1136            typ: "sst",
1137            expected: 8,
1138            found: r.data.len(),
1139        });
1140    }
1141    let mut sst = vec![];
1142
1143    // Skip cstTotal and cstUnique headers in SST record.
1144    r.data = &r.data[8..];
1145
1146    while !r.data.is_empty() || r.continue_record() {
1147        sst.push(read_rich_extended_string(r, encoding)?);
1148    }
1149
1150    Ok(sst)
1151}
1152
1153/// Decode XF (extract only ifmt - Format identifier)
1154///
1155/// See: <https://learn.microsoft.com/ru-ru/openspecs/office_file_formats/ms-xls/993d15c4-ec04-43e9-ba36-594dfb336c6d>
1156fn parse_xf(r: &Record<'_>) -> Result<u16, XlsError> {
1157    if r.data.len() < 4 {
1158        return Err(XlsError::Len {
1159            typ: "xf",
1160            expected: 4,
1161            found: r.data.len(),
1162        });
1163    }
1164
1165    Ok(read_u16(&r.data[2..]))
1166}
1167
1168/// Decode Format [MS-XLS 2.4.126]
1169///
1170/// See: <https://learn.microsoft.com/ru-ru/openspecs/office_file_formats/ms-xls/300280fd-e4fe-4675-a924-4d383af48d3b>
1171fn parse_format(
1172    r: &mut Record<'_>,
1173    encoding: &XlsEncoding,
1174    biff: Biff,
1175) -> Result<(u16, CellFormat), XlsError> {
1176    if r.data.len() < 2 {
1177        return Err(XlsError::Len {
1178            typ: "format",
1179            expected: 2,
1180            found: r.data.len(),
1181        });
1182    }
1183    let ifmt = read_u16(r.data);
1184    match ifmt {
1185        5..=8 | 23..=26 | 41..=44 | 63..=66 | 164..=382 => (),
1186        _ => return Err(XlsError::InvalidFormat { ifmt }),
1187    }
1188
1189    let s = parse_string(&r.data[2..], encoding, biff)?;
1190    Ok((ifmt, detect_custom_number_format(&s)))
1191}
1192
1193/// Decode `XLUnicodeRichExtendedString` [MS-XLS 2.5.293].
1194///
1195/// See: <https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/173d9f51-e5d3-43da-8de2-be7f22e119b9>
1196fn read_rich_extended_string(
1197    r: &mut Record<'_>,
1198    encoding: &XlsEncoding,
1199) -> Result<String, XlsError> {
1200    if r.data.is_empty() {
1201        // spec violation: at very least cch and flags should be present
1202        return Ok(String::new());
1203    }
1204    if r.data.len() < 3 {
1205        return Err(XlsError::Len {
1206            typ: "rich extended string",
1207            expected: 3,
1208            found: r.data.len(),
1209        });
1210    }
1211
1212    let cch = read_u16(r.data) as usize;
1213    let flags = r.data[2];
1214
1215    r.data = &r.data[3..];
1216
1217    let high_byte = flags & 0x1 != 0;
1218
1219    // how many FormatRun in rgRun data block
1220    let mut c_run = 0;
1221
1222    // how many bytes in ExtRst data block
1223    let mut cb_ext_rst = 0;
1224
1225    // if flag fRichSt exists, read cRun and forward.
1226    if flags & 0x8 != 0 {
1227        c_run = read_u16(r.data) as usize;
1228        r.data = &r.data[2..];
1229    }
1230
1231    // if flag fExtSt exists, read cbExtRst and forward.
1232    if flags & 0x4 != 0 {
1233        cb_ext_rst = read_i32(r.data) as usize;
1234        r.data = &r.data[4..];
1235    }
1236
1237    // read rgb data block for the string we want
1238    let s = read_dbcs(encoding, cch, r, high_byte)?;
1239
1240    // skip rgRun data block. Note: each FormatRun contain 4 bytes.
1241    r.skip(c_run * 4)?;
1242
1243    // skip ExtRst data block.
1244    r.skip(cb_ext_rst)?;
1245
1246    Ok(s)
1247}
1248
1249fn read_dbcs(
1250    encoding: &XlsEncoding,
1251    mut len: usize,
1252    r: &mut Record<'_>,
1253    mut high_byte: bool,
1254) -> Result<String, XlsError> {
1255    let mut s = String::with_capacity(len);
1256    while len > 0 {
1257        let (l, at) = encoding.decode_to(r.data, len, &mut s, Some(high_byte));
1258        r.data = &r.data[at..];
1259        len -= l;
1260        if len > 0 {
1261            if r.continue_record() {
1262                high_byte = r.data[0] & 0x1 != 0;
1263                r.data = &r.data[1..];
1264            } else {
1265                return Err(XlsError::EoStream("dbcs"));
1266            }
1267        }
1268    }
1269    Ok(s)
1270}
1271
1272/// Reads an `XLUnicodeStringNoCch` of `len` characters and
1273/// returns the number of bytes consumed.
1274///
1275/// The character data is 1 byte per character when `fHighByte` is 0, and
1276/// 2 bytes per character (UTF-16LE) when `fHighByte` is 1.
1277fn read_unicode_string_no_cch(
1278    encoding: &XlsEncoding,
1279    buf: &[u8],
1280    len: usize,
1281    s: &mut String,
1282) -> Result<usize, XlsError> {
1283    let Some(&flags) = buf.first() else {
1284        return Err(XlsError::Len {
1285            expected: 1,
1286            found: 0,
1287            typ: "XLUnicodeStringNoCch",
1288        });
1289    };
1290    let high_byte = flags & 0x1 != 0;
1291    let consumed = 1 + if high_byte { 2 * len } else { len };
1292    if buf.len() < consumed {
1293        return Err(XlsError::Len {
1294            expected: consumed,
1295            found: buf.len(),
1296            typ: "XLUnicodeStringNoCch",
1297        });
1298    }
1299    encoding.decode_to(&buf[1..consumed], len, s, Some(high_byte));
1300    Ok(consumed)
1301}
1302
1303struct Record<'a> {
1304    typ: u16,
1305    data: &'a [u8],
1306    cont: Vec<&'a [u8]>,
1307}
1308
1309impl<'a> Record<'a> {
1310    fn continue_record(&mut self) -> bool {
1311        if self.cont.is_empty() {
1312            false
1313        } else {
1314            self.data = self.cont.remove(0);
1315            true
1316        }
1317    }
1318
1319    fn skip(&mut self, mut len: usize) -> Result<(), XlsError> {
1320        while len > 0 {
1321            if self.data.is_empty() && !self.continue_record() {
1322                return Err(XlsError::ContinueRecordTooShort);
1323            }
1324            let l = min(len, self.data.len());
1325            let (_, next) = self.data.split_at(l);
1326            self.data = next;
1327            len -= l;
1328        }
1329        Ok(())
1330    }
1331}
1332
1333// Simple Debug impl to dump record data in hex format.
1334impl fmt::Debug for Record<'_> {
1335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1336        writeln!(
1337            f,
1338            "\nRecord = 0x{:04X}, Length = 0x{:04X}, {}",
1339            self.typ,
1340            self.data.len(),
1341            self.data.len()
1342        )?;
1343
1344        let mut iter = self.data.chunks(16);
1345        for chunk in iter.by_ref() {
1346            for byte in chunk {
1347                write!(f, "{byte:02X} ")?;
1348            }
1349            writeln!(f)?;
1350        }
1351
1352        Ok(())
1353    }
1354}
1355
1356struct RecordIter<'a> {
1357    stream: &'a [u8],
1358}
1359
1360impl<'a> Iterator for RecordIter<'a> {
1361    type Item = Result<Record<'a>, XlsError>;
1362    fn next(&mut self) -> Option<Self::Item> {
1363        if self.stream.len() < 4 {
1364            return if self.stream.is_empty() {
1365                None
1366            } else {
1367                Some(Err(XlsError::EoStream("record type and length")))
1368            };
1369        }
1370        let t = read_u16(self.stream);
1371        let mut len = read_u16(&self.stream[2..]) as usize;
1372        if self.stream.len() < len + 4 {
1373            return Some(Err(XlsError::EoStream("record length")));
1374        }
1375        let (data, next) = self.stream.split_at(len + 4);
1376        self.stream = next;
1377        let d = &data[4..];
1378
1379        // Append next record data if it is a Continue record
1380        let mut cont = Vec::new();
1381        if next.len() > 4 && read_u16(next) == 0x003C {
1382            while self.stream.len() > 4 && read_u16(self.stream) == 0x003C {
1383                len = read_u16(&self.stream[2..]) as usize;
1384                if self.stream.len() < len + 4 {
1385                    return Some(Err(XlsError::EoStream("continue record length")));
1386                }
1387                let sp = self.stream.split_at(len + 4);
1388                cont.push(&sp.0[4..]);
1389                self.stream = sp.1;
1390            }
1391        }
1392
1393        Some(Ok(Record {
1394            typ: t,
1395            data: d,
1396            cont,
1397        }))
1398    }
1399}
1400
1401/// Write an absolute cell reference (eg: `$B$3`) into `f`.
1402fn write_absolute_cell_ref(f: &mut String, col: u32, row: u32) {
1403    f.push('$');
1404    push_column(col, f);
1405    write!(f, "${}", row + 1).unwrap();
1406}
1407
1408/// Formula parsing
1409///
1410/// Does not implement ALL possibilities, only Area are parsed
1411fn parse_defined_names(rgce: &[u8], biff: Biff) -> Result<(Option<usize>, String), XlsError> {
1412    if rgce.is_empty() {
1413        // TODO: do something better here ...
1414        return Ok((None, "empty rgce".to_string()));
1415    }
1416    let ptg = rgce[0];
1417    let res = match ptg {
1418        0x3a | 0x5a | 0x7a => {
1419            // PtgRef3d
1420            let (sheet_idx, row, col) = match biff {
1421                Biff::Biff2 | Biff::Biff3 | Biff::Biff4 | Biff::Biff5 => {
1422                    // ixals(2) + reserved(8) + itabFirst(2) + itabLast(2) + row(2) + col(1)
1423                    (
1424                        read_u16(&rgce[11..13]) as usize,
1425                        read_u16(&rgce[15..17]) as u32 & 0x3FFF,
1426                        rgce[17] as u32,
1427                    )
1428                }
1429                Biff::Biff8 => {
1430                    // ixti(2) + row(2) + col(2)
1431                    (
1432                        read_u16(&rgce[1..3]) as usize,
1433                        read_u16(&rgce[3..5]) as u32,
1434                        read_u16(&rgce[5..7]) as u32,
1435                    )
1436                }
1437            };
1438            // TODO: check with relative columns
1439            let mut f = String::new();
1440            write_absolute_cell_ref(&mut f, col, row);
1441            (Some(sheet_idx), f)
1442        }
1443        0x3b | 0x5b | 0x7b => {
1444            // PtgArea3d
1445            let (sheet_idx, row_first, row_last, col_first, col_last) = match biff {
1446                Biff::Biff2 | Biff::Biff3 | Biff::Biff4 | Biff::Biff5 => {
1447                    // ixals(2) + reserved(8) + itabFirst(2) + itabLast(2) + rowFirst(2) + rowLast(2) + colFirst(1) + colLast(1)
1448                    (
1449                        read_u16(&rgce[11..13]) as usize,
1450                        read_u16(&rgce[15..17]) as u32 & 0x3FFF,
1451                        read_u16(&rgce[17..19]) as u32 & 0x3FFF,
1452                        rgce[19] as u32,
1453                        rgce[20] as u32,
1454                    )
1455                }
1456                Biff::Biff8 => {
1457                    // ixti(2) + rowFirst(2) + rowLast(2) + colFirst(2) + colLast(2)
1458                    (
1459                        read_u16(&rgce[1..3]) as usize,
1460                        read_u16(&rgce[3..5]) as u32,
1461                        read_u16(&rgce[5..7]) as u32,
1462                        read_u16(&rgce[7..9]) as u32,
1463                        read_u16(&rgce[9..11]) as u32,
1464                    )
1465                }
1466            };
1467            // TODO: check with relative columns
1468            let mut f = String::new();
1469            write_absolute_cell_ref(&mut f, col_first, row_first);
1470            f.push(':');
1471            write_absolute_cell_ref(&mut f, col_last, row_last);
1472            (Some(sheet_idx), f)
1473        }
1474        0x3c | 0x5c | 0x7c | 0x3d | 0x5d | 0x7d => {
1475            // PtgAreaErr3d or PtgRefErr3d
1476            let sheet_idx = match biff {
1477                Biff::Biff8 => read_u16(&rgce[1..3]) as usize,
1478                Biff::Biff2 | Biff::Biff3 | Biff::Biff4 | Biff::Biff5 => {
1479                    read_u16(&rgce[11..13]) as usize
1480                }
1481            };
1482            (Some(sheet_idx), "#REF!".to_string())
1483        }
1484        _ => (None, format!("Unsupported ptg: {ptg:x}")),
1485    };
1486    Ok(res)
1487}
1488
1489/// Formula parsing
1490///
1491/// `CellParsedFormula` [MS-XLS 2.5.198.3]
1492fn parse_formula(
1493    mut rgce: &[u8],
1494    sheets: &[String],
1495    names: &[(String, String)],
1496    xtis: &[Xti],
1497    encoding: &XlsEncoding,
1498    biff: Biff,
1499) -> Result<String, XlsError> {
1500    let is_pre_biff8 = matches!(biff, Biff::Biff2 | Biff::Biff3 | Biff::Biff4 | Biff::Biff5);
1501    let mut stack = Vec::new();
1502    let mut formula = String::with_capacity(rgce.len());
1503    let cce = read_u16(rgce) as usize;
1504    rgce = &rgce[2..2 + cce];
1505    while !rgce.is_empty() {
1506        let ptg = rgce[0];
1507        rgce = &rgce[1..];
1508        match ptg {
1509            0x3a | 0x5a | 0x7a => {
1510                // PtgRef3d
1511                let ixti = read_u16(&rgce[0..2]);
1512                let rowu = read_u16(&rgce[2..]);
1513                let colu = read_u16(&rgce[4..]);
1514                let sh = xtis
1515                    .get(ixti as usize)
1516                    .and_then(|xti| sheets.get(xti.itab_first as usize))
1517                    .map_or("#REF", |sh| sh);
1518                stack.push(formula.len());
1519                formula.push_str(sh);
1520                formula.push('!');
1521                let col = colu << 2; // first 14 bits only
1522                if colu & 2 != 0 {
1523                    formula.push('$');
1524                }
1525                push_column(col as u32, &mut formula);
1526                if colu & 1 != 0 {
1527                    formula.push('$');
1528                }
1529                write!(&mut formula, "{}", rowu + 1).unwrap();
1530                rgce = &rgce[6..];
1531            }
1532            0x3b | 0x5b | 0x7b => {
1533                // PtgArea3d
1534                let ixti = read_u16(&rgce[0..2]);
1535                stack.push(formula.len());
1536                formula.push_str(sheets.get(ixti as usize).map_or("#REF", |s| &**s));
1537                formula.push('!');
1538                // TODO: check with relative columns
1539                formula.push('$');
1540                push_column(read_u16(&rgce[6..8]) as u32, &mut formula);
1541                write!(&mut formula, "${}:$", read_u16(&rgce[2..4]) as u32 + 1).unwrap();
1542                push_column(read_u16(&rgce[8..10]) as u32, &mut formula);
1543                write!(&mut formula, "${}", read_u16(&rgce[4..6]) as u32 + 1).unwrap();
1544                rgce = &rgce[10..];
1545            }
1546            0x3c | 0x5c | 0x7c => {
1547                // PtfRefErr3d
1548                let ixti = read_u16(&rgce[0..2]);
1549                stack.push(formula.len());
1550                formula.push_str(sheets.get(ixti as usize).map_or("#REF", |s| &**s));
1551                formula.push('!');
1552                formula.push_str("#REF!");
1553                rgce = &rgce[6..];
1554            }
1555            0x3d | 0x5d | 0x7d => {
1556                // PtgAreaErr3d
1557                let ixti = read_u16(&rgce[0..2]);
1558                stack.push(formula.len());
1559                formula.push_str(sheets.get(ixti as usize).map_or("#REF", |s| &**s));
1560                formula.push('!');
1561                formula.push_str("#REF!");
1562                rgce = &rgce[10..];
1563            }
1564            0x01 => {
1565                // PtgExp: array/shared formula, ignore
1566                debug!("ignoring PtgExp array/shared formula");
1567                stack.push(formula.len());
1568                // Ensure PtgExp carries a 4-byte operand for the row/column
1569                // of the array or shared formula it points at.
1570                if rgce.len() < 4 {
1571                    return Err(XlsError::Len {
1572                        expected: 4,
1573                        found: rgce.len(),
1574                        typ: "PtgExp",
1575                    });
1576                }
1577                rgce = &rgce[4..];
1578            }
1579            0x03..=0x11 => {
1580                // binary operation
1581                let e2 = stack.pop().ok_or(XlsError::StackLen)?;
1582                // imaginary 'e1' will actually already be the start of the binary op
1583                let op = match ptg {
1584                    0x03 => "+",
1585                    0x04 => "-",
1586                    0x05 => "*",
1587                    0x06 => "/",
1588                    0x07 => "^",
1589                    0x08 => "&",
1590                    0x09 => "<",
1591                    0x0A => "<=",
1592                    0x0B => "=",
1593                    0x0C => ">",
1594                    0x0D => ">=",
1595                    0x0E => "<>",
1596                    0x0F => " ",
1597                    0x10 => ",",
1598                    0x11 => ":",
1599                    _ => unreachable!(),
1600                };
1601                let e2 = formula.split_off(e2);
1602                write!(&mut formula, "{op}{e2}").unwrap();
1603            }
1604            0x12 => {
1605                let e = stack.last().ok_or(XlsError::StackLen)?;
1606                formula.insert(*e, '+');
1607            }
1608            0x13 => {
1609                let e = stack.last().ok_or(XlsError::StackLen)?;
1610                formula.insert(*e, '-');
1611            }
1612            0x14 => {
1613                formula.push('%');
1614            }
1615            0x15 => {
1616                let e = stack.last().ok_or(XlsError::StackLen)?;
1617                formula.insert(*e, '(');
1618                formula.push(')');
1619            }
1620            0x16 => {
1621                stack.push(formula.len());
1622            }
1623            0x17 => {
1624                stack.push(formula.len());
1625                formula.push('\"');
1626                // PtgStr: cch (1 byte) followed by an XLUnicodeStringNoCch
1627                let Some(&cch) = rgce.first() else {
1628                    return Err(XlsError::Len {
1629                        expected: 1,
1630                        found: 0,
1631                        typ: "PtgStr",
1632                    });
1633                };
1634                let consumed =
1635                    read_unicode_string_no_cch(encoding, &rgce[1..], cch as usize, &mut formula)?;
1636                formula.push('\"');
1637                rgce = &rgce[1 + consumed..];
1638            }
1639            0x18 => {
1640                rgce = &rgce[5..];
1641            }
1642            0x19 => {
1643                let etpg = rgce[0];
1644                rgce = &rgce[1..];
1645                match etpg {
1646                    0x01 | 0x02 | 0x08 | 0x20 | 0x21 => rgce = &rgce[2..],
1647                    0x04 => {
1648                        // PtgAttrChoose
1649                        let n = read_u16(&rgce[..2]) as usize + 1;
1650                        rgce = &rgce[2 + 2 * n..]; // ignore
1651                    }
1652                    0x10 => {
1653                        rgce = &rgce[2..];
1654                        let e = *stack.last().ok_or(XlsError::StackLen)?;
1655                        let e = formula.split_off(e);
1656                        write!(&mut formula, "SUM({e})").unwrap();
1657                    }
1658                    0x40 | 0x41 => {
1659                        // PtfAttrSpace
1660                        let e = *stack.last().ok_or(XlsError::StackLen)?;
1661                        let space = match rgce[0] {
1662                            0x00 | 0x02 | 0x04 | 0x06 => ' ',
1663                            0x01 | 0x03 | 0x05 => '\r',
1664                            val => {
1665                                return Err(XlsError::Unrecognized {
1666                                    typ: "PtgAttrSpaceType",
1667                                    val,
1668                                });
1669                            }
1670                        };
1671                        let cch = rgce[1];
1672                        for _ in 0..cch {
1673                            formula.insert(e, space);
1674                        }
1675                        rgce = &rgce[2..];
1676                    }
1677                    e => return Err(XlsError::Etpg(e)),
1678                }
1679            }
1680            0x1C => {
1681                stack.push(formula.len());
1682                let err = rgce[0];
1683                rgce = &rgce[1..];
1684                match err {
1685                    0x00 => formula.push_str("#NULL!"),
1686                    0x07 => formula.push_str("#DIV/0!"),
1687                    0x0F => formula.push_str("#VALUE!"),
1688                    0x17 => formula.push_str("#REF!"),
1689                    0x1D => formula.push_str("#NAME?"),
1690                    0x24 => formula.push_str("#NUM!"),
1691                    0x2A => formula.push_str("#N/A"),
1692                    0x2B => formula.push_str("#GETTING_DATA"),
1693                    e => {
1694                        return Err(XlsError::Unrecognized {
1695                            typ: "BErr",
1696                            val: e,
1697                        });
1698                    }
1699                }
1700            }
1701            0x1D => {
1702                stack.push(formula.len());
1703                formula.push_str(if rgce[0] == 0 { "FALSE" } else { "TRUE" });
1704                rgce = &rgce[1..];
1705            }
1706            0x1E => {
1707                stack.push(formula.len());
1708                write!(&mut formula, "{}", read_u16(rgce)).unwrap();
1709                rgce = &rgce[2..];
1710            }
1711            0x1F => {
1712                stack.push(formula.len());
1713                write!(&mut formula, "{}", read_f64(rgce)).unwrap();
1714                rgce = &rgce[8..];
1715            }
1716            0x20 | 0x40 | 0x60 => {
1717                // PtgArray: ignore
1718                stack.push(formula.len());
1719                formula.push_str("{PtgArray}");
1720                rgce = &rgce[7..];
1721            }
1722            0x21 | 0x22 | 0x41 | 0x42 | 0x61 | 0x62 => {
1723                let (iftab, argc) = match ptg {
1724                    0x22 | 0x42 | 0x62 => {
1725                        let iftab = read_u16(&rgce[1..]) as usize;
1726                        let argc = rgce[0] as usize;
1727                        rgce = &rgce[3..];
1728                        (iftab, argc)
1729                    }
1730                    _ => {
1731                        let iftab = read_u16(rgce) as usize;
1732                        if iftab >= crate::utils::FTAB_LEN {
1733                            return Err(XlsError::IfTab(iftab));
1734                        }
1735                        rgce = &rgce[2..];
1736                        let argc = crate::utils::FTAB_ARGC[iftab] as usize;
1737                        (iftab, argc)
1738                    }
1739                };
1740                if stack.len() < argc {
1741                    return Err(XlsError::StackLen);
1742                }
1743                if argc > 0 {
1744                    let args_start = stack.len() - argc;
1745                    let mut args = stack.split_off(args_start);
1746                    let start = args[0];
1747                    for s in &mut args {
1748                        *s -= start;
1749                    }
1750                    let fargs = formula.split_off(start);
1751                    stack.push(formula.len());
1752                    args.push(fargs.len());
1753                    formula.push_str(
1754                        crate::utils::FTAB
1755                            .get(iftab)
1756                            .ok_or(XlsError::IfTab(iftab))?,
1757                    );
1758                    formula.push('(');
1759                    for w in args.windows(2) {
1760                        formula.push_str(&fargs[w[0]..w[1]]);
1761                        formula.push(',');
1762                    }
1763                    formula.pop();
1764                    formula.push(')');
1765                } else {
1766                    stack.push(formula.len());
1767                    formula.push_str(
1768                        crate::utils::FTAB
1769                            .get(iftab)
1770                            .ok_or(XlsError::IfTab(iftab))?,
1771                    );
1772                    formula.push_str("()");
1773                }
1774            }
1775            0x23 | 0x43 | 0x63 => {
1776                let iname = read_u32(rgce) as usize - 1; // one-based
1777                stack.push(formula.len());
1778                formula.push_str(names.get(iname).map_or("#REF!", |n| &*n.0));
1779                rgce = &rgce[4..];
1780            }
1781            0x24 | 0x44 | 0x64 => {
1782                stack.push(formula.len());
1783                if is_pre_biff8 {
1784                    // BIFF2-5 PtgRef: rw(2 bytes, 14-bit row + 2 rel flags) + col(1 byte)
1785                    let rw_raw = read_u16(rgce);
1786                    let row = (rw_raw & 0x3FFF) + 1;
1787                    let col = rgce[2];
1788                    if rw_raw & 0x4000 == 0 {
1789                        formula.push('$');
1790                    }
1791                    push_column(col as u32, &mut formula);
1792                    if rw_raw & 0x8000 == 0 {
1793                        formula.push('$');
1794                    }
1795                    formula.push_str(&format!("{row}"));
1796                    rgce = &rgce[3..];
1797                } else {
1798                    let row = read_u16(rgce) + 1;
1799                    let col = read_u16(&[rgce[2], rgce[3] & 0x3F]);
1800                    if rgce[3] & 0x80 != 0x80 {
1801                        formula.push('$');
1802                    }
1803                    push_column(col as u32, &mut formula);
1804                    if rgce[3] & 0x40 != 0x40 {
1805                        formula.push('$');
1806                    }
1807                    formula.push_str(&format!("{row}"));
1808                    rgce = &rgce[4..];
1809                }
1810            }
1811            0x25 | 0x45 | 0x65 => {
1812                stack.push(formula.len());
1813                if is_pre_biff8 {
1814                    // BIFF2-5 PtgArea: rwFirst(2) + rwLast(2) + colFirst(1) + colLast(1) = 6 bytes
1815                    let row_first = (read_u16(&rgce[0..2]) & 0x3FFF) as u32 + 1;
1816                    let row_last = (read_u16(&rgce[2..4]) & 0x3FFF) as u32 + 1;
1817                    let col_first = rgce[4] as u32;
1818                    let col_last = rgce[5] as u32;
1819                    formula.push('$');
1820                    push_column(col_first, &mut formula);
1821                    write!(&mut formula, "${row_first}:$").unwrap();
1822                    push_column(col_last, &mut formula);
1823                    write!(&mut formula, "${row_last}").unwrap();
1824                    rgce = &rgce[6..];
1825                } else {
1826                    // columnFirst/columnLast are ColRelU: 14-bit column + fColRel/fRwRel flags.
1827                    let col_first = read_u16(&[rgce[4], rgce[5] & 0x3F]);
1828                    let col_last = read_u16(&[rgce[6], rgce[7] & 0x3F]);
1829                    let row_first = read_u16(&rgce[0..2]) as u32 + 1;
1830                    let row_last = read_u16(&rgce[2..4]) as u32 + 1;
1831                    if rgce[5] & 0x80 != 0x80 {
1832                        formula.push('$');
1833                    }
1834                    push_column(col_first as u32, &mut formula);
1835                    if rgce[5] & 0x40 != 0x40 {
1836                        formula.push('$');
1837                    }
1838                    write!(&mut formula, "{row_first}:").unwrap();
1839                    if rgce[7] & 0x80 != 0x80 {
1840                        formula.push('$');
1841                    }
1842                    push_column(col_last as u32, &mut formula);
1843                    if rgce[7] & 0x40 != 0x40 {
1844                        formula.push('$');
1845                    }
1846                    write!(&mut formula, "{row_last}").unwrap();
1847                    rgce = &rgce[8..];
1848                }
1849            }
1850            0x2A | 0x4A | 0x6A => {
1851                stack.push(formula.len());
1852                formula.push_str("#REF!");
1853                rgce = &rgce[if is_pre_biff8 { 3 } else { 4 }..];
1854            }
1855            0x2B | 0x4B | 0x6B => {
1856                stack.push(formula.len());
1857                formula.push_str("#REF!");
1858                rgce = &rgce[if is_pre_biff8 { 6 } else { 8 }..];
1859            }
1860            0x39 | 0x59 => {
1861                // PfgNameX
1862                stack.push(formula.len());
1863                formula.push_str("[PtgNameX]");
1864                rgce = &rgce[6..];
1865            }
1866            _ => {
1867                return Err(XlsError::Unrecognized {
1868                    typ: "ptg",
1869                    val: ptg,
1870                });
1871            }
1872        }
1873    }
1874    if stack.len() == 1 {
1875        Ok(formula)
1876    } else {
1877        Err(XlsError::InvalidFormula {
1878            stack_size: stack.len(),
1879        })
1880    }
1881}
1882
1883/// `FormulaValue` [MS-XLS 2.5.133]
1884fn parse_formula_value(
1885    r: &[u8],
1886    format: Option<&CellFormat>,
1887    is_1904: bool,
1888) -> Result<Option<Data>, XlsError> {
1889    match *r {
1890        // String, value should be in next record
1891        [0x00, .., 0xFF, 0xFF] => Ok(None),
1892        [0x01, _, b, .., 0xFF, 0xFF] => Ok(Some(Data::Bool(b != 0))),
1893        [0x02, _, e, .., 0xFF, 0xFF] => parse_err(e).map(Some),
1894        // ignore, return blank string value
1895        [0x03, _, .., 0xFF, 0xFF] => Ok(Some(Data::String("".to_string()))),
1896        [e, .., 0xFF, 0xFF] => Err(XlsError::Unrecognized {
1897            typ: "error",
1898            val: e,
1899        }),
1900        _ => Ok(Some(format_excel_f64(read_f64(r), format, is_1904))),
1901    }
1902}
1903
1904// OfficeArtRecord [MS-ODRAW 1.3.1].
1905#[cfg(feature = "picture")]
1906struct ArtRecord<'a> {
1907    instance: u16,
1908    typ: u16,
1909    data: &'a [u8],
1910}
1911
1912#[cfg(feature = "picture")]
1913struct ArtRecordIter<'a> {
1914    stream: &'a [u8],
1915}
1916
1917#[cfg(feature = "picture")]
1918impl<'a> Iterator for ArtRecordIter<'a> {
1919    type Item = Result<ArtRecord<'a>, XlsError>;
1920    fn next(&mut self) -> Option<Self::Item> {
1921        if self.stream.len() < 8 {
1922            return if self.stream.is_empty() {
1923                None
1924            } else {
1925                Some(Err(XlsError::EoStream("art record header")))
1926            };
1927        }
1928        let ver_ins = read_u16(self.stream);
1929        let instance = ver_ins >> 4;
1930        let typ = read_u16(&self.stream[2..]);
1931        if typ < 0xF000 {
1932            return Some(Err(XlsError::Art("type range 0xF000 - 0xFFFF")));
1933        }
1934        let len = read_usize(&self.stream[4..]);
1935        if self.stream.len() < len + 8 {
1936            return Some(Err(XlsError::EoStream("art record length")));
1937        }
1938        let (d, next) = self.stream.split_at(len + 8);
1939        self.stream = next;
1940        let data = &d[8..];
1941
1942        Some(Ok(ArtRecord {
1943            instance,
1944            typ,
1945            data,
1946        }))
1947    }
1948}
1949
1950// Parsing pictures.
1951#[cfg(feature = "picture")]
1952fn parse_pictures(stream: &[u8]) -> Result<Vec<(String, Vec<u8>)>, XlsError> {
1953    let mut pics = Vec::new();
1954    let records = ArtRecordIter { stream };
1955    for record in records {
1956        let r = record?;
1957        match r.typ {
1958            // OfficeArtDggContainer [MS-ODRAW 2.2.12]
1959            // OfficeArtBStoreContainer [MS-ODRAW 2.2.20]
1960            0xF000 | 0xF001 => pics.extend(parse_pictures(r.data)?),
1961            // OfficeArtFBSE [MS-ODRAW 2.2.32]
1962            0xF007 => {
1963                let skip = 36 + r.data[33] as usize;
1964                pics.extend(parse_pictures(&r.data[skip..])?);
1965            }
1966            // OfficeArtBlip [MS-ODRAW 2.2.23]
1967            0xF01A | 0xF01B | 0xF01C | 0xF01D | 0xF01E | 0xF01F | 0xF029 | 0xF02A => {
1968                let ext_skip = match r.typ {
1969                    // OfficeArtBlipEMF [MS-ODRAW 2.2.24]
1970                    0xF01A => {
1971                        let skip = match r.instance {
1972                            0x3D4 => 50usize,
1973                            0x3D5 => 66,
1974                            _ => unreachable!(),
1975                        };
1976                        Ok(("emf", skip))
1977                    }
1978                    // OfficeArtBlipWMF [MS-ODRAW 2.2.25]
1979                    0xF01B => {
1980                        let skip = match r.instance {
1981                            0x216 => 50usize,
1982                            0x217 => 66,
1983                            _ => unreachable!(),
1984                        };
1985                        Ok(("wmf", skip))
1986                    }
1987                    // OfficeArtBlipPICT [MS-ODRAW 2.2.26]
1988                    0xF01C => {
1989                        let skip = match r.instance {
1990                            0x542 => 50usize,
1991                            0x543 => 66,
1992                            _ => unreachable!(),
1993                        };
1994                        Ok(("pict", skip))
1995                    }
1996                    // OfficeArtBlipJPEG [MS-ODRAW 2.2.27]
1997                    0xF01D | 0xF02A => {
1998                        let skip = match r.instance {
1999                            0x46A | 0x6E2 => 17usize,
2000                            0x46B | 0x6E3 => 33,
2001                            _ => unreachable!(),
2002                        };
2003                        Ok(("jpg", skip))
2004                    }
2005                    // OfficeArtBlipPNG [MS-ODRAW 2.2.28]
2006                    0xF01E => {
2007                        let skip = match r.instance {
2008                            0x6E0 => 17usize,
2009                            0x6E1 => 33,
2010                            _ => unreachable!(),
2011                        };
2012                        Ok(("png", skip))
2013                    }
2014                    // OfficeArtBlipDIB [MS-ODRAW 2.2.29]
2015                    0xF01F => {
2016                        let skip = match r.instance {
2017                            0x7A8 => 17usize,
2018                            0x7A9 => 33,
2019                            _ => unreachable!(),
2020                        };
2021                        Ok(("dib", skip))
2022                    }
2023                    // OfficeArtBlipTIFF [MS-ODRAW 2.2.30]
2024                    0xF029 => {
2025                        let skip = match r.instance {
2026                            0x6E4 => 17usize,
2027                            0x6E5 => 33,
2028                            _ => unreachable!(),
2029                        };
2030                        Ok(("tiff", skip))
2031                    }
2032                    _ => Err(XlsError::Art("picture type not support")),
2033                };
2034                let ext_skip = ext_skip?;
2035                pics.push((ext_skip.0.to_string(), Vec::from(&r.data[ext_skip.1..])));
2036            }
2037            _ => {}
2038        }
2039    }
2040    Ok(pics)
2041}
2042
2043#[cfg(test)]
2044mod tests {
2045    use super::*;
2046
2047    #[test]
2048    fn test_parse_string() {
2049        let enc = XlsEncoding::from_codepage(1252).unwrap();
2050        parse_string(&[0, 1], &enc, Biff::Biff8).unwrap_err();
2051    }
2052}