disarm 0.14.1-aarch64-linux → 0.16.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: 40d3335e15f74858ee0ae3ea3ce35093febb3cadcac08d62cd7a873cf0d14acd
4
- data.tar.gz: 92a8dee4515698525dcbf550b0a794543f3595b6258966f16f82a3d13407878a
3
+ metadata.gz: 8ecc331ffb977947b213f31133c21afa3aede8d64f44a43a5e624eeb122013df
4
+ data.tar.gz: e09891674097fa1eb7f61318e3df6c7e23740a132e313f309012ff68a2951bfe
5
5
  SHA512:
6
- metadata.gz: 8ddcbc8bd78371985f105e6bd4f42b631e1c8edfcb951447785f804a0cdb1f7cc9587f3a1d62925113d59bed5c9e83751e44e04a852f0f6f5f6cb9f91b654a03
7
- data.tar.gz: 6b55888ddad977c96ac7ddb4ac92941769c10bf298ca640d8706cd0841ac9ecea2625595de757e25609edddd8bd128bb8100ecd54513587c011f064578c38e0f
6
+ metadata.gz: 1784010e8ff92e9493d600eabb0bd856d56405310dd3d12b53fdc1badeddbb7c668efcf608812360f2332772c51735cb7d44dff5bf4d1287dc5ec74497ac1bd3
7
+ data.tar.gz: 0dfc2d60de872b386ac671594a8bea2bc0c4cf87b731c15343dc488d494c4d9e8a7bc4728b7249a44877b2d82ec953ea3c3135299f8ef973f458a1302f40b7ba
data/README.md CHANGED
@@ -19,9 +19,10 @@ gem "disarm"
19
19
  gem install disarm
20
20
  ```
21
21
 
22
- Requires Ruby >= 3.1. `gem install disarm` pulls a precompiled platform gem
23
- (Linux x86_64/aarch64, macOS x86_64/arm64, Windows) when one is available, and
24
- falls back to compiling from source (needs a Rust toolchain) otherwise.
22
+ Requires Ruby >= 3.1. Precompiled platform gems ship for Ruby 3.1 through 4.0
23
+ (Linux x86_64/aarch64, macOS x86_64/arm64, Windows). On a supported Ruby with no
24
+ matching platform gem, the source gem installs and compiles locally, which needs a
25
+ Rust toolchain. Below 3.1 the gem does not install at all.
25
26
 
26
27
  ## Usage
27
28
 
@@ -49,7 +50,7 @@ Disarm.demojize("I ❤️ Ruby") # => "I red heart Ruby
49
50
  Disarm.demojize("👍🏽", strip_modifiers: true)
50
51
 
51
52
  # Security presets
52
- Disarm.strip_obfuscation("Ѕ𝗲𝗰𝗿𝗲𝘁 ​data") # deobfuscated
53
+ Disarm.strip_obfuscation("Ѕ𝗲𝗰𝗿𝗲𝘁 \u200bdata") # deobfuscated
53
54
  Disarm.canonicalize("…") # homoglyph/bidi/zero-width clean
54
55
 
55
56
  # IDN / hostname spoof check (a false result is not a safety guarantee)
Binary file
Binary file
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.14.1"
5
+ VERSION = "0.16.0"
6
6
  end
data/lib/disarm.rb CHANGED
@@ -55,15 +55,22 @@ module Disarm
55
55
 
56
56
  # Fold cross-script confusables toward `target:` (:latin or :cyrillic).
57
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.
58
+ # `digit_policy:` selects how non-Latin DIGITS fold (#561).
59
+ #
60
+ # `:numeric` (default) sends them to the ASCII digit `०` becomes `0` — which is
61
+ # right for prose, where a Devanagari zero really is a zero.
62
+ #
63
+ # `:tr39` uses upstream's targets, which send most of them to a Latin letter
64
+ # (`०` `o`; three of the 45 rows fold to `.` or to the two characters `rn`
65
+ # instead). That is what an identifier *skeleton* wants, since its only job is to
66
+ # make two confusable identifiers collide. The two differ on 45 rows and agree
67
+ # everywhere else. Scoped to `target: :latin` — the override rows are generated from
68
+ # the Latin table and carry TR39's Latin-script targets, so with `target: :cyrillic`
69
+ # it is a no-op.
70
+ #
71
+ # `:preserve` leaves the digit alone (#648). The other two both yield a mixed-script
72
+ # numeral — `२०२४` becomes `२0२४` or `२o२४` — so neither keeps the script. Unlike
73
+ # `:tr39` it applies under every target script.
67
74
  def normalize_confusables(text, target: :latin, digit_policy: :numeric)
68
75
  translate_errors { _normalize_confusables(text, target.to_s, digit_policy.to_s) }
69
76
  end
@@ -109,18 +116,58 @@ module Disarm
109
116
  translate_errors { _demojize(text, strip_modifiers) }
110
117
  end
111
118
 
119
+ # Replace every emoji with +replacement+, verbatim.
120
+ #
121
+ # The counterpart to +demojize+, and a different question of a different table.
122
+ # +demojize+ asks what CLDR calls a character, so its domain is the name table, which
123
+ # is wider than the emoji: <tt>demojize("x\u2122y")</tt> is "x trade mark y". This
124
+ # asks whether the UCD calls it an emoji — Emoji_Presentation=Yes, an Emoji=Yes base
125
+ # carrying U+FE0F, and the ZWJ, modifier, keycap and flag sequences on those. Nothing
126
+ # else moves.
127
+ #
128
+ # +replacement+ is inserted exactly as given: "" closes an intra-word split and " "
129
+ # keeps two words apart, and no rule serves both.
130
+ def replace_emoji(text, replacement = "")
131
+ translate_errors { _replace_emoji(text, replacement) }
132
+ end
133
+
134
+ # Canonicalize, but raise rather than silently normalize a structural difference
135
+ # away — the half of the pair that lets a caller reject input instead of comparing
136
+ # a value the sender never wrote.
137
+ def canonicalize_strict(text, digit_policy: :numeric)
138
+ translate_errors { _canonicalize_strict(text, digit_policy.to_s) }
139
+ end
140
+
141
+ # Strip the non-interchange and invisible classes while KEEPING the script.
142
+ #
143
+ # Unlike `canonicalize` it folds no confusables, so non-Latin text survives as
144
+ # itself. It cannot be rebuilt from the seven universal `strip_*` methods, and the
145
+ # difference runs both ways: this preserves the Private Use Area (icon fonts) and
146
+ # keeps the VS15/VS16 presentation selectors after a base, which the naive chain
147
+ # deletes, and it collapses TAB/LF to a space, which the primitives leave alone.
148
+ def strip_format(text)
149
+ _strip_format(text)
150
+ end
151
+
112
152
  # Remove obfuscation (zero-width, bidi, combining-mark abuse) while keeping
113
153
  # legible content.
114
- def strip_obfuscation(text)
115
- translate_errors { _strip_obfuscation(text) }
154
+ def strip_obfuscation(text, digit_policy: :numeric)
155
+ translate_errors { _strip_obfuscation(text, digit_policy.to_s) }
116
156
  end
117
157
 
118
158
  # Canonicalize text for security-sensitive comparison: strip obfuscation,
119
159
  # control characters, and other spoofing vectors. The name describes the
120
160
  # mechanism (Unicode canonicalization for matching), not a safety guarantee —
121
161
  # this is not an output sanitizer; encode at the sink.
122
- def canonicalize(text)
123
- translate_errors { _canonicalize(text) }
162
+ #
163
+ # Two steps introduce ASCII, not one (#719): the leading NFKC, and the confusable
164
+ # fold, which reaches characters NFKC leaves alone. U+2236 RATIO becomes ":", U+2044
165
+ # FRACTION SLASH becomes "/", U+2216 SET MINUS becomes "\\". A string that carried no
166
+ # delimiter can leave here carrying one. #inspect_anomalies reports it as :confusable
167
+ # WHEN the word also carries an ASCII letter, which is the gate that keeps ordinary
168
+ # non-Latin text from firing; a delimiter-only string is not reported.
169
+ def canonicalize(text, digit_policy: :numeric)
170
+ translate_errors { _canonicalize(text, digit_policy.to_s) }
124
171
  end
125
172
 
126
173
  # @deprecated Renamed to {#canonicalize} in 0.11 (the +_clean+ name
@@ -133,22 +180,60 @@ module Disarm
133
180
  # Case/accent/script-insensitive search lookup key. `lang:` applies a
134
181
  # language profile for transliteration (e.g. "ru", "uk"); nil means none.
135
182
  # Raises Disarm::InvalidArgument on an unknown lang.
136
- def search_key(text, lang: nil)
137
- translate_errors { _search_key(text, lang&.to_s) }
183
+ def search_key(text, lang: nil, digit_policy: :numeric)
184
+ translate_errors { _search_key(text, lang&.to_s, digit_policy.to_s) }
138
185
  end
139
186
 
140
187
  # Collation sort key (like #search_key, but keeps base accented characters
141
188
  # for correct ordering). `lang:` applies a language profile; nil means none.
142
189
  # Raises Disarm::InvalidArgument on an unknown lang.
143
- def sort_key(text, lang: nil)
144
- translate_errors { _sort_key(text, lang&.to_s) }
190
+ def sort_key(text, lang: nil, digit_policy: :numeric)
191
+ translate_errors { _sort_key(text, lang&.to_s, digit_policy.to_s) }
145
192
  end
146
193
 
147
194
  # Library catalog deduplication key (search_key plus confusable folding).
148
195
  # `lang:` applies a language profile; `strict_iso9:` selects the ISO 9:1995
149
196
  # Cyrillic scheme. Raises Disarm::InvalidArgument on an unknown lang.
150
- def catalog_key(text, lang: nil, strict_iso9: false)
151
- translate_errors { _catalog_key(text, lang&.to_s, strict_iso9) }
197
+ def catalog_key(text, lang: nil, strict_iso9: false, digit_policy: :numeric)
198
+ translate_errors { _catalog_key(text, lang&.to_s, strict_iso9, digit_policy.to_s) }
199
+ end
200
+
201
+ # The TR39 identifier skeleton plus the two prototype classes disarm keeps apart
202
+ # (#650). A spoof key: its only job is to make confusable identifiers collide, and its
203
+ # output is never for display. `digit_policy:` is `:numeric` (the letter half only),
204
+ # `:tr39` (adds `1 ≡ l` and `0 ≡ O`) or `:preserve` (a non-Latin numeral keeps its
205
+ # script).
206
+ def skeleton_key(text, digit_policy: :numeric)
207
+ translate_errors { _skeleton_key(text, digit_policy.to_s) }
208
+ end
209
+
210
+ # Levenshtein edit distance between `a` and `b`, in characters (#894). The one class
211
+ # of registry spoofing the confusable tables deliberately do not model — `paypa1`,
212
+ # `adm1n`. Canonicalize both sides first when composed and decomposed spellings
213
+ # should compare equal.
214
+ def edit_distance(left, right)
215
+ translate_errors { _edit_distance(left, right) }
216
+ end
217
+
218
+ # The candidate closest to `value`, as `{ value:, distance: }`, or nil beyond
219
+ # `max_distance:` (#894). Reports; it does not decide. An exact match is reported with
220
+ # distance 0, and ties go to the first candidate at the lowest distance.
221
+ def nearest_match(value, candidates, max_distance: 1)
222
+ hit = translate_errors { _nearest_match(value, candidates.map(&:to_s), max_distance) }
223
+ hit && { value: hit[0], distance: hit[1] }
224
+ end
225
+
226
+ # Whether `text` is already its own canonical form under `preset:` (#730).
227
+ #
228
+ # The verification-path counterpart to the presets: text in, normalized text out is
229
+ # the generation path, and this is the question a caller asks about bytes that arrive
230
+ # already bound. `has_anomalies?` is not this predicate — 142,760 assigned code points are
231
+ # reported clean by the detector and are not their own canonical form.
232
+ #
233
+ # `preset:` is any name in the preset registry or any profile. Raises
234
+ # Disarm::InvalidArgument on an unknown one.
235
+ def canonical?(text, preset: "canonicalize")
236
+ translate_errors { _is_canonical?(text, preset.to_s) }
152
237
  end
153
238
 
154
239
  # Strip diacritics ("café" → "cafe").
@@ -208,14 +293,14 @@ module Disarm
208
293
  # confusable test flags essentially every non-Latin host), not a precise verdict;
209
294
  # branch on the granular signals plus your own policy. Keys: :suspicious,
210
295
  # :scripts, :mixed_script, :has_confusables, :bidi_conflict, :bidi_control,
211
- # :has_invisible, :cross_label_script,
296
+ # :has_invisible, :compat_fold, :cross_label_script,
212
297
  # :label_scripts, :whole_script_confusable, :label_whole_script_confusable,
213
298
  # :canonical. `:whole_script_confusable` is a graded signal, NOT folded into
214
299
  # `:suspicious` (see #545).
215
300
  def analyze_hostname(host, contractions: false)
216
301
  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 =
302
+ bidi_control, has_invisible, compat_fold, cross_label_script, label_scripts,
303
+ whole_script_confusable, (label_whole_script_confusable, canonical) =
219
304
  translate_errors { _analyze_hostname(host, contractions) }
220
305
  {
221
306
  suspicious:,
@@ -225,6 +310,7 @@ module Disarm
225
310
  bidi_conflict:,
226
311
  bidi_control:,
227
312
  has_invisible:,
313
+ compat_fold:,
228
314
  cross_label_script:,
229
315
  label_scripts:,
230
316
  whole_script_confusable:,
@@ -337,6 +423,12 @@ module Disarm
337
423
  # (default), :windows, or :posix; `preserve_extension:` keeps the final
338
424
  # extension when truncating to `max_length:`. Raises Disarm::InvalidArgument
339
425
  # on an unknown platform.
426
+ #
427
+ # A safe *filename*, not a safe URL path segment. "%" is legal in a filename, so one
428
+ # the caller typed is kept — sanitize_filename("..%2Fetc") returns "%2Fetc" — and a
429
+ # consumer that percent-decodes the result must validate AFTER decoding. What this
430
+ # will not do is manufacture one: "%" never appears in the output unless it appeared
431
+ # in the input (#721).
340
432
  def sanitize_filename(text, separator: "_", max_length: 255, platform: :universal,
341
433
  lang: nil, preserve_extension: true)
342
434
  translate_errors do
@@ -415,6 +507,15 @@ module Disarm
415
507
  translate_errors { _has_bidi_conflict?(text) }
416
508
  end
417
509
 
510
+ # Whether `text` carries any of the twelve UAX #9 explicit formatting characters,
511
+ # with no context taken into account. The counterpart to `bidi_conflict?`, which
512
+ # reads strong-direction letters and is blind to these; the two are disjoint. The
513
+ # anomaly detector's `bidi` kind reports nine of the twelve, holding back LRM, RLM
514
+ # and ALM because a lone directional mark is ordinary in right-to-left text.
515
+ def bidi_control?(text)
516
+ _has_bidi_control?(text)
517
+ end
518
+
418
519
  # Explain how `lang: "auto"` detection resolves `text`: a hash with
419
520
  # `:script`, `:chosen_lang` (both nil if undetected), `:reason`, and
420
521
  # `:discriminators_hit`.
@@ -438,9 +539,39 @@ module Disarm
438
539
  translate_errors { _script_info(name.to_s) }
439
540
  end
440
541
 
542
+ # TR39 confusable sources whose prototype is in +script+, and how many of those
543
+ # disarm's bundled tables fold. Returns a Hash with +:script+, +:sources+ and
544
+ # +:folded+ keys.
545
+ #
546
+ # The denominator +unmapped_confusables+ does not have: that measures one bundled
547
+ # table against the whole 6,565-source population, so a script disarm ships no table
548
+ # for reports a number determined by that absence rather than by its coverage.
549
+ #
550
+ # +:folded+ counts sources any bundled table reaches, not sources folded *toward*
551
+ # this script — Greek is 71 of 159, because the Latin table folds Greek letters that
552
+ # look Latin. A script disarm knows that TR39 never uses as a prototype returns 0 of
553
+ # 0. Raises Disarm::InvalidArgument on an unknown script.
554
+ def confusable_coverage(script)
555
+ translate_errors { _confusable_coverage(script.to_s) }
556
+ end
557
+
441
558
  # The Unicode `confusables.txt` release the bundled confusable tables were folded
442
559
  # from, e.g. "17.0.0". Not a Unicode version for the library as a whole — the
443
560
  # case-folding and width tables track different releases (see docs/provenance.md).
561
+ # The UCD release disarm's normalizer implements. Not a library-wide Unicode
562
+ # version — the bundled tables track different releases. This is the one integrators
563
+ # ask about: it decides whether disarm's normalization agrees with Ruby's.
564
+ def unicode_version
565
+ _unicode_version
566
+ end
567
+
568
+ # Whether a key stored under an earlier release still compares equal. A monotonic
569
+ # counter, not a version: two artifacts reporting the same value produce the same key
570
+ # for the same input. Meaningless in isolation, by design.
571
+ def key_schema_version
572
+ _key_schema_version
573
+ end
574
+
444
575
  def confusables_version
445
576
  translate_errors { _confusables_version }
446
577
  end
@@ -512,7 +643,9 @@ module Disarm
512
643
  # pipe.process("Café") # => "cafe"
513
644
  # pipe.process("Köln") # reuse the same handle
514
645
  #
515
- # Disarm::Pipeline#process is the Rust-defined instance method on the handle.
646
+ # Disarm::Pipeline#process is the Rust-defined instance method on the handle, and
647
+ # Disarm::Pipeline#with_digit_policy(policy) returns a copy whose confusable passes
648
+ # fold under `policy` (#646); a profile with no confusables step refuses one.
516
649
  def get_pipeline(profile)
517
650
  translate_errors { _get_pipeline(profile.to_s) }
518
651
  end
@@ -553,4 +686,17 @@ module Disarm
553
686
  raise Error, e.message, e.backtrace
554
687
  end
555
688
  end
689
+
690
+ # The reusable handle `Disarm.get_pipeline` returns. `#process` is Rust-defined; this
691
+ # reopens the class for the one method that can fail, so its error arrives as
692
+ # `Disarm::InvalidArgument` the way every module-level call's does.
693
+ class Pipeline
694
+ # A copy of this pipeline whose confusable passes fold under `policy` (#646):
695
+ # `:numeric`, `:tr39` or `:preserve`. Raises Disarm::InvalidArgument when the profile
696
+ # has no confusables step and the policy is not the default — a setting that would
697
+ # never run is refused rather than kept.
698
+ def with_digit_policy(policy)
699
+ Disarm.send(:translate_errors) { _with_digit_policy(policy.to_s) }
700
+ end
701
+ end
556
702
  end
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.14.1
4
+ version: 0.16.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-28 00:00:00.000000000 Z
11
+ date: 2026-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -98,6 +98,8 @@ files:
98
98
  - lib/disarm/3.1/disarm.so
99
99
  - lib/disarm/3.2/disarm.so
100
100
  - lib/disarm/3.3/disarm.so
101
+ - lib/disarm/3.4/disarm.so
102
+ - lib/disarm/4.0/disarm.so
101
103
  - lib/disarm/version.rb
102
104
  homepage: https://github.com/raeq/disarm
103
105
  licenses:
@@ -118,7 +120,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
118
120
  version: '3.1'
119
121
  - - "<"
120
122
  - !ruby/object:Gem::Version
121
- version: 3.4.dev
123
+ version: 4.1.dev
122
124
  required_rubygems_version: !ruby/object:Gem::Requirement
123
125
  requirements:
124
126
  - - ">="