From 45b7a90cb35928d29e6dd2190f3c40cc88af6888 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 18 Feb 2024 10:11:01 +1300 Subject: [PATCH] fix: inline `json-joy` dependency (#999) * feat: inline `json-joy` * feat: remove unneeded files * feat: remove even more unneeded files * chore: don't apply prettier to `json-joy` code --- .prettierignore | 1 + package.json | 2 +- src/fsa-to-node/json.ts | 4 +- src/json-joy/json-pack/JsonPackExtension.ts | 11 + src/json-joy/json-pack/JsonPackValue.ts | 13 + src/json-joy/json-pack/cbor/CborDecoder.ts | 408 +++++++++++ .../json-pack/cbor/CborDecoderBase.ts | 349 ++++++++++ src/json-joy/json-pack/cbor/CborEncoder.ts | 67 ++ .../json-pack/cbor/CborEncoderFast.ts | 333 +++++++++ src/json-joy/json-pack/cbor/constants.ts | 42 ++ src/json-joy/json-pack/cbor/types.ts | 1 + src/json-joy/json-pack/json/JsonDecoder.ts | 644 ++++++++++++++++++ src/json-joy/json-pack/json/JsonEncoder.ts | 238 +++++++ src/json-joy/json-pack/types.ts | 57 ++ src/json-joy/json-pointer/index.ts | 1 + src/json-joy/json-pointer/types.ts | 2 + src/json-joy/util/base64/constants.ts | 2 + .../util/base64/createFromBase64Bin.ts | 68 ++ src/json-joy/util/base64/createToBase64Bin.ts | 44 ++ src/json-joy/util/base64/fromBase64Bin.ts | 3 + src/json-joy/util/base64/toBase64Bin.ts | 3 + src/json-joy/util/buffers/Reader.ts | 105 +++ src/json-joy/util/buffers/Slice.ts | 12 + src/json-joy/util/buffers/Writer.ts | 276 ++++++++ src/json-joy/util/buffers/f16.ts | 16 + src/json-joy/util/buffers/index.ts | 1 + src/json-joy/util/buffers/isFloat32.ts | 6 + src/json-joy/util/buffers/types.ts | 126 ++++ .../util/buffers/utf8/CachedUtf8Decoder.ts | 51 ++ src/json-joy/util/buffers/utf8/decodeAscii.ts | 167 +++++ .../util/buffers/utf8/decodeUtf8/index.ts | 3 + .../util/buffers/utf8/decodeUtf8/v10.ts | 39 ++ .../util/buffers/utf8/decodeUtf8/v16.ts | 39 ++ .../util/buffers/utf8/decodeUtf8/v18.ts | 36 + .../buffers/utf8/sharedCachedUtf8Decoder.ts | 3 + src/json-joy/util/print/printTree.ts | 15 + src/print/index.ts | 2 +- src/snapshot/binary.ts | 6 +- src/snapshot/json.ts | 4 +- src/snapshot/shared.ts | 2 +- yarn.lock | 16 +- 41 files changed, 3193 insertions(+), 25 deletions(-) create mode 100644 .prettierignore create mode 100644 src/json-joy/json-pack/JsonPackExtension.ts create mode 100644 src/json-joy/json-pack/JsonPackValue.ts create mode 100644 src/json-joy/json-pack/cbor/CborDecoder.ts create mode 100644 src/json-joy/json-pack/cbor/CborDecoderBase.ts create mode 100644 src/json-joy/json-pack/cbor/CborEncoder.ts create mode 100644 src/json-joy/json-pack/cbor/CborEncoderFast.ts create mode 100644 src/json-joy/json-pack/cbor/constants.ts create mode 100644 src/json-joy/json-pack/cbor/types.ts create mode 100644 src/json-joy/json-pack/json/JsonDecoder.ts create mode 100644 src/json-joy/json-pack/json/JsonEncoder.ts create mode 100644 src/json-joy/json-pack/types.ts create mode 100644 src/json-joy/json-pointer/index.ts create mode 100644 src/json-joy/json-pointer/types.ts create mode 100644 src/json-joy/util/base64/constants.ts create mode 100644 src/json-joy/util/base64/createFromBase64Bin.ts create mode 100644 src/json-joy/util/base64/createToBase64Bin.ts create mode 100644 src/json-joy/util/base64/fromBase64Bin.ts create mode 100644 src/json-joy/util/base64/toBase64Bin.ts create mode 100644 src/json-joy/util/buffers/Reader.ts create mode 100644 src/json-joy/util/buffers/Slice.ts create mode 100644 src/json-joy/util/buffers/Writer.ts create mode 100644 src/json-joy/util/buffers/f16.ts create mode 100644 src/json-joy/util/buffers/index.ts create mode 100644 src/json-joy/util/buffers/isFloat32.ts create mode 100644 src/json-joy/util/buffers/types.ts create mode 100644 src/json-joy/util/buffers/utf8/CachedUtf8Decoder.ts create mode 100644 src/json-joy/util/buffers/utf8/decodeAscii.ts create mode 100644 src/json-joy/util/buffers/utf8/decodeUtf8/index.ts create mode 100644 src/json-joy/util/buffers/utf8/decodeUtf8/v10.ts create mode 100644 src/json-joy/util/buffers/utf8/decodeUtf8/v16.ts create mode 100644 src/json-joy/util/buffers/utf8/decodeUtf8/v18.ts create mode 100644 src/json-joy/util/buffers/utf8/sharedCachedUtf8Decoder.ts create mode 100644 src/json-joy/util/print/printTree.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..7f6da407 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +src/json-joy diff --git a/package.json b/package.json index e6e717ad..c4f5cd85 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ } }, "dependencies": { - "json-joy": "^11.0.0", + "hyperdyperid": "^1.2.0", "thingies": "^1.11.1", "tslib": "^2.0.0" }, diff --git a/src/fsa-to-node/json.ts b/src/fsa-to-node/json.ts index 8d89a237..8216404f 100644 --- a/src/fsa-to-node/json.ts +++ b/src/fsa-to-node/json.ts @@ -1,5 +1,5 @@ -import { CborEncoder } from 'json-joy/es6/json-pack/cbor/CborEncoder'; -import { CborDecoder } from 'json-joy/es6/json-pack/cbor/CborDecoder'; +import { CborEncoder } from '../json-joy/json-pack/cbor/CborEncoder'; +import { CborDecoder } from '../json-joy/json-pack/cbor/CborDecoder'; export const encoder = new CborEncoder(); export const decoder = new CborDecoder(); diff --git a/src/json-joy/json-pack/JsonPackExtension.ts b/src/json-joy/json-pack/JsonPackExtension.ts new file mode 100644 index 00000000..81ab8bbb --- /dev/null +++ b/src/json-joy/json-pack/JsonPackExtension.ts @@ -0,0 +1,11 @@ +/** + * A wrapping for MessagePack extension or CBOR tag value. When encoder + * encounters {@link JsonPackExtension} it will encode it as a MessagePack + * extension or CBOR tag. Likewise, the decoder will + * decode extensions into {@link JsonPackExtension}. + * + * @category Value + */ +export class JsonPackExtension { + constructor(public readonly tag: number, public readonly val: T) {} +} diff --git a/src/json-joy/json-pack/JsonPackValue.ts b/src/json-joy/json-pack/JsonPackValue.ts new file mode 100644 index 00000000..cdf7f41d --- /dev/null +++ b/src/json-joy/json-pack/JsonPackValue.ts @@ -0,0 +1,13 @@ +/** + * Use this wrapper is you have a pre-encoded MessagePack or CBOR value and you would + * like to dump it into a the document as-is. The contents of `buf` will + * be written as is to the document. + * + * It also serves as CBOR simple value container. In which case the type of value + * `val` field is "number". + * + * @category Value + */ +export class JsonPackValue { + constructor(public readonly val: T) {} +} diff --git a/src/json-joy/json-pack/cbor/CborDecoder.ts b/src/json-joy/json-pack/cbor/CborDecoder.ts new file mode 100644 index 00000000..64b92579 --- /dev/null +++ b/src/json-joy/json-pack/cbor/CborDecoder.ts @@ -0,0 +1,408 @@ +import {CONST, ERROR, MAJOR} from './constants'; +import {CborDecoderBase} from './CborDecoderBase'; +import {JsonPackValue} from '../JsonPackValue'; +import type {Path} from '../../json-pointer'; +import type {IReader, IReaderResettable} from '../../util/buffers'; + +export class CborDecoder< + R extends IReader & IReaderResettable = IReader & IReaderResettable, +> extends CborDecoderBase { + // -------------------------------------------------------------- Map reading + + public readAsMap(): Map { + const octet = this.reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + switch (major) { + case MAJOR.MAP: + return this.readMap(minor); + default: + throw ERROR.UNEXPECTED_MAJOR; + } + } + + public readMap(minor: number): Map { + const length = this.readMinorLen(minor); + if (length >= 0) return this.readMapRaw(length); + else return this.readMapIndef(); + } + + public readMapRaw(length: number): Map { + const map: Map = new Map(); + for (let i = 0; i < length; i++) { + const key = this.val(); + const value = this.val(); + map.set(key, value); + } + return map; + } + + public readMapIndef(): Map { + const map: Map = new Map(); + while (this.reader.peak() !== CONST.END) { + const key = this.val(); + if (this.reader.peak() === CONST.END) throw ERROR.UNEXPECTED_OBJ_BREAK; + const value = this.val(); + map.set(key, value); + } + this.reader.x++; + return map; + } + + // ----------------------------------------------------------- Value skipping + + public skipN(n: number): void { + for (let i = 0; i < n; i++) this.skipAny(); + } + public skipAny(): void { + this.skipAnyRaw(this.reader.u8()); + } + + public skipAnyRaw(octet: number): void { + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + switch (major) { + case MAJOR.UIN: + case MAJOR.NIN: + this.skipUNint(minor); + break; + case MAJOR.BIN: + this.skipBin(minor); + break; + case MAJOR.STR: + this.skipStr(minor); + break; + case MAJOR.ARR: + this.skipArr(minor); + break; + case MAJOR.MAP: + this.skipObj(minor); + break; + case MAJOR.TKN: + this.skipTkn(minor); + break; + case MAJOR.TAG: + this.skipTag(minor); + break; + } + } + + public skipMinorLen(minor: number): number { + if (minor <= 23) return minor; + switch (minor) { + case 24: + return this.reader.u8(); + case 25: + return this.reader.u16(); + case 26: + return this.reader.u32(); + case 27: + return Number(this.reader.u64()); + case 31: + return -1; + default: + throw ERROR.UNEXPECTED_MINOR; + } + } + + // --------------------------------------------------------- Integer skipping + + public skipUNint(minor: number): void { + if (minor <= 23) return; + switch (minor) { + case 24: + return this.reader.skip(1); + case 25: + return this.reader.skip(2); + case 26: + return this.reader.skip(4); + case 27: + return this.reader.skip(8); + default: + throw ERROR.UNEXPECTED_MINOR; + } + } + + // ---------------------------------------------------------- Binary skipping + + public skipBin(minor: number): void { + const length = this.skipMinorLen(minor); + if (length >= 0) this.reader.skip(length); + else { + while (this.reader.peak() !== CONST.END) this.skipBinChunk(); + this.reader.x++; + } + } + + public skipBinChunk(): void { + const octet = this.reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + if (major !== MAJOR.BIN) throw ERROR.UNEXPECTED_BIN_CHUNK_MAJOR; + if (minor > 27) throw ERROR.UNEXPECTED_BIN_CHUNK_MINOR; + this.skipBin(minor); + } + + // ---------------------------------------------------------- String skipping + + public skipStr(minor: number): void { + const length = this.skipMinorLen(minor); + if (length >= 0) this.reader.skip(length); + else { + while (this.reader.peak() !== CONST.END) this.skipStrChunk(); + this.reader.x++; + } + } + + public skipStrChunk(): void { + const octet = this.reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + if (major !== MAJOR.STR) throw ERROR.UNEXPECTED_STR_CHUNK_MAJOR; + if (minor > 27) throw ERROR.UNEXPECTED_STR_CHUNK_MINOR; + this.skipStr(minor); + } + + // ----------------------------------------------------------- Array skipping + + public skipArr(minor: number): void { + const length = this.skipMinorLen(minor); + if (length >= 0) this.skipN(length); + else { + while (this.reader.peak() !== CONST.END) this.skipAny(); + this.reader.x++; + } + } + + // ---------------------------------------------------------- Object skipping + + public skipObj(minor: number): void { + const length = this.readMinorLen(minor); + if (length >= 0) return this.skipN(length * 2); + else { + while (this.reader.peak() !== CONST.END) { + this.skipAny(); + if (this.reader.peak() === CONST.END) throw ERROR.UNEXPECTED_OBJ_BREAK; + this.skipAny(); + } + this.reader.x++; + } + } + + // ------------------------------------------------------------- Tag skipping + + public skipTag(minor: number): void { + const length = this.skipMinorLen(minor); + if (length < 0) throw ERROR.UNEXPECTED_MINOR; + this.skipAny(); + } + + // ----------------------------------------------------------- Token skipping + + public skipTkn(minor: number): void { + switch (minor) { + case 0xf8 & CONST.MINOR_MASK: + this.reader.skip(1); + return; + case 0xf9 & CONST.MINOR_MASK: + this.reader.skip(2); + return; + case 0xfa & CONST.MINOR_MASK: + this.reader.skip(4); + return; + case 0xfb & CONST.MINOR_MASK: + this.reader.skip(8); + return; + } + if (minor <= 23) return; + throw ERROR.UNEXPECTED_MINOR; + } + + // --------------------------------------------------------------- Validation + + /** + * Throws if at given offset in a buffer there is an invalid CBOR value, or + * if the value does not span the exact length specified in `size`. I.e. + * throws if: + * + * - The value is not a valid CBOR value. + * - The value is shorter than `size`. + * - The value is longer than `size`. + * + * @param value Buffer in which to validate CBOR value. + * @param offset Offset at which the value starts. + * @param size Expected size of the value. + */ + public validate(value: Uint8Array, offset: number = 0, size: number = value.length): void { + this.reader.reset(value); + this.reader.x = offset; + const start = offset; + this.skipAny(); + const end = this.reader.x; + if (end - start !== size) throw ERROR.INVALID_SIZE; + } + + // -------------------------------------------- One level reading - any value + + public decodeLevel(value: Uint8Array): unknown { + this.reader.reset(value); + return this.readLevel(); + } + + /** + * Decodes only one level of objects and arrays. Other values are decoded + * completely. + * + * @returns One level of decoded CBOR value. + */ + public readLevel(): unknown { + const octet = this.reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + switch (major) { + case MAJOR.ARR: + return this.readArrLevel(minor); + case MAJOR.MAP: + return this.readObjLevel(minor); + default: + return super.readAnyRaw(octet); + } + } + + /** + * Decodes primitive values, returns container values as `JsonPackValue`. + * + * @returns A primitive value, or CBOR container value as a blob. + */ + public readPrimitiveOrVal(): unknown | JsonPackValue { + const octet = this.reader.peak(); + const major = octet >> 5; + switch (major) { + case MAJOR.ARR: + case MAJOR.MAP: + return this.readAsValue(); + default: + return this.val(); + } + } + + public readAsValue(): JsonPackValue { + const reader = this.reader; + const start = reader.x; + this.skipAny(); + const end = reader.x; + return new JsonPackValue(reader.uint8.subarray(start, end)); + } + + // ----------------------------------------------- One level reading - object + + public readObjLevel(minor: number): Record { + const length = this.readMinorLen(minor); + if (length >= 0) return this.readObjRawLevel(length); + else return this.readObjIndefLevel(); + } + + public readObjRawLevel(length: number): Record { + const obj: Record = {}; + for (let i = 0; i < length; i++) { + const key = this.key(); + const value = this.readPrimitiveOrVal(); + obj[key] = value; + } + return obj; + } + + public readObjIndefLevel(): Record { + const obj: Record = {}; + while (this.reader.peak() !== CONST.END) { + const key = this.key(); + if (this.reader.peak() === CONST.END) throw ERROR.UNEXPECTED_OBJ_BREAK; + const value = this.readPrimitiveOrVal(); + obj[key] = value; + } + this.reader.x++; + return obj; + } + + // ------------------------------------------------ One level reading - array + + public readArrLevel(minor: number): unknown[] { + const length = this.readMinorLen(minor); + if (length >= 0) return this.readArrRawLevel(length); + return this.readArrIndefLevel(); + } + + public readArrRawLevel(length: number): unknown[] { + const arr: unknown[] = []; + for (let i = 0; i < length; i++) arr.push(this.readPrimitiveOrVal()); + return arr; + } + + public readArrIndefLevel(): unknown[] { + const arr: unknown[] = []; + while (this.reader.peak() !== CONST.END) arr.push(this.readPrimitiveOrVal()); + this.reader.x++; + return arr; + } + + // ---------------------------------------------------------- Shallow reading + + public readHdr(expectedMajor: number): number { + const octet = this.reader.u8(); + const major = octet >> 5; + if (major !== expectedMajor) throw ERROR.UNEXPECTED_MAJOR; + const minor = octet & CONST.MINOR_MASK; + if (minor < 24) return minor; + switch (minor) { + case 24: + return this.reader.u8(); + case 25: + return this.reader.u16(); + case 26: + return this.reader.u32(); + case 27: + return Number(this.reader.u64()); + case 31: + return -1; + } + throw ERROR.UNEXPECTED_MINOR; + } + + public readStrHdr(): number { + return this.readHdr(MAJOR.STR); + } + + public readObjHdr(): number { + return this.readHdr(MAJOR.MAP); + } + + public readArrHdr(): number { + return this.readHdr(MAJOR.ARR); + } + + public findKey(key: string): this { + const size = this.readObjHdr(); + for (let i = 0; i < size; i++) { + const k = this.key(); + if (k === key) return this; + this.skipAny(); + } + throw ERROR.KEY_NOT_FOUND; + } + + public findIndex(index: number): this { + const size = this.readArrHdr(); + if (index >= size) throw ERROR.INDEX_OUT_OF_BOUNDS; + for (let i = 0; i < index; i++) this.skipAny(); + return this; + } + + public find(path: Path): this { + for (let i = 0; i < path.length; i++) { + const segment = path[i]; + if (typeof segment === 'string') this.findKey(segment); + else this.findIndex(segment); + } + return this; + } +} diff --git a/src/json-joy/json-pack/cbor/CborDecoderBase.ts b/src/json-joy/json-pack/cbor/CborDecoderBase.ts new file mode 100644 index 00000000..3b27a6ea --- /dev/null +++ b/src/json-joy/json-pack/cbor/CborDecoderBase.ts @@ -0,0 +1,349 @@ +import {CONST, ERROR, MAJOR} from './constants'; +import {decodeF16} from '../../util/buffers/f16'; +import {JsonPackExtension} from '../JsonPackExtension'; +import {JsonPackValue} from '../JsonPackValue'; +import {Reader} from '../../util/buffers/Reader'; +import sharedCachedUtf8Decoder from '../../util/buffers/utf8/sharedCachedUtf8Decoder'; +import type {CachedUtf8Decoder} from '../../util/buffers/utf8/CachedUtf8Decoder'; +import type {IReader, IReaderResettable} from '../../util/buffers'; +import type {BinaryJsonDecoder, PackValue} from '../types'; + +export class CborDecoderBase + implements BinaryJsonDecoder +{ + public constructor( + public reader: R = new Reader() as any, + protected readonly keyDecoder: CachedUtf8Decoder = sharedCachedUtf8Decoder, + ) {} + + public read(uint8: Uint8Array): PackValue { + this.reader.reset(uint8); + return this.val() as PackValue; + } + + /** @deprecated */ + public decode(uint8: Uint8Array): unknown { + this.reader.reset(uint8); + return this.val(); + } + + // -------------------------------------------------------- Any value reading + + public val(): unknown { + const reader = this.reader; + const octet = reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + if (major < MAJOR.ARR) { + if (major < MAJOR.BIN) return major === MAJOR.UIN ? this.readUint(minor) : this.readNint(minor); + else return major === MAJOR.BIN ? this.readBin(minor) : this.readStr(minor); + } else { + if (major < MAJOR.TAG) return major === MAJOR.ARR ? this.readArr(minor) : this.readObj(minor); + else return major === MAJOR.TAG ? this.readTag(minor) : this.readTkn(minor); + } + } + + public readAnyRaw(octet: number): unknown { + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + if (major < MAJOR.ARR) { + if (major < MAJOR.BIN) return major === MAJOR.UIN ? this.readUint(minor) : this.readNint(minor); + else return major === MAJOR.BIN ? this.readBin(minor) : this.readStr(minor); + } else { + if (major < MAJOR.TAG) return major === MAJOR.ARR ? this.readArr(minor) : this.readObj(minor); + else return major === MAJOR.TAG ? this.readTag(minor) : this.readTkn(minor); + } + } + + public readMinorLen(minor: number): number { + if (minor < 24) return minor; + switch (minor) { + case 24: + return this.reader.u8(); + case 25: + return this.reader.u16(); + case 26: + return this.reader.u32(); + case 27: + return Number(this.reader.u64()); + case 31: + return -1; + default: + throw ERROR.UNEXPECTED_MINOR; + } + } + + // ----------------------------------------------------- Unsigned int reading + + public readUint(minor: number): number | bigint { + if (minor < 25) { + return minor === 24 ? this.reader.u8() : minor; + } else { + if (minor < 27) { + return minor === 25 ? this.reader.u16() : this.reader.u32(); + } else { + const num = this.reader.u64(); + return num > CONST.MAX_UINT ? num : Number(num); + } + } + } + + // ----------------------------------------------------- Negative int reading + + public readNint(minor: number): number | bigint { + if (minor < 25) { + return minor === 24 ? -this.reader.u8() - 1 : -minor - 1; + } else { + if (minor < 27) { + return minor === 25 ? -this.reader.u16() - 1 : -this.reader.u32() - 1; + } else { + const num = this.reader.u64(); + return num > CONST.MAX_UINT - 1 ? -num - BigInt(1) : -Number(num) - 1; + } + } + } + + // ----------------------------------------------------------- Binary reading + + public readBin(minor: number): Uint8Array { + const reader = this.reader; + if (minor <= 23) return reader.buf(minor); + switch (minor) { + case 24: + return reader.buf(reader.u8()); + case 25: + return reader.buf(reader.u16()); + case 26: + return reader.buf(reader.u32()); + case 27: + return reader.buf(Number(reader.u64())); + case 31: { + let size = 0; + const list: Uint8Array[] = []; + while (this.reader.peak() !== CONST.END) { + const uint8 = this.readBinChunk(); + size += uint8.length; + list.push(uint8); + } + this.reader.x++; + const res = new Uint8Array(size); + let offset = 0; + const length = list.length; + for (let i = 0; i < length; i++) { + const arr = list[i]; + res.set(arr, offset); + offset += arr.length; + } + return res; + } + default: + throw ERROR.UNEXPECTED_MINOR; + } + } + + public readBinChunk(): Uint8Array { + const octet = this.reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + if (major !== MAJOR.BIN) throw ERROR.UNEXPECTED_BIN_CHUNK_MAJOR; + if (minor > 27) throw ERROR.UNEXPECTED_BIN_CHUNK_MINOR; + return this.readBin(minor); + } + + // ----------------------------------------------------------- String reading + + public readAsStr(): string { + const reader = this.reader; + const octet = reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + if (major !== MAJOR.STR) throw ERROR.UNEXPECTED_STR_MAJOR; + return this.readStr(minor); + } + + public readStr(minor: number): string { + const reader = this.reader; + if (minor <= 23) return reader.utf8(minor); + switch (minor) { + case 24: + return reader.utf8(reader.u8()); + case 25: + return reader.utf8(reader.u16()); + case 26: + return reader.utf8(reader.u32()); + case 27: + return reader.utf8(Number(reader.u64())); + case 31: { + let str = ''; + while (reader.peak() !== CONST.END) str += this.readStrChunk(); + this.reader.x++; + return str; + } + default: + throw ERROR.UNEXPECTED_MINOR; + } + } + + public readStrLen(minor: number): number { + if (minor <= 23) return minor; + switch (minor) { + case 24: + return this.reader.u8(); + case 25: + return this.reader.u16(); + case 26: + return this.reader.u32(); + case 27: + return Number(this.reader.u64()); + default: + throw ERROR.UNEXPECTED_MINOR; + } + } + + public readStrChunk(): string { + const octet = this.reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + if (major !== MAJOR.STR) throw ERROR.UNEXPECTED_STR_CHUNK_MAJOR; + if (minor > 27) throw ERROR.UNEXPECTED_STR_CHUNK_MINOR; + return this.readStr(minor); + } + + // ------------------------------------------------------------ Array reading + + public readArr(minor: number): unknown[] { + const length = this.readMinorLen(minor); + if (length >= 0) return this.readArrRaw(length); + return this.readArrIndef(); + } + + public readArrRaw(length: number): unknown[] { + const arr: unknown[] = []; + for (let i = 0; i < length; i++) arr.push(this.val()); + return arr; + } + + public readArrIndef(): unknown[] { + const arr: unknown[] = []; + while (this.reader.peak() !== CONST.END) arr.push(this.val()); + this.reader.x++; + return arr; + } + + // ----------------------------------------------------------- Object reading + + public readObj(minor: number): Record { + if (minor < 28) { + let length = minor; + switch (minor) { + case 24: + length = this.reader.u8(); + break; + case 25: + length = this.reader.u16(); + break; + case 26: + length = this.reader.u32(); + break; + case 27: + length = Number(this.reader.u64()); + break; + } + const obj: Record = {}; + for (let i = 0; i < length; i++) { + const key = this.key(); + if (key === '__proto__') throw ERROR.UNEXPECTED_OBJ_KEY; + const value = this.val(); + obj[key] = value; + } + return obj; + } else if (minor === 31) return this.readObjIndef(); + else throw ERROR.UNEXPECTED_MINOR; + } + + /** Remove this? */ + public readObjRaw(length: number): Record { + const obj: Record = {}; + for (let i = 0; i < length; i++) { + const key = this.key(); + const value = this.val(); + obj[key] = value; + } + return obj; + } + + public readObjIndef(): Record { + const obj: Record = {}; + while (this.reader.peak() !== CONST.END) { + const key = this.key(); + if (this.reader.peak() === CONST.END) throw ERROR.UNEXPECTED_OBJ_BREAK; + const value = this.val(); + obj[key] = value; + } + this.reader.x++; + return obj; + } + + public key(): string { + const octet = this.reader.u8(); + const major = octet >> 5; + const minor = octet & CONST.MINOR_MASK; + if (major !== MAJOR.STR) return String(this.readAnyRaw(octet)); + const length = this.readStrLen(minor); + if (length > 31) return this.reader.utf8(length); + const key = this.keyDecoder.decode(this.reader.uint8, this.reader.x, length); + this.reader.skip(length); + return key; + } + + // -------------------------------------------------------------- Tag reading + + public readTag(minor: number): JsonPackExtension | unknown { + if (minor <= 23) return this.readTagRaw(minor); + switch (minor) { + case 24: + return this.readTagRaw(this.reader.u8()); + case 25: + return this.readTagRaw(this.reader.u16()); + case 26: + return this.readTagRaw(this.reader.u32()); + case 27: + return this.readTagRaw(Number(this.reader.u64())); + default: + throw ERROR.UNEXPECTED_MINOR; + } + } + + public readTagRaw(tag: number): JsonPackExtension | unknown { + return new JsonPackExtension(tag, this.val()); + } + + // ------------------------------------------------------------ Token reading + + public readTkn(minor: number): number | true | false | null | undefined | JsonPackValue { + switch (minor) { + case 0xf4 & CONST.MINOR_MASK: + return false; + case 0xf5 & CONST.MINOR_MASK: + return true; + case 0xf6 & CONST.MINOR_MASK: + return null; + case 0xf7 & CONST.MINOR_MASK: + return undefined; + case 0xf8 & CONST.MINOR_MASK: + return new JsonPackValue(this.reader.u8()); + case 0xf9 & CONST.MINOR_MASK: + return this.f16(); + case 0xfa & CONST.MINOR_MASK: + return this.reader.f32(); + case 0xfb & CONST.MINOR_MASK: + return this.reader.f64(); + } + if (minor <= 23) return new JsonPackValue(minor); + throw ERROR.UNEXPECTED_MINOR; + } + + public f16(): number { + return decodeF16(this.reader.u16()); + } +} diff --git a/src/json-joy/json-pack/cbor/CborEncoder.ts b/src/json-joy/json-pack/cbor/CborEncoder.ts new file mode 100644 index 00000000..63587f50 --- /dev/null +++ b/src/json-joy/json-pack/cbor/CborEncoder.ts @@ -0,0 +1,67 @@ +import {isFloat32} from '../../util/buffers/isFloat32'; +import {JsonPackExtension} from '../JsonPackExtension'; +import {CborEncoderFast} from './CborEncoderFast'; +import type {IWriter, IWriterGrowable} from '../../util/buffers'; + +export class CborEncoder extends CborEncoderFast { + /** + * Called when the encoder encounters a value that it does not know how to encode. + * + * @param value Some JavaScript value. + */ + public writeUnknown(value: unknown): void { + this.writeNull(); + } + + public writeAny(value: unknown): void { + switch (typeof value) { + case 'number': + return this.writeNumber(value as number); + case 'string': + return this.writeStr(value); + case 'boolean': + return this.writer.u8(0xf4 + +value); + case 'object': { + if (!value) return this.writer.u8(0xf6); + const constructor = value.constructor; + switch (constructor) { + case Object: + return this.writeObj(value as Record); + case Array: + return this.writeArr(value as unknown[]); + case Uint8Array: + return this.writeBin(value as Uint8Array); + case Map: + return this.writeMap(value as Map); + case JsonPackExtension: + return this.writeTag((value).tag, (value).val); + default: + return this.writeUnknown(value); + } + } + case 'undefined': + return this.writeUndef(); + case 'bigint': + return this.writeBigInt(value as bigint); + default: + return this.writeUnknown(value); + } + } + + public writeFloat(float: number): void { + if (isFloat32(float)) this.writer.u8f32(0xfa, float); + else this.writer.u8f64(0xfb, float); + } + + public writeMap(map: Map): void { + this.writeMapHdr(map.size); + map.forEach((value, key) => { + this.writeAny(key); + this.writeAny(value); + }); + } + + public writeUndef(): void { + this.writer.u8(0xf7); + } +} diff --git a/src/json-joy/json-pack/cbor/CborEncoderFast.ts b/src/json-joy/json-pack/cbor/CborEncoderFast.ts new file mode 100644 index 00000000..b54ff07c --- /dev/null +++ b/src/json-joy/json-pack/cbor/CborEncoderFast.ts @@ -0,0 +1,333 @@ +import {Writer} from '../../util/buffers/Writer'; +import {CONST, MAJOR_OVERLAY} from './constants'; +import type {IWriter, IWriterGrowable} from '../../util/buffers'; +import type {BinaryJsonEncoder, StreamingBinaryJsonEncoder, TlvBinaryJsonEncoder} from '../types'; +import type {Slice} from '../../util/buffers/Slice'; + +const isSafeInteger = Number.isSafeInteger; + +/** + * Fast CBOR encoder supports only JSON values. Use regular `CborEncoder` if + * you need ability to encode all CBOR value types. + */ +export class CborEncoderFast + implements BinaryJsonEncoder, StreamingBinaryJsonEncoder, TlvBinaryJsonEncoder +{ + constructor(public readonly writer: W = new Writer() as any) {} + + public encode(value: unknown): Uint8Array { + this.writeAny(value); + return this.writer.flush(); + } + + public encodeToSlice(value: unknown): Slice { + this.writeAny(value); + return this.writer.flushSlice(); + } + + public writeAny(value: unknown): void { + switch (typeof value) { + case 'number': + return this.writeNumber(value as number); + case 'string': + return this.writeStr(value); + case 'boolean': + return this.writer.u8(0xf4 + +value); + case 'object': { + if (!value) return this.writer.u8(0xf6); + const constructor = value.constructor; + switch (constructor) { + case Array: + return this.writeArr(value as unknown[]); + default: + return this.writeObj(value as Record); + } + } + } + } + + public writeCbor(): void { + this.writer.u8u16(0xd9, 0xd9f7); + } + + public writeEnd(): void { + this.writer.u8(CONST.END); + } + + public writeNull(): void { + this.writer.u8(0xf6); + } + + public writeBoolean(bool: boolean): void { + if (bool) this.writer.u8(0xf5); + else this.writer.u8(0xf4); + } + + public writeNumber(num: number): void { + if (isSafeInteger(num)) this.writeInteger(num); + else if (typeof num === 'bigint') this.writeBigInt(num); + else this.writeFloat(num); + } + + public writeBigInt(int: bigint): void { + if (int >= 0) this.writeBigUint(int); + else this.writeBigSint(int); + } + + public writeBigUint(uint: bigint): void { + if (uint <= Number.MAX_SAFE_INTEGER) return this.writeUInteger(Number(uint)); + this.writer.u8u64(0x1b, uint); + } + + public writeBigSint(int: bigint): void { + if (int >= Number.MIN_SAFE_INTEGER) return this.encodeNint(Number(int)); + const uint = -BigInt(1) - int; + this.writer.u8u64(0x3b, uint); + } + + public writeInteger(int: number): void { + if (int >= 0) this.writeUInteger(int); + else this.encodeNint(int); + } + + public writeUInteger(uint: number): void { + const writer = this.writer; + writer.ensureCapacity(9); + const uint8 = writer.uint8; + let x = writer.x; + if (uint <= 23) { + uint8[x++] = MAJOR_OVERLAY.UIN + uint; + } else if (uint <= 0xff) { + uint8[x++] = 0x18; + uint8[x++] = uint; + } else if (uint <= 0xffff) { + uint8[x++] = 0x19; + writer.view.setUint16(x, uint); + x += 2; + } else if (uint <= 0xffffffff) { + uint8[x++] = 0x1a; + writer.view.setUint32(x, uint); + x += 4; + } else { + uint8[x++] = 0x1b; + writer.view.setBigUint64(x, BigInt(uint)); + x += 8; + } + writer.x = x; + } + + /** @deprecated Remove and use `writeNumber` instead. */ + public encodeNumber(num: number): void { + this.writeNumber(num); + } + + /** @deprecated Remove and use `writeInteger` instead. */ + public encodeInteger(int: number): void { + this.writeInteger(int); + } + + /** @deprecated */ + public encodeUint(uint: number): void { + this.writeUInteger(uint); + } + + public encodeNint(int: number): void { + const uint = -1 - int; + const writer = this.writer; + writer.ensureCapacity(9); + const uint8 = writer.uint8; + let x = writer.x; + if (uint < 24) { + uint8[x++] = MAJOR_OVERLAY.NIN + uint; + } else if (uint <= 0xff) { + uint8[x++] = 0x38; + uint8[x++] = uint; + } else if (uint <= 0xffff) { + uint8[x++] = 0x39; + writer.view.setUint16(x, uint); + x += 2; + } else if (uint <= 0xffffffff) { + uint8[x++] = 0x3a; + writer.view.setUint32(x, uint); + x += 4; + } else { + uint8[x++] = 0x3b; + writer.view.setBigUint64(x, BigInt(uint)); + x += 8; + } + writer.x = x; + } + + public writeFloat(float: number): void { + this.writer.u8f64(0xfb, float); + } + + public writeBin(buf: Uint8Array): void { + const length = buf.length; + this.writeBinHdr(length); + this.writer.buf(buf, length); + } + + public writeBinHdr(length: number): void { + const writer = this.writer; + if (length <= 23) writer.u8(MAJOR_OVERLAY.BIN + length); + else if (length <= 0xff) writer.u16((0x58 << 8) + length); + else if (length <= 0xffff) writer.u8u16(0x59, length); + else if (length <= 0xffffffff) writer.u8u32(0x5a, length); + else writer.u8u64(0x5b, length); + } + + public writeStr(str: string): void { + const writer = this.writer; + const length = str.length; + const maxSize = length * 4; + writer.ensureCapacity(5 + maxSize); + const uint8 = writer.uint8; + let lengthOffset: number = writer.x; + if (maxSize <= 23) writer.x++; + else if (maxSize <= 0xff) { + uint8[writer.x++] = 0x78; + lengthOffset = writer.x; + writer.x++; + } else if (maxSize <= 0xffff) { + uint8[writer.x++] = 0x79; + lengthOffset = writer.x; + writer.x += 2; + } else { + uint8[writer.x++] = 0x7a; + lengthOffset = writer.x; + writer.x += 4; + } + const bytesWritten = writer.utf8(str); + if (maxSize <= 23) uint8[lengthOffset] = MAJOR_OVERLAY.STR + bytesWritten; + else if (maxSize <= 0xff) uint8[lengthOffset] = bytesWritten; + else if (maxSize <= 0xffff) writer.view.setUint16(lengthOffset, bytesWritten); + else writer.view.setUint32(lengthOffset, bytesWritten); + } + + public writeStrHdr(length: number): void { + const writer = this.writer; + if (length <= 23) writer.u8(MAJOR_OVERLAY.STR + length); + else if (length <= 0xff) writer.u16((0x78 << 8) + length); + else if (length <= 0xffff) writer.u8u16(0x79, length); + else writer.u8u32(0x7a, length); + } + + public writeAsciiStr(str: string): void { + this.writeStrHdr(str.length); + this.writer.ascii(str); + } + + public writeArr(arr: unknown[]): void { + const length = arr.length; + this.writeArrHdr(length); + for (let i = 0; i < length; i++) this.writeAny(arr[i]); + } + + public writeArrHdr(length: number): void { + const writer = this.writer; + if (length <= 23) writer.u8(MAJOR_OVERLAY.ARR + length); + else if (length <= 0xff) writer.u16((0x98 << 8) + length); + else if (length <= 0xffff) writer.u8u16(0x99, length); + else if (length <= 0xffffffff) writer.u8u32(0x9a, length); + else writer.u8u64(0x9b, length); + } + + public writeObj(obj: Record): void { + const keys = Object.keys(obj); + const length = keys.length; + this.writeObjHdr(length); + for (let i = 0; i < length; i++) { + const key = keys[i]; + this.writeStr(key); + this.writeAny(obj[key]); + } + } + + public writeObjHdr(length: number): void { + const writer = this.writer; + if (length <= 23) writer.u8(MAJOR_OVERLAY.MAP + length); + else if (length <= 0xff) writer.u16((0xb8 << 8) + length); + else if (length <= 0xffff) writer.u8u16(0xb9, length); + else if (length <= 0xffffffff) writer.u8u32(0xba, length); + else writer.u8u64(0xbb, length); + } + + public writeMapHdr(length: number): void { + this.writeObjHdr(length); + } + + public writeStartMap(): void { + this.writer.u8(0xbf); + } + + public writeTag(tag: number, value: unknown): void { + this.writeTagHdr(tag); + this.writeAny(value); + } + + public writeTagHdr(tag: number): void { + const writer = this.writer; + if (tag <= 23) writer.u8(MAJOR_OVERLAY.TAG + tag); + else if (tag <= 0xff) writer.u16((0xd8 << 8) + tag); + else if (tag <= 0xffff) writer.u8u16(0xd9, tag); + else if (tag <= 0xffffffff) writer.u8u32(0xda, tag); + else writer.u8u64(0xdb, tag); + } + + public writeTkn(value: number): void { + const writer = this.writer; + if (value <= 23) writer.u8(MAJOR_OVERLAY.TKN + value); + else if (value <= 0xff) writer.u16((0xf8 << 8) + value); + } + + // ------------------------------------------------------- Streaming encoding + + public writeStartStr(): void { + this.writer.u8(0x7f); + } + + public writeStrChunk(str: string): void { + throw new Error('Not implemented'); + } + + public writeEndStr(): void { + throw new Error('Not implemented'); + } + + public writeStartBin(): void { + this.writer.u8(0x5f); + } + + public writeBinChunk(buf: Uint8Array): void { + throw new Error('Not implemented'); + } + + public writeEndBin(): void { + throw new Error('Not implemented'); + } + + public writeStartArr(): void { + this.writer.u8(0x9f); + } + + public writeArrChunk(item: unknown): void { + throw new Error('Not implemented'); + } + + public writeEndArr(): void { + this.writer.u8(CONST.END); + } + + public writeStartObj(): void { + this.writer.u8(0xbf); + } + + public writeObjChunk(key: string, value: unknown): void { + throw new Error('Not implemented'); + } + + public writeEndObj(): void { + this.writer.u8(CONST.END); + } +} diff --git a/src/json-joy/json-pack/cbor/constants.ts b/src/json-joy/json-pack/cbor/constants.ts new file mode 100644 index 00000000..86b3a5ae --- /dev/null +++ b/src/json-joy/json-pack/cbor/constants.ts @@ -0,0 +1,42 @@ +export const enum MAJOR { + UIN = 0b000, + NIN = 0b001, + BIN = 0b010, + STR = 0b011, + ARR = 0b100, + MAP = 0b101, + TAG = 0b110, + TKN = 0b111, +} + +export const enum MAJOR_OVERLAY { + UIN = 0b000_00000, + NIN = 0b001_00000, + BIN = 0b010_00000, + STR = 0b011_00000, + ARR = 0b100_00000, + MAP = 0b101_00000, + TAG = 0b110_00000, + TKN = 0b111_00000, +} + +export const enum CONST { + MINOR_MASK = 0b11111, + MAX_UINT = 9007199254740991, + END = 0xff, +} + +export const enum ERROR { + UNEXPECTED_MAJOR, + UNEXPECTED_MINOR, + UNEXPECTED_BIN_CHUNK_MAJOR, + UNEXPECTED_BIN_CHUNK_MINOR, + UNEXPECTED_STR_CHUNK_MAJOR, + UNEXPECTED_STR_CHUNK_MINOR, + UNEXPECTED_OBJ_KEY, + UNEXPECTED_OBJ_BREAK, + INVALID_SIZE, + KEY_NOT_FOUND, + INDEX_OUT_OF_BOUNDS, + UNEXPECTED_STR_MAJOR, +} diff --git a/src/json-joy/json-pack/cbor/types.ts b/src/json-joy/json-pack/cbor/types.ts new file mode 100644 index 00000000..d80243c3 --- /dev/null +++ b/src/json-joy/json-pack/cbor/types.ts @@ -0,0 +1 @@ +export type CborUint8Array = Uint8Array & {__BRAND__: 'cbor'; __TYPE__: T}; diff --git a/src/json-joy/json-pack/json/JsonDecoder.ts b/src/json-joy/json-pack/json/JsonDecoder.ts new file mode 100644 index 00000000..7522fe96 --- /dev/null +++ b/src/json-joy/json-pack/json/JsonDecoder.ts @@ -0,0 +1,644 @@ +import {decodeUtf8} from '../../util/buffers/utf8/decodeUtf8'; +import {Reader} from '../../util/buffers/Reader'; +import {fromBase64Bin} from '../../util/base64/fromBase64Bin'; +import type {BinaryJsonDecoder, PackValue} from '../types'; + +const REGEX_REPLACE_ESCAPED_CHARS = /\\(b|f|n|r|t|"|\/|\\)/g; +const escapedCharReplacer = (char: string) => { + switch (char) { + case '\\b': + return '\b'; + case '\\f': + return '\f'; + case '\\n': + return '\n'; + case '\\r': + return '\r'; + case '\\t': + return '\t'; + case '\\"': + return '"'; + case '\\/': + return '/'; + case '\\\\': + return '\\'; + } + return char; +}; + +// Starts with "data:application/octet-stream;base64," - 64 61 74 61 3a 61 70 70 6c 69 63 61 74 69 6f 6e 2f 6f 63 74 65 74 2d 73 74 72 65 61 6d 3b 62 61 73 65 36 34 2c +const hasBinaryPrefix = (u8: Uint8Array, x: number) => + u8[x] === 0x64 && + u8[x + 1] === 0x61 && + u8[x + 2] === 0x74 && + u8[x + 3] === 0x61 && + u8[x + 4] === 0x3a && + u8[x + 5] === 0x61 && + u8[x + 6] === 0x70 && + u8[x + 7] === 0x70 && + u8[x + 8] === 0x6c && + u8[x + 9] === 0x69 && + u8[x + 10] === 0x63 && + u8[x + 11] === 0x61 && + u8[x + 12] === 0x74 && + u8[x + 13] === 0x69 && + u8[x + 14] === 0x6f && + u8[x + 15] === 0x6e && + u8[x + 16] === 0x2f && + u8[x + 17] === 0x6f && + u8[x + 18] === 0x63 && + u8[x + 19] === 0x74 && + u8[x + 20] === 0x65 && + u8[x + 21] === 0x74 && + u8[x + 22] === 0x2d && + u8[x + 23] === 0x73 && + u8[x + 24] === 0x74 && + u8[x + 25] === 0x72 && + u8[x + 26] === 0x65 && + u8[x + 27] === 0x61 && + u8[x + 28] === 0x6d && + u8[x + 29] === 0x3b && + u8[x + 30] === 0x62 && + u8[x + 31] === 0x61 && + u8[x + 32] === 0x73 && + u8[x + 33] === 0x65 && + u8[x + 34] === 0x36 && + u8[x + 35] === 0x34 && + u8[x + 36] === 0x2c; + +const findEndingQuote = (uint8: Uint8Array, x: number): number => { + const len = uint8.length; + let char = uint8[x]; + let prev = 0; + while (x < len) { + if (char === 34 && prev !== 92) break; + if (char === 92 && prev === 92) prev = 0; + else prev = char; + char = uint8[++x]; + } + if (x === len) throw new Error('Invalid JSON'); + return x; +}; + +const fromCharCode = String.fromCharCode; + +const readShortUtf8StrAndUnescape = (reader: Reader): string => { + const buf = reader.uint8; + const len = buf.length; + const points: number[] = []; + let x = reader.x; + let prev = 0; + while (x < len) { + let code = buf[x++]!; + if ((code & 0x80) === 0) { + if (prev === 92) { + switch (code) { + case 98: // \b + code = 8; + break; + case 102: // \f + code = 12; + break; + case 110: // \n + code = 10; + break; + case 114: // \r + code = 13; + break; + case 116: // \t + code = 9; + break; + case 34: // \" + code = 34; + break; + case 47: // \/ + code = 47; + break; + case 92: // \\ + code = 92; + break; + default: + throw new Error('Invalid JSON'); + } + prev = 0; + } else { + if (code === 34) break; + prev = code; + if (prev === 92) continue; + } + } else { + const octet2 = buf[x++]! & 0x3f; + if ((code & 0xe0) === 0xc0) { + code = ((code & 0x1f) << 6) | octet2; + } else { + const octet3 = buf[x++]! & 0x3f; + if ((code & 0xf0) === 0xe0) { + code = ((code & 0x1f) << 12) | (octet2 << 6) | octet3; + } else { + if ((code & 0xf8) === 0xf0) { + const octet4 = buf[x++]! & 0x3f; + let unit = ((code & 0x07) << 0x12) | (octet2 << 0x0c) | (octet3 << 0x06) | octet4; + if (unit > 0xffff) { + unit -= 0x10000; + const unit0 = ((unit >>> 10) & 0x3ff) | 0xd800; + unit = 0xdc00 | (unit & 0x3ff); + points.push(unit0); + code = unit; + } else { + code = unit; + } + } + } + } + } + points.push(code); + } + reader.x = x; + return fromCharCode.apply(String, points); +}; + +export class JsonDecoder implements BinaryJsonDecoder { + public reader = new Reader(); + + public read(uint8: Uint8Array): PackValue { + this.reader.reset(uint8); + return this.readAny(); + } + + public readAny(): PackValue { + this.skipWhitespace(); + const reader = this.reader; + const x = reader.x; + const uint8 = reader.uint8; + const char = uint8[x]; + switch (char) { + case 34: // " + return uint8[x + 1] === 0x64 // d + ? this.tryReadBin() || this.readStr() + : this.readStr(); + case 91: // [ + return this.readArr(); + case 102: // f + return this.readFalse(); + case 110: // n + return this.readNull(); + case 116: // t + return this.readTrue(); + case 123: // { + return this.readObj(); + default: + if ((char >= 48 && char <= 57) || char === 45) return this.readNum(); + throw new Error('Invalid JSON'); + } + } + + public skipWhitespace(): void { + const reader = this.reader; + const uint8 = reader.uint8; + let x = reader.x; + let char: number = 0; + while (true) { + char = uint8[x]; + switch (char) { + case 32: // space + case 9: // tab + case 10: // line feed + case 13: // carriage return + x++; + continue; + default: + reader.x = x; + return; + } + } + } + + public readNull(): null { + if (this.reader.u32() !== 0x6e756c6c) throw new Error('Invalid JSON'); + return null; + } + + public readTrue(): true { + if (this.reader.u32() !== 0x74727565) throw new Error('Invalid JSON'); + return true; + } + + public readFalse(): false { + const reader = this.reader; + if (reader.u8() !== 0x66 || reader.u32() !== 0x616c7365) throw new Error('Invalid JSON'); + return false; + } + + public readBool(): unknown { + const reader = this.reader; + switch (reader.uint8[reader.x]) { + case 102: // f + return this.readFalse(); + case 116: // t + return this.readTrue(); + default: + throw new Error('Invalid JSON'); + } + } + + public readNum(): number { + const reader = this.reader; + const uint8 = reader.uint8; + let x = reader.x; + let c = uint8[x++]; + const c1 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 43 && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c2 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 43 && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c3 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 43 && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c4 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 43 && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c5 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 43 && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c6 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 43 && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c7 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 43 && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c8 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 43 && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c9 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c10 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c11 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c12 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c13 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c14 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c15 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c16 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c17 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c18 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c19 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c20 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode( + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + ); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c21 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode( + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + ); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c22 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode( + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + c22, + ); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c23 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode( + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + c22, + c23, + ); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + const c24 = c; + c = uint8[x++]; + if (!c || ((c < 45 || c > 57) && c !== 69 && c !== 101)) { + reader.x = x - 1; + const num = +fromCharCode( + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + c22, + c24, + ); + if (num !== num) throw new Error('Invalid JSON'); + return num; + } + throw new Error('Invalid JSON'); + } + + public readStr(): string { + const reader = this.reader; + const uint8 = reader.uint8; + const char = uint8[reader.x++]; + if (char !== 0x22) throw new Error('Invalid JSON'); + const x0 = reader.x; + const x1 = findEndingQuote(uint8, x0); + let str = decodeUtf8(uint8, x0, x1 - x0); + /** @todo perf: maybe faster is to first check if there are any escaped chars. */ + str = str.replace(REGEX_REPLACE_ESCAPED_CHARS, escapedCharReplacer); + reader.x = x1 + 1; + return str; + } + + public tryReadBin(): Uint8Array | undefined { + const reader = this.reader; + const u8 = reader.uint8; + let x = reader.x; + if (u8[x++] !== 0x22) return undefined; + const hasDataUrlPrefix = hasBinaryPrefix(u8, x); + if (!hasDataUrlPrefix) return undefined; + x += 37; + const x0 = x; + x = findEndingQuote(u8, x); + reader.x = x0; + const bin = fromBase64Bin(reader.view, x0, x - x0); + reader.x = x + 1; + return bin; + } + + public readBin(): Uint8Array { + const reader = this.reader; + const u8 = reader.uint8; + let x = reader.x; + if (u8[x++] !== 0x22) throw new Error('Invalid JSON'); + const hasDataUrlPrefix = hasBinaryPrefix(u8, x); + if (!hasDataUrlPrefix) throw new Error('Invalid JSON'); + x += 37; + const x0 = x; + x = findEndingQuote(u8, x); + reader.x = x0; + const bin = fromBase64Bin(reader.view, x0, x - x0); + reader.x = x + 1; + return bin; + } + + public readArr(): PackValue[] { + const reader = this.reader; + if (reader.u8() !== 0x5b) throw new Error('Invalid JSON'); + const arr: PackValue[] = []; + const uint8 = reader.uint8; + while (true) { + this.skipWhitespace(); + const char = uint8[reader.x]; + if (char === 0x5d) return reader.x++, arr; // ] + if (char === 0x2c) { + reader.x++; + continue; + } // , + arr.push(this.readAny()); + } + } + + public readObj(): Record { + const reader = this.reader; + if (reader.u8() !== 0x7b) throw new Error('Invalid JSON'); + const obj: Record = {}; + const uint8 = reader.uint8; + while (true) { + this.skipWhitespace(); + let char = uint8[reader.x]; + if (char === 0x7d) return reader.x++, obj; // } + if (char === 0x2c) { + reader.x++; + continue; + } // , + char = uint8[reader.x++]; + if (char !== 0x22) throw new Error('Invalid JSON'); + const key = readShortUtf8StrAndUnescape(reader); + if (key === '__proto__') throw new Error('Invalid JSON'); + this.skipWhitespace(); + if (reader.u8() !== 0x3a) throw new Error('Invalid JSON'); + this.skipWhitespace(); + obj[key] = this.readAny(); + } + } +} diff --git a/src/json-joy/json-pack/json/JsonEncoder.ts b/src/json-joy/json-pack/json/JsonEncoder.ts new file mode 100644 index 00000000..b7f8cf35 --- /dev/null +++ b/src/json-joy/json-pack/json/JsonEncoder.ts @@ -0,0 +1,238 @@ +import {toBase64Bin} from '../../util/base64/toBase64Bin'; +import type {IWriter, IWriterGrowable} from '../../util/buffers'; +import type {BinaryJsonEncoder, StreamingBinaryJsonEncoder} from '../types'; + +export class JsonEncoder implements BinaryJsonEncoder, StreamingBinaryJsonEncoder { + constructor(public readonly writer: IWriter & IWriterGrowable) {} + + public encode(value: unknown): Uint8Array { + const writer = this.writer; + writer.reset(); + this.writeAny(value); + return writer.flush(); + } + + public writeAny(value: unknown): void { + switch (typeof value) { + case 'boolean': + return this.writeBoolean(value); + case 'number': + return this.writeNumber(value as number); + case 'string': + return this.writeStr(value); + case 'object': { + if (value === null) return this.writeNull(); + const constructor = value.constructor; + switch (constructor) { + case Array: + return this.writeArr(value as unknown[]); + case Uint8Array: + return this.writeBin(value as Uint8Array); + default: + return this.writeObj(value as Record); + } + } + default: + return this.writeNull(); + } + } + + public writeNull(): void { + this.writer.u32(0x6e756c6c); // null + } + + public writeBoolean(bool: boolean): void { + if (bool) this.writer.u32(0x74727565); // true + else this.writer.u8u32(0x66, 0x616c7365); // false + } + + public writeNumber(num: number): void { + const str = num.toString(); + this.writer.ascii(str); + } + + public writeInteger(int: number): void { + this.writeNumber(int >> 0 === int ? int : Math.trunc(int)); + } + + public writeUInteger(uint: number): void { + this.writeInteger(uint < 0 ? -uint : uint); + } + + public writeFloat(float: number): void { + this.writeNumber(float); + } + + public writeBin(buf: Uint8Array): void { + const writer = this.writer; + const length = buf.length; + writer.ensureCapacity(38 + 3 + (length << 1)); + // Write: "data:application/octet-stream;base64, - 22 64 61 74 61 3a 61 70 70 6c 69 63 61 74 69 6f 6e 2f 6f 63 74 65 74 2d 73 74 72 65 61 6d 3b 62 61 73 65 36 34 2c + const view = writer.view; + let x = writer.x; + view.setUint32(x, 0x22_64_61_74); // "dat + x += 4; + view.setUint32(x, 0x61_3a_61_70); // a:ap + x += 4; + view.setUint32(x, 0x70_6c_69_63); // plic + x += 4; + view.setUint32(x, 0x61_74_69_6f); // atio + x += 4; + view.setUint32(x, 0x6e_2f_6f_63); // n/oc + x += 4; + view.setUint32(x, 0x74_65_74_2d); // tet- + x += 4; + view.setUint32(x, 0x73_74_72_65); // stre + x += 4; + view.setUint32(x, 0x61_6d_3b_62); // am;b + x += 4; + view.setUint32(x, 0x61_73_65_36); // ase6 + x += 4; + view.setUint16(x, 0x34_2c); // 4, + x += 2; + x = toBase64Bin(buf, 0, length, view, x); + writer.uint8[x++] = 0x22; // " + writer.x = x; + } + + public writeStr(str: string): void { + const writer = this.writer; + const length = str.length; + writer.ensureCapacity(length * 4 + 2); + if (length < 256) { + let x = writer.x; + const uint8 = writer.uint8; + uint8[x++] = 0x22; // " + for (let i = 0; i < length; i++) { + const code = str.charCodeAt(i); + switch (code) { + case 34: // " + case 92: // \ + uint8[x++] = 0x5c; // \ + break; + } + if (code < 32 || code > 126) { + writer.utf8(JSON.stringify(str)); + return; + } else uint8[x++] = code; + } + uint8[x++] = 0x22; // " + writer.x = x; + return; + } + writer.utf8(JSON.stringify(str)); + } + + public writeAsciiStr(str: string): void { + const length = str.length; + const writer = this.writer; + writer.ensureCapacity(length * 2 + 2); + const uint8 = writer.uint8; + let x = writer.x; + uint8[x++] = 0x22; // " + for (let i = 0; i < length; i++) { + const code = str.charCodeAt(i); + switch (code) { + case 34: // " + case 92: // \ + uint8[x++] = 0x5c; // \ + break; + } + uint8[x++] = code; + } + uint8[x++] = 0x22; // " + writer.x = x; + } + + public writeArr(arr: unknown[]): void { + const writer = this.writer; + writer.u8(0x5b); // [ + const length = arr.length; + const last = length - 1; + for (let i = 0; i < last; i++) { + this.writeAny(arr[i]); + writer.u8(0x2c); // , + } + if (last >= 0) this.writeAny(arr[last]); + writer.u8(0x5d); // ] + } + + public writeArrSeparator(): void { + this.writer.u8(0x2c); // , + } + + public writeObj(obj: Record): void { + const writer = this.writer; + const keys = Object.keys(obj); + const length = keys.length; + if (!length) return writer.u16(0x7b7d); // {} + writer.u8(0x7b); // { + for (let i = 0; i < length; i++) { + const key = keys[i]; + const value = obj[key]; + this.writeStr(key); + writer.u8(0x3a); // : + this.writeAny(value); + writer.u8(0x2c); // , + } + writer.uint8[writer.x - 1] = 0x7d; // } + } + + public writeObjSeparator(): void { + this.writer.u8(0x2c); // , + } + + public writeObjKeySeparator(): void { + this.writer.u8(0x3a); // : + } + + // ------------------------------------------------------- Streaming encoding + + public writeStartStr(): void { + throw new Error('Method not implemented.'); + } + + public writeStrChunk(str: string): void { + throw new Error('Method not implemented.'); + } + + public writeEndStr(): void { + throw new Error('Method not implemented.'); + } + + public writeStartBin(): void { + throw new Error('Method not implemented.'); + } + + public writeBinChunk(buf: Uint8Array): void { + throw new Error('Method not implemented.'); + } + + public writeEndBin(): void { + throw new Error('Method not implemented.'); + } + + public writeStartArr(): void { + this.writer.u8(0x5b); // [ + } + + public writeArrChunk(item: unknown): void { + throw new Error('Method not implemented.'); + } + + public writeEndArr(): void { + this.writer.u8(0x5d); // ] + } + + public writeStartObj(): void { + this.writer.u8(0x7b); // { + } + + public writeObjChunk(key: string, value: unknown): void { + throw new Error('Method not implemented.'); + } + + public writeEndObj(): void { + this.writer.u8(0x7d); // } + } +} diff --git a/src/json-joy/json-pack/types.ts b/src/json-joy/json-pack/types.ts new file mode 100644 index 00000000..217c288b --- /dev/null +++ b/src/json-joy/json-pack/types.ts @@ -0,0 +1,57 @@ +import type {IReader, IReaderResettable, IWriter, IWriterGrowable} from '../util/buffers'; +import type {JsonPackExtension} from './JsonPackExtension'; +import type {JsonPackValue} from './JsonPackValue'; + +export type JsonPrimitive = string | number | bigint | boolean | null; +export type JsonValue = JsonPrimitive | JsonArray | JsonObject; +type JsonArray = JsonValue[] | readonly JsonValue[]; +type JsonObject = {[key: string]: JsonValue} | Readonly<{[key: string]: JsonValue}>; + +export type TypedJsonValue = T & JsonValue; + +export type PackPrimitive = JsonPrimitive | undefined | Uint8Array | JsonPackValue | JsonPackExtension | bigint; +export type PackValue = PackPrimitive | PackArray | PackObject; +type PackArray = PackValue[] | readonly PackValue[]; +type PackObject = {[key: string]: PackValue} | Readonly<{[key: string]: PackValue}>; + +export interface BinaryJsonEncoder { + writer: IWriter & IWriterGrowable; + writeAny(value: unknown): void; + writeNull(): void; + writeBoolean(bool: boolean): void; + writeNumber(num: number): void; + writeInteger(int: number): void; + writeUInteger(uint: number): void; + writeFloat(float: number): void; + writeBin(buf: Uint8Array): void; + writeAsciiStr(str: string): void; + writeStr(str: string): void; + writeArr(arr: unknown[]): void; + writeObj(obj: Record): void; +} + +export interface StreamingBinaryJsonEncoder { + writeStartStr(): void; + writeStrChunk(str: string): void; + writeEndStr(): void; + writeStartBin(): void; + writeBinChunk(buf: Uint8Array): void; + writeEndBin(): void; + writeStartArr(): void; + writeArrChunk(item: unknown): void; + writeEndArr(): void; + writeStartObj(): void; + writeObjChunk(key: string, value: unknown): void; + writeEndObj(): void; +} + +export interface TlvBinaryJsonEncoder { + writeBinHdr(length: number): void; + writeArrHdr(length: number): void; + writeObjHdr(length: number): void; +} + +export interface BinaryJsonDecoder { + reader: IReader & IReaderResettable; + read(uint8: Uint8Array): PackValue; +} diff --git a/src/json-joy/json-pointer/index.ts b/src/json-joy/json-pointer/index.ts new file mode 100644 index 00000000..fcb073fe --- /dev/null +++ b/src/json-joy/json-pointer/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/src/json-joy/json-pointer/types.ts b/src/json-joy/json-pointer/types.ts new file mode 100644 index 00000000..8c0860c5 --- /dev/null +++ b/src/json-joy/json-pointer/types.ts @@ -0,0 +1,2 @@ +export type PathStep = string | number; +export type Path = readonly PathStep[]; diff --git a/src/json-joy/util/base64/constants.ts b/src/json-joy/util/base64/constants.ts new file mode 100644 index 00000000..46b19263 --- /dev/null +++ b/src/json-joy/util/base64/constants.ts @@ -0,0 +1,2 @@ +export const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +export const hasBuffer = typeof Buffer === 'function' && typeof Buffer.from === 'function'; diff --git a/src/json-joy/util/base64/createFromBase64Bin.ts b/src/json-joy/util/base64/createFromBase64Bin.ts new file mode 100644 index 00000000..3b2ee1cc --- /dev/null +++ b/src/json-joy/util/base64/createFromBase64Bin.ts @@ -0,0 +1,68 @@ +import {alphabet} from './constants'; + +export const createFromBase64Bin = (chars: string = alphabet, paddingOctet: number = 0x3d) => { + if (chars.length !== 64) throw new Error('chars must be 64 characters long'); + let max = 0; + for (let i = 0; i < chars.length; i++) max = Math.max(max, chars.charCodeAt(i)); + const table: number[] = []; + for (let i = 0; i <= max; i += 1) table[i] = -1; + for (let i = 0; i < chars.length; i++) table[chars.charCodeAt(i)] = i; + + return (view: DataView, offset: number, length: number): Uint8Array => { + if (!length) return new Uint8Array(0); + if (length % 4 !== 0) throw new Error('Base64 string length must be a multiple of 4'); + const end = offset + length; + const last = end - 1; + const lastOctet = view.getUint8(last); + const mainEnd = offset + (lastOctet !== paddingOctet ? length : length - 4); + let bufferLength = (length >> 2) * 3; + let padding = 0; + if (last > 0 && view.getUint8(last - 1) === paddingOctet) { + padding = 2; + bufferLength -= 2; + } else if (lastOctet === paddingOctet) { + padding = 1; + bufferLength -= 1; + } + const buf = new Uint8Array(bufferLength); + let j = 0; + let i = offset; + for (; i < mainEnd; i += 4) { + const word = view.getUint32(i); + const octet0 = word >>> 24; + const octet1 = (word >>> 16) & 0xff; + const octet2 = (word >>> 8) & 0xff; + const octet3 = word & 0xff; + const sextet0 = table[octet0]; + const sextet1 = table[octet1]; + const sextet2 = table[octet2]; + const sextet3 = table[octet3]; + if (sextet0 < 0 || sextet1 < 0 || sextet2 < 0 || sextet3 < 0) throw new Error('INVALID_BASE64_SEQ'); + buf[j] = (sextet0 << 2) | (sextet1 >> 4); + buf[j + 1] = (sextet1 << 4) | (sextet2 >> 2); + buf[j + 2] = (sextet2 << 6) | sextet3; + j += 3; + } + if (padding === 2) { + const word = view.getUint16(mainEnd); + const octet0 = word >> 8; + const octet1 = word & 0xff; + const sextet0 = table[octet0]; + const sextet1 = table[octet1]; + if (sextet0 < 0 || sextet1 < 0) throw new Error('INVALID_BASE64_SEQ'); + buf[j] = (sextet0 << 2) | (sextet1 >> 4); + } else if (padding === 1) { + const word = view.getUint16(mainEnd); + const octet0 = word >> 8; + const octet1 = word & 0xff; + const octet2 = view.getUint8(mainEnd + 2); + const sextet0 = table[octet0]; + const sextet1 = table[octet1]; + const sextet2 = table[octet2]; + if (sextet0 < 0 || sextet1 < 0 || sextet2 < 0) throw new Error('INVALID_BASE64_SEQ'); + buf[j] = (sextet0 << 2) | (sextet1 >> 4); + buf[j + 1] = (sextet1 << 4) | (sextet2 >> 2); + } + return buf; + }; +}; diff --git a/src/json-joy/util/base64/createToBase64Bin.ts b/src/json-joy/util/base64/createToBase64Bin.ts new file mode 100644 index 00000000..865d0954 --- /dev/null +++ b/src/json-joy/util/base64/createToBase64Bin.ts @@ -0,0 +1,44 @@ +import {alphabet} from './constants'; + +export const createToBase64Bin = (chars: string = alphabet) => { + if (chars.length !== 64) throw new Error('chars must be 64 characters long'); + + const table = chars.split('').map((c) => c.charCodeAt(0)); + const table2: number[] = []; + + for (const c1 of table) { + for (const c2 of table) { + const two = (c1 << 8) + c2; + table2.push(two); + } + } + + return (uint8: Uint8Array, start: number, length: number, dest: DataView, offset: number): number => { + const extraLength = length % 3; + const baseLength = length - extraLength; + for (; start < baseLength; start += 3) { + const o1 = uint8[start]; + const o2 = uint8[start + 1]; + const o3 = uint8[start + 2]; + const v1 = (o1 << 4) | (o2 >> 4); + const v2 = ((o2 & 0b1111) << 8) | o3; + dest.setInt32(offset, (table2[v1] << 16) + table2[v2]); + offset += 4; + } + if (extraLength) { + if (extraLength === 1) { + const o1 = uint8[baseLength]; + dest.setInt32(offset, (table2[o1 << 4] << 16) + 0x3d3d); + return offset + 4; + } else { + const o1 = uint8[baseLength]; + const o2 = uint8[baseLength + 1]; + const v1 = (o1 << 4) | (o2 >> 4); + const v2 = (o2 & 0b1111) << 2; + dest.setInt32(offset, (table2[v1] << 16) + (table[v2] << 8) + 0x3d); + return offset + 4; + } + } + return offset; + }; +}; diff --git a/src/json-joy/util/base64/fromBase64Bin.ts b/src/json-joy/util/base64/fromBase64Bin.ts new file mode 100644 index 00000000..678898d5 --- /dev/null +++ b/src/json-joy/util/base64/fromBase64Bin.ts @@ -0,0 +1,3 @@ +import {createFromBase64Bin} from './createFromBase64Bin'; + +export const fromBase64Bin = createFromBase64Bin(); diff --git a/src/json-joy/util/base64/toBase64Bin.ts b/src/json-joy/util/base64/toBase64Bin.ts new file mode 100644 index 00000000..012f2c24 --- /dev/null +++ b/src/json-joy/util/base64/toBase64Bin.ts @@ -0,0 +1,3 @@ +import {createToBase64Bin} from './createToBase64Bin'; + +export const toBase64Bin = createToBase64Bin(); diff --git a/src/json-joy/util/buffers/Reader.ts b/src/json-joy/util/buffers/Reader.ts new file mode 100644 index 00000000..160e9cdc --- /dev/null +++ b/src/json-joy/util/buffers/Reader.ts @@ -0,0 +1,105 @@ +import {decodeUtf8} from './utf8/decodeUtf8'; +import type {IReader, IReaderResettable} from './types'; + +export class Reader implements IReader, IReaderResettable { + public uint8 = new Uint8Array([]); + public view = new DataView(this.uint8.buffer); + public x = 0; + + public reset(uint8: Uint8Array): void { + this.x = 0; + this.uint8 = uint8; + this.view = new DataView(uint8.buffer, uint8.byteOffset, uint8.length); + } + + public peak(): number { + return this.view.getUint8(this.x); + } + + public skip(length: number): void { + this.x += length; + } + + public buf(size: number): Uint8Array { + const end = this.x + size; + const bin = this.uint8.subarray(this.x, end); + this.x = end; + return bin; + } + + public u8(): number { + return this.uint8[this.x++]; + // return this.view.getUint8(this.x++); + } + + public i8(): number { + return this.view.getInt8(this.x++); + } + + public u16(): number { + // const num = this.view.getUint16(this.x); + // this.x += 2; + // return num; + let x = this.x; + const num = (this.uint8[x++] << 8) + this.uint8[x++]; + this.x = x; + return num; + } + + public i16(): number { + const num = this.view.getInt16(this.x); + this.x += 2; + return num; + } + + public u32(): number { + const num = this.view.getUint32(this.x); + this.x += 4; + return num; + } + + public i32(): number { + const num = this.view.getInt32(this.x); + this.x += 4; + return num; + } + + public u64(): bigint { + const num = this.view.getBigUint64(this.x); + this.x += 8; + return num; + } + + public i64(): bigint { + const num = this.view.getBigInt64(this.x); + this.x += 8; + return num; + } + + public f32(): number { + const pos = this.x; + this.x += 4; + return this.view.getFloat32(pos); + } + + public f64(): number { + const pos = this.x; + this.x += 8; + return this.view.getFloat64(pos); + } + + public utf8(size: number): string { + const start = this.x; + this.x += size; + return decodeUtf8(this.uint8, start, size); + } + + public ascii(length: number): string { + const uint8 = this.uint8; + let str = ''; + const end = this.x + length; + for (let i = this.x; i < end; i++) str += String.fromCharCode(uint8[i]); + this.x = end; + return str; + } +} diff --git a/src/json-joy/util/buffers/Slice.ts b/src/json-joy/util/buffers/Slice.ts new file mode 100644 index 00000000..c0287b83 --- /dev/null +++ b/src/json-joy/util/buffers/Slice.ts @@ -0,0 +1,12 @@ +export class Slice { + constructor( + public readonly uint8: Uint8Array, + public readonly view: DataView, + public readonly start: number, + public readonly end: number, + ) {} + + public subarray(): Uint8Array { + return this.uint8.subarray(this.start, this.end); + } +} diff --git a/src/json-joy/util/buffers/Writer.ts b/src/json-joy/util/buffers/Writer.ts new file mode 100644 index 00000000..7433f5cd --- /dev/null +++ b/src/json-joy/util/buffers/Writer.ts @@ -0,0 +1,276 @@ +import {Slice} from './Slice'; +import {IWriterGrowable, IWriter} from './types'; + +const EMPTY_UINT8 = new Uint8Array([]); +const EMPTY_VIEW = new DataView(EMPTY_UINT8.buffer); + +declare global { + interface Uint8Array { + utf8Write(str: string, pos: number, maxLength: number): number + write(str: string, pos: number, maxLength: number, encoding: string): number; + } +} + +const hasBuffer = typeof Buffer === 'function'; +const utf8Write = hasBuffer + ? (Buffer.prototype.utf8Write as (this: Uint8Array, str: string, pos: number, maxLength: number) => number) + : null; +const from = hasBuffer ? Buffer.from : null; +const textEncoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null; + +/** + * Encoder class provides an efficient way to encode binary data. It grows the + * internal memory buffer automatically as more space is required. It is useful + * in cases when it is not known in advance the size of memory needed. + */ +export class Writer implements IWriter, IWriterGrowable { + /** @ignore */ + public uint8: Uint8Array; + /** @ignore */ + public view: DataView = EMPTY_VIEW; + /** @ignore */ + public x0: number = 0; + /** @ignore */ + public x: number = 0; + protected size: number; + + /** + * @param allocSize Number of bytes to allocate at a time when buffer ends. + */ + constructor(public allocSize: number = 64 * 1024) { + this.uint8 = new Uint8Array(allocSize); + this.size = allocSize; + this.view = new DataView(this.uint8.buffer); + } + + /** @ignore */ + protected grow(size: number) { + const x0 = this.x0; + const x = this.x; + const oldUint8 = this.uint8; + const newUint8 = new Uint8Array(size); + const view = new DataView(newUint8.buffer); + const activeSlice = oldUint8.subarray(x0, x); + newUint8.set(activeSlice, 0); + this.x = x - x0; + this.x0 = 0; + this.uint8 = newUint8; + this.size = size; + this.view = view; + } + + /** + * Make sure the internal buffer has enough space to write the specified number + * of bytes, otherwise resize the internal buffer to accommodate for more size. + * + * @param capacity Number of bytes. + */ + public ensureCapacity(capacity: number) { + const byteLength = this.size; + const remaining = byteLength - this.x; + if (remaining < capacity) { + const total = byteLength - this.x0; + const required = capacity - remaining; + const totalRequired = total + required; + this.grow(totalRequired <= this.allocSize ? this.allocSize : totalRequired * 2); + } + } + + /** @todo Consider renaming to "skip"? */ + public move(capacity: number) { + this.ensureCapacity(capacity); + this.x += capacity; + } + + public reset() { + this.x0 = this.x; + } + + /** + * Allocates a new {@link ArrayBuffer}, useful when the underlying + * {@link ArrayBuffer} cannot be shared between threads. + * + * @param size Size of memory to allocate. + */ + public newBuffer(size: number) { + const uint8 = (this.uint8 = new Uint8Array(size)); + this.size = size; + this.view = new DataView(uint8.buffer); + this.x = this.x0 = 0; + } + + /** + * @returns Encoded memory buffer contents. + */ + public flush(): Uint8Array { + const result = this.uint8.subarray(this.x0, this.x); + this.x0 = this.x; + return result; + } + + public flushSlice(): Slice { + const slice = new Slice(this.uint8, this.view, this.x0, this.x); + this.x0 = this.x; + return slice; + } + + public u8(char: number) { + this.ensureCapacity(1); + this.uint8[this.x++] = char; + } + + public u16(word: number) { + this.ensureCapacity(2); + this.view.setUint16(this.x, word); + this.x += 2; + } + + public u32(dword: number) { + this.ensureCapacity(4); + this.view.setUint32(this.x, dword); + this.x += 4; + } + + public i32(dword: number) { + this.ensureCapacity(4); + this.view.setInt32(this.x, dword); + this.x += 4; + } + + public u64(qword: number | bigint) { + this.ensureCapacity(8); + this.view.setBigUint64(this.x, BigInt(qword)); + this.x += 8; + } + + public f64(float: number) { + this.ensureCapacity(8); + this.view.setFloat64(this.x, float); + this.x += 8; + } + + public u8u16(u8: number, u16: number) { + this.ensureCapacity(3); + let x = this.x; + this.uint8[x++] = u8; + this.uint8[x++] = u16 >>> 8; + this.uint8[x++] = u16 & 0xff; + this.x = x; + } + + public u8u32(u8: number, u32: number) { + this.ensureCapacity(5); + let x = this.x; + this.uint8[x++] = u8; + this.view.setUint32(x, u32); + this.x = x + 4; + } + + public u8u64(u8: number, u64: number | bigint) { + this.ensureCapacity(9); + let x = this.x; + this.uint8[x++] = u8; + this.view.setBigUint64(x, BigInt(u64)); + this.x = x + 8; + } + + public u8f32(u8: number, f32: number) { + this.ensureCapacity(5); + let x = this.x; + this.uint8[x++] = u8; + this.view.setFloat32(x, f32); + this.x = x + 4; + } + + public u8f64(u8: number, f64: number) { + this.ensureCapacity(9); + let x = this.x; + this.uint8[x++] = u8; + this.view.setFloat64(x, f64); + this.x = x + 8; + } + + public buf(buf: Uint8Array, length: number): void { + this.ensureCapacity(length); + const x = this.x; + this.uint8.set(buf, x); + this.x = x + length; + } + + /** + * Encodes string as UTF-8. You need to call .ensureCapacity(str.length * 4) + * before calling + * + * @param str String to encode as UTF-8. + * @returns The number of bytes written + */ + public utf8(str: string): number { + const maxLength = str.length * 4; + if (maxLength < 168) return this.utf8Native(str); + if (utf8Write) { + const writeLength = utf8Write.call(this.uint8, str, this.x, maxLength); + this.x += writeLength; + return writeLength; + } else if (from) { + const uint8 = this.uint8; + const offset = uint8.byteOffset + this.x; + const buf = from(uint8.buffer).subarray(offset, offset + maxLength); + const writeLength = buf.write(str, 0, maxLength, 'utf8'); + this.x += writeLength; + return writeLength; + } else if (maxLength > 1024 && textEncoder) { + const writeLength = textEncoder!.encodeInto(str, this.uint8.subarray(this.x, this.x + maxLength)).written!; + this.x += writeLength; + return writeLength; + } + return this.utf8Native(str); + } + + public utf8Native(str: string): number { + const length = str.length; + const uint8 = this.uint8; + let offset = this.x; + let pos = 0; + while (pos < length) { + let value = str.charCodeAt(pos++); + if ((value & 0xffffff80) === 0) { + uint8[offset++] = value; + continue; + } else if ((value & 0xfffff800) === 0) { + uint8[offset++] = ((value >> 6) & 0x1f) | 0xc0; + } else { + if (value >= 0xd800 && value <= 0xdbff) { + if (pos < length) { + const extra = str.charCodeAt(pos); + if ((extra & 0xfc00) === 0xdc00) { + pos++; + value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; + } + } + } + if ((value & 0xffff0000) === 0) { + uint8[offset++] = ((value >> 12) & 0x0f) | 0xe0; + uint8[offset++] = ((value >> 6) & 0x3f) | 0x80; + } else { + uint8[offset++] = ((value >> 18) & 0x07) | 0xf0; + uint8[offset++] = ((value >> 12) & 0x3f) | 0x80; + uint8[offset++] = ((value >> 6) & 0x3f) | 0x80; + } + } + uint8[offset++] = (value & 0x3f) | 0x80; + } + const writeLength = offset - this.x; + this.x = offset; + return writeLength; + } + + public ascii(str: string): void { + const length = str.length; + this.ensureCapacity(length); + const uint8 = this.uint8; + let x = this.x; + let pos = 0; + while (pos < length) uint8[x++] = str.charCodeAt(pos++); + this.x = x; + } +} diff --git a/src/json-joy/util/buffers/f16.ts b/src/json-joy/util/buffers/f16.ts new file mode 100644 index 00000000..51a41504 --- /dev/null +++ b/src/json-joy/util/buffers/f16.ts @@ -0,0 +1,16 @@ +const pow = Math.pow; + +export const decodeF16 = (binary: number): number => { + const exponent = (binary & 0x7c00) >> 10; + const fraction = binary & 0x03ff; + return ( + (binary >> 15 ? -1 : 1) * + (exponent + ? exponent === 0x1f + ? fraction + ? NaN + : Infinity + : pow(2, exponent - 15) * (1 + fraction / 0x400) + : 6.103515625e-5 * (fraction / 0x400)) + ); +}; diff --git a/src/json-joy/util/buffers/index.ts b/src/json-joy/util/buffers/index.ts new file mode 100644 index 00000000..fcb073fe --- /dev/null +++ b/src/json-joy/util/buffers/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/src/json-joy/util/buffers/isFloat32.ts b/src/json-joy/util/buffers/isFloat32.ts new file mode 100644 index 00000000..26e38234 --- /dev/null +++ b/src/json-joy/util/buffers/isFloat32.ts @@ -0,0 +1,6 @@ +const view = new DataView(new ArrayBuffer(4)); + +export const isFloat32 = (n: number): boolean => { + view.setFloat32(0, n); + return n === view.getFloat32(0); +}; diff --git a/src/json-joy/util/buffers/types.ts b/src/json-joy/util/buffers/types.ts new file mode 100644 index 00000000..ae29727a --- /dev/null +++ b/src/json-joy/util/buffers/types.ts @@ -0,0 +1,126 @@ +import type {Slice} from './Slice'; + +export interface IWriter { + /** + * Uint8Array view of the current memory buffer. + */ + uint8: Uint8Array; + + /** + * DataView view of the current memory buffer. + */ + view: DataView; + + /** + * Position where last flush happened. + */ + x0: number; + + /** + * Current position in the internal buffer. + */ + x: number; + + u8(char: number): void; + u16(word: number): void; + u32(dword: number): void; + i32(dword: number): void; + u64(qword: number | bigint): void; + u8u16(u8: number, u16: number): void; + u8u32(u8: number, u32: number): void; + u8u64(u8: number, u64: number | bigint): void; + u8f32(u8: number, f64: number): void; + u8f64(u8: number, f64: number): void; + f64(dword: number): void; + + /** + * Write contents of a buffer. + * + * @param buf Buffer to copy from. + * @param length Number of octets to copy. + */ + buf(buf: Uint8Array, length: number): void; + + /** + * Write string as UTF-8. You need to call .ensureCapacity(str.length * 4) + * before calling + * + * @param str JavaScript string to encode as UTF-8 byte sequence. + */ + utf8(str: string): number; + + ascii(str: string): void; +} + +export interface IWriterGrowable { + /** @deprecated */ + reset(): void; + + /** + * Calling this method might reset the internal buffer. So, your references + * (such as `x`, `uint8`, `view`) to the internal buffer might become invalid. + * + * @param capacity How many octets to ensure are available after `x`. + */ + ensureCapacity(capacity: number): void; + move(length: number): void; + flush(): Uint8Array; + flushSlice(): Slice; + newBuffer(size: number): void; +} + +export interface IReaderBase { + /** Get current byte value without advancing the cursor. */ + peak(): number; + + /** Advance the cursor given number of octets. */ + skip(length: number): void; + + /** + * Create a new Uint8Array view of provided length starting at + * the current cursor position. + */ + buf(size: number): Uint8Array; + + u8(): number; + i8(): number; + u16(): number; + i16(): number; + u32(): number; + u64(): bigint; + i64(): bigint; + i32(): number; + f32(): number; + f64(): number; + + /** + * Decode a UTF-8 string. + * + * @param size Length of the string. + */ + utf8(size: number): string; + + ascii(length: number): string; +} + +export interface IReader extends IReaderBase { + /** + * Uint8Array view of the current memory buffer. + */ + uint8: Uint8Array; + + /** + * DataView view of the current memory buffer. + */ + view: DataView; + + /** + * Cursor in the current memory buffer. + */ + x: number; +} + +export interface IReaderResettable { + /** Set a new underlying buffer and reset cursor position to 0. */ + reset(uint8: Uint8Array): void; +} diff --git a/src/json-joy/util/buffers/utf8/CachedUtf8Decoder.ts b/src/json-joy/util/buffers/utf8/CachedUtf8Decoder.ts new file mode 100644 index 00000000..4af0f9dd --- /dev/null +++ b/src/json-joy/util/buffers/utf8/CachedUtf8Decoder.ts @@ -0,0 +1,51 @@ +import decodeUtf8 from './decodeUtf8/v10'; +import {randomU32} from 'hyperdyperid/lib/randomU32'; + +class CacheItem { + constructor(public readonly bytes: Uint8Array, public readonly value: string) {} +} + +const enum CONST { + MAX_CACHED_STR_LEN = 31, + MAX_RECORDS_PER_SIZE = 16, +} + +export class CachedUtf8Decoder { + private readonly caches: CacheItem[][]; + + constructor() { + this.caches = []; + for (let i = 0; i < CONST.MAX_CACHED_STR_LEN; i++) this.caches.push([]); + } + + private get(bytes: Uint8Array, offset: number, size: number): string | null { + const records = this.caches[size - 1]!; + const len = records.length; + FIND_CHUNK: for (let i = 0; i < len; i++) { + const record = records[i]; + const recordBytes = record.bytes; + for (let j = 0; j < size; j++) if (recordBytes[j] !== bytes[offset + j]) continue FIND_CHUNK; + return record.value; + } + return null; + } + + private store(bytes: Uint8Array, value: string): void { + const records = this.caches[bytes.length - 1]!; + const record = new CacheItem(bytes, value); + const length = records.length; + if (length >= CONST.MAX_RECORDS_PER_SIZE) records[randomU32(0, CONST.MAX_RECORDS_PER_SIZE - 1)] = record; + else records.push(record); + } + + public decode(bytes: Uint8Array, offset: number, size: number): string { + if (!size) return ''; + const cachedValue = this.get(bytes, offset, size); + if (cachedValue !== null) return cachedValue; + const value = decodeUtf8(bytes, offset, size); + // Ensure to copy a slice of bytes because the byte may be NodeJS Buffer and Buffer#slice() returns a reference to its internal ArrayBuffer. + const copy = Uint8Array.prototype.slice.call(bytes, offset, offset + size); + this.store(copy, value); + return value; + } +} diff --git a/src/json-joy/util/buffers/utf8/decodeAscii.ts b/src/json-joy/util/buffers/utf8/decodeAscii.ts new file mode 100644 index 00000000..3246e613 --- /dev/null +++ b/src/json-joy/util/buffers/utf8/decodeAscii.ts @@ -0,0 +1,167 @@ +const fromCharCode = String.fromCharCode; + +/** This code was borrowed form cbor-x under the MIT license. */ + +// MIT License + +// Copyright (c) 2020 Kris Zyp + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +export const decodeAscii = (src: Uint8Array, position: number, length: number): string | undefined => { + const bytes: number[] = []; + for (let i = 0; i < length; i++) { + const byte = src[position++]; + if (byte & 0x80) return; + bytes.push(byte); + } + return fromCharCode.apply(String, bytes); +}; + +export const decodeAsciiMax15 = (src: Uint8Array, position: number, length: number): string | undefined => { + if (length < 4) { + if (length < 2) { + if (length === 0) return ''; + else { + const a = src[position++]; + if ((a & 0x80) > 1) { + position -= 1; + return; + } + return fromCharCode(a); + } + } else { + const a = src[position++]; + const b = src[position++]; + if ((a & 0x80) > 0 || (b & 0x80) > 0) { + position -= 2; + return; + } + if (length < 3) return fromCharCode(a, b); + const c = src[position++]; + if ((c & 0x80) > 0) { + position -= 3; + return; + } + return fromCharCode(a, b, c); + } + } else { + const a = src[position++]; + const b = src[position++]; + const c = src[position++]; + const d = src[position++]; + if ((a & 0x80) > 0 || (b & 0x80) > 0 || (c & 0x80) > 0 || (d & 0x80) > 0) { + position -= 4; + return; + } + if (length < 6) { + if (length === 4) return fromCharCode(a, b, c, d); + else { + const e = src[position++]; + if ((e & 0x80) > 0) { + position -= 5; + return; + } + return fromCharCode(a, b, c, d, e); + } + } else if (length < 8) { + const e = src[position++]; + const f = src[position++]; + if ((e & 0x80) > 0 || (f & 0x80) > 0) { + position -= 6; + return; + } + if (length < 7) return fromCharCode(a, b, c, d, e, f); + const g = src[position++]; + if ((g & 0x80) > 0) { + position -= 7; + return; + } + return fromCharCode(a, b, c, d, e, f, g); + } else { + const e = src[position++]; + const f = src[position++]; + const g = src[position++]; + const h = src[position++]; + if ((e & 0x80) > 0 || (f & 0x80) > 0 || (g & 0x80) > 0 || (h & 0x80) > 0) { + position -= 8; + return; + } + if (length < 10) { + if (length === 8) return fromCharCode(a, b, c, d, e, f, g, h); + else { + const i = src[position++]; + if ((i & 0x80) > 0) { + position -= 9; + return; + } + return fromCharCode(a, b, c, d, e, f, g, h, i); + } + } else if (length < 12) { + const i = src[position++]; + const j = src[position++]; + if ((i & 0x80) > 0 || (j & 0x80) > 0) { + position -= 10; + return; + } + if (length < 11) return fromCharCode(a, b, c, d, e, f, g, h, i, j); + const k = src[position++]; + if ((k & 0x80) > 0) { + position -= 11; + return; + } + return fromCharCode(a, b, c, d, e, f, g, h, i, j, k); + } else { + const i = src[position++]; + const j = src[position++]; + const k = src[position++]; + const l = src[position++]; + if ((i & 0x80) > 0 || (j & 0x80) > 0 || (k & 0x80) > 0 || (l & 0x80) > 0) { + position -= 12; + return; + } + if (length < 14) { + if (length === 12) return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l); + else { + const m = src[position++]; + if ((m & 0x80) > 0) { + position -= 13; + return; + } + return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m); + } + } else { + const m = src[position++]; + const n = src[position++]; + if ((m & 0x80) > 0 || (n & 0x80) > 0) { + position -= 14; + return; + } + if (length < 15) return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n); + const o = src[position++]; + if ((o & 0x80) > 0) { + position -= 15; + return; + } + return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o); + } + } + } + } +}; diff --git a/src/json-joy/util/buffers/utf8/decodeUtf8/index.ts b/src/json-joy/util/buffers/utf8/decodeUtf8/index.ts new file mode 100644 index 00000000..7e934989 --- /dev/null +++ b/src/json-joy/util/buffers/utf8/decodeUtf8/index.ts @@ -0,0 +1,3 @@ +import decodeUtf8 from './v16'; + +export {decodeUtf8}; diff --git a/src/json-joy/util/buffers/utf8/decodeUtf8/v10.ts b/src/json-joy/util/buffers/utf8/decodeUtf8/v10.ts new file mode 100644 index 00000000..ebf8b03d --- /dev/null +++ b/src/json-joy/util/buffers/utf8/decodeUtf8/v10.ts @@ -0,0 +1,39 @@ +const fromCharCode = String.fromCharCode; + +export default (buf: Uint8Array, start: number, length: number): string => { + let offset = start; + const end = offset + length; + let str = ''; + while (offset < end) { + const octet1 = buf[offset++]!; + if ((octet1 & 0x80) === 0) { + str += fromCharCode(octet1); + continue; + } + const octet2 = buf[offset++]! & 0x3f; + if ((octet1 & 0xe0) === 0xc0) { + str += fromCharCode(((octet1 & 0x1f) << 6) | octet2); + continue; + } + const octet3 = buf[offset++]! & 0x3f; + if ((octet1 & 0xf0) === 0xe0) { + str += fromCharCode(((octet1 & 0x1f) << 12) | (octet2 << 6) | octet3); + continue; + } + if ((octet1 & 0xf8) === 0xf0) { + const octet4 = buf[offset++]! & 0x3f; + let unit = ((octet1 & 0x07) << 0x12) | (octet2 << 0x0c) | (octet3 << 0x06) | octet4; + if (unit > 0xffff) { + unit -= 0x10000; + const unit0 = ((unit >>> 10) & 0x3ff) | 0xd800; + unit = 0xdc00 | (unit & 0x3ff); + str += fromCharCode(unit0, unit); + } else { + str += fromCharCode(unit); + } + } else { + str += fromCharCode(octet1); + } + } + return str; +}; diff --git a/src/json-joy/util/buffers/utf8/decodeUtf8/v16.ts b/src/json-joy/util/buffers/utf8/decodeUtf8/v16.ts new file mode 100644 index 00000000..5953a774 --- /dev/null +++ b/src/json-joy/util/buffers/utf8/decodeUtf8/v16.ts @@ -0,0 +1,39 @@ +import {decodeAscii, decodeAsciiMax15} from '../decodeAscii'; +import v18 from './v18'; + +declare global { + interface Buffer { + utf8Slice(buf: Uint8Array, start: number, length: number): string + } + + interface Uint8Array { + toString(encoding?: string): string; + } +} + +type Decoder = (buf: Uint8Array, start: number, length: number) => string; + +const hasBuffer = typeof Buffer !== 'undefined'; +const utf8Slice = hasBuffer ? Buffer.prototype.utf8Slice : null; +const from = hasBuffer ? Buffer.from : null; + +const shortDecoder: Decoder = (buf, start, length) => decodeAsciiMax15(buf, start, length) ?? v18(buf, start, length); + +const midDecoder: Decoder = (buf, start, length) => decodeAscii(buf, start, length) ?? v18(buf, start, length); + +const longDecoder: Decoder = utf8Slice + ? (buf, start, length) => utf8Slice.call(buf, start, start + length) + : from + ? (buf, start, length) => + from(buf) + .subarray(start, start + length) + .toString('utf8') + : v18; + +const decoder: Decoder = (buf, start, length): string => { + if (length < 16) return shortDecoder(buf, start, length); + if (length < 32) return midDecoder(buf, start, length); + return longDecoder(buf, start, length); +}; + +export default decoder; diff --git a/src/json-joy/util/buffers/utf8/decodeUtf8/v18.ts b/src/json-joy/util/buffers/utf8/decodeUtf8/v18.ts new file mode 100644 index 00000000..e1b518e0 --- /dev/null +++ b/src/json-joy/util/buffers/utf8/decodeUtf8/v18.ts @@ -0,0 +1,36 @@ +const fromCharCode = String.fromCharCode; + +export default (buf: Uint8Array, start: number, length: number): string => { + let offset = start; + const end = offset + length; + const points: number[] = []; + while (offset < end) { + let code = buf[offset++]!; + if ((code & 0x80) !== 0) { + const octet2 = buf[offset++]! & 0x3f; + if ((code & 0xe0) === 0xc0) { + code = ((code & 0x1f) << 6) | octet2; + } else { + const octet3 = buf[offset++]! & 0x3f; + if ((code & 0xf0) === 0xe0) { + code = ((code & 0x1f) << 12) | (octet2 << 6) | octet3; + } else { + if ((code & 0xf8) === 0xf0) { + const octet4 = buf[offset++]! & 0x3f; + let unit = ((code & 0x07) << 0x12) | (octet2 << 0x0c) | (octet3 << 0x06) | octet4; + if (unit > 0xffff) { + unit -= 0x10000; + const unit0 = ((unit >>> 10) & 0x3ff) | 0xd800; + code = 0xdc00 | (unit & 0x3ff); + points.push(unit0); + } else { + code = unit; + } + } + } + } + } + points.push(code); + } + return fromCharCode.apply(String, points); +}; diff --git a/src/json-joy/util/buffers/utf8/sharedCachedUtf8Decoder.ts b/src/json-joy/util/buffers/utf8/sharedCachedUtf8Decoder.ts new file mode 100644 index 00000000..86b2d4ad --- /dev/null +++ b/src/json-joy/util/buffers/utf8/sharedCachedUtf8Decoder.ts @@ -0,0 +1,3 @@ +import {CachedUtf8Decoder} from './CachedUtf8Decoder'; + +export default new CachedUtf8Decoder(); diff --git a/src/json-joy/util/print/printTree.ts b/src/json-joy/util/print/printTree.ts new file mode 100644 index 00000000..a054a550 --- /dev/null +++ b/src/json-joy/util/print/printTree.ts @@ -0,0 +1,15 @@ +type Child = (tab: string) => string; + +export const printTree = (tab = '', children: (Child | null)[]): string => { + children = children.filter(Boolean); + let str = ''; + for (let i = 0; i < children.length; i++) { + const isLast = i >= children.length - 1; + const fn = children[i]; + if (!fn) continue; + const child = fn(tab + `${isLast ? ' ' : '│'} `); + const branch = child ? (isLast ? '└─' : '├─') : '│ '; + str += `\n${tab}${branch} ${child}`; + } + return str; +}; diff --git a/src/print/index.ts b/src/print/index.ts index 336163c7..e55aca86 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -1,4 +1,4 @@ -import { printTree } from 'json-joy/es6/util/print/printTree'; +import { printTree } from '../json-joy/util/print/printTree'; import { basename } from '../node-to-fsa/util'; import type { FsSynchronousApi } from '../node/types'; import type { IDirent } from '../node/types/misc'; diff --git a/src/snapshot/binary.ts b/src/snapshot/binary.ts index 2a8fbbf7..f1c06810 100644 --- a/src/snapshot/binary.ts +++ b/src/snapshot/binary.ts @@ -1,9 +1,9 @@ -import { CborEncoder } from 'json-joy/es6/json-pack/cbor/CborEncoder'; -import { CborDecoder } from 'json-joy/es6/json-pack/cbor/CborDecoder'; +import { CborEncoder } from '../json-joy/json-pack/cbor/CborEncoder'; +import { CborDecoder } from '../json-joy/json-pack/cbor/CborDecoder'; import { fromSnapshotSync, toSnapshotSync } from './sync'; import { fromSnapshot, toSnapshot } from './async'; import { writer } from './shared'; -import type { CborUint8Array } from 'json-joy/es6/json-pack/cbor/types'; +import type { CborUint8Array } from '../json-joy/json-pack/cbor/types'; import type { AsyncSnapshotOptions, SnapshotNode, SnapshotOptions } from './types'; const encoder = new CborEncoder(writer); diff --git a/src/snapshot/json.ts b/src/snapshot/json.ts index bfd3d526..f35ec9fa 100644 --- a/src/snapshot/json.ts +++ b/src/snapshot/json.ts @@ -1,5 +1,5 @@ -import { JsonEncoder } from 'json-joy/es6/json-pack/json/JsonEncoder'; -import { JsonDecoder } from 'json-joy/es6/json-pack/json/JsonDecoder'; +import { JsonEncoder } from '../json-joy/json-pack/json/JsonEncoder'; +import { JsonDecoder } from '../json-joy/json-pack/json/JsonDecoder'; import { fromSnapshotSync, toSnapshotSync } from './sync'; import { fromSnapshot, toSnapshot } from './async'; import { writer } from './shared'; diff --git a/src/snapshot/shared.ts b/src/snapshot/shared.ts index a413e163..ef5e87d5 100644 --- a/src/snapshot/shared.ts +++ b/src/snapshot/shared.ts @@ -1,3 +1,3 @@ -import { Writer } from 'json-joy/es6/util/buffers/Writer'; +import { Writer } from '../json-joy/util/buffers/Writer'; export const writer = new Writer(1024 * 32); diff --git a/yarn.lock b/yarn.lock index 526854f0..bda61bd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1683,11 +1683,6 @@ arg@^4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -arg@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -4341,15 +4336,6 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-joy@^11.0.0: - version "11.28.0" - resolved "https://registry.yarnpkg.com/json-joy/-/json-joy-11.28.0.tgz#59e4ba7cf91556e49caaf914047dca8bc95c1491" - integrity sha512-WTq2tYD2r+0rUFId4gtUjwejV20pArh4q2WRJKxJdwLlPFHyW94HwwB2vUr5lUJTVkehhhWEVLwOUI0MSacNIw== - dependencies: - arg "^5.0.2" - hyperdyperid "^1.2.0" - thingies "^1.14.1" - json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -6746,7 +6732,7 @@ text-table@~0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -thingies@^1.11.1, thingies@^1.14.1: +thingies@^1.11.1: version "1.16.0" resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.16.0.tgz#968cde87fbf0fdd69a1a3a8e9678324f634e5053" integrity sha512-J23AVs11hSQxuJxvfQyMIaS9z1QpDxOCvMkL3ZxZl8/jmkgmnNGWrlyNxVz6Jbh0U6DuGmHqq6f7zUROfg/ncg==