rxls 0.1.2

Native Rust spreadsheet library: reads .xls (BIFF8/5/7), .xlsx, .xlsb, .ods and writes .xlsx. Typed cells, formulas, panic-free, no JVM/POI.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# rxls

[![Crates.io](https://img.shields.io/crates/v/rxls.svg)](https://crates.io/crates/rxls)
[![Docs.rs](https://docs.rs/rxls/badge.svg)](https://docs.rs/rxls)
[![CI](https://github.com/HyunjoJung/rxls/actions/workflows/ci.yml/badge.svg)](https://github.com/HyunjoJung/rxls/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
![MSRV](https://img.shields.io/badge/MSRV-1.85-orange.svg)

> **Release status:** this source tree targets the `0.1.2` compatibility
> candidate. Until `v0.1.2` is tagged and published, the crates.io badge
> continues to describe the latest public release. Remaining hosted and
> publication gates are tracked in
> [`ROADMAP-0.1.2.md`]ROADMAP-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:

```sh
cargo add rxls@0.1.2
```

Install the CLI from the same exact release:

```sh
cargo install rxls --version =0.1.2 --locked
rxls --help
```

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

```rust
// Plain text (search/indexing):
let bytes = std::fs::read("book.xls")?;
let text = rxls::extract_text(&bytes)?;

// Typed cells (structured reading):
let wb = rxls::Workbook::open(&bytes)?;
for sheet in &wb.sheets {
    if let Some(rxls::Cell::Date(serial)) = sheet.cell(0, 0) {
        println!("A1 is the Excel date serial {serial}");
    }
    for (row, col, cell) in sheet.cells() {
        // rxls::Cell::{Text(String), Number(f64), Date(f64), Bool(bool), Error(String)}
    }
}
```

## CLI

The installed CLI exposes bounded human-readable inspection, stable diagnose
JSON, CSV export, package inspection, and comparison commands:

```sh
rxls info book.xlsx
rxls diagnose book.xlsx
rxls csv book.xlsx --sheet 0 --max-output-bytes 1048576
rxls compare before.xlsx after.xlsx --limit 50
```

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`](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

```text
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`:

1. opens the container (`cfb`) and reads the `Workbook` (BIFF8) or `Book`
   (BIFF5/7) stream;
2. walks the record stream, tracking the globals and per-sheet substreams, and
   detects the BIFF generation from the first `BOF`;
3. for BIFF8, decodes the **shared string table** (SST) — including strings that
   span `CONTINUE` records and re-specify their compression at the boundary;
4. for BIFF5/7, decodes 8-bit strings in the workbook's ANSI codepage (the
   `CODEPAGE` record) — so Korean **cp949**, Japanese cp932, etc. come out as
   real text rather than mojibake (via [`encoding_rs`]);
5. decodes cell records (`LABELSST`, `LABEL`, `RSTRING`, `RK`, `MULRK`,
   `NUMBER`, `BOOLERR`, and `FORMULA` + cached `STRING`) into **typed cells**
   ([`Cell`]: `Text`/`Number`/`Date`/`Bool`/`Error`), exposed per coordinate
   (`Sheet::cell`/`cells`/`dimensions`) and flattened to tab-joined rows by
   `to_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`](https://crates.io/crates/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`](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`](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`](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`](API-COMPATIBILITY.md); the expected no-source-change
upgrade is summarized in [`MIGRATION-1.0.md`](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](https://github.com/HyunjoJung/rxls/blob/main/bindings/wasm/npm/README.md)
for initialization and memory guidance.

<!-- public-corpus-baseline:start -->
**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 |
<!-- public-corpus-baseline:end -->

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.

```bash
python3 -m pip install "xlrd==2.0.2" "openpyxl==3.1.5" "pyxlsb==1.0.10" "odfpy==1.4.1"
python3 scripts/public_hygiene_audit.py
cargo fmt --all -- --check
cargo clippy --all-targets --all-features --locked -- -D warnings
RXLS_REQUIRE_OPENPYXL=1 cargo test --all-targets --all-features --locked
cargo test --no-default-features --all-targets --locked
cargo test --doc --all-features --locked
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features --locked
python3 -m unittest discover -s scripts -p "test_*.py"
cargo package --locked
cargo publish --dry-run --locked
```

To test the exact packaged crate as both an external Rust dependency and a
`cargo install` CLI—entirely outside the checkout—run:

```sh
cargo package --locked
python3 scripts/smoke_crate_distribution.py \
  --crate target/package/rxls-0.1.2.crate \
  --fixture tests/fixtures/xlsx/reader-structural.xlsx
```

After publication, exercise the same consumer, install, version, help,
diagnose, and invalid-usage contracts through crates.io with:

```sh
python3 scripts/smoke_crate_distribution.py \
  --registry-version 0.1.2 \
  --fixture tests/fixtures/xlsx/reader-structural.xlsx
```

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](CONTRIBUTING.md) for the exact sequence.

Pinned public spreadsheet corpus for parity work:

```bash
python3 scripts/fetch-public-corpus.py --dry-run
python3 scripts/fetch-public-corpus.py
cargo build --all-features --example extract --locked
cargo run --bin rxls --all-features --locked -- corpus-report local/public-corpus/manifest.json | tee target/release-corpus-report.txt
python3 scripts/xls-xlrd-parity.py --manifest local/public-corpus/manifest.json --bin target/debug/examples/extract --corpus-report target/release-corpus-report.txt --min 0.99 --show-worst 20 --show-skips 200 | tee target/release-xls-parity-full.txt
python3 scripts/xlsx-openpyxl-parity.py --manifest local/public-corpus/manifest.json --bin target/debug/examples/extract --corpus-report target/release-corpus-report.txt --min 0.99 --show-worst 20 --show-skips 200 | tee target/release-ooxml-parity-full.txt
python3 scripts/xlsb-pyxlsb-parity.py --manifest local/public-corpus/manifest.json --bin target/debug/examples/extract --expected-values tests/oracles/xlsb-visible-values.json --corpus-report target/release-corpus-report.txt --min 0.99 --show-skips 200 | tee target/release-xlsb-parity-full.txt
python3 scripts/ods-odfpy-parity.py --manifest local/public-corpus/manifest.json --bin target/debug/examples/extract --corpus-report target/release-corpus-report.txt --min 0.99 --show-skips 200 | tee target/release-ods-parity-full.txt
python3 scripts/verify_public_baseline.py --corpus-report target/release-corpus-report.txt --xls target/release-xls-parity-full.txt --ooxml target/release-ooxml-parity-full.txt --xlsb target/release-xlsb-parity-full.txt --ods target/release-ods-parity-full.txt --readme README.md
```

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:

```rust
use rxls::{Cell, CellStyle, HAlign, Workbook};

let mut wb = Workbook::new();
let sheet = wb.add_sheet("입찰공고");

let header = CellStyle::new().bold().fill([0xDD, 0xEB, 0xF7]).align(HAlign::Center).wrap();
sheet.write_styled(0, 0, "공고명", &header);
sheet.write_styled(0, 1, "추정가격", &header);

sheet.write_url(1, 0, "https://www.g2b.go.kr/...", "뉴미디어 콘텐츠 제작");
sheet.write_styled(1, 1, 150_000_000.0, &CellStyle::new().num_fmt("₩#,##0"));

sheet.set_col_width(0, 42.0);
sheet.freeze_panes(1, 0);
sheet.autofilter(0, 0, 1, 1);

std::fs::write("report.xlsx", wb.to_xlsx())?;
```

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](docs/READER_FIDELITY.md). 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`]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`]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`](ROADMAP-0.1.2.md).

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). The local gate is
documented there and enforced by GitHub Actions.

## License

Licensed under the [MIT License](LICENSE). Third-party dependency licenses are
listed in [THIRD_PARTY_LICENSES.md](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.

[MS-XLS]: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/
[MS-XLSB]: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xlsb/
[MS-CFB]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/
[ECMA-376]: https://ecma-international.org/publications-and-standards/standards/ecma-376/
[ODF]: https://docs.oasis-open.org/office/OpenDocument/v1.3/