simpleidn 0.2.2 → 0.3.0

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.
data/lib/simpleidn.rb CHANGED
@@ -1,284 +1,293 @@
1
- require 'simpleidn/version'
2
- require 'simpleidn/uts46mapping'
3
- require 'unf'
4
-
5
- module SimpleIDN
6
- # The ConversionError is raised when an error occurs during a
7
- # Punycode <-> Unicode conversion.
8
- class ConversionError < RangeError
9
- end
10
-
11
- module Punycode
12
- INITIAL_N = 0x80
13
- INITIAL_BIAS = 72
14
- DELIMITER = 0x2D
15
- BASE = 36
16
- DAMP = 700
17
- TMIN = 1
18
- TMAX = 26
19
- SKEW = 38
20
- MAXINT = 0x7FFFFFFF
21
- ASCII_MAX = 0x7F
22
-
23
- EMPTY = ''.encode(Encoding::UTF_8).freeze
24
-
25
- module_function
26
-
27
- # decode_digit(cp) returns the numeric value of a basic code
28
- # point (for use in representing integers) in the range 0 to
29
- # base-1, or base if cp is does not represent a value.
30
- def decode_digit(cp)
31
- cp - 48 < 10 ? cp - 22 : cp - 65 < 26 ? cp - 65 : cp - 97 < 26 ? cp - 97 : BASE
32
- end
33
-
34
- # encode_digit(d) returns the basic code point whose value
35
- # (when used for representing integers) is d, which needs to be in
36
- # the range 0 to base-1.
37
- def encode_digit(d)
38
- d + 22 + 75 * (d < 26 ? 1 : 0)
39
- # 0..25 map to ASCII a..z
40
- # 26..35 map to ASCII 0..9
41
- end
42
-
43
- # Bias adaptation function
44
- def adapt(delta, numpoints, firsttime)
45
- delta = firsttime ? (delta / DAMP) : (delta >> 1)
46
- delta += (delta / numpoints)
47
-
48
- k = 0
49
- while delta > (((BASE - TMIN) * TMAX) / 2)
50
- delta /= BASE - TMIN
51
- k += BASE
52
- end
53
- k + (BASE - TMIN + 1) * delta / (delta + SKEW)
54
- end
55
-
56
- # Main decode
57
- def decode(input)
58
- input_encoding = input.encoding
59
- input = input.encode(Encoding::UTF_8).codepoints.to_a
60
- output = []
61
-
62
- # Initialize the state:
63
- n = INITIAL_N
64
- i = 0
65
- bias = INITIAL_BIAS
66
-
67
- # Handle the basic code points: Let basic be the number of input code
68
- # points before the last delimiter, or 0 if there is none, then
69
- # copy the first basic code points to the output.
70
- basic = input.rindex(DELIMITER) || 0
71
-
72
- input[0, basic].each do |char|
73
- raise(ConversionError, "Illegal input >= 0x80") if char > ASCII_MAX
74
- output << char
75
- end
76
-
77
- # Main decoding loop: Start just after the last delimiter if any
78
- # basic code points were copied; start at the beginning otherwise.
79
-
80
- ic = basic > 0 ? basic + 1 : 0
81
- while ic < input.length
82
- # ic is the index of the next character to be consumed,
83
-
84
- # Decode a generalized variable-length integer into delta,
85
- # which gets added to i. The overflow checking is easier
86
- # if we increase i as we go, then subtract off its starting
87
- # value at the end to obtain delta.
88
- oldi = i
89
- w = 1
90
- k = BASE
91
- loop do
92
- raise(ConversionError, "punycode_bad_input(1)") if ic >= input.length
93
-
94
- digit = decode_digit(input[ic])
95
- ic += 1
96
-
97
- raise(ConversionError, "punycode_bad_input(2)") if digit >= BASE
98
-
99
- raise(ConversionError, "punycode_overflow(1)") if digit > (MAXINT - i) / w
100
-
101
- i += digit * w
102
- t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias
103
- break if digit < t
104
- raise(ConversionError, "punycode_overflow(2)") if w > MAXINT / (BASE - t)
105
-
106
- w *= BASE - t
107
- k += BASE
108
- end
109
-
110
- out = output.length + 1
111
- bias = adapt(i - oldi, out, oldi == 0)
112
-
113
- # i was supposed to wrap around from out to 0,
114
- # incrementing n each time, so we'll fix that now:
115
- raise(ConversionError, "punycode_overflow(3)") if (i / out) > MAXINT - n
116
-
117
- n += (i / out)
118
- i %= out
119
-
120
- # Insert n at position i of the output:
121
- output.insert(i, n)
122
- i += 1
123
- end
124
-
125
- output.collect {|c| c.chr(Encoding::UTF_8)}.join(EMPTY).encode(input_encoding)
126
- end
127
-
128
- # Main encode function
129
- def encode(input)
130
- input_encoding = input.encoding
131
- input = input.encode(Encoding::UTF_8).codepoints.to_a
132
- output = []
133
-
134
- # Initialize the state:
135
- n = INITIAL_N
136
- delta = 0
137
- bias = INITIAL_BIAS
138
-
139
- # Handle the basic code points:
140
- output = input.select { |char| char <= ASCII_MAX }
141
-
142
- h = b = output.length
143
-
144
- # h is the number of code points that have been handled, b is the
145
- # number of basic code points
146
-
147
- output << DELIMITER if b > 0
148
-
149
- # Main encoding loop:
150
- while h < input.length
151
- # All non-basic code points < n have been
152
- # handled already. Find the next larger one:
153
-
154
- m = MAXINT
155
-
156
- input.each do |char|
157
- m = char if char >= n && char < m
158
- end
159
-
160
- # Increase delta enough to advance the decoder's
161
- # <n,i> state to <m,0>, but guard against overflow:
162
-
163
- raise(ConversionError, "punycode_overflow (1)") if m - n > ((MAXINT - delta) / (h + 1)).floor
164
-
165
- delta += (m - n) * (h + 1)
166
- n = m
167
-
168
- input.each_with_index do |char, _|
169
- if char < n
170
- delta += 1
171
- raise(ConversionError, "punycode_overflow(2)") if delta > MAXINT
172
- end
173
-
174
- next unless char == n
175
-
176
- # Represent delta as a generalized variable-length integer:
177
- q = delta
178
- k = BASE
179
- loop do
180
- t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias
181
- break if q < t
182
- output << encode_digit(t + (q - t) % (BASE - t))
183
- q = ((q - t) / (BASE - t)).floor
184
- k += BASE
185
- end
186
- output << encode_digit(q)
187
- bias = adapt(delta, h + 1, h == b)
188
- delta = 0
189
- h += 1
190
- end
191
-
192
- delta += 1
193
- n += 1
194
- end
195
- output.collect {|c| c.chr(Encoding::UTF_8)}.join(EMPTY).encode(input_encoding)
196
- end
197
- end
198
-
199
- ACE_PREFIX = 'xn--'.encode(Encoding::UTF_8).freeze
200
- ASCII_MAX = 0x7F
201
- DOT = 0x2E.chr(Encoding::UTF_8).freeze
202
- EMPTY = ''.encode(Encoding::UTF_8).freeze
203
- LABEL_SEPERATOR_RE = /[\u002e\uff0e\u3002\uff61]/
204
-
205
- unless defined?(UTS64MAPPING)
206
- # Define a basic uppercase to lowercase mapping for ASCII a..z
207
- UTS64MAPPING = Hash[(65..90).map { |n| [n, n + 32] }].freeze
208
- end
209
-
210
- # See UTS46 Table 1
211
- TRANSITIONAL = {
212
- 0x00DF => [0x0073, 0x0073],
213
- 0x03C2 => 0x03C3,
214
- 0x200C => [],
215
- 0x200D => []
216
- }.freeze
217
-
218
- module_function
219
-
220
- # Applies UTS46 mapping to a Unicode string
221
- # Returns a UTF-8 string in Normalization Form C (NFC)
222
- def uts46map(str, transitional = false)
223
- mapped = str.codepoints.map { |cp| UTS64MAPPING.fetch(cp, cp) }
224
- mapped = mapped.map { |cp| TRANSITIONAL.fetch(cp, cp) } if transitional
225
- mapped = mapped.flatten.map { |cp| cp.chr(Encoding::UTF_8) }.join(EMPTY)
226
- mapped.to_nfc
227
- end
228
-
229
- # Converts a UTF-8 unicode string to a punycode ACE string.
230
- # == Example
231
- # SimpleIDN.to_ascii("møllerriis.com")
232
- # => "xn--mllerriis-l8a.com"
233
- def to_ascii(domain, transitional = false)
234
- return nil if domain.nil?
235
- mapped_domain = uts46map(domain.encode(Encoding::UTF_8), transitional)
236
- domain_array = mapped_domain.split(LABEL_SEPERATOR_RE, -1) rescue []
237
- out = []
238
- content = false
239
- domain_array.each do |s|
240
- # Skip leading empty labels
241
- next if s.empty? && !content
242
- content = true
243
-
244
- out << (s.codepoints.any? { |cp| cp > ASCII_MAX } ? ACE_PREFIX + Punycode.encode(s) : s)
245
- end
246
-
247
- # If all we had were dots; return "."
248
- out = [DOT] if out.empty? && !mapped_domain.empty?
249
-
250
- out.join(DOT).encode(domain.encoding)
251
- end
252
-
253
- # Converts a punycode ACE string to a UTF-8 unicode string.
254
- # == Example
255
- # SimpleIDN.to_unicode("xn--mllerriis-l8a.com")
256
- # => "møllerriis.com"
257
- def to_unicode(domain, transitional = false)
258
- return nil if domain.nil?
259
- mapped_domain = uts46map(domain.encode(Encoding::UTF_8), transitional)
260
- domain_array = mapped_domain.split(LABEL_SEPERATOR_RE, -1) rescue []
261
- out = []
262
- content = false
263
- domain_array.each do |s|
264
- # Skip leading empty labels
265
- next if s.empty? && !content
266
- content = true
267
-
268
- out << (s.start_with?(ACE_PREFIX) ? Punycode.decode(s[ACE_PREFIX.length..-1]) : s)
269
- end
270
-
271
- # If all we had were dots; return "."
272
- out = [DOT] if out.empty? && !mapped_domain.empty?
273
-
274
- out = out.join(DOT)
275
- # Try to convert to the input encoding, but don't error on failure
276
- # Given that the input is plain 7-bit ASCII only, converting back
277
- # frequently fails. We will try to allow UTF-16 and Unicode encodings
278
- begin
279
- out.encode!(domain.encoding)
280
- rescue Encoding::UndefinedConversionError
281
- end
282
- out
283
- end
284
- end
1
+ require 'simpleidn/version'
2
+ require 'simpleidn/uts46mapping'
3
+
4
+ module SimpleIDN
5
+ # The ConversionError is raised when an error occurs during a
6
+ # Punycode <-> Unicode conversion.
7
+ class ConversionError < RangeError
8
+ end
9
+
10
+ module Punycode
11
+ INITIAL_N = 0x80
12
+ INITIAL_BIAS = 72
13
+ DELIMITER = 0x2D
14
+ BASE = 36
15
+ DAMP = 700
16
+ TMIN = 1
17
+ TMAX = 26
18
+ SKEW = 38
19
+ MAXINT = 0x7FFFFFFF
20
+ ASCII_MAX = 0x7F
21
+
22
+ EMPTY = ''.encode(Encoding::UTF_8).freeze
23
+
24
+ module_function
25
+
26
+ # decode_digit(cp) returns the numeric value of a basic code
27
+ # point (for use in representing integers) in the range 0 to
28
+ # base-1, or base if cp is does not represent a value.
29
+ def decode_digit(cp)
30
+ cp - 48 < 10 ? cp - 22 : cp - 65 < 26 ? cp - 65 : cp - 97 < 26 ? cp - 97 : BASE
31
+ end
32
+
33
+ # encode_digit(d) returns the basic code point whose value
34
+ # (when used for representing integers) is d, which needs to be in
35
+ # the range 0 to base-1.
36
+ def encode_digit(d)
37
+ d + 22 + 75 * (d < 26 ? 1 : 0)
38
+ # 0..25 map to ASCII a..z
39
+ # 26..35 map to ASCII 0..9
40
+ end
41
+
42
+ # Bias adaptation function
43
+ def adapt(delta, numpoints, firsttime)
44
+ delta = firsttime ? (delta / DAMP) : (delta >> 1)
45
+ delta += (delta / numpoints)
46
+
47
+ k = 0
48
+ while delta > (((BASE - TMIN) * TMAX) / 2)
49
+ delta /= BASE - TMIN
50
+ k += BASE
51
+ end
52
+ k + (BASE - TMIN + 1) * delta / (delta + SKEW)
53
+ end
54
+
55
+ # Main decode
56
+ def decode(input)
57
+ input_encoding = input.encoding
58
+ input = input.encode(Encoding::UTF_8).codepoints.to_a
59
+ output = []
60
+
61
+ # Initialize the state:
62
+ n = INITIAL_N
63
+ i = 0
64
+ bias = INITIAL_BIAS
65
+
66
+ # Handle the basic code points: Let basic be the number of input code
67
+ # points before the last delimiter, or 0 if there is none, then
68
+ # copy the first basic code points to the output.
69
+ basic = input.rindex(DELIMITER) || 0
70
+
71
+ input[0, basic].each do |char|
72
+ raise(ConversionError, "Illegal input >= 0x80") if char > ASCII_MAX
73
+ output << char
74
+ end
75
+
76
+ # Main decoding loop: Start just after the last delimiter if any
77
+ # basic code points were copied; start at the beginning otherwise.
78
+
79
+ ic = basic > 0 ? basic + 1 : 0
80
+ while ic < input.length
81
+ # ic is the index of the next character to be consumed,
82
+
83
+ # Decode a generalized variable-length integer into delta,
84
+ # which gets added to i. The overflow checking is easier
85
+ # if we increase i as we go, then subtract off its starting
86
+ # value at the end to obtain delta.
87
+ oldi = i
88
+ w = 1
89
+ k = BASE
90
+ loop do
91
+ raise(ConversionError, "punycode_bad_input(1)") if ic >= input.length
92
+
93
+ digit = decode_digit(input[ic])
94
+ ic += 1
95
+
96
+ raise(ConversionError, "punycode_bad_input(2)") if digit >= BASE
97
+
98
+ raise(ConversionError, "punycode_overflow(1)") if digit > (MAXINT - i) / w
99
+
100
+ i += digit * w
101
+ t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias
102
+ break if digit < t
103
+ raise(ConversionError, "punycode_overflow(2)") if w > MAXINT / (BASE - t)
104
+
105
+ w *= BASE - t
106
+ k += BASE
107
+ end
108
+
109
+ out = output.length + 1
110
+ bias = adapt(i - oldi, out, oldi == 0)
111
+
112
+ # i was supposed to wrap around from out to 0,
113
+ # incrementing n each time, so we'll fix that now:
114
+ raise(ConversionError, "punycode_overflow(3)") if (i / out) > MAXINT - n
115
+
116
+ n += (i / out)
117
+ i %= out
118
+
119
+ # Insert n at position i of the output:
120
+ output.insert(i, n)
121
+ i += 1
122
+ end
123
+
124
+ output.collect {|c| c.chr(Encoding::UTF_8)}.join(EMPTY).encode(input_encoding)
125
+ end
126
+
127
+ # Main encode function
128
+ def encode(input)
129
+ input_encoding = input.encoding
130
+ input = input.encode(Encoding::UTF_8).codepoints.to_a
131
+ output = []
132
+
133
+ # Initialize the state:
134
+ n = INITIAL_N
135
+ delta = 0
136
+ bias = INITIAL_BIAS
137
+
138
+ # Handle the basic code points:
139
+ output = input.select { |char| char <= ASCII_MAX }
140
+
141
+ h = b = output.length
142
+
143
+ # h is the number of code points that have been handled, b is the
144
+ # number of basic code points
145
+
146
+ output << DELIMITER if b > 0
147
+
148
+ # Main encoding loop:
149
+ while h < input.length
150
+ # All non-basic code points < n have been
151
+ # handled already. Find the next larger one:
152
+
153
+ m = MAXINT
154
+
155
+ input.each do |char|
156
+ m = char if char >= n && char < m
157
+ end
158
+
159
+ # Increase delta enough to advance the decoder's
160
+ # <n,i> state to <m,0>, but guard against overflow:
161
+
162
+ raise(ConversionError, "punycode_overflow (1)") if m - n > ((MAXINT - delta) / (h + 1)).floor
163
+
164
+ delta += (m - n) * (h + 1)
165
+ n = m
166
+
167
+ input.each_with_index do |char, _|
168
+ if char < n
169
+ delta += 1
170
+ raise(ConversionError, "punycode_overflow(2)") if delta > MAXINT
171
+ end
172
+
173
+ next unless char == n
174
+
175
+ # Represent delta as a generalized variable-length integer:
176
+ q = delta
177
+ k = BASE
178
+ loop do
179
+ t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias
180
+ break if q < t
181
+ output << encode_digit(t + (q - t) % (BASE - t))
182
+ q = ((q - t) / (BASE - t)).floor
183
+ k += BASE
184
+ end
185
+ output << encode_digit(q)
186
+ bias = adapt(delta, h + 1, h == b)
187
+ delta = 0
188
+ h += 1
189
+ end
190
+
191
+ delta += 1
192
+ n += 1
193
+ end
194
+ output.collect {|c| c.chr(Encoding::UTF_8)}.join(EMPTY).encode(input_encoding)
195
+ end
196
+ end
197
+
198
+ ACE_PREFIX = 'xn--'.encode(Encoding::UTF_8).freeze
199
+ ASCII_MAX = 0x7F
200
+ DOT = 0x2E.chr(Encoding::UTF_8).freeze
201
+ EMPTY = ''.encode(Encoding::UTF_8).freeze
202
+ LABEL_SEPERATOR_RE = /[\u002e\uff0e\u3002\uff61]/
203
+
204
+ unless defined?(UTS64MAPPING)
205
+ # Define a basic uppercase to lowercase mapping for ASCII a..z
206
+ UTS64MAPPING = Hash[(65..90).map { |n| [n, n + 32] }].freeze
207
+ end
208
+
209
+ # See UTS46 Table 1
210
+ TRANSITIONAL = {
211
+ 0x00DF => [0x0073, 0x0073],
212
+ 0x03C2 => 0x03C3,
213
+ 0x200C => [],
214
+ 0x200D => []
215
+ }.freeze
216
+
217
+ module_function
218
+
219
+ # Applies UTS46 mapping to a Unicode string
220
+ # Returns a UTF-8 string in Normalization Form C (NFC)
221
+ def uts46map(str, transitional = false)
222
+ mapped = str.codepoints.map { |cp| UTS64MAPPING.fetch(cp, cp) }
223
+ mapped = mapped.map { |cp| TRANSITIONAL.fetch(cp, cp) } if transitional
224
+ mapped = mapped.flatten.map { |cp| cp.chr(Encoding::UTF_8) }.join(EMPTY)
225
+ mapped.unicode_normalize(:nfc)
226
+ end
227
+
228
+ # Converts a UTF-8 unicode string to a punycode ACE string.
229
+ # == Example
230
+ # SimpleIDN.to_ascii("møllerriis.com")
231
+ # => "xn--mllerriis-l8a.com"
232
+ def to_ascii(domain, transitional = false)
233
+ return nil if domain.nil?
234
+ mapped_domain = uts46map(domain.encode(Encoding::UTF_8), transitional)
235
+ domain_array = mapped_domain.split(LABEL_SEPERATOR_RE, -1) rescue []
236
+ out = []
237
+ content = false
238
+ domain_array.each do |s|
239
+ # Skip leading empty labels
240
+ next if s.empty? && !content
241
+ content = true
242
+
243
+ out << (s.codepoints.any? { |cp| cp > ASCII_MAX } ? ACE_PREFIX + Punycode.encode(s) : s)
244
+ end
245
+
246
+ # If all we had were dots; return "."
247
+ out = [DOT] if out.empty? && !mapped_domain.empty?
248
+
249
+ out.join(DOT).encode(domain.encoding)
250
+ end
251
+
252
+ # Converts a punycode ACE string to a UTF-8 unicode string.
253
+ # == Example
254
+ # SimpleIDN.to_unicode("xn--mllerriis-l8a.com")
255
+ # => "møllerriis.com"
256
+ def to_unicode(domain, transitional = false)
257
+ return nil if domain.nil?
258
+ mapped_domain = uts46map(domain.encode(Encoding::UTF_8), transitional)
259
+ domain_array = mapped_domain.split(LABEL_SEPERATOR_RE, -1) rescue []
260
+ out = []
261
+ content = false
262
+ domain_array.each do |s|
263
+ # Skip leading empty labels
264
+ next if s.empty? && !content
265
+ content = true
266
+
267
+ if s.start_with?(ACE_PREFIX)
268
+ decoded = Punycode.decode(s[ACE_PREFIX.length..-1])
269
+ # UTS46 Processing step 4.2: a Punycode label must decode to a
270
+ # non-empty string that contains at least one non-ASCII code point.
271
+ if decoded.codepoints.none? { |cp| cp > ASCII_MAX }
272
+ raise(ConversionError, "Punycode label must decode to non-ASCII text")
273
+ end
274
+ out << decoded
275
+ else
276
+ out << s
277
+ end
278
+ end
279
+
280
+ # If all we had were dots; return "."
281
+ out = [DOT] if out.empty? && !mapped_domain.empty?
282
+
283
+ out = out.join(DOT)
284
+ # Try to convert to the input encoding, but don't error on failure
285
+ # Given that the input is plain 7-bit ASCII only, converting back
286
+ # frequently fails. We will try to allow UTF-16 and Unicode encodings
287
+ begin
288
+ out.encode!(domain.encoding)
289
+ rescue Encoding::UndefinedConversionError
290
+ end
291
+ out
292
+ end
293
+ end
data/simpleidn.gemspec CHANGED
@@ -13,13 +13,21 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/mmriis/simpleidn"
14
14
  spec.license = "MIT"
15
15
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.require_paths = ["lib"]
16
+ spec.metadata = {
17
+ "source_code_uri" => spec.homepage,
18
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
19
+ "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md",
20
+ "rubygems_mfa_required" => "true"
21
+ }
18
22
 
19
- spec.add_runtime_dependency "unf", '~> 0.1.4'
23
+ # Ship only what is needed at runtime plus top-level docs.
24
+ spec.files = `git ls-files -z`.split("\x0").select do |f|
25
+ f.start_with?('lib/') || %w[README.md CHANGELOG.md LICENCE simpleidn.gemspec].include?(f)
26
+ end
27
+ spec.require_paths = ["lib"]
20
28
 
21
- spec.add_development_dependency "rake", "~> 13.0.3"
22
- spec.add_development_dependency "rspec", "~> 3.10"
29
+ spec.add_development_dependency "rake", "~> 13.0"
30
+ spec.add_development_dependency "rspec", "~> 3.13"
23
31
 
24
32
  spec.required_ruby_version = '>=2.2'
25
33
  end