Skip to content

feat(sheet): read every Excel container in-house and drop calamine - #112

Open
tomsideguide wants to merge 22 commits into
mainfrom
feat/sheet-in-house
Open

feat(sheet): read every Excel container in-house and drop calamine#112
tomsideguide wants to merge 22 commits into
mainfrom
feat/sheet-in-house

Conversation

@tomsideguide

@tomsideguide tomsideguide commented Aug 19, 2026

Copy link
Copy Markdown
Member

Number formats were dropped, so a cell showing 7.5% converted to 0.075. calamine does not expose the format code, so this needed the styles part read directly, which meant reading the workbook directly.

All four containers are now read in-house: xlsx and xlsm from XML, xlsb from its binary records, xls from BIFF in an OLE container. They share one format engine and one grid assembly, so the same workbook saved in any of them converts identically. The catch_unwind barrier is gone with calamine; malformed input raises typed errors.

Hidden rows, columns and sheets are omitted now, and a merge extending past the populated range keeps its full extent.

New cap: a sheet grid spans its populated extent, so two cells at opposite corners described a whole sheet of empty positions from a few hundred bytes. MAX_GRID_SLOTS charges that before materializing. A fully populated legacy .xls at its maximum 65536x256 exceeds it and now returns ResourceLimit.

Fuzzing reached almost none of this before: random bytes never form an OLE container or a zip holding workbook.bin. Three targets wrap their input in a valid container so mutation reaches the record and format-code parsers, with checked-in seeds.

Closes #27
Closes #8
Closes #9


Summary by cubic

Reads every Excel container in-house and removes calamine, restoring correct number-format rendering and identical output across .xlsx, .xlsm, .xlsb, and .xls. Previously percentage/currency formats were lost; now parsed format codes render them, empty codes fall back to General, malformed inputs raise typed errors, and only date/time parts render as date/time with ISO-like output.

  • Omits hidden rows, columns, and sheets; preserves merge extent beyond populated cells. GridBuilder::keep_covered_tail keeps merged tails during trimming.
  • Caps sparse-sheet grids with MAX_GRID_SLOTS (4,000,000); a fully populated legacy .xls at 65536×256 now returns a ResourceLimit error.
  • Resolves the workbook’s main part before conventional paths and requires a workbook; selects the reader from the part’s bytes (XML vs binary), treats a leading BOM as XML, and propagates limit errors.
  • Matches AM/PM tokens over chars (including multibyte), keeps literals around General and drops an empty trailing decimal point, honors wider fraction denominators, and keeps the fictitious 1900‑02‑29 distinct.
  • Truncated .xls BIFF streams report a structured error with a warning instead of panicking.
  • Adds fuzz targets for the binary readers and the format engine; the xlsb target reaches cell, style, and shared-string readers, and seeds cover AM/PM token paths while preserving whitespace in format codes.

Written for commit 9729235. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 38 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/mod.rs">

<violation number="1" location="src/formats/sheet/mod.rs:47">
P2: When an OPC workbook uses a valid nonconventional main-part target, `container` returns `None` before the parser can resolve the root relationship. Classify ZIP workbooks from `_rels/.rels` and the main-part type instead of requiring these two conventional paths.</violation>
</file>

<file name="src/formats/sheet/xlsx.rs">

<violation number="1" location="src/formats/sheet/xlsx.rs:477">
P2: A large valid merge is charged twice: once by this preflight counter and again by `GridBuilder::place`, so merges larger than roughly half `MAX_EXPANSION` return `ResourceLimit` unexpectedly. Account for the merge expansion once while still reserving the covered-position allocation before materialization.</violation>
</file>

<file name="src/formats/sheet/xls.rs">

<violation number="1" location="src/formats/sheet/xls.rs:117">
P2: When a Workbook stream ends with a truncated BIFF record, `next_record` treats corruption as EOF, so parsing can silently return an empty or partial document. Return a typed `Malformed` error for non-empty positions that cannot form a record and require each substream to reach its EOF record.</violation>
</file>

<file name="fuzz/fuzz_targets/xlsb.rs">

<violation number="1" location="fuzz/fuzz_targets/xlsb.rs:14">
P2: The PR claims this target lets mutation reach the xlsb record and format-code parsers, but it never reaches read_sheet or read_styles. The package lacks xl/_rels/workbook.bin.rels, so read_rels returns an empty relationship map (missing part yields default), every BrtBundleSh sheet is skipped for lacking a worksheet relationship, and styles.bin is never resolved. Only read_workbook's BrtWbProp/BrtBundleSh records are fuzzed, leaving the newly added cell-record and format-code parsers unexercised. Add xl/_rels/workbook.bin.rels with worksheet and styles relationships (and matching parts) so mutation reaches read_sheet and read_styles.</violation>
</file>

<file name="src/formats/sheet/numfmt.rs">

<violation number="1" location="src/formats/sheet/numfmt.rs:432">
P2: When a `General` section contains a literal suffix or spacing token, the parser accepts it but `Body::General` discards it during rendering. Preserve the section tokens when rendering General, or reject these decorated formats so they fall back consistently instead of silently losing text.</violation>

<violation number="2" location="src/formats/sheet/numfmt.rs:803">
P3: Optional fractional formats leave a dangling decimal separator when the value has no fractional digits. Render `0.##` with `5.0` as `5`, not `5.`; emit the separator only when at least one fractional digit is retained.</violation>

<violation number="3" location="src/formats/sheet/numfmt.rs:897">
P2: When a fixed fraction denominator contains leading zeros, whole-number values retain the denominator because the hide check compares its canonical integer string with the original token. Compare parsed denominator values, or track the denominator token directly, so `# ?/08` renders `5` rather than `5 08`.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread src/formats/sheet/mod.rs
Comment thread src/formats/sheet/xlsx.rs
Comment thread src/formats/sheet/xls.rs
Comment thread fuzz/fuzz_targets/xlsb.rs Outdated
Comment thread src/formats/sheet/numfmt.rs Outdated
Comment thread src/formats/sheet/numfmt.rs
Comment thread src/formats/sheet/numfmt.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/numfmt.rs">

<violation number="1" location="src/formats/sheet/numfmt.rs:171">
P2: When a custom `formatCode` is empty, this parser accepts it as a valid empty numeric format and blanks every numeric cell using that style. Reject an empty code here so `resolve_format` takes its existing General fallback path.</violation>
</file>

<file name="src/formats/sheet/xlsx.rs">

<violation number="1" location="src/formats/sheet/xlsx.rs:275">
P2: When `row/@r` is present but invalid or outside the worksheet bounds, this fallback imports its unreferenced cells at `next_row`. Distinguish a missing attribute from an invalid one and skip or reject explicit invalid row references.</violation>

<violation number="2" location="src/formats/sheet/xlsx.rs:372">
P1: When a date-only or time-only format is applied to a non-midnight serial, `render_serial` emits both the date and time because it receives no information about which components the format requests. Preserve the format's date/time components and render only the requested portion.</violation>
</file>

<file name="src/formats/sheet/mod.rs">

<violation number="1" location="src/formats/sheet/mod.rs:63">
P2: When a valid OPC workbook uses a nonconventional main-part name without a lowercase `.xml` or `.bin` suffix, `container` returns `None` before the reader can resolve it. Classify the package from its `officeDocument` relationship and content type (or target contents), rather than the filename suffix.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/sheet/xlsx.rs Outdated
format!("{prefix}{}{suffix}", format_float(value))
}
Rendered::Text(s) => s,
Rendered::DateTime { elapsed } => render_serial(n, elapsed, date1904),

@cubic-dev-ai cubic-dev-ai Bot Aug 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a date-only or time-only format is applied to a non-midnight serial, render_serial emits both the date and time because it receives no information about which components the format requests. Preserve the format's date/time components and render only the requested portion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/sheet/xlsx.rs, line 374:

<comment>When a date-only or time-only format is applied to a non-midnight serial, `render_serial` emits both the date and time because it receives no information about which components the format requests. Preserve the format's date/time components and render only the requested portion.</comment>

<file context>
@@ -0,0 +1,971 @@
+                format!("{prefix}{}{suffix}", format_float(value))
+            }
+            Rendered::Text(s) => s,
+            Rendered::DateTime { elapsed } => render_serial(n, elapsed, date1904),
+        },
+    };
</file context>
Fix with cubic

Comment thread src/formats/sheet/numfmt.rs
Comment thread src/formats/sheet/xlsx.rs
Comment thread src/formats/sheet/mod.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/xlsx.rs">

<violation number="1" location="src/formats/sheet/xlsx.rs:477">
P2: A merge's covered positions are charged twice: once by `build_table` and again by `GridBuilder::place`. This rejects merges below `MAX_EXPANSION` and makes the effective limit about two million positions; charge the merge expansion in one layer or use a separate budget for the temporary coverage set.</violation>
</file>

<file name="src/formats/sheet/mod.rs">

<violation number="1" location="src/formats/sheet/mod.rs:63">
P2: When the root `officeDocument` relationship targets a workbook part without a lowercase `.xml` or `.bin` suffix, `container` returns `None` and rejects the package before the reader can use the relationship. Classify the target from its `[Content_Types].xml` entry or its content, rather than requiring a filename suffix.</violation>
</file>

<file name="fuzz/seeds/numfmt/ampm-multibyte">

<violation number="1" location="fuzz/seeds/numfmt/ampm-multibyte:1">
P3: The seed named `ampm-multibyte` contains `aaaaé`, which has no `/`, so neither `AM/PM` nor `A/P` can match and the parse returns None immediately. It never reaches the multibyte-follows-token handling this seed is meant to cover. Seed it with a token followed by a multibyte char (e.g. `AM/PMé` or `h:mm AM/PMé`) so a single seed actually hits the fixed path instead of relying on mutation to discover the `/`.</violation>
</file>

<file name="fuzz/fuzz_targets/numfmt.rs">

<violation number="1" location="fuzz/fuzz_targets/numfmt.rs:13">
P3: The `.filter(|c| !c.is_control())` also strips `\t`, `\n` and `\r`, which are legal and meaningful inside an Excel format code (multi-line formats embed a literal newline). With them removed, those codes never reach `NumberFormat::parse`, so the parser's handling of embedded whitespace is never fuzzed — a gap in a target whose stated purpose is to cover the format-code grammar. Only XML-illegal control chars (0x00–0x08, 0x0B, 0x0C, 0x0E–0x1F, 0x7F) need stripping to keep the attribute well-formed.</violation>
</file>

<file name="src/formats/sheet/numfmt.rs">

<violation number="1" location="src/formats/sheet/numfmt.rs:935">
P2: When a fixed denominator contains leading zeros, whole numbers retain the denominator after the fraction is hidden: `# ?/08` renders `5 08`. Preserve the denominator's source text or hide the entire fixed-denominator token whenever `hide` is true.</violation>

<violation number="2" location="src/formats/sheet/numfmt.rs:955">
P2: When a valid fraction format has four or more denominator placeholders, `best_fraction` cannot select denominators above 999, so values requiring them lose their fractional part. Honor the placeholder-derived bound with a bounded rational approximation instead of hard-coding 999.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread src/formats/sheet/xlsx.rs
Comment thread src/formats/sheet/mod.rs Outdated
Comment thread src/formats/sheet/numfmt.rs
Comment thread src/formats/sheet/numfmt.rs Outdated
Comment thread fuzz/seeds/numfmt/ampm-multibyte
Comment thread fuzz/fuzz_targets/numfmt.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 40 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/mod.rs">

<violation number="1" location="src/formats/sheet/mod.rs:58">
P2: When a nonconventional workbook package exceeds `MAX_ENTRY_COUNT`, `.ok()?` converts the fatal `ResourceLimit` into `None`, so callers receive `Malformed` instead. Propagate resource-limit errors from container probing while treating ordinary non-workbook ZIPs as no match.</violation>

<violation number="2" location="src/formats/sheet/mod.rs:63">
P2: When an OPC workbook uses a nonconventional main-part name, this branch rejects it or can select the wrong parser from the filename suffix. Determine XML versus XLSB from the part content type or record/XML signature instead of requiring `.xml` or `.bin`.</violation>
</file>

<file name="src/formats/sheet/numfmt.rs">

<violation number="1" location="src/formats/sheet/numfmt.rs:213">
P2: Time-only formats such as `h:mm` and `mm:ss` lose their time-only distinction here, so serials of at least one day are rendered with an unintended calendar date. Preserve whether the section contains a date component and render time-only sections without the date.</violation>

<violation number="2" location="src/formats/sheet/numfmt.rs:944">
P3: Fraction formats discard trailing spaces requested by `_x` or quoted literals, unlike other numeric formats. Return the rendered fraction without trimming its end.</violation>
</file>

<file name="fuzz/fuzz_targets/xlsb.rs">

<violation number="1" location="fuzz/fuzz_targets/xlsb.rs:39">
P3: WORKBOOK_RELS declares a worksheet relationship rId2 -> worksheets/sheet2.bin, but the zip never writes xl/worksheets/sheet2.bin, and the checked-in workbook-bin seed declares a visible "Secret" sheet on rId2. The reader resolves that sheet to the missing part, logs "skipping unreadable sheet" and increments `failed` on every fuzz iteration; whenever the mutated `data` also fails to parse as the "Data" sheet, both sheets fail and the target returns "no sheet in the workbook could be read", short-circuiting the call. Either add ("xl/worksheets/sheet2.bin", data) to `parts`, or drop the rId2 relationship (and the "Secret" bundle) so the package and seed stay consistent.</violation>
</file>

<file name="src/formats/sheet/xlsx.rs">

<violation number="1" location="src/formats/sheet/xlsx.rs:575">
P2: A valid `9999-12-31` date near midnight falls back to its raw serial when rounding carries the time into the next day. Allow the rounding boundary through the date calculation, then reject only the resulting out-of-range civil date.</violation>

<violation number="2" location="src/formats/sheet/xlsx.rs:589">
P2: For 1900-system date cells with serial 60, this offset silently renders the fictitious Excel date as `1900-02-28`, duplicating serial 59. Special-case serial 60 or emit an explicit numeric/invalid-date representation instead of silently changing the value.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread src/formats/sheet/mod.rs Outdated
Comment thread src/formats/sheet/mod.rs Outdated
Comment thread src/formats/sheet/numfmt.rs Outdated
Comment thread src/formats/sheet/xlsx.rs
Comment thread src/formats/sheet/xlsx.rs
Comment thread src/formats/sheet/numfmt.rs
Comment thread fuzz/fuzz_targets/xlsb.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/xlsx.rs">

<violation number="1" location="src/formats/sheet/xlsx.rs:591">
P2: When a date-formatted value just below serial 60 is read, seconds rounding increments `days` to 60 and this guard emits the raw serial instead of a date. Check the unrounded serial interval so only the fictitious day is handled specially.</violation>
</file>

<file name="src/formats/sheet/mod.rs">

<violation number="1" location="src/formats/sheet/mod.rs:72">
P2: When a custom-named workbook part has an XML BOM, `container` classifies it as XLSB because the BOM is the first non-whitespace byte. Strip XML BOMs before inspecting the leading byte, or reuse the XML parser’s encoding detection.</violation>

<violation number="2" location="src/formats/sheet/mod.rs:72">
P2: An XML workbook part that begins with a UTF-8 BOM (EF) or UTF-16 BOM (FF) has a non-whitespace first byte, so the fallback classifies it as `Container::Bin` and routes it to `xlsb::parse`, where the XML will fail instead of parsing. `is_ascii_whitespace` skips only space/tab/newline, not BOMs. Skip a BOM (EF BB BF / FF FE) before probing the first byte.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/sheet/xlsx.rs Outdated
Comment thread src/formats/sheet/mod.rs Outdated
Comment thread src/formats/sheet/mod.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/mod.rs">

<violation number="1" location="src/formats/sheet/mod.rs:60">
P2: When a package uses a custom officeDocument target but also contains a conventional workbook entry, the filename checks bypass this fallback and select the wrong reader. Resolve the root relationship first, using conventional paths only when no usable relationship exists.</violation>

<violation number="2" location="src/formats/sheet/mod.rs:75">
P2: The widened detection now treats any OOXML package with a root OFFICE_DOCUMENT relationship as an Excel workbook. A .docx/.pptx (or any non-workbook OOXML) resolved here starts with `<?xml` and is sent to xlsx::parse, which finds no `sheets` element and returns an empty, successful Document instead of a typed error. Require the resolved part to actually be a workbook (e.g. verify the SML workbook root or that sheets exist) before classifying it as an XML container, or have xlsx::parse error when the root part is not a SpreadsheetML workbook.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/sheet/mod.rs Outdated
Comment thread src/formats/sheet/mod.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/mod.rs">

<violation number="1" location="src/formats/sheet/mod.rs:50">
P2: Every ZIP workbook parses and retains two archive indexes here, and oversized archives are parsed before `Package` enforces its entry-count limit. Open `Package` once, handle invalid-zip versus resource-limit errors explicitly, and use `pkg.has_part` for fallback checks.</violation>
</file>

<file name="src/formats/sheet/numfmt.rs">

<violation number="1" location="src/formats/sheet/numfmt.rs:216">
P3: Every call to `format_number` for a `General` body clones `prefix` and `suffix` into two fresh `String`s, allocating on the per-cell rendering hot path. Make `Rendered::General` borrow the section strings (e.g. with `Cow<'_, str>` or by having `format_number` return borrowed data) or return a single pre-concatenated string instead of cloning each time.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/sheet/mod.rs
if bytes.starts_with(&OLE_MAGIC) {
return Ok(Some(Container::Ole));
}
let Ok(zip) = zip::ZipArchive::new(Cursor::new(bytes)) else {

@cubic-dev-ai cubic-dev-ai Bot Aug 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Every ZIP workbook parses and retains two archive indexes here, and oversized archives are parsed before Package enforces its entry-count limit. Open Package once, handle invalid-zip versus resource-limit errors explicitly, and use pkg.has_part for fallback checks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/sheet/mod.rs, line 50:

<comment>Every ZIP workbook parses and retains two archive indexes here, and oversized archives are parsed before `Package` enforces its entry-count limit. Open `Package` once, handle invalid-zip versus resource-limit errors explicitly, and use `pkg.has_part` for fallback checks.</comment>

<file context>
@@ -37,20 +40,53 @@ enum Container {
-        return Some(Container::Ole);
+        return Ok(Some(Container::Ole));
+    }
+    let Ok(zip) = zip::ZipArchive::new(Cursor::new(bytes)) else {
+        return Ok(None);
+    };
</file context>
Fix with cubic

};
match &section.body {
Body::General { prefix, suffix } => {
Rendered::General { value, prefix: prefix.clone(), suffix: suffix.clone() }

@cubic-dev-ai cubic-dev-ai Bot Aug 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Every call to format_number for a General body clones prefix and suffix into two fresh Strings, allocating on the per-cell rendering hot path. Make Rendered::General borrow the section strings (e.g. with Cow<'_, str> or by having format_number return borrowed data) or return a single pre-concatenated string instead of cloning each time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/sheet/numfmt.rs, line 216:

<comment>Every call to `format_number` for a `General` body clones `prefix` and `suffix` into two fresh `String`s, allocating on the per-cell rendering hot path. Make `Rendered::General` borrow the section strings (e.g. with `Cow<'_, str>` or by having `format_number` return borrowed data) or return a single pre-concatenated string instead of cloning each time.</comment>

<file context>
@@ -194,21 +206,27 @@ impl NumberFormat {
         match &section.body {
-            Body::General => Rendered::General(value),
+            Body::General { prefix, suffix } => {
+                Rendered::General { value, prefix: prefix.clone(), suffix: suffix.clone() }
+            }
             Body::DateTime { elapsed } => Rendered::DateTime { elapsed: *elapsed },
</file context>
Fix with cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/mod.rs">

<violation number="1" location="src/formats/sheet/mod.rs:69">
P2: For a workbook whose root relationship uses a nonconventional path, this code decompresses and allocates the entire workbook part merely to inspect its first byte, then the selected reader decompresses it again. Classify from a bounded prefix or pass the already-read bytes into the reader to avoid doubling work for every such workbook.</violation>
</file>

<file name="fuzz/fuzz_targets/xlsb.rs">

<violation number="1" location="fuzz/fuzz_targets/xlsb.rs:21">
P3: The bytes written to `xl/worksheets/sheet2.bin` never reach the parser. The seeded `workbook.bin` (the `WORKBOOK` constant, which is not fuzzed) marks the second sheet as hidden (`BrtBundleSh` state=1), and `read_workbook`/`parse` in `src/formats/sheet/xlsb.rs` skip hidden sheets before resolving their parts, so `read_sheet` never runs on `sheet2.bin`. The fuzzed input there is inert while `sheet1.bin` carries the same bytes and is the only worksheet actually read. Drop the `sheet2.bin` entry (keeping the visible `sheet1.bin`), or make the seed's second bundle visible if a second read sheet is intended.</violation>
</file>

<file name="src/formats/sheet/numfmt.rs">

<violation number="1" location="src/formats/sheet/numfmt.rs:810">
P2: The new `show_decimal` gate now drops the decimal point whenever the fraction region is empty of kept digits and `?` placeholders, regardless of what actually follows the decimal token. For a format like `0.` (or `0."kg"` / `0.%` — decimal followed only by a literal/percent with no fractional placeholder), `emit` no longer writes the `.`, so `0.` renders `5` instead of `5.`. The trimming intent applies to placeholder-backed formats (`0.##` → `5`); a bare decimal with no fractional placeholder should still render its point. Since a format that reaches an empty `frac_places` never hid the point before, keep showing it in that case.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/sheet/mod.rs Outdated
};
// The part's own bytes decide, since the name carries no guarantee: a
// SpreadsheetML workbook is XML, an xlsb one is a record stream.
let Some(part) = pkg.part(&target.path)? else {

@cubic-dev-ai cubic-dev-ai Bot Aug 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: For a workbook whose root relationship uses a nonconventional path, this code decompresses and allocates the entire workbook part merely to inspect its first byte, then the selected reader decompresses it again. Classify from a bounded prefix or pass the already-read bytes into the reader to avoid doubling work for every such workbook.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/sheet/mod.rs, line 69:

<comment>For a workbook whose root relationship uses a nonconventional path, this code decompresses and allocates the entire workbook part merely to inspect its first byte, then the selected reader decompresses it again. Classify from a bounded prefix or pass the already-read bytes into the reader to avoid doubling work for every such workbook.</comment>

<file context>
@@ -38,19 +40,42 @@ enum Container {
+    };
+    // The part's own bytes decide, since the name carries no guarantee: a
+    // SpreadsheetML workbook is XML, an xlsb one is a record stream.
+    let Some(part) = pkg.part(&target.path)? else {
+        return Ok(None);
+    };
</file context>
Fix with cubic


// The point shows only when something follows it: kept digits, or the
// alignment spaces a `?` place emits.
let show_decimal = keep_frac > 0 || frac_places.contains(&'?');

@cubic-dev-ai cubic-dev-ai Bot Aug 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new show_decimal gate now drops the decimal point whenever the fraction region is empty of kept digits and ? placeholders, regardless of what actually follows the decimal token. For a format like 0. (or 0."kg" / 0.% — decimal followed only by a literal/percent with no fractional placeholder), emit no longer writes the ., so 0. renders 5 instead of 5.. The trimming intent applies to placeholder-backed formats (0.##5); a bare decimal with no fractional placeholder should still render its point. Since a format that reaches an empty frac_places never hid the point before, keep showing it in that case.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/sheet/numfmt.rs, line 810:

<comment>The new `show_decimal` gate now drops the decimal point whenever the fraction region is empty of kept digits and `?` placeholders, regardless of what actually follows the decimal token. For a format like `0.` (or `0."kg"` / `0.%` — decimal followed only by a literal/percent with no fractional placeholder), `emit` no longer writes the `.`, so `0.` renders `5` instead of `5.`. The trimming intent applies to placeholder-backed formats (`0.##` → `5`); a bare decimal with no fractional placeholder should still render its point. Since a format that reaches an empty `frac_places` never hid the point before, keep showing it in that case.</comment>

<file context>
@@ -770,6 +805,10 @@ fn emit(
 
+    // The point shows only when something follows it: kept digits, or the
+    // alignment spaces a `?` place emits.
+    let show_decimal = keep_frac > 0 || frac_places.contains(&'?');
+
     let mut out = String::new();
</file context>
Suggested change
let show_decimal = keep_frac > 0 || frac_places.contains(&'?');
let show_decimal = keep_frac > 0 || frac_places.contains(&'?') || frac_places.is_empty();
Fix with cubic

Comment thread fuzz/fuzz_targets/xlsb.rs
("xl/workbook.bin", WORKBOOK),
("xl/_rels/workbook.bin.rels", WORKBOOK_RELS.as_bytes()),
("xl/worksheets/sheet1.bin", data),
("xl/worksheets/sheet2.bin", data),

@cubic-dev-ai cubic-dev-ai Bot Aug 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The bytes written to xl/worksheets/sheet2.bin never reach the parser. The seeded workbook.bin (the WORKBOOK constant, which is not fuzzed) marks the second sheet as hidden (BrtBundleSh state=1), and read_workbook/parse in src/formats/sheet/xlsb.rs skip hidden sheets before resolving their parts, so read_sheet never runs on sheet2.bin. The fuzzed input there is inert while sheet1.bin carries the same bytes and is the only worksheet actually read. Drop the sheet2.bin entry (keeping the visible sheet1.bin), or make the seed's second bundle visible if a second read sheet is intended.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At fuzz/fuzz_targets/xlsb.rs, line 21:

<comment>The bytes written to `xl/worksheets/sheet2.bin` never reach the parser. The seeded `workbook.bin` (the `WORKBOOK` constant, which is not fuzzed) marks the second sheet as hidden (`BrtBundleSh` state=1), and `read_workbook`/`parse` in `src/formats/sheet/xlsb.rs` skip hidden sheets before resolving their parts, so `read_sheet` never runs on `sheet2.bin`. The fuzzed input there is inert while `sheet1.bin` carries the same bytes and is the only worksheet actually read. Drop the `sheet2.bin` entry (keeping the visible `sheet1.bin`), or make the seed's second bundle visible if a second read sheet is intended.</comment>

<file context>
@@ -3,15 +3,24 @@
+        ("xl/workbook.bin", WORKBOOK),
+        ("xl/_rels/workbook.bin.rels", WORKBOOK_RELS.as_bytes()),
+        ("xl/worksheets/sheet1.bin", data),
+        ("xl/worksheets/sheet2.bin", data),
+        ("xl/styles.bin", data),
+        ("xl/sharedStrings.bin", data),
</file context>
Fix with cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/formats/sheet/numfmt.rs">

<violation number="1" location="src/formats/sheet/numfmt.rs:368">
P3: For `[h]:mm`, `date_parts` reports that an elapsed duration also requests a date because the bracketed hour is absent from `runs`. Clear `date` whenever `elapsed` is true so downstream consumers receive consistent date/time metadata.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

}
// An elapsed span is a duration, and a section with no run at all came
// from a bracket alone, so treat both as a time.
if elapsed || (!parts.date && !parts.time) {

@cubic-dev-ai cubic-dev-ai Bot Aug 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: For [h]:mm, date_parts reports that an elapsed duration also requests a date because the bracketed hour is absent from runs. Clear date whenever elapsed is true so downstream consumers receive consistent date/time metadata.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/sheet/numfmt.rs, line 368:

<comment>For `[h]:mm`, `date_parts` reports that an elapsed duration also requests a date because the bracketed hour is absent from `runs`. Clear `date` whenever `elapsed` is true so downstream consumers receive consistent date/time metadata.</comment>

<file context>
@@ -307,6 +334,43 @@ fn split_sections(code: &str) -> Option<Vec<String>> {
+    }
+    // An elapsed span is a duration, and a section with no run at all came
+    // from a bracket alone, so treat both as a time.
+    if elapsed || (!parts.date && !parts.time) {
+        parts.time = true;
+    }
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant