#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![warn(missing_debug_implementations, rust_2018_idioms)]
mod error;
mod eval;
mod format;
mod model;
#[cfg(feature = "ods")]
mod ods;
mod ole;
#[cfg(feature = "xlsx")]
mod package;
mod ptg;
mod report;
#[cfg(feature = "xlsx")]
mod spreadsheet;
mod sst;
pub mod wasm;
#[cfg(feature = "xlsx")]
mod write;
mod xls;
#[cfg(feature = "xlsb")]
mod xlsb;
#[cfg(feature = "xlsx")]
mod xlsx;
#[cfg(feature = "xlsx")]
mod xmltree;
pub use error::{Error, Result};
pub use eval::{FormulaEvaluation, FormulaUnsupportedReason};
#[cfg(all(feature = "serde", feature = "chrono"))]
pub use model::{
deserialize_as_date_1900_or_none, deserialize_as_date_1900_or_string,
deserialize_as_date_1904_or_none, deserialize_as_date_1904_or_string,
deserialize_as_datetime_1900_or_none, deserialize_as_datetime_1900_or_string,
deserialize_as_datetime_1904_or_none, deserialize_as_datetime_1904_or_string,
deserialize_as_duration_or_none, deserialize_as_duration_or_string,
deserialize_as_time_1900_or_none, deserialize_as_time_1900_or_string,
deserialize_as_time_1904_or_none, deserialize_as_time_1904_or_string,
};
#[cfg(feature = "serde")]
pub use model::{
deserialize_as_f64_or_none, deserialize_as_f64_or_string, deserialize_as_i64_or_none,
deserialize_as_i64_or_string, DeError, RangeDeserializer, RangeDeserializerBuilder,
};
pub use model::{
excel_serial_to_datetime, Alignment, Border, BorderStyle, Cell, CellErrorType, CellProtection,
CellStyle, CfRule, Chart, ChartKind, Color, Comment, CondFormat, Data, DataRef, DataType,
DataValidation, Dimensions, DocProperties, DvKind, DvOp, ExcelDateTime, Fill, Font, Format,
FormatAlign, FormatBorder, FormatPattern, FormatScript, FormulaRange, FormulaRangeRow,
FormulaRangeRowCells, FormulaRangeRows, HAlign, HeaderRow, Image, ImageFmt, PageSetup, Picture,
ProtectionOptions, Range, RangeRow, RangeRowCells, RangeRows, Reader, Series, Sheet,
SheetMetadata, SheetType, SheetView, SheetVisible, Sparkline, SparklineKind, Table, TextRun,
VAlign, Workbook, WorkbookMetadata,
};
#[cfg(feature = "chrono")]
pub use model::{excel_serial_to_duration, excel_serial_to_naive_datetime};
pub use report::WorkbookReport;
#[cfg(feature = "xlsx")]
pub use spreadsheet::{EditCapability, EditReadOnlyReason, Spreadsheet};
#[cfg(feature = "xlsx")]
pub use write::WriteError;
#[cfg(any(feature = "xlsx", feature = "ods"))]
pub(crate) use model::CellEntry;
pub(crate) const MAX_TEXT_BYTES: usize = 256 << 20;
#[cfg(any(feature = "xlsx", feature = "ods", test))]
pub(crate) const MAX_XML_GENERAL_REFS: usize = 1 << 20;
#[cfg(any(feature = "xlsx", feature = "ods", test))]
pub(crate) fn xml_reference_work_within_budget(xml: &str) -> bool {
xml_reference_bytes_within_budget(xml.as_bytes())
}
#[cfg(any(feature = "xlsx", feature = "ods", test))]
pub(crate) fn xml_reference_bytes_within_budget(xml: &[u8]) -> bool {
xml_reference_work_within_limit(xml, MAX_XML_GENERAL_REFS)
}
#[cfg(any(feature = "xlsx", feature = "ods", test))]
fn xml_reference_work_within_limit(xml: &[u8], limit: usize) -> bool {
let mut remaining = limit;
for &byte in xml {
if byte == b'&' {
if remaining == 0 {
return false;
}
remaining -= 1;
}
}
true
}
impl Workbook {
pub fn open(bytes: &[u8]) -> Result<Self> {
#[cfg(feature = "xlsb")]
if xlsb::is_xlsb(bytes) {
return xlsb::open(bytes);
}
#[cfg(feature = "ods")]
if ods::is_ods(bytes) {
return ods::open(bytes);
}
#[cfg(feature = "xlsx")]
if xlsx::is_xlsx(bytes) {
return xlsx::open(bytes);
}
Self::open_with_codepage(bytes, None)
}
#[cfg(feature = "xlsx")]
pub fn to_xlsx(&self) -> Vec<u8> {
write::to_xlsx(self)
}
}
pub fn extract_text(bytes: &[u8]) -> Result<String> {
let text = Workbook::open(bytes)?.text();
if has_indexable(&text) {
Ok(text)
} else {
Err(Error::NoText)
}
}
fn has_indexable(text: &str) -> bool {
text.chars()
.any(|c| c.is_alphanumeric() || ('가'..='힣').contains(&c))
}
pub(crate) fn rk_to_f64(rk: u32) -> f64 {
let val = if rk & 0x02 != 0 {
((rk as i32) >> 2) as f64
} else {
f64::from_bits(u64::from(rk & 0xFFFF_FFFC) << 32)
};
if rk & 0x01 != 0 {
val / 100.0
} else {
val
}
}
pub(crate) fn format_number(f: f64) -> String {
if f == 0.0 {
"0".to_string()
} else if f.is_finite() && f.fract() == 0.0 && f.abs() <= i64::MAX as f64 {
format!("{f:.0}")
} else {
normalize_scientific_exponent(&format!("{f:?}"))
}
}
fn normalize_scientific_exponent(text: &str) -> String {
let Some((mantissa, exponent)) = text.split_once('e') else {
return text.to_string();
};
let (sign, digits) = match exponent.as_bytes().first().copied() {
Some(b'+') => ('+', &exponent[1..]),
Some(b'-') => ('-', &exponent[1..]),
Some(_) => ('+', exponent),
None => return text.to_string(),
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return text.to_string();
}
let mut out = String::with_capacity(mantissa.len() + 2 + digits.len().max(2));
out.push_str(mantissa);
out.push('e');
out.push(sign);
if digits.len() == 1 {
out.push('0');
}
out.push_str(digits);
out
}
pub(crate) fn error_code(v: u8) -> &'static str {
match v {
0x00 => "#NULL!",
0x07 => "#DIV/0!",
0x0F => "#VALUE!",
0x17 => "#REF!",
0x1D => "#NAME?",
0x24 => "#NUM!",
0x2A => "#N/A",
0x2B => "#GETTING_DATA",
_ => "#ERR!",
}
}
#[cfg(test)]
mod tests {
#[test]
fn xml_reference_work_budget_rejects_one_past_the_limit() {
assert!(super::xml_reference_work_within_budget("&"));
assert!(super::xml_reference_bytes_within_budget(b"&"));
assert!(super::xml_reference_work_within_limit(b"&<", 2));
assert!(!super::xml_reference_work_within_limit(b"&<>", 2));
}
#[test]
fn format_number_keeps_signed_zero_normalized() {
assert_eq!(super::format_number(0.0), "0");
assert_eq!(super::format_number(-0.0), "0");
}
#[test]
fn format_number_uses_scientific_notation_for_tiny_general_values() {
let tiny_values = [
("3.0000000000000002E-104", "3e-104"),
("4.9999999999999998E-104", "5e-104"),
("4.9999999999999998E-106", "5e-106"),
];
for (source, expected) in tiny_values {
let value = source.parse::<f64>().unwrap();
assert_eq!(super::format_number(value), expected);
}
}
#[test]
fn format_number_uses_python_style_scientific_exponents() {
let values = [
("1.23456789E-05", "1.23456789e-05"),
("-1.23456789E-05", "-1.23456789e-05"),
("1.2345678E142", "1.2345678e+142"),
("1.0E20", "1e+20"),
];
for (source, expected) in values {
let value = source.parse::<f64>().unwrap();
assert_eq!(super::format_number(value), expected);
}
}
}