UNPKG

structured-headers

Version:

Implementation of Structured Field Values for HTTP (RFC9651, RFC8941)

670 lines (664 loc) 19.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { DisplayString: () => DisplayString, ParseError: () => ParseError, SerializeError: () => SerializeError, Token: () => Token, arrayBufferToBase64: () => arrayBufferToBase64, base64ToArrayBuffer: () => base64ToArrayBuffer, isAscii: () => isAscii, isInnerList: () => isInnerList, isValidKeyStr: () => isValidKeyStr, isValidTokenStr: () => isValidTokenStr, parseDictionary: () => parseDictionary, parseItem: () => parseItem, parseList: () => parseList, serializeBareItem: () => serializeBareItem, serializeBoolean: () => serializeBoolean, serializeByteSequence: () => serializeByteSequence, serializeDate: () => serializeDate, serializeDecimal: () => serializeDecimal, serializeDictionary: () => serializeDictionary, serializeDisplayString: () => serializeDisplayString, serializeInnerList: () => serializeInnerList, serializeInteger: () => serializeInteger, serializeItem: () => serializeItem, serializeKey: () => serializeKey, serializeList: () => serializeList, serializeParameters: () => serializeParameters, serializeString: () => serializeString, serializeToken: () => serializeToken }); module.exports = __toCommonJS(index_exports); // src/util.ts var asciiRe = /^[\x20-\x7E]*$/; var tokenRe = /^[a-zA-Z*][:/!#$%&'*+\-.^_`|~A-Za-z0-9]*$/; var keyRe = /^[a-z*][*\-_.a-z0-9]*$/; function isAscii(str) { return asciiRe.test(str); } function isValidTokenStr(str) { return tokenRe.test(str); } function isValidKeyStr(str) { return keyRe.test(str); } function isInnerList(input) { return Array.isArray(input[0]); } function arrayBufferToBase64(ab) { const bytes = new Uint8Array(ab); let binary = ""; for (const byte of bytes) { binary += String.fromCharCode(byte); } return btoa(binary); } function base64ToArrayBuffer(b64) { const binaryString = atob(b64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } // src/token.ts var Token = class { constructor(value) { if (!isValidTokenStr(value)) { throw new TypeError("Invalid character in Token string. Tokens must start with *, A-Z and the rest of the string may only contain a-z, A-Z, 0-9, :/!#$%&'*+-.^_`|~"); } this.value = value; } toString() { return this.value; } }; // src/displaystring.ts var DisplayString = class { constructor(value) { this.value = value; } toString() { return this.value; } }; // src/serializer.ts var SerializeError = class extends Error { }; function serializeList(input) { return input.map((value) => { if (isInnerList(value)) { return serializeInnerList(value); } else { return serializeItem(value); } }).join(", "); } function serializeDictionary(input) { const entries = input instanceof Map ? input.entries() : Object.entries(input); return Array.from(entries).map(([key, entry]) => { const keyStr = serializeKey(key); if (Array.isArray(entry)) { if (entry[0] === true) { return keyStr + serializeParameters(entry[1]); } else { if (isInnerList(entry)) { return keyStr + "=" + serializeInnerList(entry); } else { return keyStr + "=" + serializeItem(entry); } } } else { if (entry === true) { return keyStr; } else { return keyStr + "=" + serializeBareItem(entry); } } }).join(", "); } function serializeItem(input, params) { if (Array.isArray(input)) { return serializeBareItem(input[0]) + serializeParameters(input[1]); } else { return serializeBareItem(input) + (params ? serializeParameters(params) : ""); } } function serializeInnerList(input) { return `(${input[0].map((value) => serializeItem(value)).join(" ")})${serializeParameters(input[1])}`; } function serializeBareItem(input) { if (typeof input === "number") { if (Number.isInteger(input)) { return serializeInteger(input); } return serializeDecimal(input); } if (typeof input === "string") { return serializeString(input); } if (input instanceof Token) { return serializeToken(input); } if (input instanceof ArrayBuffer) { return serializeByteSequence(input); } if (input instanceof DisplayString) { return serializeDisplayString(input); } if (input instanceof Date) { return serializeDate(input); } if (typeof input === "boolean") { return serializeBoolean(input); } throw new SerializeError(`Cannot serialize values of type ${typeof input}`); } function serializeInteger(input) { if (input < -999999999999999 || input > 999999999999999) { throw new SerializeError("Structured headers can only encode integers in the range range of -999,999,999,999,999 to 999,999,999,999,999 inclusive"); } return input.toString(); } function serializeDecimal(input) { const out = input.toFixed(3).replace(/0+$/, ""); const signifantDigits = out.split(".")[0].replace("-", "").length; if (signifantDigits > 12) { throw new SerializeError("Fractional numbers are not allowed to have more than 12 significant digits before the decimal point"); } return out; } function serializeString(input) { if (!isAscii(input)) { throw new SerializeError("Only ASCII strings may be serialized"); } return `"${input.replace(/("|\\)/g, (v) => "\\" + v)}"`; } function serializeDisplayString(input) { let out = '%"'; const textEncoder = new TextEncoder(); for (const char of textEncoder.encode(input.toString())) { if (char === 37 || char === 34 || char <= 31 || char >= 127) { out += "%" + char.toString(16); } else { out += String.fromCharCode(char); } } return out + '"'; } function serializeBoolean(input) { return input ? "?1" : "?0"; } function serializeByteSequence(input) { return `:${arrayBufferToBase64(input)}:`; } function serializeToken(input) { return input.toString(); } function serializeDate(input) { return "@" + Math.floor(input.getTime() / 1e3); } function serializeParameters(input) { return Array.from(input).map(([key, value]) => { let out = ";" + serializeKey(key); if (value !== true) { out += "=" + serializeBareItem(value); } return out; }).join(""); } function serializeKey(input) { if (!isValidKeyStr(input)) { throw new SerializeError("Keys in dictionaries must only contain lowercase letter, numbers, _-*. and must start with a letter or *"); } return input; } // src/parser.ts function parseDictionary(input) { const parser = new Parser(input); return parser.parseDictionary(); } function parseList(input) { const parser = new Parser(input); return parser.parseList(); } function parseItem(input) { const parser = new Parser(input); return parser.parseItem(); } var ParseError = class extends Error { constructor(position, message) { super(`Parse error: ${message} at offset ${position}`); } }; var Parser = class { constructor(input) { this.input = input; this.pos = 0; } parseDictionary() { this.skipWS(); const dictionary = /* @__PURE__ */ new Map(); while (!this.eof()) { const thisKey = this.parseKey(); let member; if (this.lookChar() === "=") { this.pos++; member = this.parseItemOrInnerList(); } else { member = [true, this.parseParameters()]; } dictionary.set(thisKey, member); this.skipOWS(); if (this.eof()) { return dictionary; } this.expectChar(","); this.pos++; this.skipOWS(); if (this.eof()) { throw new ParseError(this.pos, "Dictionary contained a trailing comma"); } } return dictionary; } parseList() { this.skipWS(); const members = []; while (!this.eof()) { members.push( this.parseItemOrInnerList() ); this.skipOWS(); if (this.eof()) { return members; } this.expectChar(","); this.pos++; this.skipOWS(); if (this.eof()) { throw new ParseError(this.pos, "A list may not end with a trailing comma"); } } return members; } parseItem(standaloneItem = true) { if (standaloneItem) this.skipWS(); const result = [ this.parseBareItem(), this.parseParameters() ]; if (standaloneItem) this.checkTrail(); return result; } parseItemOrInnerList() { if (this.lookChar() === "(") { return this.parseInnerList(); } else { return this.parseItem(false); } } parseInnerList() { this.expectChar("("); this.pos++; const innerList = []; while (!this.eof()) { this.skipWS(); if (this.lookChar() === ")") { this.pos++; return [ innerList, this.parseParameters() ]; } innerList.push(this.parseItem(false)); const nextChar = this.lookChar(); if (nextChar !== " " && nextChar !== ")") { throw new ParseError(this.pos, "Expected a whitespace or ) after every item in an inner list"); } } throw new ParseError(this.pos, "Could not find end of inner list"); } parseBareItem() { const char = this.lookChar(); if (char === void 0) { throw new ParseError(this.pos, "Unexpected end of string"); } if (char.match(/^[-0-9]/)) { return this.parseIntegerOrDecimal(); } if (char === '"') { return this.parseString(); } if (char.match(/^[A-Za-z*]/)) { return this.parseToken(); } if (char === ":") { return this.parseByteSequence(); } if (char === "?") { return this.parseBoolean(); } if (char === "@") { return this.parseDate(); } if (char === "%") { return this.parseDisplayString(); } throw new ParseError(this.pos, "Unexpected input"); } parseParameters() { const parameters = /* @__PURE__ */ new Map(); while (!this.eof()) { const char = this.lookChar(); if (char !== ";") { break; } this.pos++; this.skipWS(); const key = this.parseKey(); let value = true; if (this.lookChar() === "=") { this.pos++; value = this.parseBareItem(); } parameters.set(key, value); } return parameters; } parseIntegerOrDecimal() { let type = "integer"; let sign = 1; let inputNumber = ""; if (this.lookChar() === "-") { sign = -1; this.pos++; } if (!isDigit(this.lookChar())) { throw new ParseError(this.pos, "Expected a digit (0-9)"); } while (!this.eof()) { const char = this.getChar(); if (isDigit(char)) { inputNumber += char; } else if (type === "integer" && char === ".") { if (inputNumber.length > 12) { throw new ParseError(this.pos, "Exceeded maximum decimal length"); } inputNumber += "."; type = "decimal"; } else { this.pos--; break; } if (type === "integer" && inputNumber.length > 15) { throw new ParseError(this.pos, "Exceeded maximum integer length"); } if (type === "decimal" && inputNumber.length > 16) { throw new ParseError(this.pos, "Exceeded maximum decimal length"); } } if (type === "integer") { return parseInt(inputNumber, 10) * sign; } else { if (inputNumber.endsWith(".")) { throw new ParseError(this.pos, "Decimal cannot end on a period"); } if (inputNumber.split(".")[1].length > 3) { throw new ParseError(this.pos, "Number of digits after the decimal point cannot exceed 3"); } return parseFloat(inputNumber) * sign; } } parseString() { let outputString = ""; this.expectChar('"'); this.pos++; while (!this.eof()) { const char = this.getChar(); if (char === "\\") { if (this.eof()) { throw new ParseError(this.pos, "Unexpected end of input"); } const nextChar = this.getChar(); if (nextChar !== "\\" && nextChar !== '"') { throw new ParseError(this.pos, "A backslash must be followed by another backslash or double quote"); } outputString += nextChar; } else if (char === '"') { return outputString; } else if (!isAscii(char)) { throw new ParseError(this.pos, "Strings must be in the ASCII range"); } else { outputString += char; } } throw new ParseError(this.pos, "Unexpected end of input"); } parseDisplayString() { const chars = this.getChars(2); if (chars !== '%"') { throw new ParseError(this.pos, "Unexpected character. Display strings should start with %="); } const result = []; while (!this.eof()) { const char = this.getChar(); if (char.charCodeAt(0) <= 31 || char.charCodeAt(0) >= 127 && char.charCodeAt(0) <= 255) { throw new ParseError(this.pos, "Invalid char found in DisplayString. Did you forget to escape?"); } if (char === "%") { const hexChars = this.getChars(2); if (/^[0-9a-f]{2}$/.test(hexChars)) { result.push(parseInt(hexChars, 16)); } else { throw new ParseError(this.pos, `Unexpected sequence after % in DispalyString: "${hexChars}". Note that hexidecimals must be lowercase`); } continue; } if (char === '"') { const textDecoder = new TextDecoder("utf-8", { fatal: true }); try { return new DisplayString( textDecoder.decode(new Uint8Array(result)) ); } catch { throw new ParseError(this.pos, "Fatal error decoding UTF-8 sequence in Display String"); } } result.push(char.charCodeAt(0)); } throw new ParseError(this.pos, "Unexpected end of input"); } parseToken() { let outputString = ""; while (!this.eof()) { const char = this.lookChar(); if (char === void 0 || !/^[:/!#$%&'*+\-.^_`|~A-Za-z0-9]$/.test(char)) { return new Token(outputString); } outputString += this.getChar(); } return new Token(outputString); } parseByteSequence() { this.expectChar(":"); this.pos++; const endPos = this.input.indexOf(":", this.pos); if (endPos === -1) { throw new ParseError(this.pos, 'Could not find a closing ":" character to mark end of Byte Sequence'); } const b64Content = this.input.substring(this.pos, endPos); this.pos += b64Content.length + 1; if (!/^[A-Za-z0-9+/=]*$/.test(b64Content)) { throw new ParseError(this.pos, "ByteSequence does not contain a valid base64 string"); } try { return base64ToArrayBuffer(b64Content); } catch { throw new ParseError(this.pos, "ByteSequence does not contain a valid base64 string"); } } parseBoolean() { this.expectChar("?"); this.pos++; const char = this.getChar(); if (char === "1") { return true; } if (char === "0") { return false; } throw new ParseError(this.pos, 'Unexpected character. Expected a "1" or a "0"'); } parseDate() { this.expectChar("@"); this.pos++; let sign = 1; let inputNumber = ""; if (this.lookChar() === "-") { sign = -1; this.pos++; } if (!isDigit(this.lookChar())) { throw new ParseError(this.pos, "Expected a digit (0-9)"); } while (!this.eof()) { const char = this.getChar(); if (isDigit(char)) { inputNumber += char; } else { throw new ParseError(this.pos, "Expected a digit (0-9), whitespace or EOL"); } } return new Date(parseInt(inputNumber, 10) * sign * 1e3); } parseKey() { if (!this.lookChar()?.match(/^[a-z*]/)) { throw new ParseError(this.pos, "A key must begin with an asterisk or letter (a-z)"); } let outputString = ""; while (!this.eof()) { const char = this.lookChar(); if (char === void 0 || !/^[a-z0-9_\-.*]$/.test(char)) { return outputString; } outputString += this.getChar(); } return outputString; } /** * Looks at the next character without advancing the cursor. * * Returns undefined if we were at the end of the string. */ lookChar() { return this.input[this.pos]; } /** * Checks if the next character is 'char', and fail otherwise. */ expectChar(char) { if (this.lookChar() !== char) { throw new ParseError(this.pos, `Expected ${char}`); } } getChar() { return this.input[this.pos++]; } getChars(count) { const result = this.input.substr( this.pos, count ); this.pos += count; return result; } eof() { return this.pos >= this.input.length; } // Advances the pointer to skip all whitespace. skipOWS() { while (true) { const c = this.input.substr(this.pos, 1); if (c === " " || c === " ") { this.pos++; } else { break; } } } // Advances the pointer to skip all spaces skipWS() { while (this.lookChar() === " ") { this.pos++; } } // At the end of parsing, we need to make sure there are no bytes after the // header except whitespace. checkTrail() { this.skipWS(); if (!this.eof()) { throw new ParseError(this.pos, "Unexpected characters at end of input"); } } }; var isDigitRegex = /^[0-9]$/; function isDigit(char) { if (char === void 0) return false; return isDigitRegex.test(char); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { DisplayString, ParseError, SerializeError, Token, arrayBufferToBase64, base64ToArrayBuffer, isAscii, isInnerList, isValidKeyStr, isValidTokenStr, parseDictionary, parseItem, parseList, serializeBareItem, serializeBoolean, serializeByteSequence, serializeDate, serializeDecimal, serializeDictionary, serializeDisplayString, serializeInnerList, serializeInteger, serializeItem, serializeKey, serializeList, serializeParameters, serializeString, serializeToken }); //# sourceMappingURL=index.cjs.map