rightkit-docs 0.1.0

Product-neutral DOCX, XLSX and PDF read and write: format detection from content, typed reads with caps, typed write ops through an atomic writer, honest loss receipts, hardened against hostile files
Documentation

rightkit-docs

Product-neutral read and write for DOCX, XLSX and PDF, behind one typed API, for apps and agents that must not trust the files they open. Pure Rust: no Word, no Excel, no LibreOffice, no network.

Use it to read a document into typed content with caps, to create a DOCX or XLSX from typed blocks and cells, or to edit an existing file with typed ops and get a receipt that says what the edit could not keep.

Do not use it to render pages, to recalculate formulas, to open password-protected or macro-enabled files, or for PDF page graphs and font work (that is rightkit-pdf, which this crate wraps for PDF).

Capability: document-io; rightkit owns docx|xlsx|document|spreadsheet resolve to this crate and rightkit owns pdf resolves to rightkit-pdf. Choice of engine and evidence: BAKEOFF.md.

API

use rightkit_docs::{read, write, Block, Op, ReadOptions, Source, WriteOptions};

let path = std::path::Path::new("report.docx");
write(path, &[Op::CreateDocx { blocks: vec![Block::Heading { level: 1, text: "Q3".into() }] }],
      &WriteOptions::default())?;
let doc = read(Source::Path(path), &ReadOptions::default())?;          // format from content
write(path, &[Op::ReplaceText { find: "Q3".into(), replace: "Q4".into() }],
      &WriteOptions { expect_sha256: Some(doc.sha256), ..Default::default() })?;

read(source, &ReadOptions) -> DocumentRead and write(target, &[Op], &WriteOptions) -> WriteReceipt. Every type is serde (camelCase, kind/type discriminators) and every error is a DocsError with a stable snake_case code() and a { code, message } serialisation, like rightkit-pdf's PdfEditError.

Reading

Format Content Renderings
DOCX paragraphs with style display names (heading 1) and a list flag, tables, headers, footers plain text, Markdown (headings from style names, - for list items, pipe tables)
XLSX sheets with state (visible, hidden, veryHidden), used range, non-empty cells as text, number, bool, dateTime (serial + ISO 8601), duration, error, with the formula and its cached result tab-separated text, Markdown tables per sheet
PDF page count and text per page (rightkit-pdf's extract) plain text, Markdown with ## Page N

Selection and caps: pages (PDF, 1-based), sheets and an A1 range (XLSX), max_chars and max_cells; any cap that cuts the result sets truncated. sha256 is the hash of the exact bytes read.

omitted names everything the file holds that content does not: comments, tracked changes (inserted text is included, deleted text is not), footnotes and endnotes, images, charts, pivot tables, external links (never followed), drawings, list numbers, scanned PDF pages. Nothing is dropped without being named.

Writing

A call applies its ops in order to one in-memory document and publishes once, through rightkit_fs::AtomicWriter.

Op Format What it does
CreateDocx { blocks } DOCX new file from Heading, Paragraph, Bullet, Numbered (each run restarts at 1), Table, PageBreak; first op only
CreateXlsx { sheets } XLSX new workbook; cells are Text, Number, Bool, Formula, Blank; first op only
ReplaceText { find, replace } DOCX, PDF DOCX: body, headers and footers, across run boundaries, special characters escaped; PDF: source text through rightkit-pdf's compose; zero matches is not_found, never a silent no-op
AppendBlocks { blocks } DOCX before the body's final section properties
SetCells { sheet, cells } XLSX value and formula kept explicit; an overwritten cell keeps its style
AppendRows { sheet, rows } XLSX after the last row that holds a value
AddPdfText { page, x, y, text, size_pt } PDF text in the bundled Source Serif 4
SelectPdfPages { pages } PDF keep these pages, in this order, repeats allowed

PDF create-from-nothing is not in v1.

WriteOptions: expect_sha256 (an edit refuses with source_changed unless the target's current hash matches), overwrite (a create over an existing file), limits.

Publishing. The result is re-detected as the same format before anything is written. The write goes to a temp sibling, is flushed, and a last source-hash recheck runs immediately before the rename; a file changed meanwhile is never overwritten (source_changed), and a create never clobbers a file that appears first (target_exists). Permission bits of the replaced file are kept; a symlink target is replaced where it points and the link survives. A failure before the rename leaves the original and no temp file. The recheck narrows, it does not exclude, another program replacing the file between the recheck and the rename.

Honest losses

An OOXML edit does not rebuild the document from a model. It patches exact byte ranges of the parts it touches and copies every other part raw (same bytes, same compression), so comments, tracked changes, footnotes, charts, notes, data validation, merged ranges, defined names, custom XML, extension parts and anything else this crate does not model survive unchanged. WriteReceipt.touched_parts lists the parts rewritten; everything else is byte-identical. What an edit does change is reported:

Where Change Reported as
XLSX workbook flagged to recalculate on open; existing formulas keep their stored (now possibly stale) results until a spreadsheet application opens the file warning
XLSX derived calcChain.xml dropped (the application rebuilds it) loss derived_part_dropped
XLSX rewritten rows lose the optional spans hint; sheet dimension updated none (cosmetic)
XLSX appended rows are not added to a table object's range warning
XLSX a formula cell written by an op has no stored result until recalculated read returns empty value + formula
DOCX replacement inside a document with tracked changes is itself not tracked warning
DOCX occurrences in footnotes, endnotes and comments are not edited warning with the count
DOCX appended heading style missing from the document, or no numbering definitions warning; direct bold formatting or a text marker is used
any OOXML a digital signature is invalidated by any edit loss signature_invalidated
PDF text the font cannot re-encode is not replaced loss not_replaced per page
PDF page selection drops unselected pages and document-level catalog entries loss pages_dropped, warning

A formula cell that is the master of a shared formula, or an array formula, is refused (invalid_op) rather than overwritten.

Safety

All of it is inside this crate, per call, before a parser sees a byte. Defaults are Limits::default(); raise them per call, never process-wide.

Guard Default Error
compressed input size 64 MiB too_large
entries in a package 10 000 archive_limit
decompressed size, per part / total 64 MiB / 256 MiB archive_limit
compression ratio (parts over 1 MiB; package over 4 MiB) 200:1 archive_limit
XML nesting depth 256 malformed
PDF pages / stream decode while loading 5 000 / 64 MiB per stream archive_limit / malformed
unsafe entry names (.., absolute, drive letters), case-insensitive duplicates refused malformed
DOCTYPE, entity declarations, UTF-16 XML (so no XXE and no entity expansion) refused in every XML part forbidden_xml
.docm .xlsm .dotm .xltm, any VBA project, macro content types refused for read and edit; never opened, run, or stripped macro_enabled
password-protected OOXML (OLE EncryptedPackage), zip-level encryption, encrypted PDF refused encrypted
legacy .doc .xls .ppt, PPTX, XLSB, anything else refused, never guessed unsupported_format
external relationships (linked images, templates, workbook links, hyperlinks) never followed; preserved as written; counted in omitted none
formulas that reach outside the workbook: external workbook references, DDE (|), WEBSERVICE, FILTERXML, CALL, REGISTER, EXEC, file or network locations refused on create and edit unsafe_formula
a cell value starting = + - @ Text is always a string cell; only Formula is a formula none
parser panic on hostile input (XLSX, PDF) caught and reported malformed

Control characters XML cannot carry are dropped from written text.

Limits and gaps

  • PDF decompression is bounded while the document loads (rightkit-pdf's loader bound, per stream). Page content streams are decoded later by pdf-extract (its own lopdf 0.42), which this crate cannot bound further than the 64 MiB input cap.
  • XLSX read goes through calamine, which reads the whole sheet into memory; the per-part cap bounds that.
  • The recheck-before-rename narrows but does not exclude a concurrent writer from another program.
  • XLSX dates are numbers plus ISO 8601; no date-typed writes in v1. 1900 serials below 61 have no ISO form.
  • Tracked-change, comment and chart authoring are out of scope; they are preserved, not created.
  • Hidden-state and 1904-date-system information is reported on read; edits keep it.

Platform support

Pure Rust, macOS and Windows. The only platform-specific behaviour is rightkit-fs's atomic write (and rightkit-pdf's commit code, which this crate does not use). Proofs: see CHANGELOG.

Runtime budget

Class continuous = false: bounded per call, no thread, timer, listener or watcher. The one process-wide object is rightkit-pdf's font catalog, built once on the first PDF text replace or add.