rack 1.4.7 → 2.1.4
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 +5 -5
- data/CHANGELOG.md +77 -0
- data/{COPYING → MIT-LICENSE} +4 -2
- data/README.rdoc +122 -456
- data/Rakefile +32 -31
- data/SPEC +119 -29
- data/bin/rackup +1 -0
- data/contrib/rack_logo.svg +164 -111
- data/example/lobster.ru +2 -0
- data/example/protectedlobster.rb +4 -2
- data/example/protectedlobster.ru +3 -1
- data/lib/rack/auth/abstract/handler.rb +7 -5
- data/lib/rack/auth/abstract/request.rb +8 -6
- data/lib/rack/auth/basic.rb +5 -2
- data/lib/rack/auth/digest/md5.rb +10 -8
- data/lib/rack/auth/digest/nonce.rb +6 -3
- data/lib/rack/auth/digest/params.rb +5 -4
- data/lib/rack/auth/digest/request.rb +4 -2
- data/lib/rack/body_proxy.rb +11 -9
- data/lib/rack/builder.rb +63 -20
- data/lib/rack/cascade.rb +10 -9
- data/lib/rack/chunked.rb +45 -11
- data/lib/rack/{commonlogger.rb → common_logger.rb} +24 -15
- data/lib/rack/{conditionalget.rb → conditional_get.rb} +20 -6
- data/lib/rack/config.rb +7 -0
- data/lib/rack/content_length.rb +12 -6
- data/lib/rack/content_type.rb +4 -2
- data/lib/rack/core_ext/regexp.rb +14 -0
- data/lib/rack/deflater.rb +73 -42
- data/lib/rack/directory.rb +77 -56
- data/lib/rack/etag.rb +25 -13
- data/lib/rack/events.rb +156 -0
- data/lib/rack/file.rb +4 -143
- data/lib/rack/files.rb +178 -0
- data/lib/rack/handler/cgi.rb +18 -17
- data/lib/rack/handler/fastcgi.rb +21 -17
- data/lib/rack/handler/lsws.rb +14 -12
- data/lib/rack/handler/scgi.rb +27 -21
- data/lib/rack/handler/thin.rb +19 -5
- data/lib/rack/handler/webrick.rb +66 -24
- data/lib/rack/handler.rb +29 -19
- data/lib/rack/head.rb +21 -14
- data/lib/rack/lint.rb +259 -65
- data/lib/rack/lobster.rb +17 -10
- data/lib/rack/lock.rb +19 -10
- data/lib/rack/logger.rb +4 -2
- data/lib/rack/media_type.rb +43 -0
- data/lib/rack/method_override.rb +52 -0
- data/lib/rack/mime.rb +43 -6
- data/lib/rack/mock.rb +109 -44
- data/lib/rack/multipart/generator.rb +11 -12
- data/lib/rack/multipart/parser.rb +302 -115
- data/lib/rack/multipart/uploaded_file.rb +4 -3
- data/lib/rack/multipart.rb +40 -9
- data/lib/rack/null_logger.rb +39 -0
- data/lib/rack/query_parser.rb +218 -0
- data/lib/rack/recursive.rb +14 -11
- data/lib/rack/reloader.rb +12 -5
- data/lib/rack/request.rb +484 -270
- data/lib/rack/response.rb +196 -77
- data/lib/rack/rewindable_input.rb +5 -14
- data/lib/rack/runtime.rb +13 -6
- data/lib/rack/sendfile.rb +44 -20
- data/lib/rack/server.rb +175 -61
- data/lib/rack/session/abstract/id.rb +276 -133
- data/lib/rack/session/cookie.rb +75 -40
- data/lib/rack/session/memcache.rb +4 -87
- data/lib/rack/session/pool.rb +24 -18
- data/lib/rack/show_exceptions.rb +392 -0
- data/lib/rack/{showstatus.rb → show_status.rb} +11 -9
- data/lib/rack/static.rb +65 -38
- data/lib/rack/tempfile_reaper.rb +24 -0
- data/lib/rack/urlmap.rb +40 -15
- data/lib/rack/utils.rb +316 -285
- data/lib/rack.rb +78 -23
- data/rack.gemspec +26 -19
- metadata +44 -209
- data/KNOWN-ISSUES +0 -30
- data/lib/rack/backports/uri/common_18.rb +0 -56
- data/lib/rack/backports/uri/common_192.rb +0 -52
- data/lib/rack/backports/uri/common_193.rb +0 -29
- data/lib/rack/handler/evented_mongrel.rb +0 -8
- data/lib/rack/handler/mongrel.rb +0 -100
- data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
- data/lib/rack/methodoverride.rb +0 -33
- data/lib/rack/nulllogger.rb +0 -18
- data/lib/rack/showexceptions.rb +0 -378
- data/test/builder/anything.rb +0 -5
- data/test/builder/comment.ru +0 -4
- data/test/builder/end.ru +0 -5
- data/test/builder/line.ru +0 -1
- data/test/builder/options.ru +0 -2
- data/test/cgi/assets/folder/test.js +0 -1
- data/test/cgi/assets/fonts/font.eot +0 -1
- data/test/cgi/assets/images/image.png +0 -1
- data/test/cgi/assets/index.html +0 -1
- data/test/cgi/assets/javascripts/app.js +0 -1
- data/test/cgi/assets/stylesheets/app.css +0 -1
- data/test/cgi/lighttpd.conf +0 -26
- data/test/cgi/lighttpd.errors +0 -1
- data/test/cgi/rackup_stub.rb +0 -6
- data/test/cgi/sample_rackup.ru +0 -5
- data/test/cgi/test +0 -9
- data/test/cgi/test+directory/test+file +0 -1
- data/test/cgi/test.fcgi +0 -8
- data/test/cgi/test.ru +0 -5
- data/test/gemloader.rb +0 -10
- data/test/multipart/bad_robots +0 -259
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +0 -6
- data/test/multipart/empty +0 -10
- data/test/multipart/fail_16384_nofile +0 -814
- data/test/multipart/file1.txt +0 -1
- data/test/multipart/filename_and_modification_param +0 -7
- data/test/multipart/filename_with_escaped_quotes +0 -6
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
- data/test/multipart/filename_with_percent_escaped_quotes +0 -6
- data/test/multipart/filename_with_unescaped_percentages +0 -6
- data/test/multipart/filename_with_unescaped_percentages2 +0 -6
- data/test/multipart/filename_with_unescaped_percentages3 +0 -6
- data/test/multipart/filename_with_unescaped_quotes +0 -6
- data/test/multipart/ie +0 -6
- data/test/multipart/mixed_files +0 -21
- data/test/multipart/nested +0 -10
- data/test/multipart/none +0 -9
- data/test/multipart/semicolon +0 -6
- data/test/multipart/text +0 -15
- data/test/multipart/three_files_three_fields +0 -31
- data/test/multipart/webkit +0 -32
- data/test/rackup/config.ru +0 -31
- data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
- data/test/spec_auth.rb +0 -57
- data/test/spec_auth_basic.rb +0 -81
- data/test/spec_auth_digest.rb +0 -259
- data/test/spec_body_proxy.rb +0 -69
- data/test/spec_builder.rb +0 -207
- data/test/spec_cascade.rb +0 -61
- data/test/spec_cgi.rb +0 -102
- data/test/spec_chunked.rb +0 -87
- data/test/spec_commonlogger.rb +0 -57
- data/test/spec_conditionalget.rb +0 -102
- data/test/spec_config.rb +0 -22
- data/test/spec_content_length.rb +0 -86
- data/test/spec_content_type.rb +0 -45
- data/test/spec_deflater.rb +0 -187
- data/test/spec_directory.rb +0 -88
- data/test/spec_etag.rb +0 -98
- data/test/spec_fastcgi.rb +0 -107
- data/test/spec_file.rb +0 -200
- data/test/spec_handler.rb +0 -59
- data/test/spec_head.rb +0 -48
- data/test/spec_lint.rb +0 -515
- data/test/spec_lobster.rb +0 -58
- data/test/spec_lock.rb +0 -167
- data/test/spec_logger.rb +0 -23
- data/test/spec_methodoverride.rb +0 -72
- data/test/spec_mock.rb +0 -269
- data/test/spec_mongrel.rb +0 -182
- data/test/spec_multipart.rb +0 -479
- data/test/spec_nulllogger.rb +0 -23
- data/test/spec_recursive.rb +0 -72
- data/test/spec_request.rb +0 -955
- data/test/spec_response.rb +0 -313
- data/test/spec_rewindable_input.rb +0 -118
- data/test/spec_runtime.rb +0 -49
- data/test/spec_sendfile.rb +0 -90
- data/test/spec_server.rb +0 -121
- data/test/spec_session_abstract_id.rb +0 -43
- data/test/spec_session_cookie.rb +0 -361
- data/test/spec_session_memcache.rb +0 -321
- data/test/spec_session_pool.rb +0 -209
- data/test/spec_showexceptions.rb +0 -92
- data/test/spec_showstatus.rb +0 -84
- data/test/spec_static.rb +0 -145
- data/test/spec_thin.rb +0 -86
- data/test/spec_urlmap.rb +0 -213
- data/test/spec_utils.rb +0 -554
- data/test/spec_webrick.rb +0 -143
- data/test/static/another/index.html +0 -1
- data/test/static/index.html +0 -1
- data/test/testrequest.rb +0 -78
- data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/utils.rb
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
# -*- encoding: binary -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'uri'
|
|
2
5
|
require 'fileutils'
|
|
3
6
|
require 'set'
|
|
4
7
|
require 'tempfile'
|
|
5
|
-
require 'rack/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if major == 1 && minor < 9
|
|
10
|
-
require 'rack/backports/uri/common_18'
|
|
11
|
-
elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 320 && RUBY_ENGINE != 'jruby'
|
|
12
|
-
require 'rack/backports/uri/common_192'
|
|
13
|
-
elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
|
|
14
|
-
require 'rack/backports/uri/common_193'
|
|
15
|
-
else
|
|
16
|
-
require 'uri/common'
|
|
17
|
-
end
|
|
8
|
+
require 'rack/query_parser'
|
|
9
|
+
require 'time'
|
|
10
|
+
|
|
11
|
+
require_relative 'core_ext/regexp'
|
|
18
12
|
|
|
19
13
|
module Rack
|
|
20
14
|
# Rack::Utils contains a grab-bag of useful methods for writing web
|
|
21
15
|
# applications adopted from all kinds of Ruby libraries.
|
|
22
16
|
|
|
23
17
|
module Utils
|
|
18
|
+
using ::Rack::RegexpExtensions
|
|
19
|
+
|
|
20
|
+
ParameterTypeError = QueryParser::ParameterTypeError
|
|
21
|
+
InvalidParameterError = QueryParser::InvalidParameterError
|
|
22
|
+
DEFAULT_SEP = QueryParser::DEFAULT_SEP
|
|
23
|
+
COMMON_SEP = QueryParser::COMMON_SEP
|
|
24
|
+
KeySpaceConstrainedParams = QueryParser::Params
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
attr_accessor :default_query_parser
|
|
28
|
+
end
|
|
29
|
+
# The default number of bytes to allow parameter keys to take up.
|
|
30
|
+
# This helps prevent a rogue client from flooding a Request.
|
|
31
|
+
self.default_query_parser = QueryParser.make_default(65536, 100)
|
|
32
|
+
|
|
24
33
|
# URI escapes. (CGI style space to +)
|
|
25
34
|
def escape(s)
|
|
26
35
|
URI.encode_www_form_component(s)
|
|
@@ -30,125 +39,70 @@ module Rack
|
|
|
30
39
|
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
|
31
40
|
# true URI escaping.
|
|
32
41
|
def escape_path(s)
|
|
33
|
-
escape
|
|
42
|
+
::URI::DEFAULT_PARSER.escape s
|
|
34
43
|
end
|
|
35
44
|
module_function :escape_path
|
|
36
45
|
|
|
46
|
+
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
|
|
47
|
+
# unescaping query parameters or form components.
|
|
48
|
+
def unescape_path(s)
|
|
49
|
+
::URI::DEFAULT_PARSER.unescape s
|
|
50
|
+
end
|
|
51
|
+
module_function :unescape_path
|
|
52
|
+
|
|
53
|
+
|
|
37
54
|
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
|
38
55
|
# target encoding of the string returned, and it defaults to UTF-8
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
URI.decode_www_form_component(s, encoding)
|
|
42
|
-
end
|
|
43
|
-
else
|
|
44
|
-
def unescape(s, encoding = nil)
|
|
45
|
-
URI.decode_www_form_component(s, encoding)
|
|
46
|
-
end
|
|
56
|
+
def unescape(s, encoding = Encoding::UTF_8)
|
|
57
|
+
URI.decode_www_form_component(s, encoding)
|
|
47
58
|
end
|
|
48
59
|
module_function :unescape
|
|
49
60
|
|
|
50
|
-
DEFAULT_SEP = /[&;] */n
|
|
51
|
-
|
|
52
61
|
class << self
|
|
53
|
-
attr_accessor :key_space_limit
|
|
54
|
-
attr_accessor :param_depth_limit
|
|
55
62
|
attr_accessor :multipart_part_limit
|
|
56
63
|
end
|
|
57
64
|
|
|
58
|
-
# The
|
|
59
|
-
# This helps prevent a rogue client from flooding a Request.
|
|
60
|
-
self.key_space_limit = 65536
|
|
61
|
-
|
|
62
|
-
# Default depth at which the parameter parser will raise an exception for
|
|
63
|
-
# being too deep. This helps prevent SystemStackErrors
|
|
64
|
-
self.param_depth_limit = 100
|
|
65
|
-
#
|
|
66
|
-
# The maximum number of parts a request can contain. Accepting to many part
|
|
65
|
+
# The maximum number of parts a request can contain. Accepting too many part
|
|
67
66
|
# can lead to the server running out of file handles.
|
|
68
67
|
# Set to `0` for no limit.
|
|
69
68
|
self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# and ';' characters. You can also use this to parse
|
|
74
|
-
# cookies by changing the characters used in the second
|
|
75
|
-
# parameter (which defaults to '&;').
|
|
76
|
-
def parse_query(qs, d = nil, &unescaper)
|
|
77
|
-
unescaper ||= method(:unescape)
|
|
78
|
-
|
|
79
|
-
params = KeySpaceConstrainedParams.new
|
|
80
|
-
|
|
81
|
-
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
|
82
|
-
next if p.empty?
|
|
83
|
-
k, v = p.split('=', 2).map(&unescaper)
|
|
84
|
-
next unless k || v
|
|
85
|
-
|
|
86
|
-
if cur = params[k]
|
|
87
|
-
if cur.class == Array
|
|
88
|
-
params[k] << v
|
|
89
|
-
else
|
|
90
|
-
params[k] = [cur, v]
|
|
91
|
-
end
|
|
92
|
-
else
|
|
93
|
-
params[k] = v
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
return params.to_params_hash
|
|
70
|
+
def self.param_depth_limit
|
|
71
|
+
default_query_parser.param_depth_limit
|
|
98
72
|
end
|
|
99
|
-
module_function :parse_query
|
|
100
|
-
|
|
101
|
-
def parse_nested_query(qs, d = nil)
|
|
102
|
-
params = KeySpaceConstrainedParams.new
|
|
103
73
|
|
|
104
|
-
|
|
105
|
-
|
|
74
|
+
def self.param_depth_limit=(v)
|
|
75
|
+
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
|
|
76
|
+
end
|
|
106
77
|
|
|
107
|
-
|
|
108
|
-
|
|
78
|
+
def self.key_space_limit
|
|
79
|
+
default_query_parser.key_space_limit
|
|
80
|
+
end
|
|
109
81
|
|
|
110
|
-
|
|
82
|
+
def self.key_space_limit=(v)
|
|
83
|
+
self.default_query_parser = self.default_query_parser.new_space_limit(v)
|
|
111
84
|
end
|
|
112
|
-
module_function :parse_nested_query
|
|
113
85
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return if k.empty?
|
|
122
|
-
|
|
123
|
-
if after == ""
|
|
124
|
-
params[k] = v
|
|
125
|
-
elsif after == "[]"
|
|
126
|
-
params[k] ||= []
|
|
127
|
-
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
|
128
|
-
params[k] << v
|
|
129
|
-
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
|
|
130
|
-
child_key = $1
|
|
131
|
-
params[k] ||= []
|
|
132
|
-
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
|
133
|
-
if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
|
|
134
|
-
normalize_params(params[k].last, child_key, v, depth - 1)
|
|
135
|
-
else
|
|
136
|
-
params[k] << normalize_params(params.class.new, child_key, v, depth - 1)
|
|
137
|
-
end
|
|
138
|
-
else
|
|
139
|
-
params[k] ||= params.class.new
|
|
140
|
-
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
|
|
141
|
-
params[k] = normalize_params(params[k], after, v, depth - 1)
|
|
86
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
|
87
|
+
def clock_time
|
|
88
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
def clock_time
|
|
92
|
+
Time.now.to_f
|
|
142
93
|
end
|
|
94
|
+
end
|
|
95
|
+
module_function :clock_time
|
|
143
96
|
|
|
144
|
-
|
|
97
|
+
def parse_query(qs, d = nil, &unescaper)
|
|
98
|
+
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
|
|
145
99
|
end
|
|
146
|
-
module_function :
|
|
100
|
+
module_function :parse_query
|
|
147
101
|
|
|
148
|
-
def
|
|
149
|
-
|
|
102
|
+
def parse_nested_query(qs, d = nil)
|
|
103
|
+
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
|
|
150
104
|
end
|
|
151
|
-
module_function :
|
|
105
|
+
module_function :parse_nested_query
|
|
152
106
|
|
|
153
107
|
def build_query(params)
|
|
154
108
|
params.map { |k, v|
|
|
@@ -170,16 +124,42 @@ module Rack
|
|
|
170
124
|
when Hash
|
|
171
125
|
value.map { |k, v|
|
|
172
126
|
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
|
|
173
|
-
}.join(
|
|
174
|
-
when
|
|
127
|
+
}.delete_if(&:empty?).join('&')
|
|
128
|
+
when nil
|
|
129
|
+
prefix
|
|
130
|
+
else
|
|
175
131
|
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
|
176
132
|
"#{prefix}=#{escape(value)}"
|
|
177
|
-
else
|
|
178
|
-
prefix
|
|
179
133
|
end
|
|
180
134
|
end
|
|
181
135
|
module_function :build_nested_query
|
|
182
136
|
|
|
137
|
+
def q_values(q_value_header)
|
|
138
|
+
q_value_header.to_s.split(/\s*,\s*/).map do |part|
|
|
139
|
+
value, parameters = part.split(/\s*;\s*/, 2)
|
|
140
|
+
quality = 1.0
|
|
141
|
+
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
|
142
|
+
quality = md[1].to_f
|
|
143
|
+
end
|
|
144
|
+
[value, quality]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
module_function :q_values
|
|
148
|
+
|
|
149
|
+
def best_q_match(q_value_header, available_mimes)
|
|
150
|
+
values = q_values(q_value_header)
|
|
151
|
+
|
|
152
|
+
matches = values.map do |req_mime, quality|
|
|
153
|
+
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
|
|
154
|
+
next unless match
|
|
155
|
+
[match, quality]
|
|
156
|
+
end.compact.sort_by do |match, quality|
|
|
157
|
+
(match.split('/', 2).count('*') * -10) + quality
|
|
158
|
+
end.last
|
|
159
|
+
matches && matches.first
|
|
160
|
+
end
|
|
161
|
+
module_function :best_q_match
|
|
162
|
+
|
|
183
163
|
ESCAPE_HTML = {
|
|
184
164
|
"&" => "&",
|
|
185
165
|
"<" => "<",
|
|
@@ -188,13 +168,8 @@ module Rack
|
|
|
188
168
|
'"' => """,
|
|
189
169
|
"/" => "/"
|
|
190
170
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
else
|
|
194
|
-
# On 1.8, there is a kcode = 'u' bug that allows for XSS otherwhise
|
|
195
|
-
# TODO doesn't apply to jruby, so a better condition above might be preferable?
|
|
196
|
-
ESCAPE_HTML_PATTERN = /#{Regexp.union(*ESCAPE_HTML.keys)}/n
|
|
197
|
-
end
|
|
171
|
+
|
|
172
|
+
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
|
|
198
173
|
|
|
199
174
|
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
|
200
175
|
def escape_html(string)
|
|
@@ -206,133 +181,177 @@ module Rack
|
|
|
206
181
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
|
207
182
|
|
|
208
183
|
expanded_accept_encoding =
|
|
209
|
-
accept_encoding.
|
|
184
|
+
accept_encoding.each_with_object([]) do |(m, q), list|
|
|
210
185
|
if m == "*"
|
|
211
|
-
(available_encodings - accept_encoding.map
|
|
186
|
+
(available_encodings - accept_encoding.map(&:first))
|
|
187
|
+
.each { |m2| list << [m2, q] }
|
|
212
188
|
else
|
|
213
|
-
[
|
|
189
|
+
list << [m, q]
|
|
214
190
|
end
|
|
215
|
-
|
|
216
|
-
mem + list
|
|
217
|
-
}
|
|
191
|
+
end
|
|
218
192
|
|
|
219
|
-
encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map
|
|
193
|
+
encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map!(&:first)
|
|
220
194
|
|
|
221
195
|
unless encoding_candidates.include?("identity")
|
|
222
196
|
encoding_candidates.push("identity")
|
|
223
197
|
end
|
|
224
198
|
|
|
225
|
-
expanded_accept_encoding.
|
|
226
|
-
q == 0.0
|
|
227
|
-
|
|
228
|
-
encoding_candidates.delete(m)
|
|
229
|
-
}
|
|
199
|
+
expanded_accept_encoding.each do |m, q|
|
|
200
|
+
encoding_candidates.delete(m) if q == 0.0
|
|
201
|
+
end
|
|
230
202
|
|
|
231
|
-
|
|
203
|
+
(encoding_candidates & available_encodings)[0]
|
|
232
204
|
end
|
|
233
205
|
module_function :select_best_encoding
|
|
234
206
|
|
|
235
|
-
def
|
|
207
|
+
def parse_cookies(env)
|
|
208
|
+
parse_cookies_header env[HTTP_COOKIE]
|
|
209
|
+
end
|
|
210
|
+
module_function :parse_cookies
|
|
211
|
+
|
|
212
|
+
def parse_cookies_header(header)
|
|
213
|
+
# According to RFC 2109:
|
|
214
|
+
# If multiple cookies satisfy the criteria above, they are ordered in
|
|
215
|
+
# the Cookie header such that those with more specific Path attributes
|
|
216
|
+
# precede those with less specific. Ordering with respect to other
|
|
217
|
+
# attributes (e.g., Domain) is unspecified.
|
|
218
|
+
return {} unless header
|
|
219
|
+
header.split(/[;,] */n).each_with_object({}) do |cookie, cookies|
|
|
220
|
+
next if cookie.empty?
|
|
221
|
+
key, value = cookie.split('=', 2)
|
|
222
|
+
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
module_function :parse_cookies_header
|
|
226
|
+
|
|
227
|
+
def add_cookie_to_header(header, key, value)
|
|
236
228
|
case value
|
|
237
229
|
when Hash
|
|
238
|
-
domain = "; domain
|
|
239
|
-
path = "; path
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
expires = "; expires=" +
|
|
243
|
-
rfc2822(value[:expires].clone.gmtime) if value[:expires]
|
|
230
|
+
domain = "; domain=#{value[:domain]}" if value[:domain]
|
|
231
|
+
path = "; path=#{value[:path]}" if value[:path]
|
|
232
|
+
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
|
233
|
+
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
|
244
234
|
secure = "; secure" if value[:secure]
|
|
245
|
-
httponly = "; HttpOnly" if value[:httponly]
|
|
235
|
+
httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
|
236
|
+
same_site =
|
|
237
|
+
case value[:same_site]
|
|
238
|
+
when false, nil
|
|
239
|
+
nil
|
|
240
|
+
when :none, 'None', :None
|
|
241
|
+
'; SameSite=None'
|
|
242
|
+
when :lax, 'Lax', :Lax
|
|
243
|
+
'; SameSite=Lax'
|
|
244
|
+
when true, :strict, 'Strict', :Strict
|
|
245
|
+
'; SameSite=Strict'
|
|
246
|
+
else
|
|
247
|
+
raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
|
|
248
|
+
end
|
|
246
249
|
value = value[:value]
|
|
247
250
|
end
|
|
248
251
|
value = [value] unless Array === value
|
|
249
|
-
cookie = escape(key) + "=" +
|
|
250
|
-
value.map { |v| escape v }.join("&") +
|
|
251
|
-
"#{domain}#{path}#{expires}#{secure}#{httponly}"
|
|
252
252
|
|
|
253
|
-
|
|
253
|
+
cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
|
254
|
+
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
|
|
255
|
+
|
|
256
|
+
case header
|
|
254
257
|
when nil, ''
|
|
255
|
-
|
|
258
|
+
cookie
|
|
256
259
|
when String
|
|
257
|
-
|
|
260
|
+
[header, cookie].join("\n")
|
|
258
261
|
when Array
|
|
259
|
-
|
|
262
|
+
(header + [cookie]).join("\n")
|
|
263
|
+
else
|
|
264
|
+
raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
|
|
260
265
|
end
|
|
266
|
+
end
|
|
267
|
+
module_function :add_cookie_to_header
|
|
261
268
|
|
|
269
|
+
def set_cookie_header!(header, key, value)
|
|
270
|
+
header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
|
|
262
271
|
nil
|
|
263
272
|
end
|
|
264
273
|
module_function :set_cookie_header!
|
|
265
274
|
|
|
266
|
-
def
|
|
267
|
-
case header
|
|
275
|
+
def make_delete_cookie_header(header, key, value)
|
|
276
|
+
case header
|
|
268
277
|
when nil, ''
|
|
269
278
|
cookies = []
|
|
270
279
|
when String
|
|
271
|
-
cookies = header
|
|
280
|
+
cookies = header.split("\n")
|
|
272
281
|
when Array
|
|
273
|
-
cookies = header
|
|
282
|
+
cookies = header
|
|
274
283
|
end
|
|
275
284
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
end
|
|
284
|
-
}
|
|
285
|
+
regexp = if value[:domain]
|
|
286
|
+
/\A#{escape(key)}=.*domain=#{value[:domain]}/
|
|
287
|
+
elsif value[:path]
|
|
288
|
+
/\A#{escape(key)}=.*path=#{value[:path]}/
|
|
289
|
+
else
|
|
290
|
+
/\A#{escape(key)}=/
|
|
291
|
+
end
|
|
285
292
|
|
|
286
|
-
|
|
293
|
+
cookies.reject! { |cookie| regexp.match? cookie }
|
|
287
294
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
295
|
+
cookies.join("\n")
|
|
296
|
+
end
|
|
297
|
+
module_function :make_delete_cookie_header
|
|
291
298
|
|
|
299
|
+
def delete_cookie_header!(header, key, value = {})
|
|
300
|
+
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
|
|
292
301
|
nil
|
|
293
302
|
end
|
|
294
303
|
module_function :delete_cookie_header!
|
|
295
304
|
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
305
|
+
# Adds a cookie that will *remove* a cookie from the client. Hence the
|
|
306
|
+
# strange method name.
|
|
307
|
+
def add_remove_cookie_to_header(header, key, value = {})
|
|
308
|
+
new_header = make_delete_cookie_header(header, key, value)
|
|
309
|
+
|
|
310
|
+
add_cookie_to_header(new_header, key,
|
|
311
|
+
{ value: '', path: nil, domain: nil,
|
|
312
|
+
max_age: '0',
|
|
313
|
+
expires: Time.at(0) }.merge(value))
|
|
314
|
+
|
|
306
315
|
end
|
|
307
|
-
module_function :
|
|
316
|
+
module_function :add_remove_cookie_to_header
|
|
317
|
+
|
|
318
|
+
def rfc2822(time)
|
|
319
|
+
time.rfc2822
|
|
320
|
+
end
|
|
321
|
+
module_function :rfc2822
|
|
308
322
|
|
|
309
323
|
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
|
310
324
|
# of '% %b %Y'.
|
|
311
325
|
# It assumes that the time is in GMT to comply to the RFC 2109.
|
|
312
326
|
#
|
|
313
|
-
# NOTE: I'm not sure the RFC says it requires GMT, but is
|
|
327
|
+
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
|
314
328
|
# that I'm certain someone implemented only that option.
|
|
315
329
|
# Do not use %a and %b from Time.strptime, it would use localized names for
|
|
316
330
|
# weekday and month.
|
|
317
331
|
#
|
|
318
|
-
def
|
|
332
|
+
def rfc2109(time)
|
|
319
333
|
wday = Time::RFC2822_DAY_NAME[time.wday]
|
|
320
334
|
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
|
|
321
335
|
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
|
322
336
|
end
|
|
323
|
-
module_function :
|
|
337
|
+
module_function :rfc2109
|
|
324
338
|
|
|
325
339
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
|
326
340
|
# Returns nil if the header is missing or syntactically invalid.
|
|
327
341
|
# Returns an empty array if none of the ranges are satisfiable.
|
|
328
342
|
def byte_ranges(env, size)
|
|
343
|
+
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
|
344
|
+
get_byte_ranges env['HTTP_RANGE'], size
|
|
345
|
+
end
|
|
346
|
+
module_function :byte_ranges
|
|
347
|
+
|
|
348
|
+
def get_byte_ranges(http_range, size)
|
|
329
349
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
|
330
|
-
http_range = env['HTTP_RANGE']
|
|
331
350
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
|
332
351
|
ranges = []
|
|
333
352
|
$1.split(/,\s*/).each do |range_spec|
|
|
334
353
|
return nil unless range_spec =~ /(\d*)-(\d*)/
|
|
335
|
-
r0,r1 = $1, $2
|
|
354
|
+
r0, r1 = $1, $2
|
|
336
355
|
if r0.empty?
|
|
337
356
|
return nil if r1.empty?
|
|
338
357
|
# suffix-byte-range-spec, represents trailing suffix of file
|
|
@@ -346,23 +365,28 @@ module Rack
|
|
|
346
365
|
else
|
|
347
366
|
r1 = r1.to_i
|
|
348
367
|
return nil if r1 < r0 # backwards range is syntactically invalid
|
|
349
|
-
r1 = size-1 if r1 >= size
|
|
368
|
+
r1 = size - 1 if r1 >= size
|
|
350
369
|
end
|
|
351
370
|
end
|
|
352
371
|
ranges << (r0..r1) if r0 <= r1
|
|
353
372
|
end
|
|
354
373
|
ranges
|
|
355
374
|
end
|
|
356
|
-
module_function :
|
|
375
|
+
module_function :get_byte_ranges
|
|
357
376
|
|
|
358
377
|
# Constant time string comparison.
|
|
378
|
+
#
|
|
379
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
|
380
|
+
# that have already been processed by HMAC. This should not be used
|
|
381
|
+
# on variable length plaintext strings because it could leak length info
|
|
382
|
+
# via timing attacks.
|
|
359
383
|
def secure_compare(a, b)
|
|
360
|
-
return false unless bytesize
|
|
384
|
+
return false unless a.bytesize == b.bytesize
|
|
361
385
|
|
|
362
386
|
l = a.unpack("C*")
|
|
363
387
|
|
|
364
388
|
r, i = 0, -1
|
|
365
|
-
b.each_byte { |v| r |= v ^ l[i+=1] }
|
|
389
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
|
366
390
|
r == 0
|
|
367
391
|
end
|
|
368
392
|
module_function :secure_compare
|
|
@@ -388,24 +412,28 @@ module Rack
|
|
|
388
412
|
self.class.new(@for, app)
|
|
389
413
|
end
|
|
390
414
|
|
|
391
|
-
def context(env, app
|
|
415
|
+
def context(env, app = @app)
|
|
392
416
|
recontext(app).call(env)
|
|
393
417
|
end
|
|
394
418
|
end
|
|
395
419
|
|
|
396
420
|
# A case-insensitive Hash that preserves the original case of a
|
|
397
421
|
# header when set.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
def initialize(hash={})
|
|
422
|
+
#
|
|
423
|
+
# @api private
|
|
424
|
+
class HeaderHash < Hash # :nodoc:
|
|
425
|
+
def initialize(hash = {})
|
|
404
426
|
super()
|
|
405
427
|
@names = {}
|
|
406
428
|
hash.each { |k, v| self[k] = v }
|
|
407
429
|
end
|
|
408
430
|
|
|
431
|
+
# on dup/clone, we need to duplicate @names hash
|
|
432
|
+
def initialize_copy(other)
|
|
433
|
+
super
|
|
434
|
+
@names = other.names.dup
|
|
435
|
+
end
|
|
436
|
+
|
|
409
437
|
def each
|
|
410
438
|
super do |k, v|
|
|
411
439
|
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
|
@@ -414,7 +442,7 @@ module Rack
|
|
|
414
442
|
|
|
415
443
|
def to_hash
|
|
416
444
|
hash = {}
|
|
417
|
-
each { |k,v| hash[k] = v }
|
|
445
|
+
each { |k, v| hash[k] = v }
|
|
418
446
|
hash
|
|
419
447
|
end
|
|
420
448
|
|
|
@@ -423,21 +451,20 @@ module Rack
|
|
|
423
451
|
end
|
|
424
452
|
|
|
425
453
|
def []=(k, v)
|
|
426
|
-
canonical = k.downcase
|
|
454
|
+
canonical = k.downcase.freeze
|
|
427
455
|
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
|
428
|
-
@names[
|
|
456
|
+
@names[canonical] = k
|
|
429
457
|
super k, v
|
|
430
458
|
end
|
|
431
459
|
|
|
432
460
|
def delete(k)
|
|
433
461
|
canonical = k.downcase
|
|
434
462
|
result = super @names.delete(canonical)
|
|
435
|
-
@names.delete_if { |name,| name.downcase == canonical }
|
|
436
463
|
result
|
|
437
464
|
end
|
|
438
465
|
|
|
439
466
|
def include?(k)
|
|
440
|
-
|
|
467
|
+
super || @names.include?(k.downcase)
|
|
441
468
|
end
|
|
442
469
|
|
|
443
470
|
alias_method :has_key?, :include?
|
|
@@ -459,120 +486,124 @@ module Rack
|
|
|
459
486
|
other.each { |k, v| self[k] = v }
|
|
460
487
|
self
|
|
461
488
|
end
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
class KeySpaceConstrainedParams
|
|
465
|
-
def initialize(limit = Utils.key_space_limit)
|
|
466
|
-
@limit = limit
|
|
467
|
-
@size = 0
|
|
468
|
-
@params = {}
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
def [](key)
|
|
472
|
-
@params[key]
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
def []=(key, value)
|
|
476
|
-
@size += key.size if key && !@params.key?(key)
|
|
477
|
-
raise RangeError, 'exceeded available parameter key space' if @size > @limit
|
|
478
|
-
@params[key] = value
|
|
479
|
-
end
|
|
480
489
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def to_params_hash
|
|
486
|
-
hash = @params
|
|
487
|
-
hash.keys.each do |key|
|
|
488
|
-
value = hash[key]
|
|
489
|
-
if value.kind_of?(self.class)
|
|
490
|
-
hash[key] = value.to_params_hash
|
|
491
|
-
elsif value.kind_of?(Array)
|
|
492
|
-
value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
|
|
493
|
-
end
|
|
490
|
+
protected
|
|
491
|
+
def names
|
|
492
|
+
@names
|
|
494
493
|
end
|
|
495
|
-
hash
|
|
496
|
-
end
|
|
497
494
|
end
|
|
498
495
|
|
|
499
496
|
# Every standard HTTP code mapped to the appropriate message.
|
|
500
497
|
# Generated with:
|
|
501
|
-
# curl -s
|
|
502
|
-
# ruby -
|
|
503
|
-
#
|
|
498
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
|
|
499
|
+
# ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
|
|
500
|
+
# puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
|
|
504
501
|
HTTP_STATUS_CODES = {
|
|
505
|
-
100
|
|
506
|
-
101
|
|
507
|
-
102
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
502
|
+
100 => 'Continue',
|
|
503
|
+
101 => 'Switching Protocols',
|
|
504
|
+
102 => 'Processing',
|
|
505
|
+
103 => 'Early Hints',
|
|
506
|
+
200 => 'OK',
|
|
507
|
+
201 => 'Created',
|
|
508
|
+
202 => 'Accepted',
|
|
509
|
+
203 => 'Non-Authoritative Information',
|
|
510
|
+
204 => 'No Content',
|
|
511
|
+
205 => 'Reset Content',
|
|
512
|
+
206 => 'Partial Content',
|
|
513
|
+
207 => 'Multi-Status',
|
|
514
|
+
208 => 'Already Reported',
|
|
515
|
+
226 => 'IM Used',
|
|
516
|
+
300 => 'Multiple Choices',
|
|
517
|
+
301 => 'Moved Permanently',
|
|
518
|
+
302 => 'Found',
|
|
519
|
+
303 => 'See Other',
|
|
520
|
+
304 => 'Not Modified',
|
|
521
|
+
305 => 'Use Proxy',
|
|
522
|
+
306 => '(Unused)',
|
|
523
|
+
307 => 'Temporary Redirect',
|
|
524
|
+
308 => 'Permanent Redirect',
|
|
525
|
+
400 => 'Bad Request',
|
|
526
|
+
401 => 'Unauthorized',
|
|
527
|
+
402 => 'Payment Required',
|
|
528
|
+
403 => 'Forbidden',
|
|
529
|
+
404 => 'Not Found',
|
|
530
|
+
405 => 'Method Not Allowed',
|
|
531
|
+
406 => 'Not Acceptable',
|
|
532
|
+
407 => 'Proxy Authentication Required',
|
|
533
|
+
408 => 'Request Timeout',
|
|
534
|
+
409 => 'Conflict',
|
|
535
|
+
410 => 'Gone',
|
|
536
|
+
411 => 'Length Required',
|
|
537
|
+
412 => 'Precondition Failed',
|
|
538
|
+
413 => 'Payload Too Large',
|
|
539
|
+
414 => 'URI Too Long',
|
|
540
|
+
415 => 'Unsupported Media Type',
|
|
541
|
+
416 => 'Range Not Satisfiable',
|
|
542
|
+
417 => 'Expectation Failed',
|
|
543
|
+
421 => 'Misdirected Request',
|
|
544
|
+
422 => 'Unprocessable Entity',
|
|
545
|
+
423 => 'Locked',
|
|
546
|
+
424 => 'Failed Dependency',
|
|
547
|
+
425 => 'Too Early',
|
|
548
|
+
426 => 'Upgrade Required',
|
|
549
|
+
428 => 'Precondition Required',
|
|
550
|
+
429 => 'Too Many Requests',
|
|
551
|
+
431 => 'Request Header Fields Too Large',
|
|
552
|
+
451 => 'Unavailable for Legal Reasons',
|
|
553
|
+
500 => 'Internal Server Error',
|
|
554
|
+
501 => 'Not Implemented',
|
|
555
|
+
502 => 'Bad Gateway',
|
|
556
|
+
503 => 'Service Unavailable',
|
|
557
|
+
504 => 'Gateway Timeout',
|
|
558
|
+
505 => 'HTTP Version Not Supported',
|
|
559
|
+
506 => 'Variant Also Negotiates',
|
|
560
|
+
507 => 'Insufficient Storage',
|
|
561
|
+
508 => 'Loop Detected',
|
|
562
|
+
509 => 'Bandwidth Limit Exceeded',
|
|
563
|
+
510 => 'Not Extended',
|
|
564
|
+
511 => 'Network Authentication Required'
|
|
557
565
|
}
|
|
558
566
|
|
|
559
567
|
# Responses with HTTP status codes that should not have an entity body
|
|
560
|
-
STATUS_WITH_NO_ENTITY_BODY =
|
|
568
|
+
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
|
561
569
|
|
|
562
570
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
|
563
|
-
[message.downcase.gsub(/\s
|
|
571
|
+
[message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
|
|
564
572
|
}.flatten]
|
|
565
573
|
|
|
566
574
|
def status_code(status)
|
|
567
575
|
if status.is_a?(Symbol)
|
|
568
|
-
SYMBOL_TO_STATUS_CODE
|
|
576
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
|
569
577
|
else
|
|
570
578
|
status.to_i
|
|
571
579
|
end
|
|
572
580
|
end
|
|
573
581
|
module_function :status_code
|
|
574
582
|
|
|
575
|
-
|
|
583
|
+
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
|
|
584
|
+
|
|
585
|
+
def clean_path_info(path_info)
|
|
586
|
+
parts = path_info.split PATH_SEPS
|
|
587
|
+
|
|
588
|
+
clean = []
|
|
589
|
+
|
|
590
|
+
parts.each do |part|
|
|
591
|
+
next if part.empty? || part == '.'
|
|
592
|
+
part == '..' ? clean.pop : clean << part
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
clean.unshift '/' if parts.empty? || parts.first.empty?
|
|
596
|
+
|
|
597
|
+
::File.join clean
|
|
598
|
+
end
|
|
599
|
+
module_function :clean_path_info
|
|
600
|
+
|
|
601
|
+
NULL_BYTE = "\0"
|
|
602
|
+
|
|
603
|
+
def valid_path?(path)
|
|
604
|
+
path.valid_encoding? && !path.include?(NULL_BYTE)
|
|
605
|
+
end
|
|
606
|
+
module_function :valid_path?
|
|
576
607
|
|
|
577
608
|
end
|
|
578
609
|
end
|