disarm 0.12.0-aarch64-linux → 0.14.0-aarch64-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: 6327afe7a1127ae46ad3a134207a80b5ca397b6b06ca51ec0caa3dd915f4f40b
4
- data.tar.gz: 80cb1882dd5a8a604d6eb1c3ed8d29612c2bde7832a9edd0349fdac5c3be4bd3
3
+ metadata.gz: 8626868ff11d6125f1768356c699416cbf5013b97bcdb75f02e53cefce7168d7
4
+ data.tar.gz: 236bf3cebcfdb91dd2d55a6fb4f2a510488550b37007dd4668df0b767781c523
5
5
  SHA512:
6
- metadata.gz: 96891160cb88546242ef3cd5c488d4e81f2afd1dfcd05571967aba69dd10dfd1b6fcbc66e8e9deb1f0b9e0777ed703f16d4a5f9e1c3be73cb939af1a3a45aea7
7
- data.tar.gz: 78cf97d16dc84cb43e387221245ed80fe159af2d49fa0a4c8f0bb15aa3b27d7d9f45473ec5d6711bd538e01a48e10a62b54a4f6b914a91b10c95cc19258f3347
6
+ metadata.gz: 3b1068de3adb50799dbb3c9bf408664d69c095c99f558ac6e8153a61d8a36db1088e2cd1613a3123aacef1ee222321f642ba5939991acd6f944dcd6b2979a548
7
+ data.tar.gz: ce875b46faa853663866f8357e8d3ad9ca36bd6608f9e839f3b7667a7ac549165e95d818a2eb99611b8605cdc677755a8901ed2205c762be714cb5a8d379a09d
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.12.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,14 +161,78 @@ 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) }
160
204
  end
161
205
 
206
+ # The full hostname homoglyph analysis (#549) behind #suspicious_hostname?,
207
+ # as a Hash. `:suspicious` is a maximally conservative screen (an any-character
208
+ # confusable test flags essentially every non-Latin host), not a precise verdict;
209
+ # branch on the granular signals plus your own policy. Keys: :suspicious,
210
+ # :scripts, :mixed_script, :has_confusables, :bidi_conflict, :bidi_control,
211
+ # :has_invisible, :cross_label_script,
212
+ # :label_scripts, :whole_script_confusable, :label_whole_script_confusable,
213
+ # :canonical. `:whole_script_confusable` is a graded signal, NOT folded into
214
+ # `:suspicious` (see #545).
215
+ def analyze_hostname(host, contractions: false)
216
+ suspicious, scripts, mixed_script, has_confusables, bidi_conflict,
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) }
220
+ {
221
+ suspicious:,
222
+ scripts:,
223
+ mixed_script:,
224
+ has_confusables:,
225
+ bidi_conflict:,
226
+ bidi_control:,
227
+ has_invisible:,
228
+ cross_label_script:,
229
+ label_scripts:,
230
+ whole_script_confusable:,
231
+ label_whole_script_confusable:,
232
+ canonical:,
233
+ }
234
+ end
235
+
162
236
  # Apply a Unicode normalization form. `form:` is :nfc (default), :nfd,
163
237
  # :nfkc, or :nfkd (a Symbol or String; case-insensitive).
164
238
  def normalize(text, form: :nfc)
@@ -287,6 +361,41 @@ module Disarm
287
361
  end
288
362
  end
289
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
+
290
399
  # The Unicode scripts present in `text`, in first-appearance order
291
400
  # (Common/Inherited excluded), as stable UCD identifiers (e.g. "Latin").
292
401
  def detect_scripts(text)
@@ -329,6 +438,13 @@ module Disarm
329
438
  translate_errors { _script_info(name.to_s) }
330
439
  end
331
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
+
332
448
  # Every script disarm knows, as stable UCD script identifiers (includes
333
449
  # "Common"/"Inherited"), sorted by name.
334
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.12.0
4
+ version: 0.14.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Richard Quinn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-15 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