data_redactor 0.9.0 → 0.10.1

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: 007d59e430d1675a13b84670f6c34c300f8b72fd7ee4744aa191f846bb89b072
4
- data.tar.gz: a23f3b99c3ead341d2c9415a1b4b2eb32a45ee002f052a8e58d928eb1ce03919
3
+ metadata.gz: e744f5e18d5ce2311c21197b6d47b20d6032a38a72c8bfb2287a80219b8e2d77
4
+ data.tar.gz: cfd314afec1d018175a8424d81c12958582d5ddd9c16d05baa6048c17bba8ca8
5
5
  SHA512:
6
- metadata.gz: ccd4f6f97a0110585e4f43f9402eac2a1f57b2aef01a3c6870f0e57ea578377291a7367ee924585d9d11e92af98f4178bb0b9488c1a24a2338f6a41936efad30
7
- data.tar.gz: 5281171119b4892167a6b1d55e0996db47408c8a6d334656998f8f2ca50794a3a7b5c987132369ca32965da0943f954eab61f34f5a97c683b8a14851e9beca1e
6
+ metadata.gz: f7ea267c8927f9852621180d77530818980af4fc5c089b7db95e6b0f980c1f3cb09c11806e9459241ca363b5c06203f34ede5e2b1473b1a2046a9a4e37e63fbb
7
+ data.tar.gz: 4081b898b339423bb5dd06f557e32bea5c7242869fc61a6b0a2447053ddcec28a751768b83d1ba66a2fcb82755ceb654c6e95052bc01e2cb23e1fac4b44b8d37
data/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.1] - 2026-06-10
11
+
12
+ ### Fixed
13
+ - **musl/Alpine load failure** — the `hashicorp_vault_batch_token` pattern used a
14
+ `{138,300}` interval whose upper bound exceeds POSIX `RE_DUP_MAX` (255). glibc
15
+ accepts it, but musl's `regcomp` rejects it ("Invalid contents of {}"), so the
16
+ native musl gem raised at load (`require "data_redactor"`) on Alpine. Capped the
17
+ bound at 255; tokens are still neutralized (prefix + 251+ chars redacted).
18
+
19
+ ## [0.10.0] - 2026-06-09
20
+
21
+ ### Changed
22
+ - **Engine rewrite (v19 hybrid)** — `redact` and `scan` now run through a
23
+ Thompson NFA → bytecode → lazy-DFA multi-pattern engine (v19) for all 88
24
+ built-in patterns, replacing the previous per-pattern POSIX `regexec` loop.
25
+ Custom patterns (`add_pattern`) continue to use the glibc path (hybrid split
26
+ — required for correct UTF-8 multibyte character-class matching in user regex).
27
+ - Throughput on a 1 MB log: **~8.4× faster** than the previous C engine
28
+ (0.87 i/s → 7.27 i/s); **2.25× faster** than pure-Ruby `gsub` (was 4×
29
+ slower). Small per-call strings: 1.7–2.3× faster (was 3–4.6× slower).
30
+ - Overlap resolution: built-in matches are now resolved by an index-order
31
+ greedy claim (`mm_resolve`) that reproduces today's sequential per-pattern
32
+ rewrite semantics exactly. The one accepted divergence (rewrite-created
33
+ boundary when two secrets abut with no separator) is documented in
34
+ `TODO.md §1d` and pinned by `DIVERGENCE` specs.
35
+ - `rb_data_redactor_scan`: coordinate mapping (`repl_log` / `WORKING_TO_ORIG`)
36
+ replaced by direct original-frame offset emission from the v19 engine; custom
37
+ patterns use a lightweight offset-walk over the built-in event list.
38
+
39
+ ### Fixed
40
+ - **Swiss AHV false-negative** — boundary-wrapped patterns with a
41
+ start-anchored required literal now correctly set `max_back = 1` (not 0) so
42
+ the literal-skip does not overshoot the boundary byte. `756.1234.5678.90`
43
+ now matches as expected. (Pre-existing bug in the old engine, caught by
44
+ going live.)
45
+
10
46
  ## [0.9.0] - 2026-05-22
11
47
 
12
48
  ### Added
@@ -177,7 +213,9 @@ features as 0.7.1 plus the pipeline fix.
177
213
  - `DataRedactor.redact(text)` module function returning the input with every match replaced by `[REDACTED]`.
178
214
  - RSpec suite with one example per pattern.
179
215
 
180
- [Unreleased]: https://github.com/danielefrisanco/data_redactor/compare/v0.9.0...HEAD
216
+ [Unreleased]: https://github.com/danielefrisanco/data_redactor/compare/v0.10.1...HEAD
217
+ [0.10.1]: https://github.com/danielefrisanco/data_redactor/compare/v0.10.0...v0.10.1
218
+ [0.10.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.9.0...v0.10.0
181
219
  [0.9.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.8.0...v0.9.0
182
220
  [0.8.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.7.2...v0.8.0
183
221
  [0.7.2]: https://github.com/danielefrisanco/data_redactor/compare/v0.7.1...v0.7.2
@@ -10,9 +10,10 @@ A Ruby gem with a C extension for high-performance regex-based redaction of sens
10
10
 
11
11
  DataRedactor scans text for sensitive data — API keys and cloud secrets, IBANs,
12
12
  credit cards, national IDs, emails, phone numbers, IPs, and more — and replaces
13
- each match with a placeholder. The scanning runs in a C extension backed by POSIX
14
- `regex.h`, so the heavy lifting happens outside the Ruby VM and stays fast enough
15
- to run inline on large payloads.
13
+ each match with a placeholder. The scanning runs in a C extension backed by a
14
+ zero-dependency Thompson NFA lazy-DFA multi-pattern engine (v19) that scans
15
+ all 88 built-in patterns in a single pass — 2–2.5× faster than pure-Ruby `gsub`
16
+ on large payloads, with no external library dependencies.
16
17
 
17
18
  It ships **88 built-in patterns** across 15+ countries, grouped into tags
18
19
  (`:credentials`, `:financial`, `:contact`, ...) so you can redact only what you
@@ -384,8 +385,18 @@ redactor/
384
385
  │ ├── scan.{c,h} # _scan + byte-offset replacement-log macros
385
386
  │ ├── custom_patterns.{c,h} # Dynamic registry: add/remove/clear/list
386
387
  │ └── tags.h # TAG_* bit constants
387
- └── spec/
388
- └── data_redactor_spec.rb # RSpec tests — at least one example per pattern, plus filter / placeholder / custom-pattern coverage
388
+ ├── spec/
389
+ └── data_redactor_spec.rb # RSpec tests — at least one example per pattern, plus filter / placeholder / custom-pattern coverage
390
+ ├── benchmark/ # Repo-only perf scripts (not packaged in the gem)
391
+ │ ├── README.md # How to run, what each script measures
392
+ │ ├── support/corpus.rb # Shared payload builders + pure-Ruby baseline redactor
393
+ │ ├── throughput.rb # MB/s on representative payloads
394
+ │ ├── vs_pure_ruby.rb # C extension vs pure-Ruby gsub (same 88 patterns)
395
+ │ ├── scaling.rb # Runtime vs input size 1KB → 50MB
396
+ │ └── per_pattern.rb # Per-pattern scan cost
397
+ └── docs/ # Design and execution docs for future work
398
+ ├── standalone_matcher_design.md
399
+ └── combined_matcher_plan.md
389
400
  ```
390
401
 
391
402
  ## Requirements
@@ -460,6 +471,45 @@ Or compile and test in one step:
460
471
  bundle exec rake
461
472
  ```
462
473
 
474
+ ## Benchmarks
475
+
476
+ The `benchmark/` directory holds four scripts that measure the C engine under
477
+ different angles. They are **not** packaged with the gem.
478
+
479
+ ```bash
480
+ bundle install # pulls benchmark-ips, benchmark-memory (dev deps)
481
+ bundle exec rake compile
482
+ bundle exec ruby benchmark/vs_pure_ruby.rb # head-to-head vs pure-Ruby gsub, same 88 patterns
483
+ bundle exec ruby benchmark/throughput.rb # MB/s on a log line, JSON, 1MB and 10MB log files
484
+ bundle exec ruby benchmark/scaling.rb # runtime vs input size (1KB → 50MB), confirms linear scaling
485
+ bundle exec ruby benchmark/per_pattern.rb # per-pattern scan cost over a 1MB payload
486
+ ```
487
+
488
+ See [`benchmark/README.md`](benchmark/README.md) for what each script measures
489
+ and how the pure-Ruby baseline is kept honest (it reads the same patterns the
490
+ C engine uses, via `DataRedactor::BUILTIN_PATTERN_SOURCES`).
491
+
492
+ ### Performance (0.10.0 — v19 multi-pattern engine)
493
+
494
+ As of 0.10.0 the C extension runs a **Thompson NFA → lazy-DFA multi-pattern
495
+ engine** (v19) that scans the input once across all 88 built-in patterns,
496
+ with two selective-merge passes (pure-digit group + IBAN union) that further
497
+ reduce work for the most common pattern classes. Custom patterns (`add_pattern`)
498
+ still use the glibc path (required for correct UTF-8 diacritic matching).
499
+
500
+ | Payload | v19 engine (0.10.0) | Pure-Ruby `gsub` | Ratio |
501
+ |-----------------------|---------------------|------------------|-----------------|
502
+ | log line (168 B) | 41 µs / call | 71 µs / call | **1.7× faster** |
503
+ | JSON blob (~580 B) | 81 µs / call | 132 µs / call | **1.6× faster** |
504
+ | 8 log lines (1.3 KB) | 175 µs / call | 399 µs / call | **2.3× faster** |
505
+ | 100 log lines (17 KB) | 2.0 ms / call | 4.6 ms / call | **2.3× faster** |
506
+ | 1 MB log | 138 ms / call | 294 ms / call | **2.1× faster** |
507
+ | 10 MB log | 1.44 s / call | — | 6.9 MB/s |
508
+
509
+ All payload sizes pass a correctness check (redaction count matches pure-Ruby `gsub`).
510
+ The previous engine (per-pattern `regexec`) was **4.25× slower** than pure Ruby on the
511
+ 1 MB payload — a ~9× swing. Old numbers are in git history (`CHANGELOG.md` [0.9.0]).
512
+
463
513
  ## How it works
464
514
 
465
515
  1. At load time, `Init_data_redactor` compiles all 85 regex patterns once using `regcomp` (POSIX ERE) and stores them as static `regex_t` structs. Patterns marked as boundary-wrapped are expanded with `wrap_boundary()` before compilation.
@@ -473,7 +523,7 @@ All C-side buffers are heap-allocated with `malloc`/`strdup` and freed before th
473
523
 
474
524
  ## Thread safety
475
525
 
476
- `DataRedactor.redact` and `DataRedactor.scan` are safe to call concurrently from multiple threads. Built-in patterns are compiled into a static `regex_t` array at load time and never mutated afterward, and each call allocates its own working buffers. POSIX `regexec` is documented as thread-safe.
526
+ `DataRedactor.redact` and `DataRedactor.scan` are safe to call concurrently from multiple threads. The v19 engine holds MRI's GVL for the duration of each call (no `rb_thread_call_without_gvl`), so concurrent calls are serialised by the GVL. Each call allocates its own working buffers; built-in engine state is read-only after `mm_init()` at load time.
477
527
 
478
528
  `DataRedactor.add_pattern`, `remove_pattern`, and `clear_custom_patterns!` mutate a shared dynamic array and are **not** thread-safe. Register custom patterns once at boot — before spawning worker threads or forking — and they will be visible (read-only) to every subsequent `redact`/`scan` call.
479
529
 
@@ -490,3 +540,4 @@ Released under the [MIT License](LICENSE).
490
540
  - **Pattern ordering matters** — patterns run sequentially. An early broad pattern (e.g. the 9-digit passport) may consume digits that a later pattern (e.g. credit card) depends on. Boundary wrapping mitigates this for pure-digit patterns.
491
541
  - **AWS Secret Key (pattern 1)** — 40 consecutive base64 characters is a broad match. It can produce false positives in base64-encoded content such as embedded images or binary blobs.
492
542
  - **Duplicate digit patterns** — several national ID formats share the same digit-length (11 digits: PESEL, Norwegian Fødselsnummer, Belgian National Number). They are kept as separate slots for clarity but the practical effect is that any 11-digit boundary-delimited number will be redacted.
543
+ - **Single-pass overlap semantics** — built-in patterns are resolved by an index-order greedy claim: the lower-index pattern wins any region it matches. When two secrets abut with no separator, a rewrite-created word boundary can cause the second to be missed. This is rare in real text (secrets are almost always separator-delimited) and will be fixed by the upcoming longest-match-wins resolver in 1.0.
@@ -3,6 +3,11 @@
3
3
  #include <string.h>
4
4
  #include <stdlib.h>
5
5
 
6
+ /* Custom patterns deliberately do NOT use the v19 engine: they keep the glibc
7
+ * regexec path (replace_all_matches), because user regex can contain multibyte
8
+ * UTF-8 inside character classes (e.g. name_pattern's [oOòóô…]) that the
9
+ * byte-level v19 engine cannot match the way glibc's locale-aware matcher does.
10
+ * See TODO.md §1d Gap 2 for the hybrid-split rationale. */
6
11
  custom_pattern_t *custom_patterns = NULL;
7
12
  int custom_count = 0;
8
13
  int custom_cap = 0;
@@ -7,8 +7,12 @@
7
7
  #include "redact.h"
8
8
  #include "scan.h"
9
9
  #include "custom_patterns.h"
10
+ #include "matcher.h"
10
11
 
11
12
  void Init_data_redactor(void) {
13
+ /* Build the v19 multi-pattern engines from the gem's pattern arrays. */
14
+ mm_init();
15
+
12
16
  /* Compile all built-in regex patterns at load time. */
13
17
  for (int i = 0; i < NUM_PATTERNS; i++) {
14
18
  const char *pat;
@@ -43,15 +47,25 @@ void Init_data_redactor(void) {
43
47
  rb_define_module_function(mDataRedactor, "_clear_custom_patterns", rb_clear_custom_patterns, 0);
44
48
  rb_define_module_function(mDataRedactor, "_custom_patterns", rb_custom_patterns, 0);
45
49
 
46
- /* Frozen array of built-in pattern names, for introspection and only:/except: validation. */
50
+ /* Frozen introspection arrays, parallel to the pattern table (same index = same
51
+ * pattern). NAMES/TAG_BITS back only:/except: validation; SOURCES/BOUNDARY are
52
+ * internal aids (the benchmark suite rebuilds the patterns in pure Ruby from
53
+ * them). SOURCES holds the unwrapped POSIX ERE — boundary wrapping is applied
54
+ * above at compile time for patterns with BOUNDARY[i] == true. */
47
55
  VALUE builtin_names = rb_ary_new_capa(NUM_PATTERNS);
48
56
  VALUE builtin_tag_bits = rb_ary_new_capa(NUM_PATTERNS);
57
+ VALUE builtin_sources = rb_ary_new_capa(NUM_PATTERNS);
58
+ VALUE builtin_boundary = rb_ary_new_capa(NUM_PATTERNS);
49
59
  for (int i = 0; i < NUM_PATTERNS; i++) {
50
60
  rb_ary_push(builtin_names, rb_str_new_frozen(rb_str_new_cstr(pattern_names[i])));
51
61
  rb_ary_push(builtin_tag_bits, INT2NUM(pattern_tags[i]));
62
+ rb_ary_push(builtin_sources, rb_str_new_frozen(rb_str_new_cstr(pattern_strings[i])));
63
+ rb_ary_push(builtin_boundary, boundary_wrapped[i] ? Qtrue : Qfalse);
52
64
  }
53
65
  rb_define_const(mDataRedactor, "BUILTIN_PATTERN_NAMES", rb_ary_freeze(builtin_names));
54
66
  rb_define_const(mDataRedactor, "BUILTIN_PATTERN_TAG_BITS", rb_ary_freeze(builtin_tag_bits));
67
+ rb_define_const(mDataRedactor, "BUILTIN_PATTERN_SOURCES", rb_ary_freeze(builtin_sources));
68
+ rb_define_const(mDataRedactor, "BUILTIN_PATTERN_BOUNDARY", rb_ary_freeze(builtin_boundary));
55
69
 
56
70
  /* Placeholder mode constants. */
57
71
  rb_define_const(mDataRedactor, "PH_MODE_PLAIN", INT2NUM(PLACEHOLDER_MODE_PLAIN));