structured-headers
Version:
Implementation of Structured Field Values for HTTP (RFC9651, RFC8941)
670 lines (664 loc) • 19.1 kB
JavaScript
;
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