xlsxrb 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1be4d5d307820e8ed44e46d2a990e069501f413cace5e4920f61c6c43f964ff
4
- data.tar.gz: 22df70618c1f06ccc7bdcd3e4a1d0529bc6385f122f9563e29370ef13e16dcf1
3
+ metadata.gz: 9930be8e04f9dee4e380c4789398da0b6a95b0c54d2a157265a2d1ee14340132
4
+ data.tar.gz: '086f0abccab70ef69d1e885b9f8d0ec69a77e81e3067fa718764c24c0f37a13f'
5
5
  SHA512:
6
- metadata.gz: 80cd57b81f5649d930e873b354f2b587a05c19557ff2906ffc85aadd630d38db5487ba1bfda4a4588658c2902343658aac09c3cfbd59e3550d35be45187ae80d
7
- data.tar.gz: 474385c247bed4e3b35182f7e3ecb4949d2eb5fa4adaea91b221179103cc1dc278ccd041b50cffd3dd3c3e31572e29e816f7a03ac99580f30096d0cebdd18290
6
+ metadata.gz: 2097ccb3e8b3e5344e5d55c459db6b06c298b116074bcc41a06fafa034eda67bf0c46d7eaa58184aebc970812cfa91f68388c4a6b4e7d7069a45114507aa9e83
7
+ data.tar.gz: d95b39449b15b2eec1415d1ae65e398cc2c006adefd19b54340727fc4ee2135322040a73bf0b3d2b0ca8f6d171153e88b8f64badb53b7fb841e9d7b8a87afd55
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
- ## [Unreleased]
2
- - No unreleased changes.
1
+ ## [0.1.9] - 2026-08-18
2
+
3
+ ### Added
4
+ - **Password Protection & Document Encryption ([MS-OFFCRYPTO] / [MS-CFB])**: Full native Pure-Ruby support for reading, writing, and modifying password-protected Excel spreadsheets without any external C-extension dependencies.
5
+ - **Standard Encryption**: AES-128-ECB and SHA-1 Key Derivation with CryptoAPI 50,000-spin hashing, fully interoperable across Microsoft Excel, LibreOffice, and Google Sheets.
6
+ - **Agile Encryption**: Modern AES-256-CBC, PBKDF2/SHA-512, and HMAC-SHA512 data integrity verification.
7
+ - **Compound File Binary (CFB) Engine**: Pure-Ruby reader and writer for OLE structured storage containers with Mini Stream, FAT/MiniFAT sectors, and Red-Black tree directory management.
8
+ - **Transparent Public API Integration**: Added `password:` and `encryption_mode:` arguments to `Xlsxrb.read`, `Xlsxrb.write`, and `Xlsxrb.modify`.
9
+ - **Security & Threat Model Hardening**:
10
+ - Constant-time hash verification via `OpenSSL.secure_compare` to prevent timing attacks (CWE-208).
11
+ - CSPRNG-backed salt, IV, and session key generation via `SecureRandom` (CWE-330).
12
+ - Robust DoS defense: spinCount limit ($\le 10\text{M}$), CFB circular sector chain loop detection in directory/FAT parsing, and `total_size` bounds validation (CWE-400, CWE-835).
13
+ - Strict exception hierarchy (`EncryptedFileError`, `InvalidPasswordError`, `DecryptionError`).
14
+ - **Cross-Platform & Interoperability Validation**: Bidirectional validation with Microsoft .NET OpenXML SDK and LibreOffice Calc.
15
+ - **WebAssembly (ruby.wasm) Support**: Pre-packaged `docs/wasm/ruby.wasm` updated with document encryption support for browser playground.
3
16
 
4
17
  ## [0.1.8] - 2026-08-18
5
18
 
data/README.md CHANGED
@@ -6,18 +6,18 @@ A Ruby library for reading and writing XLSX files with streaming support.
6
6
 
7
7
  The Ruby ecosystem already has great XLSX libraries. Each is well-designed for its purpose:
8
8
 
9
- | Library | Read | Write | Model | Write String Storage | Rich Formatting |
10
- | ---------------------------------------------------------- | ---- | ----- | --------------------- | -------------------- | --------------- |
11
- | [roo](https://rubygems.org/gems/roo) | ✅ | ❌ | Streaming | N/A (Read-only) | ⚠️ (Formulas, Basic styles) |
12
- | [creek](https://rubygems.org/gems/creek) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Raw cell values) |
13
- | [xsv](https://rubygems.org/gems/xsv) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Fast plain text) |
14
- | [simple_xlsx_reader](https://rubygems.org/gems/simple_xlsx_reader) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Plain data & types) |
15
- | [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) | ❌ | ✅ | In-Memory | Inline (opt: SST) | ✅ (Charts, Styles) |
16
- | [write_xlsx](https://rubygems.org/gems/write_xlsx) | ❌ | ✅ | In-Memory | SST | ✅ (Charts, Styles) |
17
- | [xlsxtream](https://rubygems.org/gems/xlsxtream) | ❌ | ✅ | Streaming | Inline (opt: SST) | ❌ (Plain data only) |
18
- | [fast_excel](https://rubygems.org/gems/fast_excel) | ❌ | ✅ | Streaming (C Ext) | SST (opt: Inline) | ⚠️ (Basic styles) |
19
- | [rubyXL](https://rubygems.org/gems/rubyXL) | ✅ | ✅ | In-Memory | Inline / Direct | ✅ (DOM editing) |
20
- | **[xlsxrb](https://github.com/niku/xlsxrb)** | ✅ | ✅ | **Streaming / In-Memory** | **SST** | ✅ **(Full Features)** |
9
+ | Library | Read | Write | Model | Write String Storage | Rich Formatting | Password Encryption |
10
+ | ---------------------------------------------------------- | ---- | ----- | --------------------- | -------------------- | --------------- | :-----------------: |
11
+ | [roo](https://rubygems.org/gems/roo) | ✅ | ❌ | Streaming | N/A (Read-only) | ⚠️ (Formulas, Basic styles) | ❌ |
12
+ | [creek](https://rubygems.org/gems/creek) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Raw cell values) | ❌ |
13
+ | [xsv](https://rubygems.org/gems/xsv) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Fast plain text) | ❌ |
14
+ | [simple_xlsx_reader](https://rubygems.org/gems/simple_xlsx_reader) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Plain data & types) | ❌ |
15
+ | [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) | ❌ | ✅ | In-Memory | Inline (opt: SST) | ✅ (Charts, Styles) | ❌ |
16
+ | [write_xlsx](https://rubygems.org/gems/write_xlsx) | ❌ | ✅ | In-Memory | SST | ✅ (Charts, Styles) | ❌ |
17
+ | [xlsxtream](https://rubygems.org/gems/xlsxtream) | ❌ | ✅ | Streaming | Inline (opt: SST) | ❌ (Plain data only) | ❌ |
18
+ | [fast_excel](https://rubygems.org/gems/fast_excel) | ❌ | ✅ | Streaming (C Ext) | SST (opt: Inline) | ⚠️ (Basic styles) | ❌ |
19
+ | [rubyXL](https://rubygems.org/gems/rubyXL) | ✅ | ✅ | In-Memory | Inline / Direct | ✅ (DOM editing) | ❌ |
20
+ | **[xlsxrb](https://github.com/niku/xlsxrb)** | ✅ | ✅ | **Streaming / In-Memory** | **SST** | ✅ **(Full Features)** | ✅ **(Standard & Agile)** |
21
21
 
22
22
  Each of these libraries makes deliberate tradeoffs, and they do so thoughtfully:
23
23
  * **Memory & Execution Model (Streaming vs In-Memory)**: Streaming libraries write or read rows sequentially on-the-fly to maintain a constant, low-memory footprint regardless of row count. In-memory libraries build complete document object trees, offering flexible random access and cell updates at the cost of high RAM usage on large sheets.
@@ -197,6 +197,43 @@ Xlsxrb.build do |builder|
197
197
  end
198
198
  ```
199
199
 
200
+ ### Password Protection & Document Encryption ([MS-OFFCRYPTO])
201
+
202
+ `xlsxrb` natively supports reading and writing password-protected Excel files with zero external C-extension dependencies, fully compliant with **[MS-OFFCRYPTO]** (Standard Encryption & Agile Encryption) and **[MS-CFB]** (Compound File Binary Format):
203
+
204
+ #### Writing Password-Protected Spreadsheets
205
+ ```ruby
206
+ # 1. Streaming write with password protection
207
+ Xlsxrb.write("confidential.xlsx", password: "SecretPassword123") do |writer|
208
+ writer.sheet("Financials") do |sheet|
209
+ sheet.row(["Account", "Balance"])
210
+ sheet.row(["Assets", 5_000_000])
211
+ end
212
+ end
213
+
214
+ # 2. In-Memory write with password protection
215
+ wb = Xlsxrb.build do |b|
216
+ b.sheet("Private") { |s| s.row(["Confidential", 100]) }
217
+ end
218
+ encrypted_binary = Xlsxrb.write(wb, password: "SecretPassword123")
219
+ ```
220
+
221
+ #### Reading Password-Protected Spreadsheets
222
+ ```ruby
223
+ # 1. Read directly with password (O(1) constant memory streaming)
224
+ Xlsxrb.read("confidential.xlsx", password: "SecretPassword123") do |sheet|
225
+ sheet.each_row { |row| puts row.cells.map(&:value) }
226
+ end
227
+
228
+ # 2. In-Memory load with password
229
+ workbook = Xlsxrb.read("confidential.xlsx", password: "SecretPassword123").load
230
+ puts workbook.sheets.first["A1"].value
231
+
232
+ # Attempting to read without password or with invalid password raises clear exceptions:
233
+ # Xlsxrb::EncryptedFileError -> Missing password
234
+ # Xlsxrb::InvalidPasswordError -> Incorrect password
235
+ ```
236
+
200
237
  ### IDE Autocompletion & Ruby LSP Support
201
238
 
202
239
  `xlsxrb` bundles a native **Ruby LSP Add-on** (`RubyLsp::Xlsxrb::Addon`) and full **RBS signatures**, enabling zero-configuration method autocompletion and rich Markdown documentation in VS Code and other LSP-enabled editors.
@@ -299,7 +336,7 @@ To support reliability, compliance with the ECMA-376 specification, and consiste
299
336
  * **Visual Regression Testing (VRT)**: Spreadsheets are rendered via a headless LibreOffice Calc engine and compared pixel-by-pixel against visual baselines to catch subtle rendering regressions.
300
337
 
301
338
  ### Performance & Types
302
- * **Continuous Benchmarking**: Memory usage and processing speeds are profiled in CI on large datasets to prevent performance regressions and OOM leaks.
339
+ * **Continuous Performance & Memory Validation**: Memory retention and processing throughput are profiled in CI on large datasets to prevent regressions and OOM leaks.
303
340
  * **Runtime Type Validation**: Strong dynamic typing using `RBS::Test` to ensure the library's types are perfectly sound at runtime.
304
341
 
305
342
  For a comprehensive breakdown of our QA matrix, see [docs/QUALITY_ASSURANCE.md](docs/QUALITY_ASSURANCE.md). For details on running tests locally, see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
data/Rakefile CHANGED
@@ -93,8 +93,8 @@ end
93
93
  Rake::TestTask.new(test: :ensure_reader_fixtures) do |t|
94
94
  t.libs << "test"
95
95
  t.libs << "lib"
96
- t.test_files = FileList["test/**/*_test.rb"]
97
- workers = ENV.fetch("TEST_WORKERS", Etc.nprocessors)
96
+ t.test_files = FileList["test/xlsxrb/**/*_test.rb", "test/*_test.rb", "test/contract/**/*_test.rb"]
97
+ workers = ENV.fetch("TEST_WORKERS", [Etc.nprocessors, 4].min)
98
98
  t.options = "--parallel --n-workers=#{workers}"
99
99
  end
100
100
 
@@ -121,6 +121,24 @@ namespace :test do
121
121
  t.options = "--parallel --n-workers=#{workers}"
122
122
  end
123
123
 
124
+ desc "Run property-based tests (PBT)"
125
+ Rake::TestTask.new(:pbt) do |t|
126
+ t.libs << "test"
127
+ t.libs << "lib"
128
+ t.test_files = FileList["test/pbt/**/*_test.rb"]
129
+ workers = ENV.fetch("TEST_WORKERS", Etc.nprocessors)
130
+ t.options = "--parallel --n-workers=#{workers}"
131
+ end
132
+
133
+ desc "Run memory and performance tests"
134
+ Rake::TestTask.new(:performance) do |t|
135
+ t.libs << "test"
136
+ t.libs << "lib"
137
+ t.test_files = FileList["test/performance/**/*_test.rb"]
138
+ end
139
+ desc "Alias for test:performance"
140
+ task perf: :performance
141
+
124
142
  Rake::TestTask.new(:e2e) do |t|
125
143
  t.libs << "test"
126
144
  t.libs << "lib"
data/docs/DEVELOPMENT.md CHANGED
@@ -14,17 +14,25 @@ To run the different tiers of our testing strategy:
14
14
  ```bash
15
15
  bundle exec rake test:contract
16
16
  ```
17
- 3. **Interoperability (E2E) Tests:**
17
+ 3. **Property-Based Tests (PBT):**
18
+ ```bash
19
+ bundle exec rake test:pbt
20
+ ```
21
+ 4. **Memory & Performance Tests:**
22
+ ```bash
23
+ bundle exec rake test:perf
24
+ ```
25
+ 5. **Interoperability (E2E) Tests:**
18
26
  Requires .NET SDK to be installed (pre-configured in Dev Container).
19
27
  ```bash
20
28
  bundle exec rake test:e2e
21
29
  ```
22
- 4. **Visual Regression Tests (VRT):**
30
+ 6. **Visual Regression Tests (VRT):**
23
31
  Requires LibreOffice, ImageMagick, and `poppler-utils`.
24
32
  ```bash
25
33
  bundle exec rake test:visual
26
34
  ```
27
- 5. **Run All Tests:**
35
+ 7. **Run All Tests:**
28
36
  ```bash
29
37
  bundle exec rake test
30
38
  ```
@@ -14,13 +14,12 @@ Below is an overview of the inspection mechanisms, when they run, the quality at
14
14
  | **Dependabot** | `.github/dependabot.yml` | - | - | ⭕ | Currency / Maintenance | Repository Monitoring | Outdated dependencies or CI actions. |
15
15
  | **Unit & Contract Tests** | `rake test:unit test:contract` | ⭕ | ⭕ | - | Accuracy / Functional Reqs | Dynamic Analysis (Assertions) | Method specification violations, unexpected return values, edge-case failures. |
16
16
  | **Runtime Type Validation (RBS::Test)** | `rake test:rbs` | △ (Opt-in) | ⭕ | - | Type Safety (Dynamic) | Dynamic Analysis (Runtime Hooks) | Type errors slipping past static checks, divergence between RBS docs and implementation. |
17
- | **Property-Based Testing (PBT)** | Included in `rake test:unit` | ⭕ | ⭕ | - | Robustness / Exhaustiveness | Automated Random Generation | Crashes caused by "unexpected inputs" (e.g., empty strings, huge numbers, special symbols like `=`). |
18
-
17
+ | **Property-Based Testing (PBT)** | `rake test:pbt` | ⭕ | ⭕ | - | Robustness / Exhaustiveness | Automated Random Generation | Crashes caused by "unexpected inputs" (e.g., empty strings, huge numbers, special symbols like `=`). |
19
18
  | **Security Validation (DoS Protection)** | Included in `rake test:unit` | ⭕ | ⭕ | - | Availability / Safety | Dynamic Analysis (Malicious Input) | Memory/disk exhaustion from ZIP bombs, infinite parsing loops from malformed files. |
20
19
  | **Concurrency Validation (Thread/Ractor)** | Included in `rake test:unit` | ⭕ | ⭕ | - | Thread Safety | Dynamic Analysis (Parallel Execution) | Global variable pollution, data mixing during concurrent request processing. |
21
20
  | **XSD Schema Validation** | Included in `rake test:unit` | ⭕ | ⭕ | - | Compatibility / Compliance | Structural Validation | "We found a problem with some content in it" errors when opening in Excel. |
22
21
  | **E2E Interoperability Tests** | `rake test:e2e` | △ (Opt-in) | ⭕ | - | Compatibility (Real-world) | 3rd-party SDK Execution | Structural defects so severe that the official .NET SDK cannot read them. |
23
- | **Load / Stress Testing** | Action: `benchmark.yml` | - | ⭕ | - | Performance / Stability | Massive Data Generation (e.g., 10k+ rows) | Out of Memory (OOM) crashes or extreme delays when processing large datasets. |
24
- | **Memory & Speed Benchmark** | Action: `benchmark.yml` | - | | - | Performance | Continuous Profiling | Memory leaks, severe performance degradation due to inefficient loop additions. |
22
+ | **Memory & Performance Tests** | `rake test:perf` / Action: `performance.yml` | | ⭕ | - | Performance / Stability | Continuous Profiling (10k+ rows) | Out of Memory (OOM) leaks or unbounded memory retention in streaming mode. |
23
+ | **Ecosystem Benchmarks** | `ruby benchmark.rb` | | - | - | Performance Comparison | Multi-Gem Isolated Profiling (1M cells) | Throughput and allocation comparison against competing libraries. |
25
24
  | **Visual Regression Testing (VRT)** | `rake test:visual` | - | ⭕ | - | Visual Accuracy (UI/UX) | Headless Rendering / Pixel Diff | Visual bugs like "cell background colors dropping" or "chart layouts breaking" after code changes. |
26
25
 
data/docs/SPEC_SOURCES.md CHANGED
@@ -22,8 +22,11 @@ To address gaps between the official ECMA standard and actual implementations in
22
22
  - **Role:** Identifies how Excel actually reads/writes files, including deviations and compatibility behaviors.
23
23
  - **Referenced Version:** July 2024 / Version 12.0 (or current release).
24
24
  3. **[[MS-OFFCRYPTO]: Office Document Cryptography Structure](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-offcrypto/)**
25
- - **Role:** Provides details on encryption, passwords, and hashing algorithms used for document/sheet protection.
25
+ - **Role:** Provides details on encryption, passwords, and hashing algorithms used for document/sheet protection (Agile Encryption and Standard Encryption).
26
26
  - **Referenced Version:** July 2024 / Version 12.0 (or current release).
27
+ 4. **[[MS-CFB]: Compound File Binary File Format](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/)**
28
+ - **Role:** Container format (OLE structured storage) wrapping encrypted Office documents (`EncryptionInfo` and `EncryptedPackage` streams).
29
+ - **Referenced Version:** July 2024 / Version 14.0 (or current release).
27
30
 
28
31
  ### ISO/IEC 29500 Note
29
32
  ISO/IEC 29500 is contents-wise equivalent to ECMA-376. However, ISO/IEC 29500 requires paid purchase in general, whereas ECMA-376 is freely available. Therefore, we primarily cite ECMA-376 sections.
@@ -40,3 +43,6 @@ When implementing features or fixing bugs that depend on specific behaviors defi
40
43
  | Shared Strings | ECMA-376 Part 1 | §18.4 (Shared String Table) | Handling of `<sst>` and `<si>` for cell value reuse |
41
44
  | Styles & Formatting | ECMA-376 Part 1 | §18.8 (Styles) | Cell style XF indexes, font, fill, border mappings |
42
45
  | Hyperlinks | ECMA-376 Part 1 | §18.3.1.48 (hyperlink) | Worksheet hyperlinks referencing external URLs or internal targets |
46
+ | Document Encryption (Agile) | [MS-OFFCRYPTO] | §2.3.4 (Agile Encryption) | AES-256-CBC, PBKDF2/SHA-512, HMAC-SHA512 data integrity |
47
+ | Document Encryption (Standard) | [MS-OFFCRYPTO] | §2.3.6 (Standard Encryption) | AES-128-ECB, SHA-1 with CryptoAPI 50,000-spin key derivation |
48
+ | Encryption Container | [MS-CFB] | §2 (Compound File Structure) | Mini Stream, FAT/MiniFAT sectors, and Red-Black tree directory |
data/docs/wasm/ruby.wasm CHANGED
Binary file
@@ -75,7 +75,7 @@ module Xlsxrb
75
75
  # @return [Enumerator, void]
76
76
  # @api public
77
77
  #: () { (Elements::Cell) -> void } -> void
78
- #: | () -> Enumerator[Elements::Cell, void]
78
+ #: () -> Enumerator[Elements::Cell, void]
79
79
  def each(&)
80
80
  return to_enum(:each) unless block_given?
81
81
 
@@ -94,7 +94,7 @@ module Xlsxrb
94
94
  # @return [Enumerator, void]
95
95
  # @api public
96
96
  #: () { (Elements::Cell) -> void } -> void
97
- #: | () -> Enumerator[Elements::Cell, void]
97
+ #: () -> Enumerator[Elements::Cell, void]
98
98
  def each_cell(&)
99
99
  return to_enum(:each_cell) unless block_given?
100
100
 
@@ -38,7 +38,7 @@ module Xlsxrb
38
38
  # @yieldparam sheet [Elements::Worksheet]
39
39
  # @return [Enumerator, void]
40
40
  #: () { (Elements::Worksheet) -> void } -> void
41
- #: | () -> Enumerator[Elements::Worksheet, void]
41
+ #: () -> Enumerator[Elements::Worksheet, void]
42
42
  def each(&)
43
43
  sheets.each(&)
44
44
  end
@@ -25,7 +25,7 @@ module Xlsxrb
25
25
  # @param charts [Array<Hash>] Charts in the sheet.
26
26
  # @param unmapped_data [Hash] Additional metadata for round-tripping.
27
27
  # @param errors [Array<String>, nil] Validation errors.
28
- #: (name: String, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
28
+ #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
29
29
  def initialize(name:, rows: [], columns: [], charts: [], unmapped_data: {}, errors: nil)
30
30
  @name = name
31
31
  @rows = (rows || []).freeze
@@ -48,7 +48,7 @@ module Xlsxrb
48
48
  # @return [Enumerator, void]
49
49
  # @api public
50
50
  #: () { (Elements::Row) -> void } -> void
51
- #: | () -> Enumerator[Elements::Row, void]
51
+ #: () -> Enumerator[Elements::Row, void]
52
52
  def each(&)
53
53
  return to_enum(:each) unless block_given?
54
54
 
@@ -67,7 +67,7 @@ module Xlsxrb
67
67
  # @return [Enumerator, void]
68
68
  # @api public
69
69
  #: () { (Elements::Row) -> void } -> void
70
- #: | () -> Enumerator[Elements::Row, void]
70
+ #: () -> Enumerator[Elements::Row, void]
71
71
  def each_row(&)
72
72
  return to_enum(:each_row) unless block_given?
73
73
 
@@ -81,7 +81,7 @@ module Xlsxrb
81
81
  # @return [Enumerator, void]
82
82
  # @api public
83
83
  #: () { (Elements::Cell) -> void } -> void
84
- #: | () -> Enumerator[Elements::Cell, void]
84
+ #: () -> Enumerator[Elements::Cell, void]
85
85
  def each_cell(&)
86
86
  return to_enum(:each_cell) unless block_given?
87
87