rxls
Release status: this source tree targets the
0.1.2compatibility candidate. Untilv0.1.2is tagged and published, the crates.io badge continues to describe the latest public release. Remaining hosted and publication gates are tracked inROADMAP-0.1.2.md.
Native Rust spreadsheet toolkit. It reads .xls (BIFF8/5/7), .xlsx,
.xlsb, and .ods into one typed cell model; writes styled .xlsx;
and package-preservingly edits .xlsx/.xlsm. No JVM, Apache POI, or
subprocess is required. Malformed input returns a typed error instead of
panicking when bounded recovery is not possible.
Install
Add the library after 0.1.2 is published:
Install the CLI from the same exact release:
The minimum supported Rust version is 1.85. Core library use does not invoke Java, Excel, LibreOffice, Python, or any other subprocess.
Library quick start
// Plain text (search/indexing):
let bytes = read?;
let text = extract_text?;
// Typed cells (structured reading):
let wb = open?;
for sheet in &wb.sheets
CLI
The installed CLI exposes bounded human-readable inspection, stable diagnose JSON, CSV export, package inspection, and comparison commands:
Successful --help and command output go to stdout. Usage and operational
errors go to stderr with the stable exit classes documented in
OUTPUT-CONTRACTS.md.
Cargo features
| Feature | Default | Surface |
|---|---|---|
cli |
Yes | Builds the rxls binary |
xlsx |
Yes | XLSX/XLSM reading, XLSX writing, and package-preserving editing |
xlsb |
No | XLSB reader; enables xlsx package support |
ods |
No | ODS reader |
serde |
No | Typed row deserialization |
chrono |
No | Date/time and duration conversions |
full |
No | All library format/data features; intentionally excludes cli |
The legacy XLS reader is always available. Features are additive. For example,
use default-features = false for an XLS-only library build, or
features = ["full"] for every reader and typed-data helper.
Examples
cargo run -p rxls --bin rxls -- --version
cargo run -p rxls --example extract -- book.xls
cargo run -p rxls --example metadata -- book.xlsx
cargo run -p rxls --example author_report -- report.xlsx
cargo run -p rxls --example robustness -- suspicious.xls
How it works
.xls is an OLE2 compound file whose Workbook stream is a sequence of BIFF
records. rxls:
- opens the container (
cfb) and reads theWorkbook(BIFF8) orBook(BIFF5/7) stream; - walks the record stream, tracking the globals and per-sheet substreams, and
detects the BIFF generation from the first
BOF; - for BIFF8, decodes the shared string table (SST) — including strings that
span
CONTINUErecords and re-specify their compression at the boundary; - for BIFF5/7, decodes 8-bit strings in the workbook's ANSI codepage (the
CODEPAGErecord) — so Korean cp949, Japanese cp932, etc. come out as real text rather than mojibake (via [encoding_rs]); - decodes cell records (
LABELSST,LABEL,RSTRING,RK,MULRK,NUMBER,BOOLERR, andFORMULA+ cachedSTRING) into typed cells ([Cell]:Text/Number/Date/Bool/Error), exposed per coordinate (Sheet::cell/cells/dimensions) and flattened to tab-joined rows byto_text.
For BIFF5/7, declarations 949 (Windows Korean/UHC) and 51949 (EUC-KR)
share encoding_rs's Windows-949-compatible decoder. Missing or unknown
codepages fall back to Windows-1252, malformed byte sequences become U+FFFD,
and [Workbook::open_with_codepage] can override a missing or incorrect
declaration. BIFF8 strings are Unicode and do not use this fallback.
Modern .xlsx (OOXML) is read too (default xlsx feature): Workbook::open
auto-detects OLE2 .xls vs ZIP .xlsx and produces the same typed cells / text.
xlsx cell data, shared strings, and number formats (for dates) are parsed via
zip + quick-xml; default-features = false drops both deps for an
.xls-only build.
Unsupported password-protected workbooks (FILEPASS) are reported as
Error::Encrypted rather than emitting ciphertext. Legacy XOR (Method 1)
workbooks using Excel's default VelvetSweatshop password are deobfuscated.
Every read is bounds-checked. Malformed structures are either handled by an
explicit bounded recovery path or return an [Error], never a panic.
Choosing a crate
calamine is the established choice when
reader maturity and ecosystem adoption are the main criteria. rxls is aimed at
applications that also need styled .xlsx generation, package-preserving
.xlsx/.xlsm edits, bounded formula evaluation, or the built-in export and
diagnostic surfaces. The public corpus results below describe rxls; they are
not presented as a current head-to-head benchmark against another crate.
Security/resource limits, absolute performance ceilings, and same-SHA
reproducibility thresholds are defined in
PERFORMANCE.md; release dependency policy is enforced by
deny.toml, CodeQL, fuzz smoke/scheduled jobs, and a deterministic CycloneDX
dependency manifest.
Scope & parity
Targets plain-text extraction for search/indexing. Date/time serials and
percentages are rendered as Excel displays them (via XF/FORMAT/DATEMODE
for Excel files and ODS value-type fallbacks when no display paragraph is
present); other cached cell values are emitted as text. Formula re-evaluation is
limited to the deterministic MVP exposed by Workbook::evaluate_cell, which
returns a typed FormulaUnsupportedReason (unsupported/volatile function,
external reference, circular reference, unresolved name, oversized range,
missing sheet, …) instead of guessing when a formula falls outside that MVP;
full custom number-format rendering and styling are out of scope.
Editing existing files is package-preserving and .xlsx/.xlsm-only.
Spreadsheet supports atomic batches; cell/formula and range edits; document,
name, sheet, layout, pane, and print-area metadata; sheet add/rename/delete;
merges; legacy notes; hyperlinks; exact-range validations; and safe bottom-row
resizing of existing tables. Untouched declared parts round-trip byte-for-byte,
including retained VBA content. .xls, .xlsb, .ods, and metadata-lossy
OOXML packages are read-only through this API. The complete method-by-method
atomicity, preservation, rejection, and explicit non-goal boundary is frozen in
EDITING-CONTRACT.md; notably, rxls does not insert or
delete rows or columns or guess how to repair unsafe structural dependencies.
A worksheet can also be exported directly to CSV, HTML, or
Markdown (Sheet/Workbook::to_csv/to_html/to_markdown), and a whole
workbook can be summarized as machine-readable JSON via WorkbookReport —
sheet/cell/formula counts, document properties, and a feature inventory,
surfaced on the CLI as rxls diagnose <file> (and rxls csv <file> for
direct CSV export). The portable adapter in src/wasm.rs is exposed to
JavaScript by the isolated bindings/wasm cdylib; the native rxls CLI
binary itself lives behind the cli feature (on by default, so existing
native workflows are unaffected). Determinism, CSV safety options, diagnose JSON
schema compatibility, CLI exit codes, and bounded-output guidance are defined in
OUTPUT-CONTRACTS.md. The Rust API inventory, coordinate
rules, feature guarantees, and 0.1.2-to-1.0 compatibility policy are in
API-COMPATIBILITY.md; the expected no-source-change
upgrade is summarized in MIGRATION-1.0.md.
The WASM distribution provides generated Node and browser entry points,
TypeScript declarations, a minimal file-picker demo, structured RxlsError
objects, and a synchronous 32 MiB input limit. Build it with
bash scripts/build-wasm-package.sh; the CI release gate executes Node and
Chromium smoke tests, compares reportJson with rxls diagnose, and enforces
raw WASM, JavaScript glue, and compressed npm bundle budgets. See the
WASM package guide
for initialization and memory guidance.
Current public-corpus gate (2026-07-15). The pinned fetch recipe selects 916
files from Apache POI and calamine at immutable upstream commits: 448 .xls,
413 .xlsx, 18 .xlsm, 21 .xlsb, and 16 .ods. rxls corpus-report opens
869; the remaining 47 are explicit expected rejections for encrypted input,
unsupported legacy BIFF, malformed containers, or structurally invalid BIFF streams.
The report records 0 unexpected failures and 0 unexpected accepts. Public visible-value checks report:
| Format | Comparable files | Result |
|---|---|---|
.xls vs xlrd |
414 | 100.000% mean parity; 414/414 at least 99% |
.xlsx/.xlsm vs openpyxl |
388 | 99.889% mean parity; 387/388 at least 99% |
.xlsb vs pyxlsb plus committed residual oracles |
18 | 100.000% mean parity |
.ods vs bounded ODF XML visible-text oracle |
14 | 100.000% mean recall |
The release claim depends only on public, reproducible fixtures and corpora. GitHub Actions runs formatting, clippy, the feature/MSRV matrix, Rust and Python harness tests, documentation, package checks, and the small pinned CI corpus. The broader 916-file run is reproducible on demand with the commands below.
Reproduce
Everything below runs from a clean checkout — no private data.
RXLS_REQUIRE_OPENPYXL=1
RUSTDOCFLAGS="-D warnings"
To test the exact packaged crate as both an external Rust dependency and a
cargo install CLI—entirely outside the checkout—run:
After publication, exercise the same consumer, install, version, help, diagnose, and invalid-usage contracts through crates.io with:
Maintainers create two clean Release workflow-dispatch candidates from the
same commit. The second receives the first run's baseline_run_id; the
fail-closed bundle comparator requires deterministic artifacts to be identical
and explains permitted test-duration and successful fuzz-log differences.
Timing, RSS, and edit-output variation must remain inside the documented
same-SHA reproducibility/noise limits; the absolute budgets remain the
performance regression guard. Tag publication is allowed only after that report
and every hosted gate pass. The second candidate emits an immutable
exact-SHA attestation that also binds the candidate release-manifest digest.
The tag-triggered job requires successful exact-SHA CI and CodeQL push runs,
downloads the attested candidate, and fails before publishing unless its own
47-file bundle compares cleanly. Post-publication verification downloads every
release asset and validates full manifest coverage and checksums. See
CONTRIBUTING.md for the exact sequence.
Pinned public spreadsheet corpus for parity work:
|
|
|
|
|
Each parity report records the oracle reader and installed version plus the
SHA-256 of the exact input manifest bytes. Directory-only development runs
explicitly report input_manifest_sha256=none; release evidence always uses
the pinned manifest.
The dry run should report 916 files (.xls 448, .xlsx 413, .xlsm 18,
.xlsb 21, .ods 16). Files download into gitignored local/public-corpus; this repo
commits the pinned recipe and docs, not the corpus payloads.
Authoring (writing .xlsx)
Beyond reading, rxls builds styled .xlsx from data — no JVM, no template:
use ;
let mut wb = new;
let sheet = wb.add_sheet;
let header = new.bold.fill.align.wrap;
sheet.write_styled;
sheet.write_styled;
sheet.write_url;
sheet.write_styled;
sheet.set_col_width;
sheet.freeze_panes;
sheet.autofilter;
write?;
Supports per-cell font (family/size/color/bold/italic/underline and
strikethrough), fill, borders, number formats, alignment + wrap, merged ranges,
column widths/row heights, frozen panes, autofilters, external hyperlinks,
page setup (orientation/margins/print-area/
repeat rows/columns/headers-footers), sheet protection (including cell-level
Format protection), tab color, data validation (dropdowns +
numeric/date rules), conditional formatting (cellIs / color scales / data
bars), images (PNG/JPEG), charts (bar/line/pie/scatter), sparklines,
worksheet tables (including named table header formats), rich strings
(including cell-level Format), and
legacy comments/notes. Styles are
interned into deduped OOXML resource tables; writer features are validated by
in-tree openpyxl gates. (Pivot tables, threaded comments, and macros are out
of scope.)
Stability
Version 0.1.2 is the 1.0 compatibility candidate. Its public API and documented
semantics target a zero-breaking-change transition to 1.0; additive APIs and
#[non_exhaustive] variants may still be introduced. Pin 0.1.2 during the
observation period if an application requires an exact dependency graph. One
deliberate design choice to be aware of: a single model serves both reading
and authoring. Readers populate the documented cross-format subset of layout,
style, and view metadata, but this is not a promise that every authoring setter
is reconstructed as a complete writer template; see the
reader-fidelity matrix. The reader also surfaces
merged ranges (Sheet::merged_ranges()),
from .xls MERGECELLS / .xlsx <mergeCells>) and best-effort formula text for
.xlsx, .xls, .xlsb, and .ods (Cell::Formula, with the cached value
retained). Read-discovered merges are tracked separately from authoring merges
so reading them never alters write output. Workbook-global user defined names
are surfaced for .xlsx, .xls, .xlsb, and .ods named ranges via
Workbook::defined_names(), and .xlsx/.xlsb package document properties,
.xls OLE properties, and .ods meta.xml populate Workbook::properties.
Sheet visibility is surfaced across the read formats, including .ods table
styles where table:display="false" maps to Sheet::is_hidden().
Hyperlinks from OOXML relationships, XLSB BrtHLink records, BIFF HLINK
records, and ODS text:a links populate Sheet::hyperlinks().
OOXML comments, XLSB comments parts, BIFF Note / TxO records, and ODS
office:annotation metadata populate Sheet::comments().
OOXML dataValidations, XLSB BrtDVal / BrtDValList, BIFF Dv records, and
ODS table:content-validation metadata populate Sheet::data_validations();
ODS conditions are preserved as custom validation formulas.
OOXML tables, XLSB binary table parts, and named ODS table:database-range
blocks populate
Sheet::tables() and workbook-level table lookup helpers
Workbook::table_names(), Workbook::table_names_in_sheet(), and
Workbook::table_by_name().
OOXML sheet views, XLSB BrtBeginWsView / BrtPane records, and BIFF
WINDOW2 / PANE records populate Sheet::sheet_view().
OOXML autoFilter, XLSB BrtBeginAFilter, BIFF _FilterDatabase, and ODS
table:database-range metadata populate Sheet::autofilter_range(). BIFF
Print_Area sheet-local built-in names and ODS table:print-ranges metadata
populate Sheet::page_setup().print_area, ODS table:table-header-rows
metadata populates Sheet::page_setup().repeat_rows, ODS
table:table-header-columns metadata populates
Sheet::page_setup().repeat_cols, and BIFF/XLSB page setup records populate
orientation, margins, scaling, centering, header, and footer fields.
OOXML worksheet charts are surfaced as anchored Sheet::charts() metadata that
maps to the writer chart model, including axis titles.
OOXML worksheet images and ODS draw:image package parts are surfaced through
Sheet::images(), with Workbook::pictures() providing a calamine-style
workbook aggregate of image extensions and bytes.
The worksheet_range facade exposes rectangular row views with absolute row and
column bounds and, with the optional serde feature, typed row deserialization
including
RangeDeserializerBuilder::with_header_row(row),
RangeDeserializerBuilder::with_deserialize_headers::<T>(), and raw Cell
rows for callers that want the exact Text / Number / Date / Bool /
Formula model instead of coercing into primitive fields.
Range::used_cells() reports calamine-style relative coordinates;
Range::used_cells_abs() keeps worksheet coordinates available. Formula ranges expose
the same rectangular lookup,
relative/absolute used-cell iteration, and allocation-free row_views() scan
surface with the same absolute row and column bounds for formula source text.
Numeric deserialize_with helpers keep invalid numeric cells non-fatal during
typed ingestion.
Calamine-style workbook helpers include worksheet_range_at, worksheets,
worksheet_formula, and sheets_metadata (SheetType + SheetVisible).
With the optional chrono feature, Excel date serials can also be converted
directly to chrono::NaiveDateTime via excel_serial_to_naive_datetime or
Cell::as_naive_datetime, with Cell::as_naive_date and
Cell::as_naive_time available when callers only need one component. Duration
serials can be converted to chrono::Duration via
excel_serial_to_duration or Cell::as_duration.
Cell::get_datetime() exposes the raw Excel serial for date/time cells when
callers want calamine-style typed access without choosing the workbook date
system yet.
0.1.2 candidate status
The local implementation and evidence scope is complete:
- BIFF/XLSB formula source, external-name provenance, shared/array formulas, and deterministic evaluation have independent source and cached-value tests.
- Reader fidelity, codepage, metadata, and explicit loss boundaries are frozen
in
docs/READER_FIDELITY.md. - Package-preserving XLSX/XLSM editing covers the declared cell, sheet,
transaction, merge, layout, note, hyperlink, validation, table-resize, and
atomic-save surface in
EDITING-CONTRACT.md. - Output, CLI/JSON, API/SemVer, MSRV, WASM packaging, security, fuzz, performance, package, SBOM, and public-corpus evidence gates are implemented and have local evidence.
- Strict invalid-edit rejection, authored/edited XLSX and edited XLSM LibreOffice smokes, same-SHA performance reproducibility comparison, and exact-SHA tag attestation enforcement are covered locally.
Publication is operationally gated on one immutable candidate commit, two clean
hosted candidates, v0.1.2, crate publication, the npm-compatible WASM archive
as a GitHub asset, and verification of crates.io, docs.rs, assets, and
checksums, including install-and-execute verification of the downloaded WASM
archive through Node package resolution and a real browser. The authoritative
checklist and local-versus-external evidence rules are in
ROADMAP-0.1.2.md.
Contributing
See CONTRIBUTING.md. The local gate is documented there and enforced by GitHub Actions.
License
Licensed under the MIT License. Third-party dependency licenses are listed in THIRD_PARTY_LICENSES.md. This crate implements only the publicly documented MS-XLS, MS-XLSB, MS-CFB, ECMA-376, and ODF specifications and contains no Microsoft source.
Microsoft and Excel are trademarks of the Microsoft group of companies. This project is not affiliated with, endorsed by, or sponsored by Microsoft.