1 # frozen_string_literal: true
3 require_relative 'bad_request'
9 COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n }
11 # ParameterTypeError is the error that is raised when incoming structural
12 # parameters (parsed by parse_nested_query) contain conflicting types.
13 class ParameterTypeError < TypeError
17 # InvalidParameterError is the error that is raised when incoming structural
18 # parameters (parsed by parse_nested_query) contain invalid format or byte
20 class InvalidParameterError < ArgumentError
24 # ParamsTooDeepError is the error that is raised when params are recursively
25 # nested over the specified limit.
26 class ParamsTooDeepError < RangeError
30 def self.make_default(param_depth_limit)
31 new Params, param_depth_limit
34 attr_reader :param_depth_limit
36 def initialize(params_class, param_depth_limit)
37 @params_class = params_class
38 @param_depth_limit = param_depth_limit
41 # Stolen from Mongrel, with some small modifications:
42 # Parses a query string by breaking it up at the '&'. You can also use this
43 # to parse cookies by changing the characters used in the second parameter
44 # (which defaults to '&').
45 def parse_query(qs, separator = nil, &unescaper)
46 unescaper ||= method(:unescape)
50 (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
52 k, v = p.split('=', 2).map!(&unescaper)
68 # parse_nested_query expands a query string into structural types. Supported
69 # types are Arrays, Hashes and basic value types. It is possible to supply
70 # query strings with parameters of conflicting types, in this case a
71 # ParameterTypeError is raised. Users are encouraged to return a 400 in this
73 def parse_nested_query(qs, separator = nil)
76 unless qs.nil? || qs.empty?
77 (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
78 k, v = p.split('=', 2).map! { |s| unescape(s) }
80 _normalize_params(params, k, v, 0)
85 rescue ArgumentError => e
86 raise InvalidParameterError, e.message, e.backtrace
89 # normalize_params recursively expands parameters into structural types. If
90 # the structural types represented by two different parameter names are in
91 # conflict, a ParameterTypeError is raised. The depth argument is deprecated
92 # and should no longer be used, it is kept for backwards compatibility with
93 # earlier versions of rack.
94 def normalize_params(params, name, v, _depth=nil)
95 _normalize_params(params, name, v, 0)
98 private def _normalize_params(params, name, v, depth)
99 raise ParamsTooDeepError if depth >= param_depth_limit
102 # nil name, treat same as empty string (required by tests)
105 # Start of parsing, don't treat [] or [ at start of string specially
106 if start = name.index('[', 1)
107 # Start of parameter nesting, use part before brackets as key
109 after = name[start, name.length]
111 # Plain parameter with no nesting
115 elsif name.start_with?('[]')
118 after = name[2, name.length]
119 elsif name.start_with?('[') && (start = name.index(']', 1))
120 # Hash nesting, use the part inside brackets as the key
122 after = name[start+1, name.length]
124 # Probably malformed input, nested but not starting with [
125 # treat full name as key for backwards compatibility.
133 if k == '[]' && depth != 0
142 raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
144 elsif after.start_with?('[]')
145 # Recognize x[][y] (hash inside array) parameters
146 unless after[2] == '[' && after.end_with?(']') && (child_key = after[3, after.length-4]) && !child_key.empty? && !child_key.index('[') && !child_key.index(']')
147 # Handle other nested array parameters
148 child_key = after[2, after.length]
151 raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
152 if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
153 _normalize_params(params[k].last, child_key, v, depth + 1)
155 params[k] << _normalize_params(make_params, child_key, v, depth + 1)
158 params[k] ||= make_params
159 raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
160 params[k] = _normalize_params(params[k], after, v, depth + 1)
170 def new_depth_limit(param_depth_limit)
171 self.class.new @params_class, param_depth_limit
176 def params_hash_type?(obj)
177 obj.kind_of?(@params_class)
180 def params_hash_has_key?(hash, key)
181 return false if /\[\]/.match?(key)
183 key.split(/[\[\]]+/).inject(hash) do |h, part|
185 return false unless params_hash_type?(h) && h.key?(part)
192 def unescape(string, encoding = Encoding::UTF_8)
193 URI.decode_www_form_component(string, encoding)
197 alias_method :to_params_hash, :to_h