thi-ng/umbrella

View on GitHub
packages/bencode/src/encode.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { isArrayLike } from "@thi.ng/checks/is-arraylike";
import { isBoolean } from "@thi.ng/checks/is-boolean";
import { isNumber } from "@thi.ng/checks/is-number";
import { isPlainObject } from "@thi.ng/checks/is-plain-object";
import { isString } from "@thi.ng/checks/is-string";
import type { MultiFn1 } from "@thi.ng/defmulti";
import { defmulti } from "@thi.ng/defmulti/defmulti";
import { assert } from "@thi.ng/errors/assert";
import { unsupported } from "@thi.ng/errors/unsupported";
import { utf8Length } from "@thi.ng/strings/utf8";
import type { BinStructItem } from "@thi.ng/transducers-binary";
import { bytes, str, u8, u8array } from "@thi.ng/transducers-binary/bytes";
import { mapcat } from "@thi.ng/transducers/mapcat";

/** @internal */
const enum Type {
    INT,
    FLOAT,
    STR,
    BINARY,
    DICT,
    LIST,
}

/** @internal */
const enum Lit {
    DICT = 0x64,
    END = 0x65,
    LIST = 0x6c,
}

/** @internal */
const FLOAT_RE = /^[0-9.-]+$/;

export const encode = (x: any, cap = 1024) => bytes(cap, __encodeBin(x));

/** @internal */
const __encodeBin: MultiFn1<any, BinStructItem[]> = defmulti<
    any,
    BinStructItem[]
>(
    (x: any): Type =>
        isNumber(x)
            ? Math.floor(x) !== x
                ? Type.FLOAT
                : Type.INT
            : isBoolean(x)
            ? Type.INT
            : isString(x)
            ? Type.STR
            : x instanceof Uint8Array
            ? Type.BINARY
            : isPlainObject(x)
            ? Type.DICT
            : isArrayLike(x)
            ? Type.LIST
            : unsupported(`unsupported data type: ${x}`),
    {},
    {
        [Type.INT]: (x: number) => {
            __ensureValidNumber(x);
            return [str(`i${Math.floor(x)}e`)];
        },

        [Type.FLOAT]: (x: number) => {
            __ensureValidNumber(x);
            assert(
                FLOAT_RE.test(x.toString()),
                `values requiring exponential notation not allowed (${x})`
            );
            return [str(`f${x}e`)];
        },

        [Type.BINARY]: (buf: Uint8Array) => [
            str(buf.length + ":"),
            u8array(buf),
        ],

        [Type.STR]: (x: string) => [str(utf8Length(x) + ":" + x)],

        [Type.LIST]: (x: Iterable<any>) => [
            u8(Lit.LIST),
            ...mapcat(__encodeBin, x),
            u8(Lit.END),
        ],

        [Type.DICT]: (x: any) => [
            u8(Lit.DICT),
            ...mapcat(
                (k: string) => __encodeBin(k).concat(__encodeBin(x[k])),
                Object.keys(x).sort()
            ),
            u8(Lit.END),
        ],
    }
);

/** @internal */
const __ensureValidNumber = (x: number) => {
    assert(isFinite(x), `can't encode infinite value`);
    assert(!isNaN(x), `can't encode NaN`);
};