httpx 0.8.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +9 -5
  4. data/doc/release_notes/0_0_1.md +7 -0
  5. data/doc/release_notes/0_0_2.md +9 -0
  6. data/doc/release_notes/0_0_3.md +9 -0
  7. data/doc/release_notes/0_0_4.md +7 -0
  8. data/doc/release_notes/0_0_5.md +5 -0
  9. data/doc/release_notes/0_10_0.md +66 -0
  10. data/doc/release_notes/0_10_1.md +37 -0
  11. data/doc/release_notes/0_10_2.md +5 -0
  12. data/doc/release_notes/0_1_0.md +9 -0
  13. data/doc/release_notes/0_2_0.md +5 -0
  14. data/doc/release_notes/0_2_1.md +16 -0
  15. data/doc/release_notes/0_3_0.md +12 -0
  16. data/doc/release_notes/0_3_1.md +6 -0
  17. data/doc/release_notes/0_4_0.md +51 -0
  18. data/doc/release_notes/0_4_1.md +3 -0
  19. data/doc/release_notes/0_5_0.md +15 -0
  20. data/doc/release_notes/0_5_1.md +14 -0
  21. data/doc/release_notes/0_6_0.md +5 -0
  22. data/doc/release_notes/0_6_1.md +6 -0
  23. data/doc/release_notes/0_6_2.md +6 -0
  24. data/doc/release_notes/0_6_3.md +13 -0
  25. data/doc/release_notes/0_6_4.md +21 -0
  26. data/doc/release_notes/0_6_5.md +22 -0
  27. data/doc/release_notes/0_6_6.md +19 -0
  28. data/doc/release_notes/0_6_7.md +5 -0
  29. data/doc/release_notes/0_7_0.md +46 -0
  30. data/doc/release_notes/0_8_0.md +27 -0
  31. data/doc/release_notes/0_8_1.md +8 -0
  32. data/doc/release_notes/0_8_2.md +7 -0
  33. data/doc/release_notes/0_9_0.md +38 -0
  34. data/lib/httpx.rb +2 -0
  35. data/lib/httpx/adapters/faraday.rb +1 -1
  36. data/lib/httpx/chainable.rb +11 -11
  37. data/lib/httpx/connection.rb +23 -31
  38. data/lib/httpx/connection/http1.rb +30 -4
  39. data/lib/httpx/connection/http2.rb +26 -9
  40. data/lib/httpx/domain_name.rb +440 -0
  41. data/lib/httpx/errors.rb +2 -1
  42. data/lib/httpx/extensions.rb +22 -2
  43. data/lib/httpx/headers.rb +1 -1
  44. data/lib/httpx/io/ssl.rb +0 -1
  45. data/lib/httpx/io/tcp.rb +6 -5
  46. data/lib/httpx/io/udp.rb +4 -1
  47. data/lib/httpx/options.rb +5 -1
  48. data/lib/httpx/parser/http1.rb +14 -17
  49. data/lib/httpx/plugins/compression.rb +46 -65
  50. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  51. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  52. data/lib/httpx/plugins/compression/gzip.rb +23 -5
  53. data/lib/httpx/plugins/cookies.rb +21 -60
  54. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  55. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  56. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  57. data/lib/httpx/plugins/expect.rb +12 -1
  58. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  59. data/lib/httpx/plugins/h2c.rb +1 -1
  60. data/lib/httpx/plugins/multipart.rb +12 -6
  61. data/lib/httpx/plugins/persistent.rb +6 -1
  62. data/lib/httpx/plugins/proxy.rb +16 -2
  63. data/lib/httpx/plugins/proxy/socks4.rb +14 -14
  64. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  65. data/lib/httpx/plugins/retries.rb +3 -2
  66. data/lib/httpx/plugins/stream.rb +109 -13
  67. data/lib/httpx/pool.rb +14 -20
  68. data/lib/httpx/request.rb +8 -20
  69. data/lib/httpx/resolver.rb +7 -10
  70. data/lib/httpx/resolver/https.rb +22 -24
  71. data/lib/httpx/resolver/native.rb +19 -16
  72. data/lib/httpx/resolver/resolver_mixin.rb +4 -2
  73. data/lib/httpx/resolver/system.rb +2 -2
  74. data/lib/httpx/response.rb +16 -25
  75. data/lib/httpx/selector.rb +11 -18
  76. data/lib/httpx/session.rb +40 -26
  77. data/lib/httpx/transcoder.rb +18 -0
  78. data/lib/httpx/transcoder/chunker.rb +0 -2
  79. data/lib/httpx/transcoder/form.rb +9 -7
  80. data/lib/httpx/transcoder/json.rb +0 -4
  81. data/lib/httpx/utils.rb +45 -0
  82. data/lib/httpx/version.rb +1 -1
  83. data/sig/buffer.rbs +24 -0
  84. data/sig/callbacks.rbs +14 -0
  85. data/sig/chainable.rbs +37 -0
  86. data/sig/connection.rbs +85 -0
  87. data/sig/connection/http1.rbs +66 -0
  88. data/sig/connection/http2.rbs +78 -0
  89. data/sig/domain_name.rbs +17 -0
  90. data/sig/errors.rbs +3 -0
  91. data/sig/headers.rbs +42 -0
  92. data/sig/httpx.rbs +15 -0
  93. data/sig/loggable.rbs +11 -0
  94. data/sig/missing.rbs +12 -0
  95. data/sig/options.rbs +118 -0
  96. data/sig/parser/http1.rbs +50 -0
  97. data/sig/plugins/authentication.rbs +11 -0
  98. data/sig/plugins/basic_authentication.rbs +13 -0
  99. data/sig/plugins/compression.rbs +55 -0
  100. data/sig/plugins/compression/brotli.rbs +21 -0
  101. data/sig/plugins/compression/deflate.rbs +17 -0
  102. data/sig/plugins/compression/gzip.rbs +29 -0
  103. data/sig/plugins/cookies.rbs +26 -0
  104. data/sig/plugins/cookies/cookie.rbs +50 -0
  105. data/sig/plugins/cookies/jar.rbs +27 -0
  106. data/sig/plugins/digest_authentication.rbs +33 -0
  107. data/sig/plugins/expect.rbs +19 -0
  108. data/sig/plugins/follow_redirects.rbs +37 -0
  109. data/sig/plugins/h2c.rbs +26 -0
  110. data/sig/plugins/multipart.rbs +21 -0
  111. data/sig/plugins/persistent.rbs +17 -0
  112. data/sig/plugins/proxy.rbs +47 -0
  113. data/sig/plugins/proxy/http.rbs +14 -0
  114. data/sig/plugins/proxy/socks4.rbs +33 -0
  115. data/sig/plugins/proxy/socks5.rbs +36 -0
  116. data/sig/plugins/proxy/ssh.rbs +18 -0
  117. data/sig/plugins/push_promise.rbs +22 -0
  118. data/sig/plugins/rate_limiter.rbs +11 -0
  119. data/sig/plugins/retries.rbs +48 -0
  120. data/sig/plugins/stream.rbs +39 -0
  121. data/sig/pool.rbs +36 -0
  122. data/sig/registry.rbs +9 -0
  123. data/sig/request.rbs +61 -0
  124. data/sig/resolver.rbs +26 -0
  125. data/sig/resolver/https.rbs +49 -0
  126. data/sig/resolver/native.rbs +60 -0
  127. data/sig/resolver/resolver_mixin.rbs +27 -0
  128. data/sig/resolver/system.rbs +17 -0
  129. data/sig/response.rbs +87 -0
  130. data/sig/selector.rbs +20 -0
  131. data/sig/session.rbs +49 -0
  132. data/sig/timeout.rbs +29 -0
  133. data/sig/transcoder.rbs +18 -0
  134. data/sig/transcoder/body.rbs +18 -0
  135. data/sig/transcoder/chunker.rbs +32 -0
  136. data/sig/transcoder/form.rbs +16 -0
  137. data/sig/transcoder/json.rbs +14 -0
  138. metadata +130 -22
  139. data/lib/httpx/resolver/options.rb +0 -25
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
3
4
  require "io/wait"
4
5
  require "http/2/next"
5
6
 
@@ -25,6 +26,7 @@ module HTTPX
25
26
  @pending = []
26
27
  @streams = {}
27
28
  @drains = {}
29
+ @pings = []
28
30
  @buffer = buffer
29
31
  @handshake_completed = false
30
32
  init_connection
@@ -49,10 +51,6 @@ module HTTPX
49
51
  :rw
50
52
  end
51
53
 
52
- def reset
53
- init_connection
54
- end
55
-
56
54
  def close
57
55
  @connection.goaway unless @connection.state == :closed
58
56
  emit(:close)
@@ -109,6 +107,13 @@ module HTTPX
109
107
  end
110
108
  end
111
109
 
110
+ def ping
111
+ ping = SecureRandom.gen_random(8)
112
+ @connection.ping(ping)
113
+ ensure
114
+ @pings << ping
115
+ end
116
+
112
117
  private
113
118
 
114
119
  def send_pending
@@ -143,6 +148,7 @@ module HTTPX
143
148
  @connection.on(:promise, &method(:on_promise))
144
149
  @connection.on(:altsvc) { |frame| on_altsvc(frame[:origin], frame) }
145
150
  @connection.on(:settings_ack, &method(:on_settings))
151
+ @connection.on(:ack, &method(:on_pong))
146
152
  @connection.on(:goaway, &method(:on_close))
147
153
  #
148
154
  # Some servers initiate HTTP/2 negotiation right away, some don't.
@@ -153,6 +159,9 @@ module HTTPX
153
159
  @connection.send_connection_preface
154
160
  end
155
161
 
162
+ alias_method :reset, :init_connection
163
+ public :reset
164
+
156
165
  def handle_stream(stream, request)
157
166
  stream.on(:close, &method(:on_stream_close).curry[stream, request])
158
167
  stream.on(:half_close) do
@@ -260,16 +269,16 @@ module HTTPX
260
269
  end
261
270
 
262
271
  def on_close(_last_frame, error, _payload)
272
+ is_connection_closed = @connection.state == :closed
263
273
  if error && error != :no_error
274
+ @buffer.clear if is_connection_closed
264
275
  ex = Error.new(0, error)
265
276
  ex.set_backtrace(caller)
266
- @streams.each_key do |request|
267
- emit(:error, request, ex)
268
- end
277
+ handle_error(ex)
269
278
  end
270
- return unless @connection.state == :closed && @streams.size.zero?
279
+ return unless is_connection_closed && @streams.size.zero?
271
280
 
272
- emit(:close)
281
+ emit(:close, is_connection_closed)
273
282
  end
274
283
 
275
284
  def on_frame_sent(frame)
@@ -306,6 +315,14 @@ module HTTPX
306
315
  emit(:origin, origin)
307
316
  end
308
317
 
318
+ def on_pong(ping)
319
+ if !@pings.delete(ping.to_s)
320
+ close(:protocol_error, "ping payload did not match")
321
+ else
322
+ emit(:pong)
323
+ end
324
+ end
325
+
309
326
  def respond_to_missing?(meth, *args)
310
327
  @connection.respond_to?(meth, *args) || super
311
328
  end
@@ -0,0 +1,440 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # domain_name.rb - Domain Name manipulation library for Ruby
5
+ #
6
+ # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions
10
+ # are met:
11
+ # 1. Redistributions of source code must retain the above copyright
12
+ # notice, this list of conditions and the following disclaimer.
13
+ # 2. Redistributions in binary form must reproduce the above copyright
14
+ # notice, this list of conditions and the following disclaimer in the
15
+ # documentation and/or other materials provided with the distribution.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23
+ # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27
+ # SUCH DAMAGE.
28
+
29
+ require "ipaddr"
30
+
31
+ module HTTPX
32
+ # Represents a domain name ready for extracting its registered domain
33
+ # and TLD.
34
+ class DomainName
35
+ include Comparable
36
+
37
+ # The full host name normalized, ASCII-ized and downcased using the
38
+ # Unicode NFC rules and the Punycode algorithm. If initialized with
39
+ # an IP address, the string representation of the IP address
40
+ # suitable for opening a connection to.
41
+ attr_reader :hostname
42
+
43
+ # The Unicode representation of the #hostname property.
44
+ #
45
+ # :attr_reader: hostname_idn
46
+
47
+ # The least "universally original" domain part of this domain name.
48
+ # For example, "example.co.uk" for "www.sub.example.co.uk". This
49
+ # may be nil if the hostname does not have one, like when it is an
50
+ # IP address, an effective TLD or higher itself, or of a
51
+ # non-canonical domain.
52
+ attr_reader :domain
53
+
54
+ DOT = "." # :nodoc:
55
+
56
+ class << self
57
+ def new(domain)
58
+ return domain if domain.is_a?(self)
59
+
60
+ super(domain)
61
+ end
62
+
63
+ # Normalizes a _domain_ using the Punycode algorithm as necessary.
64
+ # The result will be a downcased, ASCII-only string.
65
+ def normalize(domain)
66
+ domain = domain.ascii_only? ? domain : domain.chomp(DOT).unicode_normalize(:nfc)
67
+ Punycode.encode_hostname(domain).downcase
68
+ end
69
+ end
70
+
71
+ # Parses _hostname_ into a DomainName object. An IP address is also
72
+ # accepted. An IPv6 address may be enclosed in square brackets.
73
+ def initialize(hostname)
74
+ hostname = String(hostname)
75
+
76
+ raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(DOT)
77
+
78
+ begin
79
+ @ipaddr = IPAddr.new(hostname)
80
+ @hostname = @ipaddr.to_s
81
+ return
82
+ rescue IPAddr::Error
83
+ nil
84
+ end
85
+
86
+ @hostname = DomainName.normalize(hostname)
87
+ tld = if (last_dot = @hostname.rindex(DOT))
88
+ @hostname[(last_dot + 1)..-1]
89
+ else
90
+ @hostname
91
+ end
92
+
93
+ # unknown/local TLD
94
+ @domain = if last_dot
95
+ # fallback - accept cookies down to second level
96
+ # cf. http://www.dkim-reputation.org/regdom-libs/
97
+ if (penultimate_dot = @hostname.rindex(DOT, last_dot - 1))
98
+ @hostname[(penultimate_dot + 1)..-1]
99
+ else
100
+ @hostname
101
+ end
102
+ else
103
+ # no domain part - must be a local hostname
104
+ tld
105
+ end
106
+ end
107
+
108
+ # Checks if the server represented by this domain is qualified to
109
+ # send and receive cookies with a domain attribute value of
110
+ # _domain_. A true value given as the second argument represents
111
+ # cookies without a domain attribute value, in which case only
112
+ # hostname equality is checked.
113
+ def cookie_domain?(domain, host_only = false)
114
+ # RFC 6265 #5.3
115
+ # When the user agent "receives a cookie":
116
+ return self == @domain if host_only
117
+
118
+ domain = DomainName.new(domain)
119
+
120
+ # RFC 6265 #5.1.3
121
+ # Do not perform subdomain matching against IP addresses.
122
+ @hostname == domain.hostname if @ipaddr
123
+
124
+ # RFC 6265 #4.1.1
125
+ # Domain-value must be a subdomain.
126
+ @domain && self <= domain && domain <= @domain ? true : false
127
+ end
128
+
129
+ # def ==(other)
130
+ # other = DomainName.new(other)
131
+ # other.hostname == @hostname
132
+ # end
133
+
134
+ def <=>(other)
135
+ other = DomainName.new(other)
136
+ othername = other.hostname
137
+ if othername == @hostname
138
+ 0
139
+ elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT
140
+ # The other is higher
141
+ -1
142
+ elsif othername.end_with?(@hostname) && othername[-@hostname.size - 1, 1] == DOT
143
+ # The other is lower
144
+ 1
145
+ else
146
+ 1
147
+ end
148
+ end
149
+
150
+ # :nocov:
151
+ # rubocop:disable all
152
+ # -*- coding: utf-8 -*-
153
+ #--
154
+ # punycode.rb - PunyCode encoder for the Domain Name library
155
+ #
156
+ # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
157
+ #
158
+ # Ported from puny.c, a part of VeriSign XCode (encode/decode) IDN
159
+ # Library.
160
+ #
161
+ # Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
162
+ #
163
+ # Redistribution and use in source and binary forms, with or
164
+ # without modification, are permitted provided that the following
165
+ # conditions are met:
166
+ #
167
+ # 1) Redistributions of source code must retain the above copyright
168
+ # notice, this list of conditions and the following disclaimer.
169
+ #
170
+ # 2) Redistributions in binary form must reproduce the above copyright
171
+ # notice, this list of conditions and the following disclaimer in
172
+ # the documentation and/or other materials provided with the
173
+ # distribution.
174
+ #
175
+ # 3) Neither the name of the VeriSign Inc. nor the names of its
176
+ # contributors may be used to endorse or promote products derived
177
+ # from this software without specific prior written permission.
178
+ #
179
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
180
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
181
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
182
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
183
+ # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
184
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
185
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
186
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
187
+ # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
188
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
189
+ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
190
+ # POSSIBILITY OF SUCH DAMAGE.
191
+ #
192
+ # This software is licensed under the BSD open source license. For more
193
+ # information visit www.opensource.org.
194
+ #
195
+ # Authors:
196
+ # John Colosi (VeriSign)
197
+ # Srikanth Veeramachaneni (VeriSign)
198
+ # Nagesh Chigurupati (Verisign)
199
+ # Praveen Srinivasan(Verisign)
200
+ #++
201
+ module Punycode
202
+ BASE = 36
203
+ TMIN = 1
204
+ TMAX = 26
205
+ SKEW = 38
206
+ DAMP = 700
207
+ INITIAL_BIAS = 72
208
+ INITIAL_N = 0x80
209
+ DELIMITER = "-"
210
+
211
+ MAXINT = (1 << 32) - 1
212
+
213
+ LOBASE = BASE - TMIN
214
+ CUTOFF = LOBASE * TMAX / 2
215
+
216
+ RE_NONBASIC = /[^\x00-\x7f]/.freeze
217
+
218
+ # Returns the numeric value of a basic code point (for use in
219
+ # representing integers) in the range 0 to base-1, or nil if cp
220
+ # is does not represent a value.
221
+ DECODE_DIGIT = {}.tap do |map|
222
+ # ASCII A..Z map to 0..25
223
+ # ASCII a..z map to 0..25
224
+ (0..25).each { |i| map[65 + i] = map[97 + i] = i }
225
+ # ASCII 0..9 map to 26..35
226
+ (26..35).each { |i| map[22 + i] = i }
227
+ end
228
+
229
+ # Returns the basic code point whose value (when used for
230
+ # representing integers) is d, which must be in the range 0 to
231
+ # BASE-1. The lowercase form is used unless flag is true, in
232
+ # which case the uppercase form is used. The behavior is
233
+ # undefined if flag is nonzero and digit d has no uppercase
234
+ # form.
235
+ ENCODE_DIGIT = proc { |d, flag|
236
+ (d + 22 + (d < 26 ? 75 : 0) - (flag ? (1 << 5) : 0)).chr
237
+ # 0..25 map to ASCII a..z or A..Z
238
+ # 26..35 map to ASCII 0..9
239
+ }
240
+
241
+ DOT = "."
242
+ PREFIX = "xn--"
243
+
244
+ # Most errors we raise are basically kind of ArgumentError.
245
+ class ArgumentError < ::ArgumentError; end
246
+ class BufferOverflowError < ArgumentError; end
247
+
248
+ class << self
249
+ # Encode a +string+ in Punycode
250
+ def encode(string)
251
+ input = string.unpack("U*")
252
+ output = +""
253
+
254
+ # Initialize the state
255
+ n = INITIAL_N
256
+ delta = 0
257
+ bias = INITIAL_BIAS
258
+
259
+ # Handle the basic code points
260
+ input.each { |cp| output << cp.chr if cp < 0x80 }
261
+
262
+ h = b = output.length
263
+
264
+ # h is the number of code points that have been handled, b is the
265
+ # number of basic code points, and out is the number of characters
266
+ # that have been output.
267
+
268
+ output << DELIMITER if b > 0
269
+
270
+ # Main encoding loop
271
+
272
+ while h < input.length
273
+ # All non-basic code points < n have been handled already. Find
274
+ # the next larger one
275
+
276
+ m = MAXINT
277
+ input.each do |cp|
278
+ m = cp if (n...m) === cp
279
+ end
280
+
281
+ # Increase delta enough to advance the decoder's <n,i> state to
282
+ # <m,0>, but guard against overflow
283
+
284
+ delta += (m - n) * (h + 1)
285
+ raise BufferOverflowError if delta > MAXINT
286
+
287
+ n = m
288
+
289
+ input.each do |cp|
290
+ # AMC-ACE-Z can use this simplified version instead
291
+ if cp < n
292
+ delta += 1
293
+ raise BufferOverflowError if delta > MAXINT
294
+ elsif cp == n
295
+ # Represent delta as a generalized variable-length integer
296
+ q = delta
297
+ k = BASE
298
+ loop do
299
+ t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
300
+ break if q < t
301
+
302
+ q, r = (q - t).divmod(BASE - t)
303
+ output << ENCODE_DIGIT[t + r, false]
304
+ k += BASE
305
+ end
306
+
307
+ output << ENCODE_DIGIT[q, false]
308
+
309
+ # Adapt the bias
310
+ delta = h == b ? delta / DAMP : delta >> 1
311
+ delta += delta / (h + 1)
312
+ bias = 0
313
+ while delta > CUTOFF
314
+ delta /= LOBASE
315
+ bias += BASE
316
+ end
317
+ bias += (LOBASE + 1) * delta / (delta + SKEW)
318
+
319
+ delta = 0
320
+ h += 1
321
+ end
322
+ end
323
+
324
+ delta += 1
325
+ n += 1
326
+ end
327
+
328
+ output
329
+ end
330
+
331
+ # Encode a hostname using IDN/Punycode algorithms
332
+ def encode_hostname(hostname)
333
+ hostname.match(RE_NONBASIC) || (return hostname)
334
+
335
+ hostname.split(DOT).map do |name|
336
+ if name.match(RE_NONBASIC)
337
+ PREFIX + encode(name)
338
+ else
339
+ name
340
+ end
341
+ end.join(DOT)
342
+ end
343
+
344
+ # Decode a +string+ encoded in Punycode
345
+ def decode(string)
346
+ # Initialize the state
347
+ n = INITIAL_N
348
+ i = 0
349
+ bias = INITIAL_BIAS
350
+
351
+ if j = string.rindex(DELIMITER)
352
+ b = string[0...j]
353
+
354
+ b.match(RE_NONBASIC) &&
355
+ raise(ArgumentError, "Illegal character is found in basic part: #{string.inspect}")
356
+
357
+ # Handle the basic code points
358
+
359
+ output = b.unpack("U*")
360
+ u = string[(j + 1)..-1]
361
+ else
362
+ output = []
363
+ u = string
364
+ end
365
+
366
+ # Main decoding loop: Start just after the last delimiter if any
367
+ # basic code points were copied; start at the beginning
368
+ # otherwise.
369
+
370
+ input = u.unpack("C*")
371
+ input_length = input.length
372
+ h = 0
373
+ out = output.length
374
+
375
+ while h < input_length
376
+ # Decode a generalized variable-length integer into delta,
377
+ # which gets added to i. The overflow checking is easier
378
+ # if we increase i as we go, then subtract off its starting
379
+ # value at the end to obtain delta.
380
+
381
+ oldi = i
382
+ w = 1
383
+ k = BASE
384
+
385
+ loop do
386
+ (digit = DECODE_DIGIT[input[h]]) ||
387
+ raise(ArgumentError, "Illegal character is found in non-basic part: #{string.inspect}")
388
+ h += 1
389
+ i += digit * w
390
+ raise BufferOverflowError if i > MAXINT
391
+
392
+ t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
393
+ break if digit < t
394
+
395
+ w *= BASE - t
396
+ raise BufferOverflowError if w > MAXINT
397
+
398
+ k += BASE
399
+ (h < input_length) || raise(ArgumentError, "Malformed input given: #{string.inspect}")
400
+ end
401
+
402
+ # Adapt the bias
403
+ delta = oldi == 0 ? i / DAMP : (i - oldi) >> 1
404
+ delta += delta / (out + 1)
405
+ bias = 0
406
+ while delta > CUTOFF
407
+ delta /= LOBASE
408
+ bias += BASE
409
+ end
410
+ bias += (LOBASE + 1) * delta / (delta + SKEW)
411
+
412
+ # i was supposed to wrap around from out+1 to 0, incrementing
413
+ # n each time, so we'll fix that now:
414
+
415
+ q, i = i.divmod(out + 1)
416
+ n += q
417
+ raise BufferOverflowError if n > MAXINT
418
+
419
+ # Insert n at position i of the output:
420
+
421
+ output[i, 0] = n
422
+
423
+ out += 1
424
+ i += 1
425
+ end
426
+ output.pack("U*")
427
+ end
428
+
429
+ # Decode a hostname using IDN/Punycode algorithms
430
+ def decode_hostname(hostname)
431
+ hostname.gsub(/(\A|#{Regexp.quote(DOT)})#{Regexp.quote(PREFIX)}([^#{Regexp.quote(DOT)}]*)/o) do
432
+ Regexp.last_match(1) << decode(Regexp.last_match(2))
433
+ end
434
+ end
435
+ end
436
+ # rubocop:enable all
437
+ # :nocov:
438
+ end
439
+ end
440
+ end