dommy 0.13.0 → 0.14.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/backend/makiri_adapter.rb +63 -29
- data/lib/dommy/backend.rb +32 -9
- data/lib/dommy/bridge/constructor.rb +72 -0
- data/lib/dommy/bridge/errors.rb +42 -0
- data/lib/dommy/bridge/methods.rb +21 -14
- data/lib/dommy/bridge/sentinels.rb +39 -0
- data/lib/dommy/bridge/values.rb +78 -0
- data/lib/dommy/{js → bridge}/wire_tags.rb +14 -11
- data/lib/dommy/bridge.rb +22 -250
- data/lib/dommy/browser.rb +112 -11
- data/lib/dommy/character_data.rb +539 -0
- data/lib/dommy/class_list.rb +311 -0
- data/lib/dommy/crypto.rb +12 -1
- data/lib/dommy/css.rb +12 -0
- data/lib/dommy/data_transfer.rb +106 -0
- data/lib/dommy/document.rb +307 -369
- data/lib/dommy/dom_parser.rb +3 -0
- data/lib/dommy/element.rb +199 -1817
- data/lib/dommy/encodings/tables.rb +4 -4
- data/lib/dommy/encodings.rb +58 -27
- data/lib/dommy/event.rb +227 -1
- data/lib/dommy/event_source.rb +41 -0
- data/lib/dommy/form_data.rb +6 -64
- data/lib/dommy/form_entry_list.rb +227 -0
- data/lib/dommy/fragment.rb +320 -0
- data/lib/dommy/history.rb +3 -0
- data/lib/dommy/html_collection.rb +18 -8
- data/lib/dommy/html_elements/base.rb +338 -0
- data/lib/dommy/html_elements/embedded.rb +237 -0
- data/lib/dommy/html_elements/forms/form.rb +1088 -0
- data/lib/dommy/html_elements/forms/input.rb +642 -0
- data/lib/dommy/html_elements/forms/select.rb +536 -0
- data/lib/dommy/html_elements/forms.rb +8 -0
- data/lib/dommy/html_elements/interactive.rb +382 -0
- data/lib/dommy/html_elements/media.rb +329 -0
- data/lib/dommy/html_elements/metadata.rb +353 -0
- data/lib/dommy/html_elements/miscellaneous.rb +77 -0
- data/lib/dommy/html_elements/tables.rb +468 -0
- data/lib/dommy/html_elements.rb +11 -5322
- data/lib/dommy/hyperlink_utils.rb +171 -0
- data/lib/dommy/interaction/debug.rb +7 -6
- data/lib/dommy/interaction/dom_summary.rb +65 -40
- data/lib/dommy/interaction/driver.rb +29 -115
- data/lib/dommy/interaction/event_synthesis.rb +97 -4
- data/lib/dommy/interaction/field_interactor.rb +1 -13
- data/lib/dommy/interaction/form_submission.rb +89 -167
- data/lib/dommy/interaction/key_sender.rb +121 -0
- data/lib/dommy/interaction/mime_types.rb +25 -0
- data/lib/dommy/interaction.rb +2 -0
- data/lib/dommy/internal/accessibility_tree.rb +32 -9
- data/lib/dommy/internal/accessibility_visibility.rb +44 -0
- data/lib/dommy/internal/accessible_name.rb +85 -65
- data/lib/dommy/internal/aria_role.rb +21 -6
- data/lib/dommy/internal/aria_snapshot.rb +5 -0
- data/lib/dommy/internal/aria_state.rb +24 -5
- data/lib/dommy/internal/autofill.rb +141 -0
- data/lib/dommy/internal/backend_prefilter.rb +170 -0
- data/lib/dommy/internal/bounded_cache.rb +50 -0
- data/lib/dommy/internal/child_node.rb +9 -34
- data/lib/dommy/internal/css/calc.rb +143 -145
- data/lib/dommy/internal/css/cascade.rb +38 -393
- data/lib/dommy/internal/css/cascaded_declarations.rb +245 -0
- data/lib/dommy/internal/css/color.rb +61 -34
- data/lib/dommy/internal/css/computed_style_builder.rb +205 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
- data/lib/dommy/internal/css/counters.rb +60 -37
- data/lib/dommy/internal/css/custom_properties.rb +87 -39
- data/lib/dommy/internal/css/media_query.rb +228 -203
- data/lib/dommy/internal/css/parser.rb +29 -12
- data/lib/dommy/internal/css/property_registry.rb +32 -56
- data/lib/dommy/internal/css/renderability.rb +61 -0
- data/lib/dommy/internal/css/rule_index.rb +17 -140
- data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
- data/lib/dommy/internal/css/style_cache.rb +61 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +40 -1
- data/lib/dommy/internal/css/value_tokens.rb +60 -0
- data/lib/dommy/internal/directionality.rb +188 -0
- data/lib/dommy/internal/document_generations.rb +169 -0
- data/lib/dommy/internal/document_interaction_state.rb +47 -0
- data/lib/dommy/internal/document_live_ranges.rb +97 -0
- data/lib/dommy/internal/dom_matching.rb +28 -12
- data/lib/dommy/internal/element_aria.rb +163 -0
- data/lib/dommy/internal/element_geometry.rb +69 -0
- data/lib/dommy/internal/element_matching.rb +5 -12
- data/lib/dommy/internal/element_shadow.rb +117 -0
- data/lib/dommy/internal/element_state.rb +232 -0
- data/lib/dommy/internal/element_tasks.rb +19 -0
- data/lib/dommy/internal/element_top_layer.rb +63 -0
- data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
- data/lib/dommy/internal/exception_report.rb +1 -1
- data/lib/dommy/internal/input_type.rb +280 -0
- data/lib/dommy/internal/insertion_point.rb +62 -0
- data/lib/dommy/internal/mime_type.rb +74 -0
- data/lib/dommy/internal/mutation_coordinator.rb +57 -198
- data/lib/dommy/internal/namespaces.rb +41 -43
- data/lib/dommy/internal/node_adopter.rb +84 -77
- data/lib/dommy/internal/node_factory.rb +157 -0
- data/lib/dommy/internal/node_identity.rb +31 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +61 -207
- data/lib/dommy/internal/observer_options.rb +93 -0
- data/lib/dommy/internal/parent_node.rb +27 -27
- data/lib/dommy/internal/post_insertion_steps.rb +213 -0
- data/lib/dommy/internal/reflected_attributes.rb +425 -13
- data/lib/dommy/internal/rendered_text/collector.rb +221 -0
- data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
- data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
- data/lib/dommy/internal/rendered_text/items.rb +57 -0
- data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
- data/lib/dommy/internal/selector_arguments.rb +94 -0
- data/lib/dommy/internal/selector_index.rb +3 -1
- data/lib/dommy/internal/selector_matcher.rb +23 -383
- data/lib/dommy/internal/selector_parser.rb +2 -37
- data/lib/dommy/internal/template_content_registry.rb +112 -66
- data/lib/dommy/internal/text_flattening.rb +20 -0
- data/lib/dommy/internal/text_selection.rb +104 -0
- data/lib/dommy/internal/toggle_events.rb +56 -0
- data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
- data/lib/dommy/internal/url_parser.rb +722 -569
- data/lib/dommy/internal/url_record_accessors.rb +90 -0
- data/lib/dommy/internal/window_constructors.rb +190 -0
- data/lib/dommy/internal/xml_serialization.rb +168 -80
- data/lib/dommy/js/crossing_profile.rb +75 -0
- data/lib/dommy/js/dom_interfaces.rb +22 -2
- data/lib/dommy/js/host_bridge.rb +43 -187
- data/lib/dommy/js/host_callbacks.rb +96 -0
- data/lib/dommy/js/host_runtime.js +840 -763
- data/lib/dommy/js/import_map.rb +10 -1
- data/lib/dommy/js/marshaller.rb +94 -65
- data/lib/dommy/js/observable_runtime.js +125 -146
- data/lib/dommy/js/runtime.rb +10 -43
- data/lib/dommy/js/runtime_registry.rb +46 -0
- data/lib/dommy/js/script_boot.rb +38 -73
- data/lib/dommy/js/webidl_tables.js +619 -0
- data/lib/dommy/js/window_wiring.rb +69 -0
- data/lib/dommy/location.rb +152 -103
- data/lib/dommy/mutation_observer.rb +3 -57
- data/lib/dommy/navigation.rb +65 -12
- data/lib/dommy/node.rb +46 -20
- data/lib/dommy/promise.rb +58 -4
- data/lib/dommy/resources.rb +19 -1
- data/lib/dommy/shadow_root.rb +11 -5
- data/lib/dommy/style_declaration.rb +284 -0
- data/lib/dommy/svg_elements.rb +1 -45
- data/lib/dommy/url.rb +20 -69
- data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
- data/lib/dommy/url_pattern/tokenizer.rb +4 -2
- data/lib/dommy/validity_state.rb +383 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +30 -166
- data/lib/dommy/xml_http_request.rb +35 -4
- data/lib/dommy.rb +23 -3
- metadata +74 -7
- data/lib/dommy/bridge/constructor_registry.rb +0 -28
- data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
|
@@ -28,12 +28,17 @@ module Dommy
|
|
|
28
28
|
def default_port = SPECIAL[scheme]
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# `encoding` is the Encoding Standard name HTML's "encoding-parse a URL"
|
|
32
|
+
# hands the parser (the document's character encoding); nil means UTF-8,
|
|
33
|
+
# which is what the URL API and every other caller uses. It only changes
|
|
34
|
+
# the query, and only for a special URL that is not ws/wss (see
|
|
35
|
+
# BasicParser#query_encoding).
|
|
36
|
+
def parse(input, base_input = nil, encoding: nil)
|
|
32
37
|
base = nil
|
|
33
38
|
if base_input && base_input != ""
|
|
34
39
|
base = base_input.is_a?(Record) ? base_input : run(base_input.to_s, nil)
|
|
35
40
|
end
|
|
36
|
-
run(input.to_s, base)
|
|
41
|
+
run(input.to_s, base, encoding: encoding)
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
# The basic URL parser with a state override: `input` is read from
|
|
@@ -45,684 +50,832 @@ module Dommy
|
|
|
45
50
|
run(input.to_s, nil, url: url, state_override: state)
|
|
46
51
|
end
|
|
47
52
|
|
|
48
|
-
#
|
|
53
|
+
# The URL Standard vocabulary the parser is written in: the percent-encode
|
|
54
|
+
# sets, the host and IPv6 parsers, the serializers, and the path helpers.
|
|
55
|
+
# Every one is a function of its arguments alone.
|
|
56
|
+
#
|
|
57
|
+
# It is a separate module because it has two users with nothing else in
|
|
58
|
+
# common: UrlParser extends it, so callers outside this file keep writing
|
|
59
|
+
# `UrlParser.serialize(record)`, and BasicParser includes it, because a
|
|
60
|
+
# state machine is written in this vocabulary. Including UrlParser itself
|
|
61
|
+
# would also hand BasicParser `parse` / `run` — the entry points that build
|
|
62
|
+
# a BasicParser — which is not a vocabulary a parser state should reach for.
|
|
63
|
+
module Syntax
|
|
64
|
+
# ===== percent-encode sets =====
|
|
65
|
+
|
|
66
|
+
def c0?(cp) = cp <= 0x1F || cp > 0x7E
|
|
67
|
+
def fragment_set?(cp) = c0?(cp) || [0x20, 0x22, 0x3C, 0x3E, 0x60].include?(cp)
|
|
68
|
+
def query_set?(cp) = c0?(cp) || [0x20, 0x22, 0x23, 0x3C, 0x3E].include?(cp)
|
|
69
|
+
def special_query_set?(cp) = query_set?(cp) || cp == 0x27
|
|
70
|
+
def path_set?(cp) = query_set?(cp) || [0x3F, 0x5E, 0x60, 0x7B, 0x7D].include?(cp)
|
|
71
|
+
def userinfo_set?(cp) = path_set?(cp) || [0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x7C].include?(cp)
|
|
72
|
+
|
|
73
|
+
# UTF-8 percent-encode a single code point against `set` (a predicate).
|
|
74
|
+
def pe(char, set)
|
|
75
|
+
return char unless set.call(char.ord)
|
|
76
|
+
|
|
77
|
+
char.b.bytes.map { |b| format("%%%02X", b) }.join
|
|
78
|
+
end
|
|
49
79
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
80
|
+
# Spec: "percent-encode after encoding" (URL §1.3), the query state's
|
|
81
|
+
# flush. The whole buffer is encoded at once — ISO-2022-JP is stateful,
|
|
82
|
+
# so encoding code point by code point would emit a fresh escape per
|
|
83
|
+
# character — then each byte is percent-encoded if it falls in `set`. A
|
|
84
|
+
# code point the encoding cannot represent becomes the spec's numeric
|
|
85
|
+
# character reference escape ("%26%23" + decimal + "%3B").
|
|
86
|
+
def percent_encode_after_encoding(encoding_name, input, set)
|
|
87
|
+
out = +"".b
|
|
88
|
+
encode_bytes(encoding_name, input).each do |byte|
|
|
89
|
+
out << (set.call(byte) ? format("%%%02X", byte) : byte.chr)
|
|
90
|
+
end
|
|
91
|
+
out.force_encoding(::Encoding::UTF_8)
|
|
92
|
+
end
|
|
56
93
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
# The bytes `input` encodes to under `encoding_name` (an Encoding
|
|
95
|
+
# Standard name), or its UTF-8 bytes when the name is nil or Ruby has no
|
|
96
|
+
# matching encoding. Only a handful of the standard's names lack one
|
|
97
|
+
# (x-user-defined, replacement), and those are never a document's.
|
|
98
|
+
def encode_bytes(encoding_name, input)
|
|
99
|
+
ruby = begin
|
|
100
|
+
::Encoding.find(encoding_name.to_s)
|
|
101
|
+
rescue ArgumentError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
return input.b.bytes if ruby.nil? || ruby == ::Encoding::UTF_8
|
|
60
105
|
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
begin
|
|
107
|
+
input.encode(ruby).b.bytes
|
|
108
|
+
rescue ::Encoding::UndefinedConversionError, ::Encoding::InvalidByteSequenceError
|
|
109
|
+
per_code_point_bytes(ruby, input)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
63
112
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
out <<
|
|
73
|
-
i += 3
|
|
74
|
-
else
|
|
75
|
-
out << b
|
|
76
|
-
i += 1
|
|
113
|
+
# A code point the encoding cannot represent yields the numeric-reference
|
|
114
|
+
# escape, already percent-encoded so the caller's per-byte pass leaves it
|
|
115
|
+
# alone.
|
|
116
|
+
def per_code_point_bytes(ruby, input)
|
|
117
|
+
out = +"".b
|
|
118
|
+
input.each_char do |char|
|
|
119
|
+
out << char.encode(ruby)
|
|
120
|
+
rescue ::Encoding::UndefinedConversionError, ::Encoding::InvalidByteSequenceError
|
|
121
|
+
out << "%26%23#{char.ord}%3B"
|
|
77
122
|
end
|
|
123
|
+
out.bytes
|
|
78
124
|
end
|
|
79
|
-
out
|
|
80
|
-
end
|
|
81
125
|
|
|
82
|
-
|
|
126
|
+
def percent_decode(str)
|
|
127
|
+
out = +"".b
|
|
128
|
+
bytes = str.b
|
|
129
|
+
i = 0
|
|
130
|
+
while i < bytes.bytesize
|
|
131
|
+
b = bytes.getbyte(i)
|
|
132
|
+
if b == 0x25 && i + 2 < bytes.bytesize &&
|
|
133
|
+
bytes.byteslice(i + 1, 2) =~ /\A[0-9A-Fa-f]{2}\z/
|
|
134
|
+
out << bytes.byteslice(i + 1, 2).to_i(16)
|
|
135
|
+
i += 3
|
|
136
|
+
else
|
|
137
|
+
out << b
|
|
138
|
+
i += 1
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
out
|
|
142
|
+
end
|
|
83
143
|
|
|
84
|
-
|
|
85
|
-
0x3F, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C].freeze
|
|
144
|
+
# ===== host parsing =====
|
|
86
145
|
|
|
87
|
-
|
|
88
|
-
|
|
146
|
+
FORBIDDEN_HOST = [0x00, 0x09, 0x0A, 0x0D, 0x20, 0x23, 0x2F, 0x3A, 0x3C, 0x3E,
|
|
147
|
+
0x3F, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C].freeze
|
|
89
148
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
raise Failure, "unclosed IPv6 address" unless input.end_with?("]")
|
|
149
|
+
def forbidden_host?(cp) = FORBIDDEN_HOST.include?(cp)
|
|
150
|
+
def forbidden_domain?(cp) = forbidden_host?(cp) || cp <= 0x1F || cp == 0x25 || cp == 0x7F
|
|
93
151
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
raise Failure, "empty host" if input.empty?
|
|
152
|
+
def parse_host(input, special)
|
|
153
|
+
if input.start_with?("[")
|
|
154
|
+
raise Failure, "unclosed IPv6 address" unless input.end_with?("]")
|
|
98
155
|
|
|
99
|
-
|
|
100
|
-
# sequences become U+FFFD (which domain-to-ASCII then rejects),
|
|
101
|
-
# matching the spec rather than crashing on invalid encoding.
|
|
102
|
-
domain = percent_decode(input).force_encoding("UTF-8").scrub("\uFFFD")
|
|
103
|
-
# The domain parser (beStrict false): an ASCII domain is only
|
|
104
|
-
# lowercased, whatever UTS #46 would make of it, for web
|
|
105
|
-
# compatibility; a non-ASCII one goes through ToASCII.
|
|
106
|
-
ascii =
|
|
107
|
-
if domain.ascii_only?
|
|
108
|
-
domain.downcase
|
|
109
|
-
else
|
|
110
|
-
begin
|
|
111
|
-
IDNA.to_ascii(domain, check_hyphens: false, verify_dns_length: false)
|
|
112
|
-
rescue IDNA::Error, Punycode::Error => e
|
|
113
|
-
raise Failure, "domain to ASCII: #{e.message}"
|
|
114
|
-
end
|
|
156
|
+
return "[#{parse_ipv6(input[1...-1])}]"
|
|
115
157
|
end
|
|
116
|
-
|
|
117
|
-
|
|
158
|
+
return parse_opaque_host(input) unless special
|
|
159
|
+
raise Failure, "empty host" if input.empty?
|
|
160
|
+
|
|
161
|
+
# UTF-8-decode-without-BOM the percent-decoded bytes: malformed
|
|
162
|
+
# sequences become U+FFFD (which domain-to-ASCII then rejects),
|
|
163
|
+
# matching the spec rather than crashing on invalid encoding.
|
|
164
|
+
domain = percent_decode(input).force_encoding("UTF-8").scrub("\uFFFD")
|
|
165
|
+
# The domain parser (beStrict false): an ASCII domain is only
|
|
166
|
+
# lowercased, whatever UTS #46 would make of it, for web
|
|
167
|
+
# compatibility; a non-ASCII one goes through ToASCII.
|
|
168
|
+
ascii =
|
|
169
|
+
if domain.ascii_only?
|
|
170
|
+
domain.downcase
|
|
171
|
+
else
|
|
172
|
+
begin
|
|
173
|
+
IDNA.to_ascii(domain, check_hyphens: false, verify_dns_length: false)
|
|
174
|
+
rescue IDNA::Error, Punycode::Error => e
|
|
175
|
+
raise Failure, "domain to ASCII: #{e.message}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
raise Failure, "empty domain" if ascii.empty?
|
|
179
|
+
raise Failure, "forbidden domain code point" if ascii.each_char.any? { |ch| forbidden_domain?(ch.ord) }
|
|
118
180
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
181
|
+
if ends_in_number?(ascii)
|
|
182
|
+
ip = Ipv4Parser.parse(ascii)
|
|
183
|
+
raise Failure, "invalid IPv4 address" if ip.nil?
|
|
122
184
|
|
|
123
|
-
|
|
185
|
+
return ip
|
|
186
|
+
end
|
|
187
|
+
ascii
|
|
124
188
|
end
|
|
125
|
-
ascii
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def parse_opaque_host(input)
|
|
129
|
-
raise Failure, "forbidden host code point" if input.each_char.any? { |ch| forbidden_host?(ch.ord) }
|
|
130
|
-
|
|
131
|
-
input.each_char.map { |ch| pe(ch, method(:c0?)) }.join
|
|
132
|
-
end
|
|
133
189
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
parts.pop if parts.length > 1 && parts.last == ""
|
|
137
|
-
return false if parts.empty?
|
|
190
|
+
def parse_opaque_host(input)
|
|
191
|
+
raise Failure, "forbidden host code point" if input.each_char.any? { |ch| forbidden_host?(ch.ord) }
|
|
138
192
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return true if last.match?(/\A[0-9]+\z/)
|
|
142
|
-
|
|
143
|
-
last.match?(/\A0[xX][0-9A-Fa-f]*\z/)
|
|
144
|
-
end
|
|
193
|
+
input.each_char.map { |ch| pe(ch, method(:c0?)) }.join
|
|
194
|
+
end
|
|
145
195
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
compress = nil
|
|
151
|
-
chars = input.chars
|
|
152
|
-
ptr = 0
|
|
153
|
-
c = ->(i) { i < chars.length ? chars[i] : nil }
|
|
196
|
+
def ends_in_number?(input)
|
|
197
|
+
parts = input.split(".", -1)
|
|
198
|
+
parts.pop if parts.length > 1 && parts.last == ""
|
|
199
|
+
return false if parts.empty?
|
|
154
200
|
|
|
155
|
-
|
|
156
|
-
|
|
201
|
+
last = parts.last
|
|
202
|
+
return false if last.empty?
|
|
203
|
+
return true if last.match?(/\A[0-9]+\z/)
|
|
157
204
|
|
|
158
|
-
|
|
159
|
-
piece_index += 1
|
|
160
|
-
compress = piece_index
|
|
205
|
+
last.match?(/\A0[xX][0-9A-Fa-f]*\z/)
|
|
161
206
|
end
|
|
162
207
|
|
|
163
|
-
|
|
164
|
-
|
|
208
|
+
# WHATWG IPv6 parser -> compressed serialized string (no brackets).
|
|
209
|
+
def parse_ipv6(input)
|
|
210
|
+
address = [0, 0, 0, 0, 0, 0, 0, 0]
|
|
211
|
+
piece_index = 0
|
|
212
|
+
compress = nil
|
|
213
|
+
chars = input.chars
|
|
214
|
+
ptr = 0
|
|
215
|
+
c = ->(i) { i < chars.length ? chars[i] : nil }
|
|
165
216
|
|
|
166
217
|
if c.call(ptr) == ":"
|
|
167
|
-
raise Failure, "
|
|
218
|
+
raise Failure, "IPv6 starts with single colon" unless c.call(ptr + 1) == ":"
|
|
168
219
|
|
|
169
|
-
ptr +=
|
|
220
|
+
ptr += 2
|
|
170
221
|
piece_index += 1
|
|
171
222
|
compress = piece_index
|
|
172
|
-
next
|
|
173
223
|
end
|
|
174
224
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
while length < 4 && c.call(ptr)&.match?(/[0-9A-Fa-f]/)
|
|
178
|
-
value = value * 16 + c.call(ptr).to_i(16)
|
|
179
|
-
ptr += 1
|
|
180
|
-
length += 1
|
|
181
|
-
end
|
|
225
|
+
while c.call(ptr)
|
|
226
|
+
raise Failure, "too many IPv6 pieces" if piece_index == 8
|
|
182
227
|
|
|
183
|
-
|
|
184
|
-
|
|
228
|
+
if c.call(ptr) == ":"
|
|
229
|
+
raise Failure, "multiple IPv6 compressions" unless compress.nil?
|
|
185
230
|
|
|
186
|
-
|
|
187
|
-
|
|
231
|
+
ptr += 1
|
|
232
|
+
piece_index += 1
|
|
233
|
+
compress = piece_index
|
|
234
|
+
next
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
value = 0
|
|
238
|
+
length = 0
|
|
239
|
+
while length < 4 && c.call(ptr)&.match?(/[0-9A-Fa-f]/)
|
|
240
|
+
value = value * 16 + c.call(ptr).to_i(16)
|
|
241
|
+
ptr += 1
|
|
242
|
+
length += 1
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
if c.call(ptr) == "."
|
|
246
|
+
raise Failure, "IPv4-in-IPv6 with no digits" if length.zero?
|
|
247
|
+
|
|
248
|
+
ptr -= length
|
|
249
|
+
raise Failure, "too few pieces for embedded IPv4" if piece_index > 6
|
|
250
|
+
|
|
251
|
+
numbers_seen = 0
|
|
252
|
+
while c.call(ptr)
|
|
253
|
+
ipv4_piece = nil
|
|
254
|
+
if numbers_seen.positive?
|
|
255
|
+
if c.call(ptr) == "." && numbers_seen < 4
|
|
256
|
+
ptr += 1
|
|
257
|
+
else
|
|
258
|
+
raise Failure, "invalid embedded IPv4"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
raise Failure, "invalid embedded IPv4 digit" unless c.call(ptr)&.match?(/[0-9]/)
|
|
262
|
+
|
|
263
|
+
while c.call(ptr)&.match?(/[0-9]/)
|
|
264
|
+
number = c.call(ptr).to_i
|
|
265
|
+
if ipv4_piece.nil?
|
|
266
|
+
ipv4_piece = number
|
|
267
|
+
elsif ipv4_piece.zero?
|
|
268
|
+
raise Failure, "leading zero in embedded IPv4"
|
|
269
|
+
else
|
|
270
|
+
ipv4_piece = ipv4_piece * 10 + number
|
|
271
|
+
end
|
|
272
|
+
raise Failure, "embedded IPv4 piece > 255" if ipv4_piece > 255
|
|
188
273
|
|
|
189
|
-
numbers_seen = 0
|
|
190
|
-
while c.call(ptr)
|
|
191
|
-
ipv4_piece = nil
|
|
192
|
-
if numbers_seen.positive?
|
|
193
|
-
if c.call(ptr) == "." && numbers_seen < 4
|
|
194
274
|
ptr += 1
|
|
195
|
-
else
|
|
196
|
-
raise Failure, "invalid embedded IPv4"
|
|
197
275
|
end
|
|
276
|
+
address[piece_index] = address[piece_index] * 0x100 + ipv4_piece
|
|
277
|
+
numbers_seen += 1
|
|
278
|
+
piece_index += 1 if numbers_seen == 2 || numbers_seen == 4
|
|
198
279
|
end
|
|
199
|
-
raise Failure, "
|
|
200
|
-
|
|
201
|
-
while c.call(ptr)&.match?(/[0-9]/)
|
|
202
|
-
number = c.call(ptr).to_i
|
|
203
|
-
if ipv4_piece.nil?
|
|
204
|
-
ipv4_piece = number
|
|
205
|
-
elsif ipv4_piece.zero?
|
|
206
|
-
raise Failure, "leading zero in embedded IPv4"
|
|
207
|
-
else
|
|
208
|
-
ipv4_piece = ipv4_piece * 10 + number
|
|
209
|
-
end
|
|
210
|
-
raise Failure, "embedded IPv4 piece > 255" if ipv4_piece > 255
|
|
280
|
+
raise Failure, "incomplete embedded IPv4" unless numbers_seen == 4
|
|
211
281
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
282
|
+
break
|
|
283
|
+
elsif c.call(ptr) == ":"
|
|
284
|
+
ptr += 1
|
|
285
|
+
raise Failure, "trailing colon in IPv6" if c.call(ptr).nil?
|
|
286
|
+
elsif c.call(ptr)
|
|
287
|
+
raise Failure, "invalid IPv6 code point"
|
|
217
288
|
end
|
|
218
|
-
raise Failure, "incomplete embedded IPv4" unless numbers_seen == 4
|
|
219
289
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
ptr += 1
|
|
223
|
-
raise Failure, "trailing colon in IPv6" if c.call(ptr).nil?
|
|
224
|
-
elsif c.call(ptr)
|
|
225
|
-
raise Failure, "invalid IPv6 code point"
|
|
290
|
+
address[piece_index] = value
|
|
291
|
+
piece_index += 1
|
|
226
292
|
end
|
|
227
293
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
swaps -= 1
|
|
294
|
+
if compress
|
|
295
|
+
swaps = piece_index - compress
|
|
296
|
+
piece_index = 7
|
|
297
|
+
while piece_index != 0 && swaps.positive?
|
|
298
|
+
address[piece_index], address[compress + swaps - 1] = address[compress + swaps - 1], address[piece_index]
|
|
299
|
+
piece_index -= 1
|
|
300
|
+
swaps -= 1
|
|
301
|
+
end
|
|
302
|
+
elsif piece_index != 8
|
|
303
|
+
raise Failure, "too few IPv6 pieces"
|
|
239
304
|
end
|
|
240
|
-
elsif piece_index != 8
|
|
241
|
-
raise Failure, "too few IPv6 pieces"
|
|
242
|
-
end
|
|
243
305
|
|
|
244
|
-
|
|
245
|
-
|
|
306
|
+
serialize_ipv6(address)
|
|
307
|
+
end
|
|
246
308
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
309
|
+
def serialize_ipv6(pieces)
|
|
310
|
+
# Find the longest run (length > 1) of zero pieces to compress.
|
|
311
|
+
best_start = nil
|
|
312
|
+
best_len = 0
|
|
313
|
+
i = 0
|
|
314
|
+
while i < 8
|
|
315
|
+
if pieces[i].zero?
|
|
316
|
+
j = i
|
|
317
|
+
j += 1 while j < 8 && pieces[j].zero?
|
|
318
|
+
if (j - i) > best_len
|
|
319
|
+
best_len = j - i
|
|
320
|
+
best_start = i
|
|
321
|
+
end
|
|
322
|
+
i = j
|
|
323
|
+
else
|
|
324
|
+
i += 1
|
|
259
325
|
end
|
|
260
|
-
|
|
261
|
-
|
|
326
|
+
end
|
|
327
|
+
best_start = nil if best_len < 2
|
|
328
|
+
|
|
329
|
+
out = +""
|
|
330
|
+
i = 0
|
|
331
|
+
while i < 8
|
|
332
|
+
if best_start == i
|
|
333
|
+
out << (i.zero? ? "::" : ":")
|
|
334
|
+
i += best_len
|
|
335
|
+
next
|
|
336
|
+
end
|
|
337
|
+
out << pieces[i].to_s(16)
|
|
338
|
+
out << ":" if i < 7
|
|
262
339
|
i += 1
|
|
263
340
|
end
|
|
341
|
+
out
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# ===== serialization =====
|
|
345
|
+
|
|
346
|
+
def serialize_path(record)
|
|
347
|
+
return record.path if record.opaque_path?
|
|
348
|
+
|
|
349
|
+
record.path.map { |seg| "/#{seg}" }.join
|
|
264
350
|
end
|
|
265
|
-
best_start = nil if best_len < 2
|
|
266
351
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
352
|
+
def serialize(record, exclude_fragment: false)
|
|
353
|
+
out = +"#{record.scheme}:"
|
|
354
|
+
if record.host
|
|
355
|
+
out << "//"
|
|
356
|
+
if record.includes_credentials?
|
|
357
|
+
out << record.username
|
|
358
|
+
out << ":#{record.password}" unless record.password.empty?
|
|
359
|
+
out << "@"
|
|
360
|
+
end
|
|
361
|
+
out << record.host
|
|
362
|
+
out << ":#{record.port}" if record.port
|
|
363
|
+
elsif !record.opaque_path? && record.path.is_a?(Array) &&
|
|
364
|
+
record.path.length > 1 && record.path[0] == ""
|
|
365
|
+
out << "/."
|
|
274
366
|
end
|
|
275
|
-
out <<
|
|
276
|
-
out << "
|
|
277
|
-
|
|
367
|
+
out << serialize_path(record)
|
|
368
|
+
out << "?#{record.query}" if record.query
|
|
369
|
+
out << "##{record.fragment}" if record.fragment && !exclude_fragment
|
|
370
|
+
out
|
|
278
371
|
end
|
|
279
|
-
out
|
|
280
|
-
end
|
|
281
372
|
|
|
282
|
-
|
|
373
|
+
# ===== helpers for path normalization =====
|
|
283
374
|
|
|
284
|
-
|
|
285
|
-
|
|
375
|
+
def windows_drive_letter?(seg) = seg.length == 2 && seg[0].match?(/[A-Za-z]/) && [":", "|"].include?(seg[1])
|
|
376
|
+
def normalized_windows_drive_letter?(seg) = seg.length == 2 && seg[0].match?(/[A-Za-z]/) && seg[1] == ":"
|
|
377
|
+
def starts_with_windows_drive_letter?(s)
|
|
378
|
+
s.length >= 2 && s[0].match?(/[A-Za-z]/) && [":", "|"].include?(s[1]) &&
|
|
379
|
+
(s.length == 2 || ["/", "\\", "?", "#"].include?(s[2]))
|
|
380
|
+
end
|
|
286
381
|
|
|
287
|
-
|
|
288
|
-
end
|
|
382
|
+
def single_dot?(seg) = [".", "%2e"].include?(seg.downcase)
|
|
289
383
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
out << "//"
|
|
294
|
-
if record.includes_credentials?
|
|
295
|
-
out << record.username
|
|
296
|
-
out << ":#{record.password}" unless record.password.empty?
|
|
297
|
-
out << "@"
|
|
298
|
-
end
|
|
299
|
-
out << record.host
|
|
300
|
-
out << ":#{record.port}" if record.port
|
|
301
|
-
elsif !record.opaque_path? && record.path.is_a?(Array) &&
|
|
302
|
-
record.path.length > 1 && record.path[0] == ""
|
|
303
|
-
out << "/."
|
|
304
|
-
end
|
|
305
|
-
out << serialize_path(record)
|
|
306
|
-
out << "?#{record.query}" if record.query
|
|
307
|
-
out << "##{record.fragment}" if record.fragment && !exclude_fragment
|
|
308
|
-
out
|
|
309
|
-
end
|
|
384
|
+
def double_dot?(seg)
|
|
385
|
+
["..", ".%2e", "%2e.", "%2e%2e"].include?(seg.downcase)
|
|
386
|
+
end
|
|
310
387
|
|
|
311
|
-
|
|
388
|
+
def shorten_path(record)
|
|
389
|
+
path = record.path
|
|
390
|
+
return if path.empty?
|
|
391
|
+
return if record.scheme == "file" && path.length == 1 && normalized_windows_drive_letter?(path[0])
|
|
312
392
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def starts_with_windows_drive_letter?(s)
|
|
316
|
-
s.length >= 2 && s[0].match?(/[A-Za-z]/) && [":", "|"].include?(s[1]) &&
|
|
317
|
-
(s.length == 2 || ["/", "\\", "?", "#"].include?(s[2]))
|
|
393
|
+
path.pop
|
|
394
|
+
end
|
|
318
395
|
end
|
|
319
396
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def double_dot?(seg)
|
|
323
|
-
["..", ".%2e", "%2e.", "%2e%2e"].include?(seg.downcase)
|
|
324
|
-
end
|
|
397
|
+
# So `UrlParser.percent_decode` and friends keep answering.
|
|
398
|
+
extend Syntax
|
|
325
399
|
|
|
326
|
-
|
|
327
|
-
path = record.path
|
|
328
|
-
return if path.empty?
|
|
329
|
-
return if record.scheme == "file" && path.length == 1 && normalized_windows_drive_letter?(path[0])
|
|
400
|
+
# ===== the basic URL parser state machine =====
|
|
330
401
|
|
|
331
|
-
|
|
402
|
+
def run(input, base, url: nil, state_override: nil, encoding: nil)
|
|
403
|
+
BasicParser.new(input, base, url: url, state_override: state_override, encoding: encoding).run
|
|
332
404
|
end
|
|
333
405
|
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
406
|
+
# The basic URL parser's state machine (url.spec.whatwg.org §4.4), one
|
|
407
|
+
# method per state.
|
|
408
|
+
#
|
|
409
|
+
# It was one 390-line method, which is faithful to the spec's shape but
|
|
410
|
+
# leaves eight mutable locals whose writers are scattered over twenty
|
|
411
|
+
# states. As a class the locals are the parser's own state, and each spec
|
|
412
|
+
# state is a method carrying the spec's name for it, so a state can be
|
|
413
|
+
# read without reading the other nineteen.
|
|
414
|
+
#
|
|
415
|
+
# The two non-linear moves the spec makes are throws the loop catches:
|
|
416
|
+
# "do not advance, re-run this position in the new state" is
|
|
417
|
+
# `throw :reprocess`, and a setter that stops early (state_override) is
|
|
418
|
+
# `throw :done`. A `next` inside a nested block still means the next
|
|
419
|
+
# element of that block — see the authority state, which walks the
|
|
420
|
+
# buffer's characters.
|
|
421
|
+
class BasicParser
|
|
422
|
+
include Syntax
|
|
423
|
+
# Syntax is the parser's vocabulary, not part of its interface: a state
|
|
424
|
+
# calls `pe` or `shorten_path` on itself, nobody calls them on a parser.
|
|
425
|
+
private(*Syntax.public_instance_methods)
|
|
426
|
+
|
|
427
|
+
STATES = {
|
|
428
|
+
scheme_start: :state_scheme_start,
|
|
429
|
+
scheme: :state_scheme,
|
|
430
|
+
no_scheme: :state_no_scheme,
|
|
431
|
+
special_relative_or_authority: :state_special_relative_or_authority,
|
|
432
|
+
path_or_authority: :state_path_or_authority,
|
|
433
|
+
relative: :state_relative,
|
|
434
|
+
relative_slash: :state_relative_slash,
|
|
435
|
+
special_authority_slashes: :state_special_authority_slashes,
|
|
436
|
+
special_authority_ignore_slashes: :state_special_authority_ignore_slashes,
|
|
437
|
+
authority: :state_authority,
|
|
438
|
+
host: :state_host,
|
|
439
|
+
hostname: :state_host,
|
|
440
|
+
port: :state_port,
|
|
441
|
+
file: :state_file,
|
|
442
|
+
file_slash: :state_file_slash,
|
|
443
|
+
file_host: :state_file_host,
|
|
444
|
+
path_start: :state_path_start,
|
|
445
|
+
path: :state_path,
|
|
446
|
+
opaque_path: :state_opaque_path,
|
|
447
|
+
query: :state_query,
|
|
448
|
+
fragment: :state_fragment,
|
|
449
|
+
}.freeze
|
|
450
|
+
|
|
451
|
+
def initialize(input, base, url: nil, state_override: nil, encoding: nil)
|
|
452
|
+
input = input.dup
|
|
453
|
+
# Strip leading/trailing C0 controls and spaces (only for a fresh
|
|
454
|
+
# parse; a setter's input keeps them), then remove all ASCII
|
|
455
|
+
# tab/newline.
|
|
456
|
+
input = input.sub(/\A[\x00-\x20]+/, "").sub(/[\x00-\x20]+\z/, "") if url.nil?
|
|
457
|
+
@input = input.gsub(/[\t\n\r]/, "")
|
|
458
|
+
@chars = @input.chars
|
|
459
|
+
@len = @chars.length
|
|
460
|
+
@base = base
|
|
461
|
+
@state_override = state_override
|
|
462
|
+
@encoding = encoding
|
|
463
|
+
@state = state_override || :scheme_start
|
|
464
|
+
@url = url || Record.new("", "", "", nil, nil, [], nil, nil)
|
|
465
|
+
@buffer = +""
|
|
466
|
+
@at_sign_seen = false
|
|
467
|
+
@inside_brackets = false
|
|
468
|
+
@password_token_seen = false
|
|
469
|
+
@ptr = 0
|
|
470
|
+
end
|
|
370
471
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
# non-special, onto file with credentials or a port, or off
|
|
378
|
-
# file with an empty host.
|
|
379
|
-
return url if SPECIAL.key?(url.scheme) != SPECIAL.key?(buffer)
|
|
380
|
-
return url if (url.includes_credentials? || !url.port.nil?) && buffer == "file"
|
|
381
|
-
return url if url.scheme == "file" && url.host == ""
|
|
382
|
-
|
|
383
|
-
url.scheme = buffer
|
|
384
|
-
url.port = nil if url.port == url.default_port
|
|
385
|
-
return url
|
|
472
|
+
def run
|
|
473
|
+
catch(:done) do
|
|
474
|
+
loop do
|
|
475
|
+
advanced = catch(:reprocess) do
|
|
476
|
+
send(STATES.fetch(@state), current_char)
|
|
477
|
+
true
|
|
386
478
|
end
|
|
479
|
+
next unless advanced
|
|
480
|
+
break if @ptr >= @len
|
|
387
481
|
|
|
388
|
-
|
|
389
|
-
buffer = +""
|
|
390
|
-
if url.scheme == "file"
|
|
391
|
-
state = :file
|
|
392
|
-
elsif url.special? && base && base.scheme == url.scheme
|
|
393
|
-
state = :special_relative_or_authority
|
|
394
|
-
elsif url.special?
|
|
395
|
-
state = :special_authority_slashes
|
|
396
|
-
elsif input[(ptr + 1)..].to_s.start_with?("/")
|
|
397
|
-
state = :path_or_authority
|
|
398
|
-
ptr += 1
|
|
399
|
-
else
|
|
400
|
-
url.path = ""
|
|
401
|
-
state = :opaque_path
|
|
402
|
-
end
|
|
403
|
-
elsif state_override
|
|
404
|
-
raise Failure, "invalid scheme"
|
|
405
|
-
else
|
|
406
|
-
buffer = +""
|
|
407
|
-
state = :no_scheme
|
|
408
|
-
ptr = -1 # restart from 0 (advance makes it 0)
|
|
482
|
+
@ptr += 1
|
|
409
483
|
end
|
|
484
|
+
@url
|
|
485
|
+
end
|
|
486
|
+
end
|
|
410
487
|
|
|
411
|
-
|
|
412
|
-
raise Failure, "missing scheme" if base.nil? || (base.opaque_path? && c != "#")
|
|
413
|
-
|
|
414
|
-
if base.opaque_path? && c == "#"
|
|
415
|
-
url.scheme = base.scheme
|
|
416
|
-
url.path = base.path
|
|
417
|
-
url.query = base.query
|
|
418
|
-
url.fragment = +""
|
|
419
|
-
state = :fragment
|
|
420
|
-
elsif base.scheme != "file"
|
|
421
|
-
state = :relative
|
|
422
|
-
next
|
|
423
|
-
else
|
|
424
|
-
state = :file
|
|
425
|
-
next
|
|
426
|
-
end
|
|
488
|
+
private
|
|
427
489
|
|
|
428
|
-
|
|
429
|
-
if c == "/" && input[(ptr + 1)..].to_s.start_with?("/")
|
|
430
|
-
state = :special_authority_ignore_slashes
|
|
431
|
-
ptr += 1
|
|
432
|
-
else
|
|
433
|
-
state = :relative
|
|
434
|
-
next
|
|
435
|
-
end
|
|
490
|
+
def current_char = @ptr < @len ? @chars[@ptr] : nil
|
|
436
491
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
492
|
+
def state_scheme_start(c)
|
|
493
|
+
if c&.match?(/[A-Za-z]/)
|
|
494
|
+
@buffer << c.downcase
|
|
495
|
+
@state = :scheme
|
|
496
|
+
elsif @state_override
|
|
497
|
+
raise Failure, "scheme must start with a letter"
|
|
498
|
+
else
|
|
499
|
+
@state = :no_scheme
|
|
500
|
+
throw :reprocess
|
|
501
|
+
end
|
|
502
|
+
end
|
|
444
503
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
url.
|
|
454
|
-
url.
|
|
455
|
-
url.
|
|
456
|
-
|
|
457
|
-
url.
|
|
458
|
-
if
|
|
459
|
-
|
|
460
|
-
state = :query
|
|
461
|
-
elsif c == "#"
|
|
462
|
-
url.fragment = +""
|
|
463
|
-
state = :fragment
|
|
464
|
-
elsif c
|
|
465
|
-
url.query = nil
|
|
466
|
-
shorten_path(url)
|
|
467
|
-
state = :path
|
|
468
|
-
next
|
|
469
|
-
end
|
|
504
|
+
def state_scheme(c)
|
|
505
|
+
if c&.match?(/[A-Za-z0-9+\-.]/)
|
|
506
|
+
@buffer << c.downcase
|
|
507
|
+
elsif c == ":"
|
|
508
|
+
if @state_override
|
|
509
|
+
# A setter may not move a URL between special and
|
|
510
|
+
# non-special, onto file with credentials or a port, or off
|
|
511
|
+
# file with an empty host.
|
|
512
|
+
throw :done, @url if SPECIAL.key?(@url.scheme) != SPECIAL.key?(@buffer)
|
|
513
|
+
throw :done, @url if (@url.includes_credentials? || !@url.port.nil?) && @buffer == "file"
|
|
514
|
+
throw :done, @url if @url.scheme == "file" && @url.host == ""
|
|
515
|
+
|
|
516
|
+
@url.scheme = @buffer
|
|
517
|
+
@url.port = nil if @url.port == @url.default_port
|
|
518
|
+
throw :done, @url
|
|
470
519
|
end
|
|
471
520
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
521
|
+
@url.scheme = @buffer
|
|
522
|
+
@buffer = +""
|
|
523
|
+
if @url.scheme == "file"
|
|
524
|
+
@state = :file
|
|
525
|
+
elsif @url.special? && @base && @base.scheme == @url.scheme
|
|
526
|
+
@state = :special_relative_or_authority
|
|
527
|
+
elsif @url.special?
|
|
528
|
+
@state = :special_authority_slashes
|
|
529
|
+
elsif @input[(@ptr + 1)..].to_s.start_with?("/")
|
|
530
|
+
@state = :path_or_authority
|
|
531
|
+
@ptr += 1
|
|
477
532
|
else
|
|
478
|
-
url.
|
|
479
|
-
|
|
480
|
-
url.host = base.host
|
|
481
|
-
url.port = base.port
|
|
482
|
-
state = :path
|
|
483
|
-
next
|
|
533
|
+
@url.path = ""
|
|
534
|
+
@state = :opaque_path
|
|
484
535
|
end
|
|
536
|
+
elsif @state_override
|
|
537
|
+
raise Failure, "invalid scheme"
|
|
538
|
+
else
|
|
539
|
+
@buffer = +""
|
|
540
|
+
@state = :no_scheme
|
|
541
|
+
@ptr = -1 # restart from 0 (advance makes it 0)
|
|
542
|
+
end
|
|
543
|
+
end
|
|
485
544
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
545
|
+
def state_no_scheme(c)
|
|
546
|
+
raise Failure, "missing scheme" if @base.nil? || (@base.opaque_path? && c != "#")
|
|
547
|
+
|
|
548
|
+
if @base.opaque_path? && c == "#"
|
|
549
|
+
@url.scheme = @base.scheme
|
|
550
|
+
@url.path = @base.path
|
|
551
|
+
@url.query = @base.query
|
|
552
|
+
@url.fragment = +""
|
|
553
|
+
@state = :fragment
|
|
554
|
+
elsif @base.scheme != "file"
|
|
555
|
+
@state = :relative
|
|
556
|
+
throw :reprocess
|
|
557
|
+
else
|
|
558
|
+
@state = :file
|
|
559
|
+
throw :reprocess
|
|
560
|
+
end
|
|
561
|
+
end
|
|
494
562
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
563
|
+
def state_special_relative_or_authority(c)
|
|
564
|
+
if c == "/" && @input[(@ptr + 1)..].to_s.start_with?("/")
|
|
565
|
+
@state = :special_authority_ignore_slashes
|
|
566
|
+
@ptr += 1
|
|
567
|
+
else
|
|
568
|
+
@state = :relative
|
|
569
|
+
throw :reprocess
|
|
570
|
+
end
|
|
571
|
+
end
|
|
500
572
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
end
|
|
510
|
-
encoded = pe(ch, method(:userinfo_set?))
|
|
511
|
-
if password_token_seen
|
|
512
|
-
url.password += encoded
|
|
513
|
-
else
|
|
514
|
-
url.username += encoded
|
|
515
|
-
end
|
|
516
|
-
end
|
|
517
|
-
buffer = +""
|
|
518
|
-
elsif c.nil? || ["/", "?", "#"].include?(c) || (url.special? && c == "\\")
|
|
519
|
-
raise Failure, "empty host with credentials" if at_sign_seen && buffer.empty?
|
|
573
|
+
def state_path_or_authority(c)
|
|
574
|
+
if c == "/"
|
|
575
|
+
@state = :authority
|
|
576
|
+
else
|
|
577
|
+
@state = :path
|
|
578
|
+
throw :reprocess
|
|
579
|
+
end
|
|
580
|
+
end
|
|
520
581
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
582
|
+
def state_relative(c)
|
|
583
|
+
@url.scheme = @base.scheme
|
|
584
|
+
if c == "/"
|
|
585
|
+
@state = :relative_slash
|
|
586
|
+
elsif @url.special? && c == "\\"
|
|
587
|
+
@state = :relative_slash
|
|
588
|
+
else
|
|
589
|
+
@url.username = @base.username
|
|
590
|
+
@url.password = @base.password
|
|
591
|
+
@url.host = @base.host
|
|
592
|
+
@url.port = @base.port
|
|
593
|
+
@url.path = @base.path.dup
|
|
594
|
+
@url.query = @base.query
|
|
595
|
+
if c == "?"
|
|
596
|
+
@url.query = +""
|
|
597
|
+
@state = :query
|
|
598
|
+
elsif c == "#"
|
|
599
|
+
@url.fragment = +""
|
|
600
|
+
@state = :fragment
|
|
601
|
+
elsif c
|
|
602
|
+
@url.query = nil
|
|
603
|
+
shorten_path(@url)
|
|
604
|
+
@state = :path
|
|
605
|
+
throw :reprocess
|
|
526
606
|
end
|
|
607
|
+
end
|
|
608
|
+
end
|
|
527
609
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if state_override && buffer.empty? && (url.includes_credentials? || !url.port.nil?)
|
|
543
|
-
raise Failure, "a URL with credentials or a port needs a host"
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
url.host = parse_host(buffer, url.special?)
|
|
547
|
-
return url if state_override
|
|
548
|
-
|
|
549
|
-
buffer = +""
|
|
550
|
-
state = :path_start
|
|
551
|
-
else
|
|
552
|
-
inside_brackets = true if c == "["
|
|
553
|
-
inside_brackets = false if c == "]"
|
|
554
|
-
buffer << c
|
|
555
|
-
end
|
|
610
|
+
def state_relative_slash(c)
|
|
611
|
+
if @url.special? && (c == "/" || c == "\\")
|
|
612
|
+
@state = :special_authority_ignore_slashes
|
|
613
|
+
elsif c == "/"
|
|
614
|
+
@state = :authority
|
|
615
|
+
else
|
|
616
|
+
@url.username = @base.username
|
|
617
|
+
@url.password = @base.password
|
|
618
|
+
@url.host = @base.host
|
|
619
|
+
@url.port = @base.port
|
|
620
|
+
@state = :path
|
|
621
|
+
throw :reprocess
|
|
622
|
+
end
|
|
623
|
+
end
|
|
556
624
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
buffer = +""
|
|
567
|
-
return url if state_override
|
|
568
|
-
end
|
|
569
|
-
raise Failure, "empty port" if state_override
|
|
625
|
+
def state_special_authority_slashes(c)
|
|
626
|
+
if c == "/" && @input[(@ptr + 1)..].to_s.start_with?("/")
|
|
627
|
+
@state = :special_authority_ignore_slashes
|
|
628
|
+
@ptr += 1
|
|
629
|
+
else
|
|
630
|
+
@state = :special_authority_ignore_slashes
|
|
631
|
+
throw :reprocess
|
|
632
|
+
end
|
|
633
|
+
end
|
|
570
634
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
635
|
+
def state_special_authority_ignore_slashes(c)
|
|
636
|
+
if c != "/" && c != "\\"
|
|
637
|
+
@state = :authority
|
|
638
|
+
throw :reprocess
|
|
639
|
+
end
|
|
640
|
+
end
|
|
576
641
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
url.path = base.path.dup
|
|
585
|
-
url.query = base.query
|
|
586
|
-
if c == "?"
|
|
587
|
-
url.query = +""
|
|
588
|
-
state = :query
|
|
589
|
-
elsif c == "#"
|
|
590
|
-
url.fragment = +""
|
|
591
|
-
state = :fragment
|
|
592
|
-
elsif c
|
|
593
|
-
url.query = nil
|
|
594
|
-
shorten_path(url) unless starts_with_windows_drive_letter?(input[ptr..].to_s)
|
|
595
|
-
url.path = [] if starts_with_windows_drive_letter?(input[ptr..].to_s)
|
|
596
|
-
state = :path
|
|
642
|
+
def state_authority(c)
|
|
643
|
+
if c == "@"
|
|
644
|
+
@buffer = "%40#{@buffer}" if @at_sign_seen
|
|
645
|
+
@at_sign_seen = true
|
|
646
|
+
@buffer.each_char do |ch|
|
|
647
|
+
if ch == ":" && !@password_token_seen
|
|
648
|
+
@password_token_seen = true
|
|
597
649
|
next
|
|
598
650
|
end
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
651
|
+
encoded = pe(ch, method(:userinfo_set?))
|
|
652
|
+
if @password_token_seen
|
|
653
|
+
@url.password += encoded
|
|
654
|
+
else
|
|
655
|
+
@url.username += encoded
|
|
656
|
+
end
|
|
602
657
|
end
|
|
658
|
+
@buffer = +""
|
|
659
|
+
elsif c.nil? || ["/", "?", "#"].include?(c) || (@url.special? && c == "\\")
|
|
660
|
+
raise Failure, "empty host with credentials" if @at_sign_seen && @buffer.empty?
|
|
603
661
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
662
|
+
@ptr -= (@buffer.length + 1)
|
|
663
|
+
@buffer = +""
|
|
664
|
+
@state = :host
|
|
665
|
+
else
|
|
666
|
+
@buffer << c
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def state_host(c)
|
|
671
|
+
if @state_override && @url.scheme == "file"
|
|
672
|
+
@ptr -= 1
|
|
673
|
+
@state = :file_host
|
|
674
|
+
elsif c == ":" && !@inside_brackets
|
|
675
|
+
raise Failure, "empty host" if @buffer.empty?
|
|
676
|
+
raise Failure, "a hostname cannot carry a port" if @state_override == :hostname
|
|
677
|
+
|
|
678
|
+
@url.host = parse_host(@buffer, @url.special?)
|
|
679
|
+
@buffer = +""
|
|
680
|
+
@state = :port
|
|
681
|
+
elsif c.nil? || ["/", "?", "#"].include?(c) || (@url.special? && c == "\\")
|
|
682
|
+
@ptr -= 1
|
|
683
|
+
raise Failure, "empty special host" if @url.special? && @buffer.empty?
|
|
684
|
+
if @state_override && @buffer.empty? && (@url.includes_credentials? || !@url.port.nil?)
|
|
685
|
+
raise Failure, "a URL with credentials or a port needs a host"
|
|
617
686
|
end
|
|
618
687
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
ptr -= 1
|
|
622
|
-
if state_override.nil? && buffer.match?(/\A[A-Za-z][:|]\z/)
|
|
623
|
-
state = :path
|
|
624
|
-
elsif buffer.empty?
|
|
625
|
-
url.host = ""
|
|
626
|
-
return url if state_override
|
|
688
|
+
@url.host = parse_host(@buffer, @url.special?)
|
|
689
|
+
throw :done, @url if @state_override
|
|
627
690
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
691
|
+
@buffer = +""
|
|
692
|
+
@state = :path_start
|
|
693
|
+
else
|
|
694
|
+
@inside_brackets = true if c == "["
|
|
695
|
+
@inside_brackets = false if c == "]"
|
|
696
|
+
@buffer << c
|
|
697
|
+
end
|
|
698
|
+
end
|
|
634
699
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
700
|
+
def state_port(c)
|
|
701
|
+
if c&.match?(/[0-9]/)
|
|
702
|
+
@buffer << c
|
|
703
|
+
elsif c.nil? || ["/", "?", "#"].include?(c) || (@url.special? && c == "\\") || @state_override
|
|
704
|
+
unless @buffer.empty?
|
|
705
|
+
port = @buffer.to_i
|
|
706
|
+
raise Failure, "port out of range" if port > 65_535
|
|
707
|
+
|
|
708
|
+
@url.port = (port == @url.default_port ? nil : port)
|
|
709
|
+
@buffer = +""
|
|
710
|
+
throw :done, @url if @state_override
|
|
640
711
|
end
|
|
712
|
+
raise Failure, "empty port" if @state_override
|
|
713
|
+
|
|
714
|
+
@state = :path_start
|
|
715
|
+
throw :reprocess
|
|
716
|
+
else
|
|
717
|
+
raise Failure, "invalid port"
|
|
718
|
+
end
|
|
719
|
+
end
|
|
641
720
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
721
|
+
def state_file(c)
|
|
722
|
+
@url.scheme = "file"
|
|
723
|
+
@url.host = ""
|
|
724
|
+
if c == "/" || c == "\\"
|
|
725
|
+
@state = :file_slash
|
|
726
|
+
elsif @base && @base.scheme == "file"
|
|
727
|
+
@url.host = @base.host
|
|
728
|
+
@url.path = @base.path.dup
|
|
729
|
+
@url.query = @base.query
|
|
730
|
+
if c == "?"
|
|
731
|
+
@url.query = +""
|
|
732
|
+
@state = :query
|
|
733
|
+
elsif c == "#"
|
|
734
|
+
@url.fragment = +""
|
|
735
|
+
@state = :fragment
|
|
652
736
|
elsif c
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
737
|
+
@url.query = nil
|
|
738
|
+
shorten_path(@url) unless starts_with_windows_drive_letter?(@input[@ptr..].to_s)
|
|
739
|
+
@url.path = [] if starts_with_windows_drive_letter?(@input[@ptr..].to_s)
|
|
740
|
+
@state = :path
|
|
741
|
+
throw :reprocess
|
|
657
742
|
end
|
|
743
|
+
else
|
|
744
|
+
@state = :path
|
|
745
|
+
throw :reprocess
|
|
746
|
+
end
|
|
747
|
+
end
|
|
658
748
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if url.scheme == "file" && url.path.empty? && windows_drive_letter?(buffer)
|
|
669
|
-
buffer[1] = ":"
|
|
670
|
-
end
|
|
671
|
-
url.path << buffer
|
|
672
|
-
end
|
|
673
|
-
buffer = +""
|
|
674
|
-
if c == "?"
|
|
675
|
-
url.query = +""
|
|
676
|
-
state = :query
|
|
677
|
-
elsif c == "#"
|
|
678
|
-
url.fragment = +""
|
|
679
|
-
state = :fragment
|
|
749
|
+
def state_file_slash(c)
|
|
750
|
+
if c == "/" || c == "\\"
|
|
751
|
+
@state = :file_host
|
|
752
|
+
else
|
|
753
|
+
if @base && @base.scheme == "file"
|
|
754
|
+
@url.host = @base.host
|
|
755
|
+
if !starts_with_windows_drive_letter?(@input[@ptr..].to_s) &&
|
|
756
|
+
@base.path[0] && normalized_windows_drive_letter?(@base.path[0])
|
|
757
|
+
@url.path << @base.path[0]
|
|
680
758
|
end
|
|
759
|
+
end
|
|
760
|
+
@state = :path
|
|
761
|
+
throw :reprocess
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def state_file_host(c)
|
|
766
|
+
if c.nil? || ["/", "\\", "?", "#"].include?(c)
|
|
767
|
+
@ptr -= 1
|
|
768
|
+
if @state_override.nil? && @buffer.match?(/\A[A-Za-z][:|]\z/)
|
|
769
|
+
@state = :path
|
|
770
|
+
elsif @buffer.empty?
|
|
771
|
+
@url.host = ""
|
|
772
|
+
throw :done, @url if @state_override
|
|
773
|
+
|
|
774
|
+
@state = :path_start
|
|
681
775
|
else
|
|
682
|
-
|
|
776
|
+
host = parse_host(@buffer, true)
|
|
777
|
+
host = "" if host == "localhost"
|
|
778
|
+
@url.host = host
|
|
779
|
+
throw :done, @url if @state_override
|
|
780
|
+
|
|
781
|
+
@buffer = +""
|
|
782
|
+
@state = :path_start
|
|
683
783
|
end
|
|
784
|
+
else
|
|
785
|
+
@buffer << c
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def state_path_start(c)
|
|
790
|
+
if @url.special?
|
|
791
|
+
@state = :path
|
|
792
|
+
throw :reprocess unless c == "/" || c == "\\"
|
|
793
|
+
elsif @state_override.nil? && c == "?"
|
|
794
|
+
@url.query = +""
|
|
795
|
+
@state = :query
|
|
796
|
+
elsif @state_override.nil? && c == "#"
|
|
797
|
+
@url.fragment = +""
|
|
798
|
+
@state = :fragment
|
|
799
|
+
elsif c
|
|
800
|
+
@state = :path
|
|
801
|
+
throw :reprocess unless c == "/"
|
|
802
|
+
elsif @state_override && @url.host.nil?
|
|
803
|
+
@url.path << ""
|
|
804
|
+
end
|
|
805
|
+
end
|
|
684
806
|
|
|
685
|
-
|
|
807
|
+
def state_path(c)
|
|
808
|
+
if c.nil? || c == "/" || (@url.special? && c == "\\") ||
|
|
809
|
+
(@state_override.nil? && (c == "?" || c == "#"))
|
|
810
|
+
if double_dot?(@buffer)
|
|
811
|
+
shorten_path(@url)
|
|
812
|
+
@url.path << "" unless c == "/" || (@url.special? && c == "\\")
|
|
813
|
+
elsif single_dot?(@buffer)
|
|
814
|
+
@url.path << "" unless c == "/" || (@url.special? && c == "\\")
|
|
815
|
+
else
|
|
816
|
+
if @url.scheme == "file" && @url.path.empty? && windows_drive_letter?(@buffer)
|
|
817
|
+
@buffer[1] = ":"
|
|
818
|
+
end
|
|
819
|
+
@url.path << @buffer
|
|
820
|
+
end
|
|
821
|
+
@buffer = +""
|
|
686
822
|
if c == "?"
|
|
687
|
-
url.query = +""
|
|
688
|
-
state = :query
|
|
823
|
+
@url.query = +""
|
|
824
|
+
@state = :query
|
|
689
825
|
elsif c == "#"
|
|
690
|
-
url.fragment = +""
|
|
691
|
-
state = :fragment
|
|
692
|
-
elsif c == " "
|
|
693
|
-
# A space is only percent-encoded when it abuts the end of
|
|
694
|
-
# the opaque path (a following `?`/`#`); an interior space
|
|
695
|
-
# stays literal. (Trailing-at-EOF spaces are already gone
|
|
696
|
-
# via the leading/trailing strip.)
|
|
697
|
-
nxt = chars[ptr + 1]
|
|
698
|
-
url.path += (nxt == "?" || nxt == "#") ? "%20" : " "
|
|
699
|
-
elsif c
|
|
700
|
-
url.path += pe(c, method(:c0?))
|
|
826
|
+
@url.fragment = +""
|
|
827
|
+
@state = :fragment
|
|
701
828
|
end
|
|
829
|
+
else
|
|
830
|
+
@buffer << pe(c, method(:path_set?))
|
|
831
|
+
end
|
|
832
|
+
end
|
|
702
833
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
834
|
+
def state_opaque_path(c)
|
|
835
|
+
if c == "?"
|
|
836
|
+
@url.query = +""
|
|
837
|
+
@state = :query
|
|
838
|
+
elsif c == "#"
|
|
839
|
+
@url.fragment = +""
|
|
840
|
+
@state = :fragment
|
|
841
|
+
elsif c == " "
|
|
842
|
+
# A space is only percent-encoded when it abuts the end of
|
|
843
|
+
# the opaque path (a following `?`/`#`); an interior space
|
|
844
|
+
# stays literal. (Trailing-at-EOF spaces are already gone
|
|
845
|
+
# via the leading/trailing strip.)
|
|
846
|
+
nxt = @chars[@ptr + 1]
|
|
847
|
+
@url.path += (nxt == "?" || nxt == "#") ? "%20" : " "
|
|
848
|
+
elsif c
|
|
849
|
+
@url.path += pe(c, method(:c0?))
|
|
850
|
+
end
|
|
851
|
+
end
|
|
715
852
|
|
|
716
|
-
|
|
717
|
-
|
|
853
|
+
def state_query(c)
|
|
854
|
+
if c.nil? || (@state_override.nil? && c == "#")
|
|
855
|
+
set = @url.special? ? method(:special_query_set?) : method(:query_set?)
|
|
856
|
+
@url.query += percent_encode_after_encoding(query_encoding, @buffer, set)
|
|
857
|
+
@buffer = +""
|
|
858
|
+
if c == "#"
|
|
859
|
+
@url.fragment = +""
|
|
860
|
+
@state = :fragment
|
|
861
|
+
end
|
|
862
|
+
else
|
|
863
|
+
@buffer << c
|
|
718
864
|
end
|
|
865
|
+
end
|
|
719
866
|
|
|
720
|
-
|
|
867
|
+
# Spec query state step 1: the encoding override is ignored (forced to
|
|
868
|
+
# UTF-8) when the URL is not special, or is ws/wss.
|
|
869
|
+
def query_encoding
|
|
870
|
+
return "UTF-8" if @encoding.nil? || !@url.special?
|
|
871
|
+
return "UTF-8" if @url.scheme == "ws" || @url.scheme == "wss"
|
|
721
872
|
|
|
722
|
-
|
|
873
|
+
@encoding
|
|
723
874
|
end
|
|
724
875
|
|
|
725
|
-
|
|
876
|
+
def state_fragment(c)
|
|
877
|
+
@url.fragment += pe(c, method(:fragment_set?)) if c
|
|
878
|
+
end
|
|
726
879
|
end
|
|
727
880
|
end
|
|
728
881
|
end
|