dommy 0.7.0 → 0.8.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/lib/dommy/animation.rb +9 -1
- data/lib/dommy/attr.rb +192 -39
- data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
- data/lib/dommy/backend.rb +46 -0
- data/lib/dommy/blob.rb +28 -9
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/cookie_store.rb +3 -1
- data/lib/dommy/crypto.rb +7 -1
- data/lib/dommy/css.rb +46 -0
- data/lib/dommy/custom_elements.rb +27 -3
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +615 -48
- data/lib/dommy/dom_parser.rb +28 -15
- data/lib/dommy/element.rb +999 -471
- data/lib/dommy/event.rb +260 -96
- data/lib/dommy/event_source.rb +6 -2
- data/lib/dommy/fetch.rb +505 -43
- data/lib/dommy/file_reader.rb +11 -3
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +43 -8
- data/lib/dommy/html_collection.rb +55 -2
- data/lib/dommy/html_elements.rb +102 -1519
- data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +7 -1
- data/lib/dommy/message_channel.rb +32 -2
- data/lib/dommy/mutation_observer.rb +55 -12
- data/lib/dommy/navigator.rb +26 -12
- data/lib/dommy/node.rb +158 -28
- data/lib/dommy/notification.rb +3 -1
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +2 -0
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +2 -0
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +23 -54
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +18 -27
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +174 -21
- data/lib/dommy/tree_walker.rb +255 -66
- data/lib/dommy/url.rb +287 -449
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +37 -7
- data/lib/dommy/window.rb +202 -213
- data/lib/dommy/worker.rb +7 -7
- data/lib/dommy/xml_http_request.rb +15 -5
- data/lib/dommy.rb +7 -0
- metadata +12 -3
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
# A WHATWG URL Standard "basic URL parser" (https://url.spec.whatwg.org/).
|
|
6
|
+
#
|
|
7
|
+
# Replaces the previous Ruby `URI`-based resolution, which diverged from the
|
|
8
|
+
# spec on empty userinfo, relative resolution against opaque/special bases,
|
|
9
|
+
# port-range validation, leading-colon inputs, and percent-encoding. Produces
|
|
10
|
+
# a `Record` (the spec's "URL record"); `Dommy::URL` wraps it.
|
|
11
|
+
module UrlParser
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# scheme => default port (file has none).
|
|
15
|
+
SPECIAL = {"ftp" => 21, "file" => nil, "http" => 80, "https" => 443, "ws" => 80, "wss" => 443}.freeze
|
|
16
|
+
|
|
17
|
+
# Raised on a parse failure; `URL.new` maps it to DOMException::SyntaxError,
|
|
18
|
+
# `URL.parse` rescues it and returns nil.
|
|
19
|
+
class Failure < StandardError; end
|
|
20
|
+
|
|
21
|
+
# The spec URL record. `path` is an Array of segments for a hierarchical
|
|
22
|
+
# URL, or a String for an "opaque path" (cannot-be-a-base) URL. `host` is
|
|
23
|
+
# the already-serialized host string (IPv6 stored with brackets), or nil.
|
|
24
|
+
Record = Struct.new(:scheme, :username, :password, :host, :port, :path, :query, :fragment) do
|
|
25
|
+
def special? = SPECIAL.key?(scheme)
|
|
26
|
+
def opaque_path? = path.is_a?(String)
|
|
27
|
+
def includes_credentials? = !username.to_s.empty? || !password.to_s.empty?
|
|
28
|
+
def default_port = SPECIAL[scheme]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse(input, base_input = nil)
|
|
32
|
+
base = nil
|
|
33
|
+
if base_input && base_input != ""
|
|
34
|
+
base = base_input.is_a?(Record) ? base_input : run(base_input.to_s, nil)
|
|
35
|
+
end
|
|
36
|
+
run(input.to_s, base)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ===== percent-encode sets =====
|
|
40
|
+
|
|
41
|
+
def c0?(cp) = cp <= 0x1F || cp > 0x7E
|
|
42
|
+
def fragment_set?(cp) = c0?(cp) || [0x20, 0x22, 0x3C, 0x3E, 0x60].include?(cp)
|
|
43
|
+
def query_set?(cp) = c0?(cp) || [0x20, 0x22, 0x23, 0x3C, 0x3E].include?(cp)
|
|
44
|
+
def special_query_set?(cp) = query_set?(cp) || cp == 0x27
|
|
45
|
+
def path_set?(cp) = query_set?(cp) || [0x3F, 0x5E, 0x60, 0x7B, 0x7D].include?(cp)
|
|
46
|
+
def userinfo_set?(cp) = path_set?(cp) || [0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x7C].include?(cp)
|
|
47
|
+
|
|
48
|
+
# UTF-8 percent-encode a single code point against `set` (a predicate).
|
|
49
|
+
def pe(char, set)
|
|
50
|
+
return char unless set.call(char.ord)
|
|
51
|
+
|
|
52
|
+
char.b.bytes.map { |b| format("%%%02X", b) }.join
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def percent_decode(str)
|
|
56
|
+
out = +"".b
|
|
57
|
+
bytes = str.b
|
|
58
|
+
i = 0
|
|
59
|
+
while i < bytes.bytesize
|
|
60
|
+
b = bytes.getbyte(i)
|
|
61
|
+
if b == 0x25 && i + 2 < bytes.bytesize &&
|
|
62
|
+
bytes.byteslice(i + 1, 2) =~ /\A[0-9A-Fa-f]{2}\z/
|
|
63
|
+
out << bytes.byteslice(i + 1, 2).to_i(16)
|
|
64
|
+
i += 3
|
|
65
|
+
else
|
|
66
|
+
out << b
|
|
67
|
+
i += 1
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
out
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ===== host parsing =====
|
|
74
|
+
|
|
75
|
+
FORBIDDEN_HOST = [0x00, 0x09, 0x0A, 0x0D, 0x20, 0x23, 0x2F, 0x3A, 0x3C, 0x3E,
|
|
76
|
+
0x3F, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C].freeze
|
|
77
|
+
|
|
78
|
+
def forbidden_host?(cp) = FORBIDDEN_HOST.include?(cp)
|
|
79
|
+
def forbidden_domain?(cp) = forbidden_host?(cp) || cp <= 0x1F || cp == 0x25 || cp == 0x7F
|
|
80
|
+
|
|
81
|
+
def parse_host(input, special)
|
|
82
|
+
if input.start_with?("[")
|
|
83
|
+
raise Failure, "unclosed IPv6 address" unless input.end_with?("]")
|
|
84
|
+
|
|
85
|
+
return "[#{parse_ipv6(input[1...-1])}]"
|
|
86
|
+
end
|
|
87
|
+
return parse_opaque_host(input) unless special
|
|
88
|
+
raise Failure, "empty host" if input.empty?
|
|
89
|
+
|
|
90
|
+
# UTF-8-decode-without-BOM the percent-decoded bytes: malformed
|
|
91
|
+
# sequences become U+FFFD (which domain-to-ASCII then rejects),
|
|
92
|
+
# matching the spec rather than crashing on invalid encoding.
|
|
93
|
+
domain = percent_decode(input).force_encoding("UTF-8").scrub("�")
|
|
94
|
+
ascii =
|
|
95
|
+
begin
|
|
96
|
+
IDNA.to_ascii(domain, check_hyphens: false, verify_dns_length: false)
|
|
97
|
+
rescue IDNA::Error, Punycode::Error => e
|
|
98
|
+
raise Failure, "domain to ASCII: #{e.message}"
|
|
99
|
+
end
|
|
100
|
+
raise Failure, "empty domain" if ascii.empty?
|
|
101
|
+
raise Failure, "forbidden domain code point" if ascii.each_char.any? { |ch| forbidden_domain?(ch.ord) }
|
|
102
|
+
|
|
103
|
+
if ends_in_number?(ascii)
|
|
104
|
+
ip = Ipv4Parser.parse(ascii)
|
|
105
|
+
raise Failure, "invalid IPv4 address" if ip.nil?
|
|
106
|
+
|
|
107
|
+
return ip
|
|
108
|
+
end
|
|
109
|
+
ascii
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_opaque_host(input)
|
|
113
|
+
raise Failure, "forbidden host code point" if input.each_char.any? { |ch| forbidden_host?(ch.ord) }
|
|
114
|
+
|
|
115
|
+
input.each_char.map { |ch| pe(ch, method(:c0?)) }.join
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ends_in_number?(input)
|
|
119
|
+
parts = input.split(".", -1)
|
|
120
|
+
parts.pop if parts.length > 1 && parts.last == ""
|
|
121
|
+
return false if parts.empty?
|
|
122
|
+
|
|
123
|
+
last = parts.last
|
|
124
|
+
return false if last.empty?
|
|
125
|
+
return true if last.match?(/\A[0-9]+\z/)
|
|
126
|
+
|
|
127
|
+
last.match?(/\A0[xX][0-9A-Fa-f]*\z/)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# WHATWG IPv6 parser -> compressed serialized string (no brackets).
|
|
131
|
+
def parse_ipv6(input)
|
|
132
|
+
address = [0, 0, 0, 0, 0, 0, 0, 0]
|
|
133
|
+
piece_index = 0
|
|
134
|
+
compress = nil
|
|
135
|
+
chars = input.chars
|
|
136
|
+
ptr = 0
|
|
137
|
+
c = ->(i) { i < chars.length ? chars[i] : nil }
|
|
138
|
+
|
|
139
|
+
if c.call(ptr) == ":"
|
|
140
|
+
raise Failure, "IPv6 starts with single colon" unless c.call(ptr + 1) == ":"
|
|
141
|
+
|
|
142
|
+
ptr += 2
|
|
143
|
+
piece_index += 1
|
|
144
|
+
compress = piece_index
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
while c.call(ptr)
|
|
148
|
+
raise Failure, "too many IPv6 pieces" if piece_index == 8
|
|
149
|
+
|
|
150
|
+
if c.call(ptr) == ":"
|
|
151
|
+
raise Failure, "multiple IPv6 compressions" unless compress.nil?
|
|
152
|
+
|
|
153
|
+
ptr += 1
|
|
154
|
+
piece_index += 1
|
|
155
|
+
compress = piece_index
|
|
156
|
+
next
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
value = 0
|
|
160
|
+
length = 0
|
|
161
|
+
while length < 4 && c.call(ptr)&.match?(/[0-9A-Fa-f]/)
|
|
162
|
+
value = value * 16 + c.call(ptr).to_i(16)
|
|
163
|
+
ptr += 1
|
|
164
|
+
length += 1
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if c.call(ptr) == "."
|
|
168
|
+
raise Failure, "IPv4-in-IPv6 with no digits" if length.zero?
|
|
169
|
+
|
|
170
|
+
ptr -= length
|
|
171
|
+
raise Failure, "too few pieces for embedded IPv4" if piece_index > 6
|
|
172
|
+
|
|
173
|
+
numbers_seen = 0
|
|
174
|
+
while c.call(ptr)
|
|
175
|
+
ipv4_piece = nil
|
|
176
|
+
if numbers_seen.positive?
|
|
177
|
+
if c.call(ptr) == "." && numbers_seen < 4
|
|
178
|
+
ptr += 1
|
|
179
|
+
else
|
|
180
|
+
raise Failure, "invalid embedded IPv4"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
raise Failure, "invalid embedded IPv4 digit" unless c.call(ptr)&.match?(/[0-9]/)
|
|
184
|
+
|
|
185
|
+
while c.call(ptr)&.match?(/[0-9]/)
|
|
186
|
+
number = c.call(ptr).to_i
|
|
187
|
+
if ipv4_piece.nil?
|
|
188
|
+
ipv4_piece = number
|
|
189
|
+
elsif ipv4_piece.zero?
|
|
190
|
+
raise Failure, "leading zero in embedded IPv4"
|
|
191
|
+
else
|
|
192
|
+
ipv4_piece = ipv4_piece * 10 + number
|
|
193
|
+
end
|
|
194
|
+
raise Failure, "embedded IPv4 piece > 255" if ipv4_piece > 255
|
|
195
|
+
|
|
196
|
+
ptr += 1
|
|
197
|
+
end
|
|
198
|
+
address[piece_index] = address[piece_index] * 0x100 + ipv4_piece
|
|
199
|
+
numbers_seen += 1
|
|
200
|
+
piece_index += 1 if numbers_seen == 2 || numbers_seen == 4
|
|
201
|
+
end
|
|
202
|
+
raise Failure, "incomplete embedded IPv4" unless numbers_seen == 4
|
|
203
|
+
|
|
204
|
+
break
|
|
205
|
+
elsif c.call(ptr) == ":"
|
|
206
|
+
ptr += 1
|
|
207
|
+
raise Failure, "trailing colon in IPv6" if c.call(ptr).nil?
|
|
208
|
+
elsif c.call(ptr)
|
|
209
|
+
raise Failure, "invalid IPv6 code point"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
address[piece_index] = value
|
|
213
|
+
piece_index += 1
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if compress
|
|
217
|
+
swaps = piece_index - compress
|
|
218
|
+
piece_index = 7
|
|
219
|
+
while piece_index != 0 && swaps.positive?
|
|
220
|
+
address[piece_index], address[compress + swaps - 1] = address[compress + swaps - 1], address[piece_index]
|
|
221
|
+
piece_index -= 1
|
|
222
|
+
swaps -= 1
|
|
223
|
+
end
|
|
224
|
+
elsif piece_index != 8
|
|
225
|
+
raise Failure, "too few IPv6 pieces"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
serialize_ipv6(address)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def serialize_ipv6(pieces)
|
|
232
|
+
# Find the longest run (length > 1) of zero pieces to compress.
|
|
233
|
+
best_start = nil
|
|
234
|
+
best_len = 0
|
|
235
|
+
i = 0
|
|
236
|
+
while i < 8
|
|
237
|
+
if pieces[i].zero?
|
|
238
|
+
j = i
|
|
239
|
+
j += 1 while j < 8 && pieces[j].zero?
|
|
240
|
+
if (j - i) > best_len
|
|
241
|
+
best_len = j - i
|
|
242
|
+
best_start = i
|
|
243
|
+
end
|
|
244
|
+
i = j
|
|
245
|
+
else
|
|
246
|
+
i += 1
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
best_start = nil if best_len < 2
|
|
250
|
+
|
|
251
|
+
out = +""
|
|
252
|
+
i = 0
|
|
253
|
+
while i < 8
|
|
254
|
+
if best_start == i
|
|
255
|
+
out << (i.zero? ? "::" : ":")
|
|
256
|
+
i += best_len
|
|
257
|
+
next
|
|
258
|
+
end
|
|
259
|
+
out << pieces[i].to_s(16)
|
|
260
|
+
out << ":" if i < 7
|
|
261
|
+
i += 1
|
|
262
|
+
end
|
|
263
|
+
out
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# ===== serialization =====
|
|
267
|
+
|
|
268
|
+
def serialize_path(record)
|
|
269
|
+
return record.path if record.opaque_path?
|
|
270
|
+
|
|
271
|
+
record.path.map { |seg| "/#{seg}" }.join
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def serialize(record, exclude_fragment: false)
|
|
275
|
+
out = +"#{record.scheme}:"
|
|
276
|
+
if record.host
|
|
277
|
+
out << "//"
|
|
278
|
+
if record.includes_credentials?
|
|
279
|
+
out << record.username
|
|
280
|
+
out << ":#{record.password}" unless record.password.empty?
|
|
281
|
+
out << "@"
|
|
282
|
+
end
|
|
283
|
+
out << record.host
|
|
284
|
+
out << ":#{record.port}" if record.port
|
|
285
|
+
elsif !record.opaque_path? && record.path.is_a?(Array) &&
|
|
286
|
+
record.path.length > 1 && record.path[0] == ""
|
|
287
|
+
out << "/."
|
|
288
|
+
end
|
|
289
|
+
out << serialize_path(record)
|
|
290
|
+
out << "?#{record.query}" if record.query
|
|
291
|
+
out << "##{record.fragment}" if record.fragment && !exclude_fragment
|
|
292
|
+
out
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# ===== helpers for path normalization =====
|
|
296
|
+
|
|
297
|
+
def windows_drive_letter?(seg) = seg.length == 2 && seg[0].match?(/[A-Za-z]/) && [":", "|"].include?(seg[1])
|
|
298
|
+
def normalized_windows_drive_letter?(seg) = seg.length == 2 && seg[0].match?(/[A-Za-z]/) && seg[1] == ":"
|
|
299
|
+
def starts_with_windows_drive_letter?(s)
|
|
300
|
+
s.length >= 2 && s[0].match?(/[A-Za-z]/) && [":", "|"].include?(s[1]) &&
|
|
301
|
+
(s.length == 2 || ["/", "\\", "?", "#"].include?(s[2]))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def single_dot?(seg) = [".", "%2e"].include?(seg.downcase)
|
|
305
|
+
|
|
306
|
+
def double_dot?(seg)
|
|
307
|
+
["..", ".%2e", "%2e.", "%2e%2e"].include?(seg.downcase)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def shorten_path(record)
|
|
311
|
+
path = record.path
|
|
312
|
+
return if path.empty?
|
|
313
|
+
return if record.scheme == "file" && path.length == 1 && normalized_windows_drive_letter?(path[0])
|
|
314
|
+
|
|
315
|
+
path.pop
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# ===== the basic URL parser state machine =====
|
|
319
|
+
|
|
320
|
+
def run(input, base)
|
|
321
|
+
input = input.dup
|
|
322
|
+
# Strip leading/trailing C0 controls and spaces, then remove all
|
|
323
|
+
# ASCII tab/newline.
|
|
324
|
+
input = input.sub(/\A[\x00-\x20]+/, "").sub(/[\x00-\x20]+\z/, "")
|
|
325
|
+
input = input.gsub(/[\t\n\r]/, "")
|
|
326
|
+
|
|
327
|
+
chars = input.chars
|
|
328
|
+
len = chars.length
|
|
329
|
+
state = :scheme_start
|
|
330
|
+
url = Record.new("", "", "", nil, nil, [], nil, nil)
|
|
331
|
+
buffer = +""
|
|
332
|
+
at_sign_seen = false
|
|
333
|
+
inside_brackets = false
|
|
334
|
+
password_token_seen = false
|
|
335
|
+
ptr = 0
|
|
336
|
+
|
|
337
|
+
cp = lambda { ptr < len ? chars[ptr] : nil }
|
|
338
|
+
|
|
339
|
+
loop do
|
|
340
|
+
c = cp.call
|
|
341
|
+
|
|
342
|
+
case state
|
|
343
|
+
when :scheme_start
|
|
344
|
+
if c&.match?(/[A-Za-z]/)
|
|
345
|
+
buffer << c.downcase
|
|
346
|
+
state = :scheme
|
|
347
|
+
else
|
|
348
|
+
state = :no_scheme
|
|
349
|
+
next # reprocess (do not advance)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
when :scheme
|
|
353
|
+
if c&.match?(/[A-Za-z0-9+\-.]/)
|
|
354
|
+
buffer << c.downcase
|
|
355
|
+
elsif c == ":"
|
|
356
|
+
url.scheme = buffer
|
|
357
|
+
buffer = +""
|
|
358
|
+
if url.scheme == "file"
|
|
359
|
+
state = :file
|
|
360
|
+
elsif url.special? && base && base.scheme == url.scheme
|
|
361
|
+
state = :special_relative_or_authority
|
|
362
|
+
elsif url.special?
|
|
363
|
+
state = :special_authority_slashes
|
|
364
|
+
elsif input[(ptr + 1)..].to_s.start_with?("/")
|
|
365
|
+
state = :path_or_authority
|
|
366
|
+
ptr += 1
|
|
367
|
+
else
|
|
368
|
+
url.path = ""
|
|
369
|
+
state = :opaque_path
|
|
370
|
+
end
|
|
371
|
+
else
|
|
372
|
+
buffer = +""
|
|
373
|
+
state = :no_scheme
|
|
374
|
+
ptr = -1 # restart from 0 (advance makes it 0)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
when :no_scheme
|
|
378
|
+
raise Failure, "missing scheme" if base.nil? || (base.opaque_path? && c != "#")
|
|
379
|
+
|
|
380
|
+
if base.opaque_path? && c == "#"
|
|
381
|
+
url.scheme = base.scheme
|
|
382
|
+
url.path = base.path
|
|
383
|
+
url.query = base.query
|
|
384
|
+
url.fragment = +""
|
|
385
|
+
state = :fragment
|
|
386
|
+
elsif base.scheme != "file"
|
|
387
|
+
state = :relative
|
|
388
|
+
next
|
|
389
|
+
else
|
|
390
|
+
state = :file
|
|
391
|
+
next
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
when :special_relative_or_authority
|
|
395
|
+
if c == "/" && input[(ptr + 1)..].to_s.start_with?("/")
|
|
396
|
+
state = :special_authority_ignore_slashes
|
|
397
|
+
ptr += 1
|
|
398
|
+
else
|
|
399
|
+
state = :relative
|
|
400
|
+
next
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
when :path_or_authority
|
|
404
|
+
if c == "/"
|
|
405
|
+
state = :authority
|
|
406
|
+
else
|
|
407
|
+
state = :path
|
|
408
|
+
next
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
when :relative
|
|
412
|
+
url.scheme = base.scheme
|
|
413
|
+
if c == "/"
|
|
414
|
+
state = :relative_slash
|
|
415
|
+
elsif url.special? && c == "\\"
|
|
416
|
+
state = :relative_slash
|
|
417
|
+
else
|
|
418
|
+
url.username = base.username
|
|
419
|
+
url.password = base.password
|
|
420
|
+
url.host = base.host
|
|
421
|
+
url.port = base.port
|
|
422
|
+
url.path = base.path.dup
|
|
423
|
+
url.query = base.query
|
|
424
|
+
if c == "?"
|
|
425
|
+
url.query = +""
|
|
426
|
+
state = :query
|
|
427
|
+
elsif c == "#"
|
|
428
|
+
url.fragment = +""
|
|
429
|
+
state = :fragment
|
|
430
|
+
elsif c
|
|
431
|
+
url.query = nil
|
|
432
|
+
shorten_path(url)
|
|
433
|
+
state = :path
|
|
434
|
+
next
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
when :relative_slash
|
|
439
|
+
if url.special? && (c == "/" || c == "\\")
|
|
440
|
+
state = :special_authority_ignore_slashes
|
|
441
|
+
elsif c == "/"
|
|
442
|
+
state = :authority
|
|
443
|
+
else
|
|
444
|
+
url.username = base.username
|
|
445
|
+
url.password = base.password
|
|
446
|
+
url.host = base.host
|
|
447
|
+
url.port = base.port
|
|
448
|
+
state = :path
|
|
449
|
+
next
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
when :special_authority_slashes
|
|
453
|
+
if c == "/" && input[(ptr + 1)..].to_s.start_with?("/")
|
|
454
|
+
state = :special_authority_ignore_slashes
|
|
455
|
+
ptr += 1
|
|
456
|
+
else
|
|
457
|
+
state = :special_authority_ignore_slashes
|
|
458
|
+
next
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
when :special_authority_ignore_slashes
|
|
462
|
+
if c != "/" && c != "\\"
|
|
463
|
+
state = :authority
|
|
464
|
+
next
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
when :authority
|
|
468
|
+
if c == "@"
|
|
469
|
+
buffer = "%40#{buffer}" if at_sign_seen
|
|
470
|
+
at_sign_seen = true
|
|
471
|
+
buffer.each_char do |ch|
|
|
472
|
+
if ch == ":" && !password_token_seen
|
|
473
|
+
password_token_seen = true
|
|
474
|
+
next
|
|
475
|
+
end
|
|
476
|
+
encoded = pe(ch, method(:userinfo_set?))
|
|
477
|
+
if password_token_seen
|
|
478
|
+
url.password += encoded
|
|
479
|
+
else
|
|
480
|
+
url.username += encoded
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
buffer = +""
|
|
484
|
+
elsif c.nil? || ["/", "?", "#"].include?(c) || (url.special? && c == "\\")
|
|
485
|
+
raise Failure, "empty host with credentials" if at_sign_seen && buffer.empty?
|
|
486
|
+
|
|
487
|
+
ptr -= (buffer.length + 1)
|
|
488
|
+
buffer = +""
|
|
489
|
+
state = :host
|
|
490
|
+
else
|
|
491
|
+
buffer << c
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
when :host, :hostname
|
|
495
|
+
if c == ":" && !inside_brackets
|
|
496
|
+
raise Failure, "empty host" if buffer.empty?
|
|
497
|
+
|
|
498
|
+
url.host = parse_host(buffer, url.special?)
|
|
499
|
+
buffer = +""
|
|
500
|
+
state = :port
|
|
501
|
+
elsif c.nil? || ["/", "?", "#"].include?(c) || (url.special? && c == "\\")
|
|
502
|
+
ptr -= 1
|
|
503
|
+
raise Failure, "empty special host" if url.special? && buffer.empty?
|
|
504
|
+
|
|
505
|
+
url.host = parse_host(buffer, url.special?)
|
|
506
|
+
buffer = +""
|
|
507
|
+
state = :path_start
|
|
508
|
+
else
|
|
509
|
+
inside_brackets = true if c == "["
|
|
510
|
+
inside_brackets = false if c == "]"
|
|
511
|
+
buffer << c
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
when :port
|
|
515
|
+
if c&.match?(/[0-9]/)
|
|
516
|
+
buffer << c
|
|
517
|
+
elsif c.nil? || ["/", "?", "#"].include?(c) || (url.special? && c == "\\")
|
|
518
|
+
unless buffer.empty?
|
|
519
|
+
port = buffer.to_i
|
|
520
|
+
raise Failure, "port out of range" if port > 65_535
|
|
521
|
+
|
|
522
|
+
url.port = (port == url.default_port ? nil : port)
|
|
523
|
+
buffer = +""
|
|
524
|
+
end
|
|
525
|
+
state = :path_start
|
|
526
|
+
next
|
|
527
|
+
else
|
|
528
|
+
raise Failure, "invalid port"
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
when :file
|
|
532
|
+
url.scheme = "file"
|
|
533
|
+
url.host = ""
|
|
534
|
+
if c == "/" || c == "\\"
|
|
535
|
+
state = :file_slash
|
|
536
|
+
elsif base && base.scheme == "file"
|
|
537
|
+
url.host = base.host
|
|
538
|
+
url.path = base.path.dup
|
|
539
|
+
url.query = base.query
|
|
540
|
+
if c == "?"
|
|
541
|
+
url.query = +""
|
|
542
|
+
state = :query
|
|
543
|
+
elsif c == "#"
|
|
544
|
+
url.fragment = +""
|
|
545
|
+
state = :fragment
|
|
546
|
+
elsif c
|
|
547
|
+
url.query = nil
|
|
548
|
+
shorten_path(url) unless starts_with_windows_drive_letter?(input[ptr..].to_s)
|
|
549
|
+
url.path = [] if starts_with_windows_drive_letter?(input[ptr..].to_s)
|
|
550
|
+
state = :path
|
|
551
|
+
next
|
|
552
|
+
end
|
|
553
|
+
else
|
|
554
|
+
state = :path
|
|
555
|
+
next
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
when :file_slash
|
|
559
|
+
if c == "/" || c == "\\"
|
|
560
|
+
state = :file_host
|
|
561
|
+
else
|
|
562
|
+
if base && base.scheme == "file"
|
|
563
|
+
url.host = base.host
|
|
564
|
+
if !starts_with_windows_drive_letter?(input[ptr..].to_s) &&
|
|
565
|
+
base.path[0] && normalized_windows_drive_letter?(base.path[0])
|
|
566
|
+
url.path << base.path[0]
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
state = :path
|
|
570
|
+
next
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
when :file_host
|
|
574
|
+
if c.nil? || ["/", "\\", "?", "#"].include?(c)
|
|
575
|
+
ptr -= 1
|
|
576
|
+
if buffer.match?(/\A[A-Za-z][:|]\z/)
|
|
577
|
+
state = :path
|
|
578
|
+
elsif buffer.empty?
|
|
579
|
+
url.host = ""
|
|
580
|
+
state = :path_start
|
|
581
|
+
else
|
|
582
|
+
host = parse_host(buffer, true)
|
|
583
|
+
host = "" if host == "localhost"
|
|
584
|
+
url.host = host
|
|
585
|
+
buffer = +""
|
|
586
|
+
state = :path_start
|
|
587
|
+
end
|
|
588
|
+
else
|
|
589
|
+
buffer << c
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
when :path_start
|
|
593
|
+
if url.special?
|
|
594
|
+
state = :path
|
|
595
|
+
next unless c == "/" || c == "\\"
|
|
596
|
+
elsif c == "?"
|
|
597
|
+
url.query = +""
|
|
598
|
+
state = :query
|
|
599
|
+
elsif c == "#"
|
|
600
|
+
url.fragment = +""
|
|
601
|
+
state = :fragment
|
|
602
|
+
elsif c
|
|
603
|
+
state = :path
|
|
604
|
+
next unless c == "/"
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
when :path
|
|
608
|
+
if c.nil? || c == "/" || (url.special? && c == "\\") ||
|
|
609
|
+
c == "?" || c == "#"
|
|
610
|
+
if double_dot?(buffer)
|
|
611
|
+
shorten_path(url)
|
|
612
|
+
url.path << "" unless c == "/" || (url.special? && c == "\\")
|
|
613
|
+
elsif single_dot?(buffer)
|
|
614
|
+
url.path << "" unless c == "/" || (url.special? && c == "\\")
|
|
615
|
+
else
|
|
616
|
+
if url.scheme == "file" && url.path.empty? && windows_drive_letter?(buffer)
|
|
617
|
+
buffer[1] = ":"
|
|
618
|
+
end
|
|
619
|
+
url.path << buffer
|
|
620
|
+
end
|
|
621
|
+
buffer = +""
|
|
622
|
+
if c == "?"
|
|
623
|
+
url.query = +""
|
|
624
|
+
state = :query
|
|
625
|
+
elsif c == "#"
|
|
626
|
+
url.fragment = +""
|
|
627
|
+
state = :fragment
|
|
628
|
+
end
|
|
629
|
+
else
|
|
630
|
+
buffer << pe(c, method(:path_set?))
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
when :opaque_path
|
|
634
|
+
if c == "?"
|
|
635
|
+
url.query = +""
|
|
636
|
+
state = :query
|
|
637
|
+
elsif c == "#"
|
|
638
|
+
url.fragment = +""
|
|
639
|
+
state = :fragment
|
|
640
|
+
elsif c == " "
|
|
641
|
+
# A space is only percent-encoded when it abuts the end of
|
|
642
|
+
# the opaque path (a following `?`/`#`); an interior space
|
|
643
|
+
# stays literal. (Trailing-at-EOF spaces are already gone
|
|
644
|
+
# via the leading/trailing strip.)
|
|
645
|
+
nxt = chars[ptr + 1]
|
|
646
|
+
url.path += (nxt == "?" || nxt == "#") ? "%20" : " "
|
|
647
|
+
elsif c
|
|
648
|
+
url.path += pe(c, method(:c0?))
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
when :query
|
|
652
|
+
if c.nil? || c == "#"
|
|
653
|
+
set = url.special? ? method(:special_query_set?) : method(:query_set?)
|
|
654
|
+
url.query += buffer.each_char.map { |ch| pe(ch, set) }.join
|
|
655
|
+
buffer = +""
|
|
656
|
+
if c == "#"
|
|
657
|
+
url.fragment = +""
|
|
658
|
+
state = :fragment
|
|
659
|
+
end
|
|
660
|
+
else
|
|
661
|
+
buffer << c
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
when :fragment
|
|
665
|
+
url.fragment += pe(c, method(:fragment_set?)) if c
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
break if ptr >= len
|
|
669
|
+
|
|
670
|
+
ptr += 1
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
url
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
end
|