use xlsbye_core::error::{Result, XlsByeError};
use xlsbye_core::types::{
Cell, CellError, CellValue, ColumnInfo, MergeCell, PaneInfo, RangeRef, RowInfo,
SelectionInfo, SheetViewInfo,
};
use xlsbye_formula::decompile::{decompile_formula, decompile_shared_formula};
use super::cursor::RecordCursor;
use super::header::RecordIter;
use super::ids::{
BRT_BEGIN_WS_VIEW, BRT_END_WS_VIEW, BRT_PANE, BRT_SEL,
BRT_CELL_BLANK, BRT_CELL_BOOL, BRT_CELL_ERROR, BRT_CELL_ISST, BRT_CELL_REAL, BRT_CELL_RK,
BRT_CELL_ST, BRT_COL_INFO, BRT_FMLA_BOOL, BRT_FMLA_ERROR, BRT_FMLA_NUM, BRT_FMLA_STRING,
BRT_H_LINK, BRT_MERGE_CELL, BRT_ROW_HDR, BRT_SHR_FMLA, BRT_WS_DIM,
};
#[derive(Debug, Clone, PartialEq)]
struct SharedFormula {
range: RangeRef,
index: u32,
formula: String,
}
#[derive(Debug, Clone, PartialEq)]
enum ParsedFormula {
Plain(String),
Shared,
}
const PENDING_SHARED_FORMULA_INDEX: u32 = u32::MAX;
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedWorksheet {
pub dimension: Option<RangeRef>,
pub sheet_views: Vec<SheetViewInfo>,
pub columns: Vec<ColumnInfo>,
pub rows: Vec<ParsedRow>,
pub merge_cells: Vec<MergeCell>,
pub hyperlinks: Vec<Hyperlink>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedRow {
pub info: RowInfo,
pub cells: Vec<Cell>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Hyperlink {
pub range: RangeRef,
pub rel_id: Option<String>,
pub location: Option<String>,
pub display: Option<String>,
pub tooltip: Option<String>,
}
pub fn parse_worksheet(
data: &[u8],
sheets: &[String],
names: &[(String, String)],
) -> Result<ParsedWorksheet> {
let mut parsed = ParsedWorksheet {
dimension: None,
sheet_views: Vec::new(),
columns: Vec::new(),
rows: Vec::new(),
merge_cells: Vec::new(),
hyperlinks: Vec::new(),
};
let mut current_row: Option<usize> = None;
let mut shared_formulas = Vec::<SharedFormula>::new();
for record in RecordIter::new(data) {
let (record_type, payload) = record?;
match record_type {
ty if ty == BRT_WS_DIM.as_u16() => {
parsed.dimension = Some(parse_range_ref(payload)?);
}
ty if ty == BRT_BEGIN_WS_VIEW.as_u16() => {
parsed.sheet_views.push(parse_sheet_view(payload)?);
}
ty if ty == BRT_PANE.as_u16() => {
if let Some(sheet_view) = parsed.sheet_views.last_mut() {
sheet_view.pane = Some(parse_pane(payload)?);
}
}
ty if ty == BRT_SEL.as_u16() => {
if let Some(sheet_view) = parsed.sheet_views.last_mut() {
let pane = sheet_view.pane.as_ref();
if let Some(selection) = parse_selection(payload, pane)? {
sheet_view.selections.push(selection);
}
}
}
ty if ty == BRT_END_WS_VIEW.as_u16() => {}
ty if ty == BRT_COL_INFO.as_u16() => {
parsed.columns.push(parse_col_info(payload)?);
}
ty if ty == BRT_ROW_HDR.as_u16() => {
let row = parse_row_header(payload)?;
parsed.rows.push(ParsedRow {
info: row,
cells: Vec::new(),
});
current_row = Some(parsed.rows.len() - 1);
}
ty if ty == BRT_CELL_BLANK.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
parsed.rows[row_index].cells.push(parse_blank_cell(payload)?);
}
ty if ty == BRT_CELL_RK.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
parsed.rows[row_index].cells.push(parse_rk_cell(payload)?);
}
ty if ty == BRT_CELL_ERROR.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
parsed.rows[row_index].cells.push(parse_error_cell(payload)?);
}
ty if ty == BRT_CELL_BOOL.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
parsed.rows[row_index].cells.push(parse_bool_cell(payload)?);
}
ty if ty == BRT_CELL_REAL.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
parsed.rows[row_index].cells.push(parse_real_cell(payload)?);
}
ty if ty == BRT_CELL_ST.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
parsed.rows[row_index].cells.push(parse_string_cell(payload)?);
}
ty if ty == BRT_CELL_ISST.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
parsed.rows[row_index].cells.push(parse_isst_cell(payload)?);
}
ty if ty == BRT_FMLA_STRING.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
let row_number = parsed.rows[row_index].info.row;
let mut cell = parse_formula_string_cell(payload, sheets, names, row_number)?;
apply_shared_formula(&mut cell, row_number, &shared_formulas);
parsed.rows[row_index].cells.push(cell);
}
ty if ty == BRT_FMLA_NUM.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
let row_number = parsed.rows[row_index].info.row;
let mut cell = parse_formula_num_cell(payload, sheets, names, row_number)?;
apply_shared_formula(&mut cell, row_number, &shared_formulas);
parsed.rows[row_index].cells.push(cell);
}
ty if ty == BRT_FMLA_BOOL.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
let row_number = parsed.rows[row_index].info.row;
let mut cell = parse_formula_bool_cell(payload, sheets, names, row_number)?;
apply_shared_formula(&mut cell, row_number, &shared_formulas);
parsed.rows[row_index].cells.push(cell);
}
ty if ty == BRT_FMLA_ERROR.as_u16() => {
let row_index = current_row.ok_or_else(|| {
XlsByeError::Biff12("cell record encountered before row header".to_string())
})?;
let row_number = parsed.rows[row_index].info.row;
let mut cell = parse_formula_error_cell(payload, sheets, names, row_number)?;
apply_shared_formula(&mut cell, row_number, &shared_formulas);
parsed.rows[row_index].cells.push(cell);
}
ty if ty == BRT_SHR_FMLA.as_u16() => {
let shared_formula = parse_shared_formula(payload, sheets, names, shared_formulas.len() as u32)?;
let anchor = shared_formula_anchor(&shared_formula.range);
let anchor_cell = parsed
.rows
.iter_mut()
.find(|row| row.info.row == anchor.0)
.and_then(|row| row.cells.iter_mut().find(|cell| cell.col == anchor.1))
.ok_or_else(|| {
XlsByeError::Biff12("shared formula anchor cell missing before BrtShrFmla".to_string())
})?;
anchor_cell.formula = Some(shared_formula.formula.clone());
anchor_cell.shared_formula_index = Some(shared_formula.index);
anchor_cell.shared_formula_ref = Some(shared_formula.range.clone());
shared_formulas.push(shared_formula);
}
ty if ty == BRT_MERGE_CELL.as_u16() => {
parsed.merge_cells.push(MergeCell {
range: parse_range_ref(payload)?,
});
}
ty if ty == BRT_H_LINK.as_u16() => {
parsed.hyperlinks.push(parse_hyperlink(payload)?);
}
_ => {}
}
}
for row in &mut parsed.rows {
for cell in &mut row.cells {
if cell.shared_formula_index == Some(PENDING_SHARED_FORMULA_INDEX) {
cell.shared_formula_index = None;
cell.shared_formula_ref = None;
}
}
}
Ok(parsed)
}
fn parse_range_ref(payload: &[u8]) -> Result<RangeRef> {
let mut cursor = RecordCursor::new(payload);
let first_row = one_based(cursor.read_u32()?, "range first row")?;
let last_row = one_based(cursor.read_u32()?, "range last row")?;
let first_col = one_based(cursor.read_u32()?, "range first column")?;
let last_col = one_based(cursor.read_u32()?, "range last column")?;
Ok(RangeRef {
first_row,
last_row,
first_col,
last_col,
})
}
fn parse_col_info(payload: &[u8]) -> Result<ColumnInfo> {
let mut cursor = RecordCursor::new(payload);
let min = one_based(cursor.read_u32()?, "column first")?;
let max = one_based(cursor.read_u32()?, "column last")?;
let coldx = cursor.read_u32()?;
let style_index = cursor.read_u32()?;
let flags = cursor.read_u16()?;
Ok(ColumnInfo {
min,
max,
width: f64::from(coldx) / 256.0,
style_index,
hidden: flags & 0x0001 != 0,
best_fit: flags & 0x0004 != 0,
custom_width: flags & 0x0002 != 0,
outline_level: ((flags >> 8) & 0x7) as u8,
collapsed: flags & 0x1000 != 0,
})
}
fn parse_row_header(payload: &[u8]) -> Result<RowInfo> {
let mut cursor = RecordCursor::new(payload);
let row = one_based(cursor.read_u32()?, "row index")?;
let style_index = cursor.read_u32()?;
let miy_rw = cursor.read_u16()?;
let flags = if cursor.remaining() >= 2 {
cursor.read_u16()?
} else {
0
};
Ok(RowInfo {
row,
height: f64::from(miy_rw) / 20.0,
style_index,
hidden: flags & 0x1000 != 0,
custom_height: flags & 0x2000 != 0,
outline_level: ((flags >> 8) & 0x7) as u8,
collapsed: flags & 0x0800 != 0,
thick_top: flags & 0x4000 != 0,
thick_bottom: flags & 0x8000 != 0,
})
}
fn parse_sheet_view(payload: &[u8]) -> Result<SheetViewInfo> {
let mut cursor = RecordCursor::new(payload);
let flags = cursor.read_u32()?;
let top_left_col = cursor.read_u16()?;
let top_left_row = cursor.read_u16()?;
cursor.skip(6)?;
let _view = cursor.read_u16()?;
let _color_id = cursor.read_u16()?;
let zoom_scale = u32::from(cursor.read_u16()?);
let zoom_scale_normal = u32::from(cursor.read_u16()?);
let workbook_view_id = u32::from(cursor.read_u16()?);
let _ = cursor.read_u16()?;
Ok(SheetViewInfo {
tab_selected: flags & 0x40 != 0,
show_grid_lines: false,
top_left_row: u32::from(top_left_row),
top_left_col: u32::from(top_left_col),
zoom_scale: (zoom_scale != 0).then_some(zoom_scale),
zoom_scale_normal: (zoom_scale_normal != 0).then_some(zoom_scale_normal),
workbook_view_id,
pane: None,
selections: Vec::new(),
})
}
fn parse_pane(payload: &[u8]) -> Result<PaneInfo> {
let mut cursor = RecordCursor::new(payload);
let x_split = cursor.read_f64()?;
let y_split = cursor.read_f64()?;
let top_left_row = cursor.read_u32()?;
let top_left_col = cursor.read_u32()?;
let _reserved = cursor.read_u32()?;
let active_pane = pane_name(u32::from(cursor.read_u8()?)).unwrap_or("bottomRight").to_string();
Ok(PaneInfo {
x_split,
y_split,
top_left_row,
top_left_col,
active_pane,
state: "frozen".to_string(),
})
}
fn parse_selection(payload: &[u8], pane: Option<&PaneInfo>) -> Result<Option<SelectionInfo>> {
let mut cursor = RecordCursor::new(payload);
let pane_code = cursor.read_u32()?;
let active_row = cursor.read_u32()?;
let active_col = cursor.read_u32()?;
let _active_cell_id = cursor.read_u32()?;
let ref_count = cursor.read_u32()?;
if ref_count == 0 {
return Ok(None);
}
let first_row = cursor.read_u32()?;
let last_row = cursor.read_u32()?;
let first_col = cursor.read_u32()?;
let last_col = cursor.read_u32()?;
let pane_name = pane_name(pane_code);
if pane_name == Some("topLeft") {
return Ok(None);
}
let (active_row, active_col, first_row, last_row, first_col, last_col) =
if pane_name == Some("topRight") {
let offset = pane.map(|pane| pane.top_left_col).unwrap_or(0);
(
active_row,
active_col + offset,
first_row,
last_row,
first_col + offset,
last_col + offset,
)
} else {
(active_row, active_col, first_row, last_row, first_col, last_col)
};
Ok(Some(SelectionInfo {
pane: pane_name.map(ToString::to_string),
active_cell: cell_ref_zero_based_to_a1(active_row, active_col),
sqref: range_ref_zero_based_to_a1(first_row, last_row, first_col, last_col),
}))
}
fn pane_name(code: u32) -> Option<&'static str> {
match code {
0 => Some("bottomRight"),
1 => Some("topRight"),
2 => Some("bottomLeft"),
3 => Some("topLeft"),
_ => None,
}
}
fn range_ref_zero_based_to_a1(first_row: u32, last_row: u32, first_col: u32, last_col: u32) -> String {
let start = cell_ref_zero_based_to_a1(first_row, first_col);
let end = cell_ref_zero_based_to_a1(last_row, last_col);
if start == end { start } else { format!("{start}:{end}") }
}
fn cell_ref_zero_based_to_a1(row: u32, col: u32) -> String {
format!("{}{}", col_to_name_zero_based(col), row + 1)
}
fn col_to_name_zero_based(mut col: u32) -> String {
let mut letters = Vec::new();
col += 1;
while col > 0 {
let rem = ((col - 1) % 26) as u8;
letters.push((b'A' + rem) as char);
col = (col - 1) / 26;
}
letters.iter().rev().collect()
}
fn parse_cell_header(cursor: &mut RecordCursor<'_>) -> Result<(u32, u32)> {
let col = one_based(cursor.read_u32()?, "cell column")?;
let style_index = cursor.read_u32()?;
Ok((col, style_index))
}
fn parse_blank_cell(payload: &[u8]) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
Ok(Cell {
col,
style_index,
value: CellValue::Blank,
formula: None,
shared_formula_index: None,
shared_formula_ref: None,
})
}
fn parse_rk_cell(payload: &[u8]) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let rk = cursor.read_u32()?;
Ok(Cell {
col,
style_index,
value: CellValue::Number(decode_rk(rk)),
formula: None,
shared_formula_index: None,
shared_formula_ref: None,
})
}
fn parse_error_cell(payload: &[u8]) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let err_code = cursor.read_u8()?;
let err = CellError::from_code(err_code)
.ok_or_else(|| XlsByeError::Biff12(format!("invalid cell error code: 0x{err_code:02X}")))?;
Ok(Cell {
col,
style_index,
value: CellValue::Error(err),
formula: None,
shared_formula_index: None,
shared_formula_ref: None,
})
}
fn parse_bool_cell(payload: &[u8]) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let value = cursor.read_bool()?;
Ok(Cell {
col,
style_index,
value: CellValue::Bool(value),
formula: None,
shared_formula_index: None,
shared_formula_ref: None,
})
}
fn parse_real_cell(payload: &[u8]) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let value = cursor.read_f64()?;
Ok(Cell {
col,
style_index,
value: CellValue::Number(value),
formula: None,
shared_formula_index: None,
shared_formula_ref: None,
})
}
fn parse_string_cell(payload: &[u8]) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let value = cursor.read_wide_string()?;
Ok(Cell {
col,
style_index,
value: CellValue::String(value),
formula: None,
shared_formula_index: None,
shared_formula_ref: None,
})
}
fn parse_isst_cell(payload: &[u8]) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let sst_index = cursor.read_u32()?;
Ok(Cell {
col,
style_index,
value: CellValue::SharedString(sst_index),
formula: None,
shared_formula_index: None,
shared_formula_ref: None,
})
}
fn parse_formula_string_cell(
payload: &[u8],
sheets: &[String],
names: &[(String, String)],
row: u32,
) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let cached = cursor.read_wide_string()?;
let formula = parse_formula_suffix(&mut cursor, sheets, names, row, col)?;
Ok(Cell {
col,
style_index,
value: CellValue::String(cached),
formula: parsed_formula_text(&formula),
shared_formula_index: parsed_shared_formula_marker(&formula),
shared_formula_ref: None,
})
}
fn parse_formula_num_cell(
payload: &[u8],
sheets: &[String],
names: &[(String, String)],
row: u32,
) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let cached = cursor.read_f64()?;
let formula = parse_formula_suffix(&mut cursor, sheets, names, row, col)?;
Ok(Cell {
col,
style_index,
value: CellValue::Number(cached),
formula: parsed_formula_text(&formula),
shared_formula_index: parsed_shared_formula_marker(&formula),
shared_formula_ref: None,
})
}
fn parse_formula_bool_cell(
payload: &[u8],
sheets: &[String],
names: &[(String, String)],
row: u32,
) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let cached = cursor.read_bool()?;
let formula = parse_formula_suffix(&mut cursor, sheets, names, row, col)?;
Ok(Cell {
col,
style_index,
value: CellValue::Bool(cached),
formula: parsed_formula_text(&formula),
shared_formula_index: parsed_shared_formula_marker(&formula),
shared_formula_ref: None,
})
}
fn parse_formula_error_cell(
payload: &[u8],
sheets: &[String],
names: &[(String, String)],
row: u32,
) -> Result<Cell> {
let mut cursor = RecordCursor::new(payload);
let (col, style_index) = parse_cell_header(&mut cursor)?;
let err_code = cursor.read_u8()?;
let cached = CellError::from_code(err_code).ok_or_else(|| {
XlsByeError::Biff12(format!("invalid formula cached error code: 0x{err_code:02X}"))
})?;
let formula = parse_formula_suffix(&mut cursor, sheets, names, row, col)?;
Ok(Cell {
col,
style_index,
value: CellValue::Error(cached),
formula: parsed_formula_text(&formula),
shared_formula_index: parsed_shared_formula_marker(&formula),
shared_formula_ref: None,
})
}
fn parse_formula_suffix(
cursor: &mut RecordCursor<'_>,
sheets: &[String],
names: &[(String, String)],
row: u32,
col: u32,
) -> Result<Option<ParsedFormula>> {
let _flags = cursor.read_u16()?;
let cce = usize::try_from(cursor.read_u32()?)
.map_err(|_| XlsByeError::Biff12("formula token length out of range".to_string()))?;
let rgce = cursor.read_bytes(cce)?;
let cb = if cursor.remaining() >= 4 {
usize::try_from(cursor.read_u32()?)
.map_err(|_| XlsByeError::Biff12("formula extra data length out of range".to_string()))?
} else {
0
};
let rgcb = if cb > 0 { cursor.read_bytes(cb)? } else { &[][..] };
if is_shared_formula_reference(rgce, rgcb, row, col) {
return Ok(Some(ParsedFormula::Shared));
}
if cce == 0 {
return Ok(None);
}
decompile_formula(rgce, sheets, names)
.map(|formula| {
if formula.is_empty() {
None
} else {
Some(ParsedFormula::Plain(formula))
}
})
.map_err(|err| XlsByeError::Formula(format!("worksheet formula: {err}")))
}
fn parsed_formula_text(formula: &Option<ParsedFormula>) -> Option<String> {
match formula {
Some(ParsedFormula::Plain(formula)) => Some(formula.clone()),
_ => None,
}
}
fn parsed_shared_formula_marker(formula: &Option<ParsedFormula>) -> Option<u32> {
match formula {
Some(ParsedFormula::Shared) => Some(PENDING_SHARED_FORMULA_INDEX),
_ => None,
}
}
fn is_shared_formula_reference(rgce: &[u8], rgcb: &[u8], _row: u32, _col: u32) -> bool {
rgce.len() == 5 && rgce[0] == 0x01 && rgcb.len() == 4
}
fn parse_shared_formula(
payload: &[u8],
sheets: &[String],
names: &[(String, String)],
index: u32,
) -> Result<SharedFormula> {
let mut cursor = RecordCursor::new(payload);
let range = parse_range_ref(cursor.read_bytes(16)?)?;
let cce = usize::try_from(cursor.read_u32()?)
.map_err(|_| XlsByeError::Biff12("shared formula token length out of range".to_string()))?;
let rgce = cursor.read_bytes(cce)?;
let cb = if cursor.remaining() >= 4 {
usize::try_from(cursor.read_u32()?)
.map_err(|_| XlsByeError::Biff12("shared formula extra data length out of range".to_string()))?
} else {
0
};
if cb > 0 {
let _ = cursor.read_bytes(cb)?;
}
let formula = decompile_shared_formula(
rgce,
sheets,
names,
range.first_row - 1,
range.first_col - 1,
)
.map_err(|err| XlsByeError::Formula(format!("shared worksheet formula: {err}")))?;
Ok(SharedFormula {
range,
index,
formula,
})
}
fn apply_shared_formula(cell: &mut Cell, row: u32, shared_formulas: &[SharedFormula]) {
if cell.shared_formula_index != Some(PENDING_SHARED_FORMULA_INDEX) {
return;
}
if let Some(shared_formula) = shared_formulas.iter().find(|formula| range_contains(&formula.range, row, cell.col)) {
cell.shared_formula_index = Some(shared_formula.index);
if shared_formula.range.first_row == row && shared_formula.range.first_col == cell.col {
cell.formula = Some(shared_formula.formula.clone());
cell.shared_formula_ref = Some(shared_formula.range.clone());
}
}
}
fn shared_formula_anchor(range: &RangeRef) -> (u32, u32) {
(range.first_row, range.first_col)
}
fn range_contains(range: &RangeRef, row: u32, col: u32) -> bool {
range.first_row <= row && row <= range.last_row && range.first_col <= col && col <= range.last_col
}
fn parse_hyperlink(payload: &[u8]) -> Result<Hyperlink> {
let mut cursor = RecordCursor::new(payload);
let range = parse_range_ref(cursor.read_bytes(16)?)?;
let rel_id = non_empty(cursor.read_wide_string()?);
let location = non_empty(cursor.read_wide_string()?);
let tooltip = non_empty(cursor.read_wide_string()?);
let display = non_empty(cursor.read_wide_string()?);
Ok(Hyperlink {
range,
rel_id,
location,
display,
tooltip,
})
}
fn non_empty(value: String) -> Option<String> {
if value.is_empty() {
None
} else {
Some(value)
}
}
fn one_based(value: u32, field: &str) -> Result<u32> {
value.checked_add(1).ok_or_else(|| {
XlsByeError::Biff12(format!(
"{field} value {value} overflows when converting to 1-based index"
))
})
}
fn decode_rk(rk: u32) -> f64 {
let is_integer = (rk & 0x02) != 0;
let div_100 = (rk & 0x01) != 0;
let value = if is_integer {
(rk as i32 >> 2) as f64
} else {
let bits = ((rk & 0xFFFF_FFFC) as u64) << 32;
f64::from_bits(bits)
};
if div_100 {
value / 100.0
} else {
value
}
}
#[cfg(test)]
mod tests {
use super::*;
fn encode_varint(mut value: u32) -> Vec<u8> {
let mut out = Vec::new();
loop {
let mut byte = (value & 0x7F) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
out.push(byte);
if value == 0 {
break;
}
}
out
}
fn encode_record(record_type: u16, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&encode_varint(u32::from(record_type)));
out.extend_from_slice(&encode_varint(payload.len() as u32));
out.extend_from_slice(payload);
out
}
fn push_wide_string(buf: &mut Vec<u8>, value: &str) {
let chars = value.encode_utf16().collect::<Vec<_>>();
buf.extend_from_slice(&(chars.len() as u32).to_le_bytes());
for ch in chars {
buf.extend_from_slice(&ch.to_le_bytes());
}
}
fn row_hdr_payload(row_zero_based: u32, style_index: u32, miy_rw: u16, flags: u16) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&row_zero_based.to_le_bytes());
payload.extend_from_slice(&style_index.to_le_bytes());
payload.extend_from_slice(&miy_rw.to_le_bytes());
payload.extend_from_slice(&flags.to_le_bytes());
payload
}
#[test]
fn decodes_rk_edge_cases() {
let rk_int = ((42u32) << 2) | 0x02;
assert_eq!(decode_rk(rk_int), 42.0);
let rk_int_div100 = ((12345u32) << 2) | 0x03;
assert!((decode_rk(rk_int_div100) - 123.45).abs() < 1e-12);
let rk_float = (1.0f64.to_bits() >> 32) as u32;
assert_eq!(decode_rk(rk_float), 1.0);
let rk_float_div100 = ((100.0f64.to_bits() >> 32) as u32) | 0x01;
assert_eq!(decode_rk(rk_float_div100), 1.0);
}
#[test]
fn parses_dimension_columns_rows_cells_merges_and_hyperlinks() {
let mut data = Vec::new();
let mut dim = Vec::new();
dim.extend_from_slice(&0u32.to_le_bytes());
dim.extend_from_slice(&9u32.to_le_bytes());
dim.extend_from_slice(&0u32.to_le_bytes());
dim.extend_from_slice(&6u32.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_WS_DIM.as_u16(), &dim));
let mut col = Vec::new();
col.extend_from_slice(&0u32.to_le_bytes());
col.extend_from_slice(&1u32.to_le_bytes());
col.extend_from_slice(&(12u32 * 256).to_le_bytes());
col.extend_from_slice(&4u32.to_le_bytes());
let col_flags = 0x0001u16 | 0x0002u16 | 0x0004u16 | (3u16 << 8) | 0x1000u16;
col.extend_from_slice(&col_flags.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_COL_INFO.as_u16(), &col));
let row_flags = 0x2000u16 | 0x1000u16 | (2u16 << 8) | 0x0800u16;
data.extend_from_slice(&encode_record(
BRT_ROW_HDR.as_u16(),
&row_hdr_payload(0, 9, 400, row_flags),
));
let mut blank = Vec::new();
blank.extend_from_slice(&0u32.to_le_bytes());
blank.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_CELL_BLANK.as_u16(), &blank));
let mut rk = Vec::new();
rk.extend_from_slice(&1u32.to_le_bytes());
rk.extend_from_slice(&2u32.to_le_bytes());
rk.extend_from_slice(&(((25u32) << 2) | 0x03).to_le_bytes());
data.extend_from_slice(&encode_record(BRT_CELL_RK.as_u16(), &rk));
let mut err = Vec::new();
err.extend_from_slice(&2u32.to_le_bytes());
err.extend_from_slice(&3u32.to_le_bytes());
err.push(0x17);
data.extend_from_slice(&encode_record(BRT_CELL_ERROR.as_u16(), &err));
let mut b = Vec::new();
b.extend_from_slice(&3u32.to_le_bytes());
b.extend_from_slice(&4u32.to_le_bytes());
b.push(1);
data.extend_from_slice(&encode_record(BRT_CELL_BOOL.as_u16(), &b));
let mut real = Vec::new();
real.extend_from_slice(&4u32.to_le_bytes());
real.extend_from_slice(&5u32.to_le_bytes());
real.extend_from_slice(&42.5f64.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_CELL_REAL.as_u16(), &real));
let mut st = Vec::new();
st.extend_from_slice(&5u32.to_le_bytes());
st.extend_from_slice(&6u32.to_le_bytes());
push_wide_string(&mut st, "inline");
data.extend_from_slice(&encode_record(BRT_CELL_ST.as_u16(), &st));
let mut isst = Vec::new();
isst.extend_from_slice(&6u32.to_le_bytes());
isst.extend_from_slice(&7u32.to_le_bytes());
isst.extend_from_slice(&11u32.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_CELL_ISST.as_u16(), &isst));
let mut merge = Vec::new();
merge.extend_from_slice(&0u32.to_le_bytes());
merge.extend_from_slice(&0u32.to_le_bytes());
merge.extend_from_slice(&0u32.to_le_bytes());
merge.extend_from_slice(&2u32.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_MERGE_CELL.as_u16(), &merge));
let mut hlink = Vec::new();
hlink.extend_from_slice(&0u32.to_le_bytes());
hlink.extend_from_slice(&0u32.to_le_bytes());
hlink.extend_from_slice(&5u32.to_le_bytes());
hlink.extend_from_slice(&5u32.to_le_bytes());
push_wide_string(&mut hlink, "rId7");
push_wide_string(&mut hlink, "");
push_wide_string(&mut hlink, "tip");
push_wide_string(&mut hlink, "Go");
data.extend_from_slice(&encode_record(BRT_H_LINK.as_u16(), &hlink));
let parsed = parse_worksheet(&data, &[], &[]).expect("worksheet should parse");
assert_eq!(
parsed.dimension,
Some(RangeRef {
first_row: 1,
last_row: 10,
first_col: 1,
last_col: 7
})
);
assert_eq!(parsed.columns.len(), 1);
assert_eq!(parsed.columns[0].min, 1);
assert_eq!(parsed.columns[0].max, 2);
assert_eq!(parsed.columns[0].width, 12.0);
assert_eq!(parsed.columns[0].style_index, 4);
assert!(parsed.columns[0].hidden);
assert!(parsed.columns[0].best_fit);
assert!(parsed.columns[0].custom_width);
assert_eq!(parsed.columns[0].outline_level, 3);
assert!(parsed.columns[0].collapsed);
assert_eq!(parsed.rows.len(), 1);
assert_eq!(parsed.rows[0].info.row, 1);
assert_eq!(parsed.rows[0].info.style_index, 9);
assert_eq!(parsed.rows[0].info.height, 20.0);
assert!(parsed.rows[0].info.custom_height);
assert!(parsed.rows[0].info.hidden);
assert_eq!(parsed.rows[0].info.outline_level, 2);
assert!(parsed.rows[0].info.collapsed);
assert_eq!(parsed.rows[0].cells.len(), 7);
assert_eq!(parsed.rows[0].cells[0].col, 1);
assert_eq!(parsed.rows[0].cells[0].value, CellValue::Blank);
assert_eq!(parsed.rows[0].cells[1].value, CellValue::Number(0.25));
assert_eq!(
parsed.rows[0].cells[2].value,
CellValue::Error(CellError::Ref)
);
assert_eq!(parsed.rows[0].cells[3].value, CellValue::Bool(true));
assert_eq!(parsed.rows[0].cells[4].value, CellValue::Number(42.5));
assert_eq!(
parsed.rows[0].cells[5].value,
CellValue::String("inline".to_string())
);
assert_eq!(parsed.rows[0].cells[6].value, CellValue::SharedString(11));
assert_eq!(parsed.merge_cells.len(), 1);
assert_eq!(
parsed.merge_cells[0].range,
RangeRef {
first_row: 1,
last_row: 1,
first_col: 1,
last_col: 3
}
);
assert_eq!(parsed.hyperlinks.len(), 1);
assert_eq!(parsed.hyperlinks[0].range.first_col, 6);
assert_eq!(parsed.hyperlinks[0].rel_id.as_deref(), Some("rId7"));
assert_eq!(parsed.hyperlinks[0].location, None);
assert_eq!(parsed.hyperlinks[0].tooltip.as_deref(), Some("tip"));
assert_eq!(parsed.hyperlinks[0].display.as_deref(), Some("Go"));
}
#[test]
fn parses_formula_cells() {
let mut data = Vec::new();
data.extend_from_slice(&encode_record(
BRT_ROW_HDR.as_u16(),
&row_hdr_payload(0, 0, 300, 0),
));
let formula_tokens = vec![0x1E, 0x01, 0x00, 0x1E, 0x02, 0x00, 0x03];
let mut fmla_num = Vec::new();
fmla_num.extend_from_slice(&0u32.to_le_bytes());
fmla_num.extend_from_slice(&2u32.to_le_bytes());
fmla_num.extend_from_slice(&3.0f64.to_le_bytes());
fmla_num.extend_from_slice(&0u16.to_le_bytes());
fmla_num.extend_from_slice(&(formula_tokens.len() as u32).to_le_bytes());
fmla_num.extend_from_slice(&formula_tokens);
data.extend_from_slice(&encode_record(BRT_FMLA_NUM.as_u16(), &fmla_num));
let mut fmla_bool = Vec::new();
fmla_bool.extend_from_slice(&1u32.to_le_bytes());
fmla_bool.extend_from_slice(&3u32.to_le_bytes());
fmla_bool.push(1);
fmla_bool.extend_from_slice(&0u16.to_le_bytes());
fmla_bool.extend_from_slice(&(formula_tokens.len() as u32).to_le_bytes());
fmla_bool.extend_from_slice(&formula_tokens);
data.extend_from_slice(&encode_record(BRT_FMLA_BOOL.as_u16(), &fmla_bool));
let mut fmla_str = Vec::new();
fmla_str.extend_from_slice(&2u32.to_le_bytes());
fmla_str.extend_from_slice(&4u32.to_le_bytes());
push_wide_string(&mut fmla_str, "3");
fmla_str.extend_from_slice(&0u16.to_le_bytes());
fmla_str.extend_from_slice(&(formula_tokens.len() as u32).to_le_bytes());
fmla_str.extend_from_slice(&formula_tokens);
data.extend_from_slice(&encode_record(BRT_FMLA_STRING.as_u16(), &fmla_str));
let mut fmla_err = Vec::new();
fmla_err.extend_from_slice(&3u32.to_le_bytes());
fmla_err.extend_from_slice(&5u32.to_le_bytes());
fmla_err.push(0x17);
fmla_err.extend_from_slice(&0u16.to_le_bytes());
fmla_err.extend_from_slice(&(formula_tokens.len() as u32).to_le_bytes());
fmla_err.extend_from_slice(&formula_tokens);
data.extend_from_slice(&encode_record(BRT_FMLA_ERROR.as_u16(), &fmla_err));
let parsed = parse_worksheet(&data, &[], &[]).expect("formula worksheet should parse");
assert_eq!(parsed.rows.len(), 1);
assert_eq!(parsed.rows[0].cells.len(), 4);
assert_eq!(parsed.rows[0].cells[0].value, CellValue::Number(3.0));
assert_eq!(parsed.rows[0].cells[0].formula.as_deref(), Some("1+2"));
assert_eq!(parsed.rows[0].cells[1].value, CellValue::Bool(true));
assert_eq!(parsed.rows[0].cells[1].formula.as_deref(), Some("1+2"));
assert_eq!(
parsed.rows[0].cells[2].value,
CellValue::String("3".to_string())
);
assert_eq!(parsed.rows[0].cells[2].formula.as_deref(), Some("1+2"));
assert_eq!(
parsed.rows[0].cells[3].value,
CellValue::Error(CellError::Ref)
);
assert_eq!(parsed.rows[0].cells[3].formula.as_deref(), Some("1+2"));
}
#[test]
fn zero_token_formula_cells_fall_back_to_cached_values() {
let mut data = Vec::new();
data.extend_from_slice(&encode_record(
BRT_ROW_HDR.as_u16(),
&row_hdr_payload(0, 0, 300, 0),
));
let mut fmla_num = Vec::new();
fmla_num.extend_from_slice(&0u32.to_le_bytes());
fmla_num.extend_from_slice(&2u32.to_le_bytes());
fmla_num.extend_from_slice(&42.5f64.to_le_bytes());
fmla_num.extend_from_slice(&0u16.to_le_bytes());
fmla_num.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_FMLA_NUM.as_u16(), &fmla_num));
let parsed = parse_worksheet(&data, &[], &[]).expect("formula worksheet should parse");
assert_eq!(parsed.rows.len(), 1);
assert_eq!(parsed.rows[0].cells.len(), 1);
assert_eq!(parsed.rows[0].cells[0].value, CellValue::Number(42.5));
assert_eq!(parsed.rows[0].cells[0].formula, None);
}
#[test]
fn parses_shared_formula_group() {
let mut data = Vec::new();
data.extend_from_slice(&encode_record(
BRT_ROW_HDR.as_u16(),
&row_hdr_payload(5, 0, 300, 0),
));
let mut master = Vec::new();
master.extend_from_slice(&3u32.to_le_bytes());
master.extend_from_slice(&368u32.to_le_bytes());
master.extend_from_slice(&(-0.039137319960787176f64).to_le_bytes());
master.extend_from_slice(&0u16.to_le_bytes());
master.extend_from_slice(&5u32.to_le_bytes());
master.extend_from_slice(&[0x01, 0x05, 0x00, 0x00, 0x00]);
master.extend_from_slice(&4u32.to_le_bytes());
master.extend_from_slice(&3u32.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_FMLA_NUM.as_u16(), &master));
let mut shared = Vec::new();
shared.extend_from_slice(&5u32.to_le_bytes());
shared.extend_from_slice(&6u32.to_le_bytes());
shared.extend_from_slice(&3u32.to_le_bytes());
shared.extend_from_slice(&3u32.to_le_bytes());
shared.extend_from_slice(&41u32.to_le_bytes());
shared.extend_from_slice(&[
0x4C, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x4C, 0x00, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0x06, 0x1E, 0x01, 0x00, 0x04, 0x19, 0x80, 0x0B, 0x00, 0x17, 0x04, 0x00,
0x6E, 0x00, 0x2E, 0x00, 0x61, 0x00, 0x2E, 0x00, 0x19, 0x08, 0x02, 0x00, 0x41,
0xE0, 0x01,
]);
shared.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_SHR_FMLA.as_u16(), &shared));
data.extend_from_slice(&encode_record(
BRT_ROW_HDR.as_u16(),
&row_hdr_payload(6, 0, 300, 0),
));
let mut follower = Vec::new();
follower.extend_from_slice(&3u32.to_le_bytes());
follower.extend_from_slice(&371u32.to_le_bytes());
follower.extend_from_slice(&(-0.06893622586073733f64).to_le_bytes());
follower.extend_from_slice(&0u16.to_le_bytes());
follower.extend_from_slice(&5u32.to_le_bytes());
follower.extend_from_slice(&[0x01, 0x06, 0x00, 0x00, 0x00]);
follower.extend_from_slice(&4u32.to_le_bytes());
follower.extend_from_slice(&3u32.to_le_bytes());
data.extend_from_slice(&encode_record(BRT_FMLA_NUM.as_u16(), &follower));
let parsed = parse_worksheet(&data, &[], &[]).expect("shared formulas should parse");
assert_eq!(parsed.rows.len(), 2);
assert_eq!(parsed.rows[0].cells[0].formula.as_deref(), Some("IFERROR(B6/C6-1,\"n.a.\")"));
assert_eq!(parsed.rows[0].cells[0].shared_formula_index, Some(0));
assert_eq!(parsed.rows[0].cells[0].shared_formula_ref.as_ref().map(|r| (r.first_row, r.last_row, r.first_col, r.last_col)), Some((6, 7, 4, 4)));
assert_eq!(parsed.rows[1].cells[0].formula, None);
assert_eq!(parsed.rows[1].cells[0].shared_formula_index, Some(0));
}
}