chrome-devtools-frontend
Version:
Chrome DevTools UI
791 lines (706 loc) • 22.1 kB
text/typescript
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This provides parsing and serialization for HTTP structured headers as specified in:
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-19
// (the ABNF fragments are quoted from the spec, unless otherwise specified,
// and the code pretty much just follows the algorithms given there).
//
// parseList, parseItem, serializeList, and serializeItem are the main entry points.
//
// Currently dictionary handling is not implemented (but would likely be easy
// to add). Serialization of decimals and byte sequences is also not
// implemented.
export const enum ResultKind {
ERROR = 0,
PARAM_NAME = 1,
PARAMETER = 2,
PARAMETERS = 3,
ITEM = 4,
INTEGER = 5,
DECIMAL = 6,
STRING = 7,
TOKEN = 8,
BINARY = 9,
BOOLEAN = 10,
LIST = 11,
INNER_LIST = 12,
SERIALIZATION_RESULT = 13,
}
export interface Error {
kind: ResultKind.ERROR;
}
export interface Integer {
kind: ResultKind.INTEGER;
value: number;
}
export interface Decimal {
kind: ResultKind.DECIMAL;
value: number;
}
export interface String {
kind: ResultKind.STRING;
value: string;
}
export interface Token {
kind: ResultKind.TOKEN;
value: string;
}
export interface Binary {
kind: ResultKind.BINARY;
// This is undecoded base64
value: string;
}
export interface Boolean {
kind: ResultKind.BOOLEAN;
value: boolean;
}
// bare-item = sf-integer / sf-decimal / sf-string / sf-token
// / sf-binary / sf-boolean
export type BareItem = Integer|Decimal|String|Token|Binary|Boolean;
export interface ParamName {
kind: ResultKind.PARAM_NAME;
value: string;
}
// parameter = param-name [ "=" param-value ]
// param-value = bare-item
export interface Parameter {
kind: ResultKind.PARAMETER;
name: ParamName;
value: BareItem;
}
// parameters = *( ";" *SP parameter )
export interface Parameters {
kind: ResultKind.PARAMETERS;
items: Parameter[];
}
// sf-item = bare-item parameters
export interface Item {
kind: ResultKind.ITEM;
value: BareItem;
parameters: Parameters;
}
// inner-list = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")"
// parameters
export interface InnerList {
kind: ResultKind.INNER_LIST;
items: Item[];
parameters: Parameters;
}
// list-member = sf-item / inner-list
export type ListMember = Item|InnerList;
// sf-list = list-member *( OWS "," OWS list-member )
export interface List {
kind: ResultKind.LIST;
items: ListMember[];
}
export interface SerializationResult {
kind: ResultKind.SERIALIZATION_RESULT;
value: string;
}
const CHAR_MINUS: number = '-'.charCodeAt(0);
const CHAR_0: number = '0'.charCodeAt(0);
const CHAR_9: number = '9'.charCodeAt(0);
const CHAR_A: number = 'A'.charCodeAt(0);
const CHAR_Z: number = 'Z'.charCodeAt(0);
const CHAR_LOWER_A: number = 'a'.charCodeAt(0);
const CHAR_LOWER_Z: number = 'z'.charCodeAt(0);
const CHAR_DQUOTE: number = '"'.charCodeAt(0);
const CHAR_COLON: number = ':'.charCodeAt(0);
const CHAR_QUESTION_MARK: number = '?'.charCodeAt(0);
const CHAR_STAR: number = '*'.charCodeAt(0);
const CHAR_UNDERSCORE: number = '_'.charCodeAt(0);
const CHAR_DOT: number = '.'.charCodeAt(0);
const CHAR_BACKSLASH: number = '\\'.charCodeAt(0);
const CHAR_SLASH: number = '/'.charCodeAt(0);
const CHAR_PLUS: number = '+'.charCodeAt(0);
const CHAR_EQUALS: number = '='.charCodeAt(0);
const CHAR_EXCLAMATION: number = '!'.charCodeAt(0);
const CHAR_HASH: number = '#'.charCodeAt(0);
const CHAR_DOLLAR: number = '$'.charCodeAt(0);
const CHAR_PERCENT: number = '%'.charCodeAt(0);
const CHAR_AND: number = '&'.charCodeAt(0);
const CHAR_SQUOTE: number = '\''.charCodeAt(0);
const CHAR_HAT: number = '^'.charCodeAt(0);
const CHAR_BACKTICK: number = '`'.charCodeAt(0);
const CHAR_PIPE: number = '|'.charCodeAt(0);
const CHAR_TILDE: number = '~'.charCodeAt(0);
// ASCII printable range.
const CHAR_MIN_ASCII_PRINTABLE: number = 0x20;
const CHAR_MAX_ASCII_PRINTABLE: number = 0x7e;
// Note: structured headers operates over ASCII, not unicode, so these are
// all indeed supposed to return false on things outside 32-127 range regardless
// of them being other kinds of digits or letters.
function isDigit(charCode: number|undefined): boolean {
// DIGIT = %x30-39 ; 0-9 (from RFC 5234)
if (charCode === undefined) {
return false;
}
return charCode >= CHAR_0 && charCode <= CHAR_9;
}
function isAlpha(charCode: number|undefined): boolean {
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z (from RFC 5234)
if (charCode === undefined) {
return false;
}
return (charCode >= CHAR_A && charCode <= CHAR_Z) || (charCode >= CHAR_LOWER_A && charCode <= CHAR_LOWER_Z);
}
function isLcAlpha(charCode: number|undefined): boolean {
// lcalpha = %x61-7A ; a-z
if (charCode === undefined) {
return false;
}
return (charCode >= CHAR_LOWER_A && charCode <= CHAR_LOWER_Z);
}
function isTChar(charCode: number|undefined): boolean {
if (charCode === undefined) {
return false;
}
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA (from RFC 7230)
if (isDigit(charCode) || isAlpha(charCode)) {
return true;
}
switch (charCode) {
case CHAR_EXCLAMATION:
case CHAR_HASH:
case CHAR_DOLLAR:
case CHAR_PERCENT:
case CHAR_AND:
case CHAR_SQUOTE:
case CHAR_STAR:
case CHAR_PLUS:
case CHAR_MINUS:
case CHAR_DOT:
case CHAR_HAT:
case CHAR_UNDERSCORE:
case CHAR_BACKTICK:
case CHAR_PIPE:
case CHAR_TILDE:
return true;
default:
return false;
}
}
class Input {
private readonly data: string;
private pos: number;
constructor(input: string) {
this.data = input;
this.pos = 0;
// 4.2 step 2 is to discard any leading SP characters.
this.skipSP();
}
peek(): string|undefined {
return this.data[this.pos];
}
peekCharCode(): number|undefined {
return (this.pos < this.data.length ? this.data.charCodeAt(this.pos) : undefined);
}
eat(): void {
++this.pos;
}
// Matches SP*.
// SP = %x20, from RFC 5234
skipSP(): void {
while (this.data[this.pos] === ' ') {
++this.pos;
}
}
// Matches OWS
// OWS = *( SP / HTAB ) , from RFC 7230
skipOWS(): void {
while (this.data[this.pos] === ' ' || this.data[this.pos] === '\t') {
++this.pos;
}
}
atEnd(): boolean {
return (this.pos === this.data.length);
}
// 4.2 steps 6,7 --- checks for trailing characters.
allParsed(): boolean {
this.skipSP();
return (this.pos === this.data.length);
}
}
function makeError(): Error {
return {kind: ResultKind.ERROR};
}
// 4.2.1. Parsing a list
function parseListInternal(input: Input): List|Error {
const result: List = {kind: ResultKind.LIST, items: []};
while (!input.atEnd()) {
const piece: ListMember|Error = parseItemOrInnerList(input);
if (piece.kind === ResultKind.ERROR) {
return piece;
}
result.items.push(piece);
input.skipOWS();
if (input.atEnd()) {
return result;
}
if (input.peek() !== ',') {
return makeError();
}
input.eat();
input.skipOWS();
// "If input_string is empty, there is a trailing comma; fail parsing."
if (input.atEnd()) {
return makeError();
}
}
return result; // this case corresponds to an empty list.
}
// 4.2.1.1. Parsing an Item or Inner List
function parseItemOrInnerList(input: Input): ListMember|Error {
if (input.peek() === '(') {
return parseInnerList(input);
}
return parseItemInternal(input);
}
// 4.2.1.2. Parsing an Inner List
function parseInnerList(input: Input): InnerList|Error {
if (input.peek() !== '(') {
return makeError();
}
input.eat();
const items: Item[] = [];
while (!input.atEnd()) {
input.skipSP();
if (input.peek() === ')') {
input.eat();
const params: Parameters|Error = parseParameters(input);
if (params.kind === ResultKind.ERROR) {
return params;
}
return {
kind: ResultKind.INNER_LIST,
items: items,
parameters: params,
};
}
const item: Item|Error = parseItemInternal(input);
if (item.kind === ResultKind.ERROR) {
return item;
}
items.push(item);
if (input.peek() !== ' ' && input.peek() !== ')') {
return makeError();
}
}
// Didn't see ), so error.
return makeError();
}
// 4.2.3. Parsing an Item
function parseItemInternal(input: Input): Item|Error {
const bareItem: BareItem|Error = parseBareItem(input);
if (bareItem.kind === ResultKind.ERROR) {
return bareItem;
}
const params: Parameters|Error = parseParameters(input);
if (params.kind === ResultKind.ERROR) {
return params;
}
return {kind: ResultKind.ITEM, value: bareItem, parameters: params};
}
// 4.2.3.1. Parsing a Bare Item
function parseBareItem(input: Input): BareItem|Error {
const upcoming = input.peekCharCode();
if (upcoming === CHAR_MINUS || isDigit(upcoming)) {
return parseIntegerOrDecimal(input);
}
if (upcoming === CHAR_DQUOTE) {
return parseString(input);
}
if (upcoming === CHAR_COLON) {
return parseByteSequence(input);
}
if (upcoming === CHAR_QUESTION_MARK) {
return parseBoolean(input);
}
if (upcoming === CHAR_STAR || isAlpha(upcoming)) {
return parseToken(input);
}
return makeError();
}
// 4.2.3.2. Parsing Parameters
function parseParameters(input: Input): Parameters|Error {
// The main noteworthy thing here is handling of duplicates and ordering:
//
// "Note that Parameters are ordered as serialized"
//
// "If parameters already contains a name param_name (comparing
// character-for-character), overwrite its value."
//
// "Note that when duplicate Parameter keys are encountered, this has the
// effect of ignoring all but the last instance."
const items: Map<string, Parameter> = new Map<string, Parameter>();
while (!input.atEnd()) {
if (input.peek() !== ';') {
break;
}
input.eat();
input.skipSP();
const paramName = parseKey(input);
if (paramName.kind === ResultKind.ERROR) {
return paramName;
}
let paramValue: BareItem = {kind: ResultKind.BOOLEAN, value: true};
if (input.peek() === '=') {
input.eat();
const parsedParamValue: BareItem|Error = parseBareItem(input);
if (parsedParamValue.kind === ResultKind.ERROR) {
return parsedParamValue;
}
paramValue = parsedParamValue;
}
// Delete any previous occurrence of duplicates to get the ordering right.
if (items.has(paramName.value)) {
items.delete(paramName.value);
}
items.set(paramName.value, {kind: ResultKind.PARAMETER, name: paramName, value: paramValue});
}
return {kind: ResultKind.PARAMETERS, items: [...items.values()]};
}
// 4.2.3.3. Parsing a Key
function parseKey(input: Input): ParamName|Error {
let outputString: string = '';
const first = input.peekCharCode();
if (first !== CHAR_STAR && !isLcAlpha(first)) {
return makeError();
}
while (!input.atEnd()) {
const upcoming = input.peekCharCode();
if (!isLcAlpha(upcoming) && !isDigit(upcoming) && upcoming !== CHAR_UNDERSCORE && upcoming !== CHAR_MINUS &&
upcoming !== CHAR_DOT && upcoming !== CHAR_STAR) {
break;
}
outputString += input.peek();
input.eat();
}
return {kind: ResultKind.PARAM_NAME, value: outputString};
}
// 4.2.4. Parsing an Integer or Decimal
function parseIntegerOrDecimal(input: Input): Integer|Decimal|Error {
let resultKind = ResultKind.INTEGER;
let sign: number = 1;
let inputNumber = '';
if (input.peek() === '-') {
input.eat();
sign = -1;
}
// This case includes end of input.
if (!isDigit(input.peekCharCode())) {
return makeError();
}
while (!input.atEnd()) {
const char = input.peekCharCode();
if (char !== undefined && isDigit(char)) {
input.eat();
inputNumber += String.fromCodePoint(char);
} else if (char === CHAR_DOT && resultKind === ResultKind.INTEGER) {
input.eat();
if (inputNumber.length > 12) {
return makeError();
}
inputNumber += '.';
resultKind = ResultKind.DECIMAL;
} else {
break;
}
if (resultKind === ResultKind.INTEGER && inputNumber.length > 15) {
return makeError();
}
if (resultKind === ResultKind.DECIMAL && inputNumber.length > 16) {
return makeError();
}
}
if (resultKind === ResultKind.INTEGER) {
const num = sign * Number.parseInt(inputNumber, 10);
if (num < -999999999999999 || num > 999999999999999) {
return makeError();
}
return {kind: ResultKind.INTEGER, value: num};
}
const afterDot = inputNumber.length - 1 - inputNumber.indexOf('.');
if (afterDot > 3 || afterDot === 0) {
return makeError();
}
return {kind: ResultKind.DECIMAL, value: sign * Number.parseFloat(inputNumber)};
}
// 4.2.5. Parsing a String
function parseString(input: Input): String|Error {
let outputString = '';
if (input.peek() !== '"') {
return makeError();
}
input.eat();
while (!input.atEnd()) {
const char = input.peekCharCode();
// can't happen due to atEnd(), but help the typechecker out.
if (char === undefined) {
return makeError();
}
input.eat();
if (char === CHAR_BACKSLASH) {
if (input.atEnd()) {
return makeError();
}
const nextChar = input.peekCharCode();
input.eat();
if (nextChar !== CHAR_BACKSLASH && nextChar !== CHAR_DQUOTE) {
return makeError();
}
outputString += String.fromCodePoint(nextChar);
} else if (char === CHAR_DQUOTE) {
return {kind: ResultKind.STRING, value: outputString};
} else if (char<CHAR_MIN_ASCII_PRINTABLE || char>CHAR_MAX_ASCII_PRINTABLE) {
return makeError();
} else {
outputString += String.fromCodePoint(char);
}
}
// No closing quote.
return makeError();
}
// 4.2.6. Parsing a Token
function parseToken(input: Input): Token|Error {
const first = input.peekCharCode();
if (first !== CHAR_STAR && !isAlpha(first)) {
return makeError();
}
let outputString = '';
while (!input.atEnd()) {
const upcoming = input.peekCharCode();
if (upcoming === undefined || !isTChar(upcoming) && upcoming !== CHAR_COLON && upcoming !== CHAR_SLASH) {
break;
}
input.eat();
outputString += String.fromCodePoint(upcoming);
}
return {kind: ResultKind.TOKEN, value: outputString};
}
// 4.2.7. Parsing a Byte Sequence
function parseByteSequence(input: Input): Binary|Error {
let outputString = '';
if (input.peek() !== ':') {
return makeError();
}
input.eat();
while (!input.atEnd()) {
const char = input.peekCharCode();
// can't happen due to atEnd(), but help the typechecker out.
if (char === undefined) {
return makeError();
}
input.eat();
if (char === CHAR_COLON) {
return {kind: ResultKind.BINARY, value: outputString};
}
if (isDigit(char) || isAlpha(char) || char === CHAR_PLUS || char === CHAR_SLASH || char === CHAR_EQUALS) {
outputString += String.fromCodePoint(char);
} else {
return makeError();
}
}
// No closing :
return makeError();
}
// 4.2.8. Parsing a Boolean
function parseBoolean(input: Input): Boolean|Error {
if (input.peek() !== '?') {
return makeError();
}
input.eat();
if (input.peek() === '0') {
input.eat();
return {kind: ResultKind.BOOLEAN, value: false};
}
if (input.peek() === '1') {
input.eat();
return {kind: ResultKind.BOOLEAN, value: true};
}
return makeError();
}
export function parseItem(input: string): Item|Error {
const i = new Input(input);
const result: Item|Error = parseItemInternal(i);
if (!i.allParsed()) {
return makeError();
}
return result;
}
export function parseList(input: string): List|Error {
// No need to look for trailing stuff here since parseListInternal does it already.
return parseListInternal(new Input(input));
}
// 4.1.3. Serializing an Item
export function serializeItem(input: Item): SerializationResult|Error {
const bareItemVal = serializeBareItem(input.value);
if (bareItemVal.kind === ResultKind.ERROR) {
return bareItemVal;
}
const paramVal = serializeParameters(input.parameters);
if (paramVal.kind === ResultKind.ERROR) {
return paramVal;
}
return {kind: ResultKind.SERIALIZATION_RESULT, value: bareItemVal.value + paramVal.value};
}
// 4.1.1. Serializing a List
export function serializeList(input: List): SerializationResult|Error {
const outputPieces: string[] = [];
for (let i = 0; i < input.items.length; ++i) {
const item = input.items[i];
if (item.kind === ResultKind.INNER_LIST) {
const itemResult = serializeInnerList(item);
if (itemResult.kind === ResultKind.ERROR) {
return itemResult;
}
outputPieces.push(itemResult.value);
} else {
const itemResult = serializeItem(item);
if (itemResult.kind === ResultKind.ERROR) {
return itemResult;
}
outputPieces.push(itemResult.value);
}
}
const output = outputPieces.join(', ');
return {kind: ResultKind.SERIALIZATION_RESULT, value: output};
}
// 4.1.1.1. Serializing an Inner List
function serializeInnerList(input: InnerList): SerializationResult|Error {
const outputPieces: string[] = [];
for (let i = 0; i < input.items.length; ++i) {
const itemResult = serializeItem(input.items[i]);
if (itemResult.kind === ResultKind.ERROR) {
return itemResult;
}
outputPieces.push(itemResult.value);
}
let output = '(' + outputPieces.join(' ') + ')';
const paramResult = serializeParameters(input.parameters);
if (paramResult.kind === ResultKind.ERROR) {
return paramResult;
}
output += paramResult.value;
return {kind: ResultKind.SERIALIZATION_RESULT, value: output};
}
// 4.1.1.2. Serializing Parameters
function serializeParameters(input: Parameters): SerializationResult|Error {
let output = '';
for (const item of input.items) {
output += ';';
const nameResult = serializeKey(item.name);
if (nameResult.kind === ResultKind.ERROR) {
return nameResult;
}
output += nameResult.value;
const itemVal: BareItem = item.value;
if (itemVal.kind !== ResultKind.BOOLEAN || !itemVal.value) {
output += '=';
const itemValResult = serializeBareItem(itemVal);
if (itemValResult.kind === ResultKind.ERROR) {
return itemValResult;
}
output += itemValResult.value;
}
}
return {kind: ResultKind.SERIALIZATION_RESULT, value: output};
}
// 4.1.1.3. Serializing a Key
function serializeKey(input: ParamName): SerializationResult|Error {
if (input.value.length === 0) {
return makeError();
}
const firstChar = input.value.charCodeAt(0);
if (!isLcAlpha(firstChar) && firstChar !== CHAR_STAR) {
return makeError();
}
for (let i = 1; i < input.value.length; ++i) {
const char = input.value.charCodeAt(i);
if (!isLcAlpha(char) && !isDigit(char) && char !== CHAR_UNDERSCORE && char !== CHAR_MINUS && char !== CHAR_DOT &&
char !== CHAR_STAR) {
return makeError();
}
}
return {kind: ResultKind.SERIALIZATION_RESULT, value: input.value};
}
// 4.1.3.1. Serializing a Bare Item
function serializeBareItem(input: BareItem): SerializationResult|Error {
if (input.kind === ResultKind.INTEGER) {
return serializeInteger(input);
}
if (input.kind === ResultKind.DECIMAL) {
return serializeDecimal(input);
}
if (input.kind === ResultKind.STRING) {
return serializeString(input);
}
if (input.kind === ResultKind.TOKEN) {
return serializeToken(input);
}
if (input.kind === ResultKind.BOOLEAN) {
return serializeBoolean(input);
}
if (input.kind === ResultKind.BINARY) {
return serializeByteSequence(input);
}
return makeError();
}
// 4.1.4. Serializing an Integer
function serializeInteger(input: Integer): SerializationResult|Error {
if (input.value < -999999999999999 || input.value > 999999999999999 || !Number.isInteger(input.value)) {
return makeError();
}
return {kind: ResultKind.SERIALIZATION_RESULT, value: input.value.toString(10)};
}
// 4.1.5. Serializing a Decimal
function serializeDecimal(_input: Decimal): SerializationResult|Error {
throw 'Unimplemented';
}
// 4.1.6. Serializing a String
function serializeString(input: String): SerializationResult|Error {
// Only printable ASCII strings are supported by the spec.
for (let i = 0; i < input.value.length; ++i) {
const char = input.value.charCodeAt(i);
if (char<CHAR_MIN_ASCII_PRINTABLE || char>CHAR_MAX_ASCII_PRINTABLE) {
return makeError();
}
}
let output = '"';
for (let i = 0; i < input.value.length; ++i) {
const charStr = input.value[i];
if (charStr === '"' || charStr === '\\') {
output += '\\';
}
output += charStr;
}
output += '"';
return {kind: ResultKind.SERIALIZATION_RESULT, value: output};
}
// 4.1.7. Serializing a Token
function serializeToken(input: Token): SerializationResult|Error {
if (input.value.length === 0) {
return makeError();
}
const firstChar = input.value.charCodeAt(0);
if (!isAlpha(firstChar) && firstChar !== CHAR_STAR) {
return makeError();
}
for (let i = 1; i < input.value.length; ++i) {
const char = input.value.charCodeAt(i);
if (!isTChar(char) && char !== CHAR_COLON && char !== CHAR_SLASH) {
return makeError();
}
}
return {kind: ResultKind.SERIALIZATION_RESULT, value: input.value};
}
// 4.1.8. Serializing a Byte Sequence
function serializeByteSequence(_input: Binary): SerializationResult|Error {
throw 'Unimplemented';
}
// 4.1.9. Serializing a Boolean
function serializeBoolean(input: Boolean): SerializationResult|Error {
return {kind: ResultKind.SERIALIZATION_RESULT, value: input.value ? '?1' : '?0'};
}