xlsxparser 0.10.1

A lightweight, high-performance .xlsx (OOXML) parser library
Documentation
# `parse/shared_strings.rs` Design Doc

*[日本語](shared_strings.md)*

Design doc for `src/parse/shared_strings.rs`. Per [architecture.md](../architecture.en.md), this implements the `parse/` responsibility "parsing `sharedStrings.xml` (extracting the SST's structured data)." It parses `xl/sharedStrings.xml` and builds the `SharedStringTable` consumed by [resolve/shared_strings.md](../resolve/shared_strings.en.md). It also resolves that file's [Open Question 1](../resolve/shared_strings.en.md) ("`SharedStringTable`'s type and location are to be settled when `parse/shared_strings.rs` is designed").

## Responsibility / Scope

- Parses `xl/sharedStrings.xml`'s `<sst><si>...</si>...</sst>` and builds a `SharedStringTable` that holds strings in `<si>`'s source order (i.e. shared-string index order)
- For each `<si>`, resolves to a single `String` containing only the concatenated text — whether it's a plain `<t>...</t>` or rich-text runs (`<r><t>...</t></r>...`) — discarding formatting info (`<rPr>`), matching how [model/cell.md](../model/cell.en.md)'s `CellValue::Text` holds only a plain string
- Avoids accidentally concatenating `<t>` nested under `<rPh>` (phonetic annotations — furigana Excel sometimes auto-generates for Japanese names, place names, etc.) into the main text
- Preserves leading/trailing whitespace for a `<t>` carrying `xml:space="preserve"` (implements requirements chapter 4's "must honor `xml:space=\"preserve\"`")
- **Not responsible for**: resolving a `t="s"` cell's index into the actual string ([`resolve/shared_strings.rs`](../resolve/shared_strings.en.md) — this file only builds the table that resolution uses), resolving formula-result strings (`t="str"`) or inline strings (`t="inlineStr"`) (handled directly during streaming by [`parse/worksheet.rs`](worksheet.en.md); see [resolve/shared_strings.md](../resolve/shared_strings.en.md))

## Key Types / Functions (draft)

```rust
use crate::error::Error;
use crate::parse::{concat_rich_text, convert_xml_error, create_secure_reader};
use std::io::BufRead;
use std::sync::Arc;

/// The shared string table. [resolve/shared_strings.rs](../resolve/shared_strings.en.md)
/// uses it to resolve `t="s"` cells' indices (this resolves that file's
/// Open Question 1).
#[derive(Debug, Clone, Default)]
pub(crate) struct SharedStringTable {
    strings: Vec<Arc<str>>,
}

impl SharedStringTable {
    /// Looks up the string at an index. Used by `resolve/shared_strings.rs::resolve`
    /// to build `Error::SharedStringIndexOutOfBounds` when out of range.
    pub(crate) fn get(&self, index: usize) -> Option<&Arc<str>> {
        self.strings.get(index)
    }

    pub(crate) fn len(&self) -> usize {
        self.strings.len()
    }
}

/// Parses `xl/sharedStrings.xml` and builds a `SharedStringTable`.
pub(crate) fn parse_shared_strings(reader: impl BufRead, path: &str) -> Result<SharedStringTable, Error> {
    let mut xml_reader = create_secure_reader(reader);
    let mut buf = Vec::new();
    let mut strings = Vec::new();
    // Implementation plan: for each <si> start event directly under <sst>,
    // call concat_rich_text and push the resulting string onto `strings` via
    // Arc::from(). An <si> may be rich text with multiple <r>, a single
    // <t>, or hold no text at all (an empty <si/>).
    let _ = (&mut xml_reader, path, &mut buf, &mut strings);
    unimplemented!()
}
```

## Dependencies

- Depends on: [`parse/mod.rs`](mod.en.md) (`create_secure_reader`, `convert_xml_error`, `concat_rich_text`), [`error.rs`](../error.en.md). No dependency on `model/` — `SharedStringTable` is a `parse/`-internal intermediate data structure; converting to `model::CellValue::Text` is [`resolve/shared_strings.rs`](../resolve/shared_strings.en.md)'s job
- Depended on by: [`resolve/shared_strings.rs`](../resolve/shared_strings.en.md) (`SharedStringTable::get`), [`resolve/mod.rs`](../resolve/mod.en.md) (passed through as an argument to `resolve_sheet`), `pipeline.rs` (built once between Phases 1–3 and passed into every `resolve_sheet` call; per architecture.md — "`SharedStringTable` and `StyleSheet` are discarded once Phase 4 completes" — it is dropped once every sheet has finished resolving)

`resolve/shared_strings.rs` importing `SharedStringTable` directly from `parse::shared_strings` (bypassing `model/`) is exactly the implementation of what [resolve/mod.md Dependencies](../resolve/mod.en.md) had already reasoned through: "the dependency on `parse::shared_strings::SharedStringTable` is not the 'dependency on I/O' that architecture.md design policy 2 forbids — it's a dependency on structured, in-memory data that Phase 3 has already built."

## Error Handling Policy

- Structurally invalid XML is converted into `Error::XmlParse` or `Error::ZipBombDetected` via [`convert_xml_error`](mod.en.md)
- An `<si>`'s structure itself (neither `<t>` nor `<r>`, i.e. an empty `<si/>` with no text) is treated as an empty string rather than an error — Excel legitimately can produce a shared-string entry for an empty cell
- A failure to interpret an individual `<si>` never panics (the input is untrusted external data)

## Testing Strategy

- Verify that multiple plain `<si><t>...</t></si>` entries are stored in `SharedStringTable` in the correct order (source order = index order)
- Verify that an `<si>` with rich text (multiple `<r><t>...</t></r>`) resolves to a single string concatenating each run's text
- Verify that `<t>` nested under `<rPh>` (furigana) is not accidentally concatenated into the main text (a regression test specific to Japanese business use cases, matching the context requirements chapter 1 targets)
- Verify that whitespace in a `<t xml:space="preserve"> ... </t>` with leading/trailing spaces is preserved, not trimmed
- Verify that an empty `<si/>` (no text) is treated as an empty string and does not fail the parse as a whole
- Verify that an empty `<sst>` (`uniqueCount="0"`) yields an empty `SharedStringTable` (`len() == 0`)
- Verify that `SharedStringTable::get` returns `None` for an out-of-range index (the wiring test underlying `resolve/shared_strings.rs`'s error conversion)

## Open Questions

1. **Soundness of always preserving whitespace instead of branching on the `xml:space` attribute's value**: because [`parse/mod.rs`](mod.en.md)'s `create_secure_reader` defaults to `trim_text(false)`, this file preserves whitespace unconditionally regardless of `xml:space`'s value. Given the requirements only explicitly call for honoring `xml:space="preserve"`, this unconditional-preserve simplification is judged sufficient — but it may need revisiting if selective trimming under `xml:space="default"` turns out to be required.
2. **Memory-allocation strategy for a large `sharedStrings.xml`** (e.g. a "grid-paper Excel" file with a very large `uniqueCount`): whether to pre-read the `<sst count="N" uniqueCount="M">` element's `uniqueCount` attribute and pre-allocate via `Vec::with_capacity(M)` is to be settled together with performance requirements.
3. **Namespace handling**: same topic as [parse/mod.md Open Question 4](mod.en.md). `<t>` / `<r>` / `<rPh>` themselves carry no prefix, so this file is expected to be affected minimally.