simpleidn 0.2.3 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/LICENCE +21 -21
- data/README.md +82 -0
- data/lib/simpleidn/uts46mapping.rb +166 -4
- data/lib/simpleidn/version.rb +1 -1
- data/lib/simpleidn.rb +293 -283
- data/simpleidn.gemspec +13 -3
- metadata +14 -18
- data/.gitignore +0 -11
- data/.travis.yml +0 -9
- data/Gemfile +0 -7
- data/README.rdoc +0 -39
- data/Rakefile +0 -6
- data/tables/IdnaMappingTable.txt +0 -9025
- data/tables/generate_mapping_table.rb +0 -51
data/lib/simpleidn.rb
CHANGED
|
@@ -1,283 +1,293 @@
|
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
out
|
|
282
|
-
|
|
283
|
-
|
|
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,11 +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.
|
|
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
|
+
}
|
|
22
|
+
|
|
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
|
|
17
27
|
spec.require_paths = ["lib"]
|
|
18
28
|
|
|
19
|
-
spec.add_development_dependency "rake", "~> 13.0
|
|
20
|
-
spec.add_development_dependency "rspec", "~> 3.
|
|
29
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
21
31
|
|
|
22
32
|
spec.required_ruby_version = '>=2.2'
|
|
23
33
|
end
|