1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty.handler.codec.http;
17
18 import io.netty.util.CharsetUtil;
19 import io.netty.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.netty.util.internal.ObjectUtil.checkNotNull;
31 import static io.netty.util.internal.ObjectUtil.checkPositive;
32 import static io.netty.util.internal.StringUtil.EMPTY_STRING;
33 import static io.netty.util.internal.StringUtil.SPACE;
34 import static io.netty.util.internal.StringUtil.decodeHexByte;
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 final boolean htmlQueryDecoding;
72 private int pathEndIdx;
73 private String path;
74 private Map<String, List<String>> params;
75
76
77
78
79
80 public QueryStringDecoder(String uri) {
81 this(builder(), uri);
82 }
83
84
85
86
87
88 public QueryStringDecoder(String uri, boolean hasPath) {
89 this(builder().hasPath(hasPath), uri);
90 }
91
92
93
94
95
96 public QueryStringDecoder(String uri, Charset charset) {
97 this(builder().charset(charset), uri);
98 }
99
100
101
102
103
104 public QueryStringDecoder(String uri, Charset charset, boolean hasPath) {
105 this(builder().hasPath(hasPath).charset(charset), uri);
106 }
107
108
109
110
111
112 public QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) {
113 this(builder().hasPath(hasPath).charset(charset).maxParams(maxParams), uri);
114 }
115
116
117
118
119
120 public QueryStringDecoder(String uri, Charset charset, boolean hasPath,
121 int maxParams, boolean semicolonIsNormalChar) {
122 this(
123 builder()
124 .hasPath(hasPath)
125 .charset(charset)
126 .maxParams(maxParams)
127 .semicolonIsNormalChar(semicolonIsNormalChar),
128 uri);
129 }
130
131
132
133
134
135 public QueryStringDecoder(URI uri) {
136 this(builder(), uri);
137 }
138
139
140
141
142
143 public QueryStringDecoder(URI uri, Charset charset) {
144 this(builder().charset(charset), uri);
145 }
146
147
148
149
150
151 public QueryStringDecoder(URI uri, Charset charset, int maxParams) {
152 this(builder().charset(charset).maxParams(maxParams), uri);
153 }
154
155
156
157
158
159 public QueryStringDecoder(URI uri, Charset charset, int maxParams, boolean semicolonIsNormalChar) {
160 this(builder().charset(charset).maxParams(maxParams).semicolonIsNormalChar(semicolonIsNormalChar), uri);
161 }
162
163 private QueryStringDecoder(Builder builder, String uri) {
164 this.uri = checkNotNull(uri, "uri");
165 this.charset = checkNotNull(builder.charset, "charset");
166 this.maxParams = checkPositive(builder.maxParams, "maxParams");
167 this.semicolonIsNormalChar = builder.semicolonIsNormalChar;
168 this.htmlQueryDecoding = builder.htmlQueryDecoding;
169
170
171 pathEndIdx = builder.hasPath ? -1 : 0;
172 }
173
174 private QueryStringDecoder(Builder builder, URI uri) {
175 String rawPath = uri.getRawPath();
176 if (rawPath == null) {
177 rawPath = EMPTY_STRING;
178 }
179 String rawQuery = uri.getRawQuery();
180
181 this.uri = rawQuery == null? rawPath : rawPath + '?' + rawQuery;
182 this.charset = checkNotNull(builder.charset, "charset");
183 this.maxParams = checkPositive(builder.maxParams, "maxParams");
184 this.semicolonIsNormalChar = builder.semicolonIsNormalChar;
185 this.htmlQueryDecoding = builder.htmlQueryDecoding;
186 pathEndIdx = rawPath.length();
187 }
188
189 @Override
190 public String toString() {
191 return uri();
192 }
193
194
195
196
197 public String uri() {
198 return uri;
199 }
200
201
202
203
204 public String path() {
205 if (path == null) {
206 path = decodeComponent(uri, 0, pathEndIdx(), charset, false);
207 }
208 return path;
209 }
210
211
212
213
214 public Map<String, List<String>> parameters() {
215 if (params == null) {
216 params = decodeParams(uri, pathEndIdx(), charset, maxParams);
217 }
218 return params;
219 }
220
221
222
223
224 public String rawPath() {
225 return uri.substring(0, pathEndIdx());
226 }
227
228
229
230
231 public String rawQuery() {
232 int start = pathEndIdx() + 1;
233 return start < uri.length() ? uri.substring(start) : EMPTY_STRING;
234 }
235
236 private int pathEndIdx() {
237 if (pathEndIdx == -1) {
238 pathEndIdx = findPathEndIndex(uri);
239 }
240 return pathEndIdx;
241 }
242
243 private Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit) {
244 int len = s.length();
245 if (from >= len) {
246 return Collections.emptyMap();
247 }
248 if (s.charAt(from) == '?') {
249 from++;
250 }
251 Map<String, List<String>> params = new LinkedHashMap<String, List<String>>();
252 int nameStart = from;
253 int valueStart = -1;
254 int i;
255 loop:
256 for (i = from; i < len; i++) {
257 switch (s.charAt(i)) {
258 case '=':
259 if (nameStart == i) {
260 nameStart = i + 1;
261 } else if (valueStart < nameStart) {
262 valueStart = i + 1;
263 }
264 break;
265 case ';':
266 if (semicolonIsNormalChar) {
267 continue;
268 }
269
270 case '&':
271 if (addParam(s, nameStart, valueStart, i, params, charset)) {
272 paramsLimit--;
273 if (paramsLimit == 0) {
274 return params;
275 }
276 }
277 nameStart = i + 1;
278 break;
279 case '#':
280 break loop;
281 default:
282
283 }
284 }
285 addParam(s, nameStart, valueStart, i, params, charset);
286 return params;
287 }
288
289 private boolean addParam(String s, int nameStart, int valueStart, int valueEnd,
290 Map<String, List<String>> params, Charset charset) {
291 if (nameStart >= valueEnd) {
292 return false;
293 }
294 if (valueStart <= nameStart) {
295 valueStart = valueEnd + 1;
296 }
297 String name = decodeComponent(s, nameStart, valueStart - 1, charset, htmlQueryDecoding);
298 String value = decodeComponent(s, valueStart, valueEnd, charset, htmlQueryDecoding);
299 List<String> values = params.get(name);
300 if (values == null) {
301 values = new ArrayList<String>(1);
302 params.put(name, values);
303 }
304 values.add(value);
305 return true;
306 }
307
308
309
310
311
312
313
314
315
316
317
318
319 public static String decodeComponent(final String s) {
320 return decodeComponent(s, HttpConstants.DEFAULT_CHARSET);
321 }
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345 public static String decodeComponent(final String s, final Charset charset) {
346 if (s == null) {
347 return EMPTY_STRING;
348 }
349 return decodeComponent(s, 0, s.length(), charset, true);
350 }
351
352 private static String decodeComponent(String s, int from, int toExcluded, Charset charset, boolean plusToSpace) {
353 int len = toExcluded - from;
354 if (len <= 0) {
355 return EMPTY_STRING;
356 }
357 int firstEscaped = -1;
358 for (int i = from; i < toExcluded; i++) {
359 char c = s.charAt(i);
360 if (c == '%' || (c == '+' && plusToSpace)) {
361 firstEscaped = i;
362 break;
363 }
364 }
365 if (firstEscaped == -1) {
366 return s.substring(from, toExcluded);
367 }
368
369
370 int decodedCapacity = (toExcluded - firstEscaped) / 3;
371 byte[] buf = PlatformDependent.allocateUninitializedArray(decodedCapacity);
372 int bufIdx;
373
374 StringBuilder strBuf = new StringBuilder(len);
375 strBuf.append(s, from, firstEscaped);
376
377 for (int i = firstEscaped; i < toExcluded; i++) {
378 char c = s.charAt(i);
379 if (c != '%') {
380 strBuf.append(c != '+' || !plusToSpace ? c : SPACE);
381 continue;
382 }
383
384 bufIdx = 0;
385 do {
386 if (i + 3 > toExcluded) {
387 throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s);
388 }
389 buf[bufIdx++] = decodeHexByte(s, i + 1);
390 i += 3;
391 } while (i < toExcluded && s.charAt(i) == '%');
392 i--;
393
394 strBuf.append(new String(buf, 0, bufIdx, charset));
395 }
396 return strBuf.toString();
397 }
398
399 private static int findPathEndIndex(String uri) {
400 int len = uri.length();
401 for (int i = 0; i < len; i++) {
402 char c = uri.charAt(i);
403 if (c == '?' || c == '#') {
404 return i;
405 }
406 }
407 return len;
408 }
409
410 public static Builder builder() {
411 return new Builder();
412 }
413
414 public static final class Builder {
415 private boolean hasPath = true;
416 private int maxParams = DEFAULT_MAX_PARAMS;
417 private boolean semicolonIsNormalChar;
418 private Charset charset = HttpConstants.DEFAULT_CHARSET;
419 private boolean htmlQueryDecoding = true;
420
421 private Builder() {
422 }
423
424
425
426
427
428
429
430
431 public Builder hasPath(boolean hasPath) {
432 this.hasPath = hasPath;
433 return this;
434 }
435
436
437
438
439
440
441
442 public Builder maxParams(int maxParams) {
443 this.maxParams = maxParams;
444 return this;
445 }
446
447
448
449
450
451
452
453
454 public Builder semicolonIsNormalChar(boolean semicolonIsNormalChar) {
455 this.semicolonIsNormalChar = semicolonIsNormalChar;
456 return this;
457 }
458
459
460
461
462
463
464
465 public Builder charset(Charset charset) {
466 this.charset = charset;
467 return this;
468 }
469
470
471
472
473
474
475
476
477
478
479
480
481 public Builder htmlQueryDecoding(boolean htmlQueryDecoding) {
482 this.htmlQueryDecoding = htmlQueryDecoding;
483 return this;
484 }
485
486
487
488
489
490
491
492 public QueryStringDecoder build(String uri) {
493 return new QueryStringDecoder(this, uri);
494 }
495
496
497
498
499
500
501
502
503 public QueryStringDecoder build(URI uri) {
504 return new QueryStringDecoder(this, uri);
505 }
506 }
507 }