UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

617 lines 20.3 kB
// 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. const CHAR_MINUS = '-'.charCodeAt(0); const CHAR_0 = '0'.charCodeAt(0); const CHAR_9 = '9'.charCodeAt(0); const CHAR_A = 'A'.charCodeAt(0); const CHAR_Z = 'Z'.charCodeAt(0); const CHAR_LOWER_A = 'a'.charCodeAt(0); const CHAR_LOWER_Z = 'z'.charCodeAt(0); const CHAR_DQUOTE = '"'.charCodeAt(0); const CHAR_COLON = ':'.charCodeAt(0); const CHAR_QUESTION_MARK = '?'.charCodeAt(0); const CHAR_STAR = '*'.charCodeAt(0); const CHAR_UNDERSCORE = '_'.charCodeAt(0); const CHAR_DOT = '.'.charCodeAt(0); const CHAR_BACKSLASH = '\\'.charCodeAt(0); const CHAR_SLASH = '/'.charCodeAt(0); const CHAR_PLUS = '+'.charCodeAt(0); const CHAR_EQUALS = '='.charCodeAt(0); const CHAR_EXCLAMATION = '!'.charCodeAt(0); const CHAR_HASH = '#'.charCodeAt(0); const CHAR_DOLLAR = '$'.charCodeAt(0); const CHAR_PERCENT = '%'.charCodeAt(0); const CHAR_AND = '&'.charCodeAt(0); const CHAR_SQUOTE = '\''.charCodeAt(0); const CHAR_HAT = '^'.charCodeAt(0); const CHAR_BACKTICK = '`'.charCodeAt(0); const CHAR_PIPE = '|'.charCodeAt(0); const CHAR_TILDE = '~'.charCodeAt(0); // ASCII printable range. const CHAR_MIN_ASCII_PRINTABLE = 0x20; const CHAR_MAX_ASCII_PRINTABLE = 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) { // DIGIT = %x30-39 ; 0-9 (from RFC 5234) if (charCode === undefined) { return false; } return charCode >= CHAR_0 && charCode <= CHAR_9; } function isAlpha(charCode) { // 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) { // lcalpha = %x61-7A ; a-z if (charCode === undefined) { return false; } return (charCode >= CHAR_LOWER_A && charCode <= CHAR_LOWER_Z); } function isTChar(charCode) { 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 { data; pos; constructor(input) { this.data = input; this.pos = 0; // 4.2 step 2 is to discard any leading SP characters. this.skipSP(); } peek() { return this.data[this.pos]; } peekCharCode() { return (this.pos < this.data.length ? this.data.charCodeAt(this.pos) : undefined); } eat() { ++this.pos; } // Matches SP*. // SP = %x20, from RFC 5234 skipSP() { while (this.data[this.pos] === ' ') { ++this.pos; } } // Matches OWS // OWS = *( SP / HTAB ) , from RFC 7230 skipOWS() { while (this.data[this.pos] === ' ' || this.data[this.pos] === '\t') { ++this.pos; } } atEnd() { return (this.pos === this.data.length); } // 4.2 steps 6,7 --- checks for trailing characters. allParsed() { this.skipSP(); return (this.pos === this.data.length); } } function makeError() { return { kind: 0 /* ResultKind.ERROR */ }; } // 4.2.1. Parsing a list function parseListInternal(input) { const result = { kind: 11 /* ResultKind.LIST */, items: [] }; while (!input.atEnd()) { const piece = parseItemOrInnerList(input); if (piece.kind === 0 /* 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) { if (input.peek() === '(') { return parseInnerList(input); } return parseItemInternal(input); } // 4.2.1.2. Parsing an Inner List function parseInnerList(input) { if (input.peek() !== '(') { return makeError(); } input.eat(); const items = []; while (!input.atEnd()) { input.skipSP(); if (input.peek() === ')') { input.eat(); const params = parseParameters(input); if (params.kind === 0 /* ResultKind.ERROR */) { return params; } return { kind: 12 /* ResultKind.INNER_LIST */, items: items, parameters: params, }; } const item = parseItemInternal(input); if (item.kind === 0 /* 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) { const bareItem = parseBareItem(input); if (bareItem.kind === 0 /* ResultKind.ERROR */) { return bareItem; } const params = parseParameters(input); if (params.kind === 0 /* ResultKind.ERROR */) { return params; } return { kind: 4 /* ResultKind.ITEM */, value: bareItem, parameters: params }; } // 4.2.3.1. Parsing a Bare Item function parseBareItem(input) { 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) { // 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 = new Map(); while (!input.atEnd()) { if (input.peek() !== ';') { break; } input.eat(); input.skipSP(); const paramName = parseKey(input); if (paramName.kind === 0 /* ResultKind.ERROR */) { return paramName; } let paramValue = { kind: 10 /* ResultKind.BOOLEAN */, value: true }; if (input.peek() === '=') { input.eat(); const parsedParamValue = parseBareItem(input); if (parsedParamValue.kind === 0 /* 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: 2 /* ResultKind.PARAMETER */, name: paramName, value: paramValue }); } return { kind: 3 /* ResultKind.PARAMETERS */, items: [...items.values()] }; } // 4.2.3.3. Parsing a Key function parseKey(input) { let outputString = ''; 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: 1 /* ResultKind.PARAM_NAME */, value: outputString }; } // 4.2.4. Parsing an Integer or Decimal function parseIntegerOrDecimal(input) { let resultKind = 5 /* ResultKind.INTEGER */; let sign = 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 === 5 /* ResultKind.INTEGER */) { input.eat(); if (inputNumber.length > 12) { return makeError(); } inputNumber += '.'; resultKind = 6 /* ResultKind.DECIMAL */; } else { break; } if (resultKind === 5 /* ResultKind.INTEGER */ && inputNumber.length > 15) { return makeError(); } if (resultKind === 6 /* ResultKind.DECIMAL */ && inputNumber.length > 16) { return makeError(); } } if (resultKind === 5 /* ResultKind.INTEGER */) { const num = sign * Number.parseInt(inputNumber, 10); if (num < -999999999999999 || num > 999999999999999) { return makeError(); } return { kind: 5 /* ResultKind.INTEGER */, value: num }; } const afterDot = inputNumber.length - 1 - inputNumber.indexOf('.'); if (afterDot > 3 || afterDot === 0) { return makeError(); } return { kind: 6 /* ResultKind.DECIMAL */, value: sign * Number.parseFloat(inputNumber) }; } // 4.2.5. Parsing a String function parseString(input) { 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: 7 /* 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) { 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: 8 /* ResultKind.TOKEN */, value: outputString }; } // 4.2.7. Parsing a Byte Sequence function parseByteSequence(input) { 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: 9 /* 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) { if (input.peek() !== '?') { return makeError(); } input.eat(); if (input.peek() === '0') { input.eat(); return { kind: 10 /* ResultKind.BOOLEAN */, value: false }; } if (input.peek() === '1') { input.eat(); return { kind: 10 /* ResultKind.BOOLEAN */, value: true }; } return makeError(); } export function parseItem(input) { const i = new Input(input); const result = parseItemInternal(i); if (!i.allParsed()) { return makeError(); } return result; } export function parseList(input) { // 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) { const bareItemVal = serializeBareItem(input.value); if (bareItemVal.kind === 0 /* ResultKind.ERROR */) { return bareItemVal; } const paramVal = serializeParameters(input.parameters); if (paramVal.kind === 0 /* ResultKind.ERROR */) { return paramVal; } return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: bareItemVal.value + paramVal.value }; } // 4.1.1. Serializing a List export function serializeList(input) { const outputPieces = []; for (let i = 0; i < input.items.length; ++i) { const item = input.items[i]; if (item.kind === 12 /* ResultKind.INNER_LIST */) { const itemResult = serializeInnerList(item); if (itemResult.kind === 0 /* ResultKind.ERROR */) { return itemResult; } outputPieces.push(itemResult.value); } else { const itemResult = serializeItem(item); if (itemResult.kind === 0 /* ResultKind.ERROR */) { return itemResult; } outputPieces.push(itemResult.value); } } const output = outputPieces.join(', '); return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: output }; } // 4.1.1.1. Serializing an Inner List function serializeInnerList(input) { const outputPieces = []; for (let i = 0; i < input.items.length; ++i) { const itemResult = serializeItem(input.items[i]); if (itemResult.kind === 0 /* ResultKind.ERROR */) { return itemResult; } outputPieces.push(itemResult.value); } let output = '(' + outputPieces.join(' ') + ')'; const paramResult = serializeParameters(input.parameters); if (paramResult.kind === 0 /* ResultKind.ERROR */) { return paramResult; } output += paramResult.value; return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: output }; } // 4.1.1.2. Serializing Parameters function serializeParameters(input) { let output = ''; for (const item of input.items) { output += ';'; const nameResult = serializeKey(item.name); if (nameResult.kind === 0 /* ResultKind.ERROR */) { return nameResult; } output += nameResult.value; const itemVal = item.value; if (itemVal.kind !== 10 /* ResultKind.BOOLEAN */ || !itemVal.value) { output += '='; const itemValResult = serializeBareItem(itemVal); if (itemValResult.kind === 0 /* ResultKind.ERROR */) { return itemValResult; } output += itemValResult.value; } } return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: output }; } // 4.1.1.3. Serializing a Key function serializeKey(input) { 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: 13 /* ResultKind.SERIALIZATION_RESULT */, value: input.value }; } // 4.1.3.1. Serializing a Bare Item function serializeBareItem(input) { if (input.kind === 5 /* ResultKind.INTEGER */) { return serializeInteger(input); } if (input.kind === 6 /* ResultKind.DECIMAL */) { return serializeDecimal(input); } if (input.kind === 7 /* ResultKind.STRING */) { return serializeString(input); } if (input.kind === 8 /* ResultKind.TOKEN */) { return serializeToken(input); } if (input.kind === 10 /* ResultKind.BOOLEAN */) { return serializeBoolean(input); } if (input.kind === 9 /* ResultKind.BINARY */) { return serializeByteSequence(input); } return makeError(); } // 4.1.4. Serializing an Integer function serializeInteger(input) { if (input.value < -999999999999999 || input.value > 999999999999999 || !Number.isInteger(input.value)) { return makeError(); } return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: input.value.toString(10) }; } // 4.1.5. Serializing a Decimal function serializeDecimal(_input) { throw 'Unimplemented'; } // 4.1.6. Serializing a String function serializeString(input) { // 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: 13 /* ResultKind.SERIALIZATION_RESULT */, value: output }; } // 4.1.7. Serializing a Token function serializeToken(input) { 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: 13 /* ResultKind.SERIALIZATION_RESULT */, value: input.value }; } // 4.1.8. Serializing a Byte Sequence function serializeByteSequence(_input) { throw 'Unimplemented'; } // 4.1.9. Serializing a Boolean function serializeBoolean(input) { return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: input.value ? '?1' : '?0' }; } //# sourceMappingURL=StructuredHeaders.js.map