fix: malformed charset param (#2263)
[rack.git] / lib / rack / query_parser.rb
blob28cbce18fc8076fb2aed9d59c79badea5174fd58
1 # frozen_string_literal: true
3 require_relative 'bad_request'
4 require 'uri'
6 module Rack
7   class QueryParser
8     DEFAULT_SEP = /& */n
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
14       include BadRequest
15     end
17     # InvalidParameterError is the error that is raised when incoming structural
18     # parameters (parsed by parse_nested_query) contain invalid format or byte
19     # sequence.
20     class InvalidParameterError < ArgumentError
21       include BadRequest
22     end
24     # ParamsTooDeepError is the error that is raised when params are recursively
25     # nested over the specified limit.
26     class ParamsTooDeepError < RangeError
27       include BadRequest
28     end
30     def self.make_default(param_depth_limit)
31       new Params, param_depth_limit
32     end
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
39     end
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)
48       params = make_params
50       (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
51         next if p.empty?
52         k, v = p.split('=', 2).map!(&unescaper)
54         if cur = params[k]
55           if cur.class == Array
56             params[k] << v
57           else
58             params[k] = [cur, v]
59           end
60         else
61           params[k] = v
62         end
63       end
65       return params.to_h
66     end
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
72     # case.
73     def parse_nested_query(qs, separator = nil)
74       params = make_params
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)
81         end
82       end
84       return params.to_h
85     rescue ArgumentError => e
86       raise InvalidParameterError, e.message, e.backtrace
87     end
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)
96     end
98     private def _normalize_params(params, name, v, depth)
99       raise ParamsTooDeepError if depth >= param_depth_limit
101       if !name
102         # nil name, treat same as empty string (required by tests)
103         k = after = ''
104       elsif depth == 0
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
108           k = name[0, start]
109           after = name[start, name.length]
110         else
111           # Plain parameter with no nesting
112           k = name
113           after = ''
114         end
115       elsif name.start_with?('[]')
116         # Array nesting
117         k = '[]'
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
121         k = name[1, start-1]
122         after = name[start+1, name.length]
123       else
124         # Probably malformed input, nested but not starting with [
125         # treat full name as key for backwards compatibility.
126         k = name
127         after = ''
128       end
130       return if k.empty?
132       if after == ''
133         if k == '[]' && depth != 0
134           return [v]
135         else
136           params[k] = v
137         end
138       elsif after == "["
139         params[name] = v
140       elsif after == "[]"
141         params[k] ||= []
142         raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
143         params[k] << v
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]
149         end
150         params[k] ||= []
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)
154         else
155           params[k] << _normalize_params(make_params, child_key, v, depth + 1)
156         end
157       else
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)
161       end
163       params
164     end
166     def make_params
167       @params_class.new
168     end
170     def new_depth_limit(param_depth_limit)
171       self.class.new @params_class, param_depth_limit
172     end
174     private
176     def params_hash_type?(obj)
177       obj.kind_of?(@params_class)
178     end
180     def params_hash_has_key?(hash, key)
181       return false if /\[\]/.match?(key)
183       key.split(/[\[\]]+/).inject(hash) do |h, part|
184         next h if part == ''
185         return false unless params_hash_type?(h) && h.key?(part)
186         h[part]
187       end
189       true
190     end
192     def unescape(string, encoding = Encoding::UTF_8)
193       URI.decode_www_form_component(string, encoding)
194     end
196     class Params < Hash
197       alias_method :to_params_hash, :to_h
198     end
199   end