disarm 0.13.0-x86_64-linux → 0.14.0-x86_64-linux

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: d81c4bbe604fd099bba7266d2921248be9972a688da6a5b15b355f582a69f058
4
- data.tar.gz: 0ec9829b26b563078dfac28f4b13989055d85ba80fcf985b917627c983ed1ed5
3
+ metadata.gz: 4e0669625aaa90d822968d52986b7fe06971564991ab212439d18c23e8f6a71b
4
+ data.tar.gz: 27e026ce2b0985c5c42a837ec80078fba14ab8c41dd9e853212c2999cf335f23
5
5
  SHA512:
6
- metadata.gz: 466a9a07bf889ecd2d17acdb319bd306f52bbad97bc1ba09dd99243c6c9450bac2a54c651022a2c0c0ca0da388933d5442cf8fbd25328f0e9a457642d80e5f39
7
- data.tar.gz: 87ec554bed96f2dcc23d8642a20e41a7e88d8f772a3410a3751c1056ed3978bfd25c07025070ca3e49803ca4cc55a567195dde8a0f7efb176b172e5cf3ee646a
6
+ metadata.gz: 41f8fa679ebcb84c16b801f33c7d5a6ef943d4bd6b87a80ab7f40d152a02c62c6ac394fe0376224e4a22746ee26432dc807e223e7991d1737ce4d78f42a826a5
7
+ data.tar.gz: d96b321c9a911bd50d3d02deb89e9cee5dde089f2c56edb418999a093e6a772f5f9e6cd10bb9276724d91e60d5979e3e1147e2e3bb8ebfe0ad9815c2f8422dba
Binary file
Binary file
Binary file
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Disarm
4
4
  # Kept in lockstep with the Rust crate / Python package version.
5
- VERSION = "0.13.0"
5
+ VERSION = "0.14.0"
6
6
  end
data/lib/disarm.rb CHANGED
@@ -54,8 +54,18 @@ module Disarm
54
54
  end
55
55
 
56
56
  # Fold cross-script confusables toward `target:` (:latin or :cyrillic).
57
- def normalize_confusables(text, target: :latin)
58
- translate_errors { _normalize_confusables(text, target.to_s) }
57
+ #
58
+ # `digit_policy:` selects how non-Latin DIGITS fold (#561). `:numeric` (default)
59
+ # sends them to the ASCII digit — `०` becomes `0` — which is right for prose, where
60
+ # a Devanagari zero really is a zero. `:tr39` uses upstream's targets, which send
61
+ # most of them to a Latin letter (`०` → `o`; three of the 45 rows fold to `.` or
62
+ # to the two characters `rn` instead); that is what an identifier *skeleton*
63
+ # wants, since its only job is to make two confusable identifiers collide. The two
64
+ # differ on 45 rows and agree everywhere else. Scoped to `target: :latin` — the
65
+ # override rows are generated from the Latin table and carry TR39's Latin-script
66
+ # targets, so with `target: :cyrillic` it is a no-op.
67
+ def normalize_confusables(text, target: :latin, digit_policy: :numeric)
68
+ translate_errors { _normalize_confusables(text, target.to_s, digit_policy.to_s) }
59
69
  end
60
70
 
61
71
  # Whether `text` contains a character confusable with `target:` (:latin or
@@ -151,9 +161,43 @@ module Disarm
151
161
  translate_errors { _fold_case(text) }
152
162
  end
153
163
 
164
+ # Whether `text` is a stable identity key under case folding — whether
165
+ # `fold_case` and `String#downcase` agree on it (#619). A false result means
166
+ # some other string folds to the same value, so a table keyed on this one can
167
+ # collide: "groß.txt" and "gross.txt" are the pair node-tar collided on
168
+ # (CVE-2026-23950). It is a fact about the string, not an accusation.
169
+ def case_fold_stable?(text)
170
+ translate_errors { _is_case_fold_stable?(text) }
171
+ end
172
+
173
+ # Which of `values` are the same name under `key` (#620).
174
+ #
175
+ # Every other disarm detector is a single-string predicate, and a collision is
176
+ # not a property of a single string — "groß.txt" is an ordinary German filename,
177
+ # and "аdmin" is only a problem next to "admin". This is the set-shaped question
178
+ # node-tar's PathReservations guard failed to ask (CVE-2026-23950).
179
+ #
180
+ # `key:` is one of "fold_case", "search_key", "catalog_key", "canonicalize",
181
+ # "canonicalize_strict", "normalize_confusables". There is no default: a stronger
182
+ # key finds more collisions, including ones nobody attacked ("search_key" collides
183
+ # "Muller" with "Müller"), so the choice is the policy.
184
+ #
185
+ # A group is reported only when two or more DISTINCT inputs share a key; the same
186
+ # name twice is the same name twice. `lang:` reaches "search_key" and
187
+ # "catalog_key" and is ignored by the rest.
188
+ #
189
+ # Returns an array of hashes with `:key`, `:values` (distinct, first-appearance
190
+ # order) and `:indices` (every position, ascending — not parallel to `:values`).
191
+ def find_key_collisions(values, key:, lang: nil)
192
+ translate_errors { _find_key_collisions(values, key.to_s, lang&.to_s) }
193
+ .map { |k, vals, idx| { key: k, values: vals, indices: idx } }
194
+ end
195
+
154
196
  # Whether the hostname looks like a mixed-script / confusable / bidi-reorder
155
197
  # IDN spoof. Flags a mixed-script label, a Latin confusable, or a
156
- # bidi-direction conflict (see #bidi_conflict?, the "BiDi Swap" precondition).
198
+ # bidi-direction conflict (see #bidi_conflict?, the "BiDi Swap" precondition),
199
+ # or a UAX #9 bidi control character (#603 — the RLO spoof, which the direction
200
+ # conflict is blind to).
157
201
  # A false result asserts nothing was *found*, not that the host is safe.
158
202
  def suspicious_hostname?(host)
159
203
  translate_errors { _suspicious_hostname?(host) }
@@ -163,21 +207,24 @@ module Disarm
163
207
  # as a Hash. `:suspicious` is a maximally conservative screen (an any-character
164
208
  # confusable test flags essentially every non-Latin host), not a precise verdict;
165
209
  # branch on the granular signals plus your own policy. Keys: :suspicious,
166
- # :scripts, :mixed_script, :has_confusables, :bidi_conflict, :cross_label_script,
210
+ # :scripts, :mixed_script, :has_confusables, :bidi_conflict, :bidi_control,
211
+ # :has_invisible, :cross_label_script,
167
212
  # :label_scripts, :whole_script_confusable, :label_whole_script_confusable,
168
213
  # :canonical. `:whole_script_confusable` is a graded signal, NOT folded into
169
214
  # `:suspicious` (see #545).
170
- def analyze_hostname(host)
215
+ def analyze_hostname(host, contractions: false)
171
216
  suspicious, scripts, mixed_script, has_confusables, bidi_conflict,
172
- cross_label_script, label_scripts, whole_script_confusable,
173
- label_whole_script_confusable, canonical =
174
- translate_errors { _analyze_hostname(host) }
217
+ bidi_control, has_invisible, cross_label_script, label_scripts,
218
+ whole_script_confusable, label_whole_script_confusable, canonical =
219
+ translate_errors { _analyze_hostname(host, contractions) }
175
220
  {
176
221
  suspicious:,
177
222
  scripts:,
178
223
  mixed_script:,
179
224
  has_confusables:,
180
225
  bidi_conflict:,
226
+ bidi_control:,
227
+ has_invisible:,
181
228
  cross_label_script:,
182
229
  label_scripts:,
183
230
  whole_script_confusable:,
@@ -314,6 +361,41 @@ module Disarm
314
361
  end
315
362
  end
316
363
 
364
+ # Every upstream confusable source the bundled table does not fold, as a sorted
365
+ # Array of single-character Strings (#563). An Array rather than a Set, matching
366
+ # `list_scripts` and avoiding a `require "set"` in the gem entrypoint.
367
+ #
368
+ # Read as exposure, not as a score — this is where an adaptive attacker goes once
369
+ # the mapped sources stop working. It includes five ASCII characters ("%", "0",
370
+ # "1", "I", "m"): TR39 is a skeleton transform, and disarm deliberately does not
371
+ # apply those rows because folding a legitimate "m" to "rn" corrupts prose.
372
+ def unmapped_confusables(target: :latin)
373
+ translate_errors { _unmapped_confusables(target.to_s) }
374
+ end
375
+
376
+ # Confusable sources in `text` the bundled table does not fold, as an Array of
377
+ # `{ char:, offset: }` hashes in order of appearance — the confusables analogue of
378
+ # `find_untranslatable`, with the same byte-offset convention (#563).
379
+ def find_unmapped_confusables(text, target: :latin)
380
+ translate_errors do
381
+ _find_unmapped_confusables(text, target.to_s)
382
+ .map { |ch, offset| { char: ch, offset: offset } }
383
+ end
384
+ end
385
+
386
+ # ML/NLP normalization: NFKC → emoji→text → transliterate → strip accents →
387
+ # [case fold] → strip control → strip zero-width → collapse whitespace.
388
+ #
389
+ # `fold_case:` defaults to true. Pass false in front of a CASED model — folding is
390
+ # destructive, cannot be undone downstream, and an uncased evaluation harness cannot
391
+ # measure what it cost. It restores case, not diacritics: accents are still stripped.
392
+ #
393
+ # Folds no confusables, so it is not a homoglyph defence at any setting; compose it
394
+ # after `normalize_confusables` when a model needs both.
395
+ def ml_normalize(text, lang: nil, emoji_style: "cldr", fold_case: true)
396
+ translate_errors { _ml_normalize(text, lang&.to_s, emoji_style.to_s, fold_case) }
397
+ end
398
+
317
399
  # The Unicode scripts present in `text`, in first-appearance order
318
400
  # (Common/Inherited excluded), as stable UCD identifiers (e.g. "Latin").
319
401
  def detect_scripts(text)
@@ -356,6 +438,13 @@ module Disarm
356
438
  translate_errors { _script_info(name.to_s) }
357
439
  end
358
440
 
441
+ # The Unicode `confusables.txt` release the bundled confusable tables were folded
442
+ # from, e.g. "17.0.0". Not a Unicode version for the library as a whole — the
443
+ # case-folding and width tables track different releases (see docs/provenance.md).
444
+ def confusables_version
445
+ translate_errors { _confusables_version }
446
+ end
447
+
359
448
  # Every script disarm knows, as stable UCD script identifiers (includes
360
449
  # "Common"/"Inherited"), sorted by name.
361
450
  def list_scripts
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: disarm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Richard Quinn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-17 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake