1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty5.handler.codec.http;
17
18 import io.netty5.util.CharsetUtil;
19 import io.netty5.util.internal.PlatformDependent;
20
21 import java.net.URI;
22 import java.net.URLDecoder;
23 import java.nio.charset.Charset;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29
30 import static io.netty5.util.internal.ObjectUtil.checkPositive;
31 import static io.netty5.util.internal.StringUtil.EMPTY_STRING;
32 import static io.netty5.util.internal.StringUtil.SPACE;
33 import static io.netty5.util.internal.StringUtil.decodeHexByte;
34 import static java.util.Objects.requireNonNull;
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 public class QueryStringDecoder {
64
65 private static final int DEFAULT_MAX_PARAMS = 1024;
66
67 private final Charset charset;
68 private final String uri;
69 private final int maxParams;
70 private final boolean semicolonIsNormalChar;
71 private int pathEndIdx;
72 private String path;
73 private Map<String, List<String>> params;
74
75
76
77
78
79 public QueryStringDecoder(String uri) {
80 this(uri, HttpConstants.DEFAULT_CHARSET);
81 }
82
83
84
85
86
87 public QueryStringDecoder(String uri, boolean hasPath) {
88 this(uri, HttpConstants.DEFAULT_CHARSET, hasPath);
89 }
90
91
92
93
94
95 public QueryStringDecoder(String uri, Charset charset) {
96 this(uri, charset, true);
97 }
98
99
100
101
102
103 public QueryStringDecoder(String uri, Charset charset, boolean hasPath) {
104 this(uri, charset, hasPath, DEFAULT_MAX_PARAMS);
105 }
106
107
108
109
110
111 public QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) {
112 this(uri, charset, hasPath, maxParams, false);
113 }
114
115
116
117
118
119 public QueryStringDecoder(String uri, Charset charset, boolean hasPath,
120 int maxParams, boolean semicolonIsNormalChar) {
121 this.uri = requireNonNull(uri, "uri");
122 this.charset = requireNonNull(charset, "charset");
123 this.maxParams = checkPositive(maxParams, "maxParams");
124 this.semicolonIsNormalChar = semicolonIsNormalChar;
125
126
127 pathEndIdx = hasPath ? -1 : 0;
128 }
129
130
131
132
133
134 public QueryStringDecoder(URI uri) {
135 this(uri, HttpConstants.DEFAULT_CHARSET);
136 }
137
138
139
140
141
142 public QueryStringDecoder(URI uri, Charset charset) {
143 this(uri, charset, DEFAULT_MAX_PARAMS);
144 }
145
146
147
148
149
150 public QueryStringDecoder(URI uri, Charset charset, int maxParams) {
151 this(uri, charset, maxParams, false);
152 }
153
154
155
156
157
158 public QueryStringDecoder(URI uri, Charset charset, int maxParams, boolean semicolonIsNormalChar) {
159 String rawPath = uri.getRawPath();
160 if (rawPath == null) {
161 rawPath = EMPTY_STRING;
162 }
163 String rawQuery = uri.getRawQuery();
164
165 this.uri = rawQuery == null? rawPath : rawPath + '?' + rawQuery;
166 this.charset = requireNonNull(charset, "charset");
167 this.maxParams = checkPositive(maxParams, "maxParams");
168 this.semicolonIsNormalChar = semicolonIsNormalChar;
169 pathEndIdx = rawPath.length();
170 }
171
172 @Override
173 public String toString() {
174 return uri();
175 }
176
177
178
179
180 public String uri() {
181 return uri;
182 }
183
184
185
186
187 public String path() {
188 if (path == null) {
189 path = decodeComponent(uri, 0, pathEndIdx(), charset, true);
190 }
191 return path;
192 }
193
194
195
196
197 public Map<String, List<String>> parameters() {
198 if (params == null) {
199 params = decodeParams(uri, pathEndIdx(), charset, maxParams, semicolonIsNormalChar);
200 }
201 return params;
202 }
203
204
205
206
207 public String rawPath() {
208 return uri.substring(0, pathEndIdx());
209 }
210
211
212
213
214 public String rawQuery() {
215 int start = pathEndIdx() + 1;
216 return start < uri.length() ? uri.substring(start) : EMPTY_STRING;
217 }
218
219 private int pathEndIdx() {
220 if (pathEndIdx == -1) {
221 pathEndIdx = findPathEndIndex(uri);
222 }
223 return pathEndIdx;
224 }
225
226 private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit,
227 boolean semicolonIsNormalChar) {
228 int len = s.length();
229 if (from >= len) {
230 return Collections.emptyMap();
231 }
232 if (s.charAt(from) == '?') {
233 from++;
234 }
235 Map<String, List<String>> params = new LinkedHashMap<>();
236 int nameStart = from;
237 int valueStart = -1;
238 int i;
239 loop:
240 for (i = from; i < len; i++) {
241 switch (s.charAt(i)) {
242 case '=':
243 if (nameStart == i) {
244 nameStart = i + 1;
245 } else if (valueStart < nameStart) {
246 valueStart = i + 1;
247 }
248 break;
249 case ';':
250 if (semicolonIsNormalChar) {
251 continue;
252 }
253
254 case '&':
255 if (addParam(s, nameStart, valueStart, i, params, charset)) {
256 paramsLimit--;
257 if (paramsLimit == 0) {
258 return params;
259 }
260 }
261 nameStart = i + 1;
262 break;
263 case '#':
264 break loop;
265 default:
266
267 }
268 }
269 addParam(s, nameStart, valueStart, i, params, charset);
270 return params;
271 }
272
273 private static boolean addParam(String s, int nameStart, int valueStart, int valueEnd,
274 Map<String, List<String>> params, Charset charset) {
275 if (nameStart >= valueEnd) {
276 return false;
277 }
278 if (valueStart <= nameStart) {
279 valueStart = valueEnd + 1;
280 }
281 String name = decodeComponent(s, nameStart, valueStart - 1, charset, false);
282 String value = decodeComponent(s, valueStart, valueEnd, charset, false);
283 List<String> values = params.computeIfAbsent(name, k -> new ArrayList<>(1));
284
285 values.add(value);
286 return true;
287 }
288
289
290
291
292
293
294
295
296
297
298
299
300 public static String decodeComponent(final String s) {
301 return decodeComponent(s, HttpConstants.DEFAULT_CHARSET);
302 }
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326 public static String decodeComponent(final String s, final Charset charset) {
327 if (s == null) {
328 return EMPTY_STRING;
329 }
330 return decodeComponent(s, 0, s.length(), charset, false);
331 }
332
333 private static String decodeComponent(String s, int from, int toExcluded, Charset charset, boolean isPath) {
334 int len = toExcluded - from;
335 if (len <= 0) {
336 return EMPTY_STRING;
337 }
338 int firstEscaped = -1;
339 for (int i = from; i < toExcluded; i++) {
340 char c = s.charAt(i);
341 if (c == '%' || c == '+' && !isPath) {
342 firstEscaped = i;
343 break;
344 }
345 }
346 if (firstEscaped == -1) {
347 return s.substring(from, toExcluded);
348 }
349
350
351 int decodedCapacity = (toExcluded - firstEscaped) / 3;
352 byte[] buf = PlatformDependent.allocateUninitializedArray(decodedCapacity);
353 int bufIdx;
354
355 StringBuilder strBuf = new StringBuilder(len);
356 strBuf.append(s, from, firstEscaped);
357
358 for (int i = firstEscaped; i < toExcluded; i++) {
359 char c = s.charAt(i);
360 if (c != '%') {
361 strBuf.append(c != '+' || isPath? c : SPACE);
362 continue;
363 }
364
365 bufIdx = 0;
366 do {
367 if (i + 3 > toExcluded) {
368 throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s);
369 }
370 buf[bufIdx++] = decodeHexByte(s, i + 1);
371 i += 3;
372 } while (i < toExcluded && s.charAt(i) == '%');
373 i--;
374
375 strBuf.append(new String(buf, 0, bufIdx, charset));
376 }
377 return strBuf.toString();
378 }
379
380 private static int findPathEndIndex(String uri) {
381 int len = uri.length();
382 for (int i = 0; i < len; i++) {
383 char c = uri.charAt(i);
384 if (c == '?' || c == '#') {
385 return i;
386 }
387 }
388 return len;
389 }
390 }