zxcvbn-ruby 2.0.0 → 2.0.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: da4a05d3abd0061434bb0bcedfa1992a9e975c479a0fa9679f9b7c139ea1fa1a
4
- data.tar.gz: c7a0a8aa222f29b4550e636701e23069599497290c0fb23edddfc7dbfc37fec5
3
+ metadata.gz: ba194667b2557bfc70fe0bae1294067b9d0ce0806bd02d9fdca7d7a4e5c13d0f
4
+ data.tar.gz: 2cf420153522153586d434a30d216ae7b115971e97ff76ec33e10870eaef1fcd
5
5
  SHA512:
6
- metadata.gz: 5fcabd8ce5ebe85f5127225c27f1fea83bf26c80885e7f4470d037f8d2c35bda07c8886f185627ee1fdf99feabb7d29ccafba80322257b1724b2c6fc7fc1352a
7
- data.tar.gz: 559d0b16b68a68890e898992ef11c2e45c499ab0772d928313a3ab01767c8c251002e136b80eb7ac90d8155ea7e2432d9fb6bae1c47f0d3876b8f623a1bde9bc
6
+ metadata.gz: 85b798676bab221fd1c1bcd3d822305a1f0772ff13291e2b8a475d2dba5f6b0c3bd1ff0cf724913cf63b7ce7f7db14a95b563e06079c1e34d06497779a7c4368
7
+ data.tar.gz: 5d97602b0d05a1b4bd5afe263cc5ecc1f384df5bc4470d9321fb6029981cc5a4c5d35c4c87474154eba6545d63a66a1d0e5ff9e30224f45343b104ad9a72a221
data/CHANGELOG.md CHANGED
@@ -6,7 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- [Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v2.0.0...HEAD
9
+ [Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v2.0.1...HEAD
10
+
11
+ ## [2.0.1] - 2026-07-11
12
+
13
+ ### Fixed
14
+ - `NoMethodError` in `Scorer` for passwords containing characters whose lowercased form is longer than the original (e.g. "İ" U+0130 downcases to "i" plus a combining dot). Dictionary and l33t matchers now lowercase per character, keeping match indices aligned with the original password. Dictionary word ranking applies the same normalisation, so user-input words containing such characters now match ([#124])
15
+
16
+ [2.0.1]: https://github.com/envato/zxcvbn-ruby/compare/v2.0.0...v2.0.1
17
+ [#124]: https://github.com/envato/zxcvbn-ruby/pull/124
10
18
 
11
19
  ## [2.0.0] - 2026-05-28
12
20
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zxcvbn
4
+ # Shared helper for lowercasing strings without changing their length.
5
+ # @api private
6
+ module CaseHelpers
7
+ module_function
8
+
9
+ # String#downcase can grow a string — "İ" (U+0130) downcases to "i" plus
10
+ # combining dot above (U+0069 U+0307). Matchers rely on the lowercased
11
+ # password sharing the original's character indices, so a grown string
12
+ # sends out-of-range match positions to the scorer. When lengths diverge,
13
+ # fall back to per-character downcasing, truncating any multi-character
14
+ # mapping to its first character to keep positions aligned 1:1.
15
+ #
16
+ # @param string [String]
17
+ # @return [String] lowercased string with the same character length
18
+ def downcase_preserving_length(string)
19
+ lowercased = string.downcase
20
+ return lowercased if lowercased.length == string.length
21
+
22
+ string.each_char.map { |char| char.downcase[0] }.join
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'zxcvbn/case_helpers'
4
+
3
5
  module Zxcvbn
4
6
  # Converts raw word lists into frequency-ranked dictionaries for matcher use.
5
7
  # @api private
@@ -21,7 +23,9 @@ module Zxcvbn
21
23
  def self.rank_dictionary(words)
22
24
  words
23
25
  .each_with_index
24
- .with_object({}) { |(word, i), dictionary| dictionary[word.downcase] = i + 1 }
26
+ .with_object({}) do |(word, i), dictionary|
27
+ dictionary[CaseHelpers.downcase_preserving_length(word)] = i + 1
28
+ end
25
29
  end
26
30
  end
27
31
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'zxcvbn/case_helpers'
3
4
  require 'zxcvbn/match_builder'
4
5
 
5
6
  module Zxcvbn
@@ -8,6 +9,7 @@ module Zxcvbn
8
9
  # a ranked dictionary.
9
10
  # @api private
10
11
  class Dictionary
12
+ include CaseHelpers
11
13
  # @param name [String] dictionary identifier used in match results
12
14
  # @param ranked_dictionary [Hash{String => Integer}] lowercased word → rank
13
15
  # @param trie [Trie, nil] optional prefix trie for faster lookups
@@ -22,7 +24,7 @@ module Zxcvbn
22
24
  # @param password [String]
23
25
  # @return [Array<MatchBuilder>] matches with pattern "dictionary"
24
26
  def matches(password)
25
- lowercased_password = password.downcase
27
+ lowercased_password = downcase_preserving_length(password)
26
28
 
27
29
  if @trie
28
30
  trie_matches(password, lowercased_password)
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'zxcvbn/case_helpers'
4
+
3
5
  module Zxcvbn
4
6
  module Matchers
5
7
  # Matches dictionary words after substituting common l33t-speak character
6
8
  # replacements (e.g. "@" for "a", "3" for "e").
7
9
  # @api private
8
10
  class L33t
11
+ include CaseHelpers
9
12
  # Mapping from plain letter to the l33t characters that can represent it.
10
13
  L33T_TABLE = {
11
14
  'a' => ['4', '@'].freeze,
@@ -33,7 +36,7 @@ module Zxcvbn
33
36
  # @return [Array<MatchBuilder>] matches with pattern "dictionary" and l33t: true
34
37
  def matches(password)
35
38
  matches = []
36
- lowercased_password = password.downcase
39
+ lowercased_password = downcase_preserving_length(password)
37
40
  relevent_subtable = relevent_l33t_subtable(lowercased_password)
38
41
 
39
42
  # Early bailout: if no l33t characters present, return empty matches
data/lib/zxcvbn/scorer.rb CHANGED
@@ -61,7 +61,13 @@ module Zxcvbn
61
61
 
62
62
  return build_score(password, [], 1.0) if n.zero?
63
63
 
64
- # index matches by their last character
64
+ # Index matches by their last character (j). matches_by_j is sized to the
65
+ # password, so this relies on every match satisfying 0 <= m.j < n (and
66
+ # 0 <= m.i <= m.j) — match indices must be positions in `password`.
67
+ # Matchers uphold this by matching against a lowercased view that keeps the
68
+ # original character length; see CaseHelpers.downcase_preserving_length. A
69
+ # match with j >= n reads a nil slot below and raises NoMethodError, so any
70
+ # new matcher must preserve this invariant.
65
71
  matches_by_j = Array.new(n) { [] }
66
72
  matches.each { |m| matches_by_j[m.j] << m }
67
73
  matches_by_j.each { |arr| arr.sort_by!(&:i) }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zxcvbn
4
- VERSION = '2.0.0'
4
+ VERSION = '2.0.1'
5
5
  end
@@ -0,0 +1,5 @@
1
+ module Zxcvbn
2
+ module CaseHelpers
3
+ def self?.downcase_preserving_length: (String string) -> String
4
+ end
5
+ end
@@ -1,6 +1,8 @@
1
1
  module Zxcvbn
2
2
  module Matchers
3
3
  class Dictionary
4
+ include CaseHelpers
5
+
4
6
  @name: String
5
7
  @ranked_dictionary: Data::ranked_dictionary
6
8
  @trie: Trie?
@@ -1,6 +1,8 @@
1
1
  module Zxcvbn
2
2
  module Matchers
3
3
  class L33t
4
+ include CaseHelpers
5
+
4
6
  L33T_TABLE: Hash[String, Array[String]]
5
7
 
6
8
  @dictionary_matchers: Array[Dictionary]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zxcvbn-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Hodgkiss
@@ -32,6 +32,7 @@ files:
32
32
  - data/frequency_lists/surnames.txt
33
33
  - data/frequency_lists/us_tv_and_film.txt
34
34
  - lib/zxcvbn.rb
35
+ - lib/zxcvbn/case_helpers.rb
35
36
  - lib/zxcvbn/clock.rb
36
37
  - lib/zxcvbn/crack_time.rb
37
38
  - lib/zxcvbn/data.rb
@@ -61,6 +62,7 @@ files:
61
62
  - lib/zxcvbn/version.rb
62
63
  - sig/README.md
63
64
  - sig/zxcvbn.rbs
65
+ - sig/zxcvbn/case_helpers.rbs
64
66
  - sig/zxcvbn/clock.rbs
65
67
  - sig/zxcvbn/crack_time.rbs
66
68
  - sig/zxcvbn/data.rbs
@@ -93,9 +95,9 @@ metadata:
93
95
  allowed_push_host: https://rubygems.org
94
96
  bug_tracker_uri: https://github.com/envato/zxcvbn-ruby/issues
95
97
  changelog_uri: https://github.com/envato/zxcvbn-ruby/blob/HEAD/CHANGELOG.md
96
- documentation_uri: https://www.rubydoc.info/gems/zxcvbn-ruby/2.0.0
98
+ documentation_uri: https://www.rubydoc.info/gems/zxcvbn-ruby/2.0.1
97
99
  homepage_uri: https://github.com/envato/zxcvbn-ruby
98
- source_code_uri: https://github.com/envato/zxcvbn-ruby/tree/v2.0.0
100
+ source_code_uri: https://github.com/envato/zxcvbn-ruby/tree/v2.0.1
99
101
  rubygems_mfa_required: 'true'
100
102
  rdoc_options: []
101
103
  require_paths:
@@ -111,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
113
  - !ruby/object:Gem::Version
112
114
  version: '0'
113
115
  requirements: []
114
- rubygems_version: 4.0.12
116
+ rubygems_version: 4.0.15
115
117
  specification_version: 4
116
118
  summary: Ruby port of Dropbox's zxcvbn.js JavaScript password strength estimator
117
119
  test_files: []