Skip to content

Release v2.3.0: encrypted workbooks, parallel CSV, ExcelReader.Arrow package - #98

Merged
GabrielMarquezMatte merged 69 commits into
masterfrom
develop
Aug 30, 2026
Merged

GabrielMarquezMatte merged 69 commits into
masterfrom
develop

Conversation

@GabrielMarquezMatte

Copy link
Copy Markdown
Owner

Highlights

  • Encrypted workbook reading (OOXML agile + standard): malformed-CFB detection,
    DecryptedPackageStream with sequential/random access, password support threaded through
    .NET/Rust/Python/C++/native (ABI bump to 4).
  • Parallel CSV parsing: positional partitioning (RangedFileStream), lock-free per-chunk
    pull queue, quote-parity boundary reconciliation, automatic sequential fallback for sources
    that can't be partitioned (non-seekable streams, non-UTF-8 encodings, small files).
  • ExcelReader.Arrow (new package): conversion to Apache.Arrow.RecordBatch with schema
    inference, string/int64/float64/bool/date/timestamp column support, shared-string caching.
    Ready for NuGet publishing (packaged README, snupkg symbols, SourceLink, release notes).
  • Performance: decryption, sequential parsing, memory allocation, and Arrow conversion
    optimizations; CSV read improvements.
  • Fuzzing campaign: 2 real bugs found and fixed; csv/csv-sniff corpora still growing.

Publishing

ExcelReader.Arrow ships to NuGet for the first time alongside ExcelReader.NET and
ExcelReader.NET.Cli. Trusted Publishing on nuget.org is account-scoped, so the existing
policy already covers it with no extra setup.

Native ABI

xl_open_options gained a password field; native ABI bumps to version 4. Affects only
direct native/Rust/Python/C++ binding consumers, not the .NET public API.

GabrielMarquezMatte and others added 30 commits August 25, 2026 23:39
Extracts the OLE/CFB parse prefix that used to live inline in
XlsCompoundFile.BuildWorkbook (header/DIFAT/FAT/directory parsing,
mini-stream reads, FAT-chain reads, bounds checks) into a standalone
CfbContainer type that can look up any named directory stream, not just
"Workbook"/"Book". Every bounds/limit check (sector size, mini-stream
cutoff, maxSectors, name-length, FAT-chain cycle detection, etc.) was
moved verbatim, including its explanatory comment.

CfbContainer exposes ContainsStream, ReadStream (materializing, with an
ExcelLimitExceededException guard), OpenStreamView (a seekable read-only
Stream over the FAT or mini-FAT chain), and StreamLength.
XlsCompoundFile.BuildWorkbook now parses via CfbContainer.Parse and keeps
only the Workbook-specific mini-cutoff / Chained / Streamed selection.

Pure code motion: no crypto code, no behavior change on the XLS path.
This is the foundation an encrypted-workbook reader needs, since an
encrypted OOXML file is a CFB container holding "EncryptionInfo" and
"EncryptedPackage" streams instead of "Workbook".
Implements AgileKeyDerivation per [MS-OFFCRYPTO] 2.3.4.7 (Encryption Key
Generation), 2.3.4.10 (PasswordKeyEncryptor Generation), 2.3.4.12
(Initialization Vector Generation), 2.3.4.13 (Data Encryption), and
2.3.4.14 (DataIntegrity Generation), cross-checked against the live
spec pages rather than trusting the brief's sketch verbatim.

One deviation from the brief, spec-verified: BlockKey pads a
short hash with 0x36 bytes, not zero bytes, per 2.3.4.7's final
paragraph and the general IV rule in 2.3.4.12 ("pad ... by appending
0x36"). This path isn't exercised by the current AES-256/SHA-512
fixtures (hash always longer than the target key), so it had no
oracle to catch a wrong constant.

Verified against the real EncryptedFixtures (password "hunter2"):
Should_Derive_Key_When_Password_Correct and
Should_Report_PasswordIncorrect_When_Password_Wrong both pass for
agile-aes256-sha512.xlsx and .xlsb on net10.0 and net8.0.
DecryptNoPadding fed a block-size-misaligned ciphertext straight into
Aes.TransformFinalBlock with Padding = None, which throws a raw
CryptographicException. EncryptionInfo is untrusted, attacker-controlled
input parsed before any password check, and every other malformed value
in this codebase is converted to InvalidDataException/
ExcelEncryptionException/ExcelLimitExceededException before reaching a
caller (see EncryptionDescriptor.ReadCryptoParameters). This also
matters for Task 10's fuzz target: FuzzMutation.AcceptableExceptionTypes
doesn't include CryptographicException, so a mutated
EncryptedVerifierHashInput/Value, EncryptedKeyValue, or
EncryptedHmacKey/Value would have failed that fuzz test.

Adds a bounds check (ciphertext.Length % aes.BlockSize/8) before the
AES call, and a regression test that truncates a real fixture's
EncryptedVerifierHashInput by one byte and asserts InvalidDataException,
not CryptographicException.
Implements the oracle-tested read-only Stream that decrypts the
EncryptedPackage CFB stream of an agile-encrypted OOXML workbook on
demand, in 4096-byte segments (AES-CBC, one segment cached at a time).

- Create() opens the EncryptedPackage view, validates the 8-byte
  declared plaintext length against both the ciphertext length and
  MaxTotalDecompressedBytes before allocating anything, and derives
  the intermediate key via AgileKeyDerivation.
- Read/ReadAsync follow the three-tier sync/async convention from
  ARCHITECTURE.md: a blocking Read, a ReadAsync whose cached-segment
  fast path returns a completed ValueTask, and a split-out
  ReadSlowAsync holding the awaiting refill loop.
- The final segment's ciphertext is block-padded to 16 bytes while
  its plaintext is truncated to the declared Length; the copy bound
  min(..., Length - position) in Read/ReadSlowAsync handles this
  without any special-casing.
- Dispose zeroes the derived key and segment cache with
  CryptographicOperations.ZeroMemory, returns the pooled cache
  buffer, and disposes the ciphertext view.

Should_Match_Oracle_When_Read_Sequentially/_Async are byte-exact
against msoffcrypto-tool's independently produced plaintext for both
real fixtures (agile-aes256-sha512.xlsx/.xlsb), passing on the first
run with no debugging needed.
…ataException

Review of commit 29eb0e1 found that SegmentCiphertextRange's comment
claimed the container's total ciphertext length was validated (in
Create) to be block-aligned, but no such validation existed. Both the
CFB entry size and the 8-byte declared-length prefix inside
EncryptedPackage are attacker-controlled; a crafted file can make the
final segment's remaining ciphertext land mid-block. That reached
ICryptoTransform.TransformBlock (PaddingMode.None) and threw a raw
ArgumentOutOfRangeException instead of a graceful, typed rejection -
the same class of bug Task 5 already fixed in
AgileKeyDerivation.DecryptNoPadding (commit f5ce19c).

- Create now validates view.Length - PrefixSize is a multiple of the
  16-byte AES block size, right after the existing declared-length
  bounds check, and throws InvalidDataException (matching
  DecryptNoPadding's convention) before any segment is ever touched.
- Added a CipherBlockSize constant alongside the existing
  SegmentSize/PrefixSize constants.
- Fixed SegmentCiphertextRange's comment to state the validation
  truthfully instead of asserting an invariant nothing enforced.
- Added Should_Reject_NonBlockAligned_Ciphertext, which patches a real
  fixture's on-disk EncryptedPackage entry (CFB directory Size field
  and on-disk declared-length prefix) 5 bytes shorter than its real,
  valid ciphertext length - always landing on a non-block-aligned
  total regardless of the fixture's exact size - and asserts
  InvalidDataException, not ArgumentOutOfRangeException.

Both TFMs green: 2413/2413, including all byte-exact oracle tests
against both real fixtures.
Bumps Meziantou.Analyzer from 3.0.150 to 3.0.182
Bumps Microsoft.CodeAnalysis.Analyzers from 5.9.0-1.26328.17 to 5.9.0
Bumps Roslynator.Analyzers from 4.16.0 to 5.0.0
Bumps Roslynator.CodeAnalysis.Analyzers from 4.16.0 to 5.0.0
Bumps Roslynator.Formatting.Analyzers from 4.16.0 to 5.0.0
Bumps SonarAnalyzer.CSharp from 10.32.0.713 to 10.33.0.1635

---
updated-dependencies:
- dependency-name: Meziantou.Analyzer
  dependency-version: 3.0.182
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: analyzers
- dependency-name: Microsoft.CodeAnalysis.Analyzers
  dependency-version: 5.9.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: analyzers
- dependency-name: Roslynator.Analyzers
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: analyzers
- dependency-name: Roslynator.CodeAnalysis.Analyzers
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: analyzers
- dependency-name: Roslynator.Formatting.Analyzers
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: analyzers
- dependency-name: SonarAnalyzer.CSharp
  dependency-version: 10.33.0.1635
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: analyzers
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps MiniExcel from 1.45.0 to 1.46.0

---
updated-dependencies:
- dependency-name: MiniExcel
  dependency-version: 1.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: benchmarks
...

Signed-off-by: dependabot[bot] <support@github.com>
Task 8 of the encrypted-workbook-reading plan: wires the CFB/CFB
decryption pipeline from Tasks 2-7 into Excel.Open/OpenAsync's
detection and the FromXlsx/FromXlsb format-specific entry points.

- ExcelFileFormat gains EncryptedOoxml = 4 (explicit values for all
  members); NativeFormat's doc comment now explains the 5th managed
  value has no native counterpart.
- New EncryptedPackageOpener: IsEncryptedContainer probes a CFB
  container's directory for an EncryptedPackage stream (detection
  only); Decrypt turns one into a seekable plaintext-ZIP stream,
  bundling the CfbContainer with DecryptedPackageStream via a small
  owning Stream wrapper since the latter never takes ownership of the
  former.
- Excel.cs: TryClassifyHeader no longer finalizes on a CFB signature
  alone (only the OLE directory can tell legacy .xls apart from
  encrypted OOXML); DetectSeekable/DetectSeekableAsync resolve it via
  EncryptedPackageOpener.IsEncryptedContainer. ClassifyMemory keeps
  its pre-existing CFB-is-always-Xls behavior since the in-memory
  Open overload has no CFB directory probe. Open/OpenAsync gain an
  EncryptedOoxml arm that decrypts then classifies the resulting ZIP
  via the existing ClassifyZipStream. From/FromXlsb(+Async) gain a
  guard: a CFB stream with a password supplied is decrypted before
  construction, replacing a confusing raw ZipArchive error.
- XlsReader.cs: the FilePass rejection message now names the real
  boundary (OOXML encryption is supported via
  ExcelReaderOptions.Password; legacy .xls encryption is not) instead
  of reading as a plain bug report.
- XlsWorkbookBuilder gains WithFilePassRecord(), a byte[] twin of the
  existing BuildEncrypted() for tests using Excel.Open(ReadOnlyMemory<byte>, ...).

New tests/ExcelReader.Tests/EncryptedOpenTests.cs covers: same rows
from encrypted vs. plaintext fixtures (sync+async), PasswordRequired/
PasswordIncorrect, EncryptedOoxml detection, a real .xls still
detecting as Xls through the new two-stage CFB probe, password
support on FromXlsx, and the improved encrypted-.xls message.
Closes a review-flagged gap in Task 8's encrypted-workbook detection
wiring: every existing corrupt-OLE regression test (CorruptOleSignatureThrows,
UnsupportedOleSectorSizeThrows, the crafted-FAT/mini-FAT-sector-count
tests) calls Excel.FromXls directly, bypassing TryClassifyHeader/DetectSeekable
entirely — so EncryptedPackageOpener.IsEncryptedContainer's
catch (InvalidDataException) fallback (the thing that lets a malformed
file fall through to "not encrypted, let XLS diagnose it") had never
been exercised end-to-end through the path a real caller uses.

Adds Should_Preserve_Corrupt_Ole_Diagnosis_Through_Excel_Open to
EncryptedOpenTests.cs, reusing XlsWorkbookBuilder.BuildPatched with
the same sector-size corruption UnsupportedOleSectorSizeThrows already
uses (signature intact, so detection actually reaches the CFB probe).
Asserts Excel.DetectFileFormat still reports Xls without throwing, and
Excel.Open still throws the same InvalidDataException it always has
via Excel.FromXls — proving the new two-stage CFB probe doesn't change
behavior for a genuinely corrupt (not encrypted, not valid .xls) file.

No src/ changes: test-only, confirming behavior already matched the
brief's contract.
- EncryptedPackageOpener.DecryptToMemory/IsEncryptedMemory: eager decrypt
  path for Excel.From/Excel.Open's ReadOnlyMemory<byte> overloads, which
  are documented to never suspend, so lazy decrypt-on-demand isn't an
  option here. Always verifies the dataIntegrity HMAC since everything
  is already decrypted.
- PackageIntegrity.Verify: HMAC verification over the whole
  EncryptedPackage stream (8-byte prefix included). Wired into
  DecryptedPackageStream.Create as opt-in via
  ExcelReaderOptions.VerifyEncryptedIntegrity (default false, since it
  costs a full extra pass before the first row on the streaming path).
- Fixes a real bug in AgileKeyDerivation.UnwrapHmac: the decrypted HMAC
  key was truncated to KeyData.SaltSize instead of the hash's native
  output length. Per [MS-OFFCRYPTO] 2.3.4.14 (and confirmed against
  msoffcrypto-tool's own writer, which allocates the key salt as
  hashSize random bytes) the HMAC key is hashSize bytes, not saltSize.
  With the wrong length, HMAC verification failed even for legitimate,
  untampered files. Found and fixed by cross-checking against an
  independent from-scratch Python re-implementation over the real
  fixture.
Addresses 6 findings from the final whole-branch review of the encrypted
workbook reading plan:

- AgileKeyDerivation: reject an encryptedKeyValue that decrypts shorter than
  the declared key size with InvalidDataException, instead of letting the
  keyLen slice throw ArgumentOutOfRangeException on attacker-controlled input.
- EncryptionDescriptor: restrict blockSize to exactly 16 (AES-CBC's fixed
  block size) instead of the old [1,64] range, rejecting anything else as
  ExcelEncryptionException/UnsupportedScheme instead of letting aes.IV throw
  CryptographicException.
- Fuzz: remove the 1_000 MaxPasswordSpinCount override that made every input
  to the `encrypted` target dead-end on ExcelLimitExceededException before
  reaching any real derivation/decryption code; add a self-check
  (OpenEncryptedSeedForSelfCheck / VerifyEncryptedSeedReachesRealCode) that
  hard-fails if this regresses, and raise the mutation-fuzz regression test's
  round count from 200 to 1000.
- Excel.DetectFileFormat(ReadOnlyMemory<byte>): probe for encrypted OOXML via
  EncryptedPackageOpener.IsEncryptedMemory before falling back to Xls, mirroring
  the stream-based overload.
- Python docs: remove the stale claim that an explicit format bypasses CFB
  sniffing for encrypted files (fixed by an earlier merged change).
- Add `encrypted` to the nightly fuzz workflow's target matrix, and document
  the C++ password API in cpp/README.md.
…api-v2.2.0

chore: promote public API for v2.2.0
GabrielMarquezMatte and others added 23 commits August 27, 2026 15:14
Covers Excel.ParseCsvParallelAsync's architecture (chunk partitioning,
speculative boundary resolution, ordered merge) in ARCHITECTURE.md and
usage in README.md, plus CsvParallelParseBenchmark with generated
conversion-heavy and narrow-int corpora to measure it against the
sequential parser.
… path

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…et crash, Core reader reuse, README, polish)
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

Benchmark Results

Measured on ubuntu-latest (GitHub Actions). Runner noise may affect absolute numbers; use these for relative comparisons within a PR.

ExcelReader.Benchmarks.ColdStartBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-GOEFXF : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

InvocationCount=1  IterationCount=1  LaunchCount=16  
RunStrategy=ColdStart  UnrollFactor=1  WarmupCount=0  

Method Rows Mean Error StdDev Allocated
TypedParseFirstUse 200 32.77 ms 1.445 ms 1.419 ms 28.38 KB
RecordWriteFirstUse 200 22.86 ms 0.290 ms 0.285 ms 84.27 KB
FluentParseFirstUse 200 29.67 ms 0.269 ms 0.264 ms 30.76 KB
FluentParseWithAttributeFallbackFirstUse 200 35.50 ms 0.608 ms 0.597 ms 32.46 KB

ExcelReader.Benchmarks.CsvParseBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon 6973P-C 4.20GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
ExcelParserSync 50000 5.521 ms 0.0759 ms 0.0117 ms 1.00 0.00 46.8750 - 3.86 MB 1.00
ExcelParserAsync 50000 5.178 ms 0.2496 ms 0.0386 ms 0.94 0.01 46.8750 - 3.86 MB 1.00
Sep 50000 8.082 ms 0.2825 ms 0.0734 ms 1.46 0.01 46.8750 - 3.87 MB 1.00
Sylvan 50000 10.994 ms 0.5048 ms 0.1311 ms 1.99 0.02 125.0000 15.6250 10.95 MB 2.84
CsvHelperLib 50000 21.084 ms 0.5241 ms 0.0811 ms 3.82 0.02 156.2500 - 14.41 MB 3.73

ExcelReader.Benchmarks.CsvReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Allocated Alloc Ratio
ExcelReader 50000 5.334 ms 0.0183 ms 0.0028 ms 1.00 - - 368 B 1.00
ExcelReaderWide 50000 13.708 ms 0.1465 ms 0.0381 ms 2.57 - - 304 B 0.83
ExcelReaderAsync 50000 5.040 ms 0.0161 ms 0.0042 ms 0.94 - - 440 B 1.20
Sep 50000 12.030 ms 0.2585 ms 0.0671 ms 2.26 - - 4024 B 10.93
Sylvan 50000 6.262 ms 0.0729 ms 0.0189 ms 1.17 93.7500 7.8125 1688701 B 4,588.86
CsvHelperLib 50000 34.347 ms 0.3185 ms 0.0827 ms 6.44 866.6667 66.6667 15073424 B 40,960.39

ExcelReader.Benchmarks.CsvWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
ExcelReaderWriter 50000 9.836 ms 0.2818 ms 0.0732 ms 1.00 0.01 500.0000 500.0000 500.0000 4 MB 1.00
Sep 50000 13.412 ms 0.4117 ms 0.0637 ms 1.36 0.01 500.0000 500.0000 500.0000 4.01 MB 1.00
SylvanWriter 50000 10.549 ms 0.0609 ms 0.0158 ms 1.07 0.01 500.0000 500.0000 500.0000 4.04 MB 1.01
CsvHelperLib 50000 20.714 ms 0.4935 ms 0.1282 ms 2.11 0.02 1187.5000 656.2500 593.7500 13.79 MB 3.44

ExcelReader.Benchmarks.ParseBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon 6973P-C 4.20GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
ExcelParserSync 50000 13.978 ms 0.3404 ms 0.0884 ms 1.00 0.01 46.8750 3966.76 KB 1.000
ExcelParserSyncSharedStrings 50000 10.693 ms 0.0480 ms 0.0074 ms 0.77 0.00 15.6250 2357.25 KB 0.594
ExcelParserStructSync 50000 13.263 ms 0.0828 ms 0.0128 ms 0.95 0.01 15.6250 1623.03 KB 0.409
RefParserParseNamedSync 50000 12.063 ms 0.0443 ms 0.0069 ms 0.86 0.01 - 11.63 KB 0.003
ExcelParserAsync 50000 13.755 ms 0.0374 ms 0.0097 ms 0.98 0.01 46.8750 3968.91 KB 1.001
ExcelParserXlsbSync 50000 7.290 ms 0.0485 ms 0.0126 ms 0.52 0.00 46.8750 3968.86 KB 1.001
ExcelParserXlsbAsync 50000 7.374 ms 0.7097 ms 0.1098 ms 0.53 0.01 46.8750 3971.43 KB 1.001
MiniExcel 50000 173.132 ms 24.8061 ms 3.8388 ms 12.39 0.25 3000.0000 262290.47 KB 66.122
Sylvan 50000 52.699 ms 0.3236 ms 0.0501 ms 3.77 0.02 111.1111 10726.41 KB 2.704
SylvanAsync 50000 54.368 ms 0.3938 ms 0.1023 ms 3.89 0.02 100.0000 10735.13 KB 2.706

ExcelReader.Benchmarks.ReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
ExcelReader 50000 16.109 ms 0.1266 ms 0.0196 ms 1.00 0.00 - 11.13 KB 1.00
ExcelReaderAsync 50000 16.523 ms 0.1240 ms 0.0322 ms 1.03 0.00 - 13.27 KB 1.19
ExcelReaderXlsb 50000 6.798 ms 0.0288 ms 0.0045 ms 0.42 0.00 - 13.23 KB 1.19
ExcelReaderXlsbAsync 50000 7.032 ms 0.2936 ms 0.0762 ms 0.44 0.00 - 15.79 KB 1.42
ExcelReaderMaterialized 50000 18.821 ms 0.1405 ms 0.0217 ms 1.17 0.00 93.7500 1622.46 KB 145.74
MiniExcel 50000 233.858 ms 6.3941 ms 1.6605 ms 14.52 0.10 16000.0000 273787.42 KB 24,592.84
Sylvan 50000 52.966 ms 1.4427 ms 0.3747 ms 3.29 0.02 100.0000 1939.18 KB 174.19

ExcelReader.Benchmarks.RealDataReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Xlsx_ExcelReader 93.703 ms 1.2730 ms 0.3306 ms 1.00 0.00 - - 18232 B 1.00
Xlsx_Sylvan 316.789 ms 12.9553 ms 2.0048 ms 3.38 0.02 - - 659568 B 36.18
Xlsx_ExcelReader_Materialized 97.389 ms 0.4506 ms 0.0697 ms 1.04 0.00 - - 27928 B 1.53
Xlsx_ExcelReader_Prefetch 73.596 ms 6.2841 ms 1.6320 ms 0.79 0.02 - - 38480 B 2.11
Xlsx_ExcelReader_Memory 96.563 ms 1.3026 ms 0.3383 ms 1.03 0.00 - - 7120 B 0.39
Xlsx_ExcelReader_Memory_Prefetch 72.121 ms 11.0729 ms 1.7135 ms 0.77 0.02 - - 27512 B 1.51
Xlsm_ExcelReader 94.513 ms 0.5013 ms 0.0776 ms 1.01 0.00 - - 18232 B 1.00
Xlsm_Sylvan 321.583 ms 7.3598 ms 1.1389 ms 3.43 0.02 - - 659648 B 36.18
Xlsm_ExcelReader_Materialized 100.711 ms 1.5313 ms 0.2370 ms 1.07 0.00 - - 27928 B 1.53
Xlsm_ExcelReader_Prefetch 69.129 ms 19.0385 ms 4.9442 ms 0.74 0.05 - - 38482 B 2.11
Xlsm_ExcelReader_Memory 93.954 ms 5.0995 ms 1.3243 ms 1.00 0.01 - - 7120 B 0.39
Xlsm_ExcelReader_Memory_Prefetch 72.120 ms 10.5791 ms 1.6371 ms 0.77 0.02 - - 27384 B 1.50
Xlsb_ExcelReader 35.534 ms 0.3530 ms 0.0917 ms 0.38 0.00 - - 19208 B 1.05
Xlsb_Sylvan 43.939 ms 1.3102 ms 0.3403 ms 0.47 0.00 - - 346673 B 19.01
Xlsb_ExcelReader_Materialized 36.619 ms 0.1918 ms 0.0498 ms 0.39 0.00 - - 28904 B 1.59
Xlsb_ExcelReader_Prefetch 20.241 ms 2.8832 ms 0.7488 ms 0.22 0.01 - - 27386 B 1.50
Xlsb_ExcelReader_Memory 35.440 ms 0.4768 ms 0.0738 ms 0.38 0.00 - - 8848 B 0.49
Xlsb_ExcelReader_Memory_Prefetch 20.851 ms 4.7363 ms 1.2300 ms 0.22 0.01 - - 17636 B 0.97
Xls_ExcelReader 15.304 ms 0.1549 ms 0.0402 ms 0.16 0.00 - - 12176 B 0.67
Xls_Sylvan 27.308 ms 0.4280 ms 0.0662 ms 0.29 0.00 - - 190366 B 10.44
Xls_ExcelReader_Materialized 17.080 ms 0.0795 ms 0.0206 ms 0.18 0.00 - - 21872 B 1.20
Xls_ExcelReader_Memory 15.325 ms 0.0986 ms 0.0256 ms 0.16 0.00 - - 12176 B 0.67
Csv_ExcelReader 8.709 ms 0.0725 ms 0.0112 ms 0.09 0.00 - - 304 B 0.02
Csv_Sylvan 16.166 ms 0.2822 ms 0.0733 ms 0.17 0.00 2218.7500 125.0000 37484040 B 2,055.95
Csv_ExcelReader_Materialized 30.166 ms 0.4214 ms 0.1094 ms 0.32 0.00 2218.7500 - 37442912 B 2,053.69
Csv_ExcelReader_Memory 8.424 ms 0.1095 ms 0.0169 ms 0.09 0.00 - - 240 B 0.01

ExcelReader.Benchmarks.RecordWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
Xlsx 50000 27.050 ms 0.1429 ms 0.0371 ms 1.00 500.0000 500.0000 500.0000 4.02 MB 1.00
Xlsb 50000 10.458 ms 0.1206 ms 0.0313 ms 0.39 500.0000 500.0000 500.0000 4.02 MB 1.00
Xls 50000 7.702 ms 0.0449 ms 0.0117 ms 0.28 273.4375 273.4375 273.4375 4.03 MB 1.00
Csv 50000 10.066 ms 0.0981 ms 0.0255 ms 0.37 500.0000 500.0000 500.0000 4 MB 1.00

ExcelReader.Benchmarks.WriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
ExcelReaderWriter 50000 23.767 ms 0.0774 ms 0.0201 ms 1.00 0.00 500.0000 500.0000 500.0000 4.02 MB 1.00
ExcelReaderWriterSharedStrings 50000 23.706 ms 0.1477 ms 0.0229 ms 1.00 0.00 500.0000 500.0000 500.0000 4.06 MB 1.01
ExcelReaderWriterPrefetch 50000 20.017 ms 4.7362 ms 1.2300 ms 0.84 0.05 468.7500 468.7500 468.7500 4.03 MB 1.00
ExcelReaderXlsbWriter 50000 9.852 ms 0.3643 ms 0.0564 ms 0.41 0.00 500.0000 500.0000 500.0000 4.02 MB 1.00
ExcelReaderXlsbWriterSharedStrings 50000 9.403 ms 0.0697 ms 0.0108 ms 0.40 0.00 500.0000 500.0000 500.0000 4.06 MB 1.01
ExcelReaderXlsbWriterPrefetch 50000 9.279 ms 0.7592 ms 0.1972 ms 0.39 0.01 484.3750 484.3750 484.3750 4.03 MB 1.00
MiniExcel 50000 106.551 ms 9.2701 ms 1.4346 ms 4.48 0.05 5000.0000 1000.0000 1000.0000 84.89 MB 21.11
SpreadCheetah 50000 21.104 ms 0.4461 ms 0.1158 ms 0.89 0.00 1437.5000 718.7500 718.7500 15.84 MB 3.94

ExcelReader.Benchmarks.XlsReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Allocated Alloc Ratio
ExcelReader 50000 4.317 ms 0.0717 ms 0.0186 ms 1.00 - - 3.04 KB 1.00
ExcelReaderAsync 50000 4.411 ms 0.0547 ms 0.0085 ms 1.02 - - 3.11 KB 1.02
Sylvan 50000 8.171 ms 0.1031 ms 0.0268 ms 1.89 93.7500 15.6250 1717.73 KB 565.22

ExcelReader.Benchmarks.XlsWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
XlsWriter 50000 7.252 ms 0.0352 ms 0.0091 ms 1.00 0.00 492.1875 492.1875 492.1875 16.03 MB 1.00
XlsxWriter 50000 23.645 ms 0.6995 ms 0.1816 ms 3.26 0.02 500.0000 500.0000 500.0000 4.02 MB 0.25

@codecov-commenter

codecov-commenter commented Aug 30, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 85.95420% with 184 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.45%. Comparing base (2c3bbe5) to head (748a86f).

Files with missing lines Patch % Lines
src/ExcelReader.Core/Crypto/AgileKeyDerivation.cs 80.82% 20 Missing and 8 partials ⚠️
src/ExcelReader.Arrow/ColumnAppender.cs 78.35% 14 Missing and 7 partials ⚠️
src/ExcelReader.Core/Reader/Excel.cs 82.30% 14 Missing and 6 partials ⚠️
.../ExcelReader.Core/Crypto/DecryptedPackageStream.cs 87.50% 11 Missing and 7 partials ⚠️
.../ExcelReader.Core/Crypto/EncryptedPackageOpener.cs 84.82% 13 Missing and 4 partials ⚠️
...ExcelReader.Core/Parser/Internal/CsvChunkWorker.cs 83.65% 10 Missing and 7 partials ⚠️
...lReader.Core/Parser/Internal/ParallelCsvFactory.cs 75.00% 6 Missing and 5 partials ⚠️
...celReader.Core/Parser/Internal/RangedFileStream.cs 70.37% 8 Missing ⚠️
src/ExcelReader.Core/Crypto/PackageIntegrity.cs 79.31% 5 Missing and 1 partial ⚠️
...elReader.Core/Parser/Internal/CsvSourceResolver.cs 66.66% 4 Missing and 2 partials ⚠️
... and 11 more
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #98      +/-   ##
==========================================
+ Coverage   85.12%   85.45%   +0.33%     
==========================================
  Files         136      154      +18     
  Lines       10041    11296    +1255     
  Branches     1868     2077     +209     
==========================================
+ Hits         8547     9653    +1106     
- Misses       1162     1255      +93     
- Partials      332      388      +56     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

GabrielMarquezMatte added 3 commits August 30, 2026 14:48
rust.yml, python.yml, and cpp.yml each called build_native.py
independently, publishing ExcelReader.Native from scratch 3x per OS
(9 NativeAOT publishes per PR, on top of ci.yml's aot-sanity build).
Replaced them with native-bindings.yml: a single build-native job per
OS uploads the binary as an artifact, and test-rust/test-python/test-cpp
jobs download it instead of rebuilding.

build-native has no `needs`, so its failure fails a required check
directly rather than only cascading into skipped (and thus
GitHub-treated-as-passed) downstream jobs.

Also added missing caches: NuGet in build-native, cargo via
Swatinem/rust-cache in test-rust, pip in test-python.

Job names kept identical to the old workflows so the branch protection
required status checks (already updated on GitHub, plus the 3 new
Build native library contexts) still match.
Adds a changes job (dorny/paths-filter) at the top of native-bindings.yml.
build-native runs when any of native/rust/python/cpp paths changed;
test-rust/test-python/test-cpp each additionally require native OR
their own language's paths. A PR touching only C# core code or docs no
longer pays for 3 OS x 3 binding matrices.

Filtering is inside the workflow (per-job `if:`), not at
`on.pull_request.paths` - that form would skip the whole workflow run
for a non-matching PR, leaving its required status checks stuck at
"expected" and blocking merge forever. A job skipped via `if:` still
posts a real skipped conclusion, which GitHub treats as passing a
required check.

test-* jobs check needs.changes.outputs.*, not build-native's result,
so an explicit `if:` doesn't accidentally bypass a genuine build-native
failure - if build-native really fails, the artifact never exists and
the test job's download-artifact step fails loudly instead of being
silently skipped.

`changes` itself is now a required check too: if paths-filter errors,
that must fail loud rather than making every downstream `if:` evaluate
false and skip silently.
actions/upload-artifact strips the executable bit. dlopen() itself
tolerates that, but the NativeAOT runtime's PAL init - triggered by
the first real P/Invoke, not by CDLL()/dlopen() - remaps the file with
PROT_EXEC and hits SIGABRT (uncatchable from Python/Rust/C++, not a
graceful load error) when that fails. Missed this in the artifact
consolidation since none of the old rust.yml/python.yml/cpp.yml built
the binary in a separate job from the one that used it.

Observed as `Fatal Python error: Aborted` in test_native.py::test_library_loads,
crashing inside lib.xl_abi_version() right after a successful CDLL()
call - confirms the file loaded but the runtime's own remap failed.

chmod +x after download in test-rust/test-python/test-cpp, Linux/macOS
only (no such bit on Windows).
@GabrielMarquezMatte
GabrielMarquezMatte merged commit c7a05b1 into master Aug 30, 2026
59 of 60 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants