• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.apihelpers;
6 
7 import androidx.annotation.Nullable;
8 
9 import java.text.ParseException;
10 import java.util.AbstractMap;
11 import java.util.Map;
12 
13 /**
14  * A helper for parsing the optional parameters section of the {@code Content-Type} header.
15  *
16  * <p>See {@link https://www.rfc-editor.org/rfc/rfc9110.html#name-media-type} for more details.
17  */
18 final class ContentTypeParametersParser {
19     private static final String TOKEN_ALLOWED_SPECIAL_CHARS = "!#$%&'*+-.^_`|~";
20 
21     private final String mHeaderValue;
22     private int mCurrentPosition;
23 
ContentTypeParametersParser(String mHeaderValue)24     ContentTypeParametersParser(String mHeaderValue) {
25         this.mHeaderValue = mHeaderValue;
26         int semicolonIndex = mHeaderValue.indexOf(';');
27         mCurrentPosition = semicolonIndex != -1 ? semicolonIndex + 1 : mHeaderValue.length();
28     }
29 
30     @Nullable
getNextParameter()31     Map.Entry<String, String> getNextParameter() throws ContentTypeParametersParserException {
32         int startPos = mCurrentPosition;
33         optionallySkipWhitespace();
34         String parameterName = getNextToken();
35         if (currentChar() != '=') {
36             throw new ContentTypeParametersParserException(
37                     "Invalid parameter format: expected = at "
38                             + mCurrentPosition
39                             + ": ["
40                             + mHeaderValue
41                             + "]",
42                     mCurrentPosition);
43         }
44 
45         advance();
46 
47         String parameterValue;
48         if (currentChar() == '"') {
49             parameterValue = getNextQuotedString();
50         } else {
51             parameterValue = getNextToken();
52         }
53 
54         optionallySkipWhitespace();
55 
56         if (hasMore()) {
57             if (currentChar() != ';') {
58                 throw new ContentTypeParametersParserException(
59                         "Invalid parameter format: expected ; at "
60                                 + mCurrentPosition
61                                 + ": ["
62                                 + mHeaderValue
63                                 + "]",
64                         mCurrentPosition);
65             }
66 
67             advance();
68         }
69         return new AbstractMap.SimpleEntry<>(parameterName, parameterValue);
70     }
71 
getNextQuotedString()72     private String getNextQuotedString() throws ContentTypeParametersParserException {
73         int start = mCurrentPosition;
74         if (currentChar() != '"') {
75             throw new ContentTypeParametersParserException(
76                     "Not a quoted string: expected \" at "
77                             + mCurrentPosition
78                             + ": ["
79                             + mHeaderValue
80                             + "]",
81                     mCurrentPosition);
82         }
83         advance();
84 
85         StringBuilder sb = new StringBuilder();
86 
87         boolean escapeNext = false;
88 
89         while (true) {
90             if (!hasMore()) {
91                 throw new ContentTypeParametersParserException(
92                         "Unterminated quoted string at " + start + ": [" + mHeaderValue + "]",
93                         start);
94             }
95 
96             if (escapeNext) {
97                 if (!isQuotedPairChar(currentChar())) {
98                     throw new ContentTypeParametersParserException(
99                             "Invalid character at " + mCurrentPosition + ": [" + mHeaderValue + "]",
100                             mCurrentPosition);
101                 }
102                 escapeNext = false;
103                 sb.append(currentChar());
104                 advance();
105             } else if (currentChar() == '"') {
106                 advance();
107                 return sb.toString();
108             } else if (currentChar() == '\\') {
109                 escapeNext = true;
110                 advance();
111             } else {
112                 if (!isQdtextChar(currentChar())) {
113                     throw new ContentTypeParametersParserException(
114                             "Invalid character at " + mCurrentPosition + ": [" + mHeaderValue + "]",
115                             mCurrentPosition);
116                 }
117                 sb.append(currentChar());
118                 advance();
119             }
120         }
121     }
122 
getNextToken()123     private String getNextToken() throws ContentTypeParametersParserException {
124         int start = mCurrentPosition;
125         while (hasMore() && isTokenCharacter(currentChar())) {
126             advance();
127         }
128         if (start == mCurrentPosition) {
129             throw new ContentTypeParametersParserException(
130                     "Token not found at position " + start + ": [" + mHeaderValue + "]", start);
131         }
132         return mHeaderValue.substring(start, mCurrentPosition);
133     }
134 
hasMore()135     boolean hasMore() {
136         return mCurrentPosition < mHeaderValue.length();
137     }
138 
currentChar()139     private char currentChar() throws ContentTypeParametersParserException {
140         if (!hasMore()) {
141             throw new ContentTypeParametersParserException(
142                     "End of header reached", mCurrentPosition);
143         }
144         return mHeaderValue.charAt(mCurrentPosition);
145     }
146 
advance()147     private void advance() throws ContentTypeParametersParserException {
148         if (!hasMore()) {
149             throw new ContentTypeParametersParserException(
150                     "End of header reached", mCurrentPosition);
151         }
152         mCurrentPosition++;
153     }
154 
optionallySkipWhitespace()155     private void optionallySkipWhitespace() throws ContentTypeParametersParserException {
156         while (hasMore() && isWhitespace(currentChar())) {
157             advance();
158         }
159     }
160 
isQdtextChar(char c)161     private static boolean isQdtextChar(char c) {
162         return c != '\\' && c != '"' && isQuotedPairChar(c);
163     }
164 
isQuotedPairChar(char c)165     private static boolean isQuotedPairChar(char c) {
166         return isWhitespace(c) || ('!' <= c && c <= (char) 255 && c != (char) 0x7F);
167     }
168 
isTokenCharacter(char ch)169     private static boolean isTokenCharacter(char ch) {
170         return isAscii(ch)
171                 && (Character.isLetterOrDigit(ch) || TOKEN_ALLOWED_SPECIAL_CHARS.indexOf(ch) != -1);
172     }
173 
isAscii(char ch)174     private static boolean isAscii(char ch) {
175         return (char) 0 <= ch && ch <= (char) 127;
176     }
177 
isWhitespace(char c)178     private static boolean isWhitespace(char c) {
179         return c == '\t' || c == ' ';
180     }
181 
182     static class ContentTypeParametersParserException extends ParseException {
ContentTypeParametersParserException(String reason, int offset)183         ContentTypeParametersParserException(String reason, int offset) {
184             super(reason, offset);
185         }
186     }
187 }
188