password_strength 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +5 -0
- data/README.rdoc +97 -0
- data/lib/password_strength.js +194 -0
- data/lib/password_strength.rb +17 -0
- data/lib/password_strength/active_record.rb +8 -0
- data/lib/password_strength/active_record/ar2.rb +34 -0
- data/lib/password_strength/active_record/ar3.rb +40 -0
- data/lib/password_strength/base.rb +153 -0
- data/lib/password_strength/version.rb +8 -0
- data/test/active_record_test.rb +48 -0
- data/test/jsunittest/jsunittest.js +1017 -0
- data/test/jsunittest/unittest.css +54 -0
- data/test/password_strength_test.html +50 -0
- data/test/password_strength_test.js +239 -0
- data/test/password_strength_test.rb +192 -0
- data/test/schema.rb +8 -0
- data/test/test_helper.rb +12 -0
- data/test/user.rb +1 -0
- metadata +86 -0
data/CHANGELOG.rdoc
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
= Introduction
|
2
|
+
|
3
|
+
Validates the strength of a password according to several rules:
|
4
|
+
|
5
|
+
* size
|
6
|
+
* 3+ numbers
|
7
|
+
* 2+ special characters
|
8
|
+
* uppercased and downcased letters
|
9
|
+
* combination of numbers, letters and symbols
|
10
|
+
* password contains username
|
11
|
+
* sequences (123, abc, aaa)
|
12
|
+
|
13
|
+
Some results:
|
14
|
+
|
15
|
+
* <tt>123</tt>: weak
|
16
|
+
* <tt>123abc</tt>: weak
|
17
|
+
* <tt>aaaaaa</tt>: weak
|
18
|
+
* <tt>myPass145</tt>: good
|
19
|
+
* <tt>myPass145$</tt>: strong
|
20
|
+
|
21
|
+
= Install
|
22
|
+
|
23
|
+
sudo gem install password_strength
|
24
|
+
|
25
|
+
If you want the source go to http://github.com/fnando/password_strength
|
26
|
+
|
27
|
+
= Usage
|
28
|
+
|
29
|
+
strength = PasswordStrength.test("johndoe", "mypass")
|
30
|
+
#=> return a object
|
31
|
+
|
32
|
+
strength.good?
|
33
|
+
#=> status == :good
|
34
|
+
|
35
|
+
strength.weak?
|
36
|
+
#=> status == :weak
|
37
|
+
|
38
|
+
strength.strong?
|
39
|
+
#=> status == :strong
|
40
|
+
|
41
|
+
strength.status
|
42
|
+
#=> can be :weak, :good, :strong
|
43
|
+
|
44
|
+
strength.valid?(:strong)
|
45
|
+
#=> strength == :strong
|
46
|
+
|
47
|
+
strength.valid?(:good)
|
48
|
+
#=> strength == :good or strength == :strong
|
49
|
+
|
50
|
+
= ActiveRecord
|
51
|
+
|
52
|
+
The PasswordStrength library comes with ActiveRecord support (tested on AR 2.3.5 and 3.0.0-beta).
|
53
|
+
|
54
|
+
class Person < ActiveRecord::Base
|
55
|
+
validates_strength_of :password
|
56
|
+
end
|
57
|
+
|
58
|
+
The default options are <tt>:level => :good, :with => :username</tt>.
|
59
|
+
|
60
|
+
If you want to compare your password against other field, you have to set the <tt>:with</tt> option.
|
61
|
+
|
62
|
+
validates_strength_of :password, :with => :email
|
63
|
+
|
64
|
+
The available levels are: <tt>:weak</tt>, <tt>:good</tt> and <tt>:strong</tt>
|
65
|
+
|
66
|
+
= JavaScript
|
67
|
+
|
68
|
+
The PasswordStrength also implements the algorithm in the JavaScript.
|
69
|
+
|
70
|
+
var strength = PasswordStrength.test("johndoe", "mypass");
|
71
|
+
strength.isGood();
|
72
|
+
strength.isStrong();
|
73
|
+
strength.isWeak();
|
74
|
+
strength.isValid("good");
|
75
|
+
|
76
|
+
The API is basically the same!
|
77
|
+
|
78
|
+
Get the file: http://github.com/fnando/password_strength/raw/master/lib/password_strength.js
|
79
|
+
|
80
|
+
= TO-DO
|
81
|
+
|
82
|
+
* Detect repetitions
|
83
|
+
* Rake task to get the latest JavaScript file
|
84
|
+
|
85
|
+
= License
|
86
|
+
|
87
|
+
(The MIT License)
|
88
|
+
|
89
|
+
Copyright © 2010:
|
90
|
+
|
91
|
+
* Nando Vieira (http://simplesideias.com.br)
|
92
|
+
|
93
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
94
|
+
|
95
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
96
|
+
|
97
|
+
THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,194 @@
|
|
1
|
+
var PasswordStrength = function() {
|
2
|
+
var MULTIPLE_NUMBERS_RE = /\d.*?\d.*?\d/;
|
3
|
+
var MULTIPLE_SYMBOLS_RE = /[!@#$%^&*?_~].*?[!@#$%^&*?_~]/;
|
4
|
+
var UPPERCASE_LOWERCASE_RE = /([a-z].*[A-Z])|([A-Z].*[a-z])/;
|
5
|
+
var SYMBOL_RE = /[!@#\$%^&*?_~]/;
|
6
|
+
|
7
|
+
this.username = null;
|
8
|
+
this.password = null;
|
9
|
+
this.score = 0;
|
10
|
+
this.status = null;
|
11
|
+
|
12
|
+
this.test = function() {
|
13
|
+
this.score = 0;
|
14
|
+
this.score += this.scoreFor("password_size");
|
15
|
+
this.score += this.scoreFor("numbers");
|
16
|
+
this.score += this.scoreFor("symbols");
|
17
|
+
this.score += this.scoreFor("uppercase_lowercase");
|
18
|
+
this.score += this.scoreFor("numbers_chars");
|
19
|
+
this.score += this.scoreFor("numbers_symbols");
|
20
|
+
this.score += this.scoreFor("symbols_chars");
|
21
|
+
this.score += this.scoreFor("only_chars");
|
22
|
+
this.score += this.scoreFor("only_numbers");
|
23
|
+
this.score += this.scoreFor("username");
|
24
|
+
this.score += this.scoreFor("sequences");
|
25
|
+
|
26
|
+
if (this.score < 0) {
|
27
|
+
this.score = 0;
|
28
|
+
}
|
29
|
+
|
30
|
+
if (this.score > 100) {
|
31
|
+
this.score = 100;
|
32
|
+
}
|
33
|
+
|
34
|
+
if (this.score < 35) {
|
35
|
+
this.status = "weak";
|
36
|
+
}
|
37
|
+
|
38
|
+
if (this.score >= 35 && this.score < 70) {
|
39
|
+
this.status = "good";
|
40
|
+
}
|
41
|
+
|
42
|
+
if (this.score >= 70) {
|
43
|
+
this.status = "strong";
|
44
|
+
}
|
45
|
+
|
46
|
+
return this.score;
|
47
|
+
};
|
48
|
+
|
49
|
+
this.scoreFor = function(name) {
|
50
|
+
score = 0;
|
51
|
+
|
52
|
+
switch (name) {
|
53
|
+
case "password_size":
|
54
|
+
if (this.password.length < 4) {
|
55
|
+
score = -100;
|
56
|
+
} else {
|
57
|
+
score = this.password.length * 4;
|
58
|
+
}
|
59
|
+
break;
|
60
|
+
|
61
|
+
case "numbers":
|
62
|
+
if (this.password.match(MULTIPLE_NUMBERS_RE)) {
|
63
|
+
score = 5;
|
64
|
+
}
|
65
|
+
break;
|
66
|
+
|
67
|
+
case "symbols":
|
68
|
+
if (this.password.match(MULTIPLE_SYMBOLS_RE)) {
|
69
|
+
score = 5;
|
70
|
+
}
|
71
|
+
break;
|
72
|
+
|
73
|
+
case "uppercase_lowercase":
|
74
|
+
if (this.password.match(UPPERCASE_LOWERCASE_RE)) {
|
75
|
+
score = 10;
|
76
|
+
}
|
77
|
+
break;
|
78
|
+
|
79
|
+
case "numbers_chars":
|
80
|
+
if (this.password.match(/[a-z]/i) && this.password.match(/[0-9]/)) {
|
81
|
+
score = 15;
|
82
|
+
}
|
83
|
+
break;
|
84
|
+
|
85
|
+
case "numbers_symbols":
|
86
|
+
if (this.password.match(/[0-9]/) && this.password.match(SYMBOL_RE)) {
|
87
|
+
score = 15;
|
88
|
+
}
|
89
|
+
break;
|
90
|
+
|
91
|
+
case "symbols_chars":
|
92
|
+
if (this.password.match(/[a-z]/i) && this.password.match(SYMBOL_RE)) {
|
93
|
+
score = 15;
|
94
|
+
}
|
95
|
+
break;
|
96
|
+
|
97
|
+
case "only_chars":
|
98
|
+
if (this.password.match(/^[a-z]+$/i)) {
|
99
|
+
score = -15;
|
100
|
+
}
|
101
|
+
break;
|
102
|
+
|
103
|
+
case "only_numbers":
|
104
|
+
if (this.password.match(/^\d+$/i)) {
|
105
|
+
score = -15;
|
106
|
+
}
|
107
|
+
break;
|
108
|
+
|
109
|
+
case "username":
|
110
|
+
if (this.password == this.username) {
|
111
|
+
score = -100;
|
112
|
+
} else if (this.password.indexOf(this.username) != -1) {
|
113
|
+
score = -15;
|
114
|
+
}
|
115
|
+
break;
|
116
|
+
|
117
|
+
case "sequences":
|
118
|
+
score += -15 * this.detectSequences(this.password);
|
119
|
+
score += -15 * this.detectSequences(this.reversed(this.password));
|
120
|
+
break
|
121
|
+
};
|
122
|
+
|
123
|
+
return score;
|
124
|
+
};
|
125
|
+
|
126
|
+
this.isGood = function() {
|
127
|
+
return this.status == "good";
|
128
|
+
};
|
129
|
+
|
130
|
+
this.isWeak = function() {
|
131
|
+
return this.status == "weak";
|
132
|
+
};
|
133
|
+
|
134
|
+
this.isStrong = function() {
|
135
|
+
return this.status == "strong";
|
136
|
+
};
|
137
|
+
|
138
|
+
this.isValid = function(level) {
|
139
|
+
if(level == "strong") {
|
140
|
+
return this.isStrong();
|
141
|
+
} else if (level == "good") {
|
142
|
+
return this.isStrong() || this.isGood();
|
143
|
+
} else {
|
144
|
+
return true;
|
145
|
+
}
|
146
|
+
};
|
147
|
+
|
148
|
+
this.detectSequences = function(text) {
|
149
|
+
var matches = 0;
|
150
|
+
var sequenceSize = 0;
|
151
|
+
var codes = [];
|
152
|
+
var len = text.length;
|
153
|
+
var previousCode, currentCode;
|
154
|
+
|
155
|
+
for (var i = 0; i < len; i++) {
|
156
|
+
currentCode = text.charCodeAt(i);
|
157
|
+
previousCode = codes[codes.length - 1];
|
158
|
+
codes.push(currentCode);
|
159
|
+
|
160
|
+
if (previousCode) {
|
161
|
+
if (currentCode == previousCode + 1 || previousCode == currentCode) {
|
162
|
+
sequenceSize += 1;
|
163
|
+
} else {
|
164
|
+
sequenceSize = 0;
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|
168
|
+
if (sequenceSize == 2) {
|
169
|
+
matches += 1;
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
return matches;
|
174
|
+
};
|
175
|
+
|
176
|
+
this.reversed = function(text) {
|
177
|
+
var newText = "";
|
178
|
+
var len = text.length;
|
179
|
+
|
180
|
+
for (var i = len -1; i >= 0; i--) {
|
181
|
+
newText += text.charAt(i);
|
182
|
+
}
|
183
|
+
|
184
|
+
return newText;
|
185
|
+
};
|
186
|
+
};
|
187
|
+
|
188
|
+
PasswordStrength.test = function(username, password) {
|
189
|
+
strength = new PasswordStrength();
|
190
|
+
strength.username = username;
|
191
|
+
strength.password = password;
|
192
|
+
strength.test();
|
193
|
+
return strength;
|
194
|
+
};
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "password_strength/base"
|
2
|
+
require "password_strength/active_record"
|
3
|
+
|
4
|
+
module PasswordStrength
|
5
|
+
# Test the password strength by applying several rules.
|
6
|
+
# The username is required to match its substring in passwords.
|
7
|
+
#
|
8
|
+
# strength = PasswordStrength.test("johndoe", "mypass")
|
9
|
+
# strength.weak?
|
10
|
+
# #=> true
|
11
|
+
#
|
12
|
+
def self.test(username, password)
|
13
|
+
strength = Base.new(username, password)
|
14
|
+
strength.test
|
15
|
+
strength
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PasswordStrength
|
2
|
+
module ActiveRecord
|
3
|
+
# Validates that the specified attributes are not weak (according to several rules).
|
4
|
+
#
|
5
|
+
# class Person < ActiveRecord::Base
|
6
|
+
# validates_strength_of :password
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# The default options are <tt>:level => :good, :with => :username</tt>.
|
10
|
+
#
|
11
|
+
# If you want to compare your password against other field, you have to set the <tt>:with</tt> option.
|
12
|
+
#
|
13
|
+
# validates_strength_of :password, :with => :email
|
14
|
+
#
|
15
|
+
# The available levels are: <tt>:weak</tt>, <tt>:good</tt> and <tt>:strong</tt>
|
16
|
+
#
|
17
|
+
def validates_strength_of(*attr_names)
|
18
|
+
options = attr_names.extract_options!
|
19
|
+
options.reverse_merge!(:level => :good, :with => :username, :message => "is too weak")
|
20
|
+
|
21
|
+
raise ArgumentError, "The :with option must be supplied" unless options.include?(:with)
|
22
|
+
raise ArgumentError, "The :level option must be one of [:weak, :good, :strong]" unless [:weak, :good, :strong].include?(options[:level])
|
23
|
+
|
24
|
+
validates_each(attr_names, options) do |record, attr_name, value|
|
25
|
+
strength = PasswordStrength.test(record.send(options[:with]), value)
|
26
|
+
record.errors.add(attr_name, :too_weak, :default => options[:message]) unless strength.valid?(options[:level])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class ActiveRecord::Base # :nodoc:
|
33
|
+
extend PasswordStrength::ActiveRecord
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveModel # :nodoc:
|
2
|
+
module Validations # :nodoc:
|
3
|
+
class StrengthValidator < EachValidator # :nodoc: all
|
4
|
+
def initialize(options)
|
5
|
+
super(options.reverse_merge(:level => :good, :with => :username, :message => "is too weak"))
|
6
|
+
end
|
7
|
+
|
8
|
+
def validate_each(record, attribute, value)
|
9
|
+
strength = PasswordStrength.test(record.send(options[:with]), value)
|
10
|
+
record.errors.add(attribute, :too_weak, :default => options[:message], :value => value) unless strength.valid?(options[:level])
|
11
|
+
end
|
12
|
+
|
13
|
+
def check_validity!
|
14
|
+
raise ArgumentError, "The :with option must be supplied" unless options.include?(:with)
|
15
|
+
raise ArgumentError, "The :level option must be one of [:weak, :good, :strong]" unless [:weak, :good, :strong].include?(options[:level])
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# Validates that the specified attributes are not weak (according to several rules).
|
22
|
+
#
|
23
|
+
# class Person < ActiveRecord::Base
|
24
|
+
# validates_strength_of :password
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# The default options are <tt>:level => :good, :with => :username</tt>.
|
28
|
+
#
|
29
|
+
# If you want to compare your password against other field, you have to set the <tt>:with</tt> option.
|
30
|
+
#
|
31
|
+
# validates_strength_of :password, :with => :email
|
32
|
+
#
|
33
|
+
# The available levels are: <tt>:weak</tt>, <tt>:good</tt> and <tt>:strong</tt>
|
34
|
+
#
|
35
|
+
def validates_strength_of(*attr_names)
|
36
|
+
validates_with StrengthValidator, _merge_attributes(attr_names)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module PasswordStrength
|
2
|
+
class Base
|
3
|
+
MULTIPLE_NUMBERS_RE = /\d.*?\d.*?\d/
|
4
|
+
MULTIPLE_SYMBOLS_RE = /[!@#\$%^&*?_~-].*?[!@#\$%^&*?_~-]/
|
5
|
+
SYMBOL_RE = /[!@#\$%^&*?_~-]/
|
6
|
+
UPPERCASE_LOWERCASE_RE = /([a-z].*[A-Z])|([A-Z].*[a-z])/
|
7
|
+
|
8
|
+
# Hold the username that will be matched against password
|
9
|
+
attr_accessor :username
|
10
|
+
|
11
|
+
# The password that will be tested
|
12
|
+
attr_accessor :password
|
13
|
+
|
14
|
+
# The score for the latest test. Will be nil if the password has not been tested.
|
15
|
+
attr_reader :score
|
16
|
+
|
17
|
+
# The current test status. Can be +:weak+, +:good+ or +:strong+
|
18
|
+
attr_reader :status
|
19
|
+
|
20
|
+
def initialize(username, password)
|
21
|
+
@username = username.to_s
|
22
|
+
@password = password.to_s
|
23
|
+
@score = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# Check if the password has the specified score.
|
27
|
+
# Level can be +:weak+, +:good+ or +:strong+.
|
28
|
+
def valid?(level)
|
29
|
+
case level
|
30
|
+
when :strong then
|
31
|
+
strong?
|
32
|
+
when :good then
|
33
|
+
good? || strong?
|
34
|
+
else
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if the password has been detected as strong.
|
40
|
+
def strong?
|
41
|
+
status == :strong
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check if the password has been detected as weak.
|
45
|
+
def weak?
|
46
|
+
status == :weak
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if the password has been detected as good.
|
50
|
+
def good?
|
51
|
+
status == :good
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return the score for the specified rule.
|
55
|
+
# Available rules:
|
56
|
+
#
|
57
|
+
# * :password_size
|
58
|
+
# * :numbers
|
59
|
+
# * :symbols
|
60
|
+
# * :uppercase_lowercase
|
61
|
+
# * :numbers_chars
|
62
|
+
# * :numbers_symbols
|
63
|
+
# * :symbols_chars
|
64
|
+
# * :only_chars
|
65
|
+
# * :only_numbers
|
66
|
+
# * :username
|
67
|
+
# * :sequences
|
68
|
+
def score_for(name)
|
69
|
+
score = 0
|
70
|
+
|
71
|
+
case name
|
72
|
+
when :password_size then
|
73
|
+
if password.size < 4
|
74
|
+
score = -100
|
75
|
+
else
|
76
|
+
score = password.size * 4
|
77
|
+
end
|
78
|
+
when :numbers then
|
79
|
+
score = 5 if password =~ MULTIPLE_NUMBERS_RE
|
80
|
+
when :symbols then
|
81
|
+
score = 5 if password =~ MULTIPLE_SYMBOLS_RE
|
82
|
+
when :uppercase_lowercase then
|
83
|
+
score = 10 if password =~ UPPERCASE_LOWERCASE_RE
|
84
|
+
when :numbers_chars then
|
85
|
+
score = 15 if password =~ /[a-z]/i && password =~ /[0-9]/
|
86
|
+
when :numbers_symbols then
|
87
|
+
score = 15 if password =~ /[0-9]/ && password =~ SYMBOL_RE
|
88
|
+
when :symbols_chars then
|
89
|
+
score = 15 if password =~ /[a-z]/i && password =~ SYMBOL_RE
|
90
|
+
when :only_chars then
|
91
|
+
score = -15 if password =~ /^[a-z]+$/i
|
92
|
+
when :only_numbers then
|
93
|
+
score = -15 if password =~ /^\d+$/
|
94
|
+
when :username then
|
95
|
+
if password == username
|
96
|
+
score = -100
|
97
|
+
else
|
98
|
+
score = -15 if password =~ /#{Regexp.escape(username)}/
|
99
|
+
end
|
100
|
+
when :sequences then
|
101
|
+
score = -15 * detect_sequences(password)
|
102
|
+
score += -15 * detect_sequences(password.to_s.reverse)
|
103
|
+
end
|
104
|
+
|
105
|
+
score
|
106
|
+
end
|
107
|
+
|
108
|
+
def test
|
109
|
+
@score = 0
|
110
|
+
@score += score_for(:password_size)
|
111
|
+
@score += score_for(:numbers)
|
112
|
+
@score += score_for(:symbols)
|
113
|
+
@score += score_for(:uppercase_lowercase)
|
114
|
+
@score += score_for(:numbers_chars)
|
115
|
+
@score += score_for(:numbers_symbols)
|
116
|
+
@score += score_for(:symbols_chars)
|
117
|
+
@score += score_for(:only_chars)
|
118
|
+
@score += score_for(:only_numbers)
|
119
|
+
@score += score_for(:username)
|
120
|
+
@score += score_for(:sequences)
|
121
|
+
|
122
|
+
@score = 0 if score < 0
|
123
|
+
@score = 100 if score > 100
|
124
|
+
|
125
|
+
@status = :weak if score < 35
|
126
|
+
@status = :good if score >= 35 && score < 70
|
127
|
+
@status = :strong if score >= 70
|
128
|
+
|
129
|
+
score
|
130
|
+
end
|
131
|
+
|
132
|
+
def detect_sequences(text) # :nodoc:
|
133
|
+
matches = 0
|
134
|
+
sequence_size = 0
|
135
|
+
bytes = []
|
136
|
+
|
137
|
+
text.to_s.each_byte do |byte|
|
138
|
+
previous_byte = bytes.last
|
139
|
+
bytes << byte
|
140
|
+
|
141
|
+
if previous_byte && ((byte == previous_byte + 1) || (previous_byte == byte))
|
142
|
+
sequence_size += 1
|
143
|
+
else
|
144
|
+
sequence_size = 0
|
145
|
+
end
|
146
|
+
|
147
|
+
matches += 1 if sequence_size == 2
|
148
|
+
end
|
149
|
+
|
150
|
+
matches
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|