structured-headers
Version:
Implementation of Structured Field Values for HTTP (RFC9651, RFC8941)
198 lines (156 loc) • 5.25 kB
text/typescript
import {
BareItem,
Dictionary,
DictionaryObject,
InnerList,
Item,
List,
Parameters,
} from './types.js';
import { Token } from './token.js';
import { isAscii, isInnerList, isValidKeyStr, arrayBufferToBase64 } from './util.js';
import { DisplayString } from './displaystring.js';
export class SerializeError extends Error {}
export function serializeList(input: List): string {
return input.map(value => {
if (isInnerList(value)) {
return serializeInnerList(value);
} else {
return serializeItem(value);
}
}).join(', ');
}
export function serializeDictionary(input: Dictionary | DictionaryObject): string {
const entries: Iterable<[string, BareItem|Item|InnerList]> = 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(', ');
}
/**
* Serialize a Structured Fields Item.
*
* An Item is a standalone value like a string, number of date, followed by
* an optional set of parameters.
*
* You can either pass the value in the first argument and parameters in the second, or pass both as a tuple. The later exists for symmetry with parseItem.
*/
export function serializeItem(input: Item): string;
export function serializeItem(input: BareItem, params?: Parameters): string;
export function serializeItem(input: Item|BareItem, params?: Parameters): string {
if (Array.isArray(input)) {
return serializeBareItem(input[0]) + serializeParameters(input[1]);
} else {
return serializeBareItem(input) + (params?serializeParameters(params):'');
}
}
export function serializeInnerList(input: InnerList): string {
return `(${input[0].map(value => serializeItem(value)).join(' ')})${serializeParameters(input[1])}`;
}
export function serializeBareItem(input: BareItem): string {
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}`);
}
export function serializeInteger(input: number): string {
if (input < -999_999_999_999_999 || input > 999_999_999_999_999) {
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();
}
export function serializeDecimal(input: number): string {
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;
}
export function serializeString(input: string): string {
if (!isAscii(input)) {
throw new SerializeError('Only ASCII strings may be serialized');
}
return `"${input.replace(/("|\\)/g, (v) => '\\' + v)}"`;
}
export function serializeDisplayString(input: DisplayString): string {
let out = '%"';
const textEncoder = new TextEncoder();
for (const char of textEncoder.encode(input.toString())) {
if (
char === 0x25 // %
|| char === 0x22 // "
|| char <= 0x1f
|| char >= 0x7f
) {
out += '%' + char.toString(16);
} else {
out += String.fromCharCode(char);
}
}
return out + '"';
}
export function serializeBoolean(input: boolean): string {
return input ? '?1' : '?0';
}
export function serializeByteSequence(input: ArrayBuffer): string {
return `:${arrayBufferToBase64(input)}:`;
}
export function serializeToken(input: Token): string {
return input.toString();
}
export function serializeDate(input: Date): string {
return '@' + Math.floor(input.getTime()/1000);
}
export function serializeParameters(input: Parameters): string {
return Array.from(input).map(([key, value]) => {
let out = ';' + serializeKey(key);
if (value!==true) {
out+='=' + serializeBareItem(value);
}
return out;
}).join('');
}
export function serializeKey(input: string): string {
if (!isValidKeyStr(input)) {
throw new SerializeError('Keys in dictionaries must only contain lowercase letter, numbers, _-*. and must start with a letter or *');
}
return input;
}