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