From 4f07166508cfb25c955e6163c983c8748e31431d Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:39:08 +1000 Subject: [PATCH 01/45] setup(cbor): workspace to look for `cbor/` --- _tools/check_circular_package_dependencies.ts | 2 ++ _tools/check_docs.ts | 1 + browser-compat.tsconfig.json | 1 + deno.json | 1 + import_map.json | 1 + 5 files changed, 6 insertions(+) diff --git a/_tools/check_circular_package_dependencies.ts b/_tools/check_circular_package_dependencies.ts index 9c7203ca7e36..864275d80e18 100644 --- a/_tools/check_circular_package_dependencies.ts +++ b/_tools/check_circular_package_dependencies.ts @@ -40,6 +40,7 @@ type Mod = | "async" | "bytes" | "cache" + | "cbor" | "cli" | "collections" | "crypto" @@ -82,6 +83,7 @@ const ENTRYPOINTS: Record = { async: ["mod.ts"], bytes: ["mod.ts"], cache: ["mod.ts"], + cbor: ["mod.ts"], cli: ["mod.ts"], collections: ["mod.ts"], crypto: ["mod.ts"], diff --git a/_tools/check_docs.ts b/_tools/check_docs.ts index 7e71ea5c9ae6..a2d07c9165e9 100644 --- a/_tools/check_docs.ts +++ b/_tools/check_docs.ts @@ -34,6 +34,7 @@ const ENTRY_POINTS = [ "../async/mod.ts", "../bytes/mod.ts", "../cache/mod.ts", + "../cbor/mod.ts", "../cli/mod.ts", "../crypto/mod.ts", "../collections/mod.ts", diff --git a/browser-compat.tsconfig.json b/browser-compat.tsconfig.json index cad0c19ef432..5ceb091bbebd 100644 --- a/browser-compat.tsconfig.json +++ b/browser-compat.tsconfig.json @@ -12,6 +12,7 @@ "./assert", "./async", "./bytes", + "./cbor", "./cli", "./collections", "./crypto", diff --git a/deno.json b/deno.json index e54d3bba8fd5..cdd98ec493b9 100644 --- a/deno.json +++ b/deno.json @@ -52,6 +52,7 @@ "./async", "./bytes", "./cache", + "./cbor", "./cli", "./collections", "./crypto", diff --git a/import_map.json b/import_map.json index a192a3a59eac..3f4f150482aa 100644 --- a/import_map.json +++ b/import_map.json @@ -11,6 +11,7 @@ "@std/async": "jsr:@std/async@^1.0.4", "@std/bytes": "jsr:@std/bytes@^1.0.2", "@std/cache": "jsr:@std/cache@^0.1.1", + "@std/cbor": "jsr:@std/cbor@^0.0.0", "@std/cli": "jsr:@std/cli@^1.0.4", "@std/collections": "jsr:@std/collections@^1.0.5", "@std/crypto": "jsr:@std/crypto@^1.0.3", From a410e42a3980d479000245b84ce984842be91be0 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:41:57 +1000 Subject: [PATCH 02/45] feat(cbor): `new CborEncoder()` --- cbor/_common.ts | 8 + cbor/deno.json | 8 + cbor/encode.ts | 274 ++++++++++++++++++++++++++++++++++ cbor/encode_test.ts | 348 ++++++++++++++++++++++++++++++++++++++++++++ cbor/mod.ts | 18 +++ 5 files changed, 656 insertions(+) create mode 100644 cbor/_common.ts create mode 100644 cbor/deno.json create mode 100644 cbor/encode.ts create mode 100644 cbor/encode_test.ts create mode 100644 cbor/mod.ts diff --git a/cbor/_common.ts b/cbor/_common.ts new file mode 100644 index 000000000000..aa8f9631242c --- /dev/null +++ b/cbor/_common.ts @@ -0,0 +1,8 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +export function numberToArray(bytes: number, x: number | bigint): Uint8Array { + const view = new DataView(new ArrayBuffer(8)); + if (typeof x === "bigint" || x % 1 === 0) view.setBigUint64(0, BigInt(x)); + else view.setFloat64(0, x); + return new Uint8Array(view.buffer.slice(-bytes)); +} diff --git a/cbor/deno.json b/cbor/deno.json new file mode 100644 index 000000000000..95a9082d9589 --- /dev/null +++ b/cbor/deno.json @@ -0,0 +1,8 @@ +{ + "name": "@std/cbor", + "version": "0.0.0", + "exports": { + ".": "./mod.ts", + "./encode": "./encode.ts" + } +} diff --git a/cbor/encode.ts b/cbor/encode.ts new file mode 100644 index 000000000000..94a535f6684b --- /dev/null +++ b/cbor/encode.ts @@ -0,0 +1,274 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +/** + * @module + */ + +import { concat } from "@std/bytes"; +import { numberToArray } from "./_common.ts"; + +/** + * This type specifies the primitive types that the implementation can + * encode/decode into/from. + */ +export type CborPrimitiveType = + | undefined + | null + | boolean + | number + | bigint + | string + | Uint8Array + | Date; + +/** + * This type specifies the values that the implementation can encode/decode + * into/from. + */ +export type CborType = CborPrimitiveType | CborTag | CborType[] | { + [k: string]: CborType; +}; + +/** + * A class that wraps {@link CborType} values, assuming the `tagContent` is the + * appropriate type and format for encoding. A list of the different types can + * be found [here](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). + * + * This class will be returned out of CborDecoder if it doesn't automatically + * know how to handle the tag number. + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborTag { + /** + * Potato + * @example Usage + * ```ts no-eval + * + * ``` + */ + tagNumber: number | bigint; + /** + * Cake + * @example Usage + * ```ts no-eval + * + * ``` + */ + tagContent: CborType; + /** + * Constructs a new instance. + * @param tagNumber The value to tag the {@link CborType} with. + * @param tagContent The {@link CborType} formatted to the correct semantics. + */ + constructor(tagNumber: number | bigint, tagContent: CborType) { + this.tagNumber = tagNumber; + this.tagContent = tagContent; + } +} + +/** + * A class to encode JavaScript values into the CBOR format based off the + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * spec. + * + * @example Usage + * ```ts no-assert + * import { CborEncoder } from "@std/cbor"; + * + * const encoder = new CborEncoder(); + * console.log(encoder.encode(5)); + * ``` + */ +export class CborEncoder { + /** + * Constructs a new instance. + */ + constructor() {} + + /** + * Encodes a {@link CborType} into a {@link Uint8Array}. + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param value Value to encode to CBOR format. + * @returns Encoded CBOR data. + */ + encode(value: CborType): Uint8Array { + switch (typeof value) { + case "number": + return this.#encodeNumber(value); + case "string": + return this.#encodeString(value); + case "boolean": + return value ? this.#TRUE : this.#FALSE; + case "undefined": + return this.#UNDEFINED; + case "bigint": + return this.#encodeBigInt(value); + } + if (value === null) return this.#NULL; + if (value instanceof Date) return this.#encodeDate(value); + if (value instanceof Uint8Array) return this.#encodeUint8Array(value); + if (value instanceof Array) return this.#encodeArray(value); + if (value instanceof CborTag) return this.#encodeTag(value); + return this.#encodeObject(value); + } + + /** + * Encodes an array of {@link CborType} into a {@link Uint8Array}. + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param array Values to encode into CBOR format. + * @returns Encoded CBOR data. + */ + encodeSequence(array: CborType[]): Uint8Array { + return concat(array.map((x) => this.encode(x))); + } + + get #UNDEFINED(): Uint8Array { + return new Uint8Array([0b111_10111]); + } + + get #TRUE(): Uint8Array { + return new Uint8Array([0b111_10101]); + } + + get #FALSE(): Uint8Array { + return new Uint8Array([0b111_10100]); + } + + get #NULL(): Uint8Array { + return new Uint8Array([0b111_10110]); + } + + #encodeNumber(x: number): Uint8Array { + if (x % 1 === 0) { + const majorType = x < 0 ? 0b001_00000 : 0b000_00000; + if (x < 0) x = -x - 1; + + if (x < 24) return new Uint8Array([majorType + x]); + if (x < 2 ** 8) return new Uint8Array([majorType + 24, x]); + if (x < 2 ** 16) { + return concat([new Uint8Array([majorType + 25]), numberToArray(2, x)]); + } + if (x < 2 ** 32) { + return concat([new Uint8Array([majorType + 26]), numberToArray(4, x)]); + } + if (x < 2 ** 64) { + return concat([new Uint8Array([majorType + 27]), numberToArray(8, x)]); + } + throw new RangeError( + `Cannot encode number: It (${x}) exceeds 2 ** 64 - 1`, + ); + } + return concat([new Uint8Array([0b111_11011]), numberToArray(8, x)]); + } + + #encodeBigInt(x: bigint): Uint8Array { + if ((x < 0n ? -x : x) < 2n ** 32n) return this.#encodeNumber(Number(x)); + + const head = new Uint8Array([x < 0n ? 0b010_11011 : 0b000_11011]); + if (x < 0n) x = -x - 1n; + + if (x < 2n ** 64n) return concat([head, numberToArray(8, x)]); + throw new RangeError(`Cannot encode bigint: It (${x}) exceeds 2 ** 64 - 1`); + } + + #encodeString(x: string): Uint8Array { + const array = this.#encodeUint8Array(new TextEncoder().encode(x)); + array[0]! += 1 << 5; + return array; + } + + #encodeUint8Array(x: Uint8Array): Uint8Array { + if (x.length < 24) { + return concat([new Uint8Array([0b010_00000 + x.length]), x]); + } + if (x.length < 2 ** 8) { + return concat([new Uint8Array([0b010_11000, x.length]), x]); + } + if (x.length < 2 ** 16) { + return concat([ + new Uint8Array([0b010_11001]), + numberToArray(2, x.length), + x, + ]); + } + if (x.length < 2 ** 32) { + return concat([ + new Uint8Array([0b010_11010]), + numberToArray(4, x.length), + x, + ]); + } + // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. + return concat([ + new Uint8Array([0b010_11011]), + numberToArray(8, x.length), + x, + ]); + } + + #encodeDate(x: Date): Uint8Array { + return concat([ + new Uint8Array([0b110_00001]), + this.#encodeNumber(x.getTime() / 1000), + ]); + } + + #encodeArray(x: CborType[]): Uint8Array { + let head: number[]; + if (x.length < 24) head = [0b100_00000 + x.length]; + else if (x.length < 2 ** 8) head = [0b100_11000, x.length]; + else if (x.length < 2 ** 16) { + head = [0b100_11001, ...numberToArray(2, x.length)]; + } else if (x.length < 2 ** 32) { + head = [0b100_11010, ...numberToArray(4, x.length)]; + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support an `Array` being that large. + else head = [0b100_11011, ...numberToArray(8, x.length)]; + return concat([Uint8Array.from(head), ...x.map((x) => this.encode(x))]); + } + + #encodeObject(x: { [k: string]: CborType }): Uint8Array { + const len = Object.keys(x).length; + let head: number[]; + if (len < 24) head = [0b101_00000 + len]; + else if (len < 2 ** 8) head = [0b101_11000, len]; + else if (len < 2 ** 16) head = [0b101_11001, ...numberToArray(2, len)]; + else if (len < 2 ** 32) head = [0b101_11010, ...numberToArray(4, len)]; + // Can safely assume `len < 2 ** 64` as JavaScript doesn't support an `Object` being that Large. + else head = [0b101_11011, ...numberToArray(8, len)]; + return concat([ + Uint8Array.from(head), + ...Object.entries(x).map(( + [k, v], + ) => [this.#encodeString(k), this.encode(v)]).flat(), + ]); + } + + #encodeTag(x: CborTag): Uint8Array { + let head: number[]; + if (x.tagNumber < 24) head = [0b110_00000 + Number(x.tagNumber)]; + else if (x.tagNumber < 2 ** 8) head = [0b110_11000, Number(x.tagNumber)]; + else if (x.tagNumber < 2 ** 16) { + head = [0b110_11001, ...numberToArray(2, x.tagNumber)]; + } else if (x.tagNumber < 2 ** 32) { + head = [0b110_11010, ...numberToArray(4, x.tagNumber)]; + } else if (x.tagNumber < 2 ** 64) { + head = [0b110_11011, ...numberToArray(8, x.tagNumber)]; + } else { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, + ); + } + return concat([Uint8Array.from(head), this.encode(x.tagContent)]); + } +} diff --git a/cbor/encode_test.ts b/cbor/encode_test.ts new file mode 100644 index 000000000000..ef6221bce1c3 --- /dev/null +++ b/cbor/encode_test.ts @@ -0,0 +1,348 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { CborEncoder, CborTag } from "./mod.ts"; + +function random(start: number, end: number): number { + return Math.floor(Math.random() * (end - start) + start); +} + +Deno.test("CborEncoder() encoding undefined", () => { + assertEquals( + new CborEncoder().encode(undefined), + new Uint8Array([0b111_10111]), + ); +}); + +Deno.test("CborEncoder() encoding null", () => { + assertEquals(new CborEncoder().encode(null), new Uint8Array([0b111_10110])); +}); + +Deno.test("CborEncoder() encoding true", () => { + assertEquals(new CborEncoder().encode(true), new Uint8Array([0b111_10101])); +}); + +Deno.test("CborEncoder() encoding false", () => { + assertEquals(new CborEncoder().encode(false), new Uint8Array([0b111_10100])); +}); + +Deno.test("CborEncoder() encoding numbers as Uint", () => { + const encoder = new CborEncoder(); + + let num: number | bigint = random(0, 24); + assertEquals(encoder.encode(num), new Uint8Array([0b000_00000 + num])); + + num = random(24, 2 ** 8); + assertEquals(encoder.encode(num), new Uint8Array([0b000_11000, num])); + + num = random(2 ** 8, 2 ** 16); + assertEquals( + encoder.encode(num), + new Uint8Array([0b000_11001, num >> 8 & 0xFF, num & 0xFF]), + ); + + num = random(2 ** 16, 2 ** 32); + assertEquals( + encoder.encode(num), + new Uint8Array([ + 0b000_11010, + num >> 24 & 0xFF, + num >> 16 & 0xFF, + num >> 8 & 0xFF, + num & 0xFF, + ]), + ); + + // JavaScript fails at correctly bit-wising this many bits as a number. + num = BigInt(Number.MAX_SAFE_INTEGER); + assertEquals( + encoder.encode(Number.MAX_SAFE_INTEGER), + new Uint8Array( + [ + 0b000_11011, + num >> 56n & 0xFFn, + num >> 48n & 0xFFn, + num >> 40n & 0xFFn, + num >> 32n & 0xFFn, + num >> 24n & 0xFFn, + num >> 16n & 0xFFn, + num >> 8n & 0xFFn, + num & 0xFFn, + ].map((x) => Number(x)), + ), + ); +}); + +Deno.test("CborEncoder() encoding numbers as Int", () => { + const num = -random(1, 24); // -0 === 0 + assertEquals( + new CborEncoder().encode(num), + new Uint8Array([0b001_00000 + (-num - 1)]), + ); +}); + +Deno.test("CborEncoder() encoding numbers as Float", () => { + const num = Math.random() * 2 ** 32; + const view = new DataView(new ArrayBuffer(8)); + view.setFloat64(0, num); + assertEquals( + new CborEncoder().encode(num), + concat([new Uint8Array([0b111_11011]), new Uint8Array(view.buffer)]), + ); +}); + +Deno.test("CborEncoder() encoding bigints as Uint", () => { + const encoder = new CborEncoder(); + + let num = BigInt(random(0, 24)); + assertEquals( + encoder.encode(num), + new Uint8Array([0b000_00000 + Number(num)]), + ); + + num = BigInt(random(24, 2 ** 8)); + assertEquals(encoder.encode(num), new Uint8Array([0b000_11000, Number(num)])); + + num = BigInt(random(2 ** 8, 2 ** 16)); + assertEquals( + encoder.encode(num), + new Uint8Array([ + 0b000_11001, + Number(num >> 8n & 0xFFn), + Number(num & 0xFFn), + ]), + ); + + num = BigInt(random(2 ** 16, 2 ** 32)); + assertEquals( + encoder.encode(num), + new Uint8Array([ + 0b000_11010, + Number(num >> 24n & 0xFFn), + Number(num >> 16n & 0xFFn), + Number(num >> 8n & 0xFFn), + Number(num & 0xFFn), + ]), + ); + + num = BigInt(random(2 ** 32, 2 ** 64)); + assertEquals( + encoder.encode(num), + new Uint8Array([ + 0b000_11011, + Number(num >> 56n & 0xFFn), + Number(num >> 48n & 0xFFn), + Number(num >> 40n & 0xFFn), + Number(num >> 32n & 0xFFn), + Number(num >> 24n & 0xFFn), + Number(num >> 16n & 0xFFn), + Number(num >> 8n & 0xFFn), + Number(num & 0xFFn), + ]), + ); +}); + +Deno.test("CborEncoder() encoding bigints as Int", () => { + const num = -BigInt(random(1, 24)); // -0 === 0 + assertEquals( + new CborEncoder().encode(num), + new Uint8Array([0b001_00000 + Number(-num - 1n)]), + ); +}); + +Deno.test("CborEncoder() encoding strings", () => { + const encoder = new CborEncoder(); + const decoder = new TextDecoder(); + + let bytes = new Uint8Array(random(0, 24)).map((_) => random(97, 123)); // Range: `a` - `z` + assertEquals( + encoder.encode(decoder.decode(bytes)), + new Uint8Array([0b011_00000 + bytes.length, ...bytes]), + ); + + bytes = new Uint8Array(random(24, 2 ** 8)).map((_) => random(97, 123)); // Range: `a` - `z` + assertEquals( + encoder.encode(decoder.decode(bytes)), + new Uint8Array([0b011_11000, bytes.length, ...bytes]), + ); + + bytes = new Uint8Array(random(2 ** 8, 2 ** 16)).map((_) => random(97, 123)); // Range: `a` - `z` + assertEquals( + encoder.encode(decoder.decode(bytes)), + new Uint8Array([ + 0b011_11001, + bytes.length >> 8 & 0xFF, + bytes.length & 0xFF, + ...bytes, + ]), + ); + + bytes = new Uint8Array(random(2 ** 16, 2 ** 17)).map((_) => random(97, 123)); // Range: `a` - `z` + assertEquals( + encoder.encode(decoder.decode(bytes)), + new Uint8Array([ + 0b011_11010, + bytes.length >> 24 & 0xFF, + bytes.length >> 16 & 0xFF, + bytes.length >> 8 & 0xFF, + bytes.length & 0xFF, + ...bytes, + ]), + ); + + // Can't test the next bracket up due to JavaScript limitations. +}); + +Deno.test("CborEncoder() encoding Uint8Arrays", () => { + const encoder = new CborEncoder(); + + let bytes = new Uint8Array(random(0, 24)); + assertEquals( + encoder.encode(bytes), + new Uint8Array([0b010_00000 + bytes.length, ...bytes]), + ); + + bytes = new Uint8Array(random(24, 2 ** 8)); + assertEquals( + encoder.encode(bytes), + new Uint8Array([0b010_11000, bytes.length, ...bytes]), + ); + + bytes = new Uint8Array(random(2 ** 8, 2 ** 16)); + assertEquals( + encoder.encode(bytes), + new Uint8Array([ + 0b010_11001, + bytes.length >> 8 & 0xFF, + bytes.length & 0xFF, + ...bytes, + ]), + ); + + bytes = new Uint8Array(random(2 ** 16, 2 ** 17)); + assertEquals( + encoder.encode(bytes), + new Uint8Array([ + 0b010_11010, + bytes.length >> 24 & 0xFF, + bytes.length >> 16 & 0xFF, + bytes.length >> 8 & 0xFF, + bytes.length & 0xFF, + ...bytes, + ]), + ); + + // Can't test the next bracket up due to JavaScript limitations. +}); + +Deno.test("CborEncoder() encoding Dates", () => { + const encoder = new CborEncoder(); + const date = new Date(); + assertEquals( + encoder.encode(date), + new Uint8Array([0b110_00001, ...encoder.encode(date.getTime() / 1000)]), + ); +}); + +Deno.test("CborEncoder() encoding arrays", () => { + const encoder = new CborEncoder(); + + let array = new Array(random(0, 24)).fill(0); + assertEquals( + encoder.encode(array), + new Uint8Array([ + 0b100_00000 + array.length, + ...array.map((x) => [...encoder.encode(x)]).flat(), + ]), + ); + + array = new Array(random(24, 2 ** 8)).fill(0); + assertEquals( + encoder.encode(array), + new Uint8Array([ + 0b100_11000, + array.length, + ...array.map((x) => [...encoder.encode(x)]).flat(), + ]), + ); + + array = new Array(random(2 ** 8, 2 ** 16)).fill(0); + assertEquals( + encoder.encode(array), + new Uint8Array([ + 0b100_11001, + array.length >> 8 & 0xFF, + array.length & 0xFF, + ...array.map((x) => [...encoder.encode(x)]).flat(), + ]), + ); + + array = new Array(random(2 ** 16, 2 ** 17)).fill(0); + assertEquals( + encoder.encode(array), + new Uint8Array([ + 0b100_11010, + array.length >> 24 & 0xFF, + array.length >> 16 & 0xFF, + array.length >> 8 & 0xFF, + array.length & 0xFF, + ...array.map((x) => [...encoder.encode(x)]).flat(), + ]), + ); + + // Can't test the next bracket up due to JavaScript limitations. +}); + +Deno.test("CborEncoder() encoding objects", () => { + const encoder = new CborEncoder(); + + let pairs = random(0, 24); + let entries: [string, number][] = new Array(pairs).fill(0).map(( + _, + i, + ) => [i.toString(), i]); + assertEquals( + encoder.encode(Object.fromEntries(entries)), + new Uint8Array([ + 0b101_00000 + pairs, + ...entries.map(([k, v]) => [...encoder.encode(k), ...encoder.encode(v)]) + .flat(), + ]), + ); + + pairs = random(24, 2 ** 8); + entries = new Array(pairs).fill(0).map((_, i) => [i.toString(), i]); + assertEquals( + encoder.encode(Object.fromEntries(entries)), + new Uint8Array([ + 0b101_11000, + pairs, + ...entries.map(([k, v]) => [...encoder.encode(k), ...encoder.encode(v)]) + .flat(), + ]), + ); + + pairs = random(2 ** 8, 2 ** 16); + entries = new Array(pairs).fill(0).map((_, i) => [i.toString(), i]); + assertEquals( + encoder.encode(Object.fromEntries(entries)), + new Uint8Array([ + 0b101_11001, + pairs >> 8 & 0xFF, + pairs & 0xFF, + ...entries.map(([k, v]) => [...encoder.encode(k), ...encoder.encode(v)]) + .flat(), + ]), + ); + + // Can't test the next two bracket up due to JavaScript limitations. +}); + +Deno.test("CborEncoder() encoding CborTag()", () => { + const bytes = new Uint8Array(random(0, 24)).map((_) => random(0, 256)); + assertEquals( + new CborEncoder().encode(new CborTag(2, bytes)), + new Uint8Array([0b110_00010, 0b010_00000 + bytes.length, ...bytes]), + ); +}); diff --git a/cbor/mod.ts b/cbor/mod.ts new file mode 100644 index 000000000000..77d00b5980b9 --- /dev/null +++ b/cbor/mod.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +/** + * CBOR is a binary serialisation format that is language agnostic. It is like + * JSON, but allows more a much more wide range of values, and supports + * streaming. This implementation is based off the + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * spec. + * + * @example Usage + * ```ts no-assert + * import { CborEncoder } from "@std/cbor"; + * + * const encoder = new CborEncoder(); + * console.log(encoder.encode(5)); + * ``` + */ +export * from "./encode.ts"; From 953a60d1b30f81656ca585a0b11f8bb3905f5739 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:42:45 +1000 Subject: [PATCH 03/45] feat(cbor): `new CborDecoder()` --- cbor/_common.ts | 35 +++++ cbor/decode.ts | 331 ++++++++++++++++++++++++++++++++++++++++++++ cbor/decode_test.ts | 166 ++++++++++++++++++++++ cbor/deno.json | 1 + cbor/mod.ts | 1 + 5 files changed, 534 insertions(+) create mode 100644 cbor/decode.ts create mode 100644 cbor/decode_test.ts diff --git a/cbor/_common.ts b/cbor/_common.ts index aa8f9631242c..49a3263188da 100644 --- a/cbor/_common.ts +++ b/cbor/_common.ts @@ -6,3 +6,38 @@ export function numberToArray(bytes: number, x: number | bigint): Uint8Array { else view.setFloat64(0, x); return new Uint8Array(view.buffer.slice(-bytes)); } + +export function arrayToNumber( + buffer: ArrayBufferLike & { BYTES_PER_ELEMENT?: never }, + isInteger: true, +): number | bigint; +export function arrayToNumber( + buffer: ArrayBufferLike & { BYTES_PER_ELEMENT?: never }, + isInteger: false, +): number; +export function arrayToNumber( + buffer: ArrayBufferLike & { BYTES_PER_ELEMENT?: never }, + isInteger: boolean, +): number | bigint { + const view = new DataView(buffer); + if (isInteger) { + switch (buffer.byteLength) { + case 1: + return view.getUint8(0); + case 2: + return view.getUint16(0); + case 4: + return view.getUint32(0); + default: + return view.getBigUint64(0); + } + } + switch (buffer.byteLength) { + case 2: + return view.getFloat16(0); + case 4: + return view.getFloat32(0); + default: + return view.getFloat64(0); + } +} diff --git a/cbor/decode.ts b/cbor/decode.ts new file mode 100644 index 000000000000..891f984e7045 --- /dev/null +++ b/cbor/decode.ts @@ -0,0 +1,331 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { concat } from "@std/bytes"; +import { arrayToNumber } from "./_common.ts"; +import { CborTag, type CborType } from "./encode.ts"; + +/** + * A class to decode CBOR encoded {@link Uint8Array} into {@link CborType} + * values, based off the + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * spec. + * + * @example Usage + * ```ts + * import { assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder } from "@std/cbor"; + * + * const encoder = new CborEncoder(); + * const decoder = new CborDecoder(); + * + * assertEquals(decoder.decode(encoder.encode('Hello World')), 'Hello World') + * ``` + */ +export class CborDecoder { + #source: number[] = []; + /** + * Constructs a new instance. + */ + constructor() {} + + /** + * Decodes a {@link Uint8Array} into a {@link CborType}. + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param x Value to decode from CBOR format. + * @returns Decoded CBOR data. + */ + decode(x: Uint8Array): CborType { + if (!x.length) throw RangeError("Cannot decode empty Uint8Array"); + + this.#source = Array.from(x).reverse(); + const y = this.#decode(); + this.#source = []; + return y; + } + + /** + * Decodes an array of {@link CborType} from a {@link Uint8Array}. + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param data Encoded data to be decoded from CBOR format. + * @returns Decoded CBOR data. + */ + decodeSequence(data: Uint8Array): CborType[] { + this.#source = Array.from(data).reverse(); + const output: CborType[] = []; + while (this.#source.length) output.push(this.#decode()); + return output; + } + + #decode(): CborType { + const byte = this.#source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); + + const majorType = byte >> 5; + const aI = byte & 0b000_11111; + switch (majorType) { + case 0: + return this.#decodeZero(aI); + case 1: + return this.#decodeOne(aI); + case 2: + return this.#decodeTwo(aI); + case 3: + return this.#decodeThree(aI); + case 4: + return this.#decodeFour(aI); + case 5: + return this.#decodeFive(aI); + case 6: + return this.#decodeSix(aI); + default: // Only possible for it to be 7 + return this.#decodeSeven(aI); + } + } + + #decodeZero(aI: number): number | bigint { + if (aI < 24) return aI; + if (aI <= 27) { + return arrayToNumber( + Uint8Array.from( + this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ); + } + throw new RangeError( + `Cannot decode value (0b000_${aI.toString(2).padStart(5, "0")})`, + ); + } + + #decodeOne(aI: number): number | bigint { + if (aI > 27) { + throw new RangeError( + `Cannot decode value (0b001_${aI.toString(2).padStart(5, "0")})`, + ); + } + const x = this.#decodeZero(aI); + if (typeof x === "bigint") return -x - 1n; + return -x - 1; + } + + #decodeTwo(aI: number): Uint8Array { + if (aI < 24) return Uint8Array.from(this.#source.splice(-aI, aI).reverse()); + if (aI <= 27) { + // Can safely assume `this.#source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // 2 ** 53 is the tipping point where integers loose precision. + const len = Number( + arrayToNumber( + Uint8Array.from( + this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ), + ); + return Uint8Array.from(this.#source.splice(-len, len).reverse()); + } + if (aI === 31) { + let byte = this.#source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); + + const output: Uint8Array[] = []; + while (byte !== 0b111_11111) { + if (byte >> 5 === 2) { + if ((byte & 0b11111) !== 31) { + output.push(this.#decodeTwo(byte & 0b11111)); + } else { + throw new TypeError( + "Indefinite length byte strings cannot contain indefinite length byte strings", + ); + } + } else { + throw new TypeError( + `Cannot decode value (b${ + (byte >> 5).toString(2).padStart(3, "0") + }_${ + (byte & 0b11111).toString(2).padStart(5, "0") + }) inside an indefinite length byte string`, + ); + } + + byte = this.#source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); + } + return concat(output); + } + throw new RangeError( + `Cannot decode value (0b010_${aI.toString(2).padStart(5, "0")})`, + ); + } + + #decodeThree(aI: number): string { + if (aI > 27) { + if (aI === 31) { + let byte = this.#source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); + + const output: string[] = []; + while (byte !== 0b111_11111) { + if (byte >> 5 === 2) { + if ((byte & 0b11111) !== 31) { + output.push(this.#decodeThree(byte & 0b11111)); + } else { + throw new TypeError( + "Indefinite length text strings cannot contain indefinite length text strings", + ); + } + } else { + throw new TypeError( + `Cannot decode value (b${ + (byte >> 5).toString(2).padStart(3, "0") + }_${ + (byte & 0b11111).toString(2).padStart(5, "0") + }) inside an indefinite length text string`, + ); + } + byte = this.#source.pop(); + if (byte == undefined) { + throw new RangeError("More bytes were expected"); + } + } + return output.join(""); + } + throw new RangeError( + `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, + ); + } + return new TextDecoder().decode(this.#decodeTwo(aI)); + } + + #decodeFour(aI: number): CborType[] { + if (aI > 27) { + if (aI === 31) { + const array: CborType[] = []; + while (this.#source[this.#source.length - 1] !== 0b111_11111) { + array.push(this.#decode()); + } + this.#source.pop(); + return array; + } + throw new RangeError( + `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, + ); + } + const array: CborType[] = []; + // Can safely assume `this.#source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // 2 ** 53 is the tipping point where integers loose precision. + const len = aI < 24 ? aI : Number( + arrayToNumber( + Uint8Array.from( + this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ), + ); + for (let i = 0; i < len; ++i) array.push(this.#decode()); + return array; + } + + #decodeFive(aI: number): { [k: string]: CborType } { + if (aI > 27) { + if (aI === 31) { + const object: { [k: string]: CborType } = {}; + while (this.#source[this.#source.length - 1] !== 0b111_11111) { + const key = this.#decode(); + if (typeof key !== "string") { + throw new TypeError( + `Cannot decode key of type "${typeof key}": This implementation only support "text string" keys`, + ); + } + if (object[key] !== undefined) { + throw new TypeError( + `A Map cannot have duplicate keys: Key (${key}) already exists`, + ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps + } + object[key] = this.#decode(); + } + return object; + } + throw new RangeError( + `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, + ); + } + const object: { [k: string]: CborType } = {}; + // Can safely assume `this.#source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // 2 ** 53 is the tipping point where integers loose precision. + const len = aI < 24 ? aI : Number( + arrayToNumber( + Uint8Array.from( + this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ), + ); + for (let i = 0; i < len; ++i) { + const key = this.#decode(); + if (typeof key !== "string") { + throw new TypeError( + `Cannot decode key of type "${typeof key}": This implementation only support "text string" keys`, + ); + } + if (object[key] !== undefined) { + throw new TypeError( + `A Map cannot have duplicate keys: Key (${key}) already exists`, + ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps + } + object[key] = this.#decode(); + } + return object; + } + + #decodeSix(aI: number): Date | CborTag { + const tagNumber = this.#decodeZero(aI) as number; + const tagContent = this.#decode(); + switch (tagNumber) { + case 0: + if (typeof tagContent !== "string") { + throw new TypeError('Invalid TagItem: Expected a "text string"'); + } + return new Date(tagContent); + case 1: + if (typeof tagContent !== "number" && typeof tagContent !== "bigint") { + throw new TypeError( + 'Invalid TagItem: Expected a "integer" or "float"', + ); + } + return new Date(Number(tagContent) * 1000); + } + return new CborTag(tagNumber, tagContent); + } + + #decodeSeven(aI: number): undefined | null | boolean | number { + switch (aI) { + case 20: + return false; + case 21: + return true; + case 22: + return null; + case 23: + return undefined; + } + if (25 <= aI && aI <= 27) { + return arrayToNumber( + Uint8Array.from( + this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + false, + ); + } + throw new RangeError( + `Cannot decode value (0b111_${aI.toString(2).padStart(5, "0")})`, + ); + } +} diff --git a/cbor/decode_test.ts b/cbor/decode_test.ts new file mode 100644 index 000000000000..4ac7f4816e27 --- /dev/null +++ b/cbor/decode_test.ts @@ -0,0 +1,166 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { CborDecoder, CborEncoder, CborTag } from "./mod.ts"; + +function random(start: number, end: number): number { + return Math.floor(Math.random() * (end - start) + start); +} + +Deno.test("CborDecoder() decoding undefined", () => { + assertEquals( + new CborDecoder().decode(new CborEncoder().encode(undefined)), + undefined, + ); +}); + +Deno.test("CborDecoder() decoding null", () => { + assertEquals(new CborDecoder().decode(new CborEncoder().encode(null)), null); +}); + +Deno.test("CborDecoder() decoding true", () => { + assertEquals(new CborDecoder().decode(new CborEncoder().encode(true)), true); +}); + +Deno.test("CborDecoder() decoding false", () => { + assertEquals( + new CborDecoder().decode(new CborEncoder().encode(false)), + false, + ); +}); + +Deno.test("CborDecoder() decoding integers", () => { + const encoder = new CborEncoder(); + const decoder = new CborDecoder(); + + let num = random(0, 24); + assertEquals(decoder.decode(encoder.encode(num)), num); + assertEquals(decoder.decode(encoder.encode(BigInt(num))), num); + + num = random(24, 2 ** 8); + assertEquals(decoder.decode(encoder.encode(num)), num); + assertEquals(decoder.decode(encoder.encode(BigInt(num))), num); + + num = random(2 ** 8, 2 ** 16); + assertEquals(decoder.decode(encoder.encode(num)), num); + assertEquals(decoder.decode(encoder.encode(BigInt(num))), num); + + num = random(2 ** 16, 2 ** 32); + assertEquals(decoder.decode(encoder.encode(num)), num); + assertEquals(decoder.decode(encoder.encode(BigInt(num))), num); + + num = random(2 ** 32, 2 ** 64); + assertEquals(decoder.decode(encoder.encode(num)), BigInt(num)); + assertEquals(decoder.decode(encoder.encode(BigInt(num))), BigInt(num)); +}); + +Deno.test("CborDecoder() decoding strings", () => { + const encoder = new CborEncoder(); + const decoder = new CborDecoder(); + const textDecoder = new TextDecoder(); + + let text = textDecoder.decode( + new Uint8Array(random(0, 24)).map((_) => random(97, 123)), + ); // Range: `a` - `z` + assertEquals(decoder.decode(encoder.encode(text)), text); + + text = textDecoder.decode( + new Uint8Array(random(24, 2 ** 8)).map((_) => random(97, 123)), + ); // Range: `a` - `z` + assertEquals(decoder.decode(encoder.encode(text)), text); + + text = textDecoder.decode( + new Uint8Array(random(2 ** 8, 2 ** 16)).map((_) => random(97, 123)), + ); // Range: `a` - `z` + assertEquals(decoder.decode(encoder.encode(text)), text); + + text = textDecoder.decode( + new Uint8Array(random(2 ** 16, 2 ** 17)).map((_) => random(97, 123)), + ); // Range: `a` - `z` + assertEquals(decoder.decode(encoder.encode(text)), text); + + // Can't test the next bracket up due to JavaScript limitations. +}); + +Deno.test("CborDecoder() decoding Uint8Arrays", () => { + const encoder = new CborEncoder(); + const decoder = new CborDecoder(); + + let bytes = new Uint8Array(random(0, 24)).map((_) => random(0, 256)); + assertEquals(decoder.decode(encoder.encode(bytes)), bytes); + + bytes = new Uint8Array(random(24, 2 ** 8)).map((_) => random(0, 256)); + assertEquals(decoder.decode(encoder.encode(bytes)), bytes); + + bytes = new Uint8Array(random(2 ** 8, 2 ** 16)).map((_) => random(0, 256)); + assertEquals(decoder.decode(encoder.encode(bytes)), bytes); + + bytes = new Uint8Array(random(2 ** 16, 2 ** 17)).map((_) => random(0, 256)); + assertEquals(decoder.decode(encoder.encode(bytes)), bytes); + + // Can't test the next bracket up due to JavaScript limitations. +}); + +Deno.test("CborDecoder() decoding Dates", () => { + const date = new Date(); + assertEquals(new CborDecoder().decode(new CborEncoder().encode(date)), date); +}); + +Deno.test("CborDecoder() decoding arrays", () => { + const encoder = new CborEncoder(); + const decoder = new CborDecoder(); + + let array = new Array(random(0, 24)).fill(0).map((_) => random(0, 2 ** 32)); + assertEquals(decoder.decode(encoder.encode(array)), array); + + array = new Array(random(24, 2 ** 8)).fill(0).map((_) => random(0, 2 ** 32)); + assertEquals(decoder.decode(encoder.encode(array)), array); + + array = new Array(random(2 ** 8, 2 ** 16)).fill(0).map((_) => + random(0, 2 ** 32) + ); + assertEquals(decoder.decode(encoder.encode(array)), array); + + array = new Array(random(2 ** 16, 2 ** 17)).fill(0).map((_) => + random(0, 2 ** 32) + ); + assertEquals(decoder.decode(encoder.encode(array)), array); + + // Can't test the next bracket up due to JavaScript limitations. +}); + +Deno.test("CborDecoder() decoding objects", () => { + const encoder = new CborEncoder(); + const decoder = new CborDecoder(); + + let pairs = random(0, 24); + let object = Object.fromEntries( + new Array(pairs).fill(0).map((_, i) => [i, i]), + ); + assertEquals(decoder.decode(encoder.encode(object)), object); + + pairs = random(24, 2 ** 8); + object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); + assertEquals(decoder.decode(encoder.encode(object)), object); + + pairs = random(2 ** 8, 2 ** 16); + object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); + assertEquals(decoder.decode(encoder.encode(object)), object); + + pairs = random(2 ** 16, 2 ** 17); + object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); + assertEquals(decoder.decode(encoder.encode(object)), object); + + // Can't test the next bracket up due to JavaScript limitations. +}); + +Deno.test("CborDecoder() decoding CborTag()", () => { + const tag = new CborTag( + 2, + new Uint8Array(random(0, 24)).map((_) => random(0, 256)), + ); + assertEquals( + new CborDecoder().decode(new CborEncoder().encode(tag)), + tag, + ); +}); diff --git a/cbor/deno.json b/cbor/deno.json index 95a9082d9589..cd67f15d11c4 100644 --- a/cbor/deno.json +++ b/cbor/deno.json @@ -3,6 +3,7 @@ "version": "0.0.0", "exports": { ".": "./mod.ts", + "./decode": "./decode.ts", "./encode": "./encode.ts" } } diff --git a/cbor/mod.ts b/cbor/mod.ts index 77d00b5980b9..a2c451cca8bc 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -15,4 +15,5 @@ * console.log(encoder.encode(5)); * ``` */ +export * from "./decode.ts"; export * from "./encode.ts"; From 4c295c26fc6ebbb232c833d9f278336f5c9b982d Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:44:45 +1000 Subject: [PATCH 04/45] fix(cbor): `@module` being in the wrong file --- cbor/encode.ts | 4 ---- cbor/mod.ts | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cbor/encode.ts b/cbor/encode.ts index 94a535f6684b..90bf686e9ed4 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -1,9 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -/** - * @module - */ - import { concat } from "@std/bytes"; import { numberToArray } from "./_common.ts"; diff --git a/cbor/mod.ts b/cbor/mod.ts index a2c451cca8bc..f7eecaa13a9b 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -14,6 +14,8 @@ * const encoder = new CborEncoder(); * console.log(encoder.encode(5)); * ``` + * + * @module */ export * from "./decode.ts"; export * from "./encode.ts"; From 5e96fc0021b41f99244c89006cae74078ce6a4a6 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:06:30 +1000 Subject: [PATCH 05/45] chore(cbor): `deno fmt` --- cbor/mod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbor/mod.ts b/cbor/mod.ts index f7eecaa13a9b..4bfad700f7b7 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -14,7 +14,7 @@ * const encoder = new CborEncoder(); * console.log(encoder.encode(5)); * ``` - * + * * @module */ export * from "./decode.ts"; From b36ce134327829192d672a28240ba699f2b8bb99 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:15:26 +1000 Subject: [PATCH 06/45] docs(cbor): Updated `CborTag` docs --- cbor/encode.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/cbor/encode.ts b/cbor/encode.ts index 90bf686e9ed4..f8b42ed27e81 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -33,24 +33,81 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * This class will be returned out of CborDecoder if it doesn't automatically * know how to handle the tag number. * @example Usage - * ```ts no-eval + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder, CborTag } from "@std/cbor"; + * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; * + * const decoder = new CborDecoder(); + * const encoder = new CborEncoder(); + * + * const rawMessage = new TextEncoder().encode("Hello World"); + * + * const encodedMessage = encoder.encode( + * new CborTag( + * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". + * encodeBase64Url(rawMessage), + * ), + * ); + * const decodedMessage = decoder.decode(encodedMessage); + * assert(decodedMessage instanceof CborTag); + * assert(typeof decodedMessage.tagContent === "string"); + * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); * ``` */ export class CborTag { /** - * Potato + * The number indicating how the tagContent should be interpreted based off + * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). * @example Usage - * ```ts no-eval + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder, CborTag } from "@std/cbor"; + * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; + * + * const decoder = new CborDecoder(); + * const encoder = new CborEncoder(); * + * const rawMessage = new TextEncoder().encode("Hello World"); + * + * const encodedMessage = encoder.encode( + * new CborTag( + * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". + * encodeBase64Url(rawMessage), + * ), + * ); + * const decodedMessage = decoder.decode(encodedMessage); + * assert(decodedMessage instanceof CborTag); + * assert(typeof decodedMessage.tagContent === "string"); + * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); * ``` */ tagNumber: number | bigint; /** - * Cake + * The content wrapped around the tagNumber indicating how it should be + * interpreted based off + * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). * @example Usage - * ```ts no-eval + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder, CborTag } from "@std/cbor"; + * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; + * + * const decoder = new CborDecoder(); + * const encoder = new CborEncoder(); + * + * const rawMessage = new TextEncoder().encode("Hello World"); * + * const encodedMessage = encoder.encode( + * new CborTag( + * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". + * encodeBase64Url(rawMessage), + * ), + * ); + * const decodedMessage = decoder.decode(encodedMessage); + * assert(decodedMessage instanceof CborTag); + * assert(typeof decodedMessage.tagContent === "string"); + * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); * ``` */ tagContent: CborType; From 3a330ba5e44edc110c0861995dc77373423f0f29 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:16:29 +1000 Subject: [PATCH 07/45] fix(cbor): link:docs --- cbor/mod.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cbor/mod.ts b/cbor/mod.ts index 4bfad700f7b7..99de2d6c8e8a 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -7,7 +7,6 @@ * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * spec. * - * @example Usage * ```ts no-assert * import { CborEncoder } from "@std/cbor"; * From 0a6eb45ec1aac46e177728277c3a7853301b78ee Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:28:12 +1000 Subject: [PATCH 08/45] docs(cbor): Updated `CborEncoder` docs --- cbor/encode.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/cbor/encode.ts b/cbor/encode.ts index f8b42ed27e81..2bd26b43e386 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -128,11 +128,28 @@ export class CborTag { * spec. * * @example Usage - * ```ts no-assert - * import { CborEncoder } from "@std/cbor"; + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder } from "@std/cbor"; * + * const decoder = new CborDecoder(); * const encoder = new CborEncoder(); - * console.log(encoder.encode(5)); + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encoder.encode(rawMessage); + * const decodedMessage = decoder.decode(encodedMessage); + * + * assert(decodedMessage instanceof Array); + * assertEquals(decodedMessage, rawMessage); * ``` */ export class CborEncoder { @@ -144,8 +161,28 @@ export class CborEncoder { /** * Encodes a {@link CborType} into a {@link Uint8Array}. * @example Usage - * ```ts no-eval + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder } from "@std/cbor"; + * + * const decoder = new CborDecoder(); + * const encoder = new CborEncoder(); + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encoder.encode(rawMessage); + * const decodedMessage = decoder.decode(encodedMessage); * + * assert(decodedMessage instanceof Array); + * assertEquals(decodedMessage, rawMessage); * ``` * * @param value Value to encode to CBOR format. @@ -175,8 +212,28 @@ export class CborEncoder { /** * Encodes an array of {@link CborType} into a {@link Uint8Array}. * @example Usage - * ```ts no-eval + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder } from "@std/cbor"; + * + * const decoder = new CborDecoder(); + * const encoder = new CborEncoder(); + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encoder.encodeSequence(rawMessage); + * const decodedMessage = decoder.decodeSequence(encodedMessage); * + * assert(decodedMessage instanceof Array); + * assertEquals(decodedMessage, rawMessage); * ``` * * @param array Values to encode into CBOR format. From b5d5f3cf8cd740154a1638b3e65883c2046cecd5 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:28:24 +1000 Subject: [PATCH 09/45] docs(cbor): Updated `CborDecoder` docs --- cbor/decode.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index 891f984e7045..d7ebad414abb 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -12,13 +12,27 @@ import { CborTag, type CborType } from "./encode.ts"; * * @example Usage * ```ts - * import { assertEquals } from "@std/assert"; + * import { assert, assertEquals } from "@std/assert"; * import { CborDecoder, CborEncoder } from "@std/cbor"; * - * const encoder = new CborEncoder(); * const decoder = new CborDecoder(); + * const encoder = new CborEncoder(); + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; * - * assertEquals(decoder.decode(encoder.encode('Hello World')), 'Hello World') + * const encodedMessage = encoder.encode(rawMessage); + * const decodedMessage = decoder.decode(encodedMessage); + * + * assert(decodedMessage instanceof Array); + * assertEquals(decodedMessage, rawMessage); * ``` */ export class CborDecoder { @@ -31,8 +45,28 @@ export class CborDecoder { /** * Decodes a {@link Uint8Array} into a {@link CborType}. * @example Usage - * ```ts no-eval + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder } from "@std/cbor"; + * + * const decoder = new CborDecoder(); + * const encoder = new CborEncoder(); * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encoder.encode(rawMessage); + * const decodedMessage = decoder.decode(encodedMessage); + * + * assert(decodedMessage instanceof Array); + * assertEquals(decodedMessage, rawMessage); * ``` * * @param x Value to decode from CBOR format. @@ -50,8 +84,28 @@ export class CborDecoder { /** * Decodes an array of {@link CborType} from a {@link Uint8Array}. * @example Usage - * ```ts no-eval + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborDecoder, CborEncoder } from "@std/cbor"; + * + * const decoder = new CborDecoder(); + * const encoder = new CborEncoder(); + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encoder.encodeSequence(rawMessage); + * const decodedMessage = decoder.decodeSequence(encodedMessage); * + * assert(decodedMessage instanceof Array); + * assertEquals(decodedMessage, rawMessage); * ``` * * @param data Encoded data to be decoded from CBOR format. From b544f7f0cd59076214ec5bf4b63c1b03e13848b4 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:56:12 +1000 Subject: [PATCH 10/45] github(cbor): Update GitHub Action to recognise CBOR --- .github/workflows/title.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/title.yml b/.github/workflows/title.yml index f77a47888e8e..3767237d96fa 100644 --- a/.github/workflows/title.yml +++ b/.github/workflows/title.yml @@ -40,6 +40,7 @@ jobs: async(/unstable)? bytes(/unstable)? cache(/unstable)? + cbor(/unstable)? cli(/unstable)? collections(/unstable)? crypto(/unstable)? From 6c59514e61091b8ef49b60f09e19b463f70a696a Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:58:06 +1000 Subject: [PATCH 11/45] github(cbor): Update GitHub Action to recognise cbor/ --- .github/labeler.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index 8b71b1662f7b..8e77c666a181 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -10,6 +10,9 @@ async: bytes: - changed-files: - any-glob-to-any-file: bytes/** +cbor: + - changed-files: + - any-glob-to-any-file: cbor/** cli: - changed-files: - any-glob-to-any-file: cli/** From 8c597e7ec5c8ac86fab85fa9ee32946b6974ef8c Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Sun, 8 Sep 2024 20:53:55 +1000 Subject: [PATCH 12/45] refactor(cbor): `CborEncoder` to `encodeCbor` --- cbor/decode.ts | 18 +-- cbor/decode_test.ts | 71 ++++----- cbor/encode.ts | 366 +++++++++++++++++--------------------------- cbor/encode_test.ts | 128 +++++++--------- cbor/mod.ts | 5 +- 5 files changed, 241 insertions(+), 347 deletions(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index d7ebad414abb..fc25121c0727 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -13,10 +13,9 @@ import { CborTag, type CborType } from "./encode.ts"; * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder } from "@std/cbor"; + * import { CborDecoder, encodeCbor } from "@std/cbor"; * * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); * * const rawMessage = [ * "Hello World", @@ -28,7 +27,7 @@ import { CborTag, type CborType } from "./encode.ts"; * Uint8Array.from([0, 1, 2, 3]), * ]; * - * const encodedMessage = encoder.encode(rawMessage); + * const encodedMessage = encodeCbor(rawMessage); * const decodedMessage = decoder.decode(encodedMessage); * * assert(decodedMessage instanceof Array); @@ -47,10 +46,9 @@ export class CborDecoder { * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder } from "@std/cbor"; + * import { CborDecoder, encodeCbor } from "@std/cbor"; * * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); * * const rawMessage = [ * "Hello World", @@ -62,7 +60,7 @@ export class CborDecoder { * Uint8Array.from([0, 1, 2, 3]), * ]; * - * const encodedMessage = encoder.encode(rawMessage); + * const encodedMessage = encodeCbor(rawMessage); * const decodedMessage = decoder.decode(encodedMessage); * * assert(decodedMessage instanceof Array); @@ -86,10 +84,10 @@ export class CborDecoder { * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder } from "@std/cbor"; + * import { concat } from "@std/bytes"; + * import { CborDecoder, encodeCbor } from "@std/cbor"; * * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); * * const rawMessage = [ * "Hello World", @@ -101,7 +99,7 @@ export class CborDecoder { * Uint8Array.from([0, 1, 2, 3]), * ]; * - * const encodedMessage = encoder.encodeSequence(rawMessage); + * const encodedMessage = concat(rawMessage.map(x => encodeCbor(x))) * const decodedMessage = decoder.decodeSequence(encodedMessage); * * assert(decodedMessage instanceof Array); @@ -339,7 +337,7 @@ export class CborDecoder { return object; } - #decodeSix(aI: number): Date | CborTag { + #decodeSix(aI: number): Date | CborTag { const tagNumber = this.#decodeZero(aI) as number; const tagContent = this.#decode(); switch (tagNumber) { diff --git a/cbor/decode_test.ts b/cbor/decode_test.ts index 4ac7f4816e27..3892a9ff9448 100644 --- a/cbor/decode_test.ts +++ b/cbor/decode_test.ts @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertEquals } from "@std/assert"; -import { CborDecoder, CborEncoder, CborTag } from "./mod.ts"; +import { CborDecoder, CborTag, encodeCbor } from "./mod.ts"; function random(start: number, end: number): number { return Math.floor(Math.random() * (end - start) + start); @@ -9,147 +9,142 @@ function random(start: number, end: number): number { Deno.test("CborDecoder() decoding undefined", () => { assertEquals( - new CborDecoder().decode(new CborEncoder().encode(undefined)), + new CborDecoder().decode(encodeCbor(undefined)), undefined, ); }); Deno.test("CborDecoder() decoding null", () => { - assertEquals(new CborDecoder().decode(new CborEncoder().encode(null)), null); + assertEquals(new CborDecoder().decode(encodeCbor(null)), null); }); Deno.test("CborDecoder() decoding true", () => { - assertEquals(new CborDecoder().decode(new CborEncoder().encode(true)), true); + assertEquals(new CborDecoder().decode(encodeCbor(true)), true); }); Deno.test("CborDecoder() decoding false", () => { assertEquals( - new CborDecoder().decode(new CborEncoder().encode(false)), + new CborDecoder().decode(encodeCbor(false)), false, ); }); Deno.test("CborDecoder() decoding integers", () => { - const encoder = new CborEncoder(); const decoder = new CborDecoder(); let num = random(0, 24); - assertEquals(decoder.decode(encoder.encode(num)), num); - assertEquals(decoder.decode(encoder.encode(BigInt(num))), num); + assertEquals(decoder.decode(encodeCbor(num)), num); + assertEquals(decoder.decode(encodeCbor(BigInt(num))), num); num = random(24, 2 ** 8); - assertEquals(decoder.decode(encoder.encode(num)), num); - assertEquals(decoder.decode(encoder.encode(BigInt(num))), num); + assertEquals(decoder.decode(encodeCbor(num)), num); + assertEquals(decoder.decode(encodeCbor(BigInt(num))), num); num = random(2 ** 8, 2 ** 16); - assertEquals(decoder.decode(encoder.encode(num)), num); - assertEquals(decoder.decode(encoder.encode(BigInt(num))), num); + assertEquals(decoder.decode(encodeCbor(num)), num); + assertEquals(decoder.decode(encodeCbor(BigInt(num))), num); num = random(2 ** 16, 2 ** 32); - assertEquals(decoder.decode(encoder.encode(num)), num); - assertEquals(decoder.decode(encoder.encode(BigInt(num))), num); + assertEquals(decoder.decode(encodeCbor(num)), num); + assertEquals(decoder.decode(encodeCbor(BigInt(num))), num); num = random(2 ** 32, 2 ** 64); - assertEquals(decoder.decode(encoder.encode(num)), BigInt(num)); - assertEquals(decoder.decode(encoder.encode(BigInt(num))), BigInt(num)); + assertEquals(decoder.decode(encodeCbor(num)), BigInt(num)); + assertEquals(decoder.decode(encodeCbor(BigInt(num))), BigInt(num)); }); Deno.test("CborDecoder() decoding strings", () => { - const encoder = new CborEncoder(); const decoder = new CborDecoder(); const textDecoder = new TextDecoder(); let text = textDecoder.decode( new Uint8Array(random(0, 24)).map((_) => random(97, 123)), ); // Range: `a` - `z` - assertEquals(decoder.decode(encoder.encode(text)), text); + assertEquals(decoder.decode(encodeCbor(text)), text); text = textDecoder.decode( new Uint8Array(random(24, 2 ** 8)).map((_) => random(97, 123)), ); // Range: `a` - `z` - assertEquals(decoder.decode(encoder.encode(text)), text); + assertEquals(decoder.decode(encodeCbor(text)), text); text = textDecoder.decode( new Uint8Array(random(2 ** 8, 2 ** 16)).map((_) => random(97, 123)), ); // Range: `a` - `z` - assertEquals(decoder.decode(encoder.encode(text)), text); + assertEquals(decoder.decode(encodeCbor(text)), text); text = textDecoder.decode( new Uint8Array(random(2 ** 16, 2 ** 17)).map((_) => random(97, 123)), ); // Range: `a` - `z` - assertEquals(decoder.decode(encoder.encode(text)), text); + assertEquals(decoder.decode(encodeCbor(text)), text); // Can't test the next bracket up due to JavaScript limitations. }); Deno.test("CborDecoder() decoding Uint8Arrays", () => { - const encoder = new CborEncoder(); const decoder = new CborDecoder(); let bytes = new Uint8Array(random(0, 24)).map((_) => random(0, 256)); - assertEquals(decoder.decode(encoder.encode(bytes)), bytes); + assertEquals(decoder.decode(encodeCbor(bytes)), bytes); bytes = new Uint8Array(random(24, 2 ** 8)).map((_) => random(0, 256)); - assertEquals(decoder.decode(encoder.encode(bytes)), bytes); + assertEquals(decoder.decode(encodeCbor(bytes)), bytes); bytes = new Uint8Array(random(2 ** 8, 2 ** 16)).map((_) => random(0, 256)); - assertEquals(decoder.decode(encoder.encode(bytes)), bytes); + assertEquals(decoder.decode(encodeCbor(bytes)), bytes); bytes = new Uint8Array(random(2 ** 16, 2 ** 17)).map((_) => random(0, 256)); - assertEquals(decoder.decode(encoder.encode(bytes)), bytes); + assertEquals(decoder.decode(encodeCbor(bytes)), bytes); // Can't test the next bracket up due to JavaScript limitations. }); Deno.test("CborDecoder() decoding Dates", () => { const date = new Date(); - assertEquals(new CborDecoder().decode(new CborEncoder().encode(date)), date); + assertEquals(new CborDecoder().decode(encodeCbor(date)), date); }); Deno.test("CborDecoder() decoding arrays", () => { - const encoder = new CborEncoder(); const decoder = new CborDecoder(); let array = new Array(random(0, 24)).fill(0).map((_) => random(0, 2 ** 32)); - assertEquals(decoder.decode(encoder.encode(array)), array); + assertEquals(decoder.decode(encodeCbor(array)), array); array = new Array(random(24, 2 ** 8)).fill(0).map((_) => random(0, 2 ** 32)); - assertEquals(decoder.decode(encoder.encode(array)), array); + assertEquals(decoder.decode(encodeCbor(array)), array); array = new Array(random(2 ** 8, 2 ** 16)).fill(0).map((_) => random(0, 2 ** 32) ); - assertEquals(decoder.decode(encoder.encode(array)), array); + assertEquals(decoder.decode(encodeCbor(array)), array); array = new Array(random(2 ** 16, 2 ** 17)).fill(0).map((_) => random(0, 2 ** 32) ); - assertEquals(decoder.decode(encoder.encode(array)), array); + assertEquals(decoder.decode(encodeCbor(array)), array); // Can't test the next bracket up due to JavaScript limitations. }); Deno.test("CborDecoder() decoding objects", () => { - const encoder = new CborEncoder(); const decoder = new CborDecoder(); let pairs = random(0, 24); let object = Object.fromEntries( new Array(pairs).fill(0).map((_, i) => [i, i]), ); - assertEquals(decoder.decode(encoder.encode(object)), object); + assertEquals(decoder.decode(encodeCbor(object)), object); pairs = random(24, 2 ** 8); object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); - assertEquals(decoder.decode(encoder.encode(object)), object); + assertEquals(decoder.decode(encodeCbor(object)), object); pairs = random(2 ** 8, 2 ** 16); object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); - assertEquals(decoder.decode(encoder.encode(object)), object); + assertEquals(decoder.decode(encodeCbor(object)), object); pairs = random(2 ** 16, 2 ** 17); object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); - assertEquals(decoder.decode(encoder.encode(object)), object); + assertEquals(decoder.decode(encodeCbor(object)), object); // Can't test the next bracket up due to JavaScript limitations. }); @@ -160,7 +155,7 @@ Deno.test("CborDecoder() decoding CborTag()", () => { new Uint8Array(random(0, 24)).map((_) => random(0, 256)), ); assertEquals( - new CborDecoder().decode(new CborEncoder().encode(tag)), + new CborDecoder().decode(encodeCbor(tag)), tag, ); }); diff --git a/cbor/encode.ts b/cbor/encode.ts index 2bd26b43e386..994177b41247 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -21,7 +21,7 @@ export type CborPrimitiveType = * This type specifies the values that the implementation can encode/decode * into/from. */ -export type CborType = CborPrimitiveType | CborTag | CborType[] | { +export type CborType = CborPrimitiveType | CborTag | CborType[] | { [k: string]: CborType; }; @@ -35,15 +35,14 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder, CborTag } from "@std/cbor"; + * import { CborDecoder, CborTag, encodeCbor } from "@std/cbor"; * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; * * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); * * const rawMessage = new TextEncoder().encode("Hello World"); * - * const encodedMessage = encoder.encode( + * const encodedMessage = encodeCbor( * new CborTag( * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". * encodeBase64Url(rawMessage), @@ -54,23 +53,24 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * assert(typeof decodedMessage.tagContent === "string"); * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); * ``` + * + * @typeParam T extends {@link CborType} */ -export class CborTag { +export class CborTag { /** * The number indicating how the tagContent should be interpreted based off * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder, CborTag } from "@std/cbor"; + * import { CborDecoder, CborTag, encodeCbor } from "@std/cbor"; * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; * * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); * * const rawMessage = new TextEncoder().encode("Hello World"); * - * const encodedMessage = encoder.encode( + * const encodedMessage = encodeCbor( * new CborTag( * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". * encodeBase64Url(rawMessage), @@ -90,15 +90,14 @@ export class CborTag { * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder, CborTag } from "@std/cbor"; + * import { CborDecoder, CborTag, encodeCbor } from "@std/cbor"; * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; * * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); * * const rawMessage = new TextEncoder().encode("Hello World"); * - * const encodedMessage = encoder.encode( + * const encodedMessage = encodeCbor( * new CborTag( * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". * encodeBase64Url(rawMessage), @@ -110,30 +109,29 @@ export class CborTag { * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); * ``` */ - tagContent: CborType; + tagContent: T; /** * Constructs a new instance. * @param tagNumber The value to tag the {@link CborType} with. - * @param tagContent The {@link CborType} formatted to the correct semantics. + * @param tagContent The {@link CborType} or {@link CborInputStream} formatted to the correct semantics. */ - constructor(tagNumber: number | bigint, tagContent: CborType) { + constructor(tagNumber: number | bigint, tagContent: T) { this.tagNumber = tagNumber; this.tagContent = tagContent; } } /** - * A class to encode JavaScript values into the CBOR format based off the + * A function to encode JavaScript values into the CBOR format based off the * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * spec. * * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder } from "@std/cbor"; + * import { CborDecoder, encodeCbor } from "@std/cbor"; * * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); * * const rawMessage = [ * "Hello World", @@ -145,240 +143,156 @@ export class CborTag { * Uint8Array.from([0, 1, 2, 3]), * ]; * - * const encodedMessage = encoder.encode(rawMessage); + * const encodedMessage = encodeCbor(rawMessage); * const decodedMessage = decoder.decode(encodedMessage); * * assert(decodedMessage instanceof Array); * assertEquals(decodedMessage, rawMessage); * ``` + * + * @param value Value to encode to CBOR format. + * @returns Encoded CBOR data. */ -export class CborEncoder { - /** - * Constructs a new instance. - */ - constructor() {} - - /** - * Encodes a {@link CborType} into a {@link Uint8Array}. - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder } from "@std/cbor"; - * - * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); - * - * const rawMessage = [ - * "Hello World", - * 35, - * 0.5, - * false, - * -1, - * null, - * Uint8Array.from([0, 1, 2, 3]), - * ]; - * - * const encodedMessage = encoder.encode(rawMessage); - * const decodedMessage = decoder.decode(encodedMessage); - * - * assert(decodedMessage instanceof Array); - * assertEquals(decodedMessage, rawMessage); - * ``` - * - * @param value Value to encode to CBOR format. - * @returns Encoded CBOR data. - */ - encode(value: CborType): Uint8Array { - switch (typeof value) { - case "number": - return this.#encodeNumber(value); - case "string": - return this.#encodeString(value); - case "boolean": - return value ? this.#TRUE : this.#FALSE; - case "undefined": - return this.#UNDEFINED; - case "bigint": - return this.#encodeBigInt(value); - } - if (value === null) return this.#NULL; - if (value instanceof Date) return this.#encodeDate(value); - if (value instanceof Uint8Array) return this.#encodeUint8Array(value); - if (value instanceof Array) return this.#encodeArray(value); - if (value instanceof CborTag) return this.#encodeTag(value); - return this.#encodeObject(value); - } - - /** - * Encodes an array of {@link CborType} into a {@link Uint8Array}. - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborEncoder } from "@std/cbor"; - * - * const decoder = new CborDecoder(); - * const encoder = new CborEncoder(); - * - * const rawMessage = [ - * "Hello World", - * 35, - * 0.5, - * false, - * -1, - * null, - * Uint8Array.from([0, 1, 2, 3]), - * ]; - * - * const encodedMessage = encoder.encodeSequence(rawMessage); - * const decodedMessage = decoder.decodeSequence(encodedMessage); - * - * assert(decodedMessage instanceof Array); - * assertEquals(decodedMessage, rawMessage); - * ``` - * - * @param array Values to encode into CBOR format. - * @returns Encoded CBOR data. - */ - encodeSequence(array: CborType[]): Uint8Array { - return concat(array.map((x) => this.encode(x))); - } - - get #UNDEFINED(): Uint8Array { - return new Uint8Array([0b111_10111]); - } - - get #TRUE(): Uint8Array { - return new Uint8Array([0b111_10101]); - } - - get #FALSE(): Uint8Array { - return new Uint8Array([0b111_10100]); - } - - get #NULL(): Uint8Array { - return new Uint8Array([0b111_10110]); +export function encodeCbor(value: CborType): Uint8Array { + switch (typeof value) { + case "number": + return encodeNumber(value); + case "string": + return encodeString(value); + case "boolean": + return new Uint8Array([value ? 0b111_10101 : 0b111_10100]); + case "undefined": + return new Uint8Array([0b111_10111]); + case "bigint": + return encodeBigInt(value); } + if (value === null) return new Uint8Array([0b111_10110]); + if (value instanceof Date) return encodeDate(value); + if (value instanceof Uint8Array) return encodeUint8Array(value); + if (value instanceof Array) return encodeArray(value); + if (value instanceof CborTag) return encodeTag(value); + return encodeObject(value); +} - #encodeNumber(x: number): Uint8Array { - if (x % 1 === 0) { - const majorType = x < 0 ? 0b001_00000 : 0b000_00000; - if (x < 0) x = -x - 1; +function encodeNumber(x: number): Uint8Array { + if (x % 1 === 0) { + const majorType = x < 0 ? 0b001_00000 : 0b000_00000; + if (x < 0) x = -x - 1; - if (x < 24) return new Uint8Array([majorType + x]); - if (x < 2 ** 8) return new Uint8Array([majorType + 24, x]); - if (x < 2 ** 16) { - return concat([new Uint8Array([majorType + 25]), numberToArray(2, x)]); - } - if (x < 2 ** 32) { - return concat([new Uint8Array([majorType + 26]), numberToArray(4, x)]); - } - if (x < 2 ** 64) { - return concat([new Uint8Array([majorType + 27]), numberToArray(8, x)]); - } - throw new RangeError( - `Cannot encode number: It (${x}) exceeds 2 ** 64 - 1`, - ); + if (x < 24) return new Uint8Array([majorType + x]); + if (x < 2 ** 8) return new Uint8Array([majorType + 24, x]); + if (x < 2 ** 16) { + return concat([new Uint8Array([majorType + 25]), numberToArray(2, x)]); } - return concat([new Uint8Array([0b111_11011]), numberToArray(8, x)]); + if (x < 2 ** 32) { + return concat([new Uint8Array([majorType + 26]), numberToArray(4, x)]); + } + if (x < 2 ** 64) { + return concat([new Uint8Array([majorType + 27]), numberToArray(8, x)]); + } + throw new RangeError( + `Cannot encode number: It (${x}) exceeds 2 ** 64 - 1`, + ); } + return concat([new Uint8Array([0b111_11011]), numberToArray(8, x)]); +} - #encodeBigInt(x: bigint): Uint8Array { - if ((x < 0n ? -x : x) < 2n ** 32n) return this.#encodeNumber(Number(x)); +function encodeBigInt(x: bigint): Uint8Array { + if ((x < 0n ? -x : x) < 2n ** 32n) return encodeNumber(Number(x)); - const head = new Uint8Array([x < 0n ? 0b010_11011 : 0b000_11011]); - if (x < 0n) x = -x - 1n; + const head = new Uint8Array([x < 0n ? 0b010_11011 : 0b000_11011]); + if (x < 0n) x = -x - 1n; - if (x < 2n ** 64n) return concat([head, numberToArray(8, x)]); - throw new RangeError(`Cannot encode bigint: It (${x}) exceeds 2 ** 64 - 1`); - } + if (x < 2n ** 64n) return concat([head, numberToArray(8, x)]); + throw new RangeError(`Cannot encode bigint: It (${x}) exceeds 2 ** 64 - 1`); +} - #encodeString(x: string): Uint8Array { - const array = this.#encodeUint8Array(new TextEncoder().encode(x)); - array[0]! += 1 << 5; - return array; +function encodeUint8Array(x: Uint8Array): Uint8Array { + if (x.length < 24) { + return concat([new Uint8Array([0b010_00000 + x.length]), x]); } - - #encodeUint8Array(x: Uint8Array): Uint8Array { - if (x.length < 24) { - return concat([new Uint8Array([0b010_00000 + x.length]), x]); - } - if (x.length < 2 ** 8) { - return concat([new Uint8Array([0b010_11000, x.length]), x]); - } - if (x.length < 2 ** 16) { - return concat([ - new Uint8Array([0b010_11001]), - numberToArray(2, x.length), - x, - ]); - } - if (x.length < 2 ** 32) { - return concat([ - new Uint8Array([0b010_11010]), - numberToArray(4, x.length), - x, - ]); - } - // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. + if (x.length < 2 ** 8) { + return concat([new Uint8Array([0b010_11000, x.length]), x]); + } + if (x.length < 2 ** 16) { return concat([ - new Uint8Array([0b010_11011]), - numberToArray(8, x.length), + new Uint8Array([0b010_11001]), + numberToArray(2, x.length), x, ]); } - - #encodeDate(x: Date): Uint8Array { + if (x.length < 2 ** 32) { return concat([ - new Uint8Array([0b110_00001]), - this.#encodeNumber(x.getTime() / 1000), + new Uint8Array([0b010_11010]), + numberToArray(4, x.length), + x, ]); } + // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. + return concat([ + new Uint8Array([0b010_11011]), + numberToArray(8, x.length), + x, + ]); +} - #encodeArray(x: CborType[]): Uint8Array { - let head: number[]; - if (x.length < 24) head = [0b100_00000 + x.length]; - else if (x.length < 2 ** 8) head = [0b100_11000, x.length]; - else if (x.length < 2 ** 16) { - head = [0b100_11001, ...numberToArray(2, x.length)]; - } else if (x.length < 2 ** 32) { - head = [0b100_11010, ...numberToArray(4, x.length)]; - } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support an `Array` being that large. - else head = [0b100_11011, ...numberToArray(8, x.length)]; - return concat([Uint8Array.from(head), ...x.map((x) => this.encode(x))]); - } +function encodeString(x: string): Uint8Array { + const array = encodeUint8Array(new TextEncoder().encode(x)); + array[0]! += 1 << 5; + return array; +} - #encodeObject(x: { [k: string]: CborType }): Uint8Array { - const len = Object.keys(x).length; - let head: number[]; - if (len < 24) head = [0b101_00000 + len]; - else if (len < 2 ** 8) head = [0b101_11000, len]; - else if (len < 2 ** 16) head = [0b101_11001, ...numberToArray(2, len)]; - else if (len < 2 ** 32) head = [0b101_11010, ...numberToArray(4, len)]; - // Can safely assume `len < 2 ** 64` as JavaScript doesn't support an `Object` being that Large. - else head = [0b101_11011, ...numberToArray(8, len)]; - return concat([ - Uint8Array.from(head), - ...Object.entries(x).map(( - [k, v], - ) => [this.#encodeString(k), this.encode(v)]).flat(), - ]); - } +function encodeDate(x: Date): Uint8Array { + return concat([ + new Uint8Array([0b110_00001]), + encodeNumber(x.getTime() / 1000), + ]); +} - #encodeTag(x: CborTag): Uint8Array { - let head: number[]; - if (x.tagNumber < 24) head = [0b110_00000 + Number(x.tagNumber)]; - else if (x.tagNumber < 2 ** 8) head = [0b110_11000, Number(x.tagNumber)]; - else if (x.tagNumber < 2 ** 16) { - head = [0b110_11001, ...numberToArray(2, x.tagNumber)]; - } else if (x.tagNumber < 2 ** 32) { - head = [0b110_11010, ...numberToArray(4, x.tagNumber)]; - } else if (x.tagNumber < 2 ** 64) { - head = [0b110_11011, ...numberToArray(8, x.tagNumber)]; - } else { - throw new RangeError( - `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, - ); - } - return concat([Uint8Array.from(head), this.encode(x.tagContent)]); +function encodeArray(x: CborType[]): Uint8Array { + let head: number[]; + if (x.length < 24) head = [0b100_00000 + x.length]; + else if (x.length < 2 ** 8) head = [0b100_11000, x.length]; + else if (x.length < 2 ** 16) { + head = [0b100_11001, ...numberToArray(2, x.length)]; + } else if (x.length < 2 ** 32) { + head = [0b100_11010, ...numberToArray(4, x.length)]; + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support an `Array` being that large. + else head = [0b100_11011, ...numberToArray(8, x.length)]; + return concat([Uint8Array.from(head), ...x.map((x) => encodeCbor(x))]); +} + +function encodeObject(x: { [k: string]: CborType }): Uint8Array { + const len = Object.keys(x).length; + let head: number[]; + if (len < 24) head = [0b101_00000 + len]; + else if (len < 2 ** 8) head = [0b101_11000, len]; + else if (len < 2 ** 16) head = [0b101_11001, ...numberToArray(2, len)]; + else if (len < 2 ** 32) head = [0b101_11010, ...numberToArray(4, len)]; + // Can safely assume `len < 2 ** 64` as JavaScript doesn't support an `Object` being that Large. + else head = [0b101_11011, ...numberToArray(8, len)]; + return concat([ + Uint8Array.from(head), + ...Object.entries(x).map(( + [k, v], + ) => [encodeString(k), encodeCbor(v)]).flat(), + ]); +} + +function encodeTag(x: CborTag) { + let head: number[]; + if (x.tagNumber < 24) head = [0b110_00000 + Number(x.tagNumber)]; + else if (x.tagNumber < 2 ** 8) head = [0b110_11000, Number(x.tagNumber)]; + else if (x.tagNumber < 2 ** 16) { + head = [0b110_11001, ...numberToArray(2, x.tagNumber)]; + } else if (x.tagNumber < 2 ** 32) { + head = [0b110_11010, ...numberToArray(4, x.tagNumber)]; + } else if (x.tagNumber < 2 ** 64) { + head = [0b110_11011, ...numberToArray(8, x.tagNumber)]; + } else { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, + ); } + return concat([Uint8Array.from(head), encodeCbor(x.tagContent)]); } diff --git a/cbor/encode_test.ts b/cbor/encode_test.ts index ef6221bce1c3..8041eb776c68 100644 --- a/cbor/encode_test.ts +++ b/cbor/encode_test.ts @@ -2,49 +2,47 @@ import { assertEquals } from "@std/assert"; import { concat } from "@std/bytes"; -import { CborEncoder, CborTag } from "./mod.ts"; +import { CborTag, encodeCbor } from "./mod.ts"; function random(start: number, end: number): number { return Math.floor(Math.random() * (end - start) + start); } -Deno.test("CborEncoder() encoding undefined", () => { +Deno.test("encodeCbor() encoding undefined", () => { assertEquals( - new CborEncoder().encode(undefined), + encodeCbor(undefined), new Uint8Array([0b111_10111]), ); }); -Deno.test("CborEncoder() encoding null", () => { - assertEquals(new CborEncoder().encode(null), new Uint8Array([0b111_10110])); +Deno.test("encodeCbor() encoding null", () => { + assertEquals(encodeCbor(null), new Uint8Array([0b111_10110])); }); -Deno.test("CborEncoder() encoding true", () => { - assertEquals(new CborEncoder().encode(true), new Uint8Array([0b111_10101])); +Deno.test("encodeCbor() encoding true", () => { + assertEquals(encodeCbor(true), new Uint8Array([0b111_10101])); }); -Deno.test("CborEncoder() encoding false", () => { - assertEquals(new CborEncoder().encode(false), new Uint8Array([0b111_10100])); +Deno.test("encodeCbor() encoding false", () => { + assertEquals(encodeCbor(false), new Uint8Array([0b111_10100])); }); -Deno.test("CborEncoder() encoding numbers as Uint", () => { - const encoder = new CborEncoder(); - +Deno.test("encodeCbor() encoding numbers as Uint", () => { let num: number | bigint = random(0, 24); - assertEquals(encoder.encode(num), new Uint8Array([0b000_00000 + num])); + assertEquals(encodeCbor(num), new Uint8Array([0b000_00000 + num])); num = random(24, 2 ** 8); - assertEquals(encoder.encode(num), new Uint8Array([0b000_11000, num])); + assertEquals(encodeCbor(num), new Uint8Array([0b000_11000, num])); num = random(2 ** 8, 2 ** 16); assertEquals( - encoder.encode(num), + encodeCbor(num), new Uint8Array([0b000_11001, num >> 8 & 0xFF, num & 0xFF]), ); num = random(2 ** 16, 2 ** 32); assertEquals( - encoder.encode(num), + encodeCbor(num), new Uint8Array([ 0b000_11010, num >> 24 & 0xFF, @@ -57,7 +55,7 @@ Deno.test("CborEncoder() encoding numbers as Uint", () => { // JavaScript fails at correctly bit-wising this many bits as a number. num = BigInt(Number.MAX_SAFE_INTEGER); assertEquals( - encoder.encode(Number.MAX_SAFE_INTEGER), + encodeCbor(Number.MAX_SAFE_INTEGER), new Uint8Array( [ 0b000_11011, @@ -74,39 +72,37 @@ Deno.test("CborEncoder() encoding numbers as Uint", () => { ); }); -Deno.test("CborEncoder() encoding numbers as Int", () => { +Deno.test("encodeCbor() encoding numbers as Int", () => { const num = -random(1, 24); // -0 === 0 assertEquals( - new CborEncoder().encode(num), + encodeCbor(num), new Uint8Array([0b001_00000 + (-num - 1)]), ); }); -Deno.test("CborEncoder() encoding numbers as Float", () => { +Deno.test("encodeCbor() encoding numbers as Float", () => { const num = Math.random() * 2 ** 32; const view = new DataView(new ArrayBuffer(8)); view.setFloat64(0, num); assertEquals( - new CborEncoder().encode(num), + encodeCbor(num), concat([new Uint8Array([0b111_11011]), new Uint8Array(view.buffer)]), ); }); -Deno.test("CborEncoder() encoding bigints as Uint", () => { - const encoder = new CborEncoder(); - +Deno.test("encodeCbor() encoding bigints as Uint", () => { let num = BigInt(random(0, 24)); assertEquals( - encoder.encode(num), + encodeCbor(num), new Uint8Array([0b000_00000 + Number(num)]), ); num = BigInt(random(24, 2 ** 8)); - assertEquals(encoder.encode(num), new Uint8Array([0b000_11000, Number(num)])); + assertEquals(encodeCbor(num), new Uint8Array([0b000_11000, Number(num)])); num = BigInt(random(2 ** 8, 2 ** 16)); assertEquals( - encoder.encode(num), + encodeCbor(num), new Uint8Array([ 0b000_11001, Number(num >> 8n & 0xFFn), @@ -116,7 +112,7 @@ Deno.test("CborEncoder() encoding bigints as Uint", () => { num = BigInt(random(2 ** 16, 2 ** 32)); assertEquals( - encoder.encode(num), + encodeCbor(num), new Uint8Array([ 0b000_11010, Number(num >> 24n & 0xFFn), @@ -128,7 +124,7 @@ Deno.test("CborEncoder() encoding bigints as Uint", () => { num = BigInt(random(2 ** 32, 2 ** 64)); assertEquals( - encoder.encode(num), + encodeCbor(num), new Uint8Array([ 0b000_11011, Number(num >> 56n & 0xFFn), @@ -143,33 +139,32 @@ Deno.test("CborEncoder() encoding bigints as Uint", () => { ); }); -Deno.test("CborEncoder() encoding bigints as Int", () => { +Deno.test("encodeCbor() encoding bigints as Int", () => { const num = -BigInt(random(1, 24)); // -0 === 0 assertEquals( - new CborEncoder().encode(num), + encodeCbor(num), new Uint8Array([0b001_00000 + Number(-num - 1n)]), ); }); -Deno.test("CborEncoder() encoding strings", () => { - const encoder = new CborEncoder(); +Deno.test("encodeCbor() encoding strings", () => { const decoder = new TextDecoder(); let bytes = new Uint8Array(random(0, 24)).map((_) => random(97, 123)); // Range: `a` - `z` assertEquals( - encoder.encode(decoder.decode(bytes)), + encodeCbor(decoder.decode(bytes)), new Uint8Array([0b011_00000 + bytes.length, ...bytes]), ); bytes = new Uint8Array(random(24, 2 ** 8)).map((_) => random(97, 123)); // Range: `a` - `z` assertEquals( - encoder.encode(decoder.decode(bytes)), + encodeCbor(decoder.decode(bytes)), new Uint8Array([0b011_11000, bytes.length, ...bytes]), ); bytes = new Uint8Array(random(2 ** 8, 2 ** 16)).map((_) => random(97, 123)); // Range: `a` - `z` assertEquals( - encoder.encode(decoder.decode(bytes)), + encodeCbor(decoder.decode(bytes)), new Uint8Array([ 0b011_11001, bytes.length >> 8 & 0xFF, @@ -180,7 +175,7 @@ Deno.test("CborEncoder() encoding strings", () => { bytes = new Uint8Array(random(2 ** 16, 2 ** 17)).map((_) => random(97, 123)); // Range: `a` - `z` assertEquals( - encoder.encode(decoder.decode(bytes)), + encodeCbor(decoder.decode(bytes)), new Uint8Array([ 0b011_11010, bytes.length >> 24 & 0xFF, @@ -194,24 +189,22 @@ Deno.test("CborEncoder() encoding strings", () => { // Can't test the next bracket up due to JavaScript limitations. }); -Deno.test("CborEncoder() encoding Uint8Arrays", () => { - const encoder = new CborEncoder(); - +Deno.test("encodeCbor() encoding Uint8Arrays", () => { let bytes = new Uint8Array(random(0, 24)); assertEquals( - encoder.encode(bytes), + encodeCbor(bytes), new Uint8Array([0b010_00000 + bytes.length, ...bytes]), ); bytes = new Uint8Array(random(24, 2 ** 8)); assertEquals( - encoder.encode(bytes), + encodeCbor(bytes), new Uint8Array([0b010_11000, bytes.length, ...bytes]), ); bytes = new Uint8Array(random(2 ** 8, 2 ** 16)); assertEquals( - encoder.encode(bytes), + encodeCbor(bytes), new Uint8Array([ 0b010_11001, bytes.length >> 8 & 0xFF, @@ -222,7 +215,7 @@ Deno.test("CborEncoder() encoding Uint8Arrays", () => { bytes = new Uint8Array(random(2 ** 16, 2 ** 17)); assertEquals( - encoder.encode(bytes), + encodeCbor(bytes), new Uint8Array([ 0b010_11010, bytes.length >> 24 & 0xFF, @@ -236,77 +229,72 @@ Deno.test("CborEncoder() encoding Uint8Arrays", () => { // Can't test the next bracket up due to JavaScript limitations. }); -Deno.test("CborEncoder() encoding Dates", () => { - const encoder = new CborEncoder(); +Deno.test("encodeCbor() encoding Dates", () => { const date = new Date(); assertEquals( - encoder.encode(date), - new Uint8Array([0b110_00001, ...encoder.encode(date.getTime() / 1000)]), + encodeCbor(date), + new Uint8Array([0b110_00001, ...encodeCbor(date.getTime() / 1000)]), ); }); -Deno.test("CborEncoder() encoding arrays", () => { - const encoder = new CborEncoder(); - +Deno.test("encodeCbor() encoding arrays", () => { let array = new Array(random(0, 24)).fill(0); assertEquals( - encoder.encode(array), + encodeCbor(array), new Uint8Array([ 0b100_00000 + array.length, - ...array.map((x) => [...encoder.encode(x)]).flat(), + ...array.map((x) => [...encodeCbor(x)]).flat(), ]), ); array = new Array(random(24, 2 ** 8)).fill(0); assertEquals( - encoder.encode(array), + encodeCbor(array), new Uint8Array([ 0b100_11000, array.length, - ...array.map((x) => [...encoder.encode(x)]).flat(), + ...array.map((x) => [...encodeCbor(x)]).flat(), ]), ); array = new Array(random(2 ** 8, 2 ** 16)).fill(0); assertEquals( - encoder.encode(array), + encodeCbor(array), new Uint8Array([ 0b100_11001, array.length >> 8 & 0xFF, array.length & 0xFF, - ...array.map((x) => [...encoder.encode(x)]).flat(), + ...array.map((x) => [...encodeCbor(x)]).flat(), ]), ); array = new Array(random(2 ** 16, 2 ** 17)).fill(0); assertEquals( - encoder.encode(array), + encodeCbor(array), new Uint8Array([ 0b100_11010, array.length >> 24 & 0xFF, array.length >> 16 & 0xFF, array.length >> 8 & 0xFF, array.length & 0xFF, - ...array.map((x) => [...encoder.encode(x)]).flat(), + ...array.map((x) => [...encodeCbor(x)]).flat(), ]), ); // Can't test the next bracket up due to JavaScript limitations. }); -Deno.test("CborEncoder() encoding objects", () => { - const encoder = new CborEncoder(); - +Deno.test("encodeCbor() encoding objects", () => { let pairs = random(0, 24); let entries: [string, number][] = new Array(pairs).fill(0).map(( _, i, ) => [i.toString(), i]); assertEquals( - encoder.encode(Object.fromEntries(entries)), + encodeCbor(Object.fromEntries(entries)), new Uint8Array([ 0b101_00000 + pairs, - ...entries.map(([k, v]) => [...encoder.encode(k), ...encoder.encode(v)]) + ...entries.map(([k, v]) => [...encodeCbor(k), ...encodeCbor(v)]) .flat(), ]), ); @@ -314,11 +302,11 @@ Deno.test("CborEncoder() encoding objects", () => { pairs = random(24, 2 ** 8); entries = new Array(pairs).fill(0).map((_, i) => [i.toString(), i]); assertEquals( - encoder.encode(Object.fromEntries(entries)), + encodeCbor(Object.fromEntries(entries)), new Uint8Array([ 0b101_11000, pairs, - ...entries.map(([k, v]) => [...encoder.encode(k), ...encoder.encode(v)]) + ...entries.map(([k, v]) => [...encodeCbor(k), ...encodeCbor(v)]) .flat(), ]), ); @@ -326,12 +314,12 @@ Deno.test("CborEncoder() encoding objects", () => { pairs = random(2 ** 8, 2 ** 16); entries = new Array(pairs).fill(0).map((_, i) => [i.toString(), i]); assertEquals( - encoder.encode(Object.fromEntries(entries)), + encodeCbor(Object.fromEntries(entries)), new Uint8Array([ 0b101_11001, pairs >> 8 & 0xFF, pairs & 0xFF, - ...entries.map(([k, v]) => [...encoder.encode(k), ...encoder.encode(v)]) + ...entries.map(([k, v]) => [...encodeCbor(k), ...encodeCbor(v)]) .flat(), ]), ); @@ -339,10 +327,10 @@ Deno.test("CborEncoder() encoding objects", () => { // Can't test the next two bracket up due to JavaScript limitations. }); -Deno.test("CborEncoder() encoding CborTag()", () => { +Deno.test("encodeCbor() encoding CborTag()", () => { const bytes = new Uint8Array(random(0, 24)).map((_) => random(0, 256)); assertEquals( - new CborEncoder().encode(new CborTag(2, bytes)), + encodeCbor(new CborTag(2, bytes)), new Uint8Array([0b110_00010, 0b010_00000 + bytes.length, ...bytes]), ); }); diff --git a/cbor/mod.ts b/cbor/mod.ts index 99de2d6c8e8a..32ed02474884 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -8,10 +8,9 @@ * spec. * * ```ts no-assert - * import { CborEncoder } from "@std/cbor"; + * import { encodeCbor } from "@std/cbor"; * - * const encoder = new CborEncoder(); - * console.log(encoder.encode(5)); + * console.log(encodeCbor(5)); * ``` * * @module From 29ef8ac2f3a81efead6c70d0c95625f8ec228106 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:43:29 +1000 Subject: [PATCH 13/45] feat(cbor): CborEncoderStreams --- cbor/_common.ts | 29 ++ cbor/deno.json | 3 +- cbor/encode.ts | 16 +- cbor/encode_stream.ts | 530 +++++++++++++++++++++++++++++++++++++ cbor/encode_stream_test.ts | 124 +++++++++ cbor/mod.ts | 1 + 6 files changed, 695 insertions(+), 8 deletions(-) create mode 100644 cbor/encode_stream.ts create mode 100644 cbor/encode_stream_test.ts diff --git a/cbor/_common.ts b/cbor/_common.ts index 49a3263188da..2b3e2c5642f4 100644 --- a/cbor/_common.ts +++ b/cbor/_common.ts @@ -41,3 +41,32 @@ export function arrayToNumber( return view.getFloat64(0); } } + +export function upgradeStreamFromGen( + gen: AsyncGenerator, +): ReadableStream { + return new ReadableStream({ + type: "bytes", + async pull(controller) { + const { done, value } = await gen.next(); + if (done) { + controller.byobRequest?.respond(0); + return controller.close(); + } + if (controller.byobRequest?.view) { + const buffer = new Uint8Array(controller.byobRequest.view.buffer); + const size = buffer.length; + if (value.length > size) { + buffer.set(value.slice(0, size)); + controller.byobRequest.respond(size); + controller.enqueue(value.slice(size)); + } else { + buffer.set(value); + controller.byobRequest.respond(value.length); + } + } else { + controller.enqueue(value); + } + }, + }); +} diff --git a/cbor/deno.json b/cbor/deno.json index cd67f15d11c4..65bda71620f3 100644 --- a/cbor/deno.json +++ b/cbor/deno.json @@ -4,6 +4,7 @@ "exports": { ".": "./mod.ts", "./decode": "./decode.ts", - "./encode": "./encode.ts" + "./encode": "./encode.ts", + "./encode-stream": "./encode_stream.ts" } } diff --git a/cbor/encode.ts b/cbor/encode.ts index 994177b41247..2f0218347f09 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -2,6 +2,7 @@ import { concat } from "@std/bytes"; import { numberToArray } from "./_common.ts"; +import type { CborInputStream } from "./encode_stream.ts"; /** * This type specifies the primitive types that the implementation can @@ -18,17 +19,18 @@ export type CborPrimitiveType = | Date; /** - * This type specifies the values that the implementation can encode/decode - * into/from. + * This type specifies the encodable and decodable values for {@link encodeCbor} + * and {@link CborDecoder}. */ export type CborType = CborPrimitiveType | CborTag | CborType[] | { [k: string]: CborType; }; /** - * A class that wraps {@link CborType} values, assuming the `tagContent` is the - * appropriate type and format for encoding. A list of the different types can - * be found [here](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). + * A class that wraps {@link CborType} and {@link CborInputStream} values, + * assuming the `tagContent` is the appropriate type and format for encoding. + * A list of the different types can be found + * [here](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). * * This class will be returned out of CborDecoder if it doesn't automatically * know how to handle the tag number. @@ -54,9 +56,9 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); * ``` * - * @typeParam T extends {@link CborType} + * @typeParam T extends {@link CborType} | {@link CborInputStream} */ -export class CborTag { +export class CborTag { /** * The number indicating how the tagContent should be interpreted based off * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). diff --git a/cbor/encode_stream.ts b/cbor/encode_stream.ts new file mode 100644 index 000000000000..8339542acee9 --- /dev/null +++ b/cbor/encode_stream.ts @@ -0,0 +1,530 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { numberToArray, upgradeStreamFromGen } from "./_common.ts"; +import { type CborPrimitiveType, CborTag, encodeCbor } from "./encode.ts"; + +/** + * This type specifies the encodable values for + * {@link CborSequenceEncoderStream} and {@link CborArrayEncoderStream}. + */ +export type CborInputStream = + | CborPrimitiveType + | CborTag + | CborInputStream[] + | { [k: string]: CborInputStream } + | CborByteEncoderStream + | CborTextEncoderStream + | CborArrayEncoderStream + | CborMapEncoderStream; + +/** + * This type specifies the structure of input for {@link CborMapEncoderStream}. + */ +export type CborMapInputStream = [string, CborInputStream]; + +/** + * The CborByteEncoderStream encodes a ReadableStream into a CBOR + * "indefinite byte string". + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborByteEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + Uint8Array, + Uint8Array + >(); + this.#readable = upgradeStreamFromGen(async function* () { + yield new Uint8Array([0b010_11111]); + for await (const x of readable) { + if (x.length < 24) yield new Uint8Array([0b010_00000 + x.length]); + else if (x.length < 2 ** 8) { + yield new Uint8Array([0b010_11000, x.length]); + } else if (x.length < 2 ** 16) { + yield new Uint8Array([0b010_11001, ...numberToArray(2, x.length)]); + } else if (x.length < 2 ** 32) { + yield new Uint8Array([0b010_11010, ...numberToArray(4, x.length)]); + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. + else yield new Uint8Array([0b010_11011, ...numberToArray(8, x.length)]); + yield x; + } + yield new Uint8Array([0b111_11111]); + }()); + this.#writable = writable; + } + + /** + * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param asyncIterable The iterable to convert to a {@link CborByteEncoderStream} instance. + * @returns a {@link CborByteEncoderStream} instance. + */ + static from( + asyncIterable: AsyncIterable | Iterable, + ): CborByteEncoderStream { + const encoder = new CborByteEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The ReadableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a ReadableStream. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The WritableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a WritableStream. + */ + get writable(): WritableStream { + return this.#writable; + } +} + +/** + * The CborTextEncoderStream encodes a ReadableStream into a CBOR + * "indefinite text string". + * + * @example Usage + * ```ts no-eval + * ``` + */ +export class CborTextEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream(); + this.#readable = upgradeStreamFromGen(async function* () { + yield new Uint8Array([0b011_11111]); + for await (const x of readable.pipeThrough(new TextEncoderStream())) { + if (x.length < 24) yield new Uint8Array([0b011_00000 + x.length]); + else if (x.length < 2 ** 8) { + yield new Uint8Array([0b011_11000, x.length]); + } else if (x.length < 2 ** 16) { + yield new Uint8Array([0b011_11001, ...numberToArray(2, x.length)]); + } else if (x.length < 2 ** 32) { + yield new Uint8Array([0b011_11010, ...numberToArray(4, x.length)]); + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. + else yield new Uint8Array([0b011_11011, ...numberToArray(8, x.length)]); + yield x; + } + yield new Uint8Array([0b111_11111]); + }()); + this.#writable = writable; + } + + /** + * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param asyncIterable The iterable to convert to a {@link CborTextEncoderStream} instance. + * @returns a {@link CborTextEncoderStream} instance. + */ + static from( + asyncIterable: AsyncIterable | Iterable, + ): CborTextEncoderStream { + const encoder = new CborTextEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The ReadableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a ReadableStream. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The WritableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a WritableStream. + */ + get writable(): WritableStream { + return this.#writable; + } +} + +/** + * The CborArrayEncoderStream encodes a ReadableStream into a CBOR + * "indefinite array". + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborArrayEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + CborInputStream, + CborInputStream + >(); + this.#readable = upgradeStreamFromGen(async function* () { + yield new Uint8Array([0b100_11111]); + for await ( + const x of readable.pipeThrough(new CborSequenceEncoderStream()) + ) { + yield x; + } + yield new Uint8Array([0b111_11111]); + }()); + this.#writable = writable; + } + + /** + * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param asyncIterable The iterable to convert to a {@link CborArrayEncoderStream} instance. + * @returns a {@link CborArrayEncoderStream} instance. + */ + static from( + asyncIterable: AsyncIterable | Iterable, + ): CborArrayEncoderStream { + const encoder = new CborArrayEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The ReadableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a ReadableStream. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The WritableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a WritableStream. + */ + get writable(): WritableStream { + return this.#writable; + } +} + +/** + * The CborByteEncoderStream encodes a ReadableStream into a CBOR + * "indefinite map". + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborMapEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + CborMapInputStream, + CborMapInputStream + >(); + this.#readable = upgradeStreamFromGen(async function* () { + yield new Uint8Array([0b101_11111]); + for await (const [k, v] of readable) { + yield encodeCbor(k); + for await (const x of CborSequenceEncoderStream.from([v]).readable) { + yield x; + } + } + yield new Uint8Array([0b111_11111]); + }()); + this.#writable = writable; + } + + /** + * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param asyncIterable The iterable to convert to a {@link CborMapEncoderStream} instance. + * @returns a {@link CborMapEncoderStream} instance. + */ + static from( + asyncIterable: + | AsyncIterable + | Iterable, + ): CborMapEncoderStream { + const encoder = new CborMapEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The ReadableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a ReadableStream. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The WritableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a WritableStream. + */ + get writable(): WritableStream { + return this.#writable; + } +} + +/** + * The CborSequenceEncoderStream encodes a ReadableStream into + * a sequence of CBOR encoded values. + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborSequenceEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + CborInputStream, + CborInputStream + >(); + this.#readable = upgradeStreamFromGen(this.#encodeFromReadable(readable)); + this.#writable = writable; + } + + async *#encodeFromReadable( + readable: ReadableStream, + ): AsyncGenerator { + for await (const x of readable) { + for await (const y of this.#encode(x)) { + yield y; + } + } + } + + async *#encode( + x: CborInputStream, + ): AsyncGenerator { + if ( + x instanceof CborByteEncoderStream || + x instanceof CborTextEncoderStream || + x instanceof CborArrayEncoderStream || + x instanceof CborMapEncoderStream + ) { + for await (const y of x.readable) { + yield y; + } + } else if (x instanceof Array) { + for await (const y of this.#encodeArray(x)) { + yield y; + } + } else if (x instanceof CborTag) { + for await (const y of this.#encodeTag(x)) { + yield y; + } + } else if (typeof x === "object" && x !== null) { + if (x instanceof Date || x instanceof Uint8Array) yield encodeCbor(x); + else { + for await (const y of this.#encodeObject(x)) { + yield y; + } + } + } else yield encodeCbor(x); + } + + async *#encodeArray(x: CborInputStream[]): AsyncGenerator { + if (x.length < 24) yield new Uint8Array([0b100_00000 + x.length]); + else if (x.length < 2 ** 8) yield new Uint8Array([0b100_11000, x.length]); + else if (x.length < 2 ** 16) { + yield new Uint8Array([0b100_11001, ...numberToArray(2, x.length)]); + } else if (x.length < 2 ** 32) { + yield new Uint8Array([0b100_11010, ...numberToArray(4, x.length)]); + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support an `Array` being that large. + else yield new Uint8Array([0b100_11011, ...numberToArray(8, x.length)]); + for (const y of x) { + for await (const z of this.#encode(y)) { + yield z; + } + } + } + + async *#encodeObject( + x: { [k: string]: CborInputStream }, + ): AsyncGenerator { + const len = Object.keys(x).length; + if (len < 24) yield new Uint8Array([0b101_00000 + len]); + else if (len < 2 ** 8) yield new Uint8Array([0b101_11000, len]); + else if (len < 2 ** 16) { + yield new Uint8Array([0b101_11001, ...numberToArray(2, len)]); + } else if (len < 2 ** 32) { + yield new Uint8Array([0b101_11010, ...numberToArray(4, len)]); + } // Can safely assume `len < 2 ** 64` as JavaScript doesn't support an `Object` being that Large. + else yield new Uint8Array([0b101_11011, ...numberToArray(8, len)]); + for (const [k, v] of Object.entries(x)) { + yield encodeCbor(k); + for await (const y of this.#encode(v)) { + yield y; + } + } + } + + async *#encodeTag(x: CborTag): AsyncGenerator { + if (x.tagNumber < 24) { + yield new Uint8Array([0b110_00000 + Number(x.tagNumber)]); + } else if (x.tagNumber < 2 ** 8) { + yield new Uint8Array([0b110_11000, Number(x.tagNumber)]); + } else if (x.tagNumber < 2 ** 16) { + yield new Uint8Array([0b110_11001, ...numberToArray(2, x.tagNumber)]); + } else if (x.tagNumber < 2 ** 32) { + yield new Uint8Array([0b110_11010, ...numberToArray(4, x.tagNumber)]); + } else if (x.tagNumber < 2 ** 64) { + yield new Uint8Array([0b110_11011, ...numberToArray(8, x.tagNumber)]); + } else { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, + ); + } + for await (const y of this.#encode(x.tagContent)) { + yield y; + } + } + + /** + * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @param asyncIterable The iterable to convert to a {@link CborSequenceEncoderStream} instance. + * @returns a {@link CborSequenceEncoderStream} instance. + */ + static from( + asyncIterable: AsyncIterable | Iterable, + ): CborSequenceEncoderStream { + const encoder = new CborSequenceEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The ReadableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a ReadableStream. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The WritableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a WritableStream. + */ + get writable(): WritableStream { + return this.#writable; + } +} diff --git a/cbor/encode_stream_test.ts b/cbor/encode_stream_test.ts new file mode 100644 index 000000000000..9972b8f0f4c9 --- /dev/null +++ b/cbor/encode_stream_test.ts @@ -0,0 +1,124 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { + CborArrayEncoderStream, + CborByteEncoderStream, + CborMapEncoderStream, + type CborMapInputStream, + CborTextEncoderStream, + type CborType, + encodeCbor, +} from "./mod.ts"; +import { CborSequenceEncoderStream } from "./encode_stream.ts"; + +function random(start: number, end: number): number { + return Math.floor(Math.random() * (end - start) + start); +} + +Deno.test("CborByteEncoderStream()", async () => { + const bytes = [ + new Uint8Array(random(0, 24)), + new Uint8Array(random(24, 2 ** 8)), + new Uint8Array(random(2 ** 8, 2 ** 16)), + new Uint8Array(random(2 ** 16, 2 ** 17)), + ]; + + const expectedOutput = concat([ + new Uint8Array([0b010_11111]), + ...bytes.map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(bytes).pipeThrough(new CborByteEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborTextEncoderStream()", async () => { + const strings = [ + "a".repeat(random(0, 24)), + "a".repeat(random(24, 2 ** 8)), + "a".repeat(random(2 ** 8, 2 ** 16)), + "a".repeat(random(2 ** 16, 2 ** 17)), + ]; + + const expectedOutput = concat([ + new Uint8Array([0b011_11111]), + ...strings.map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(strings).pipeThrough(new CborTextEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborArrayEncoderStream()", async () => { + const arrays = [random(0, 2 ** 32)]; + + const expectedOutput = concat([ + new Uint8Array([0b100_11111]), + ...arrays.map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(arrays).pipeThrough(new CborArrayEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborMapEncoderStream()", async () => { + const maps: CborMapInputStream[] = [["a", random(0, 2 ** 32)]]; + + const expectedOutput = concat([ + new Uint8Array([0b101_11111]), + ...maps.map(([k, v]) => [encodeCbor(k), encodeCbor(v as CborType)]).flat(), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(maps).pipeThrough(new CborMapEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream()", async () => { + const input = [ + undefined, + null, + true, + false, + random(0, 24), + BigInt(random(0, 24)), + "a".repeat(random(0, 24)), + new Uint8Array(random(0, 24)), + new Date(), + ]; + + const expectedOutput = concat(input.map((x) => encodeCbor(x))); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(input).pipeThrough(new CborSequenceEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); diff --git a/cbor/mod.ts b/cbor/mod.ts index 32ed02474884..a85f3e980297 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -17,3 +17,4 @@ */ export * from "./decode.ts"; export * from "./encode.ts"; +export * from "./encode_stream.ts"; From b479d7d73337bab3a53f7685dda2a7b0084ad8cc Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:48:06 +1000 Subject: [PATCH 14/45] fix(cbor): random bug _maybe_ --- cbor/_common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbor/_common.ts b/cbor/_common.ts index 2b3e2c5642f4..2263fae3dafe 100644 --- a/cbor/_common.ts +++ b/cbor/_common.ts @@ -64,7 +64,7 @@ export function upgradeStreamFromGen( buffer.set(value); controller.byobRequest.respond(value.length); } - } else { + } else if (value.length) { controller.enqueue(value); } }, From 2a746b9f27ad92f8aa890c64e5ddbea3ea039c19 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:59:51 +1000 Subject: [PATCH 15/45] fix(cbor): bug where `CborTextEncoderStream` was filtering out empty strings --- cbor/encode_stream_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbor/encode_stream_test.ts b/cbor/encode_stream_test.ts index 9972b8f0f4c9..ecd6d41e6ba1 100644 --- a/cbor/encode_stream_test.ts +++ b/cbor/encode_stream_test.ts @@ -50,7 +50,7 @@ Deno.test("CborTextEncoderStream()", async () => { const expectedOutput = concat([ new Uint8Array([0b011_11111]), - ...strings.map((x) => encodeCbor(x)), + ...strings.filter(x => x).map((x) => encodeCbor(x)), new Uint8Array([0b111_11111]), ]); From ace4b9ce4492d10533a706faf1d973692088dde7 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:03:48 +1000 Subject: [PATCH 16/45] chore(cbor): fmt --- cbor/encode_stream_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbor/encode_stream_test.ts b/cbor/encode_stream_test.ts index ecd6d41e6ba1..523bacbd5a5e 100644 --- a/cbor/encode_stream_test.ts +++ b/cbor/encode_stream_test.ts @@ -50,7 +50,7 @@ Deno.test("CborTextEncoderStream()", async () => { const expectedOutput = concat([ new Uint8Array([0b011_11111]), - ...strings.filter(x => x).map((x) => encodeCbor(x)), + ...strings.filter((x) => x).map((x) => encodeCbor(x)), new Uint8Array([0b111_11111]), ]); From b85145081f408a196988006e40e084edc30eb116 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Mon, 9 Sep 2024 13:02:38 +1000 Subject: [PATCH 17/45] refactor(cbor): `CborDecoder` to `decodeCbor` --- cbor/decode.ts | 376 ++++++++++++++++++-------------------------- cbor/decode_test.ts | 106 ++++++------- cbor/encode.ts | 28 ++-- 3 files changed, 204 insertions(+), 306 deletions(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index fc25121c0727..59c37b8844ba 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -13,9 +13,7 @@ import { CborTag, type CborType } from "./encode.ts"; * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, encodeCbor } from "@std/cbor"; - * - * const decoder = new CborDecoder(); + * import { decodeCbor, encodeCbor } from "@std/cbor"; * * const rawMessage = [ * "Hello World", @@ -28,126 +26,51 @@ import { CborTag, type CborType } from "./encode.ts"; * ]; * * const encodedMessage = encodeCbor(rawMessage); - * const decodedMessage = decoder.decode(encodedMessage); + * const decodedMessage = decodeCbor(encodedMessage); * * assert(decodedMessage instanceof Array); * assertEquals(decodedMessage, rawMessage); * ``` + * + * @param value Value to decode from CBOR format. + * @returns Decoded CBOR data. */ -export class CborDecoder { - #source: number[] = []; - /** - * Constructs a new instance. - */ - constructor() {} - - /** - * Decodes a {@link Uint8Array} into a {@link CborType}. - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, encodeCbor } from "@std/cbor"; - * - * const decoder = new CborDecoder(); - * - * const rawMessage = [ - * "Hello World", - * 35, - * 0.5, - * false, - * -1, - * null, - * Uint8Array.from([0, 1, 2, 3]), - * ]; - * - * const encodedMessage = encodeCbor(rawMessage); - * const decodedMessage = decoder.decode(encodedMessage); - * - * assert(decodedMessage instanceof Array); - * assertEquals(decodedMessage, rawMessage); - * ``` - * - * @param x Value to decode from CBOR format. - * @returns Decoded CBOR data. - */ - decode(x: Uint8Array): CborType { - if (!x.length) throw RangeError("Cannot decode empty Uint8Array"); - - this.#source = Array.from(x).reverse(); - const y = this.#decode(); - this.#source = []; - return y; - } - - /** - * Decodes an array of {@link CborType} from a {@link Uint8Array}. - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { concat } from "@std/bytes"; - * import { CborDecoder, encodeCbor } from "@std/cbor"; - * - * const decoder = new CborDecoder(); - * - * const rawMessage = [ - * "Hello World", - * 35, - * 0.5, - * false, - * -1, - * null, - * Uint8Array.from([0, 1, 2, 3]), - * ]; - * - * const encodedMessage = concat(rawMessage.map(x => encodeCbor(x))) - * const decodedMessage = decoder.decodeSequence(encodedMessage); - * - * assert(decodedMessage instanceof Array); - * assertEquals(decodedMessage, rawMessage); - * ``` - * - * @param data Encoded data to be decoded from CBOR format. - * @returns Decoded CBOR data. - */ - decodeSequence(data: Uint8Array): CborType[] { - this.#source = Array.from(data).reverse(); - const output: CborType[] = []; - while (this.#source.length) output.push(this.#decode()); - return output; - } - - #decode(): CborType { - const byte = this.#source.pop(); +export function decodeCbor(value: Uint8Array): CborType { + if (!value.length) throw RangeError("Cannot decode empty Uint8Array"); + const source = Array.from(value).reverse(); + return decode(); + function decode(): CborType { + const byte = source.pop(); if (byte == undefined) throw new RangeError("More bytes were expected"); const majorType = byte >> 5; const aI = byte & 0b000_11111; switch (majorType) { case 0: - return this.#decodeZero(aI); + return decodeZero(aI); case 1: - return this.#decodeOne(aI); + return decodeOne(aI); case 2: - return this.#decodeTwo(aI); + return decodeTwo(aI); case 3: - return this.#decodeThree(aI); + return decodeThree(aI); case 4: - return this.#decodeFour(aI); + return decodeFour(aI); case 5: - return this.#decodeFive(aI); + return decodeFive(aI); case 6: - return this.#decodeSix(aI); + return decodeSix(aI); default: // Only possible for it to be 7 - return this.#decodeSeven(aI); + return decodeSeven(aI); } } - #decodeZero(aI: number): number | bigint { + function decodeZero(aI: number): number | bigint { if (aI < 24) return aI; if (aI <= 27) { return arrayToNumber( Uint8Array.from( - this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), ).buffer, true, ); @@ -157,47 +80,39 @@ export class CborDecoder { ); } - #decodeOne(aI: number): number | bigint { + function decodeOne(aI: number): number | bigint { if (aI > 27) { throw new RangeError( `Cannot decode value (0b001_${aI.toString(2).padStart(5, "0")})`, ); } - const x = this.#decodeZero(aI); + const x = decodeZero(aI); if (typeof x === "bigint") return -x - 1n; return -x - 1; } - #decodeTwo(aI: number): Uint8Array { - if (aI < 24) return Uint8Array.from(this.#source.splice(-aI, aI).reverse()); + function decodeTwo(aI: number): Uint8Array { + if (aI < 24) return Uint8Array.from(source.splice(-aI, aI).reverse()); if (aI <= 27) { - // Can safely assume `this.#source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. // 2 ** 53 is the tipping point where integers loose precision. const len = Number( arrayToNumber( Uint8Array.from( - this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), ).buffer, true, ), ); - return Uint8Array.from(this.#source.splice(-len, len).reverse()); + return Uint8Array.from(source.splice(-len, len).reverse()); } if (aI === 31) { - let byte = this.#source.pop(); + let byte = source.pop(); if (byte == undefined) throw new RangeError("More bytes were expected"); const output: Uint8Array[] = []; while (byte !== 0b111_11111) { - if (byte >> 5 === 2) { - if ((byte & 0b11111) !== 31) { - output.push(this.#decodeTwo(byte & 0b11111)); - } else { - throw new TypeError( - "Indefinite length byte strings cannot contain indefinite length byte strings", - ); - } - } else { + if (byte >> 5 !== 2) { throw new TypeError( `Cannot decode value (b${ (byte >> 5).toString(2).padStart(3, "0") @@ -207,7 +122,15 @@ export class CborDecoder { ); } - byte = this.#source.pop(); + const aI = byte & 0b11111; + if (aI === 31) { + throw new TypeError( + "Indefinite length byte strings cannot contain indefinite length byte strings", + ); + } + + output.push(decodeTwo(aI)); + byte = source.pop(); if (byte == undefined) throw new RangeError("More bytes were expected"); } return concat(output); @@ -217,136 +140,137 @@ export class CborDecoder { ); } - #decodeThree(aI: number): string { - if (aI > 27) { - if (aI === 31) { - let byte = this.#source.pop(); - if (byte == undefined) throw new RangeError("More bytes were expected"); + function decodeThree(aI: number): string { + if (aI <= 27) return new TextDecoder().decode(decodeTwo(aI)); + if (aI === 31) { + let byte = source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); - const output: string[] = []; - while (byte !== 0b111_11111) { - if (byte >> 5 === 2) { - if ((byte & 0b11111) !== 31) { - output.push(this.#decodeThree(byte & 0b11111)); - } else { - throw new TypeError( - "Indefinite length text strings cannot contain indefinite length text strings", - ); - } - } else { - throw new TypeError( - `Cannot decode value (b${ - (byte >> 5).toString(2).padStart(3, "0") - }_${ - (byte & 0b11111).toString(2).padStart(5, "0") - }) inside an indefinite length text string`, - ); - } - byte = this.#source.pop(); - if (byte == undefined) { - throw new RangeError("More bytes were expected"); - } + const output: string[] = []; + while (byte !== 0b111_11111) { + if (byte >> 5 !== 3) { + throw new TypeError( + `Cannot decode value (b${ + (byte >> 5).toString(2).padStart(3, "0") + }_${ + (byte & 0b11111).toString(2).padStart(5, "0") + }) inside an indefinite length text string`, + ); + } + + const aI = byte & 0b11111; + if (aI === 31) { + throw new TypeError( + "Indefinite length text strings cannot contain definite length text strings", + ); } - return output.join(""); + + output.push(decodeThree(aI)); + byte = source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); } - throw new RangeError( - `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, - ); + return output.join(""); } - return new TextDecoder().decode(this.#decodeTwo(aI)); + throw new RangeError( + `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, + ); } - #decodeFour(aI: number): CborType[] { - if (aI > 27) { - if (aI === 31) { - const array: CborType[] = []; - while (this.#source[this.#source.length - 1] !== 0b111_11111) { - array.push(this.#decode()); - } - this.#source.pop(); - return array; - } - throw new RangeError( - `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, + function decodeFour(aI: number): CborType[] { + if (aI <= 27) { + const array: CborType[] = []; + // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // 2 ** 53 is the tipping point where integers loose precision. + const len = aI < 24 ? aI : Number( + arrayToNumber( + Uint8Array.from( + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ), ); + for (let i = 0; i < len; ++i) array.push(decode()); + return array; } - const array: CborType[] = []; - // Can safely assume `this.#source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. - // 2 ** 53 is the tipping point where integers loose precision. - const len = aI < 24 ? aI : Number( - arrayToNumber( - Uint8Array.from( - this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), - ).buffer, - true, - ), + if (aI === 31) { + const array: CborType[] = []; + while (source[source.length - 1] !== 0b111_11111) { + array.push(decode()); + } + source.pop(); + return array; + } + throw new RangeError( + `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, ); - for (let i = 0; i < len; ++i) array.push(this.#decode()); - return array; } - #decodeFive(aI: number): { [k: string]: CborType } { - if (aI > 27) { - if (aI === 31) { - const object: { [k: string]: CborType } = {}; - while (this.#source[this.#source.length - 1] !== 0b111_11111) { - const key = this.#decode(); - if (typeof key !== "string") { - throw new TypeError( - `Cannot decode key of type "${typeof key}": This implementation only support "text string" keys`, - ); - } - if (object[key] !== undefined) { - throw new TypeError( - `A Map cannot have duplicate keys: Key (${key}) already exists`, - ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps - } - object[key] = this.#decode(); + function decodeFive(aI: number): { [k: string]: CborType } { + if (aI <= 27) { + const object: { [k: string]: CborType } = {}; + // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // 2 ** 53 is the tipping point where integers loose precision. + const len = aI < 24 ? aI : Number( + arrayToNumber( + Uint8Array.from( + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ), + ); + for (let i = 0; i < len; ++i) { + const key = decode(); + if (typeof key !== "string") { + throw new TypeError( + `Cannot decode key of type "${typeof key}": This implementation only support "text string" keys`, + ); + } + + if (object[key] !== undefined) { + throw new TypeError( + `A Map cannot have duplicate keys: Key (${key}) already exists`, + ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps } - return object; + + object[key] = decode(); } - throw new RangeError( - `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, - ); + return object; } - const object: { [k: string]: CborType } = {}; - // Can safely assume `this.#source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. - // 2 ** 53 is the tipping point where integers loose precision. - const len = aI < 24 ? aI : Number( - arrayToNumber( - Uint8Array.from( - this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), - ).buffer, - true, - ), - ); - for (let i = 0; i < len; ++i) { - const key = this.#decode(); - if (typeof key !== "string") { - throw new TypeError( - `Cannot decode key of type "${typeof key}": This implementation only support "text string" keys`, - ); - } - if (object[key] !== undefined) { - throw new TypeError( - `A Map cannot have duplicate keys: Key (${key}) already exists`, - ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps + if (aI === 31) { + const object: { [k: string]: CborType } = {}; + while (source[source.length - 1] !== 0b111_11111) { + const key = decode(); + if (typeof key !== "string") { + throw new TypeError( + `Cannot decode key of type "${typeof key}": This implementation only support "text string" keys`, + ); + } + + if (object[key] !== undefined) { + throw new TypeError( + `A Map cannot have duplicate keys: Key (${key}) already exists`, + ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps + } + + object[key] = decode(); } - object[key] = this.#decode(); + return object; } - return object; + throw new RangeError( + `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, + ); } - #decodeSix(aI: number): Date | CborTag { - const tagNumber = this.#decodeZero(aI) as number; - const tagContent = this.#decode(); - switch (tagNumber) { - case 0: + function decodeSix(aI: number): Date | CborTag { + const tagNumber = decodeZero(aI); + const tagContent = decode(); + switch (BigInt(tagNumber)) { + case 0n: if (typeof tagContent !== "string") { throw new TypeError('Invalid TagItem: Expected a "text string"'); } return new Date(tagContent); - case 1: + case 1n: if (typeof tagContent !== "number" && typeof tagContent !== "bigint") { throw new TypeError( 'Invalid TagItem: Expected a "integer" or "float"', @@ -357,7 +281,7 @@ export class CborDecoder { return new CborTag(tagNumber, tagContent); } - #decodeSeven(aI: number): undefined | null | boolean | number { + function decodeSeven(aI: number): undefined | null | boolean | number { switch (aI) { case 20: return false; @@ -371,7 +295,7 @@ export class CborDecoder { if (25 <= aI && aI <= 27) { return arrayToNumber( Uint8Array.from( - this.#source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), ).buffer, false, ); diff --git a/cbor/decode_test.ts b/cbor/decode_test.ts index 3892a9ff9448..de2efe4df37c 100644 --- a/cbor/decode_test.ts +++ b/cbor/decode_test.ts @@ -1,161 +1,143 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertEquals } from "@std/assert"; -import { CborDecoder, CborTag, encodeCbor } from "./mod.ts"; +import { CborTag, decodeCbor, encodeCbor } from "./mod.ts"; function random(start: number, end: number): number { return Math.floor(Math.random() * (end - start) + start); } -Deno.test("CborDecoder() decoding undefined", () => { - assertEquals( - new CborDecoder().decode(encodeCbor(undefined)), - undefined, - ); +Deno.test("decodeCbor() decoding undefined", () => { + assertEquals(decodeCbor(encodeCbor(undefined)), undefined); }); -Deno.test("CborDecoder() decoding null", () => { - assertEquals(new CborDecoder().decode(encodeCbor(null)), null); +Deno.test("decodeCbor() decoding null", () => { + assertEquals(decodeCbor(encodeCbor(null)), null); }); -Deno.test("CborDecoder() decoding true", () => { - assertEquals(new CborDecoder().decode(encodeCbor(true)), true); +Deno.test("decodeCbor() decoding true", () => { + assertEquals(decodeCbor(encodeCbor(true)), true); }); -Deno.test("CborDecoder() decoding false", () => { - assertEquals( - new CborDecoder().decode(encodeCbor(false)), - false, - ); +Deno.test("decodeCbor() decoding false", () => { + assertEquals(decodeCbor(encodeCbor(false)), false); }); -Deno.test("CborDecoder() decoding integers", () => { - const decoder = new CborDecoder(); - +Deno.test("decodeCbor() decoding integers", () => { let num = random(0, 24); - assertEquals(decoder.decode(encodeCbor(num)), num); - assertEquals(decoder.decode(encodeCbor(BigInt(num))), num); + assertEquals(decodeCbor(encodeCbor(num)), num); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), num); num = random(24, 2 ** 8); - assertEquals(decoder.decode(encodeCbor(num)), num); - assertEquals(decoder.decode(encodeCbor(BigInt(num))), num); + assertEquals(decodeCbor(encodeCbor(num)), num); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), num); num = random(2 ** 8, 2 ** 16); - assertEquals(decoder.decode(encodeCbor(num)), num); - assertEquals(decoder.decode(encodeCbor(BigInt(num))), num); + assertEquals(decodeCbor(encodeCbor(num)), num); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), num); num = random(2 ** 16, 2 ** 32); - assertEquals(decoder.decode(encodeCbor(num)), num); - assertEquals(decoder.decode(encodeCbor(BigInt(num))), num); + assertEquals(decodeCbor(encodeCbor(num)), num); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), num); num = random(2 ** 32, 2 ** 64); - assertEquals(decoder.decode(encodeCbor(num)), BigInt(num)); - assertEquals(decoder.decode(encodeCbor(BigInt(num))), BigInt(num)); + assertEquals(decodeCbor(encodeCbor(num)), BigInt(num)); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), BigInt(num)); }); -Deno.test("CborDecoder() decoding strings", () => { - const decoder = new CborDecoder(); +Deno.test("decodeCbor() decoding strings", () => { const textDecoder = new TextDecoder(); let text = textDecoder.decode( new Uint8Array(random(0, 24)).map((_) => random(97, 123)), ); // Range: `a` - `z` - assertEquals(decoder.decode(encodeCbor(text)), text); + assertEquals(decodeCbor(encodeCbor(text)), text); text = textDecoder.decode( new Uint8Array(random(24, 2 ** 8)).map((_) => random(97, 123)), ); // Range: `a` - `z` - assertEquals(decoder.decode(encodeCbor(text)), text); + assertEquals(decodeCbor(encodeCbor(text)), text); text = textDecoder.decode( new Uint8Array(random(2 ** 8, 2 ** 16)).map((_) => random(97, 123)), ); // Range: `a` - `z` - assertEquals(decoder.decode(encodeCbor(text)), text); + assertEquals(decodeCbor(encodeCbor(text)), text); text = textDecoder.decode( new Uint8Array(random(2 ** 16, 2 ** 17)).map((_) => random(97, 123)), ); // Range: `a` - `z` - assertEquals(decoder.decode(encodeCbor(text)), text); + assertEquals(decodeCbor(encodeCbor(text)), text); // Can't test the next bracket up due to JavaScript limitations. }); -Deno.test("CborDecoder() decoding Uint8Arrays", () => { - const decoder = new CborDecoder(); - +Deno.test("decodeCbor() decoding Uint8Arrays", () => { let bytes = new Uint8Array(random(0, 24)).map((_) => random(0, 256)); - assertEquals(decoder.decode(encodeCbor(bytes)), bytes); + assertEquals(decodeCbor(encodeCbor(bytes)), bytes); bytes = new Uint8Array(random(24, 2 ** 8)).map((_) => random(0, 256)); - assertEquals(decoder.decode(encodeCbor(bytes)), bytes); + assertEquals(decodeCbor(encodeCbor(bytes)), bytes); bytes = new Uint8Array(random(2 ** 8, 2 ** 16)).map((_) => random(0, 256)); - assertEquals(decoder.decode(encodeCbor(bytes)), bytes); + assertEquals(decodeCbor(encodeCbor(bytes)), bytes); bytes = new Uint8Array(random(2 ** 16, 2 ** 17)).map((_) => random(0, 256)); - assertEquals(decoder.decode(encodeCbor(bytes)), bytes); + assertEquals(decodeCbor(encodeCbor(bytes)), bytes); // Can't test the next bracket up due to JavaScript limitations. }); -Deno.test("CborDecoder() decoding Dates", () => { +Deno.test("decodeCbor() decoding Dates", () => { const date = new Date(); - assertEquals(new CborDecoder().decode(encodeCbor(date)), date); + assertEquals(decodeCbor(encodeCbor(date)), date); }); -Deno.test("CborDecoder() decoding arrays", () => { - const decoder = new CborDecoder(); - +Deno.test("decodeCbor() decoding arrays", () => { let array = new Array(random(0, 24)).fill(0).map((_) => random(0, 2 ** 32)); - assertEquals(decoder.decode(encodeCbor(array)), array); + assertEquals(decodeCbor(encodeCbor(array)), array); array = new Array(random(24, 2 ** 8)).fill(0).map((_) => random(0, 2 ** 32)); - assertEquals(decoder.decode(encodeCbor(array)), array); + assertEquals(decodeCbor(encodeCbor(array)), array); array = new Array(random(2 ** 8, 2 ** 16)).fill(0).map((_) => random(0, 2 ** 32) ); - assertEquals(decoder.decode(encodeCbor(array)), array); + assertEquals(decodeCbor(encodeCbor(array)), array); array = new Array(random(2 ** 16, 2 ** 17)).fill(0).map((_) => random(0, 2 ** 32) ); - assertEquals(decoder.decode(encodeCbor(array)), array); + assertEquals(decodeCbor(encodeCbor(array)), array); // Can't test the next bracket up due to JavaScript limitations. }); -Deno.test("CborDecoder() decoding objects", () => { - const decoder = new CborDecoder(); - +Deno.test("decodeCbor() decoding objects", () => { let pairs = random(0, 24); let object = Object.fromEntries( new Array(pairs).fill(0).map((_, i) => [i, i]), ); - assertEquals(decoder.decode(encodeCbor(object)), object); + assertEquals(decodeCbor(encodeCbor(object)), object); pairs = random(24, 2 ** 8); object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); - assertEquals(decoder.decode(encodeCbor(object)), object); + assertEquals(decodeCbor(encodeCbor(object)), object); pairs = random(2 ** 8, 2 ** 16); object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); - assertEquals(decoder.decode(encodeCbor(object)), object); + assertEquals(decodeCbor(encodeCbor(object)), object); pairs = random(2 ** 16, 2 ** 17); object = Object.fromEntries(new Array(pairs).fill(0).map((_, i) => [i, i])); - assertEquals(decoder.decode(encodeCbor(object)), object); + assertEquals(decodeCbor(encodeCbor(object)), object); // Can't test the next bracket up due to JavaScript limitations. }); -Deno.test("CborDecoder() decoding CborTag()", () => { +Deno.test("decodeCbor() decoding CborTag()", () => { const tag = new CborTag( 2, new Uint8Array(random(0, 24)).map((_) => random(0, 256)), ); - assertEquals( - new CborDecoder().decode(encodeCbor(tag)), - tag, - ); + assertEquals(decodeCbor(encodeCbor(tag)), tag); }); diff --git a/cbor/encode.ts b/cbor/encode.ts index 2f0218347f09..acbea896a97f 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -20,7 +20,7 @@ export type CborPrimitiveType = /** * This type specifies the encodable and decodable values for {@link encodeCbor} - * and {@link CborDecoder}. + * and {@link decodeCbor}. */ export type CborType = CborPrimitiveType | CborTag | CborType[] | { [k: string]: CborType; @@ -32,16 +32,14 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * A list of the different types can be found * [here](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). * - * This class will be returned out of CborDecoder if it doesn't automatically + * This class will be returned out of {@link decodeCbor} if it doesn't automatically * know how to handle the tag number. * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborTag, encodeCbor } from "@std/cbor"; + * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; * - * const decoder = new CborDecoder(); - * * const rawMessage = new TextEncoder().encode("Hello World"); * * const encodedMessage = encodeCbor( @@ -50,7 +48,7 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * encodeBase64Url(rawMessage), * ), * ); - * const decodedMessage = decoder.decode(encodedMessage); + * const decodedMessage = decodeCbor(encodedMessage); * assert(decodedMessage instanceof CborTag); * assert(typeof decodedMessage.tagContent === "string"); * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); @@ -65,11 +63,9 @@ export class CborTag { * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborTag, encodeCbor } from "@std/cbor"; + * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; * - * const decoder = new CborDecoder(); - * * const rawMessage = new TextEncoder().encode("Hello World"); * * const encodedMessage = encodeCbor( @@ -78,7 +74,7 @@ export class CborTag { * encodeBase64Url(rawMessage), * ), * ); - * const decodedMessage = decoder.decode(encodedMessage); + * const decodedMessage = decodeCbor(encodedMessage); * assert(decodedMessage instanceof CborTag); * assert(typeof decodedMessage.tagContent === "string"); * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); @@ -92,11 +88,9 @@ export class CborTag { * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, CborTag, encodeCbor } from "@std/cbor"; + * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; * - * const decoder = new CborDecoder(); - * * const rawMessage = new TextEncoder().encode("Hello World"); * * const encodedMessage = encodeCbor( @@ -105,7 +99,7 @@ export class CborTag { * encodeBase64Url(rawMessage), * ), * ); - * const decodedMessage = decoder.decode(encodedMessage); + * const decodedMessage = decodeCbor(encodedMessage); * assert(decodedMessage instanceof CborTag); * assert(typeof decodedMessage.tagContent === "string"); * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); @@ -131,9 +125,7 @@ export class CborTag { * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { CborDecoder, encodeCbor } from "@std/cbor"; - * - * const decoder = new CborDecoder(); + * import { decodeCbor, encodeCbor } from "@std/cbor"; * * const rawMessage = [ * "Hello World", @@ -146,7 +138,7 @@ export class CborTag { * ]; * * const encodedMessage = encodeCbor(rawMessage); - * const decodedMessage = decoder.decode(encodedMessage); + * const decodedMessage = decodeCbor(encodedMessage); * * assert(decodedMessage instanceof Array); * assertEquals(decodedMessage, rawMessage); From 049931d17ce443c71d8cae96ecf3687f16028038 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 18 Sep 2024 20:16:55 +1000 Subject: [PATCH 18/45] feat(cbor): CborDecoderStreams --- cbor/decode_stream.ts | 491 +++++++++++++++++++++++++++++++++++++ cbor/decode_stream_test.ts | 1 + cbor/encode.ts | 7 +- cbor/mod.ts | 1 + 4 files changed, 497 insertions(+), 3 deletions(-) create mode 100644 cbor/decode_stream.ts create mode 100644 cbor/decode_stream_test.ts diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts new file mode 100644 index 000000000000..30b6dec255ea --- /dev/null +++ b/cbor/decode_stream.ts @@ -0,0 +1,491 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { arrayToNumber } from "./_common.ts"; +import { upgradeStreamFromGen } from "./_common.ts"; +import { type CborPrimitiveType, CborTag } from "./encode.ts"; + +/** + * This type specifies the decodable values for + * {@link CborSequenceDecoderStream}. + */ +export type CborOutputStream = + | CborPrimitiveType + | CborTag + | CborByteDecodedStream + | CborTextDecodedStream + | CborArrayDecodedStream + | CborMapDecodedStream; +/** + * This type specifies the structure of output for {@link CborMapDecodedStream}. + */ +export type CborMapOutputStream = [string, CborOutputStream]; + +/** + * The CborByteDecodedStream is an extension of ReadableStream that + * is outputted from {@link CborSequenceDecoderStream}. + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborByteDecodedStream extends ReadableStream { + /** + * Constructs a new instance. + * + * @param gen A generator that yields the decoded CBOR byte string. + */ + constructor(gen: AsyncGenerator) { + super({ + async pull(controller) { + const { done, value } = await gen.next(); + if (done) controller.close(); + else controller.enqueue(value); + }, + async cancel() { + // deno-lint-ignore no-empty + for await (const _ of gen) {} + }, + }); + } +} + +/** + * The CborTextDecodedStream is an extension of the ReadableStream that + * is outputted from {@link CborSequenceDecoderStream}. + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborTextDecodedStream extends ReadableStream { + /** + * Constructs a new instance. + * + * @param gen A generator that yields the decoded CBOR text string. + */ + constructor(gen: AsyncGenerator) { + super({ + async pull(controller) { + const { done, value } = await gen.next(); + if (done) controller.close(); + else controller.enqueue(value); + }, + async cancel() { + // deno-lint-ignore no-empty + for await (const _ of gen) {} + }, + }); + } +} + +/** + * The CborArrayDecodedStream is an extension of the + * ReadableStream that is outputted from + * {@link CborSequenceDecoderStream}. + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborArrayDecodedStream extends ReadableStream { + /** + * Constructs a new instance. + * + * @param gen A generator that yields the decoded CBOR array. + */ + constructor(gen: AsyncGenerator) { + super({ + async pull(controller) { + const { done, value } = await gen.next(); + if (done) controller.close(); + else controller.enqueue(value); + }, + async cancel() { + // deno-lint-ignore no-empty + for await (const _ of gen) {} + }, + }); + } +} + +/** + * The CborMapDecodedStream is an extension of the + * ReadableStream that is outputted from + * {@link CborSequenceDecoderStream}. + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborMapDecodedStream extends ReadableStream { + /** + * Constructs a new instance. + * + * @param gen A generator that yields the decoded CBOR map. + */ + constructor(gen: AsyncGenerator) { + super({ + async pull(controller) { + const { done, value } = await gen.next(); + if (done) controller.close(); + else controller.enqueue(value); + }, + async cancel() { + // deno-lint-ignore no-empty + for await (const _ of gen) {} + }, + }); + } +} + +/** + * The CborSequenceDecoderStream decodes a CBOR encoded + * ReadableStream into a sequence of {@link CborOutputStream}. + * + * @example Usage + * ```ts no-eval + * + * ``` + */ +export class CborSequenceDecoderStream + implements TransformStream { + #source: ReadableStreamBYOBReader; + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + Uint8Array, + Uint8Array + >(); + try { + this.#source = readable.getReader({ mode: "byob" }); + } catch { + this.#source = upgradeStreamFromGen(async function* () { + for await (const chunk of readable) { + yield chunk; + } + }()).getReader({ mode: "byob" }); + } + this.#readable = ReadableStream.from( + this.#decodeSequence(), + ); + this.#writable = writable; + } + + async #read(bytes: number, expectMore: true): Promise; + async #read( + bytes: number, + expectMore: false, + ): Promise; + async #read( + bytes: number, + expectMore: boolean, + ): Promise { + const { done, value } = await this.#source.read(new Uint8Array(bytes), { + min: bytes, + }); + if (done) { + if (expectMore) throw new RangeError("More bytes were expected"); + else return undefined; + } + if (value.length < bytes) throw new RangeError("More bytes were expected"); + return value; + } + + async *#decodeSequence(): AsyncGenerator { + while (true) { + const value = await this.#read(1, false); + if (value == undefined) return; + // Since `value` is only 1 byte long, it will be of type `number` + yield decode(this.#read, arrayToNumber(value.buffer, true) as number); + } + } + + /** + * The ReadableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a ReadableStream. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The WritableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a WritableStream. + */ + get writable(): WritableStream { + return this.#writable; + } +} + +interface ReadFunc { + (bytes: number, expectMore: true): Promise; + (bytes: number, expectMore: false): Promise; +} + +async function* readGen( + read: ReadFunc, + bytes: bigint, +): AsyncGenerator { + for (let i = 0n; i < bytes; i += 2n ** 32n) { + yield await read(Math.min(Number(bytes - i), 2 ** 32), true); + } +} + +async function* readDefinite( + read: ReadFunc, + items: number | bigint, +): AsyncGenerator { + items = BigInt(items); + for (let i = 0n; i < items; ++i) { + yield await decode( + read, + arrayToNumber((await read(1, true)).buffer, true) as number, + ); + } +} + +async function* readIndefinite( + read: ReadFunc, + denyInnerIndefinite: boolean, + message?: string, +): AsyncGenerator { + while (true) { + const byte = arrayToNumber((await read(1, true)).buffer, true) as number; + if (byte === 0b111_11111) break; + if (denyInnerIndefinite && (byte & 0b000_11111) === 31) { + throw new TypeError(message); + } + yield await decode(read, byte); + } +} + +function decode(read: ReadFunc, byte: number): Promise { + const majorType = byte >> 5; + const aI = byte & 0b000_11111; + switch (majorType) { + case 0: + return decodeZero(read, aI); + case 1: + return decodeOne(read, aI); + case 2: + return decodeTwo(read, aI); + case 3: + return decodeThree(read, aI); + case 4: + return decodeFour(read, aI); + case 5: + return decodeFive(read, aI); + case 6: + return decodeSix(read, aI); + default: + return decodeSeven(read, aI); + } +} + +async function decodeZero( + read: ReadFunc, + aI: number, +): Promise { + if (aI < 24) return aI; + read(1, true); + if (aI <= 27) { + return arrayToNumber((await read(2 ** (aI - 24), true)).buffer, true); + } + throw new RangeError( + `Cannot decode value (0b000_${aI.toString(2).padStart(5, "0")})`, + ); +} + +async function decodeOne(read: ReadFunc, aI: number): Promise { + if (aI > 27) { + throw new RangeError( + `Cannot decode value (0b001_${aI.toString(2).padStart(5, "0")})`, + ); + } + const x = await decodeZero(read, aI); + return typeof x === "bigint" ? -x - 1n : -x - 1; +} + +async function decodeTwo( + read: ReadFunc, + aI: number, +): Promise { + if (aI < 24) return await read(aI, true); + if (aI <= 27) { + const bytes = arrayToNumber( + (await read(2 ** (aI - 24), true)).buffer, + true, + ); + return typeof bytes === "bigint" + ? new CborByteDecodedStream(readGen(read, bytes)) + : await read(bytes, true); + } + if (aI === 31) { + return new CborByteDecodedStream(async function* () { + for await (const x of readIndefinite(read, true, "")) { + if (x instanceof Uint8Array) yield x; + else if (x instanceof CborByteDecodedStream) { + for await (const y of x) yield y; + } else throw new TypeError(); + } + }()); + } + throw new RangeError( + `Cannot decode value (0b010_${aI.toString(2).padStart(5, "0")})`, + ); +} + +async function decodeThree( + read: ReadFunc, + aI: number, +): Promise { + if (aI < 24) return new TextDecoder().decode(await read(aI, true)); + if (aI <= 27) { + const bytes = arrayToNumber( + (await read(2 ** (aI - 24), true)).buffer, + true, + ); + return typeof bytes === "bigint" + ? new CborTextDecodedStream(async function* () { + const decoder = new TextDecoder(); + for await (const chunk of readGen(read, bytes)) { + yield decoder.decode(chunk, { stream: true }); + } + }()) + : new TextDecoder().decode(await read(bytes, true)); + } + if (aI === 31) { + return new CborTextDecodedStream(async function* () { + for await (const x of readIndefinite(read, true, "")) { + if (typeof x === "string") yield x; + else if (x instanceof CborTextDecodedStream) { + for await (const y of x) yield y; + } else throw new TypeError(); + } + }()); + } + throw new RangeError( + `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, + ); +} + +async function decodeFour( + read: ReadFunc, + aI: number, +): Promise { + if (aI < 24) return new CborArrayDecodedStream(readDefinite(read, aI)); + if (aI <= 27) { + return new CborArrayDecodedStream( + readDefinite( + read, + arrayToNumber((await read(2 ** (aI - 24), true)).buffer, true), + ), + ); + } + if (aI === 31) return new CborArrayDecodedStream(readIndefinite(read, false)); + throw new Error(`Unexpected value: 0b100_${aI.toString(2).padStart(5, "0")}`); +} + +async function decodeFive( + read: ReadFunc, + aI: number, +): Promise { + async function* convert( + gen: AsyncGenerator, + ): AsyncGenerator { + while (true) { + const key = await gen.next(); + if (key.done) break; + if (typeof key.value !== "string") throw new TypeError(); + + const value = await gen.next(); + if (value.done) throw new RangeError(); + + yield [key.value, value.value]; + } + } + + if (aI < 24) { + return new CborMapDecodedStream(convert(readDefinite(read, aI * 2))); + } + if (aI <= 27) { + return new CborMapDecodedStream( + convert( + readDefinite( + read, + arrayToNumber((await read(2 ** (aI - 24), true)).buffer, true), + ), + ), + ); + } + if (aI === 31) { + return new CborMapDecodedStream(convert(readIndefinite(read, false))); + } + throw new RangeError( + `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, + ); +} + +async function decodeSix( + read: ReadFunc, + aI: number, +): Promise> { + const tagNumber = await decodeZero(read, aI); + const tagContent = await decode( + read, + arrayToNumber((await read(1, true)).buffer, true) as number, + ); + switch (tagNumber) { + case 0: + if (typeof tagContent !== "string") throw new TypeError(); + return new Date(tagContent); + case 1: + if (typeof tagContent !== "number" && typeof tagContent !== "bigint") { + throw new TypeError(); + } + return new Date(Number(tagContent) * 1000); + } + return new CborTag(tagNumber, tagContent); +} + +async function decodeSeven( + read: ReadFunc, + aI: number, +): Promise { + switch (aI) { + case 20: + return false; + case 21: + return true; + case 22: + return null; + case 23: + return undefined; + } + if (25 <= aI && aI <= 27) { + return arrayToNumber((await read(2 ** (aI - 24), true)).buffer, false); + } + throw new RangeError( + `Cannot decode value (0b111_${aI.toString(2).padStart(5, "0")})`, + ); +} diff --git a/cbor/decode_stream_test.ts b/cbor/decode_stream_test.ts new file mode 100644 index 000000000000..0a39b9f87d69 --- /dev/null +++ b/cbor/decode_stream_test.ts @@ -0,0 +1 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. diff --git a/cbor/encode.ts b/cbor/encode.ts index acbea896a97f..56969c9aa15a 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -3,6 +3,7 @@ import { concat } from "@std/bytes"; import { numberToArray } from "./_common.ts"; import type { CborInputStream } from "./encode_stream.ts"; +import type { CborOutputStream } from "./decode_stream.ts"; /** * This type specifies the primitive types that the implementation can @@ -27,7 +28,7 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { }; /** - * A class that wraps {@link CborType} and {@link CborInputStream} values, + * A class that wraps {@link CborType}, {@link CborInputStream}, and {@link CborOutputStream} values, * assuming the `tagContent` is the appropriate type and format for encoding. * A list of the different types can be found * [here](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). @@ -54,9 +55,9 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); * ``` * - * @typeParam T extends {@link CborType} | {@link CborInputStream} + * @typeParam T extends {@link CborType} | {@link CborInputStream} | {@link CborOutputStream} */ -export class CborTag { +export class CborTag { /** * The number indicating how the tagContent should be interpreted based off * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). diff --git a/cbor/mod.ts b/cbor/mod.ts index a85f3e980297..fd43cff71222 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -17,4 +17,5 @@ */ export * from "./decode.ts"; export * from "./encode.ts"; +export * from "./decode_stream.ts"; export * from "./encode_stream.ts"; From e1780ea8d53d52431a11e68bfafb638cfe83de03 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 18 Sep 2024 20:33:17 +1000 Subject: [PATCH 19/45] fix(cbor): missing error messages from CborDecoderStreams --- cbor/decode_stream.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index 30b6dec255ea..d36f17671743 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -346,7 +346,7 @@ async function decodeTwo( if (x instanceof Uint8Array) yield x; else if (x instanceof CborByteDecodedStream) { for await (const y of x) yield y; - } else throw new TypeError(); + } else throw new TypeError('Unexpected type in CBOR byte string'); } }()); } @@ -380,7 +380,7 @@ async function decodeThree( if (typeof x === "string") yield x; else if (x instanceof CborTextDecodedStream) { for await (const y of x) yield y; - } else throw new TypeError(); + } else throw new TypeError('Unexpected type in CBOR text string'); } }()); } @@ -403,7 +403,7 @@ async function decodeFour( ); } if (aI === 31) return new CborArrayDecodedStream(readIndefinite(read, false)); - throw new Error(`Unexpected value: 0b100_${aI.toString(2).padStart(5, "0")}`); + throw new RangeError(`Cannot decode value (0b100_${aI.toString(2).padStart(5, "0")})`); } async function decodeFive( @@ -416,10 +416,10 @@ async function decodeFive( while (true) { const key = await gen.next(); if (key.done) break; - if (typeof key.value !== "string") throw new TypeError(); + if (typeof key.value !== "string") throw new TypeError('Cannot parse map key: Only text string map keys are supported'); const value = await gen.next(); - if (value.done) throw new RangeError(); + if (value.done) throw new RangeError('Impossible State: readDefinite | readIndefinite should have thrown an error'); yield [key.value, value.value]; } @@ -457,11 +457,11 @@ async function decodeSix( ); switch (tagNumber) { case 0: - if (typeof tagContent !== "string") throw new TypeError(); + if (typeof tagContent !== "string") throw new TypeError('Invalid DataItem: Expected text string to follow tag number 0'); return new Date(tagContent); case 1: if (typeof tagContent !== "number" && typeof tagContent !== "bigint") { - throw new TypeError(); + throw new TypeError('Invalid DataItem: Expected integer or float to follow tagNumber 1'); } return new Date(Number(tagContent) * 1000); } From 8d93b05ff0b8baf839906c30d04663c19bb375f2 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:31:40 +1000 Subject: [PATCH 20/45] tests(cbor): added one test --- cbor/_common.ts | 9 +- cbor/decode_stream.ts | 493 ++++++++++++++++++------------------- cbor/decode_stream_test.ts | 29 +++ cbor/encode_stream_test.ts | 2 +- 4 files changed, 279 insertions(+), 254 deletions(-) diff --git a/cbor/_common.ts b/cbor/_common.ts index 2263fae3dafe..15fa91503d9c 100644 --- a/cbor/_common.ts +++ b/cbor/_common.ts @@ -50,8 +50,13 @@ export function upgradeStreamFromGen( async pull(controller) { const { done, value } = await gen.next(); if (done) { - controller.byobRequest?.respond(0); - return controller.close(); + try { + controller.byobRequest?.respond(0); + return controller.close(); + } catch { + controller.close(); + return controller.byobRequest?.respond(0); + } } if (controller.byobRequest?.view) { const buffer = new Uint8Array(controller.byobRequest.view.buffer); diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index d36f17671743..83f3fd9a5083 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -199,293 +199,284 @@ export class CborSequenceDecoderStream return value; } - async *#decodeSequence(): AsyncGenerator { - while (true) { - const value = await this.#read(1, false); - if (value == undefined) return; - // Since `value` is only 1 byte long, it will be of type `number` - yield decode(this.#read, arrayToNumber(value.buffer, true) as number); + async *#readGen(bytes: bigint): AsyncGenerator { + for (let i = 0n; i < bytes; i += 2n ** 32n) { + yield await this.#read(Math.min(Number(bytes - i), 2 ** 32), true); } } - /** - * The ReadableStream property. - * - * @example Usage - * ```ts no-eval - * - * ``` - * - * @returns a ReadableStream. - */ - get readable(): ReadableStream { - return this.#readable; + async *#readDefinite( + items: number | bigint, + ): AsyncGenerator { + items = BigInt(items); + for (let i = 0n; i < items; ++i) { + yield await this.#decode( + arrayToNumber((await this.#read(1, true)).buffer, true) as number, + ); + } } - /** - * The WritableStream property. - * - * @example Usage - * ```ts no-eval - * - * ``` - * - * @returns a WritableStream. - */ - get writable(): WritableStream { - return this.#writable; + async *#readIndefinite( + denyInnerIndefinite: boolean, + message?: string, + ): AsyncGenerator { + while (true) { + const byte = arrayToNumber( + (await this.#read(1, true)).buffer, + true, + ) as number; + if (byte === 0b111_11111) break; + if (denyInnerIndefinite && (byte & 0b000_11111) === 31) { + throw new TypeError(message); + } + yield await this.#decode(byte); + } } -} - -interface ReadFunc { - (bytes: number, expectMore: true): Promise; - (bytes: number, expectMore: false): Promise; -} -async function* readGen( - read: ReadFunc, - bytes: bigint, -): AsyncGenerator { - for (let i = 0n; i < bytes; i += 2n ** 32n) { - yield await read(Math.min(Number(bytes - i), 2 ** 32), true); + async *#decodeSequence(): AsyncGenerator { + while (true) { + const value = await this.#read(1, false); + if (value == undefined) return; + // Since `value` is only 1 byte long, it will be of type `number` + yield this.#decode(arrayToNumber(value.buffer, true) as number); + } } -} -async function* readDefinite( - read: ReadFunc, - items: number | bigint, -): AsyncGenerator { - items = BigInt(items); - for (let i = 0n; i < items; ++i) { - yield await decode( - read, - arrayToNumber((await read(1, true)).buffer, true) as number, - ); + #decode(byte: number): Promise { + const majorType = byte >> 5; + const aI = byte & 0b000_11111; + switch (majorType) { + case 0: + return this.#decodeZero(aI); + case 1: + return this.#decodeOne(aI); + case 2: + return this.#decodeTwo(aI); + case 3: + return this.#decodeThree(aI); + case 4: + return this.#decodeFour(aI); + case 5: + return this.#decodeFive(aI); + case 6: + return this.#decodeSix(aI); + default: + return this.#decodeSeven(aI); + } } -} -async function* readIndefinite( - read: ReadFunc, - denyInnerIndefinite: boolean, - message?: string, -): AsyncGenerator { - while (true) { - const byte = arrayToNumber((await read(1, true)).buffer, true) as number; - if (byte === 0b111_11111) break; - if (denyInnerIndefinite && (byte & 0b000_11111) === 31) { - throw new TypeError(message); + async #decodeZero(aI: number): Promise { + if (aI < 24) return aI; + if (aI <= 27) { + return arrayToNumber( + (await this.#read(2 ** (aI - 24), true)).buffer, + true, + ); } - yield await decode(read, byte); + throw new RangeError( + `Cannot decode value (0b000_${aI.toString(2).padStart(5, "0")})`, + ); } -} -function decode(read: ReadFunc, byte: number): Promise { - const majorType = byte >> 5; - const aI = byte & 0b000_11111; - switch (majorType) { - case 0: - return decodeZero(read, aI); - case 1: - return decodeOne(read, aI); - case 2: - return decodeTwo(read, aI); - case 3: - return decodeThree(read, aI); - case 4: - return decodeFour(read, aI); - case 5: - return decodeFive(read, aI); - case 6: - return decodeSix(read, aI); - default: - return decodeSeven(read, aI); + async #decodeOne(aI: number): Promise { + if (aI > 27) { + throw new RangeError( + `Cannot decode value (0b001_${aI.toString(2).padStart(5, "0")})`, + ); + } + const x = await this.#decodeZero(aI); + return typeof x === "bigint" ? -x - 1n : -x - 1; } -} -async function decodeZero( - read: ReadFunc, - aI: number, -): Promise { - if (aI < 24) return aI; - read(1, true); - if (aI <= 27) { - return arrayToNumber((await read(2 ** (aI - 24), true)).buffer, true); + async #decodeTwo(aI: number): Promise { + if (aI < 24) return await this.#read(aI, true); + if (aI <= 27) { + const bytes = arrayToNumber( + (await this.#read(2 ** (aI - 24), true)).buffer, + true, + ); + return typeof bytes === "bigint" + ? new CborByteDecodedStream(this.#readGen(bytes)) + : await this.#read(bytes, true); + } + if (aI === 31) { + return new CborByteDecodedStream(async function* (gen) { + for await (const x of gen) { + if (x instanceof Uint8Array) yield x; + else if (x instanceof CborByteDecodedStream) { + for await (const y of x) yield y; + } else throw new TypeError("Unexpected type in CBOR byte string"); + } + }(this.#readIndefinite(true, ""))); + } + throw new RangeError( + `Cannot decode value (0b010_${aI.toString(2).padStart(5, "0")})`, + ); } - throw new RangeError( - `Cannot decode value (0b000_${aI.toString(2).padStart(5, "0")})`, - ); -} -async function decodeOne(read: ReadFunc, aI: number): Promise { - if (aI > 27) { + async #decodeThree(aI: number): Promise { + if (aI < 24) return new TextDecoder().decode(await this.#read(aI, true)); + if (aI <= 27) { + const bytes = arrayToNumber( + (await this.#read(2 ** (aI - 24), true)).buffer, + true, + ); + return typeof bytes === "bigint" + ? new CborTextDecodedStream(async function* (gen) { + const decoder = new TextDecoder(); + for await (const chunk of gen) { + yield decoder.decode(chunk, { stream: true }); + } + }(this.#readGen(bytes))) + : new TextDecoder().decode(await this.#read(bytes, true)); + } + if (aI === 31) { + return new CborTextDecodedStream(async function* (gen) { + for await (const x of gen) { + if (typeof x === "string") yield x; + else if (x instanceof CborTextDecodedStream) { + for await (const y of x) yield y; + } else throw new TypeError("Unexpected type in CBOR text string"); + } + }(this.#readIndefinite(true, ""))); + } throw new RangeError( - `Cannot decode value (0b001_${aI.toString(2).padStart(5, "0")})`, + `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, ); } - const x = await decodeZero(read, aI); - return typeof x === "bigint" ? -x - 1n : -x - 1; -} -async function decodeTwo( - read: ReadFunc, - aI: number, -): Promise { - if (aI < 24) return await read(aI, true); - if (aI <= 27) { - const bytes = arrayToNumber( - (await read(2 ** (aI - 24), true)).buffer, - true, + async #decodeFour(aI: number): Promise { + if (aI < 24) return new CborArrayDecodedStream(this.#readDefinite(aI)); + if (aI <= 27) { + return new CborArrayDecodedStream( + this.#readDefinite( + arrayToNumber((await this.#read(2 ** (aI - 24), true)).buffer, true), + ), + ); + } + if (aI === 31) { + return new CborArrayDecodedStream(this.#readIndefinite(false)); + } + throw new RangeError( + `Cannot decode value (0b100_${aI.toString(2).padStart(5, "0")})`, ); - return typeof bytes === "bigint" - ? new CborByteDecodedStream(readGen(read, bytes)) - : await read(bytes, true); - } - if (aI === 31) { - return new CborByteDecodedStream(async function* () { - for await (const x of readIndefinite(read, true, "")) { - if (x instanceof Uint8Array) yield x; - else if (x instanceof CborByteDecodedStream) { - for await (const y of x) yield y; - } else throw new TypeError('Unexpected type in CBOR byte string'); - } - }()); } - throw new RangeError( - `Cannot decode value (0b010_${aI.toString(2).padStart(5, "0")})`, - ); -} + async #decodeFive(aI: number): Promise { + async function* convert( + gen: AsyncGenerator, + ): AsyncGenerator { + while (true) { + const key = await gen.next(); + if (key.done) break; + if (typeof key.value !== "string") { + throw new TypeError( + "Cannot parse map key: Only text string map keys are supported", + ); + } -async function decodeThree( - read: ReadFunc, - aI: number, -): Promise { - if (aI < 24) return new TextDecoder().decode(await read(aI, true)); - if (aI <= 27) { - const bytes = arrayToNumber( - (await read(2 ** (aI - 24), true)).buffer, - true, - ); - return typeof bytes === "bigint" - ? new CborTextDecodedStream(async function* () { - const decoder = new TextDecoder(); - for await (const chunk of readGen(read, bytes)) { - yield decoder.decode(chunk, { stream: true }); + const value = await gen.next(); + if (value.done) { + throw new RangeError( + "Impossible State: readDefinite | readIndefinite should have thrown an error", + ); } - }()) - : new TextDecoder().decode(await read(bytes, true)); - } - if (aI === 31) { - return new CborTextDecodedStream(async function* () { - for await (const x of readIndefinite(read, true, "")) { - if (typeof x === "string") yield x; - else if (x instanceof CborTextDecodedStream) { - for await (const y of x) yield y; - } else throw new TypeError('Unexpected type in CBOR text string'); + + yield [key.value, value.value]; } - }()); - } - throw new RangeError( - `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, - ); -} + } -async function decodeFour( - read: ReadFunc, - aI: number, -): Promise { - if (aI < 24) return new CborArrayDecodedStream(readDefinite(read, aI)); - if (aI <= 27) { - return new CborArrayDecodedStream( - readDefinite( - read, - arrayToNumber((await read(2 ** (aI - 24), true)).buffer, true), - ), + if (aI < 24) { + return new CborMapDecodedStream(convert(this.#readDefinite(aI * 2))); + } + if (aI <= 27) { + return new CborMapDecodedStream( + convert( + this.#readDefinite( + arrayToNumber( + (await this.#read(2 ** (aI - 24), true)).buffer, + true, + ), + ), + ), + ); + } + if (aI === 31) { + return new CborMapDecodedStream(convert(this.#readIndefinite(false))); + } + throw new RangeError( + `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, ); } - if (aI === 31) return new CborArrayDecodedStream(readIndefinite(read, false)); - throw new RangeError(`Cannot decode value (0b100_${aI.toString(2).padStart(5, "0")})`); -} - -async function decodeFive( - read: ReadFunc, - aI: number, -): Promise { - async function* convert( - gen: AsyncGenerator, - ): AsyncGenerator { - while (true) { - const key = await gen.next(); - if (key.done) break; - if (typeof key.value !== "string") throw new TypeError('Cannot parse map key: Only text string map keys are supported'); - - const value = await gen.next(); - if (value.done) throw new RangeError('Impossible State: readDefinite | readIndefinite should have thrown an error'); - - yield [key.value, value.value]; + async #decodeSix(aI: number): Promise> { + const tagNumber = await this.#decodeZero(aI); + const tagContent = await this.#decode( + arrayToNumber((await this.#read(1, true)).buffer, true) as number, + ); + switch (tagNumber) { + case 0: + if (typeof tagContent !== "string") { + throw new TypeError( + "Invalid DataItem: Expected text string to follow tag number 0", + ); + } + return new Date(tagContent); + case 1: + if (typeof tagContent !== "number" && typeof tagContent !== "bigint") { + throw new TypeError( + "Invalid DataItem: Expected integer or float to follow tagNumber 1", + ); + } + return new Date(Number(tagContent) * 1000); } + return new CborTag(tagNumber, tagContent); } - - if (aI < 24) { - return new CborMapDecodedStream(convert(readDefinite(read, aI * 2))); - } - if (aI <= 27) { - return new CborMapDecodedStream( - convert( - readDefinite( - read, - arrayToNumber((await read(2 ** (aI - 24), true)).buffer, true), - ), - ), + async #decodeSeven(aI: number): Promise { + switch (aI) { + case 20: + return false; + case 21: + return true; + case 22: + return null; + case 23: + return undefined; + } + if (25 <= aI && aI <= 27) { + return arrayToNumber( + (await this.#read(2 ** (aI - 24), true)).buffer, + false, + ); + } + throw new RangeError( + `Cannot decode value (0b111_${aI.toString(2).padStart(5, "0")})`, ); } - if (aI === 31) { - return new CborMapDecodedStream(convert(readIndefinite(read, false))); - } - throw new RangeError( - `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, - ); -} -async function decodeSix( - read: ReadFunc, - aI: number, -): Promise> { - const tagNumber = await decodeZero(read, aI); - const tagContent = await decode( - read, - arrayToNumber((await read(1, true)).buffer, true) as number, - ); - switch (tagNumber) { - case 0: - if (typeof tagContent !== "string") throw new TypeError('Invalid DataItem: Expected text string to follow tag number 0'); - return new Date(tagContent); - case 1: - if (typeof tagContent !== "number" && typeof tagContent !== "bigint") { - throw new TypeError('Invalid DataItem: Expected integer or float to follow tagNumber 1'); - } - return new Date(Number(tagContent) * 1000); + /** + * The ReadableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a ReadableStream. + */ + get readable(): ReadableStream { + return this.#readable; } - return new CborTag(tagNumber, tagContent); -} -async function decodeSeven( - read: ReadFunc, - aI: number, -): Promise { - switch (aI) { - case 20: - return false; - case 21: - return true; - case 22: - return null; - case 23: - return undefined; - } - if (25 <= aI && aI <= 27) { - return arrayToNumber((await read(2 ** (aI - 24), true)).buffer, false); + /** + * The WritableStream property. + * + * @example Usage + * ```ts no-eval + * + * ``` + * + * @returns a WritableStream. + */ + get writable(): WritableStream { + return this.#writable; } - throw new RangeError( - `Cannot decode value (0b111_${aI.toString(2).padStart(5, "0")})`, - ); } diff --git a/cbor/decode_stream_test.ts b/cbor/decode_stream_test.ts index 0a39b9f87d69..938621b618f3 100644 --- a/cbor/decode_stream_test.ts +++ b/cbor/decode_stream_test.ts @@ -1 +1,30 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { CborSequenceDecoderStream, CborSequenceEncoderStream } from "./mod.ts"; + +function random(start: number, end: number): number { + return Math.floor(Math.random() * (end - start) + start); +} + +Deno.test("CborSequenceDecoderStream()", async () => { + const input = [ + undefined, + null, + true, + false, + random(0, 24), + BigInt(random(0, 24)), + "a".repeat(random(0, 24)), + new Uint8Array(random(0, 24)), + new Date(), + ]; + + assertEquals( + await Array.fromAsync( + ReadableStream.from(input).pipeThrough(new CborSequenceEncoderStream()) + .pipeThrough(new CborSequenceDecoderStream()), + ), + input.map((x) => typeof x === "bigint" && x < 2n ** 32n ? Number(x) : x), + ); +}); diff --git a/cbor/encode_stream_test.ts b/cbor/encode_stream_test.ts index 523bacbd5a5e..74c757579bce 100644 --- a/cbor/encode_stream_test.ts +++ b/cbor/encode_stream_test.ts @@ -7,11 +7,11 @@ import { CborByteEncoderStream, CborMapEncoderStream, type CborMapInputStream, + CborSequenceEncoderStream, CborTextEncoderStream, type CborType, encodeCbor, } from "./mod.ts"; -import { CborSequenceEncoderStream } from "./encode_stream.ts"; function random(start: number, end: number): number { return Math.floor(Math.random() * (end - start) + start); From df3692cd494077ac9786d80a1e5c1768134d54b8 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:13:39 +1000 Subject: [PATCH 21/45] tests(cbor): added more tests for `encodeCbor()` testing errors --- cbor/encode.ts | 20 ++++++++++----- cbor/encode_test.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/cbor/encode.ts b/cbor/encode.ts index 56969c9aa15a..903253358d4a 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -171,8 +171,9 @@ export function encodeCbor(value: CborType): Uint8Array { function encodeNumber(x: number): Uint8Array { if (x % 1 === 0) { - const majorType = x < 0 ? 0b001_00000 : 0b000_00000; - if (x < 0) x = -x - 1; + const isNegative = x < 0; + const majorType = isNegative ? 0b001_00000 : 0b000_00000; + if (isNegative) x = -x - 1; if (x < 24) return new Uint8Array([majorType + x]); if (x < 2 ** 8) return new Uint8Array([majorType + 24, x]); @@ -186,20 +187,27 @@ function encodeNumber(x: number): Uint8Array { return concat([new Uint8Array([majorType + 27]), numberToArray(8, x)]); } throw new RangeError( - `Cannot encode number: It (${x}) exceeds 2 ** 64 - 1`, + `Cannot encode number: It (${isNegative ? -x - 1 : x}) exceeds ${ + isNegative ? "-" : "" + }2 ** 64 - 1`, ); } return concat([new Uint8Array([0b111_11011]), numberToArray(8, x)]); } function encodeBigInt(x: bigint): Uint8Array { - if ((x < 0n ? -x : x) < 2n ** 32n) return encodeNumber(Number(x)); + const isNegative = x < 0n; + if ((isNegative ? -x : x) < 2n ** 32n) return encodeNumber(Number(x)); const head = new Uint8Array([x < 0n ? 0b010_11011 : 0b000_11011]); - if (x < 0n) x = -x - 1n; + if (isNegative) x = -x - 1n; if (x < 2n ** 64n) return concat([head, numberToArray(8, x)]); - throw new RangeError(`Cannot encode bigint: It (${x}) exceeds 2 ** 64 - 1`); + throw new RangeError( + `Cannot encode bigint: It (${isNegative ? -x - 1n : x}) exceeds ${ + isNegative ? "-" : "" + }2 ** 64 - 1`, + ); } function encodeUint8Array(x: Uint8Array): Uint8Array { diff --git a/cbor/encode_test.ts b/cbor/encode_test.ts index 8041eb776c68..1fb686864ed5 100644 --- a/cbor/encode_test.ts +++ b/cbor/encode_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { concat } from "@std/bytes"; import { CborTag, encodeCbor } from "./mod.ts"; @@ -334,3 +334,63 @@ Deno.test("encodeCbor() encoding CborTag()", () => { new Uint8Array([0b110_00010, 0b010_00000 + bytes.length, ...bytes]), ); }); + +Deno.test("encodeCbor() rejecting numbers as Uint", () => { + const num = 2 ** 65; + assertThrows( + () => { + encodeCbor(num); + }, + RangeError, + `Cannot encode number: It (${num}) exceeds 2 ** 64 - 1`, + ); +}); + +Deno.test("encodeCbor() rejecting numbers as Int", () => { + const num = -(2 ** 65); + assertThrows( + () => { + encodeCbor(num); + }, + RangeError, + `Cannot encode number: It (${num}) exceeds -2 ** 64 - 1`, + ); +}); + +Deno.test("encodeCbor() rejecting bigints as Uint", () => { + const num = 2n ** 65n; + assertThrows( + () => { + encodeCbor(num); + }, + RangeError, + `Cannot encode bigint: It (${num}) exceeds 2 ** 64 - 1`, + ); +}); + +Deno.test("encodeCbor() rejecting bigints as Int", () => { + const num = -(2n ** 65n); + assertThrows( + () => { + encodeCbor(num); + }, + RangeError, + `Cannot encode bigint: It (${num}) exceeds -2 ** 64 - 1`, + ); +}); + +Deno.test("encodeCbor() rejecting CborTag()", () => { + const num = 2 ** 65; + assertThrows( + () => { + encodeCbor( + new CborTag( + num, + new Uint8Array(random(0, 24)).map((_) => random(0, 256)), + ), + ); + }, + RangeError, + `Cannot encode Tag Item: Tag Number (${num}) exceeds 2 ** 64 - 1`, + ); +}); From ab8bf00051921e216d086fabfd42b4420dc3ed19 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:14:21 +1000 Subject: [PATCH 22/45] fix(cbor): CborDecoderStreams not handling empty strings correctly --- cbor/decode_stream.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index 83f3fd9a5083..fecc0fb08da6 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -188,6 +188,7 @@ export class CborSequenceDecoderStream bytes: number, expectMore: boolean, ): Promise { + if (bytes === 0) return new Uint8Array(0); const { done, value } = await this.#source.read(new Uint8Array(bytes), { min: bytes, }); From 0e77bc3160c34c88da3f78c8692c9b50d249aa9c Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:36:54 +1000 Subject: [PATCH 23/45] tests(cbor): added more tests for `decodeCbor()` testing errors --- cbor/decode.ts | 2 +- cbor/decode_test.ts | 309 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 309 insertions(+), 2 deletions(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index 59c37b8844ba..0cb9ebc5e005 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -201,7 +201,7 @@ export function decodeCbor(value: Uint8Array): CborType { return array; } throw new RangeError( - `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, + `Cannot decode value (0b100_${aI.toString(2).padStart(5, "0")})`, ); } diff --git a/cbor/decode_test.ts b/cbor/decode_test.ts index de2efe4df37c..4228a3c73711 100644 --- a/cbor/decode_test.ts +++ b/cbor/decode_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { CborTag, decodeCbor, encodeCbor } from "./mod.ts"; function random(start: number, end: number): number { @@ -141,3 +141,310 @@ Deno.test("decodeCbor() decoding CborTag()", () => { ); assertEquals(decodeCbor(encodeCbor(tag)), tag); }); + +Deno.test("decodeCbor() rejecting empty encoded data", () => { + assertThrows( + () => { + decodeCbor(new Uint8Array(0)); + }, + RangeError, + "Cannot decode empty Uint8Array", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 0", () => { + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b000_11100, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b000_11100)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b000_11101, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b000_11101)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b000_11110, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b000_11110)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b000_11111, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b000_11111)", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 1", () => { + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b001_11100, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b001_11100)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b001_11101, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b001_11101)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b001_11110, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b001_11110)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b001_11111, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b001_11111)", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 2", () => { + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b010_11100, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b010_11100)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b010_11101, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b010_11101)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b010_11110, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b010_11110)", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 3", () => { + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b011_11100, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b011_11100)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b011_11101, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b011_11101)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b011_11110, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b011_11110)", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 4", () => { + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b100_11100, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b100_11100)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b100_11101, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b100_11101)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b100_11110, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b100_11110)", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 5", () => { + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b101_11100, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b101_11100)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b101_11101, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b101_11101)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b101_11110, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b101_11110)", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 7", () => { + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b111_11100, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b111_11100)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b111_11101, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b111_11101)", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b111_11110, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b111_11110)", + ); +}); From 89e22a530ae67d5cbeb2d52201c30bda98e21576 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:52:46 +1000 Subject: [PATCH 24/45] tests(cbor): finished writing tests for `decodeCbor()`, hopefully --- cbor/decode.ts | 21 +++-- cbor/decode_test.ts | 194 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 206 insertions(+), 9 deletions(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index 0cb9ebc5e005..8c92ade26edf 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -114,7 +114,7 @@ export function decodeCbor(value: Uint8Array): CborType { while (byte !== 0b111_11111) { if (byte >> 5 !== 2) { throw new TypeError( - `Cannot decode value (b${ + `Cannot decode value (0b${ (byte >> 5).toString(2).padStart(3, "0") }_${ (byte & 0b11111).toString(2).padStart(5, "0") @@ -150,7 +150,7 @@ export function decodeCbor(value: Uint8Array): CborType { while (byte !== 0b111_11111) { if (byte >> 5 !== 3) { throw new TypeError( - `Cannot decode value (b${ + `Cannot decode value (0b${ (byte >> 5).toString(2).padStart(3, "0") }_${ (byte & 0b11111).toString(2).padStart(5, "0") @@ -161,7 +161,7 @@ export function decodeCbor(value: Uint8Array): CborType { const aI = byte & 0b11111; if (aI === 31) { throw new TypeError( - "Indefinite length text strings cannot contain definite length text strings", + "Indefinite length text strings cannot contain indefinite length text strings", ); } @@ -194,8 +194,10 @@ export function decodeCbor(value: Uint8Array): CborType { } if (aI === 31) { const array: CborType[] = []; + if (!source.length) throw new RangeError("More bytes were expected"); while (source[source.length - 1] !== 0b111_11111) { array.push(decode()); + if (!source.length) throw new RangeError("More bytes were expected"); } source.pop(); return array; @@ -222,7 +224,7 @@ export function decodeCbor(value: Uint8Array): CborType { const key = decode(); if (typeof key !== "string") { throw new TypeError( - `Cannot decode key of type "${typeof key}": This implementation only support "text string" keys`, + `Cannot decode key of type (${typeof key}): This implementation only supports "text string" keys`, ); } @@ -238,11 +240,12 @@ export function decodeCbor(value: Uint8Array): CborType { } if (aI === 31) { const object: { [k: string]: CborType } = {}; + if (!source.length) throw new RangeError("More bytes were expected"); while (source[source.length - 1] !== 0b111_11111) { const key = decode(); if (typeof key !== "string") { throw new TypeError( - `Cannot decode key of type "${typeof key}": This implementation only support "text string" keys`, + `Cannot decode key of type (${typeof key}): This implementation only supports "text string" keys`, ); } @@ -253,7 +256,10 @@ export function decodeCbor(value: Uint8Array): CborType { } object[key] = decode(); + console.log(source.length); + if (!source.length) throw new RangeError("More bytes were expected"); } + source.pop(); return object; } throw new RangeError( @@ -262,6 +268,11 @@ export function decodeCbor(value: Uint8Array): CborType { } function decodeSix(aI: number): Date | CborTag { + if (aI > 27) { + throw new RangeError( + `Cannot decode value (0b110_${aI.toString(2).padStart(5, "0")})`, + ); + } const tagNumber = decodeZero(aI); const tagContent = decode(); switch (BigInt(tagNumber)) { diff --git a/cbor/decode_test.ts b/cbor/decode_test.ts index 4228a3c73711..efb2a596dfe2 100644 --- a/cbor/decode_test.ts +++ b/cbor/decode_test.ts @@ -254,7 +254,7 @@ Deno.test("decodeCbor() rejecting majorType 1", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 2", () => { +Deno.test("decodeCbor() rejecting majorType 2 | Reserved Additional Information", () => { assertThrows( () => { decodeCbor( @@ -293,7 +293,38 @@ Deno.test("decodeCbor() rejecting majorType 2", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 3", () => { +Deno.test("decodeCbor() rejecting majorType 2 | Indefinite Byte String", () => { + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b010_11111])); + }, + RangeError, + "More bytes were expected", + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b010_11111, 0b000_00000])); + }, + TypeError, + "Cannot decode value (0b000_00000) inside an indefinite length byte string", + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b010_11111, 0b010_11111])); + }, + TypeError, + "Indefinite length byte strings cannot contain indefinite length byte strings", + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b010_11111, 0b010_00000])); + }, + RangeError, + "More bytes were expected", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 3 | Reserved Additional Information", () => { assertThrows( () => { decodeCbor( @@ -332,7 +363,38 @@ Deno.test("decodeCbor() rejecting majorType 3", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 4", () => { +Deno.test("decodeCbor() rejecting majorType 3 | Indefinite Text String", () => { + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b011_11111])); + }, + RangeError, + "More bytes were expected", + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b011_11111, 0b000_00000])); + }, + TypeError, + "Cannot decode value (0b000_00000) inside an indefinite length text string", + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b011_11111, 0b011_11111])); + }, + TypeError, + "Indefinite length text strings cannot contain indefinite length text strings", + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b011_11111, 0b011_00000])); + }, + RangeError, + "More bytes were expected", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 4 | Reserved Additional Information", () => { assertThrows( () => { decodeCbor( @@ -371,7 +433,24 @@ Deno.test("decodeCbor() rejecting majorType 4", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 5", () => { +Deno.test("decodeCbor() rejecting majorType 4 | Indefinite Arrays", () => { + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b100_11111])); + }, + RangeError, + "More bytes were expected", + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b100_11111, 0b000_00000])); + }, + RangeError, + "More bytes were expected", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 5 | Reserved Additional Information", () => { assertThrows( () => { decodeCbor( @@ -410,6 +489,113 @@ Deno.test("decodeCbor() rejecting majorType 5", () => { ); }); +Deno.test("decodeCbor() rejecting majorType 5 | Invalid Keys", () => { + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b101_00001, 0b000_00000, 0b000_00000])); + }, + TypeError, + 'Cannot decode key of type (number): This implementation only supports "text string" keys', + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b101_00010, + 0b011_00001, + 48, + 0b000_00000, + 0b011_00001, + 48, + 0b000_00001, + ]), + ); + }, + TypeError, + "A Map cannot have duplicate keys: Key (0) already exists", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 5 | Indefinite Maps", () => { + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b101_11111])); + }, + RangeError, + "More bytes were expected", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([0b101_11111, 0b000_00000, 0b000_00000, 0b111_11111]), + ); + }, + TypeError, + 'Cannot decode key of type (number): This implementation only supports "text string" keys', + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b101_11111, + 0b011_00001, + 48, + 0b000_00000, + 0b011_00001, + 48, + 0b000_00001, + 0b111_11111, + ]), + ); + }, + TypeError, + "A Map cannot have duplicate keys: Key (0) already exists", + ); + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b101_11111, + 0b011_00001, + 48, + 0b000_00000, + ]), + ); + }, + RangeError, + "More bytes were expected", + ); +}); + +Deno.test("decodeCbor() rejecting majorType 6", () => { + assertThrows( + () => { + decodeCbor( + Uint8Array.from([ + 0b110_11100, + ...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)), + ]), + ); + }, + RangeError, + "Cannot decode value (0b110_11100)", + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b110_00000, 0b000_00000])); + }, + TypeError, + 'Invalid TagItem: Expected a "text string"', + ); + assertThrows( + () => { + decodeCbor(Uint8Array.from([0b110_00001, 0b010_00000])); + }, + TypeError, + 'Invalid TagItem: Expected a "integer" or "float"', + ); +}); + Deno.test("decodeCbor() rejecting majorType 7", () => { assertThrows( () => { From 60ca9aebcda25497b44fcc1aac256ed08c080106 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:59:49 +1000 Subject: [PATCH 25/45] chore(cbor): remove floating `console.log` --- cbor/decode.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index 8c92ade26edf..369cdf5b510a 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -256,7 +256,6 @@ export function decodeCbor(value: Uint8Array): CborType { } object[key] = decode(); - console.log(source.length); if (!source.length) throw new RangeError("More bytes were expected"); } source.pop(); From bd93905372f3ff29ba3db937627a32d6e88c07fb Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:06:53 +1000 Subject: [PATCH 26/45] fix(cbor): new linting error --- cbor/decode_stream.ts | 14 +++++++------- cbor/encode_stream.ts | 40 ++++++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index fecc0fb08da6..03bb4c22b6c6 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -25,7 +25,7 @@ export type CborMapOutputStream = [string, CborOutputStream]; * is outputted from {@link CborSequenceDecoderStream}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -55,7 +55,7 @@ export class CborByteDecodedStream extends ReadableStream { * is outputted from {@link CborSequenceDecoderStream}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -86,7 +86,7 @@ export class CborTextDecodedStream extends ReadableStream { * {@link CborSequenceDecoderStream}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -117,7 +117,7 @@ export class CborArrayDecodedStream extends ReadableStream { * {@link CborSequenceDecoderStream}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -147,7 +147,7 @@ export class CborMapDecodedStream extends ReadableStream { * ReadableStream into a sequence of {@link CborOutputStream}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -457,7 +457,7 @@ export class CborSequenceDecoderStream * The ReadableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -471,7 +471,7 @@ export class CborSequenceDecoderStream * The WritableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * diff --git a/cbor/encode_stream.ts b/cbor/encode_stream.ts index 8339542acee9..55a9b1a46632 100644 --- a/cbor/encode_stream.ts +++ b/cbor/encode_stream.ts @@ -27,7 +27,7 @@ export type CborMapInputStream = [string, CborInputStream]; * "indefinite byte string". * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -66,7 +66,7 @@ export class CborByteEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -85,7 +85,7 @@ export class CborByteEncoderStream * The ReadableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -99,7 +99,7 @@ export class CborByteEncoderStream * The WritableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -115,7 +115,7 @@ export class CborByteEncoderStream * "indefinite text string". * * @example Usage - * ```ts no-eval + * ```ts ignore * ``` */ export class CborTextEncoderStream @@ -150,7 +150,7 @@ export class CborTextEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -169,7 +169,7 @@ export class CborTextEncoderStream * The ReadableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -183,7 +183,7 @@ export class CborTextEncoderStream * The WritableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -199,7 +199,7 @@ export class CborTextEncoderStream * "indefinite array". * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -231,7 +231,7 @@ export class CborArrayEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -250,7 +250,7 @@ export class CborArrayEncoderStream * The ReadableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -264,7 +264,7 @@ export class CborArrayEncoderStream * The WritableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -280,7 +280,7 @@ export class CborArrayEncoderStream * "indefinite map". * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -313,7 +313,7 @@ export class CborMapEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -334,7 +334,7 @@ export class CborMapEncoderStream * The ReadableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -348,7 +348,7 @@ export class CborMapEncoderStream * The WritableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -364,7 +364,7 @@ export class CborMapEncoderStream * a sequence of CBOR encoded values. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` */ @@ -485,7 +485,7 @@ export class CborSequenceEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -504,7 +504,7 @@ export class CborSequenceEncoderStream * The ReadableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * @@ -518,7 +518,7 @@ export class CborSequenceEncoderStream * The WritableStream property. * * @example Usage - * ```ts no-eval + * ```ts ignore * * ``` * From 1d2827dd140f908ccfc6983a6cae027ec1f7f68e Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:31:07 +1000 Subject: [PATCH 27/45] chore(cbor): clean up imports --- cbor/decode_stream.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index 03bb4c22b6c6..d07be8cb999f 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -1,7 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { arrayToNumber } from "./_common.ts"; -import { upgradeStreamFromGen } from "./_common.ts"; +import { arrayToNumber, upgradeStreamFromGen } from "./_common.ts"; import { type CborPrimitiveType, CborTag } from "./encode.ts"; /** From 7aec71dfe94e45b1c05f66fa31c1b5e1ceb1e8eb Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:09:24 +1000 Subject: [PATCH 28/45] tests(cbor): Completed the last of the tests for `decodeCbor()` --- cbor/decode_test.ts | 20 ++++++++++++++++++++ cbor/encode.ts | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cbor/decode_test.ts b/cbor/decode_test.ts index efb2a596dfe2..eb29daaccb28 100644 --- a/cbor/decode_test.ts +++ b/cbor/decode_test.ts @@ -43,6 +43,26 @@ Deno.test("decodeCbor() decoding integers", () => { num = random(2 ** 32, 2 ** 64); assertEquals(decodeCbor(encodeCbor(num)), BigInt(num)); assertEquals(decodeCbor(encodeCbor(BigInt(num))), BigInt(num)); + + num = -random(0, 24); + assertEquals(decodeCbor(encodeCbor(num)), num); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), num); + + num = -random(24, 2 ** 8); + assertEquals(decodeCbor(encodeCbor(num)), num); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), num); + + num = -random(2 ** 8, 2 ** 16); + assertEquals(decodeCbor(encodeCbor(num)), num); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), num); + + num = -random(2 ** 16, 2 ** 32); + assertEquals(decodeCbor(encodeCbor(num)), num); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), num); + + num = -random(2 ** 32, 2 ** 64); + assertEquals(decodeCbor(encodeCbor(num)), BigInt(num)); + assertEquals(decodeCbor(encodeCbor(BigInt(num))), BigInt(num)); }); Deno.test("decodeCbor() decoding strings", () => { diff --git a/cbor/encode.ts b/cbor/encode.ts index 903253358d4a..fda98f915b70 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -184,7 +184,8 @@ function encodeNumber(x: number): Uint8Array { return concat([new Uint8Array([majorType + 26]), numberToArray(4, x)]); } if (x < 2 ** 64) { - return concat([new Uint8Array([majorType + 27]), numberToArray(8, x)]); + // Due to possible precision loss with numbers this large, it's best to do conversion under BigInt or end up with 1n off. + return encodeBigInt(BigInt(isNegative ? -x - 1 : x)); } throw new RangeError( `Cannot encode number: It (${isNegative ? -x - 1 : x}) exceeds ${ @@ -199,7 +200,7 @@ function encodeBigInt(x: bigint): Uint8Array { const isNegative = x < 0n; if ((isNegative ? -x : x) < 2n ** 32n) return encodeNumber(Number(x)); - const head = new Uint8Array([x < 0n ? 0b010_11011 : 0b000_11011]); + const head = new Uint8Array([x < 0n ? 0b001_11011 : 0b000_11011]); if (isNegative) x = -x - 1n; if (x < 2n ** 64n) return concat([head, numberToArray(8, x)]); From 60bfa11ebda10ff08cf75925feca635dccb9803c Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:10:17 +1000 Subject: [PATCH 29/45] tests(cbor): Improved `CborTag()` test for `encodeCbor()` --- cbor/encode.ts | 21 ++++++++++----------- cbor/encode_test.ts | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/cbor/encode.ts b/cbor/encode.ts index fda98f915b70..0cdcd32605a5 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -284,19 +284,18 @@ function encodeObject(x: { [k: string]: CborType }): Uint8Array { } function encodeTag(x: CborTag) { - let head: number[]; - if (x.tagNumber < 24) head = [0b110_00000 + Number(x.tagNumber)]; - else if (x.tagNumber < 2 ** 8) head = [0b110_11000, Number(x.tagNumber)]; - else if (x.tagNumber < 2 ** 16) { - head = [0b110_11001, ...numberToArray(2, x.tagNumber)]; - } else if (x.tagNumber < 2 ** 32) { - head = [0b110_11010, ...numberToArray(4, x.tagNumber)]; - } else if (x.tagNumber < 2 ** 64) { - head = [0b110_11011, ...numberToArray(8, x.tagNumber)]; - } else { + const tagNumber = BigInt(x.tagNumber); + if (tagNumber < 0n) { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) is less than zero`, + ); + } + if (tagNumber > 2n ** 64n) { throw new RangeError( `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, ); } - return concat([Uint8Array.from(head), encodeCbor(x.tagContent)]); + const head = encodeBigInt(tagNumber); + head[0]! += 0b110_00000; + return concat([head, encodeCbor(x.tagContent)]); } diff --git a/cbor/encode_test.ts b/cbor/encode_test.ts index 1fb686864ed5..9e4a2e8d80a6 100644 --- a/cbor/encode_test.ts +++ b/cbor/encode_test.ts @@ -380,7 +380,20 @@ Deno.test("encodeCbor() rejecting bigints as Int", () => { }); Deno.test("encodeCbor() rejecting CborTag()", () => { - const num = 2 ** 65; + let num = -5; + assertThrows( + () => { + encodeCbor( + new CborTag( + num, + new Uint8Array(random(0, 24)).map((_) => random(0, 256)), + ), + ); + }, + RangeError, + `Cannot encode Tag Item: Tag Number (${num}) is less than zero`, + ); + num = 2 ** 65; assertThrows( () => { encodeCbor( From 4314b6c1813b29ed53345fc71e211d448ed42d58 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:41:21 +1000 Subject: [PATCH 30/45] feat(cbor): `encodeCborSequence()` --- cbor/encode.ts | 19 +++++++++++++++++++ cbor/encode_test.ts | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cbor/encode.ts b/cbor/encode.ts index 0cdcd32605a5..e672848bc887 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -169,6 +169,25 @@ export function encodeCbor(value: CborType): Uint8Array { return encodeObject(value); } +/** + * A function to encode JavaScript values into the CBOR sequence format based + * off the [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * spec. + * + * @example Usage + * ```ts ignore + * + * ``` + * + * @param values Values to encode to CBOR format. + * @returns Encoded CBOR data. + */ +export function encodeCborSequence(values: CborType[]): Uint8Array { + const output: Uint8Array[] = []; + for (const value of values) output.push(encodeCbor(value)); + return concat(output); +} + function encodeNumber(x: number): Uint8Array { if (x % 1 === 0) { const isNegative = x < 0; diff --git a/cbor/encode_test.ts b/cbor/encode_test.ts index 9e4a2e8d80a6..d3b227371a61 100644 --- a/cbor/encode_test.ts +++ b/cbor/encode_test.ts @@ -2,7 +2,7 @@ import { assertEquals, assertThrows } from "@std/assert"; import { concat } from "@std/bytes"; -import { CborTag, encodeCbor } from "./mod.ts"; +import { CborTag, encodeCbor, encodeCborSequence } from "./mod.ts"; function random(start: number, end: number): number { return Math.floor(Math.random() * (end - start) + start); @@ -407,3 +407,10 @@ Deno.test("encodeCbor() rejecting CborTag()", () => { `Cannot encode Tag Item: Tag Number (${num}) exceeds 2 ** 64 - 1`, ); }); + +Deno.test("encodeCborSequence()", () => { + assertEquals( + encodeCborSequence([0, 0]), + Uint8Array.from([0b000_00000, 0b000_00000]), + ); +}); From 84652765790c2c6a6a5e81597616242317583404 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:41:37 +1000 Subject: [PATCH 31/45] feat(cbor): `decodeCborSequence()` --- cbor/decode.ts | 499 +++++++++++++++++++++++--------------------- cbor/decode_test.ts | 9 +- 2 files changed, 268 insertions(+), 240 deletions(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index 369cdf5b510a..c27bd11dd279 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -5,7 +5,7 @@ import { arrayToNumber } from "./_common.ts"; import { CborTag, type CborType } from "./encode.ts"; /** - * A class to decode CBOR encoded {@link Uint8Array} into {@link CborType} + * A function to decode CBOR encoded {@link Uint8Array} into {@link CborType} * values, based off the * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * spec. @@ -38,280 +38,301 @@ import { CborTag, type CborType } from "./encode.ts"; export function decodeCbor(value: Uint8Array): CborType { if (!value.length) throw RangeError("Cannot decode empty Uint8Array"); const source = Array.from(value).reverse(); - return decode(); - function decode(): CborType { - const byte = source.pop(); - if (byte == undefined) throw new RangeError("More bytes were expected"); + return decode(source); +} - const majorType = byte >> 5; - const aI = byte & 0b000_11111; - switch (majorType) { - case 0: - return decodeZero(aI); - case 1: - return decodeOne(aI); - case 2: - return decodeTwo(aI); - case 3: - return decodeThree(aI); - case 4: - return decodeFour(aI); - case 5: - return decodeFive(aI); - case 6: - return decodeSix(aI); - default: // Only possible for it to be 7 - return decodeSeven(aI); - } +/** + * A function to decode CBOR sequence encoded {@link Uint8Array} into + * {@link CborType} values, based off the + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * spec. + * + * @example Usage + * ```ts ignore + * + * ``` + * + * @param value Value to decode from CBOR format. + * @returns Decoded CBOR data. + */ +export function decodeCborSequence(value: Uint8Array): CborType[] { + const output: CborType[] = []; + const source = Array.from(value).reverse(); + while (source.length) output.push(decode(source)); + return output; +} + +function decode(source: number[]): CborType { + const byte = source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); + + const majorType = byte >> 5; + const aI = byte & 0b000_11111; + switch (majorType) { + case 0: + return decodeZero(source, aI); + case 1: + return decodeOne(source, aI); + case 2: + return decodeTwo(source, aI); + case 3: + return decodeThree(source, aI); + case 4: + return decodeFour(source, aI); + case 5: + return decodeFive(source, aI); + case 6: + return decodeSix(source, aI); + default: // Only possible for it to be 7 + return decodeSeven(source, aI); } +} + +function decodeZero(source: number[], aI: number): number | bigint { + if (aI < 24) return aI; + if (aI <= 27) { + return arrayToNumber( + Uint8Array.from( + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ); + } + throw new RangeError( + `Cannot decode value (0b000_${aI.toString(2).padStart(5, "0")})`, + ); +} - function decodeZero(aI: number): number | bigint { - if (aI < 24) return aI; - if (aI <= 27) { - return arrayToNumber( +function decodeOne(source: number[], aI: number): number | bigint { + if (aI > 27) { + throw new RangeError( + `Cannot decode value (0b001_${aI.toString(2).padStart(5, "0")})`, + ); + } + const x = decodeZero(source, aI); + if (typeof x === "bigint") return -x - 1n; + return -x - 1; +} + +function decodeTwo(source: number[], aI: number): Uint8Array { + if (aI < 24) return Uint8Array.from(source.splice(-aI, aI).reverse()); + if (aI <= 27) { + // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // 2 ** 53 is the tipping point where integers loose precision. + const len = Number( + arrayToNumber( Uint8Array.from( source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), ).buffer, true, - ); - } - throw new RangeError( - `Cannot decode value (0b000_${aI.toString(2).padStart(5, "0")})`, + ), ); + return Uint8Array.from(source.splice(-len, len).reverse()); } + if (aI === 31) { + let byte = source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); - function decodeOne(aI: number): number | bigint { - if (aI > 27) { - throw new RangeError( - `Cannot decode value (0b001_${aI.toString(2).padStart(5, "0")})`, - ); - } - const x = decodeZero(aI); - if (typeof x === "bigint") return -x - 1n; - return -x - 1; - } - - function decodeTwo(aI: number): Uint8Array { - if (aI < 24) return Uint8Array.from(source.splice(-aI, aI).reverse()); - if (aI <= 27) { - // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. - // 2 ** 53 is the tipping point where integers loose precision. - const len = Number( - arrayToNumber( - Uint8Array.from( - source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), - ).buffer, - true, - ), - ); - return Uint8Array.from(source.splice(-len, len).reverse()); - } - if (aI === 31) { - let byte = source.pop(); - if (byte == undefined) throw new RangeError("More bytes were expected"); - - const output: Uint8Array[] = []; - while (byte !== 0b111_11111) { - if (byte >> 5 !== 2) { - throw new TypeError( - `Cannot decode value (0b${ - (byte >> 5).toString(2).padStart(3, "0") - }_${ - (byte & 0b11111).toString(2).padStart(5, "0") - }) inside an indefinite length byte string`, - ); - } - - const aI = byte & 0b11111; - if (aI === 31) { - throw new TypeError( - "Indefinite length byte strings cannot contain indefinite length byte strings", - ); - } + const output: Uint8Array[] = []; + while (byte !== 0b111_11111) { + if (byte >> 5 !== 2) { + throw new TypeError( + `Cannot decode value (0b${(byte >> 5).toString(2).padStart(3, "0")}_${ + (byte & 0b11111).toString(2).padStart(5, "0") + }) inside an indefinite length byte string`, + ); + } - output.push(decodeTwo(aI)); - byte = source.pop(); - if (byte == undefined) throw new RangeError("More bytes were expected"); + const aI = byte & 0b11111; + if (aI === 31) { + throw new TypeError( + "Indefinite length byte strings cannot contain indefinite length byte strings", + ); } - return concat(output); - } - throw new RangeError( - `Cannot decode value (0b010_${aI.toString(2).padStart(5, "0")})`, - ); - } - function decodeThree(aI: number): string { - if (aI <= 27) return new TextDecoder().decode(decodeTwo(aI)); - if (aI === 31) { - let byte = source.pop(); + output.push(decodeTwo(source, aI)); + byte = source.pop(); if (byte == undefined) throw new RangeError("More bytes were expected"); + } + return concat(output); + } + throw new RangeError( + `Cannot decode value (0b010_${aI.toString(2).padStart(5, "0")})`, + ); +} - const output: string[] = []; - while (byte !== 0b111_11111) { - if (byte >> 5 !== 3) { - throw new TypeError( - `Cannot decode value (0b${ - (byte >> 5).toString(2).padStart(3, "0") - }_${ - (byte & 0b11111).toString(2).padStart(5, "0") - }) inside an indefinite length text string`, - ); - } +function decodeThree(source: number[], aI: number): string { + if (aI <= 27) return new TextDecoder().decode(decodeTwo(source, aI)); + if (aI === 31) { + let byte = source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); - const aI = byte & 0b11111; - if (aI === 31) { - throw new TypeError( - "Indefinite length text strings cannot contain indefinite length text strings", - ); - } + const output: string[] = []; + while (byte !== 0b111_11111) { + if (byte >> 5 !== 3) { + throw new TypeError( + `Cannot decode value (0b${(byte >> 5).toString(2).padStart(3, "0")}_${ + (byte & 0b11111).toString(2).padStart(5, "0") + }) inside an indefinite length text string`, + ); + } - output.push(decodeThree(aI)); - byte = source.pop(); - if (byte == undefined) throw new RangeError("More bytes were expected"); + const aI = byte & 0b11111; + if (aI === 31) { + throw new TypeError( + "Indefinite length text strings cannot contain indefinite length text strings", + ); } - return output.join(""); + + output.push(decodeThree(source, aI)); + byte = source.pop(); + if (byte == undefined) throw new RangeError("More bytes were expected"); } - throw new RangeError( - `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, - ); + return output.join(""); } + throw new RangeError( + `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, + ); +} - function decodeFour(aI: number): CborType[] { - if (aI <= 27) { - const array: CborType[] = []; - // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. - // 2 ** 53 is the tipping point where integers loose precision. - const len = aI < 24 ? aI : Number( - arrayToNumber( - Uint8Array.from( - source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), - ).buffer, - true, - ), - ); - for (let i = 0; i < len; ++i) array.push(decode()); - return array; - } - if (aI === 31) { - const array: CborType[] = []; +function decodeFour(source: number[], aI: number): CborType[] { + if (aI <= 27) { + const array: CborType[] = []; + // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // 2 ** 53 is the tipping point where integers loose precision. + const len = aI < 24 ? aI : Number( + arrayToNumber( + Uint8Array.from( + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ), + ); + for (let i = 0; i < len; ++i) array.push(decode(source)); + return array; + } + if (aI === 31) { + const array: CborType[] = []; + if (!source.length) throw new RangeError("More bytes were expected"); + while (source[source.length - 1] !== 0b111_11111) { + array.push(decode(source)); if (!source.length) throw new RangeError("More bytes were expected"); - while (source[source.length - 1] !== 0b111_11111) { - array.push(decode()); - if (!source.length) throw new RangeError("More bytes were expected"); - } - source.pop(); - return array; } - throw new RangeError( - `Cannot decode value (0b100_${aI.toString(2).padStart(5, "0")})`, - ); + source.pop(); + return array; } + throw new RangeError( + `Cannot decode value (0b100_${aI.toString(2).padStart(5, "0")})`, + ); +} - function decodeFive(aI: number): { [k: string]: CborType } { - if (aI <= 27) { - const object: { [k: string]: CborType } = {}; - // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. - // 2 ** 53 is the tipping point where integers loose precision. - const len = aI < 24 ? aI : Number( - arrayToNumber( - Uint8Array.from( - source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), - ).buffer, - true, - ), - ); - for (let i = 0; i < len; ++i) { - const key = decode(); - if (typeof key !== "string") { - throw new TypeError( - `Cannot decode key of type (${typeof key}): This implementation only supports "text string" keys`, - ); - } - - if (object[key] !== undefined) { - throw new TypeError( - `A Map cannot have duplicate keys: Key (${key}) already exists`, - ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps - } +function decodeFive(source: number[], aI: number): { [k: string]: CborType } { + if (aI <= 27) { + const object: { [k: string]: CborType } = {}; + // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. + // 2 ** 53 is the tipping point where integers loose precision. + const len = aI < 24 ? aI : Number( + arrayToNumber( + Uint8Array.from( + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + true, + ), + ); + for (let i = 0; i < len; ++i) { + const key = decode(source); + if (typeof key !== "string") { + throw new TypeError( + `Cannot decode key of type (${typeof key}): This implementation only supports "text string" keys`, + ); + } - object[key] = decode(); + if (object[key] !== undefined) { + throw new TypeError( + `A Map cannot have duplicate keys: Key (${key}) already exists`, + ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps } - return object; - } - if (aI === 31) { - const object: { [k: string]: CborType } = {}; - if (!source.length) throw new RangeError("More bytes were expected"); - while (source[source.length - 1] !== 0b111_11111) { - const key = decode(); - if (typeof key !== "string") { - throw new TypeError( - `Cannot decode key of type (${typeof key}): This implementation only supports "text string" keys`, - ); - } - if (object[key] !== undefined) { - throw new TypeError( - `A Map cannot have duplicate keys: Key (${key}) already exists`, - ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps - } + object[key] = decode(source); + } + return object; + } + if (aI === 31) { + const object: { [k: string]: CborType } = {}; + if (!source.length) throw new RangeError("More bytes were expected"); + while (source[source.length - 1] !== 0b111_11111) { + const key = decode(source); + if (typeof key !== "string") { + throw new TypeError( + `Cannot decode key of type (${typeof key}): This implementation only supports "text string" keys`, + ); + } - object[key] = decode(); - if (!source.length) throw new RangeError("More bytes were expected"); + if (object[key] !== undefined) { + throw new TypeError( + `A Map cannot have duplicate keys: Key (${key}) already exists`, + ); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps } - source.pop(); - return object; + + object[key] = decode(source); + if (!source.length) throw new RangeError("More bytes were expected"); } + source.pop(); + return object; + } + throw new RangeError( + `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, + ); +} + +function decodeSix(source: number[], aI: number): Date | CborTag { + if (aI > 27) { throw new RangeError( - `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, + `Cannot decode value (0b110_${aI.toString(2).padStart(5, "0")})`, ); } - - function decodeSix(aI: number): Date | CborTag { - if (aI > 27) { - throw new RangeError( - `Cannot decode value (0b110_${aI.toString(2).padStart(5, "0")})`, - ); - } - const tagNumber = decodeZero(aI); - const tagContent = decode(); - switch (BigInt(tagNumber)) { - case 0n: - if (typeof tagContent !== "string") { - throw new TypeError('Invalid TagItem: Expected a "text string"'); - } - return new Date(tagContent); - case 1n: - if (typeof tagContent !== "number" && typeof tagContent !== "bigint") { - throw new TypeError( - 'Invalid TagItem: Expected a "integer" or "float"', - ); - } - return new Date(Number(tagContent) * 1000); - } - return new CborTag(tagNumber, tagContent); + const tagNumber = decodeZero(source, aI); + const tagContent = decode(source); + switch (BigInt(tagNumber)) { + case 0n: + if (typeof tagContent !== "string") { + throw new TypeError('Invalid TagItem: Expected a "text string"'); + } + return new Date(tagContent); + case 1n: + if (typeof tagContent !== "number" && typeof tagContent !== "bigint") { + throw new TypeError( + 'Invalid TagItem: Expected a "integer" or "float"', + ); + } + return new Date(Number(tagContent) * 1000); } + return new CborTag(tagNumber, tagContent); +} - function decodeSeven(aI: number): undefined | null | boolean | number { - switch (aI) { - case 20: - return false; - case 21: - return true; - case 22: - return null; - case 23: - return undefined; - } - if (25 <= aI && aI <= 27) { - return arrayToNumber( - Uint8Array.from( - source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), - ).buffer, - false, - ); - } - throw new RangeError( - `Cannot decode value (0b111_${aI.toString(2).padStart(5, "0")})`, +function decodeSeven( + source: number[], + aI: number, +): undefined | null | boolean | number { + switch (aI) { + case 20: + return false; + case 21: + return true; + case 22: + return null; + case 23: + return undefined; + } + if (25 <= aI && aI <= 27) { + return arrayToNumber( + Uint8Array.from( + source.splice(-(2 ** (aI - 24)), 2 ** (aI - 24)).reverse(), + ).buffer, + false, ); } + throw new RangeError( + `Cannot decode value (0b111_${aI.toString(2).padStart(5, "0")})`, + ); } diff --git a/cbor/decode_test.ts b/cbor/decode_test.ts index eb29daaccb28..4bed7697f9c7 100644 --- a/cbor/decode_test.ts +++ b/cbor/decode_test.ts @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertEquals, assertThrows } from "@std/assert"; -import { CborTag, decodeCbor, encodeCbor } from "./mod.ts"; +import { CborTag, decodeCbor, decodeCborSequence, encodeCbor } from "./mod.ts"; function random(start: number, end: number): number { return Math.floor(Math.random() * (end - start) + start); @@ -654,3 +654,10 @@ Deno.test("decodeCbor() rejecting majorType 7", () => { "Cannot decode value (0b111_11110)", ); }); + +Deno.test("decodeCborSequence()", () => { + assertEquals( + decodeCborSequence(Uint8Array.from([0b000_00000, 0b000_00000])), + [0, 0], + ); +}); From 1b88c9af4cbc1c5c62eae2685bffbb0e3aea76f7 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Fri, 20 Sep 2024 18:57:33 +1000 Subject: [PATCH 32/45] tests(cbor): Added more tests for CborEncoderStreams --- cbor/encode_stream.ts | 22 ++--- cbor/encode_stream_test.ts | 180 +++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 11 deletions(-) diff --git a/cbor/encode_stream.ts b/cbor/encode_stream.ts index 55a9b1a46632..c30087659351 100644 --- a/cbor/encode_stream.ts +++ b/cbor/encode_stream.ts @@ -461,21 +461,21 @@ export class CborSequenceEncoderStream } async *#encodeTag(x: CborTag): AsyncGenerator { - if (x.tagNumber < 24) { - yield new Uint8Array([0b110_00000 + Number(x.tagNumber)]); - } else if (x.tagNumber < 2 ** 8) { - yield new Uint8Array([0b110_11000, Number(x.tagNumber)]); - } else if (x.tagNumber < 2 ** 16) { - yield new Uint8Array([0b110_11001, ...numberToArray(2, x.tagNumber)]); - } else if (x.tagNumber < 2 ** 32) { - yield new Uint8Array([0b110_11010, ...numberToArray(4, x.tagNumber)]); - } else if (x.tagNumber < 2 ** 64) { - yield new Uint8Array([0b110_11011, ...numberToArray(8, x.tagNumber)]); - } else { + const tagNumber = BigInt(x.tagNumber); + if (tagNumber < 0n) { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) is less than zero`, + ); + } + if (tagNumber > 2n ** 64n) { throw new RangeError( `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, ); } + + const head = encodeCbor(tagNumber); + head[0]! = 0b110_00000; + yield head; for await (const y of this.#encode(x.tagContent)) { yield y; } diff --git a/cbor/encode_stream_test.ts b/cbor/encode_stream_test.ts index 74c757579bce..8b6516f138a6 100644 --- a/cbor/encode_stream_test.ts +++ b/cbor/encode_stream_test.ts @@ -8,6 +8,7 @@ import { CborMapEncoderStream, type CborMapInputStream, CborSequenceEncoderStream, + CborTag, CborTextEncoderStream, type CborType, encodeCbor, @@ -122,3 +123,182 @@ Deno.test("CborSequenceEncoderStream()", async () => { assertEquals(actualOutput, expectedOutput); }); + +Deno.test("CborByteEncoderStream.from()", async () => { + const bytes = [ + new Uint8Array(random(0, 24)), + new Uint8Array(random(24, 2 ** 8)), + new Uint8Array(random(2 ** 8, 2 ** 16)), + new Uint8Array(random(2 ** 16, 2 ** 17)), + ]; + + const expectedOutput = concat([ + Uint8Array.from([0b010_11111]), + ...bytes.map((x) => encodeCbor(x)), + Uint8Array.from([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + CborByteEncoderStream.from(bytes).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborTextEncoderStream.from()", async () => { + const strings = [ + "a".repeat(random(0, 24)), + "a".repeat(random(24, 2 ** 8)), + "a".repeat(random(2 ** 8, 2 ** 16)), + "a".repeat(random(2 ** 16, 2 ** 17)), + ]; + + const expectedOutput = concat([ + new Uint8Array([0b011_11111]), + ...strings.filter((x) => x).map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + CborTextEncoderStream.from(strings).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborArrayEncoderStream.from()", async () => { + const arrays = [random(0, 2 ** 32)]; + + const expectedOutput = concat([ + new Uint8Array([0b100_11111]), + ...arrays.map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + CborArrayEncoderStream.from(arrays).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborMapEncoderStream.from()", async () => { + const maps: CborMapInputStream[] = [["a", random(0, 2 ** 32)]]; + + const expectedOutput = concat([ + new Uint8Array([0b101_11111]), + ...maps.map(([k, v]) => [encodeCbor(k), encodeCbor(v as CborType)]).flat(), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + CborMapEncoderStream.from(maps).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream() accepting the other streams", async () => { + const input = [ + CborByteEncoderStream.from( + new Array(random(10, 20)).fill(0).map((_) => + new Uint8Array(random(0, 10)) + ), + ), + CborTextEncoderStream.from( + new Array(random(10, 20)).fill(0).map((_) => "a".repeat(random(0, 10))), + ), + CborArrayEncoderStream.from( + new Array(random(10, 20)).fill(0).map((_) => random(0, 10)), + ), + CborMapEncoderStream.from( + new Array(random(10, 20)).fill(0).map(( + _, + i, + ) => [String.fromCharCode(97 + i), true]), + ), + ] as const; + + const expectedOutput = concat( + await Promise.all( + input.map(async (stream) => + concat(await Array.fromAsync(stream.readable)) + ), + ), + ); + + const actualOutput = concat( + await Array.fromAsync( + CborSequenceEncoderStream.from(input).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream() accepting CborInputStream[]", async () => { + const input = [ + new Array(random(0, 24)).fill(0), + new Array(random(24, 2 ** 8)).fill(0), + new Array(random(2 ** 8, 2 ** 16)).fill(0), + new Array(random(2 ** 16, 2 ** 17)).fill(0), + ]; + + const expectedOutput = concat(input.map((x) => encodeCbor(x))); + + const actualOutput = concat( + await Array.fromAsync( + CborSequenceEncoderStream.from(input).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream() accepting { [k: string]: CborInputStream }", async () => { + const input = [ + Object.fromEntries( + new Array(random(10, 20)).fill(0).map(( + _, + i, + ) => [String.fromCharCode(97 + i), false]), + ), + ]; + + const expectedOutput = concat(input.map((x) => encodeCbor(x))); + + const actualOutput = concat( + await Array.fromAsync( + CborSequenceEncoderStream.from(input).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream() accepting CborTag()", async () => { + const input = [ + new CborTag(0, 0), + new CborTag(1, 1), + new CborTag(2, 2), + new CborTag(3, 3), + ]; + + const expectedOutput = concat(input.map((x) => encodeCbor(x))); + + const actualOutput = concat( + await Array.fromAsync( + CborSequenceEncoderStream.from(input).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); From 52aa06d45dbb080670f78b1074c48658210d02b1 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Fri, 20 Sep 2024 19:07:39 +1000 Subject: [PATCH 33/45] fix(cbor): broken tests --- cbor/encode_stream.ts | 2 +- cbor/encode_stream_test.ts | 36 +++++++++++++++--------------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/cbor/encode_stream.ts b/cbor/encode_stream.ts index c30087659351..38e427589b96 100644 --- a/cbor/encode_stream.ts +++ b/cbor/encode_stream.ts @@ -474,7 +474,7 @@ export class CborSequenceEncoderStream } const head = encodeCbor(tagNumber); - head[0]! = 0b110_00000; + head[0]! += 0b110_00000; yield head; for await (const y of this.#encode(x.tagContent)) { yield y; diff --git a/cbor/encode_stream_test.ts b/cbor/encode_stream_test.ts index 8b6516f138a6..c444cdbd0cf4 100644 --- a/cbor/encode_stream_test.ts +++ b/cbor/encode_stream_test.ts @@ -207,29 +207,23 @@ Deno.test("CborMapEncoderStream.from()", async () => { }); Deno.test("CborSequenceEncoderStream() accepting the other streams", async () => { - const input = [ - CborByteEncoderStream.from( - new Array(random(10, 20)).fill(0).map((_) => - new Uint8Array(random(0, 10)) - ), - ), - CborTextEncoderStream.from( - new Array(random(10, 20)).fill(0).map((_) => "a".repeat(random(0, 10))), - ), - CborArrayEncoderStream.from( - new Array(random(10, 20)).fill(0).map((_) => random(0, 10)), - ), - CborMapEncoderStream.from( - new Array(random(10, 20)).fill(0).map(( - _, - i, - ) => [String.fromCharCode(97 + i), true]), - ), - ] as const; + // Inputs should be identical. We need two of them as the contents will be consumed when calculating expectedOutput and actualOutput. + const input1 = [ + CborByteEncoderStream.from([new Uint8Array(10), new Uint8Array(20)]), + CborTextEncoderStream.from(["a".repeat(10), "b".repeat(20)]), + CborArrayEncoderStream.from([10, 20]), + CborMapEncoderStream.from([["a", 0], ["b", 1], ["c", 2], ["d", 3]]), + ]; + const input2 = [ + CborByteEncoderStream.from([new Uint8Array(10), new Uint8Array(20)]), + CborTextEncoderStream.from(["a".repeat(10), "b".repeat(20)]), + CborArrayEncoderStream.from([10, 20]), + CborMapEncoderStream.from([["a", 0], ["b", 1], ["c", 2], ["d", 3]]), + ]; const expectedOutput = concat( await Promise.all( - input.map(async (stream) => + input1.map(async (stream) => concat(await Array.fromAsync(stream.readable)) ), ), @@ -237,7 +231,7 @@ Deno.test("CborSequenceEncoderStream() accepting the other streams", async () => const actualOutput = concat( await Array.fromAsync( - CborSequenceEncoderStream.from(input).readable, + CborSequenceEncoderStream.from(input2).readable, ), ); From ceeece09b7bc0300c804927500757fb7d9e054ec Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Sat, 21 Sep 2024 20:24:57 +1000 Subject: [PATCH 34/45] tests(cbor): improved tests for decode_stream.ts --- cbor/decode_stream.ts | 56 +++++++++++-- cbor/decode_stream_test.ts | 164 +++++++++++++++++++++++++++++++++++-- 2 files changed, 205 insertions(+), 15 deletions(-) diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index d07be8cb999f..ad9f2667cd20 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -152,6 +152,7 @@ export class CborMapDecodedStream extends ReadableStream { */ export class CborSequenceDecoderStream implements TransformStream { + #locks = 0; #source: ReadableStreamBYOBReader; #readable: ReadableStream; #writable: WritableStream; @@ -178,6 +179,14 @@ export class CborSequenceDecoderStream this.#writable = writable; } + #isStream(x: CborOutputStream): boolean { + return x instanceof CborByteDecodedStream || + x instanceof CborTextDecodedStream || + x instanceof CborArrayDecodedStream || + x instanceof CborMapDecodedStream || + (x instanceof CborTag && this.#isStream(x.tagContent)); + } + async #read(bytes: number, expectMore: true): Promise; async #read( bytes: number, @@ -200,26 +209,38 @@ export class CborSequenceDecoderStream } async *#readGen(bytes: bigint): AsyncGenerator { - for (let i = 0n; i < bytes; i += 2n ** 32n) { - yield await this.#read(Math.min(Number(bytes - i), 2 ** 32), true); + ++this.#locks; + for (let i = 0n; i < bytes; i += 2n ** 16n) { + yield await this.#read(Math.min(Number(bytes - i), 2 ** 16), true); } + --this.#locks; } async *#readDefinite( items: number | bigint, ): AsyncGenerator { + ++this.#locks; items = BigInt(items); for (let i = 0n; i < items; ++i) { - yield await this.#decode( + const x = await this.#decode( arrayToNumber((await this.#read(1, true)).buffer, true) as number, ); + const lockID = this.#locks; + yield x; + if (this.#isStream(x)) { + while (lockID <= this.#locks) { + await new Promise((a) => setTimeout(a, 0)); + } + } } + --this.#locks; } async *#readIndefinite( denyInnerIndefinite: boolean, message?: string, ): AsyncGenerator { + ++this.#locks; while (true) { const byte = arrayToNumber( (await this.#read(1, true)).buffer, @@ -229,8 +250,16 @@ export class CborSequenceDecoderStream if (denyInnerIndefinite && (byte & 0b000_11111) === 31) { throw new TypeError(message); } - yield await this.#decode(byte); + const x = await this.#decode(byte); + const lockID = this.#locks; + yield x; + if (this.#isStream(x)) { + while (lockID <= this.#locks) { + await new Promise((a) => setTimeout(a, 0)); + } + } } + --this.#locks; } async *#decodeSequence(): AsyncGenerator { @@ -238,7 +267,14 @@ export class CborSequenceDecoderStream const value = await this.#read(1, false); if (value == undefined) return; // Since `value` is only 1 byte long, it will be of type `number` - yield this.#decode(arrayToNumber(value.buffer, true) as number); + const x = await this.#decode(arrayToNumber(value.buffer, true) as number); + const lockID = this.#locks; + yield x; + if (this.#isStream(x)) { + while (lockID <= this.#locks) { + await new Promise((a) => setTimeout(a, 0)); + } + } } } @@ -321,14 +357,15 @@ export class CborSequenceDecoderStream (await this.#read(2 ** (aI - 24), true)).buffer, true, ); - return typeof bytes === "bigint" + // Strings can't be as long as Uint8Arrays so a lower bound is set before switching to a stream. + return bytes > 2 ** 16 ? new CborTextDecodedStream(async function* (gen) { const decoder = new TextDecoder(); for await (const chunk of gen) { yield decoder.decode(chunk, { stream: true }); } - }(this.#readGen(bytes))) - : new TextDecoder().decode(await this.#read(bytes, true)); + }(this.#readGen(BigInt(bytes)))) + : new TextDecoder().decode(await this.#read(Number(bytes), true)); } if (aI === 31) { return new CborTextDecodedStream(async function* (gen) { @@ -361,6 +398,7 @@ export class CborSequenceDecoderStream `Cannot decode value (0b100_${aI.toString(2).padStart(5, "0")})`, ); } + async #decodeFive(aI: number): Promise { async function* convert( gen: AsyncGenerator, @@ -407,6 +445,7 @@ export class CborSequenceDecoderStream `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, ); } + async #decodeSix(aI: number): Promise> { const tagNumber = await this.#decodeZero(aI); const tagContent = await this.#decode( @@ -430,6 +469,7 @@ export class CborSequenceDecoderStream } return new CborTag(tagNumber, tagContent); } + async #decodeSeven(aI: number): Promise { switch (aI) { case 20: diff --git a/cbor/decode_stream_test.ts b/cbor/decode_stream_test.ts index 938621b618f3..081310e5121f 100644 --- a/cbor/decode_stream_test.ts +++ b/cbor/decode_stream_test.ts @@ -1,20 +1,34 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { assertEquals } from "@std/assert"; -import { CborSequenceDecoderStream, CborSequenceEncoderStream } from "./mod.ts"; +import { assert, assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { + CborArrayDecodedStream, + CborByteDecodedStream, + CborByteEncoderStream, + CborMapDecodedStream, + type CborMapOutputStream, + CborSequenceDecoderStream, + CborTag, + CborTextDecodedStream, + CborTextEncoderStream, + encodeCbor, + encodeCborSequence, +} from "./mod.ts"; function random(start: number, end: number): number { return Math.floor(Math.random() * (end - start) + start); } -Deno.test("CborSequenceDecoderStream()", async () => { +Deno.test("CborSequenceDecoderStream() decoding CborPrimitiveType", async () => { const input = [ undefined, null, true, false, + Math.random() * 10, random(0, 24), - BigInt(random(0, 24)), + -BigInt(random(2 ** 32, 2 ** 64)), "a".repeat(random(0, 24)), new Uint8Array(random(0, 24)), new Date(), @@ -22,9 +36,145 @@ Deno.test("CborSequenceDecoderStream()", async () => { assertEquals( await Array.fromAsync( - ReadableStream.from(input).pipeThrough(new CborSequenceEncoderStream()) - .pipeThrough(new CborSequenceDecoderStream()), + ReadableStream.from([encodeCborSequence(input)]).pipeThrough( + new CborSequenceDecoderStream(), + ), ), - input.map((x) => typeof x === "bigint" && x < 2n ** 32n ? Number(x) : x), + input, ); }); + +Deno.test("CborSequenceDecoderStream() decoding Indefinite Length Byte String", async () => { + const inputSize = 10; + + const reader = CborByteEncoderStream.from([ + new Uint8Array(inputSize), + new Uint8Array(inputSize * 2), + new Uint8Array(inputSize * 3), + ]).readable.pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborByteDecodedStream); + assertEquals(await Array.fromAsync(value), [ + new Uint8Array(inputSize), + new Uint8Array(inputSize * 2), + new Uint8Array(inputSize * 3), + ]); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); + +Deno.test("CborSequenceDecoderStream() decoding large Definite Length Byte String", async () => { + // Uint8Array needs to be 2 ** 32 bytes+ to be decoded via a CborByteDecodedStream. + const size = random(2 ** 32, 2 ** 33); + + const reader = ReadableStream.from([encodeCbor(new Uint8Array(size))]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborByteDecodedStream); + assertEquals(concat(await Array.fromAsync(value)).length, size); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); + +Deno.test("CborSequenceDecoderStream() decoding Indefinite Length Text String", async () => { + const inputSize = 10; + + const reader = CborTextEncoderStream.from([ + "a".repeat(inputSize), + "b".repeat(inputSize * 2), + "c".repeat(inputSize * 3), + ]).readable.pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborTextDecodedStream); + assertEquals(await Array.fromAsync(value), [ + "a".repeat(inputSize), + "b".repeat(inputSize * 2), + "c".repeat(inputSize * 3), + ]); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); + +Deno.test("CborSequenceDecoderStream() decoding large Definite Text Byte String", async () => { + // Strings need to be 2 ** 16 bytes+ to be decoded via a CborTextDecodedStream. + const size = random(2 ** 16, 2 ** 17); + + const reader = ReadableStream.from([ + encodeCbor( + new TextDecoder().decode(new Uint8Array(size).fill("a".charCodeAt(0))), + ), + ]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborTextDecodedStream); + assertEquals((await Array.fromAsync(value)).join(""), "a".repeat(size)); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); + +Deno.test("CborSequenceDecoderStream() decoding Arrays", async () => { + const size = random(0, 24); + + const reader = ReadableStream.from([encodeCbor(new Array(size).fill(0))]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborArrayDecodedStream); + assertEquals(await Array.fromAsync(value), new Array(size).fill(0)); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); + +Deno.test("CborSequenceDecoderStream() decoding Objects", async () => { + const size = random(0, 24); + const entries = new Array(size).fill(0).map((_, i) => + [String.fromCharCode(97 + i), i] satisfies CborMapOutputStream + ); + + const reader = ReadableStream.from([encodeCbor(Object.fromEntries(entries))]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborMapDecodedStream); + assertEquals(await Array.fromAsync(value), entries); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); + +Deno.test("CborSequenceDecoderStream() decoding CborTag()", async () => { + const tagNumber = 2; // Tag Number needs to be a value that will return a CborTag. + const size = random(0, 24); + + const reader = ReadableStream.from([ + encodeCbor(new CborTag(tagNumber, new Array(size).fill(0))), + ]).pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborTag); + assertEquals(value.tagNumber, tagNumber); + assert(value.tagContent instanceof CborArrayDecodedStream); + assertEquals( + await Array.fromAsync(value.tagContent), + new Array(size).fill(0), + ); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); From 6557f25906389e48d776f9adb25e99f3032bff99 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Sat, 21 Sep 2024 21:01:40 +1000 Subject: [PATCH 35/45] fix(cbor): decode_stream.ts when dealing with empty streams --- cbor/decode_stream.ts | 265 ++++++++++++++++++++++++++++-------------- 1 file changed, 177 insertions(+), 88 deletions(-) diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index ad9f2667cd20..3fff9c9e0b8a 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -19,6 +19,8 @@ export type CborOutputStream = */ export type CborMapOutputStream = [string, CborOutputStream]; +type ReleaseLock = (value?: unknown) => void; + /** * The CborByteDecodedStream is an extension of ReadableStream that * is outputted from {@link CborSequenceDecoderStream}. @@ -34,16 +36,19 @@ export class CborByteDecodedStream extends ReadableStream { * * @param gen A generator that yields the decoded CBOR byte string. */ - constructor(gen: AsyncGenerator) { + constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ async pull(controller) { const { done, value } = await gen.next(); - if (done) controller.close(); - else controller.enqueue(value); + if (done) { + releaseLock(); + controller.close(); + } else controller.enqueue(value); }, async cancel() { // deno-lint-ignore no-empty for await (const _ of gen) {} + releaseLock(); }, }); } @@ -64,16 +69,19 @@ export class CborTextDecodedStream extends ReadableStream { * * @param gen A generator that yields the decoded CBOR text string. */ - constructor(gen: AsyncGenerator) { + constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ async pull(controller) { const { done, value } = await gen.next(); - if (done) controller.close(); - else controller.enqueue(value); + if (done) { + releaseLock(); + controller.close(); + } else controller.enqueue(value); }, async cancel() { // deno-lint-ignore no-empty for await (const _ of gen) {} + releaseLock(); }, }); } @@ -95,16 +103,19 @@ export class CborArrayDecodedStream extends ReadableStream { * * @param gen A generator that yields the decoded CBOR array. */ - constructor(gen: AsyncGenerator) { + constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ async pull(controller) { const { done, value } = await gen.next(); - if (done) controller.close(); - else controller.enqueue(value); + if (done) { + releaseLock(); + controller.close(); + } else controller.enqueue(value); }, async cancel() { // deno-lint-ignore no-empty for await (const _ of gen) {} + releaseLock(); }, }); } @@ -126,16 +137,22 @@ export class CborMapDecodedStream extends ReadableStream { * * @param gen A generator that yields the decoded CBOR map. */ - constructor(gen: AsyncGenerator) { + constructor( + gen: AsyncGenerator, + releaseLock: ReleaseLock, + ) { super({ async pull(controller) { const { done, value } = await gen.next(); - if (done) controller.close(); - else controller.enqueue(value); + if (done) { + releaseLock(); + controller.close(); + } else controller.enqueue(value); }, async cancel() { // deno-lint-ignore no-empty for await (const _ of gen) {} + releaseLock(); }, }); } @@ -152,7 +169,6 @@ export class CborMapDecodedStream extends ReadableStream { */ export class CborSequenceDecoderStream implements TransformStream { - #locks = 0; #source: ReadableStreamBYOBReader; #readable: ReadableStream; #writable: WritableStream; @@ -209,38 +225,30 @@ export class CborSequenceDecoderStream } async *#readGen(bytes: bigint): AsyncGenerator { - ++this.#locks; for (let i = 0n; i < bytes; i += 2n ** 16n) { yield await this.#read(Math.min(Number(bytes - i), 2 ** 16), true); } - --this.#locks; } async *#readDefinite( items: number | bigint, ): AsyncGenerator { - ++this.#locks; items = BigInt(items); for (let i = 0n; i < items; ++i) { const x = await this.#decode( arrayToNumber((await this.#read(1, true)).buffer, true) as number, ); - const lockID = this.#locks; - yield x; - if (this.#isStream(x)) { - while (lockID <= this.#locks) { - await new Promise((a) => setTimeout(a, 0)); - } - } + if (x instanceof Array) { + yield x[0]; + await x[1]; + } else yield x; } - --this.#locks; } async *#readIndefinite( denyInnerIndefinite: boolean, message?: string, ): AsyncGenerator { - ++this.#locks; while (true) { const byte = arrayToNumber( (await this.#read(1, true)).buffer, @@ -251,15 +259,11 @@ export class CborSequenceDecoderStream throw new TypeError(message); } const x = await this.#decode(byte); - const lockID = this.#locks; - yield x; - if (this.#isStream(x)) { - while (lockID <= this.#locks) { - await new Promise((a) => setTimeout(a, 0)); - } - } + if (x instanceof Array) { + yield x[0]; + await x[1]; + } else yield x; } - --this.#locks; } async *#decodeSequence(): AsyncGenerator { @@ -268,17 +272,16 @@ export class CborSequenceDecoderStream if (value == undefined) return; // Since `value` is only 1 byte long, it will be of type `number` const x = await this.#decode(arrayToNumber(value.buffer, true) as number); - const lockID = this.#locks; - yield x; - if (this.#isStream(x)) { - while (lockID <= this.#locks) { - await new Promise((a) => setTimeout(a, 0)); - } - } + if (x instanceof Array) { + yield x[0]; + await x[1]; + } else yield x; } } - #decode(byte: number): Promise { + #decode( + byte: number, + ): Promise]> { const majorType = byte >> 5; const aI = byte & 0b000_11111; switch (majorType) { @@ -324,33 +327,50 @@ export class CborSequenceDecoderStream return typeof x === "bigint" ? -x - 1n : -x - 1; } - async #decodeTwo(aI: number): Promise { + async #decodeTwo( + aI: number, + ): Promise]> { if (aI < 24) return await this.#read(aI, true); if (aI <= 27) { const bytes = arrayToNumber( (await this.#read(2 ** (aI - 24), true)).buffer, true, ); - return typeof bytes === "bigint" - ? new CborByteDecodedStream(this.#readGen(bytes)) - : await this.#read(bytes, true); + if (typeof bytes === "bigint") { + let releaseLock: ReleaseLock = () => {}; + const lock = new Promise((x) => releaseLock = x); + return [ + new CborByteDecodedStream(this.#readGen(bytes), releaseLock), + lock, + ]; + } else return await this.#read(bytes, true); } if (aI === 31) { - return new CborByteDecodedStream(async function* (gen) { - for await (const x of gen) { - if (x instanceof Uint8Array) yield x; - else if (x instanceof CborByteDecodedStream) { - for await (const y of x) yield y; - } else throw new TypeError("Unexpected type in CBOR byte string"); - } - }(this.#readIndefinite(true, ""))); + let releaseLock: ReleaseLock = () => {}; + const lock = new Promise((x) => releaseLock = x); + return [ + new CborByteDecodedStream( + async function* (gen) { + for await (const x of gen) { + if (x instanceof Uint8Array) yield x; + else if (x instanceof CborByteDecodedStream) { + for await (const y of x) yield y; + } else throw new TypeError("Unexpected type in CBOR byte string"); + } + }(this.#readIndefinite(true, "")), + releaseLock, + ), + lock, + ]; } throw new RangeError( `Cannot decode value (0b010_${aI.toString(2).padStart(5, "0")})`, ); } - async #decodeThree(aI: number): Promise { + async #decodeThree( + aI: number, + ): Promise]> { if (aI < 24) return new TextDecoder().decode(await this.#read(aI, true)); if (aI <= 27) { const bytes = arrayToNumber( @@ -358,48 +378,88 @@ export class CborSequenceDecoderStream true, ); // Strings can't be as long as Uint8Arrays so a lower bound is set before switching to a stream. - return bytes > 2 ** 16 - ? new CborTextDecodedStream(async function* (gen) { - const decoder = new TextDecoder(); - for await (const chunk of gen) { - yield decoder.decode(chunk, { stream: true }); - } - }(this.#readGen(BigInt(bytes)))) - : new TextDecoder().decode(await this.#read(Number(bytes), true)); + if (bytes > 2 ** 16) { + let releaseLock: ReleaseLock = () => {}; + const lock = new Promise((x) => releaseLock = x); + return [ + new CborTextDecodedStream( + async function* (gen) { + const decoder = new TextDecoder(); + for await (const chunk of gen) { + yield decoder.decode(chunk, { stream: true }); + } + }(this.#readGen(BigInt(bytes))), + releaseLock, + ), + lock, + ]; + } else {return new TextDecoder().decode( + await this.#read(Number(bytes), true), + );} } if (aI === 31) { - return new CborTextDecodedStream(async function* (gen) { - for await (const x of gen) { - if (typeof x === "string") yield x; - else if (x instanceof CborTextDecodedStream) { - for await (const y of x) yield y; - } else throw new TypeError("Unexpected type in CBOR text string"); - } - }(this.#readIndefinite(true, ""))); + let releaseLock: ReleaseLock = () => {}; + const lock = new Promise((x) => releaseLock = x); + return [ + new CborTextDecodedStream( + async function* (gen) { + for await (const x of gen) { + if (typeof x === "string") yield x; + else if (x instanceof CborTextDecodedStream) { + for await (const y of x) yield y; + } else throw new TypeError("Unexpected type in CBOR text string"); + } + }(this.#readIndefinite(true, "")), + releaseLock, + ), + lock, + ]; } throw new RangeError( `Cannot decode value (0b011_${aI.toString(2).padStart(5, "0")})`, ); } - async #decodeFour(aI: number): Promise { - if (aI < 24) return new CborArrayDecodedStream(this.#readDefinite(aI)); + async #decodeFour( + aI: number, + ): Promise<[CborArrayDecodedStream, lock: Promise]> { + let releaseLock: ReleaseLock = () => {}; + const lock = new Promise((x) => releaseLock = x); + if (aI < 24) { + return [ + new CborArrayDecodedStream(this.#readDefinite(aI), releaseLock), + lock, + ]; + } if (aI <= 27) { - return new CborArrayDecodedStream( - this.#readDefinite( - arrayToNumber((await this.#read(2 ** (aI - 24), true)).buffer, true), + return [ + new CborArrayDecodedStream( + this.#readDefinite( + arrayToNumber( + (await this.#read(2 ** (aI - 24), true)).buffer, + true, + ), + ), + releaseLock, ), - ); + lock, + ]; } if (aI === 31) { - return new CborArrayDecodedStream(this.#readIndefinite(false)); + return [ + new CborArrayDecodedStream(this.#readIndefinite(false), releaseLock), + lock, + ]; } + releaseLock(); throw new RangeError( `Cannot decode value (0b100_${aI.toString(2).padStart(5, "0")})`, ); } - async #decodeFive(aI: number): Promise { + async #decodeFive( + aI: number, + ): Promise<[CborMapDecodedStream, lock: Promise]> { async function* convert( gen: AsyncGenerator, ): AsyncGenerator { @@ -423,30 +483,56 @@ export class CborSequenceDecoderStream } } + let releaseLock: ReleaseLock = () => {}; + const lock = new Promise((x) => releaseLock = x); if (aI < 24) { - return new CborMapDecodedStream(convert(this.#readDefinite(aI * 2))); + return [ + new CborMapDecodedStream( + convert(this.#readDefinite(aI * 2)), + releaseLock, + ), + lock, + ]; } if (aI <= 27) { - return new CborMapDecodedStream( - convert( - this.#readDefinite( - arrayToNumber( - (await this.#read(2 ** (aI - 24), true)).buffer, - true, + return [ + new CborMapDecodedStream( + convert( + this.#readDefinite( + arrayToNumber( + (await this.#read(2 ** (aI - 24), true)).buffer, + true, + ), ), ), + releaseLock, ), - ); + lock, + ]; } if (aI === 31) { - return new CborMapDecodedStream(convert(this.#readIndefinite(false))); + return [ + new CborMapDecodedStream( + convert(this.#readIndefinite(false)), + releaseLock, + ), + lock, + ]; } + releaseLock(); throw new RangeError( `Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`, ); } - async #decodeSix(aI: number): Promise> { + async #decodeSix( + aI: number, + ): Promise< + Date | CborTag | [ + CborTag, + lock: Promise, + ] + > { const tagNumber = await this.#decodeZero(aI); const tagContent = await this.#decode( arrayToNumber((await this.#read(1, true)).buffer, true) as number, @@ -467,6 +553,9 @@ export class CborSequenceDecoderStream } return new Date(Number(tagContent) * 1000); } + if (tagContent instanceof Array) { + return [new CborTag(tagNumber, tagContent[0]), tagContent[1]]; + } return new CborTag(tagNumber, tagContent); } From 8519017225447c8a6376371f37f299d4dcc8605e Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Sat, 21 Sep 2024 21:05:31 +1000 Subject: [PATCH 36/45] fix(cbor): docs --- cbor/decode_stream.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index 3fff9c9e0b8a..6a9dc26ab968 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -35,6 +35,7 @@ export class CborByteDecodedStream extends ReadableStream { * Constructs a new instance. * * @param gen A generator that yields the decoded CBOR byte string. + * @param releaseLock A function to call when the stream is finished. */ constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ @@ -68,6 +69,7 @@ export class CborTextDecodedStream extends ReadableStream { * Constructs a new instance. * * @param gen A generator that yields the decoded CBOR text string. + * @param releaseLock A function to call when the stream is finished. */ constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ @@ -102,6 +104,7 @@ export class CborArrayDecodedStream extends ReadableStream { * Constructs a new instance. * * @param gen A generator that yields the decoded CBOR array. + * @param releaseLock A function to call when the stream is finished. */ constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ @@ -136,6 +139,7 @@ export class CborMapDecodedStream extends ReadableStream { * Constructs a new instance. * * @param gen A generator that yields the decoded CBOR map. + * @param releaseLock A function to call when the stream is finished. */ constructor( gen: AsyncGenerator, From 718fd18280a12e72f994d3f5c0267470ca864c70 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Sat, 21 Sep 2024 22:41:34 +1000 Subject: [PATCH 37/45] docs(cbor): Filled in all @examples --- cbor/decode.ts | 18 +- cbor/decode_stream.ts | 303 +++++++++++++++++++- cbor/encode.ts | 24 +- cbor/encode_stream.ts | 629 ++++++++++++++++++++++++++++++++++++++++-- cbor/mod.ts | 13 +- 5 files changed, 947 insertions(+), 40 deletions(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index c27bd11dd279..fb89c6d8c195 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -48,8 +48,24 @@ export function decodeCbor(value: Uint8Array): CborType { * spec. * * @example Usage - * ```ts ignore + * ```ts + * import { assertEquals } from "@std/assert"; + * import { decodeCborSequence, encodeCborSequence } from "@std/cbor"; * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encodeCborSequence(rawMessage); + * const decodedMessage = decodeCborSequence(encodedMessage); + * + * assertEquals(decodedMessage, rawMessage); * ``` * * @param value Value to decode from CBOR format. diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index 6a9dc26ab968..81f12a9f2088 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -26,8 +26,27 @@ type ReleaseLock = (value?: unknown) => void; * is outputted from {@link CborSequenceDecoderStream}. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; * + * const rawMessage = new Uint8Array(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborByteEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } * ``` */ export class CborByteDecodedStream extends ReadableStream { @@ -60,8 +79,26 @@ export class CborByteDecodedStream extends ReadableStream { * is outputted from {@link CborSequenceDecoderStream}. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = "a".repeat(100); * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborTextEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } * ``` */ export class CborTextDecodedStream extends ReadableStream { @@ -95,8 +132,28 @@ export class CborTextDecodedStream extends ReadableStream { * {@link CborSequenceDecoderStream}. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborArrayEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } * ``` */ export class CborArrayDecodedStream extends ReadableStream { @@ -130,8 +187,31 @@ export class CborArrayDecodedStream extends ReadableStream { * {@link CborSequenceDecoderStream}. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; + * + * for await ( + * const value of ReadableStream.from(Object.entries(rawMessage)) + * .pipeThrough(new CborMapEncoderStream) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } * ``` */ export class CborMapDecodedStream extends ReadableStream { @@ -167,8 +247,75 @@ export class CborMapDecodedStream extends ReadableStream { * ReadableStream into a sequence of {@link CborOutputStream}. * * @example Usage - * ```ts ignore + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } * ``` */ export class CborSequenceDecoderStream @@ -199,14 +346,6 @@ export class CborSequenceDecoderStream this.#writable = writable; } - #isStream(x: CborOutputStream): boolean { - return x instanceof CborByteDecodedStream || - x instanceof CborTextDecodedStream || - x instanceof CborArrayDecodedStream || - x instanceof CborMapDecodedStream || - (x instanceof CborTag && this.#isStream(x.tagContent)); - } - async #read(bytes: number, expectMore: true): Promise; async #read( bytes: number, @@ -589,8 +728,75 @@ export class CborSequenceDecoderStream * The ReadableStream property. * * @example Usage - * ```ts ignore + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } * ``` * * @returns a ReadableStream. @@ -603,8 +809,75 @@ export class CborSequenceDecoderStream * The WritableStream property. * * @example Usage - * ```ts ignore + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } * ``` * * @returns a WritableStream. diff --git a/cbor/encode.ts b/cbor/encode.ts index e672848bc887..b3c5d16d93bc 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -49,7 +49,9 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * encodeBase64Url(rawMessage), * ), * ); + * * const decodedMessage = decodeCbor(encodedMessage); + * * assert(decodedMessage instanceof CborTag); * assert(typeof decodedMessage.tagContent === "string"); * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); @@ -75,7 +77,9 @@ export class CborTag { * encodeBase64Url(rawMessage), * ), * ); + * * const decodedMessage = decodeCbor(encodedMessage); + * * assert(decodedMessage instanceof CborTag); * assert(typeof decodedMessage.tagContent === "string"); * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); @@ -100,7 +104,9 @@ export class CborTag { * encodeBase64Url(rawMessage), * ), * ); + * * const decodedMessage = decodeCbor(encodedMessage); + * * assert(decodedMessage instanceof CborTag); * assert(typeof decodedMessage.tagContent === "string"); * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); @@ -175,8 +181,24 @@ export function encodeCbor(value: CborType): Uint8Array { * spec. * * @example Usage - * ```ts ignore + * ```ts + * import { assertEquals } from "@std/assert"; + * import { decodeCborSequence, encodeCborSequence } from "@std/cbor"; * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encodeCborSequence(rawMessage); + * const decodedMessage = decodeCborSequence(encodedMessage); + * + * assertEquals(decodedMessage, rawMessage); * ``` * * @param values Values to encode to CBOR format. diff --git a/cbor/encode_stream.ts b/cbor/encode_stream.ts index 38e427589b96..9817c3ae46d6 100644 --- a/cbor/encode_stream.ts +++ b/cbor/encode_stream.ts @@ -27,8 +27,27 @@ export type CborMapInputStream = [string, CborInputStream]; * "indefinite byte string". * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; * + * const rawMessage = new Uint8Array(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborByteEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } * ``` */ export class CborByteEncoderStream @@ -66,8 +85,27 @@ export class CborByteEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; + * + * const rawMessage = new Uint8Array(100); * + * for await ( + * const value of CborByteEncoderStream.from([rawMessage]) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } * ``` * * @param asyncIterable The iterable to convert to a {@link CborByteEncoderStream} instance. @@ -85,8 +123,27 @@ export class CborByteEncoderStream * The ReadableStream property. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; * + * const rawMessage = new Uint8Array(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborByteEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } * ``` * * @returns a ReadableStream. @@ -99,8 +156,27 @@ export class CborByteEncoderStream * The WritableStream property. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; + * + * const rawMessage = new Uint8Array(100); * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborByteEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } * ``` * * @returns a WritableStream. @@ -115,7 +191,26 @@ export class CborByteEncoderStream * "indefinite text string". * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = "a".repeat(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborTextEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } * ``` */ export class CborTextEncoderStream @@ -150,8 +245,26 @@ export class CborTextEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = "a".repeat(100); * + * for await ( + * const value of CborTextEncoderStream.from([rawMessage]) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } * ``` * * @param asyncIterable The iterable to convert to a {@link CborTextEncoderStream} instance. @@ -169,8 +282,26 @@ export class CborTextEncoderStream * The ReadableStream property. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; * + * const rawMessage = "a".repeat(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborTextEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } * ``` * * @returns a ReadableStream. @@ -183,8 +314,26 @@ export class CborTextEncoderStream * The WritableStream property. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = "a".repeat(100); * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborTextEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } * ``` * * @returns a WritableStream. @@ -199,8 +348,28 @@ export class CborTextEncoderStream * "indefinite array". * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborArrayEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } * ``` */ export class CborArrayEncoderStream @@ -231,8 +400,28 @@ export class CborArrayEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; + * + * for await ( + * const value of CborArrayEncoderStream.from(rawMessage) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } * ``` * * @param asyncIterable The iterable to convert to a {@link CborArrayEncoderStream} instance. @@ -250,8 +439,28 @@ export class CborArrayEncoderStream * The ReadableStream property. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborArrayEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } * ``` * * @returns a ReadableStream. @@ -264,8 +473,28 @@ export class CborArrayEncoderStream * The WritableStream property. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborArrayEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } * ``` * * @returns a WritableStream. @@ -280,8 +509,31 @@ export class CborArrayEncoderStream * "indefinite map". * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; + * + * for await ( + * const value of ReadableStream.from(Object.entries(rawMessage)) + * .pipeThrough(new CborMapEncoderStream) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } * ``` */ export class CborMapEncoderStream @@ -313,8 +565,31 @@ export class CborMapEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; + * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; * + * for await ( + * const value of CborMapEncoderStream.from(Object.entries(rawMessage)) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } * ``` * * @param asyncIterable The iterable to convert to a {@link CborMapEncoderStream} instance. @@ -334,8 +609,31 @@ export class CborMapEncoderStream * The ReadableStream property. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; + * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; * + * for await ( + * const value of ReadableStream.from(Object.entries(rawMessage)) + * .pipeThrough(new CborMapEncoderStream) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } * ``` * * @returns a ReadableStream. @@ -348,8 +646,31 @@ export class CborMapEncoderStream * The WritableStream property. * * @example Usage - * ```ts ignore + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "./mod.ts"; * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; + * + * for await ( + * const value of ReadableStream.from(Object.entries(rawMessage)) + * .pipeThrough(new CborMapEncoderStream) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } * ``` * * @returns a WritableStream. @@ -364,8 +685,75 @@ export class CborMapEncoderStream * a sequence of CBOR encoded values. * * @example Usage - * ```ts ignore + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } * ``` */ export class CborSequenceEncoderStream @@ -485,8 +873,75 @@ export class CborSequenceEncoderStream * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. * * @example Usage - * ```ts ignore + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } + * + * for await ( + * const value of CborSequenceEncoderStream.from(rawMessage) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } * ``` * * @param asyncIterable The iterable to convert to a {@link CborSequenceEncoderStream} instance. @@ -504,8 +959,75 @@ export class CborSequenceEncoderStream * The ReadableStream property. * * @example Usage - * ```ts ignore + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } * ``` * * @returns a ReadableStream. @@ -518,8 +1040,75 @@ export class CborSequenceEncoderStream * The WritableStream property. * * @example Usage - * ```ts ignore + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "./mod.ts"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } * ``` * * @returns a WritableStream. diff --git a/cbor/mod.ts b/cbor/mod.ts index fd43cff71222..a0e3aedb7957 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -7,10 +7,17 @@ * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * spec. * - * ```ts no-assert - * import { encodeCbor } from "@std/cbor"; + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { decodeCbor, encodeCbor } from "./mod.ts"; * - * console.log(encodeCbor(5)); + * const rawMessage = "I am a raw Message!"; + * + * const encodedMessage = encodeCbor(rawMessage); + * const decodedMessage = decodeCbor(encodedMessage); + * + * assert(typeof decodedMessage === "string"); + * assertEquals(decodedMessage, rawMessage); * ``` * * @module From 97b062238c0045d468ea1360d6814506b36ac2f8 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Sun, 22 Sep 2024 11:23:32 +1000 Subject: [PATCH 38/45] docs(cbor): Improved JSDocs for all exports. --- cbor/decode.ts | 38 ++++++++---- cbor/decode_stream.ts | 96 +++++++++++++++++++++--------- cbor/encode.ts | 49 ++++++++------- cbor/encode_stream.ts | 135 +++++++++++++++++++++++++++--------------- cbor/mod.ts | 18 +++++- 5 files changed, 225 insertions(+), 111 deletions(-) diff --git a/cbor/decode.ts b/cbor/decode.ts index fb89c6d8c195..bbf13ef9091d 100644 --- a/cbor/decode.ts +++ b/cbor/decode.ts @@ -5,10 +5,19 @@ import { arrayToNumber } from "./_common.ts"; import { CborTag, type CborType } from "./encode.ts"; /** - * A function to decode CBOR encoded {@link Uint8Array} into {@link CborType} - * values, based off the + * Decodes a CBOR-encoded {@link Uint8Array} into the JavaScript equivalent + * values represented as a {@link CborType}. * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * spec. + * + * **Limitations:** + * - While CBOR does support map keys of any type, this + * implementation only supports map keys being of type {@link string}, and will + * throw if detected decoding otherwise. + * - This decoder will throw if duplicate map keys are detected. This behaviour + * differentiates from {@link CborSequenceDecoderStream}. + * + * **Notice:** This decoder handles the tag numbers 0, and 1 automatically, all + * others returned are wrapped in a {@link CborTag} instance. * * @example Usage * ```ts @@ -32,8 +41,8 @@ import { CborTag, type CborType } from "./encode.ts"; * assertEquals(decodedMessage, rawMessage); * ``` * - * @param value Value to decode from CBOR format. - * @returns Decoded CBOR data. + * @param value The value to decode of type CBOR-encoded {@link Uint8Array}. + * @returns A {@link CborType} representing the decoded data. */ export function decodeCbor(value: Uint8Array): CborType { if (!value.length) throw RangeError("Cannot decode empty Uint8Array"); @@ -42,10 +51,18 @@ export function decodeCbor(value: Uint8Array): CborType { } /** - * A function to decode CBOR sequence encoded {@link Uint8Array} into - * {@link CborType} values, based off the + * Decodes a CBOR-sequence-encoded {@link Uint8Array} into the JavaScript + * equivalent values represented as a {@link CBorType} array. * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * spec. + * + * **Limitations:** + * - While CBOR does support map keys of any type, this implementation only + * supports map keys being of type {@link string}, and will throw if detected + * decoding otherwise. + * - This decoder will throw an error if duplicate keys are detected. + * + * **Notice:** This decoder handles the tag numbers 0, and 1 automatically, all + * others returned are wrapped in a {@link CborTag} instance. * * @example Usage * ```ts @@ -68,8 +85,9 @@ export function decodeCbor(value: Uint8Array): CborType { * assertEquals(decodedMessage, rawMessage); * ``` * - * @param value Value to decode from CBOR format. - * @returns Decoded CBOR data. + * @param value The value to decode of type CBOR-sequence-encoded + * {@link Uint8Array}. + * @returns A {@link CborType} array representing the decoded data. */ export function decodeCborSequence(value: Uint8Array): CborType[] { const output: CborType[] = []; diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index 81f12a9f2088..59e80e2d6f2a 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -4,8 +4,8 @@ import { arrayToNumber, upgradeStreamFromGen } from "./_common.ts"; import { type CborPrimitiveType, CborTag } from "./encode.ts"; /** - * This type specifies the decodable values for - * {@link CborSequenceDecoderStream}. + * Specifies the decodable value types for the {@link CborSequenceDecoderStream} + * and {@link CborMapDecodedStream}. */ export type CborOutputStream = | CborPrimitiveType @@ -14,16 +14,21 @@ export type CborOutputStream = | CborTextDecodedStream | CborArrayDecodedStream | CborMapDecodedStream; + /** - * This type specifies the structure of output for {@link CborMapDecodedStream}. + * Specifies the structure of the output for the {@link CborMapDecodedStream}. */ export type CborMapOutputStream = [string, CborOutputStream]; type ReleaseLock = (value?: unknown) => void; /** - * The CborByteDecodedStream is an extension of ReadableStream that - * is outputted from {@link CborSequenceDecoderStream}. + * A {@link ReadableStream} that wraps the decoded CBOR "Byte String". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * Instances of this class is created from {@link CborSequenceDecoderStream}. + * This class is not designed for you to create instances of it yourself. It is + * merely a way for you to validate the type being returned. * * @example Usage * ```ts @@ -53,8 +58,8 @@ export class CborByteDecodedStream extends ReadableStream { /** * Constructs a new instance. * - * @param gen A generator that yields the decoded CBOR byte string. - * @param releaseLock A function to call when the stream is finished. + * @param gen A {@link AsyncGenerator}. + * @param releaseLock A Function that's called when the stream is finished. */ constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ @@ -75,8 +80,12 @@ export class CborByteDecodedStream extends ReadableStream { } /** - * The CborTextDecodedStream is an extension of the ReadableStream that - * is outputted from {@link CborSequenceDecoderStream}. + * A {@link ReadableStream} that wraps the decoded CBOR "Text String". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * Instances of this class is created from {@link CborSequenceDecoderStream}. + * This class is not designed for you to create instances of it yourself. It is + * merely a way for you to validate the type being returned. * * @example Usage * ```ts @@ -105,8 +114,8 @@ export class CborTextDecodedStream extends ReadableStream { /** * Constructs a new instance. * - * @param gen A generator that yields the decoded CBOR text string. - * @param releaseLock A function to call when the stream is finished. + * @param gen A {@link AsyncGenerator}. + * @param releaseLock A Function that's called when the stream is finished. */ constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ @@ -127,9 +136,12 @@ export class CborTextDecodedStream extends ReadableStream { } /** - * The CborArrayDecodedStream is an extension of the - * ReadableStream that is outputted from - * {@link CborSequenceDecoderStream}. + * A {@link ReadableStream} that wraps the decoded CBOR "Array". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * Instances of this class is created from {@link CborSequenceDecoderStream}. + * This class is not designed for you to create instances of it yourself. It is + * merely a way for you to validate the type being returned. * * @example Usage * ```ts @@ -160,8 +172,8 @@ export class CborArrayDecodedStream extends ReadableStream { /** * Constructs a new instance. * - * @param gen A generator that yields the decoded CBOR array. - * @param releaseLock A function to call when the stream is finished. + * @param gen A {@link AsyncGenerator}. + * @param releaseLock A Function that's called when the stream is finished. */ constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { super({ @@ -182,9 +194,12 @@ export class CborArrayDecodedStream extends ReadableStream { } /** - * The CborMapDecodedStream is an extension of the - * ReadableStream that is outputted from - * {@link CborSequenceDecoderStream}. + * A {@link ReadableStream} that wraps the decoded CBOR "Map". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * Instances of this class is created from {@link CborSequenceDecoderStream}. + * This class is not designed for you to create instances of it yourself. It is + * merely a way for you to validate the type being returned. * * @example Usage * ```ts @@ -218,8 +233,8 @@ export class CborMapDecodedStream extends ReadableStream { /** * Constructs a new instance. * - * @param gen A generator that yields the decoded CBOR map. - * @param releaseLock A function to call when the stream is finished. + * @param gen A {@link AsyncGenerator}. + * @param releaseLock A Function that's called when the stream is finished. */ constructor( gen: AsyncGenerator, @@ -243,8 +258,33 @@ export class CborMapDecodedStream extends ReadableStream { } /** - * The CborSequenceDecoderStream decodes a CBOR encoded - * ReadableStream into a sequence of {@link CborOutputStream}. + * A {@link TransformStream} that decodes a CBOR-sequence-encoded + * {@link ReadableStream} into the JavaScript equivalent values + * represented as {@link ReadableStream}. + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * **Limitations:** + * - While CBOR does support map keys of any type, this implementation only + * supports map keys being of type {@link string}, and will throw if detected + * decoding otherwise. + * - This decoder does not validate that the encoded data is free of duplicate + * map keys, and will serve them all. This behaviour differentiates from + * {@link decodeCbor} and {@link decodeCborSequence}. + * - Arrays and Maps will always be decoded as a {@link CborArrayDecodedStream} + * and {@link CborMapDecodedStream}, respectively. + * - "Byte Strings" and "Text Strings" will be decoded as a + * {@link CborByteDecodedStream} and {@link CborTextDecodedStream}, + * respectively, if they are encoded as an "Indefinite Length String" or their + * "Definite Length" is 2 ** 32 and 2 ** 16, respectively, or greater. + * + * **Notice:** + * - This decoder handles the tag numbers 0, and 1 automatically, all + * others returned are wrapped in a {@link CborTag} instance. + * - If a parent stream yields {@link CborByteDecodedStream}, + * {@link CborTextDecodedStream}, {@link CborArrayDecodedStream}, + * {@link CborMapDecodedStream}, or {@link CborTag} (with any of these types as + * content), it will not resolve the next chunk until the yielded stream is + * fully consumed or canceled. * * @example Usage * ```ts no-assert @@ -725,7 +765,8 @@ export class CborSequenceDecoderStream } /** - * The ReadableStream property. + * The {@link ReadableStream} associated with the instance, + * which provides the decoded CBOR data as {@link CborOutputStream} chunks. * * @example Usage * ```ts no-assert @@ -799,14 +840,15 @@ export class CborSequenceDecoderStream * } * ``` * - * @returns a ReadableStream. + * @returns A {@link ReadableStream}. */ get readable(): ReadableStream { return this.#readable; } /** - * The WritableStream property. + * The {@link WritableStream} associated with the instance, + * which accepts {@link Uint8Array} chunks to be decoded from CBOR format. * * @example Usage * ```ts no-assert @@ -880,7 +922,7 @@ export class CborSequenceDecoderStream * } * ``` * - * @returns a WritableStream. + * @returns A {@link WritableStream}. */ get writable(): WritableStream { return this.#writable; diff --git a/cbor/encode.ts b/cbor/encode.ts index b3c5d16d93bc..79139375efba 100644 --- a/cbor/encode.ts +++ b/cbor/encode.ts @@ -20,21 +20,19 @@ export type CborPrimitiveType = | Date; /** - * This type specifies the encodable and decodable values for {@link encodeCbor} - * and {@link decodeCbor}. + * This type specifies the encodable and decodable values for + * {@link encodeCbor}, {@link decodeCbor}, {@link encodeCborSequence}, and + * {@link decodeCborSequence}. */ export type CborType = CborPrimitiveType | CborTag | CborType[] | { [k: string]: CborType; }; /** - * A class that wraps {@link CborType}, {@link CborInputStream}, and {@link CborOutputStream} values, - * assuming the `tagContent` is the appropriate type and format for encoding. - * A list of the different types can be found - * [here](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). + * Represents a CBOR tag, which pairs a tag number with content, used to convey + * additional semantic information in CBOR-encoded data. + * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). * - * This class will be returned out of {@link decodeCbor} if it doesn't automatically - * know how to handle the tag number. * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; @@ -57,12 +55,15 @@ export type CborType = CborPrimitiveType | CborTag | CborType[] | { * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); * ``` * - * @typeParam T extends {@link CborType} | {@link CborInputStream} | {@link CborOutputStream} + * @typeParam T The type of the tag's content, which can be a + * {@link CborType}, {@link CborInputStream}, or {@link CborOutputStream}. */ export class CborTag { /** - * The number indicating how the tagContent should be interpreted based off + * A {@link number} or {@link bigint} representing the CBOR tag number, used + * to identify the type of the tagged content. * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). + * * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; @@ -87,9 +88,9 @@ export class CborTag { */ tagNumber: number | bigint; /** - * The content wrapped around the tagNumber indicating how it should be - * interpreted based off + * The content associated with the tag of type {@link T}. * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). + * * @example Usage * ```ts * import { assert, assertEquals } from "@std/assert"; @@ -115,8 +116,10 @@ export class CborTag { tagContent: T; /** * Constructs a new instance. - * @param tagNumber The value to tag the {@link CborType} with. - * @param tagContent The {@link CborType} or {@link CborInputStream} formatted to the correct semantics. + * + * @param tagNumber A {@link number} or {@link bigint} representing the CBOR + * tag number, used to identify the type of the tagged content. + * @param tagContent The content associated with the tag of type {@link T}. */ constructor(tagNumber: number | bigint, tagContent: T) { this.tagNumber = tagNumber; @@ -125,9 +128,9 @@ export class CborTag { } /** - * A function to encode JavaScript values into the CBOR format based off the + * Encodes a {@link CborType} value into a CBOR format represented as a + * {@link Uint8Array}. * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * spec. * * @example Usage * ```ts @@ -151,8 +154,8 @@ export class CborTag { * assertEquals(decodedMessage, rawMessage); * ``` * - * @param value Value to encode to CBOR format. - * @returns Encoded CBOR data. + * @param value The value to encode of type {@link CborType}. + * @returns A {@link Uint8Array} representing the encoded data. */ export function encodeCbor(value: CborType): Uint8Array { switch (typeof value) { @@ -176,9 +179,9 @@ export function encodeCbor(value: CborType): Uint8Array { } /** - * A function to encode JavaScript values into the CBOR sequence format based - * off the [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * spec. + * Encodes an array of {@link CborType} values into a CBOR format sequence + * represented as a {@link Uint8Array}. + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * * @example Usage * ```ts @@ -201,8 +204,8 @@ export function encodeCbor(value: CborType): Uint8Array { * assertEquals(decodedMessage, rawMessage); * ``` * - * @param values Values to encode to CBOR format. - * @returns Encoded CBOR data. + * @param values An array of values to encode of type {@link CborType} + * @returns A {@link Uint8Array} representing the encoded data. */ export function encodeCborSequence(values: CborType[]): Uint8Array { const output: Uint8Array[] = []; diff --git a/cbor/encode_stream.ts b/cbor/encode_stream.ts index 9817c3ae46d6..c5bae1b53aca 100644 --- a/cbor/encode_stream.ts +++ b/cbor/encode_stream.ts @@ -4,8 +4,8 @@ import { numberToArray, upgradeStreamFromGen } from "./_common.ts"; import { type CborPrimitiveType, CborTag, encodeCbor } from "./encode.ts"; /** - * This type specifies the encodable values for - * {@link CborSequenceEncoderStream} and {@link CborArrayEncoderStream}. + * Specifies the encodable value types for the {@link CborSequenceEncoderStream} + * and {@link CborArrayEncoderStream}. */ export type CborInputStream = | CborPrimitiveType @@ -18,13 +18,18 @@ export type CborInputStream = | CborMapEncoderStream; /** - * This type specifies the structure of input for {@link CborMapEncoderStream}. + * Specifies the structure of input for the {@link CborMapEncoderStream}. */ export type CborMapInputStream = [string, CborInputStream]; /** - * The CborByteEncoderStream encodes a ReadableStream into a CBOR - * "indefinite byte string". + * A {@link TransformStream} that encodes a {@link ReadableStream} + * into CBOR "Indefinite Length Byte String". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * **Notice:** Each chunk of the {@link ReadableStream} is encoded + * as its own "Definite Length Byte String" meaning space can be saved if large + * chunks are pipped through instead of small chunks. * * @example Usage * ```ts @@ -82,7 +87,8 @@ export class CborByteEncoderStream } /** - * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * Creates a {@link CborByteEncoderStream} instance from an iterable of + * {@link Uint8Array} chunks. * * @example Usage * ```ts @@ -108,8 +114,9 @@ export class CborByteEncoderStream * } * ``` * - * @param asyncIterable The iterable to convert to a {@link CborByteEncoderStream} instance. - * @returns a {@link CborByteEncoderStream} instance. + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or {@link Iterable}. + * @returns A {@link CborByteEncoderStream} instance of the encoded data. */ static from( asyncIterable: AsyncIterable | Iterable, @@ -120,7 +127,8 @@ export class CborByteEncoderStream } /** - * The ReadableStream property. + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. * * @example Usage * ```ts @@ -146,14 +154,15 @@ export class CborByteEncoderStream * } * ``` * - * @returns a ReadableStream. + * @returns A {@link ReadableStream}. */ get readable(): ReadableStream { return this.#readable; } /** - * The WritableStream property. + * The {@link WritableStream} associated with the instance, which + * accepts {@link Uint8Array} chunks to be encoded into CBOR format. * * @example Usage * ```ts @@ -179,7 +188,7 @@ export class CborByteEncoderStream * } * ``` * - * @returns a WritableStream. + * @returns a {@link WritableStream}. */ get writable(): WritableStream { return this.#writable; @@ -187,8 +196,13 @@ export class CborByteEncoderStream } /** - * The CborTextEncoderStream encodes a ReadableStream into a CBOR - * "indefinite text string". + * A {@link TransformStream} that encodes a {@link ReadableStream} into + * CBOR "Indefinite Length Text String". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * **Notice:** Each chunk of the {@link ReadableStream} is encoded as + * its own "Definite Length Text String" meaning space can be saved if large + * chunks are pipped through instead of small chunks. * * @example Usage * ```ts @@ -242,7 +256,8 @@ export class CborTextEncoderStream } /** - * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * Creates a {@link CborTextEncoderStream} instance from an iterable of + * {@link string} chunks. * * @example Usage * ```ts @@ -267,8 +282,9 @@ export class CborTextEncoderStream * } * ``` * - * @param asyncIterable The iterable to convert to a {@link CborTextEncoderStream} instance. - * @returns a {@link CborTextEncoderStream} instance. + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or {@link Iterable} + * @returns A {@link CborTextEncoderStream} instance of the encoded data. */ static from( asyncIterable: AsyncIterable | Iterable, @@ -279,7 +295,8 @@ export class CborTextEncoderStream } /** - * The ReadableStream property. + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. * * @example Usage * ```ts @@ -304,14 +321,15 @@ export class CborTextEncoderStream * } * ``` * - * @returns a ReadableStream. + * @returns A {@link ReadableStream}. */ get readable(): ReadableStream { return this.#readable; } /** - * The WritableStream property. + * The {@link WritableStream} associated with the instance, which + * accepts {@link string} chunks to be encoded into CBOR format. * * @example Usage * ```ts @@ -336,7 +354,7 @@ export class CborTextEncoderStream * } * ``` * - * @returns a WritableStream. + * @returns A {@link WritableStream}. */ get writable(): WritableStream { return this.#writable; @@ -344,8 +362,9 @@ export class CborTextEncoderStream } /** - * The CborArrayEncoderStream encodes a ReadableStream into a CBOR - * "indefinite array". + * A {@link TransformStream} that encodes a + * {@link ReadableStream} into CBOR "Indefinite Length Array". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * * @example Usage * ```ts @@ -397,7 +416,8 @@ export class CborArrayEncoderStream } /** - * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * Creates a {@link CborArrayEncoderStream} instance from an iterable of + * {@link CborInputStream} chunks. * * @example Usage * ```ts @@ -424,8 +444,10 @@ export class CborArrayEncoderStream * } * ``` * - * @param asyncIterable The iterable to convert to a {@link CborArrayEncoderStream} instance. - * @returns a {@link CborArrayEncoderStream} instance. + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or + * {@link Iterable}. + * @returns A {@link CborArrayEncoderStream} instance of the encoded data. */ static from( asyncIterable: AsyncIterable | Iterable, @@ -436,7 +458,8 @@ export class CborArrayEncoderStream } /** - * The ReadableStream property. + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. * * @example Usage * ```ts @@ -463,14 +486,16 @@ export class CborArrayEncoderStream * } * ``` * - * @returns a ReadableStream. + * @returns A {@link ReadableStream}. */ get readable(): ReadableStream { return this.#readable; } /** - * The WritableStream property. + * The {@link WritableStream} associated with the instance, + * which accepts {@link CborInputStream} chunks to be encoded into CBOR + * format. * * @example Usage * ```ts @@ -497,7 +522,7 @@ export class CborArrayEncoderStream * } * ``` * - * @returns a WritableStream. + * @returns A {@link WritableStream}. */ get writable(): WritableStream { return this.#writable; @@ -505,8 +530,9 @@ export class CborArrayEncoderStream } /** - * The CborByteEncoderStream encodes a ReadableStream into a CBOR - * "indefinite map". + * A {@link TransformStream} that encodes a + * {@link ReadableStream} into CBOR "Indefinite Length Map". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * * @example Usage * ```ts @@ -562,7 +588,8 @@ export class CborMapEncoderStream } /** - * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * Creates a {@link CborMapEncoderStream} instance from an iterable of + * {@link CborMapInputStream} chunks. * * @example Usage * ```ts @@ -592,8 +619,10 @@ export class CborMapEncoderStream * } * ``` * - * @param asyncIterable The iterable to convert to a {@link CborMapEncoderStream} instance. - * @returns a {@link CborMapEncoderStream} instance. + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or + * {@link Iterable}. + * @returns A {@link CborMapEncoderStream} instance of the encoded data. */ static from( asyncIterable: @@ -606,7 +635,8 @@ export class CborMapEncoderStream } /** - * The ReadableStream property. + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. * * @example Usage * ```ts @@ -636,14 +666,16 @@ export class CborMapEncoderStream * } * ``` * - * @returns a ReadableStream. + * @returns A {@link ReadableStream}. */ get readable(): ReadableStream { return this.#readable; } /** - * The WritableStream property. + * The {@link WritableStream} associated with the + * instance, which accepts {@link CborMapInputStream} chunks to be encoded + * into CBOR format. * * @example Usage * ```ts @@ -673,7 +705,7 @@ export class CborMapEncoderStream * } * ``` * - * @returns a WritableStream. + * @returns A {@link WritableStream}. */ get writable(): WritableStream { return this.#writable; @@ -681,8 +713,9 @@ export class CborMapEncoderStream } /** - * The CborSequenceEncoderStream encodes a ReadableStream into - * a sequence of CBOR encoded values. + * A {@link TransformStream} that encodes a + * {@link ReadableStream} into CBOR format sequence. + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * * @example Usage * ```ts no-assert @@ -870,7 +903,8 @@ export class CborSequenceEncoderStream } /** - * Derives a new instance from an {@link AsyncIterable} or {@link Iterable}. + * Creates a {@link CborSequenceEncoderStream} instance from an iterable of + * {@link CborInputStream} chunks. * * @example Usage * ```ts no-assert @@ -944,8 +978,10 @@ export class CborSequenceEncoderStream * } * ``` * - * @param asyncIterable The iterable to convert to a {@link CborSequenceEncoderStream} instance. - * @returns a {@link CborSequenceEncoderStream} instance. + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or + * {@link Iterable}. + * @returns A {@link CborSequenceEncoderStream} instance of the encoded data. */ static from( asyncIterable: AsyncIterable | Iterable, @@ -956,7 +992,8 @@ export class CborSequenceEncoderStream } /** - * The ReadableStream property. + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. * * @example Usage * ```ts no-assert @@ -1030,14 +1067,16 @@ export class CborSequenceEncoderStream * } * ``` * - * @returns a ReadableStream. + * @returns A {@link ReadableStream}. */ get readable(): ReadableStream { return this.#readable; } /** - * The WritableStream property. + * The {@link WritableStream} associated with the + * instance, which accepts {@link CborInputStream} chunks to be encoded + * into CBOR format. * * @example Usage * ```ts no-assert @@ -1111,7 +1150,7 @@ export class CborSequenceEncoderStream * } * ``` * - * @returns a WritableStream. + * @returns A {@link WritableStream}. */ get writable(): WritableStream { return this.#writable; diff --git a/cbor/mod.ts b/cbor/mod.ts index a0e3aedb7957..e2da4ab5502f 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -1,12 +1,24 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. /** - * CBOR is a binary serialisation format that is language agnostic. It is like - * JSON, but allows more a much more wide range of values, and supports - * streaming. This implementation is based off the + * ## Overview + * Concise Binary Object Representation (CBOR) is a binary data serialisation + * format optimised for compactness and efficiency. It is designed to encode a + * wide range of data types, including integers, strings, arrays, and maps, in a + * space-efficient manner. * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * spec. * + * ## Limitations + * - This implementation only supports the encoding and decoding of + * "Text String" keys. + * - This implementation encodes decimal numbers with 64 bits. It takes no + * effort to figure out if the decimal can be encoded with 32 or 16 bits. + * - When decoding, integers with a value below 2 ** 32 will be of type + * {@link number}, with all larger integers being of type {@link bigint}. + * + * Functions and classes may have more specific limitations listed. + * * ```ts * import { assert, assertEquals } from "@std/assert"; * import { decodeCbor, encodeCbor } from "./mod.ts"; From 4cb87429e1724e0531c7fcfc751314baef42d73a Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Sun, 22 Sep 2024 11:45:09 +1000 Subject: [PATCH 39/45] docs(cbor): fix import statment in examples --- cbor/decode_stream.ts | 20 +++++++++++--------- cbor/encode_stream.ts | 40 ++++++++++++++++++++-------------------- cbor/mod.ts | 2 +- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/cbor/decode_stream.ts b/cbor/decode_stream.ts index 59e80e2d6f2a..4f5a625a0a72 100644 --- a/cbor/decode_stream.ts +++ b/cbor/decode_stream.ts @@ -38,7 +38,7 @@ type ReleaseLock = (value?: unknown) => void; * CborByteDecodedStream, * CborByteEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = new Uint8Array(100); * @@ -94,7 +94,7 @@ export class CborByteDecodedStream extends ReadableStream { * CborSequenceDecoderStream, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = "a".repeat(100); * @@ -150,7 +150,7 @@ export class CborTextDecodedStream extends ReadableStream { * CborArrayDecodedStream, * CborArrayEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * @@ -208,7 +208,7 @@ export class CborArrayDecodedStream extends ReadableStream { * CborMapDecodedStream, * CborMapEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage: Record = { * a: 0, @@ -302,7 +302,7 @@ export class CborMapDecodedStream extends ReadableStream { * CborTag, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = [ * undefined, @@ -576,9 +576,11 @@ export class CborSequenceDecoderStream ), lock, ]; - } else {return new TextDecoder().decode( + } else { + return new TextDecoder().decode( await this.#read(Number(bytes), true), - );} + ); + } } if (aI === 31) { let releaseLock: ReleaseLock = () => {}; @@ -784,7 +786,7 @@ export class CborSequenceDecoderStream * CborTag, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = [ * undefined, @@ -866,7 +868,7 @@ export class CborSequenceDecoderStream * CborTag, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = [ * undefined, diff --git a/cbor/encode_stream.ts b/cbor/encode_stream.ts index c5bae1b53aca..d723aeef8e13 100644 --- a/cbor/encode_stream.ts +++ b/cbor/encode_stream.ts @@ -39,7 +39,7 @@ export type CborMapInputStream = [string, CborInputStream]; * CborByteDecodedStream, * CborByteEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = new Uint8Array(100); * @@ -98,7 +98,7 @@ export class CborByteEncoderStream * CborByteDecodedStream, * CborByteEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = new Uint8Array(100); * @@ -138,7 +138,7 @@ export class CborByteEncoderStream * CborByteDecodedStream, * CborByteEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = new Uint8Array(100); * @@ -172,7 +172,7 @@ export class CborByteEncoderStream * CborByteDecodedStream, * CborByteEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = new Uint8Array(100); * @@ -211,7 +211,7 @@ export class CborByteEncoderStream * CborSequenceDecoderStream, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = "a".repeat(100); * @@ -266,7 +266,7 @@ export class CborTextEncoderStream * CborSequenceDecoderStream, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = "a".repeat(100); * @@ -305,7 +305,7 @@ export class CborTextEncoderStream * CborSequenceDecoderStream, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = "a".repeat(100); * @@ -338,7 +338,7 @@ export class CborTextEncoderStream * CborSequenceDecoderStream, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = "a".repeat(100); * @@ -373,7 +373,7 @@ export class CborTextEncoderStream * CborArrayDecodedStream, * CborArrayEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * @@ -426,7 +426,7 @@ export class CborArrayEncoderStream * CborArrayDecodedStream, * CborArrayEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * @@ -468,7 +468,7 @@ export class CborArrayEncoderStream * CborArrayDecodedStream, * CborArrayEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * @@ -504,7 +504,7 @@ export class CborArrayEncoderStream * CborArrayDecodedStream, * CborArrayEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; * @@ -541,7 +541,7 @@ export class CborArrayEncoderStream * CborMapDecodedStream, * CborMapEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage: Record = { * a: 0, @@ -598,7 +598,7 @@ export class CborMapEncoderStream * CborMapDecodedStream, * CborMapEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage: Record = { * a: 0, @@ -645,7 +645,7 @@ export class CborMapEncoderStream * CborMapDecodedStream, * CborMapEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage: Record = { * a: 0, @@ -684,7 +684,7 @@ export class CborMapEncoderStream * CborMapDecodedStream, * CborMapEncoderStream, * CborSequenceDecoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage: Record = { * a: 0, @@ -733,7 +733,7 @@ export class CborMapEncoderStream * CborTag, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = [ * undefined, @@ -922,7 +922,7 @@ export class CborSequenceEncoderStream * CborTag, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = [ * undefined, @@ -1011,7 +1011,7 @@ export class CborSequenceEncoderStream * CborTag, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = [ * undefined, @@ -1094,7 +1094,7 @@ export class CborSequenceEncoderStream * CborTag, * CborTextDecodedStream, * CborTextEncoderStream, - * } from "./mod.ts"; + * } from "@std/cbor"; * * const rawMessage = [ * undefined, diff --git a/cbor/mod.ts b/cbor/mod.ts index e2da4ab5502f..eee6b2e3b6f4 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -21,7 +21,7 @@ * * ```ts * import { assert, assertEquals } from "@std/assert"; - * import { decodeCbor, encodeCbor } from "./mod.ts"; + * import { decodeCbor, encodeCbor } from "@std/cbor"; * * const rawMessage = "I am a raw Message!"; * From ec405c515e8d170eba0604bb7f700cbf80eae598 Mon Sep 17 00:00:00 2001 From: Doctor <44320105+BlackAsLight@users.noreply.github.com> Date: Tue, 24 Sep 2024 06:10:41 +1000 Subject: [PATCH 40/45] fix(cbor): deno.json missing export & bumped minor version --- cbor/deno.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cbor/deno.json b/cbor/deno.json index 65bda71620f3..8f43239ecaa4 100644 --- a/cbor/deno.json +++ b/cbor/deno.json @@ -1,10 +1,11 @@ { "name": "@std/cbor", - "version": "0.0.0", + "version": "0.1.0", "exports": { ".": "./mod.ts", "./decode": "./decode.ts", "./encode": "./encode.ts", + "./decode-stream": "./decode_stream.ts", "./encode-stream": "./encode_stream.ts" } } From 89e9b533b8a8208392910ab7bc7c3531a6e61e63 Mon Sep 17 00:00:00 2001 From: Doctor <44320105+BlackAsLight@users.noreply.github.com> Date: Tue, 24 Sep 2024 06:13:55 +1000 Subject: [PATCH 41/45] fix(cbor): import_map --- import_map.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/import_map.json b/import_map.json index abbd69c69c25..5ef26dba5d49 100644 --- a/import_map.json +++ b/import_map.json @@ -11,7 +11,7 @@ "@std/async": "jsr:@std/async@^1.0.5", "@std/bytes": "jsr:@std/bytes@^1.0.2", "@std/cache": "jsr:@std/cache@^0.1.3", - "@std/cbor": "jsr:@std/cbor@^0.0.0", + "@std/cbor": "jsr:@std/cbor@^0.1.0", "@std/cli": "jsr:@std/cli@^1.0.6", "@std/collections": "jsr:@std/collections@^1.0.6", "@std/crypto": "jsr:@std/crypto@^1.0.3", From efe84381926ed159aae4370b34b804f4f9a7e54b Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:56:43 +1000 Subject: [PATCH 42/45] refactor(cbor): so every API has it's own file. --- cbor/_common.ts | 2 + cbor/{decode.ts => _common_decode.ts} | 119 +- cbor/_common_encode.ts | 138 ++ cbor/_common_test.ts | 5 + cbor/array_decoded_stream.ts | 62 + cbor/array_decoded_stream_test.ts | 22 + cbor/array_encoder_stream.ts | 173 +++ cbor/array_encoder_stream_test.ts | 43 + cbor/byte_decoded_stream.ts | 60 + cbor/byte_decoded_stream_test.ts | 47 + cbor/byte_encoder_stream.ts | 176 +++ cbor/byte_encoder_stream_test.ts | 53 + cbor/decode_cbor.ts | 50 + cbor/decode_cbor_sequence.ts | 50 + cbor/decode_cbor_sequence_test.ts | 11 + cbor/{decode_test.ts => decode_cbor_test.ts} | 16 +- cbor/decode_stream_test.ts | 180 --- cbor/deno.json | 20 +- cbor/encode.ts | 345 ----- cbor/encode_cbor.ts | 65 + cbor/encode_cbor_sequence.ts | 40 + cbor/encode_cbor_sequence_test.ts | 11 + cbor/{encode_test.ts => encode_cbor_test.ts} | 15 +- cbor/encode_stream.ts | 1158 ----------------- cbor/encode_stream_test.ts | 298 ----- cbor/map_decoded_stream.ts | 68 + cbor/map_decoded_stream_test.ts | 26 + cbor/map_encoder_stream.ts | 189 +++ cbor/map_encoder_stream_test.ts | 44 + cbor/mod.ts | 20 +- ...e_stream.ts => sequence_decoder_stream.ts} | 267 +--- cbor/sequence_decoder_stream_test.ts | 55 + cbor/sequence_encoder_stream.ts | 455 +++++++ cbor/sequence_encoder_stream_test.ts | 127 ++ cbor/tag.ts | 102 ++ cbor/text_decoded_stream.ts | 59 + cbor/text_decoded_stream_test.ts | 50 + cbor/text_encoder_stream.ts | 169 +++ cbor/text_encoder_stream_test.ts | 53 + cbor/types.ts | 70 + 40 files changed, 2542 insertions(+), 2371 deletions(-) rename cbor/{decode.ts => _common_decode.ts} (68%) create mode 100644 cbor/_common_encode.ts create mode 100644 cbor/_common_test.ts create mode 100644 cbor/array_decoded_stream.ts create mode 100644 cbor/array_decoded_stream_test.ts create mode 100644 cbor/array_encoder_stream.ts create mode 100644 cbor/array_encoder_stream_test.ts create mode 100644 cbor/byte_decoded_stream.ts create mode 100644 cbor/byte_decoded_stream_test.ts create mode 100644 cbor/byte_encoder_stream.ts create mode 100644 cbor/byte_encoder_stream_test.ts create mode 100644 cbor/decode_cbor.ts create mode 100644 cbor/decode_cbor_sequence.ts create mode 100644 cbor/decode_cbor_sequence_test.ts rename cbor/{decode_test.ts => decode_cbor_test.ts} (97%) delete mode 100644 cbor/decode_stream_test.ts delete mode 100644 cbor/encode.ts create mode 100644 cbor/encode_cbor.ts create mode 100644 cbor/encode_cbor_sequence.ts create mode 100644 cbor/encode_cbor_sequence_test.ts rename cbor/{encode_test.ts => encode_cbor_test.ts} (96%) delete mode 100644 cbor/encode_stream.ts delete mode 100644 cbor/encode_stream_test.ts create mode 100644 cbor/map_decoded_stream.ts create mode 100644 cbor/map_decoded_stream_test.ts create mode 100644 cbor/map_encoder_stream.ts create mode 100644 cbor/map_encoder_stream_test.ts rename cbor/{decode_stream.ts => sequence_decoder_stream.ts} (71%) create mode 100644 cbor/sequence_decoder_stream_test.ts create mode 100644 cbor/sequence_encoder_stream.ts create mode 100644 cbor/sequence_encoder_stream_test.ts create mode 100644 cbor/tag.ts create mode 100644 cbor/text_decoded_stream.ts create mode 100644 cbor/text_decoded_stream_test.ts create mode 100644 cbor/text_encoder_stream.ts create mode 100644 cbor/text_encoder_stream_test.ts create mode 100644 cbor/types.ts diff --git a/cbor/_common.ts b/cbor/_common.ts index 15fa91503d9c..7a500e58a451 100644 --- a/cbor/_common.ts +++ b/cbor/_common.ts @@ -1,5 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +export type ReleaseLock = (value?: unknown) => void; + export function numberToArray(bytes: number, x: number | bigint): Uint8Array { const view = new DataView(new ArrayBuffer(8)); if (typeof x === "bigint" || x % 1 === 0) view.setBigUint64(0, BigInt(x)); diff --git a/cbor/decode.ts b/cbor/_common_decode.ts similarity index 68% rename from cbor/decode.ts rename to cbor/_common_decode.ts index bbf13ef9091d..1a0bd7629c0e 100644 --- a/cbor/decode.ts +++ b/cbor/_common_decode.ts @@ -2,101 +2,10 @@ import { concat } from "@std/bytes"; import { arrayToNumber } from "./_common.ts"; -import { CborTag, type CborType } from "./encode.ts"; +import { CborTag } from "./tag.ts"; +import type { CborType } from "./types.ts"; -/** - * Decodes a CBOR-encoded {@link Uint8Array} into the JavaScript equivalent - * values represented as a {@link CborType}. - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * **Limitations:** - * - While CBOR does support map keys of any type, this - * implementation only supports map keys being of type {@link string}, and will - * throw if detected decoding otherwise. - * - This decoder will throw if duplicate map keys are detected. This behaviour - * differentiates from {@link CborSequenceDecoderStream}. - * - * **Notice:** This decoder handles the tag numbers 0, and 1 automatically, all - * others returned are wrapped in a {@link CborTag} instance. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { decodeCbor, encodeCbor } from "@std/cbor"; - * - * const rawMessage = [ - * "Hello World", - * 35, - * 0.5, - * false, - * -1, - * null, - * Uint8Array.from([0, 1, 2, 3]), - * ]; - * - * const encodedMessage = encodeCbor(rawMessage); - * const decodedMessage = decodeCbor(encodedMessage); - * - * assert(decodedMessage instanceof Array); - * assertEquals(decodedMessage, rawMessage); - * ``` - * - * @param value The value to decode of type CBOR-encoded {@link Uint8Array}. - * @returns A {@link CborType} representing the decoded data. - */ -export function decodeCbor(value: Uint8Array): CborType { - if (!value.length) throw RangeError("Cannot decode empty Uint8Array"); - const source = Array.from(value).reverse(); - return decode(source); -} - -/** - * Decodes a CBOR-sequence-encoded {@link Uint8Array} into the JavaScript - * equivalent values represented as a {@link CBorType} array. - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * **Limitations:** - * - While CBOR does support map keys of any type, this implementation only - * supports map keys being of type {@link string}, and will throw if detected - * decoding otherwise. - * - This decoder will throw an error if duplicate keys are detected. - * - * **Notice:** This decoder handles the tag numbers 0, and 1 automatically, all - * others returned are wrapped in a {@link CborTag} instance. - * - * @example Usage - * ```ts - * import { assertEquals } from "@std/assert"; - * import { decodeCborSequence, encodeCborSequence } from "@std/cbor"; - * - * const rawMessage = [ - * "Hello World", - * 35, - * 0.5, - * false, - * -1, - * null, - * Uint8Array.from([0, 1, 2, 3]), - * ]; - * - * const encodedMessage = encodeCborSequence(rawMessage); - * const decodedMessage = decodeCborSequence(encodedMessage); - * - * assertEquals(decodedMessage, rawMessage); - * ``` - * - * @param value The value to decode of type CBOR-sequence-encoded - * {@link Uint8Array}. - * @returns A {@link CborType} array representing the decoded data. - */ -export function decodeCborSequence(value: Uint8Array): CborType[] { - const output: CborType[] = []; - const source = Array.from(value).reverse(); - while (source.length) output.push(decode(source)); - return output; -} - -function decode(source: number[]): CborType { +export function decode(source: number[]): CborType { const byte = source.pop(); if (byte == undefined) throw new RangeError("More bytes were expected"); @@ -122,7 +31,7 @@ function decode(source: number[]): CborType { } } -function decodeZero(source: number[], aI: number): number | bigint { +export function decodeZero(source: number[], aI: number): number | bigint { if (aI < 24) return aI; if (aI <= 27) { return arrayToNumber( @@ -137,7 +46,7 @@ function decodeZero(source: number[], aI: number): number | bigint { ); } -function decodeOne(source: number[], aI: number): number | bigint { +export function decodeOne(source: number[], aI: number): number | bigint { if (aI > 27) { throw new RangeError( `Cannot decode value (0b001_${aI.toString(2).padStart(5, "0")})`, @@ -148,7 +57,7 @@ function decodeOne(source: number[], aI: number): number | bigint { return -x - 1; } -function decodeTwo(source: number[], aI: number): Uint8Array { +export function decodeTwo(source: number[], aI: number): Uint8Array { if (aI < 24) return Uint8Array.from(source.splice(-aI, aI).reverse()); if (aI <= 27) { // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. @@ -195,7 +104,7 @@ function decodeTwo(source: number[], aI: number): Uint8Array { ); } -function decodeThree(source: number[], aI: number): string { +export function decodeThree(source: number[], aI: number): string { if (aI <= 27) return new TextDecoder().decode(decodeTwo(source, aI)); if (aI === 31) { let byte = source.pop(); @@ -229,7 +138,7 @@ function decodeThree(source: number[], aI: number): string { ); } -function decodeFour(source: number[], aI: number): CborType[] { +export function decodeFour(source: number[], aI: number): CborType[] { if (aI <= 27) { const array: CborType[] = []; // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. @@ -260,7 +169,10 @@ function decodeFour(source: number[], aI: number): CborType[] { ); } -function decodeFive(source: number[], aI: number): { [k: string]: CborType } { +export function decodeFive( + source: number[], + aI: number, +): { [k: string]: CborType } { if (aI <= 27) { const object: { [k: string]: CborType } = {}; // Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large. @@ -319,7 +231,10 @@ function decodeFive(source: number[], aI: number): { [k: string]: CborType } { ); } -function decodeSix(source: number[], aI: number): Date | CborTag { +export function decodeSix( + source: number[], + aI: number, +): Date | CborTag { if (aI > 27) { throw new RangeError( `Cannot decode value (0b110_${aI.toString(2).padStart(5, "0")})`, @@ -344,7 +259,7 @@ function decodeSix(source: number[], aI: number): Date | CborTag { return new CborTag(tagNumber, tagContent); } -function decodeSeven( +export function decodeSeven( source: number[], aI: number, ): undefined | null | boolean | number { diff --git a/cbor/_common_encode.ts b/cbor/_common_encode.ts new file mode 100644 index 000000000000..665bf19792d1 --- /dev/null +++ b/cbor/_common_encode.ts @@ -0,0 +1,138 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { concat } from "@std/bytes"; +import { numberToArray } from "./_common.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import type { CborTag } from "./tag.ts"; +import type { CborType } from "./types.ts"; + +export function encodeNumber(x: number): Uint8Array { + if (x % 1 === 0) { + const isNegative = x < 0; + const majorType = isNegative ? 0b001_00000 : 0b000_00000; + if (isNegative) x = -x - 1; + + if (x < 24) return new Uint8Array([majorType + x]); + if (x < 2 ** 8) return new Uint8Array([majorType + 24, x]); + if (x < 2 ** 16) { + return concat([new Uint8Array([majorType + 25]), numberToArray(2, x)]); + } + if (x < 2 ** 32) { + return concat([new Uint8Array([majorType + 26]), numberToArray(4, x)]); + } + if (x < 2 ** 64) { + // Due to possible precision loss with numbers this large, it's best to do conversion under BigInt or end up with 1n off. + return encodeBigInt(BigInt(isNegative ? -x - 1 : x)); + } + throw new RangeError( + `Cannot encode number: It (${isNegative ? -x - 1 : x}) exceeds ${ + isNegative ? "-" : "" + }2 ** 64 - 1`, + ); + } + return concat([new Uint8Array([0b111_11011]), numberToArray(8, x)]); +} + +export function encodeBigInt(x: bigint): Uint8Array { + const isNegative = x < 0n; + if ((isNegative ? -x : x) < 2n ** 32n) return encodeNumber(Number(x)); + + const head = new Uint8Array([x < 0n ? 0b001_11011 : 0b000_11011]); + if (isNegative) x = -x - 1n; + + if (x < 2n ** 64n) return concat([head, numberToArray(8, x)]); + throw new RangeError( + `Cannot encode bigint: It (${isNegative ? -x - 1n : x}) exceeds ${ + isNegative ? "-" : "" + }2 ** 64 - 1`, + ); +} + +export function encodeUint8Array(x: Uint8Array): Uint8Array { + if (x.length < 24) { + return concat([new Uint8Array([0b010_00000 + x.length]), x]); + } + if (x.length < 2 ** 8) { + return concat([new Uint8Array([0b010_11000, x.length]), x]); + } + if (x.length < 2 ** 16) { + return concat([ + new Uint8Array([0b010_11001]), + numberToArray(2, x.length), + x, + ]); + } + if (x.length < 2 ** 32) { + return concat([ + new Uint8Array([0b010_11010]), + numberToArray(4, x.length), + x, + ]); + } + // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. + return concat([ + new Uint8Array([0b010_11011]), + numberToArray(8, x.length), + x, + ]); +} + +export function encodeString(x: string): Uint8Array { + const array = encodeUint8Array(new TextEncoder().encode(x)); + array[0]! += 1 << 5; + return array; +} + +export function encodeDate(x: Date): Uint8Array { + return concat([ + new Uint8Array([0b110_00001]), + encodeNumber(x.getTime() / 1000), + ]); +} + +export function encodeArray(x: CborType[]): Uint8Array { + let head: number[]; + if (x.length < 24) head = [0b100_00000 + x.length]; + else if (x.length < 2 ** 8) head = [0b100_11000, x.length]; + else if (x.length < 2 ** 16) { + head = [0b100_11001, ...numberToArray(2, x.length)]; + } else if (x.length < 2 ** 32) { + head = [0b100_11010, ...numberToArray(4, x.length)]; + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support an `Array` being that large. + else head = [0b100_11011, ...numberToArray(8, x.length)]; + return concat([Uint8Array.from(head), ...x.map((x) => encodeCbor(x))]); +} + +export function encodeObject(x: { [k: string]: CborType }): Uint8Array { + const len = Object.keys(x).length; + let head: number[]; + if (len < 24) head = [0b101_00000 + len]; + else if (len < 2 ** 8) head = [0b101_11000, len]; + else if (len < 2 ** 16) head = [0b101_11001, ...numberToArray(2, len)]; + else if (len < 2 ** 32) head = [0b101_11010, ...numberToArray(4, len)]; + // Can safely assume `len < 2 ** 64` as JavaScript doesn't support an `Object` being that Large. + else head = [0b101_11011, ...numberToArray(8, len)]; + return concat([ + Uint8Array.from(head), + ...Object.entries(x).map(( + [k, v], + ) => [encodeString(k), encodeCbor(v)]).flat(), + ]); +} + +export function encodeTag(x: CborTag) { + const tagNumber = BigInt(x.tagNumber); + if (tagNumber < 0n) { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) is less than zero`, + ); + } + if (tagNumber > 2n ** 64n) { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, + ); + } + const head = encodeBigInt(tagNumber); + head[0]! += 0b110_00000; + return concat([head, encodeCbor(x.tagContent)]); +} diff --git a/cbor/_common_test.ts b/cbor/_common_test.ts new file mode 100644 index 000000000000..94dc07761f6d --- /dev/null +++ b/cbor/_common_test.ts @@ -0,0 +1,5 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +export function random(start: number, end: number): number { + return Math.floor(Math.random() * (end - start) + start); +} diff --git a/cbor/array_decoded_stream.ts b/cbor/array_decoded_stream.ts new file mode 100644 index 000000000000..e270d918cf81 --- /dev/null +++ b/cbor/array_decoded_stream.ts @@ -0,0 +1,62 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import type { ReleaseLock } from "./_common.ts"; +import type { CborOutputStream } from "./types.ts"; + +/** + * A {@link ReadableStream} that wraps the decoded CBOR "Array". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * Instances of this class is created from {@link CborSequenceDecoderStream}. + * This class is not designed for you to create instances of it yourself. It is + * merely a way for you to validate the type being returned. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborArrayEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } + * ``` + */ +export class CborArrayDecodedStream extends ReadableStream { + /** + * Constructs a new instance. + * + * @param gen A {@link AsyncGenerator}. + * @param releaseLock A Function that's called when the stream is finished. + */ + constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { + super({ + async pull(controller) { + const { done, value } = await gen.next(); + if (done) { + releaseLock(); + controller.close(); + } else controller.enqueue(value); + }, + async cancel() { + // deno-lint-ignore no-empty + for await (const _ of gen) {} + releaseLock(); + }, + }); + } +} diff --git a/cbor/array_decoded_stream_test.ts b/cbor/array_decoded_stream_test.ts new file mode 100644 index 000000000000..42a807862f2c --- /dev/null +++ b/cbor/array_decoded_stream_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals } from "@std/assert"; +import { random } from "./_common_test.ts"; +import { CborArrayDecodedStream } from "./array_decoded_stream.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; + +Deno.test("CborSequenceDecoderStream() decoding Arrays", async () => { + const size = random(0, 24); + + const reader = ReadableStream.from([encodeCbor(new Array(size).fill(0))]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborArrayDecodedStream); + assertEquals(await Array.fromAsync(value), new Array(size).fill(0)); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/array_encoder_stream.ts b/cbor/array_encoder_stream.ts new file mode 100644 index 000000000000..78daf24b60c9 --- /dev/null +++ b/cbor/array_encoder_stream.ts @@ -0,0 +1,173 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { upgradeStreamFromGen } from "./_common.ts"; +import { CborSequenceEncoderStream } from "./sequence_encoder_stream.ts"; +import type { CborInputStream } from "./types.ts"; + +/** + * A {@link TransformStream} that encodes a + * {@link ReadableStream} into CBOR "Indefinite Length Array". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborArrayEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } + * ``` + */ +export class CborArrayEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + CborInputStream, + CborInputStream + >(); + this.#readable = upgradeStreamFromGen(async function* () { + yield new Uint8Array([0b100_11111]); + for await ( + const x of readable.pipeThrough(new CborSequenceEncoderStream()) + ) { + yield x; + } + yield new Uint8Array([0b111_11111]); + }()); + this.#writable = writable; + } + + /** + * Creates a {@link CborArrayEncoderStream} instance from an iterable of + * {@link CborInputStream} chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; + * + * for await ( + * const value of CborArrayEncoderStream.from(rawMessage) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } + * ``` + * + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or + * {@link Iterable}. + * @returns A {@link CborArrayEncoderStream} instance of the encoded data. + */ + static from( + asyncIterable: AsyncIterable | Iterable, + ): CborArrayEncoderStream { + const encoder = new CborArrayEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborArrayEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } + * ``` + * + * @returns A {@link ReadableStream}. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The {@link WritableStream} associated with the instance, + * which accepts {@link CborInputStream} chunks to be encoded into CBOR + * format. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborArrayEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborArrayDecodedStream); + * let i = 0; + * for await (const text of value) { + * assert(typeof text === "string"); + * assertEquals(text, rawMessage[i++]); + * } + * } + * ``` + * + * @returns A {@link WritableStream}. + */ + get writable(): WritableStream { + return this.#writable; + } +} diff --git a/cbor/array_encoder_stream_test.ts b/cbor/array_encoder_stream_test.ts new file mode 100644 index 000000000000..a70cafa622c0 --- /dev/null +++ b/cbor/array_encoder_stream_test.ts @@ -0,0 +1,43 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { random } from "./_common_test.ts"; +import { CborArrayEncoderStream } from "./array_encoder_stream.ts"; +import { encodeCbor } from "./encode_cbor.ts"; + +Deno.test("CborArrayEncoderStream()", async () => { + const arrays = [random(0, 2 ** 32)]; + + const expectedOutput = concat([ + new Uint8Array([0b100_11111]), + ...arrays.map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(arrays).pipeThrough(new CborArrayEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborArrayEncoderStream.from()", async () => { + const arrays = [random(0, 2 ** 32)]; + + const expectedOutput = concat([ + new Uint8Array([0b100_11111]), + ...arrays.map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + CborArrayEncoderStream.from(arrays).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); diff --git a/cbor/byte_decoded_stream.ts b/cbor/byte_decoded_stream.ts new file mode 100644 index 000000000000..51eb46808961 --- /dev/null +++ b/cbor/byte_decoded_stream.ts @@ -0,0 +1,60 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import type { ReleaseLock } from "./_common.ts"; + +/** + * A {@link ReadableStream} that wraps the decoded CBOR "Byte String". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * Instances of this class is created from {@link CborSequenceDecoderStream}. + * This class is not designed for you to create instances of it yourself. It is + * merely a way for you to validate the type being returned. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = new Uint8Array(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborByteEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } + * ``` + */ +export class CborByteDecodedStream extends ReadableStream { + /** + * Constructs a new instance. + * + * @param gen A {@link AsyncGenerator}. + * @param releaseLock A Function that's called when the stream is finished. + */ + constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { + super({ + async pull(controller) { + const { done, value } = await gen.next(); + if (done) { + releaseLock(); + controller.close(); + } else controller.enqueue(value); + }, + async cancel() { + // deno-lint-ignore no-empty + for await (const _ of gen) {} + releaseLock(); + }, + }); + } +} diff --git a/cbor/byte_decoded_stream_test.ts b/cbor/byte_decoded_stream_test.ts new file mode 100644 index 000000000000..86b3140bbda9 --- /dev/null +++ b/cbor/byte_decoded_stream_test.ts @@ -0,0 +1,47 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { random } from "./_common_test.ts"; +import { CborByteDecodedStream } from "./byte_decoded_stream.ts"; +import { CborByteEncoderStream } from "./byte_encoder_stream.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; + +Deno.test("CborSequenceDecoderStream() decoding Indefinite Length Byte String", async () => { + const inputSize = 10; + + const reader = CborByteEncoderStream.from([ + new Uint8Array(inputSize), + new Uint8Array(inputSize * 2), + new Uint8Array(inputSize * 3), + ]).readable.pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborByteDecodedStream); + assertEquals(await Array.fromAsync(value), [ + new Uint8Array(inputSize), + new Uint8Array(inputSize * 2), + new Uint8Array(inputSize * 3), + ]); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); + +Deno.test("CborSequenceDecoderStream() decoding large Definite Length Byte String", async () => { + // Uint8Array needs to be 2 ** 32 bytes+ to be decoded via a CborByteDecodedStream. + const size = random(2 ** 32, 2 ** 33); + + const reader = ReadableStream.from([encodeCbor(new Uint8Array(size))]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborByteDecodedStream); + assertEquals(concat(await Array.fromAsync(value)).length, size); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/byte_encoder_stream.ts b/cbor/byte_encoder_stream.ts new file mode 100644 index 000000000000..8796320c0e0e --- /dev/null +++ b/cbor/byte_encoder_stream.ts @@ -0,0 +1,176 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { numberToArray, upgradeStreamFromGen } from "./_common.ts"; + +/** + * A {@link TransformStream} that encodes a {@link ReadableStream} + * into CBOR "Indefinite Length Byte String". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * **Notice:** Each chunk of the {@link ReadableStream} is encoded + * as its own "Definite Length Byte String" meaning space can be saved if large + * chunks are pipped through instead of small chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = new Uint8Array(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborByteEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } + * ``` + */ +export class CborByteEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + Uint8Array, + Uint8Array + >(); + this.#readable = upgradeStreamFromGen(async function* () { + yield new Uint8Array([0b010_11111]); + for await (const x of readable) { + if (x.length < 24) yield new Uint8Array([0b010_00000 + x.length]); + else if (x.length < 2 ** 8) { + yield new Uint8Array([0b010_11000, x.length]); + } else if (x.length < 2 ** 16) { + yield new Uint8Array([0b010_11001, ...numberToArray(2, x.length)]); + } else if (x.length < 2 ** 32) { + yield new Uint8Array([0b010_11010, ...numberToArray(4, x.length)]); + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. + else yield new Uint8Array([0b010_11011, ...numberToArray(8, x.length)]); + yield x; + } + yield new Uint8Array([0b111_11111]); + }()); + this.#writable = writable; + } + + /** + * Creates a {@link CborByteEncoderStream} instance from an iterable of + * {@link Uint8Array} chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = new Uint8Array(100); + * + * for await ( + * const value of CborByteEncoderStream.from([rawMessage]) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } + * ``` + * + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or {@link Iterable}. + * @returns A {@link CborByteEncoderStream} instance of the encoded data. + */ + static from( + asyncIterable: AsyncIterable | Iterable, + ): CborByteEncoderStream { + const encoder = new CborByteEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = new Uint8Array(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborByteEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } + * ``` + * + * @returns A {@link ReadableStream}. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The {@link WritableStream} associated with the instance, which + * accepts {@link Uint8Array} chunks to be encoded into CBOR format. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { concat } from "@std/bytes"; + * import { + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage = new Uint8Array(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborByteEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); + * if (value instanceof CborByteDecodedStream) { + * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); + * } else assertEquals(value, new Uint8Array(100)); + * } + * ``` + * + * @returns a {@link WritableStream}. + */ + get writable(): WritableStream { + return this.#writable; + } +} diff --git a/cbor/byte_encoder_stream_test.ts b/cbor/byte_encoder_stream_test.ts new file mode 100644 index 000000000000..2ddfdd65e108 --- /dev/null +++ b/cbor/byte_encoder_stream_test.ts @@ -0,0 +1,53 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { random } from "./_common_test.ts"; +import { CborByteEncoderStream } from "./byte_encoder_stream.ts"; +import { encodeCbor } from "./encode_cbor.ts"; + +Deno.test("CborByteEncoderStream()", async () => { + const bytes = [ + new Uint8Array(random(0, 24)), + new Uint8Array(random(24, 2 ** 8)), + new Uint8Array(random(2 ** 8, 2 ** 16)), + new Uint8Array(random(2 ** 16, 2 ** 17)), + ]; + + const expectedOutput = concat([ + new Uint8Array([0b010_11111]), + ...bytes.map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(bytes).pipeThrough(new CborByteEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborByteEncoderStream.from()", async () => { + const bytes = [ + new Uint8Array(random(0, 24)), + new Uint8Array(random(24, 2 ** 8)), + new Uint8Array(random(2 ** 8, 2 ** 16)), + new Uint8Array(random(2 ** 16, 2 ** 17)), + ]; + + const expectedOutput = concat([ + Uint8Array.from([0b010_11111]), + ...bytes.map((x) => encodeCbor(x)), + Uint8Array.from([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + CborByteEncoderStream.from(bytes).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); diff --git a/cbor/decode_cbor.ts b/cbor/decode_cbor.ts new file mode 100644 index 000000000000..7346bd369d56 --- /dev/null +++ b/cbor/decode_cbor.ts @@ -0,0 +1,50 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { decode } from "./_common_decode.ts"; +import type { CborType } from "./types.ts"; + +/** + * Decodes a CBOR-encoded {@link Uint8Array} into the JavaScript equivalent + * values represented as a {@link CborType}. + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * **Limitations:** + * - While CBOR does support map keys of any type, this + * implementation only supports map keys being of type {@link string}, and will + * throw if detected decoding otherwise. + * - This decoder will throw if duplicate map keys are detected. This behaviour + * differentiates from {@link CborSequenceDecoderStream}. + * + * **Notice:** This decoder handles the tag numbers 0, and 1 automatically, all + * others returned are wrapped in a {@link CborTag} instance. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { decodeCbor, encodeCbor } from "@std/cbor"; + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encodeCbor(rawMessage); + * const decodedMessage = decodeCbor(encodedMessage); + * + * assert(decodedMessage instanceof Array); + * assertEquals(decodedMessage, rawMessage); + * ``` + * + * @param value The value to decode of type CBOR-encoded {@link Uint8Array}. + * @returns A {@link CborType} representing the decoded data. + */ +export function decodeCbor(value: Uint8Array): CborType { + if (!value.length) throw RangeError("Cannot decode empty Uint8Array"); + const source = Array.from(value).reverse(); + return decode(source); +} diff --git a/cbor/decode_cbor_sequence.ts b/cbor/decode_cbor_sequence.ts new file mode 100644 index 000000000000..699e38afec72 --- /dev/null +++ b/cbor/decode_cbor_sequence.ts @@ -0,0 +1,50 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { decode } from "./_common_decode.ts"; +import type { CborType } from "./types.ts"; + +/** + * Decodes a CBOR-sequence-encoded {@link Uint8Array} into the JavaScript + * equivalent values represented as a {@link CBorType} array. + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * **Limitations:** + * - While CBOR does support map keys of any type, this implementation only + * supports map keys being of type {@link string}, and will throw if detected + * decoding otherwise. + * - This decoder will throw an error if duplicate keys are detected. + * + * **Notice:** This decoder handles the tag numbers 0, and 1 automatically, all + * others returned are wrapped in a {@link CborTag} instance. + * + * @example Usage + * ```ts + * import { assertEquals } from "@std/assert"; + * import { decodeCborSequence, encodeCborSequence } from "@std/cbor"; + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encodeCborSequence(rawMessage); + * const decodedMessage = decodeCborSequence(encodedMessage); + * + * assertEquals(decodedMessage, rawMessage); + * ``` + * + * @param value The value to decode of type CBOR-sequence-encoded + * {@link Uint8Array}. + * @returns A {@link CborType} array representing the decoded data. + */ +export function decodeCborSequence(value: Uint8Array): CborType[] { + const output: CborType[] = []; + const source = Array.from(value).reverse(); + while (source.length) output.push(decode(source)); + return output; +} diff --git a/cbor/decode_cbor_sequence_test.ts b/cbor/decode_cbor_sequence_test.ts new file mode 100644 index 000000000000..b69f837a3524 --- /dev/null +++ b/cbor/decode_cbor_sequence_test.ts @@ -0,0 +1,11 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { decodeCborSequence } from "./decode_cbor_sequence.ts"; + +Deno.test("decodeCborSequence()", () => { + assertEquals( + decodeCborSequence(Uint8Array.from([0b000_00000, 0b000_00000])), + [0, 0], + ); +}); diff --git a/cbor/decode_test.ts b/cbor/decode_cbor_test.ts similarity index 97% rename from cbor/decode_test.ts rename to cbor/decode_cbor_test.ts index 4bed7697f9c7..a490d123f099 100644 --- a/cbor/decode_test.ts +++ b/cbor/decode_cbor_test.ts @@ -1,11 +1,10 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertEquals, assertThrows } from "@std/assert"; -import { CborTag, decodeCbor, decodeCborSequence, encodeCbor } from "./mod.ts"; - -function random(start: number, end: number): number { - return Math.floor(Math.random() * (end - start) + start); -} +import { random } from "./_common_test.ts"; +import { decodeCbor } from "./decode_cbor.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborTag } from "./tag.ts"; Deno.test("decodeCbor() decoding undefined", () => { assertEquals(decodeCbor(encodeCbor(undefined)), undefined); @@ -654,10 +653,3 @@ Deno.test("decodeCbor() rejecting majorType 7", () => { "Cannot decode value (0b111_11110)", ); }); - -Deno.test("decodeCborSequence()", () => { - assertEquals( - decodeCborSequence(Uint8Array.from([0b000_00000, 0b000_00000])), - [0, 0], - ); -}); diff --git a/cbor/decode_stream_test.ts b/cbor/decode_stream_test.ts deleted file mode 100644 index 081310e5121f..000000000000 --- a/cbor/decode_stream_test.ts +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -import { assert, assertEquals } from "@std/assert"; -import { concat } from "@std/bytes"; -import { - CborArrayDecodedStream, - CborByteDecodedStream, - CborByteEncoderStream, - CborMapDecodedStream, - type CborMapOutputStream, - CborSequenceDecoderStream, - CborTag, - CborTextDecodedStream, - CborTextEncoderStream, - encodeCbor, - encodeCborSequence, -} from "./mod.ts"; - -function random(start: number, end: number): number { - return Math.floor(Math.random() * (end - start) + start); -} - -Deno.test("CborSequenceDecoderStream() decoding CborPrimitiveType", async () => { - const input = [ - undefined, - null, - true, - false, - Math.random() * 10, - random(0, 24), - -BigInt(random(2 ** 32, 2 ** 64)), - "a".repeat(random(0, 24)), - new Uint8Array(random(0, 24)), - new Date(), - ]; - - assertEquals( - await Array.fromAsync( - ReadableStream.from([encodeCborSequence(input)]).pipeThrough( - new CborSequenceDecoderStream(), - ), - ), - input, - ); -}); - -Deno.test("CborSequenceDecoderStream() decoding Indefinite Length Byte String", async () => { - const inputSize = 10; - - const reader = CborByteEncoderStream.from([ - new Uint8Array(inputSize), - new Uint8Array(inputSize * 2), - new Uint8Array(inputSize * 3), - ]).readable.pipeThrough(new CborSequenceDecoderStream()).getReader(); - - const { done, value } = await reader.read(); - assert(done === false); - assert(value instanceof CborByteDecodedStream); - assertEquals(await Array.fromAsync(value), [ - new Uint8Array(inputSize), - new Uint8Array(inputSize * 2), - new Uint8Array(inputSize * 3), - ]); - - assert((await reader.read()).done === true); - reader.releaseLock(); -}); - -Deno.test("CborSequenceDecoderStream() decoding large Definite Length Byte String", async () => { - // Uint8Array needs to be 2 ** 32 bytes+ to be decoded via a CborByteDecodedStream. - const size = random(2 ** 32, 2 ** 33); - - const reader = ReadableStream.from([encodeCbor(new Uint8Array(size))]) - .pipeThrough(new CborSequenceDecoderStream()).getReader(); - - const { done, value } = await reader.read(); - assert(done === false); - assert(value instanceof CborByteDecodedStream); - assertEquals(concat(await Array.fromAsync(value)).length, size); - - assert((await reader.read()).done === true); - reader.releaseLock(); -}); - -Deno.test("CborSequenceDecoderStream() decoding Indefinite Length Text String", async () => { - const inputSize = 10; - - const reader = CborTextEncoderStream.from([ - "a".repeat(inputSize), - "b".repeat(inputSize * 2), - "c".repeat(inputSize * 3), - ]).readable.pipeThrough(new CborSequenceDecoderStream()).getReader(); - - const { done, value } = await reader.read(); - assert(done === false); - assert(value instanceof CborTextDecodedStream); - assertEquals(await Array.fromAsync(value), [ - "a".repeat(inputSize), - "b".repeat(inputSize * 2), - "c".repeat(inputSize * 3), - ]); - - assert((await reader.read()).done === true); - reader.releaseLock(); -}); - -Deno.test("CborSequenceDecoderStream() decoding large Definite Text Byte String", async () => { - // Strings need to be 2 ** 16 bytes+ to be decoded via a CborTextDecodedStream. - const size = random(2 ** 16, 2 ** 17); - - const reader = ReadableStream.from([ - encodeCbor( - new TextDecoder().decode(new Uint8Array(size).fill("a".charCodeAt(0))), - ), - ]) - .pipeThrough(new CborSequenceDecoderStream()).getReader(); - - const { done, value } = await reader.read(); - assert(done === false); - assert(value instanceof CborTextDecodedStream); - assertEquals((await Array.fromAsync(value)).join(""), "a".repeat(size)); - - assert((await reader.read()).done === true); - reader.releaseLock(); -}); - -Deno.test("CborSequenceDecoderStream() decoding Arrays", async () => { - const size = random(0, 24); - - const reader = ReadableStream.from([encodeCbor(new Array(size).fill(0))]) - .pipeThrough(new CborSequenceDecoderStream()).getReader(); - - const { done, value } = await reader.read(); - assert(done === false); - assert(value instanceof CborArrayDecodedStream); - assertEquals(await Array.fromAsync(value), new Array(size).fill(0)); - - assert((await reader.read()).done === true); - reader.releaseLock(); -}); - -Deno.test("CborSequenceDecoderStream() decoding Objects", async () => { - const size = random(0, 24); - const entries = new Array(size).fill(0).map((_, i) => - [String.fromCharCode(97 + i), i] satisfies CborMapOutputStream - ); - - const reader = ReadableStream.from([encodeCbor(Object.fromEntries(entries))]) - .pipeThrough(new CborSequenceDecoderStream()).getReader(); - - const { done, value } = await reader.read(); - assert(done === false); - assert(value instanceof CborMapDecodedStream); - assertEquals(await Array.fromAsync(value), entries); - - assert((await reader.read()).done === true); - reader.releaseLock(); -}); - -Deno.test("CborSequenceDecoderStream() decoding CborTag()", async () => { - const tagNumber = 2; // Tag Number needs to be a value that will return a CborTag. - const size = random(0, 24); - - const reader = ReadableStream.from([ - encodeCbor(new CborTag(tagNumber, new Array(size).fill(0))), - ]).pipeThrough(new CborSequenceDecoderStream()).getReader(); - - const { done, value } = await reader.read(); - assert(done === false); - assert(value instanceof CborTag); - assertEquals(value.tagNumber, tagNumber); - assert(value.tagContent instanceof CborArrayDecodedStream); - assertEquals( - await Array.fromAsync(value.tagContent), - new Array(size).fill(0), - ); - - assert((await reader.read()).done === true); - reader.releaseLock(); -}); diff --git a/cbor/deno.json b/cbor/deno.json index 8f43239ecaa4..b865286b6821 100644 --- a/cbor/deno.json +++ b/cbor/deno.json @@ -3,9 +3,21 @@ "version": "0.1.0", "exports": { ".": "./mod.ts", - "./decode": "./decode.ts", - "./encode": "./encode.ts", - "./decode-stream": "./decode_stream.ts", - "./encode-stream": "./encode_stream.ts" + "./array-decoded-stream": "./array_decoded_stream.ts", + "./array-encoder-stream": "./array_encoder_stream.ts", + "./byte-decoded-stream": "./byte_decoded_stream.ts", + "./byte-encoder-stream": "./byte_encoder_stream.ts", + "./decode-cbor-sequence": "./decode_cbor_sequence.ts", + "./decode-cbor": "./decode_cbor.ts", + "./encode-cbor-sequence": "./encode_cbor_sequence.ts", + "./encode-cbor": "./encode_cbor.ts", + "./map-decoded-stream": "./map_decoded_stream.ts", + "./map-encoder-stream": "./map_encoder_stream.ts", + "./sequence-decoder-stream": "./sequence_decoder_stream.ts", + "./sequence-encoder-stream": "./sequence_encoder_stream.ts", + "./tag": "./tag.ts", + "./text-decoded-stream": "./text_decoded_stream.ts", + "./text-encoder-stream": "./text_encoder_stream.ts", + "./types": "./types.ts" } } diff --git a/cbor/encode.ts b/cbor/encode.ts deleted file mode 100644 index 79139375efba..000000000000 --- a/cbor/encode.ts +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -import { concat } from "@std/bytes"; -import { numberToArray } from "./_common.ts"; -import type { CborInputStream } from "./encode_stream.ts"; -import type { CborOutputStream } from "./decode_stream.ts"; - -/** - * This type specifies the primitive types that the implementation can - * encode/decode into/from. - */ -export type CborPrimitiveType = - | undefined - | null - | boolean - | number - | bigint - | string - | Uint8Array - | Date; - -/** - * This type specifies the encodable and decodable values for - * {@link encodeCbor}, {@link decodeCbor}, {@link encodeCborSequence}, and - * {@link decodeCborSequence}. - */ -export type CborType = CborPrimitiveType | CborTag | CborType[] | { - [k: string]: CborType; -}; - -/** - * Represents a CBOR tag, which pairs a tag number with content, used to convey - * additional semantic information in CBOR-encoded data. - * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; - * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; - * - * const rawMessage = new TextEncoder().encode("Hello World"); - * - * const encodedMessage = encodeCbor( - * new CborTag( - * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". - * encodeBase64Url(rawMessage), - * ), - * ); - * - * const decodedMessage = decodeCbor(encodedMessage); - * - * assert(decodedMessage instanceof CborTag); - * assert(typeof decodedMessage.tagContent === "string"); - * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); - * ``` - * - * @typeParam T The type of the tag's content, which can be a - * {@link CborType}, {@link CborInputStream}, or {@link CborOutputStream}. - */ -export class CborTag { - /** - * A {@link number} or {@link bigint} representing the CBOR tag number, used - * to identify the type of the tagged content. - * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; - * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; - * - * const rawMessage = new TextEncoder().encode("Hello World"); - * - * const encodedMessage = encodeCbor( - * new CborTag( - * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". - * encodeBase64Url(rawMessage), - * ), - * ); - * - * const decodedMessage = decodeCbor(encodedMessage); - * - * assert(decodedMessage instanceof CborTag); - * assert(typeof decodedMessage.tagContent === "string"); - * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); - * ``` - */ - tagNumber: number | bigint; - /** - * The content associated with the tag of type {@link T}. - * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; - * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; - * - * const rawMessage = new TextEncoder().encode("Hello World"); - * - * const encodedMessage = encodeCbor( - * new CborTag( - * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". - * encodeBase64Url(rawMessage), - * ), - * ); - * - * const decodedMessage = decodeCbor(encodedMessage); - * - * assert(decodedMessage instanceof CborTag); - * assert(typeof decodedMessage.tagContent === "string"); - * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); - * ``` - */ - tagContent: T; - /** - * Constructs a new instance. - * - * @param tagNumber A {@link number} or {@link bigint} representing the CBOR - * tag number, used to identify the type of the tagged content. - * @param tagContent The content associated with the tag of type {@link T}. - */ - constructor(tagNumber: number | bigint, tagContent: T) { - this.tagNumber = tagNumber; - this.tagContent = tagContent; - } -} - -/** - * Encodes a {@link CborType} value into a CBOR format represented as a - * {@link Uint8Array}. - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { decodeCbor, encodeCbor } from "@std/cbor"; - * - * const rawMessage = [ - * "Hello World", - * 35, - * 0.5, - * false, - * -1, - * null, - * Uint8Array.from([0, 1, 2, 3]), - * ]; - * - * const encodedMessage = encodeCbor(rawMessage); - * const decodedMessage = decodeCbor(encodedMessage); - * - * assert(decodedMessage instanceof Array); - * assertEquals(decodedMessage, rawMessage); - * ``` - * - * @param value The value to encode of type {@link CborType}. - * @returns A {@link Uint8Array} representing the encoded data. - */ -export function encodeCbor(value: CborType): Uint8Array { - switch (typeof value) { - case "number": - return encodeNumber(value); - case "string": - return encodeString(value); - case "boolean": - return new Uint8Array([value ? 0b111_10101 : 0b111_10100]); - case "undefined": - return new Uint8Array([0b111_10111]); - case "bigint": - return encodeBigInt(value); - } - if (value === null) return new Uint8Array([0b111_10110]); - if (value instanceof Date) return encodeDate(value); - if (value instanceof Uint8Array) return encodeUint8Array(value); - if (value instanceof Array) return encodeArray(value); - if (value instanceof CborTag) return encodeTag(value); - return encodeObject(value); -} - -/** - * Encodes an array of {@link CborType} values into a CBOR format sequence - * represented as a {@link Uint8Array}. - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * @example Usage - * ```ts - * import { assertEquals } from "@std/assert"; - * import { decodeCborSequence, encodeCborSequence } from "@std/cbor"; - * - * const rawMessage = [ - * "Hello World", - * 35, - * 0.5, - * false, - * -1, - * null, - * Uint8Array.from([0, 1, 2, 3]), - * ]; - * - * const encodedMessage = encodeCborSequence(rawMessage); - * const decodedMessage = decodeCborSequence(encodedMessage); - * - * assertEquals(decodedMessage, rawMessage); - * ``` - * - * @param values An array of values to encode of type {@link CborType} - * @returns A {@link Uint8Array} representing the encoded data. - */ -export function encodeCborSequence(values: CborType[]): Uint8Array { - const output: Uint8Array[] = []; - for (const value of values) output.push(encodeCbor(value)); - return concat(output); -} - -function encodeNumber(x: number): Uint8Array { - if (x % 1 === 0) { - const isNegative = x < 0; - const majorType = isNegative ? 0b001_00000 : 0b000_00000; - if (isNegative) x = -x - 1; - - if (x < 24) return new Uint8Array([majorType + x]); - if (x < 2 ** 8) return new Uint8Array([majorType + 24, x]); - if (x < 2 ** 16) { - return concat([new Uint8Array([majorType + 25]), numberToArray(2, x)]); - } - if (x < 2 ** 32) { - return concat([new Uint8Array([majorType + 26]), numberToArray(4, x)]); - } - if (x < 2 ** 64) { - // Due to possible precision loss with numbers this large, it's best to do conversion under BigInt or end up with 1n off. - return encodeBigInt(BigInt(isNegative ? -x - 1 : x)); - } - throw new RangeError( - `Cannot encode number: It (${isNegative ? -x - 1 : x}) exceeds ${ - isNegative ? "-" : "" - }2 ** 64 - 1`, - ); - } - return concat([new Uint8Array([0b111_11011]), numberToArray(8, x)]); -} - -function encodeBigInt(x: bigint): Uint8Array { - const isNegative = x < 0n; - if ((isNegative ? -x : x) < 2n ** 32n) return encodeNumber(Number(x)); - - const head = new Uint8Array([x < 0n ? 0b001_11011 : 0b000_11011]); - if (isNegative) x = -x - 1n; - - if (x < 2n ** 64n) return concat([head, numberToArray(8, x)]); - throw new RangeError( - `Cannot encode bigint: It (${isNegative ? -x - 1n : x}) exceeds ${ - isNegative ? "-" : "" - }2 ** 64 - 1`, - ); -} - -function encodeUint8Array(x: Uint8Array): Uint8Array { - if (x.length < 24) { - return concat([new Uint8Array([0b010_00000 + x.length]), x]); - } - if (x.length < 2 ** 8) { - return concat([new Uint8Array([0b010_11000, x.length]), x]); - } - if (x.length < 2 ** 16) { - return concat([ - new Uint8Array([0b010_11001]), - numberToArray(2, x.length), - x, - ]); - } - if (x.length < 2 ** 32) { - return concat([ - new Uint8Array([0b010_11010]), - numberToArray(4, x.length), - x, - ]); - } - // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. - return concat([ - new Uint8Array([0b010_11011]), - numberToArray(8, x.length), - x, - ]); -} - -function encodeString(x: string): Uint8Array { - const array = encodeUint8Array(new TextEncoder().encode(x)); - array[0]! += 1 << 5; - return array; -} - -function encodeDate(x: Date): Uint8Array { - return concat([ - new Uint8Array([0b110_00001]), - encodeNumber(x.getTime() / 1000), - ]); -} - -function encodeArray(x: CborType[]): Uint8Array { - let head: number[]; - if (x.length < 24) head = [0b100_00000 + x.length]; - else if (x.length < 2 ** 8) head = [0b100_11000, x.length]; - else if (x.length < 2 ** 16) { - head = [0b100_11001, ...numberToArray(2, x.length)]; - } else if (x.length < 2 ** 32) { - head = [0b100_11010, ...numberToArray(4, x.length)]; - } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support an `Array` being that large. - else head = [0b100_11011, ...numberToArray(8, x.length)]; - return concat([Uint8Array.from(head), ...x.map((x) => encodeCbor(x))]); -} - -function encodeObject(x: { [k: string]: CborType }): Uint8Array { - const len = Object.keys(x).length; - let head: number[]; - if (len < 24) head = [0b101_00000 + len]; - else if (len < 2 ** 8) head = [0b101_11000, len]; - else if (len < 2 ** 16) head = [0b101_11001, ...numberToArray(2, len)]; - else if (len < 2 ** 32) head = [0b101_11010, ...numberToArray(4, len)]; - // Can safely assume `len < 2 ** 64` as JavaScript doesn't support an `Object` being that Large. - else head = [0b101_11011, ...numberToArray(8, len)]; - return concat([ - Uint8Array.from(head), - ...Object.entries(x).map(( - [k, v], - ) => [encodeString(k), encodeCbor(v)]).flat(), - ]); -} - -function encodeTag(x: CborTag) { - const tagNumber = BigInt(x.tagNumber); - if (tagNumber < 0n) { - throw new RangeError( - `Cannot encode Tag Item: Tag Number (${x.tagNumber}) is less than zero`, - ); - } - if (tagNumber > 2n ** 64n) { - throw new RangeError( - `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, - ); - } - const head = encodeBigInt(tagNumber); - head[0]! += 0b110_00000; - return concat([head, encodeCbor(x.tagContent)]); -} diff --git a/cbor/encode_cbor.ts b/cbor/encode_cbor.ts new file mode 100644 index 000000000000..701833a658df --- /dev/null +++ b/cbor/encode_cbor.ts @@ -0,0 +1,65 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { + encodeArray, + encodeBigInt, + encodeDate, + encodeNumber, + encodeObject, + encodeString, + encodeTag, + encodeUint8Array, +} from "./_common_encode.ts"; +import { CborTag } from "./tag.ts"; +import type { CborType } from "./types.ts"; + +/** + * Encodes a {@link CborType} value into a CBOR format represented as a + * {@link Uint8Array}. + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { decodeCbor, encodeCbor } from "@std/cbor"; + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encodeCbor(rawMessage); + * const decodedMessage = decodeCbor(encodedMessage); + * + * assert(decodedMessage instanceof Array); + * assertEquals(decodedMessage, rawMessage); + * ``` + * + * @param value The value to encode of type {@link CborType}. + * @returns A {@link Uint8Array} representing the encoded data. + */ +export function encodeCbor(value: CborType): Uint8Array { + switch (typeof value) { + case "number": + return encodeNumber(value); + case "string": + return encodeString(value); + case "boolean": + return new Uint8Array([value ? 0b111_10101 : 0b111_10100]); + case "undefined": + return new Uint8Array([0b111_10111]); + case "bigint": + return encodeBigInt(value); + } + if (value === null) return new Uint8Array([0b111_10110]); + if (value instanceof Date) return encodeDate(value); + if (value instanceof Uint8Array) return encodeUint8Array(value); + if (value instanceof Array) return encodeArray(value); + if (value instanceof CborTag) return encodeTag(value); + return encodeObject(value); +} diff --git a/cbor/encode_cbor_sequence.ts b/cbor/encode_cbor_sequence.ts new file mode 100644 index 000000000000..40f9e0811361 --- /dev/null +++ b/cbor/encode_cbor_sequence.ts @@ -0,0 +1,40 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { concat } from "../bytes/concat.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import type { CborType } from "./types.ts"; + +/** + * Encodes an array of {@link CborType} values into a CBOR format sequence + * represented as a {@link Uint8Array}. + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * @example Usage + * ```ts + * import { assertEquals } from "@std/assert"; + * import { decodeCborSequence, encodeCborSequence } from "@std/cbor"; + * + * const rawMessage = [ + * "Hello World", + * 35, + * 0.5, + * false, + * -1, + * null, + * Uint8Array.from([0, 1, 2, 3]), + * ]; + * + * const encodedMessage = encodeCborSequence(rawMessage); + * const decodedMessage = decodeCborSequence(encodedMessage); + * + * assertEquals(decodedMessage, rawMessage); + * ``` + * + * @param values An array of values to encode of type {@link CborType} + * @returns A {@link Uint8Array} representing the encoded data. + */ +export function encodeCborSequence(values: CborType[]): Uint8Array { + const output: Uint8Array[] = []; + for (const value of values) output.push(encodeCbor(value)); + return concat(output); +} diff --git a/cbor/encode_cbor_sequence_test.ts b/cbor/encode_cbor_sequence_test.ts new file mode 100644 index 000000000000..ac5d29c271b1 --- /dev/null +++ b/cbor/encode_cbor_sequence_test.ts @@ -0,0 +1,11 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { encodeCborSequence } from "./encode_cbor_sequence.ts"; + +Deno.test("encodeCborSequence()", () => { + assertEquals( + encodeCborSequence([0, 0]), + Uint8Array.from([0b000_00000, 0b000_00000]), + ); +}); diff --git a/cbor/encode_test.ts b/cbor/encode_cbor_test.ts similarity index 96% rename from cbor/encode_test.ts rename to cbor/encode_cbor_test.ts index d3b227371a61..a4761bd86345 100644 --- a/cbor/encode_test.ts +++ b/cbor/encode_cbor_test.ts @@ -2,11 +2,9 @@ import { assertEquals, assertThrows } from "@std/assert"; import { concat } from "@std/bytes"; -import { CborTag, encodeCbor, encodeCborSequence } from "./mod.ts"; - -function random(start: number, end: number): number { - return Math.floor(Math.random() * (end - start) + start); -} +import { random } from "./_common_test.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborTag } from "./tag.ts"; Deno.test("encodeCbor() encoding undefined", () => { assertEquals( @@ -407,10 +405,3 @@ Deno.test("encodeCbor() rejecting CborTag()", () => { `Cannot encode Tag Item: Tag Number (${num}) exceeds 2 ** 64 - 1`, ); }); - -Deno.test("encodeCborSequence()", () => { - assertEquals( - encodeCborSequence([0, 0]), - Uint8Array.from([0b000_00000, 0b000_00000]), - ); -}); diff --git a/cbor/encode_stream.ts b/cbor/encode_stream.ts deleted file mode 100644 index d723aeef8e13..000000000000 --- a/cbor/encode_stream.ts +++ /dev/null @@ -1,1158 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -import { numberToArray, upgradeStreamFromGen } from "./_common.ts"; -import { type CborPrimitiveType, CborTag, encodeCbor } from "./encode.ts"; - -/** - * Specifies the encodable value types for the {@link CborSequenceEncoderStream} - * and {@link CborArrayEncoderStream}. - */ -export type CborInputStream = - | CborPrimitiveType - | CborTag - | CborInputStream[] - | { [k: string]: CborInputStream } - | CborByteEncoderStream - | CborTextEncoderStream - | CborArrayEncoderStream - | CborMapEncoderStream; - -/** - * Specifies the structure of input for the {@link CborMapEncoderStream}. - */ -export type CborMapInputStream = [string, CborInputStream]; - -/** - * A {@link TransformStream} that encodes a {@link ReadableStream} - * into CBOR "Indefinite Length Byte String". - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * **Notice:** Each chunk of the {@link ReadableStream} is encoded - * as its own "Definite Length Byte String" meaning space can be saved if large - * chunks are pipped through instead of small chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { concat } from "@std/bytes"; - * import { - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = new Uint8Array(100); - * - * for await ( - * const value of ReadableStream.from([rawMessage]) - * .pipeThrough(new CborByteEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); - * if (value instanceof CborByteDecodedStream) { - * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); - * } else assertEquals(value, new Uint8Array(100)); - * } - * ``` - */ -export class CborByteEncoderStream - implements TransformStream { - #readable: ReadableStream; - #writable: WritableStream; - /** - * Constructs a new instance. - */ - constructor() { - const { readable, writable } = new TransformStream< - Uint8Array, - Uint8Array - >(); - this.#readable = upgradeStreamFromGen(async function* () { - yield new Uint8Array([0b010_11111]); - for await (const x of readable) { - if (x.length < 24) yield new Uint8Array([0b010_00000 + x.length]); - else if (x.length < 2 ** 8) { - yield new Uint8Array([0b010_11000, x.length]); - } else if (x.length < 2 ** 16) { - yield new Uint8Array([0b010_11001, ...numberToArray(2, x.length)]); - } else if (x.length < 2 ** 32) { - yield new Uint8Array([0b010_11010, ...numberToArray(4, x.length)]); - } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. - else yield new Uint8Array([0b010_11011, ...numberToArray(8, x.length)]); - yield x; - } - yield new Uint8Array([0b111_11111]); - }()); - this.#writable = writable; - } - - /** - * Creates a {@link CborByteEncoderStream} instance from an iterable of - * {@link Uint8Array} chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { concat } from "@std/bytes"; - * import { - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = new Uint8Array(100); - * - * for await ( - * const value of CborByteEncoderStream.from([rawMessage]) - * .readable - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); - * if (value instanceof CborByteDecodedStream) { - * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); - * } else assertEquals(value, new Uint8Array(100)); - * } - * ``` - * - * @param asyncIterable The value to encode of type - * {@link AsyncIterable} or {@link Iterable}. - * @returns A {@link CborByteEncoderStream} instance of the encoded data. - */ - static from( - asyncIterable: AsyncIterable | Iterable, - ): CborByteEncoderStream { - const encoder = new CborByteEncoderStream(); - ReadableStream.from(asyncIterable).pipeTo(encoder.writable); - return encoder; - } - - /** - * The {@link ReadableStream} associated with the instance, which - * provides the encoded CBOR data as {@link Uint8Array} chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { concat } from "@std/bytes"; - * import { - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = new Uint8Array(100); - * - * for await ( - * const value of ReadableStream.from([rawMessage]) - * .pipeThrough(new CborByteEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); - * if (value instanceof CborByteDecodedStream) { - * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); - * } else assertEquals(value, new Uint8Array(100)); - * } - * ``` - * - * @returns A {@link ReadableStream}. - */ - get readable(): ReadableStream { - return this.#readable; - } - - /** - * The {@link WritableStream} associated with the instance, which - * accepts {@link Uint8Array} chunks to be encoded into CBOR format. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { concat } from "@std/bytes"; - * import { - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = new Uint8Array(100); - * - * for await ( - * const value of ReadableStream.from([rawMessage]) - * .pipeThrough(new CborByteEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); - * if (value instanceof CborByteDecodedStream) { - * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); - * } else assertEquals(value, new Uint8Array(100)); - * } - * ``` - * - * @returns a {@link WritableStream}. - */ - get writable(): WritableStream { - return this.#writable; - } -} - -/** - * A {@link TransformStream} that encodes a {@link ReadableStream} into - * CBOR "Indefinite Length Text String". - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * **Notice:** Each chunk of the {@link ReadableStream} is encoded as - * its own "Definite Length Text String" meaning space can be saved if large - * chunks are pipped through instead of small chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborSequenceDecoderStream, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = "a".repeat(100); - * - * for await ( - * const value of ReadableStream.from([rawMessage]) - * .pipeThrough(new CborTextEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(typeof value === "string" || value instanceof CborTextDecodedStream); - * if (value instanceof CborTextDecodedStream) { - * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); - * } else assertEquals(value, rawMessage); - * } - * ``` - */ -export class CborTextEncoderStream - implements TransformStream { - #readable: ReadableStream; - #writable: WritableStream; - /** - * Constructs a new instance. - */ - constructor() { - const { readable, writable } = new TransformStream(); - this.#readable = upgradeStreamFromGen(async function* () { - yield new Uint8Array([0b011_11111]); - for await (const x of readable.pipeThrough(new TextEncoderStream())) { - if (x.length < 24) yield new Uint8Array([0b011_00000 + x.length]); - else if (x.length < 2 ** 8) { - yield new Uint8Array([0b011_11000, x.length]); - } else if (x.length < 2 ** 16) { - yield new Uint8Array([0b011_11001, ...numberToArray(2, x.length)]); - } else if (x.length < 2 ** 32) { - yield new Uint8Array([0b011_11010, ...numberToArray(4, x.length)]); - } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. - else yield new Uint8Array([0b011_11011, ...numberToArray(8, x.length)]); - yield x; - } - yield new Uint8Array([0b111_11111]); - }()); - this.#writable = writable; - } - - /** - * Creates a {@link CborTextEncoderStream} instance from an iterable of - * {@link string} chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborSequenceDecoderStream, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = "a".repeat(100); - * - * for await ( - * const value of CborTextEncoderStream.from([rawMessage]) - * .readable - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(typeof value === "string" || value instanceof CborTextDecodedStream); - * if (value instanceof CborTextDecodedStream) { - * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); - * } else assertEquals(value, rawMessage); - * } - * ``` - * - * @param asyncIterable The value to encode of type - * {@link AsyncIterable} or {@link Iterable} - * @returns A {@link CborTextEncoderStream} instance of the encoded data. - */ - static from( - asyncIterable: AsyncIterable | Iterable, - ): CborTextEncoderStream { - const encoder = new CborTextEncoderStream(); - ReadableStream.from(asyncIterable).pipeTo(encoder.writable); - return encoder; - } - - /** - * The {@link ReadableStream} associated with the instance, which - * provides the encoded CBOR data as {@link Uint8Array} chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborSequenceDecoderStream, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = "a".repeat(100); - * - * for await ( - * const value of ReadableStream.from([rawMessage]) - * .pipeThrough(new CborTextEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(typeof value === "string" || value instanceof CborTextDecodedStream); - * if (value instanceof CborTextDecodedStream) { - * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); - * } else assertEquals(value, rawMessage); - * } - * ``` - * - * @returns A {@link ReadableStream}. - */ - get readable(): ReadableStream { - return this.#readable; - } - - /** - * The {@link WritableStream} associated with the instance, which - * accepts {@link string} chunks to be encoded into CBOR format. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborSequenceDecoderStream, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = "a".repeat(100); - * - * for await ( - * const value of ReadableStream.from([rawMessage]) - * .pipeThrough(new CborTextEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(typeof value === "string" || value instanceof CborTextDecodedStream); - * if (value instanceof CborTextDecodedStream) { - * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); - * } else assertEquals(value, rawMessage); - * } - * ``` - * - * @returns A {@link WritableStream}. - */ - get writable(): WritableStream { - return this.#writable; - } -} - -/** - * A {@link TransformStream} that encodes a - * {@link ReadableStream} into CBOR "Indefinite Length Array". - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; - * - * for await ( - * const value of ReadableStream.from(rawMessage) - * .pipeThrough(new CborArrayEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborArrayDecodedStream); - * let i = 0; - * for await (const text of value) { - * assert(typeof text === "string"); - * assertEquals(text, rawMessage[i++]); - * } - * } - * ``` - */ -export class CborArrayEncoderStream - implements TransformStream { - #readable: ReadableStream; - #writable: WritableStream; - /** - * Constructs a new instance. - */ - constructor() { - const { readable, writable } = new TransformStream< - CborInputStream, - CborInputStream - >(); - this.#readable = upgradeStreamFromGen(async function* () { - yield new Uint8Array([0b100_11111]); - for await ( - const x of readable.pipeThrough(new CborSequenceEncoderStream()) - ) { - yield x; - } - yield new Uint8Array([0b111_11111]); - }()); - this.#writable = writable; - } - - /** - * Creates a {@link CborArrayEncoderStream} instance from an iterable of - * {@link CborInputStream} chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; - * - * for await ( - * const value of CborArrayEncoderStream.from(rawMessage) - * .readable - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborArrayDecodedStream); - * let i = 0; - * for await (const text of value) { - * assert(typeof text === "string"); - * assertEquals(text, rawMessage[i++]); - * } - * } - * ``` - * - * @param asyncIterable The value to encode of type - * {@link AsyncIterable} or - * {@link Iterable}. - * @returns A {@link CborArrayEncoderStream} instance of the encoded data. - */ - static from( - asyncIterable: AsyncIterable | Iterable, - ): CborArrayEncoderStream { - const encoder = new CborArrayEncoderStream(); - ReadableStream.from(asyncIterable).pipeTo(encoder.writable); - return encoder; - } - - /** - * The {@link ReadableStream} associated with the instance, which - * provides the encoded CBOR data as {@link Uint8Array} chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; - * - * for await ( - * const value of ReadableStream.from(rawMessage) - * .pipeThrough(new CborArrayEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborArrayDecodedStream); - * let i = 0; - * for await (const text of value) { - * assert(typeof text === "string"); - * assertEquals(text, rawMessage[i++]); - * } - * } - * ``` - * - * @returns A {@link ReadableStream}. - */ - get readable(): ReadableStream { - return this.#readable; - } - - /** - * The {@link WritableStream} associated with the instance, - * which accepts {@link CborInputStream} chunks to be encoded into CBOR - * format. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; - * - * for await ( - * const value of ReadableStream.from(rawMessage) - * .pipeThrough(new CborArrayEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborArrayDecodedStream); - * let i = 0; - * for await (const text of value) { - * assert(typeof text === "string"); - * assertEquals(text, rawMessage[i++]); - * } - * } - * ``` - * - * @returns A {@link WritableStream}. - */ - get writable(): WritableStream { - return this.#writable; - } -} - -/** - * A {@link TransformStream} that encodes a - * {@link ReadableStream} into CBOR "Indefinite Length Map". - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborMapDecodedStream, - * CborMapEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage: Record = { - * a: 0, - * b: 1, - * c: 2, - * d: 3, - * }; - * - * for await ( - * const value of ReadableStream.from(Object.entries(rawMessage)) - * .pipeThrough(new CborMapEncoderStream) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborMapDecodedStream); - * for await (const [k, v] of value) { - * assertEquals(rawMessage[k], v); - * } - * } - * ``` - */ -export class CborMapEncoderStream - implements TransformStream { - #readable: ReadableStream; - #writable: WritableStream; - /** - * Constructs a new instance. - */ - constructor() { - const { readable, writable } = new TransformStream< - CborMapInputStream, - CborMapInputStream - >(); - this.#readable = upgradeStreamFromGen(async function* () { - yield new Uint8Array([0b101_11111]); - for await (const [k, v] of readable) { - yield encodeCbor(k); - for await (const x of CborSequenceEncoderStream.from([v]).readable) { - yield x; - } - } - yield new Uint8Array([0b111_11111]); - }()); - this.#writable = writable; - } - - /** - * Creates a {@link CborMapEncoderStream} instance from an iterable of - * {@link CborMapInputStream} chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborMapDecodedStream, - * CborMapEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage: Record = { - * a: 0, - * b: 1, - * c: 2, - * d: 3, - * }; - * - * for await ( - * const value of CborMapEncoderStream.from(Object.entries(rawMessage)) - * .readable - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborMapDecodedStream); - * for await (const [k, v] of value) { - * assertEquals(rawMessage[k], v); - * } - * } - * ``` - * - * @param asyncIterable The value to encode of type - * {@link AsyncIterable} or - * {@link Iterable}. - * @returns A {@link CborMapEncoderStream} instance of the encoded data. - */ - static from( - asyncIterable: - | AsyncIterable - | Iterable, - ): CborMapEncoderStream { - const encoder = new CborMapEncoderStream(); - ReadableStream.from(asyncIterable).pipeTo(encoder.writable); - return encoder; - } - - /** - * The {@link ReadableStream} associated with the instance, which - * provides the encoded CBOR data as {@link Uint8Array} chunks. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborMapDecodedStream, - * CborMapEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage: Record = { - * a: 0, - * b: 1, - * c: 2, - * d: 3, - * }; - * - * for await ( - * const value of ReadableStream.from(Object.entries(rawMessage)) - * .pipeThrough(new CborMapEncoderStream) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborMapDecodedStream); - * for await (const [k, v] of value) { - * assertEquals(rawMessage[k], v); - * } - * } - * ``` - * - * @returns A {@link ReadableStream}. - */ - get readable(): ReadableStream { - return this.#readable; - } - - /** - * The {@link WritableStream} associated with the - * instance, which accepts {@link CborMapInputStream} chunks to be encoded - * into CBOR format. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborMapDecodedStream, - * CborMapEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage: Record = { - * a: 0, - * b: 1, - * c: 2, - * d: 3, - * }; - * - * for await ( - * const value of ReadableStream.from(Object.entries(rawMessage)) - * .pipeThrough(new CborMapEncoderStream) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborMapDecodedStream); - * for await (const [k, v] of value) { - * assertEquals(rawMessage[k], v); - * } - * } - * ``` - * - * @returns A {@link WritableStream}. - */ - get writable(): WritableStream { - return this.#writable; - } -} - -/** - * A {@link TransformStream} that encodes a - * {@link ReadableStream} into CBOR format sequence. - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * @example Usage - * ```ts no-assert - * import { encodeBase64Url } from "@std/encoding"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborMapDecodedStream, - * CborMapEncoderStream, - * type CborOutputStream, - * CborSequenceDecoderStream, - * CborSequenceEncoderStream, - * CborTag, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = [ - * undefined, - * null, - * true, - * false, - * 3.14, - * 5, - * 2n ** 32n, - * "Hello World", - * new Uint8Array(25), - * new Date(), - * new CborTag(33, encodeBase64Url(new Uint8Array(7))), - * ["cake", "carrot"], - * { a: 3, b: "d" }, - * CborByteEncoderStream.from([new Uint8Array(7)]), - * CborTextEncoderStream.from(["Bye!"]), - * CborArrayEncoderStream.from([ - * "Hey!", - * CborByteEncoderStream.from([new Uint8Array(18)]), - * ]), - * CborMapEncoderStream.from([ - * ["a", 0], - * ["b", "potato"], - * ]), - * ]; - * - * async function logValue(value: CborOutputStream) { - * if ( - * value instanceof CborByteDecodedStream || - * value instanceof CborTextDecodedStream - * ) { - * for await (const x of value) console.log(x); - * } else if (value instanceof CborArrayDecodedStream) { - * for await (const x of value) logValue(x); - * } else if (value instanceof CborMapDecodedStream) { - * for await (const [k, v] of value) { - * console.log(k); - * logValue(v); - * } - * } else if (value instanceof CborTag) { - * console.log(value); - * logValue(value.tagContent); - * } else console.log(value); - * } - * - * for await ( - * const value of ReadableStream.from(rawMessage) - * .pipeThrough(new CborSequenceEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * logValue(value); - * } - * ``` - */ -export class CborSequenceEncoderStream - implements TransformStream { - #readable: ReadableStream; - #writable: WritableStream; - /** - * Constructs a new instance. - */ - constructor() { - const { readable, writable } = new TransformStream< - CborInputStream, - CborInputStream - >(); - this.#readable = upgradeStreamFromGen(this.#encodeFromReadable(readable)); - this.#writable = writable; - } - - async *#encodeFromReadable( - readable: ReadableStream, - ): AsyncGenerator { - for await (const x of readable) { - for await (const y of this.#encode(x)) { - yield y; - } - } - } - - async *#encode( - x: CborInputStream, - ): AsyncGenerator { - if ( - x instanceof CborByteEncoderStream || - x instanceof CborTextEncoderStream || - x instanceof CborArrayEncoderStream || - x instanceof CborMapEncoderStream - ) { - for await (const y of x.readable) { - yield y; - } - } else if (x instanceof Array) { - for await (const y of this.#encodeArray(x)) { - yield y; - } - } else if (x instanceof CborTag) { - for await (const y of this.#encodeTag(x)) { - yield y; - } - } else if (typeof x === "object" && x !== null) { - if (x instanceof Date || x instanceof Uint8Array) yield encodeCbor(x); - else { - for await (const y of this.#encodeObject(x)) { - yield y; - } - } - } else yield encodeCbor(x); - } - - async *#encodeArray(x: CborInputStream[]): AsyncGenerator { - if (x.length < 24) yield new Uint8Array([0b100_00000 + x.length]); - else if (x.length < 2 ** 8) yield new Uint8Array([0b100_11000, x.length]); - else if (x.length < 2 ** 16) { - yield new Uint8Array([0b100_11001, ...numberToArray(2, x.length)]); - } else if (x.length < 2 ** 32) { - yield new Uint8Array([0b100_11010, ...numberToArray(4, x.length)]); - } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support an `Array` being that large. - else yield new Uint8Array([0b100_11011, ...numberToArray(8, x.length)]); - for (const y of x) { - for await (const z of this.#encode(y)) { - yield z; - } - } - } - - async *#encodeObject( - x: { [k: string]: CborInputStream }, - ): AsyncGenerator { - const len = Object.keys(x).length; - if (len < 24) yield new Uint8Array([0b101_00000 + len]); - else if (len < 2 ** 8) yield new Uint8Array([0b101_11000, len]); - else if (len < 2 ** 16) { - yield new Uint8Array([0b101_11001, ...numberToArray(2, len)]); - } else if (len < 2 ** 32) { - yield new Uint8Array([0b101_11010, ...numberToArray(4, len)]); - } // Can safely assume `len < 2 ** 64` as JavaScript doesn't support an `Object` being that Large. - else yield new Uint8Array([0b101_11011, ...numberToArray(8, len)]); - for (const [k, v] of Object.entries(x)) { - yield encodeCbor(k); - for await (const y of this.#encode(v)) { - yield y; - } - } - } - - async *#encodeTag(x: CborTag): AsyncGenerator { - const tagNumber = BigInt(x.tagNumber); - if (tagNumber < 0n) { - throw new RangeError( - `Cannot encode Tag Item: Tag Number (${x.tagNumber}) is less than zero`, - ); - } - if (tagNumber > 2n ** 64n) { - throw new RangeError( - `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, - ); - } - - const head = encodeCbor(tagNumber); - head[0]! += 0b110_00000; - yield head; - for await (const y of this.#encode(x.tagContent)) { - yield y; - } - } - - /** - * Creates a {@link CborSequenceEncoderStream} instance from an iterable of - * {@link CborInputStream} chunks. - * - * @example Usage - * ```ts no-assert - * import { encodeBase64Url } from "@std/encoding"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborMapDecodedStream, - * CborMapEncoderStream, - * type CborOutputStream, - * CborSequenceDecoderStream, - * CborSequenceEncoderStream, - * CborTag, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = [ - * undefined, - * null, - * true, - * false, - * 3.14, - * 5, - * 2n ** 32n, - * "Hello World", - * new Uint8Array(25), - * new Date(), - * new CborTag(33, encodeBase64Url(new Uint8Array(7))), - * ["cake", "carrot"], - * { a: 3, b: "d" }, - * CborByteEncoderStream.from([new Uint8Array(7)]), - * CborTextEncoderStream.from(["Bye!"]), - * CborArrayEncoderStream.from([ - * "Hey!", - * CborByteEncoderStream.from([new Uint8Array(18)]), - * ]), - * CborMapEncoderStream.from([ - * ["a", 0], - * ["b", "potato"], - * ]), - * ]; - * - * async function logValue(value: CborOutputStream) { - * if ( - * value instanceof CborByteDecodedStream || - * value instanceof CborTextDecodedStream - * ) { - * for await (const x of value) console.log(x); - * } else if (value instanceof CborArrayDecodedStream) { - * for await (const x of value) logValue(x); - * } else if (value instanceof CborMapDecodedStream) { - * for await (const [k, v] of value) { - * console.log(k); - * logValue(v); - * } - * } else if (value instanceof CborTag) { - * console.log(value); - * logValue(value.tagContent); - * } else console.log(value); - * } - * - * for await ( - * const value of CborSequenceEncoderStream.from(rawMessage) - * .readable - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * logValue(value); - * } - * ``` - * - * @param asyncIterable The value to encode of type - * {@link AsyncIterable} or - * {@link Iterable}. - * @returns A {@link CborSequenceEncoderStream} instance of the encoded data. - */ - static from( - asyncIterable: AsyncIterable | Iterable, - ): CborSequenceEncoderStream { - const encoder = new CborSequenceEncoderStream(); - ReadableStream.from(asyncIterable).pipeTo(encoder.writable); - return encoder; - } - - /** - * The {@link ReadableStream} associated with the instance, which - * provides the encoded CBOR data as {@link Uint8Array} chunks. - * - * @example Usage - * ```ts no-assert - * import { encodeBase64Url } from "@std/encoding"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborMapDecodedStream, - * CborMapEncoderStream, - * type CborOutputStream, - * CborSequenceDecoderStream, - * CborSequenceEncoderStream, - * CborTag, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = [ - * undefined, - * null, - * true, - * false, - * 3.14, - * 5, - * 2n ** 32n, - * "Hello World", - * new Uint8Array(25), - * new Date(), - * new CborTag(33, encodeBase64Url(new Uint8Array(7))), - * ["cake", "carrot"], - * { a: 3, b: "d" }, - * CborByteEncoderStream.from([new Uint8Array(7)]), - * CborTextEncoderStream.from(["Bye!"]), - * CborArrayEncoderStream.from([ - * "Hey!", - * CborByteEncoderStream.from([new Uint8Array(18)]), - * ]), - * CborMapEncoderStream.from([ - * ["a", 0], - * ["b", "potato"], - * ]), - * ]; - * - * async function logValue(value: CborOutputStream) { - * if ( - * value instanceof CborByteDecodedStream || - * value instanceof CborTextDecodedStream - * ) { - * for await (const x of value) console.log(x); - * } else if (value instanceof CborArrayDecodedStream) { - * for await (const x of value) logValue(x); - * } else if (value instanceof CborMapDecodedStream) { - * for await (const [k, v] of value) { - * console.log(k); - * logValue(v); - * } - * } else if (value instanceof CborTag) { - * console.log(value); - * logValue(value.tagContent); - * } else console.log(value); - * } - * - * for await ( - * const value of ReadableStream.from(rawMessage) - * .pipeThrough(new CborSequenceEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * logValue(value); - * } - * ``` - * - * @returns A {@link ReadableStream}. - */ - get readable(): ReadableStream { - return this.#readable; - } - - /** - * The {@link WritableStream} associated with the - * instance, which accepts {@link CborInputStream} chunks to be encoded - * into CBOR format. - * - * @example Usage - * ```ts no-assert - * import { encodeBase64Url } from "@std/encoding"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborMapDecodedStream, - * CborMapEncoderStream, - * type CborOutputStream, - * CborSequenceDecoderStream, - * CborSequenceEncoderStream, - * CborTag, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = [ - * undefined, - * null, - * true, - * false, - * 3.14, - * 5, - * 2n ** 32n, - * "Hello World", - * new Uint8Array(25), - * new Date(), - * new CborTag(33, encodeBase64Url(new Uint8Array(7))), - * ["cake", "carrot"], - * { a: 3, b: "d" }, - * CborByteEncoderStream.from([new Uint8Array(7)]), - * CborTextEncoderStream.from(["Bye!"]), - * CborArrayEncoderStream.from([ - * "Hey!", - * CborByteEncoderStream.from([new Uint8Array(18)]), - * ]), - * CborMapEncoderStream.from([ - * ["a", 0], - * ["b", "potato"], - * ]), - * ]; - * - * async function logValue(value: CborOutputStream) { - * if ( - * value instanceof CborByteDecodedStream || - * value instanceof CborTextDecodedStream - * ) { - * for await (const x of value) console.log(x); - * } else if (value instanceof CborArrayDecodedStream) { - * for await (const x of value) logValue(x); - * } else if (value instanceof CborMapDecodedStream) { - * for await (const [k, v] of value) { - * console.log(k); - * logValue(v); - * } - * } else if (value instanceof CborTag) { - * console.log(value); - * logValue(value.tagContent); - * } else console.log(value); - * } - * - * for await ( - * const value of ReadableStream.from(rawMessage) - * .pipeThrough(new CborSequenceEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * logValue(value); - * } - * ``` - * - * @returns A {@link WritableStream}. - */ - get writable(): WritableStream { - return this.#writable; - } -} diff --git a/cbor/encode_stream_test.ts b/cbor/encode_stream_test.ts deleted file mode 100644 index c444cdbd0cf4..000000000000 --- a/cbor/encode_stream_test.ts +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -import { assertEquals } from "@std/assert"; -import { concat } from "@std/bytes"; -import { - CborArrayEncoderStream, - CborByteEncoderStream, - CborMapEncoderStream, - type CborMapInputStream, - CborSequenceEncoderStream, - CborTag, - CborTextEncoderStream, - type CborType, - encodeCbor, -} from "./mod.ts"; - -function random(start: number, end: number): number { - return Math.floor(Math.random() * (end - start) + start); -} - -Deno.test("CborByteEncoderStream()", async () => { - const bytes = [ - new Uint8Array(random(0, 24)), - new Uint8Array(random(24, 2 ** 8)), - new Uint8Array(random(2 ** 8, 2 ** 16)), - new Uint8Array(random(2 ** 16, 2 ** 17)), - ]; - - const expectedOutput = concat([ - new Uint8Array([0b010_11111]), - ...bytes.map((x) => encodeCbor(x)), - new Uint8Array([0b111_11111]), - ]); - - const actualOutput = concat( - await Array.fromAsync( - ReadableStream.from(bytes).pipeThrough(new CborByteEncoderStream()), - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborTextEncoderStream()", async () => { - const strings = [ - "a".repeat(random(0, 24)), - "a".repeat(random(24, 2 ** 8)), - "a".repeat(random(2 ** 8, 2 ** 16)), - "a".repeat(random(2 ** 16, 2 ** 17)), - ]; - - const expectedOutput = concat([ - new Uint8Array([0b011_11111]), - ...strings.filter((x) => x).map((x) => encodeCbor(x)), - new Uint8Array([0b111_11111]), - ]); - - const actualOutput = concat( - await Array.fromAsync( - ReadableStream.from(strings).pipeThrough(new CborTextEncoderStream()), - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborArrayEncoderStream()", async () => { - const arrays = [random(0, 2 ** 32)]; - - const expectedOutput = concat([ - new Uint8Array([0b100_11111]), - ...arrays.map((x) => encodeCbor(x)), - new Uint8Array([0b111_11111]), - ]); - - const actualOutput = concat( - await Array.fromAsync( - ReadableStream.from(arrays).pipeThrough(new CborArrayEncoderStream()), - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborMapEncoderStream()", async () => { - const maps: CborMapInputStream[] = [["a", random(0, 2 ** 32)]]; - - const expectedOutput = concat([ - new Uint8Array([0b101_11111]), - ...maps.map(([k, v]) => [encodeCbor(k), encodeCbor(v as CborType)]).flat(), - new Uint8Array([0b111_11111]), - ]); - - const actualOutput = concat( - await Array.fromAsync( - ReadableStream.from(maps).pipeThrough(new CborMapEncoderStream()), - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborSequenceEncoderStream()", async () => { - const input = [ - undefined, - null, - true, - false, - random(0, 24), - BigInt(random(0, 24)), - "a".repeat(random(0, 24)), - new Uint8Array(random(0, 24)), - new Date(), - ]; - - const expectedOutput = concat(input.map((x) => encodeCbor(x))); - - const actualOutput = concat( - await Array.fromAsync( - ReadableStream.from(input).pipeThrough(new CborSequenceEncoderStream()), - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborByteEncoderStream.from()", async () => { - const bytes = [ - new Uint8Array(random(0, 24)), - new Uint8Array(random(24, 2 ** 8)), - new Uint8Array(random(2 ** 8, 2 ** 16)), - new Uint8Array(random(2 ** 16, 2 ** 17)), - ]; - - const expectedOutput = concat([ - Uint8Array.from([0b010_11111]), - ...bytes.map((x) => encodeCbor(x)), - Uint8Array.from([0b111_11111]), - ]); - - const actualOutput = concat( - await Array.fromAsync( - CborByteEncoderStream.from(bytes).readable, - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborTextEncoderStream.from()", async () => { - const strings = [ - "a".repeat(random(0, 24)), - "a".repeat(random(24, 2 ** 8)), - "a".repeat(random(2 ** 8, 2 ** 16)), - "a".repeat(random(2 ** 16, 2 ** 17)), - ]; - - const expectedOutput = concat([ - new Uint8Array([0b011_11111]), - ...strings.filter((x) => x).map((x) => encodeCbor(x)), - new Uint8Array([0b111_11111]), - ]); - - const actualOutput = concat( - await Array.fromAsync( - CborTextEncoderStream.from(strings).readable, - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborArrayEncoderStream.from()", async () => { - const arrays = [random(0, 2 ** 32)]; - - const expectedOutput = concat([ - new Uint8Array([0b100_11111]), - ...arrays.map((x) => encodeCbor(x)), - new Uint8Array([0b111_11111]), - ]); - - const actualOutput = concat( - await Array.fromAsync( - CborArrayEncoderStream.from(arrays).readable, - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborMapEncoderStream.from()", async () => { - const maps: CborMapInputStream[] = [["a", random(0, 2 ** 32)]]; - - const expectedOutput = concat([ - new Uint8Array([0b101_11111]), - ...maps.map(([k, v]) => [encodeCbor(k), encodeCbor(v as CborType)]).flat(), - new Uint8Array([0b111_11111]), - ]); - - const actualOutput = concat( - await Array.fromAsync( - CborMapEncoderStream.from(maps).readable, - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborSequenceEncoderStream() accepting the other streams", async () => { - // Inputs should be identical. We need two of them as the contents will be consumed when calculating expectedOutput and actualOutput. - const input1 = [ - CborByteEncoderStream.from([new Uint8Array(10), new Uint8Array(20)]), - CborTextEncoderStream.from(["a".repeat(10), "b".repeat(20)]), - CborArrayEncoderStream.from([10, 20]), - CborMapEncoderStream.from([["a", 0], ["b", 1], ["c", 2], ["d", 3]]), - ]; - const input2 = [ - CborByteEncoderStream.from([new Uint8Array(10), new Uint8Array(20)]), - CborTextEncoderStream.from(["a".repeat(10), "b".repeat(20)]), - CborArrayEncoderStream.from([10, 20]), - CborMapEncoderStream.from([["a", 0], ["b", 1], ["c", 2], ["d", 3]]), - ]; - - const expectedOutput = concat( - await Promise.all( - input1.map(async (stream) => - concat(await Array.fromAsync(stream.readable)) - ), - ), - ); - - const actualOutput = concat( - await Array.fromAsync( - CborSequenceEncoderStream.from(input2).readable, - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborSequenceEncoderStream() accepting CborInputStream[]", async () => { - const input = [ - new Array(random(0, 24)).fill(0), - new Array(random(24, 2 ** 8)).fill(0), - new Array(random(2 ** 8, 2 ** 16)).fill(0), - new Array(random(2 ** 16, 2 ** 17)).fill(0), - ]; - - const expectedOutput = concat(input.map((x) => encodeCbor(x))); - - const actualOutput = concat( - await Array.fromAsync( - CborSequenceEncoderStream.from(input).readable, - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborSequenceEncoderStream() accepting { [k: string]: CborInputStream }", async () => { - const input = [ - Object.fromEntries( - new Array(random(10, 20)).fill(0).map(( - _, - i, - ) => [String.fromCharCode(97 + i), false]), - ), - ]; - - const expectedOutput = concat(input.map((x) => encodeCbor(x))); - - const actualOutput = concat( - await Array.fromAsync( - CborSequenceEncoderStream.from(input).readable, - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); - -Deno.test("CborSequenceEncoderStream() accepting CborTag()", async () => { - const input = [ - new CborTag(0, 0), - new CborTag(1, 1), - new CborTag(2, 2), - new CborTag(3, 3), - ]; - - const expectedOutput = concat(input.map((x) => encodeCbor(x))); - - const actualOutput = concat( - await Array.fromAsync( - CborSequenceEncoderStream.from(input).readable, - ), - ); - - assertEquals(actualOutput, expectedOutput); -}); diff --git a/cbor/map_decoded_stream.ts b/cbor/map_decoded_stream.ts new file mode 100644 index 000000000000..d004378cdc65 --- /dev/null +++ b/cbor/map_decoded_stream.ts @@ -0,0 +1,68 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import type { ReleaseLock } from "./_common.ts"; +import type { CborMapOutputStream } from "./types.ts"; + +/** + * A {@link ReadableStream} that wraps the decoded CBOR "Map". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * Instances of this class is created from {@link CborSequenceDecoderStream}. + * This class is not designed for you to create instances of it yourself. It is + * merely a way for you to validate the type being returned. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; + * + * for await ( + * const value of ReadableStream.from(Object.entries(rawMessage)) + * .pipeThrough(new CborMapEncoderStream) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } + * ``` + */ +export class CborMapDecodedStream extends ReadableStream { + /** + * Constructs a new instance. + * + * @param gen A {@link AsyncGenerator}. + * @param releaseLock A Function that's called when the stream is finished. + */ + constructor( + gen: AsyncGenerator, + releaseLock: ReleaseLock, + ) { + super({ + async pull(controller) { + const { done, value } = await gen.next(); + if (done) { + releaseLock(); + controller.close(); + } else controller.enqueue(value); + }, + async cancel() { + // deno-lint-ignore no-empty + for await (const _ of gen) {} + releaseLock(); + }, + }); + } +} diff --git a/cbor/map_decoded_stream_test.ts b/cbor/map_decoded_stream_test.ts new file mode 100644 index 000000000000..3ea49cb8ad8b --- /dev/null +++ b/cbor/map_decoded_stream_test.ts @@ -0,0 +1,26 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals } from "@std/assert"; +import { random } from "./_common_test.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborMapDecodedStream } from "./map_decoded_stream.ts"; +import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; +import type { CborMapOutputStream } from "./types.ts"; + +Deno.test("CborSequenceDecoderStream() decoding Objects", async () => { + const size = random(0, 24); + const entries = new Array(size).fill(0).map((_, i) => + [String.fromCharCode(97 + i), i] satisfies CborMapOutputStream + ); + + const reader = ReadableStream.from([encodeCbor(Object.fromEntries(entries))]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborMapDecodedStream); + assertEquals(await Array.fromAsync(value), entries); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/map_encoder_stream.ts b/cbor/map_encoder_stream.ts new file mode 100644 index 000000000000..4c99d62f9200 --- /dev/null +++ b/cbor/map_encoder_stream.ts @@ -0,0 +1,189 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { upgradeStreamFromGen } from "./_common.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborSequenceEncoderStream } from "./sequence_encoder_stream.ts"; +import type { CborMapInputStream } from "./types.ts"; + +/** + * A {@link TransformStream} that encodes a + * {@link ReadableStream} into CBOR "Indefinite Length Map". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; + * + * for await ( + * const value of ReadableStream.from(Object.entries(rawMessage)) + * .pipeThrough(new CborMapEncoderStream) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } + * ``` + */ +export class CborMapEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + CborMapInputStream, + CborMapInputStream + >(); + this.#readable = upgradeStreamFromGen(async function* () { + yield new Uint8Array([0b101_11111]); + for await (const [k, v] of readable) { + yield encodeCbor(k); + for await (const x of CborSequenceEncoderStream.from([v]).readable) { + yield x; + } + } + yield new Uint8Array([0b111_11111]); + }()); + this.#writable = writable; + } + + /** + * Creates a {@link CborMapEncoderStream} instance from an iterable of + * {@link CborMapInputStream} chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; + * + * for await ( + * const value of CborMapEncoderStream.from(Object.entries(rawMessage)) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } + * ``` + * + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or + * {@link Iterable}. + * @returns A {@link CborMapEncoderStream} instance of the encoded data. + */ + static from( + asyncIterable: + | AsyncIterable + | Iterable, + ): CborMapEncoderStream { + const encoder = new CborMapEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; + * + * for await ( + * const value of ReadableStream.from(Object.entries(rawMessage)) + * .pipeThrough(new CborMapEncoderStream) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } + * ``` + * + * @returns A {@link ReadableStream}. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The {@link WritableStream} associated with the + * instance, which accepts {@link CborMapInputStream} chunks to be encoded + * into CBOR format. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborMapDecodedStream, + * CborMapEncoderStream, + * CborSequenceDecoderStream, + * } from "@std/cbor"; + * + * const rawMessage: Record = { + * a: 0, + * b: 1, + * c: 2, + * d: 3, + * }; + * + * for await ( + * const value of ReadableStream.from(Object.entries(rawMessage)) + * .pipeThrough(new CborMapEncoderStream) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(value instanceof CborMapDecodedStream); + * for await (const [k, v] of value) { + * assertEquals(rawMessage[k], v); + * } + * } + * ``` + * + * @returns A {@link WritableStream}. + */ + get writable(): WritableStream { + return this.#writable; + } +} diff --git a/cbor/map_encoder_stream_test.ts b/cbor/map_encoder_stream_test.ts new file mode 100644 index 000000000000..3e4e96dc13a2 --- /dev/null +++ b/cbor/map_encoder_stream_test.ts @@ -0,0 +1,44 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { random } from "./_common_test.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborMapEncoderStream } from "./map_encoder_stream.ts"; +import type { CborMapInputStream, CborType } from "./types.ts"; + +Deno.test("CborMapEncoderStream()", async () => { + const maps: CborMapInputStream[] = [["a", random(0, 2 ** 32)]]; + + const expectedOutput = concat([ + new Uint8Array([0b101_11111]), + ...maps.map(([k, v]) => [encodeCbor(k), encodeCbor(v as CborType)]).flat(), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(maps).pipeThrough(new CborMapEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborMapEncoderStream.from()", async () => { + const maps: CborMapInputStream[] = [["a", random(0, 2 ** 32)]]; + + const expectedOutput = concat([ + new Uint8Array([0b101_11111]), + ...maps.map(([k, v]) => [encodeCbor(k), encodeCbor(v as CborType)]).flat(), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + CborMapEncoderStream.from(maps).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); diff --git a/cbor/mod.ts b/cbor/mod.ts index eee6b2e3b6f4..3fc7ee468e8b 100644 --- a/cbor/mod.ts +++ b/cbor/mod.ts @@ -34,7 +34,19 @@ * * @module */ -export * from "./decode.ts"; -export * from "./encode.ts"; -export * from "./decode_stream.ts"; -export * from "./encode_stream.ts"; +export * from "./array_decoded_stream.ts"; +export * from "./array_encoder_stream.ts"; +export * from "./byte_decoded_stream.ts"; +export * from "./byte_encoder_stream.ts"; +export * from "./decode_cbor_sequence.ts"; +export * from "./decode_cbor.ts"; +export * from "./encode_cbor_sequence.ts"; +export * from "./encode_cbor.ts"; +export * from "./map_decoded_stream.ts"; +export * from "./map_encoder_stream.ts"; +export * from "./sequence_decoder_stream.ts"; +export * from "./sequence_encoder_stream.ts"; +export * from "./tag.ts"; +export * from "./text_decoded_stream.ts"; +export * from "./text_encoder_stream.ts"; +export * from "./types.ts"; diff --git a/cbor/decode_stream.ts b/cbor/sequence_decoder_stream.ts similarity index 71% rename from cbor/decode_stream.ts rename to cbor/sequence_decoder_stream.ts index 4f5a625a0a72..773717d60965 100644 --- a/cbor/decode_stream.ts +++ b/cbor/sequence_decoder_stream.ts @@ -1,261 +1,16 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { arrayToNumber, upgradeStreamFromGen } from "./_common.ts"; -import { type CborPrimitiveType, CborTag } from "./encode.ts"; - -/** - * Specifies the decodable value types for the {@link CborSequenceDecoderStream} - * and {@link CborMapDecodedStream}. - */ -export type CborOutputStream = - | CborPrimitiveType - | CborTag - | CborByteDecodedStream - | CborTextDecodedStream - | CborArrayDecodedStream - | CborMapDecodedStream; - -/** - * Specifies the structure of the output for the {@link CborMapDecodedStream}. - */ -export type CborMapOutputStream = [string, CborOutputStream]; - -type ReleaseLock = (value?: unknown) => void; - -/** - * A {@link ReadableStream} that wraps the decoded CBOR "Byte String". - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * Instances of this class is created from {@link CborSequenceDecoderStream}. - * This class is not designed for you to create instances of it yourself. It is - * merely a way for you to validate the type being returned. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { concat } from "@std/bytes"; - * import { - * CborByteDecodedStream, - * CborByteEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = new Uint8Array(100); - * - * for await ( - * const value of ReadableStream.from([rawMessage]) - * .pipeThrough(new CborByteEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof Uint8Array || value instanceof CborByteDecodedStream); - * if (value instanceof CborByteDecodedStream) { - * assertEquals(concat(await Array.fromAsync(value)), new Uint8Array(100)); - * } else assertEquals(value, new Uint8Array(100)); - * } - * ``` - */ -export class CborByteDecodedStream extends ReadableStream { - /** - * Constructs a new instance. - * - * @param gen A {@link AsyncGenerator}. - * @param releaseLock A Function that's called when the stream is finished. - */ - constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { - super({ - async pull(controller) { - const { done, value } = await gen.next(); - if (done) { - releaseLock(); - controller.close(); - } else controller.enqueue(value); - }, - async cancel() { - // deno-lint-ignore no-empty - for await (const _ of gen) {} - releaseLock(); - }, - }); - } -} - -/** - * A {@link ReadableStream} that wraps the decoded CBOR "Text String". - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * Instances of this class is created from {@link CborSequenceDecoderStream}. - * This class is not designed for you to create instances of it yourself. It is - * merely a way for you to validate the type being returned. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborSequenceDecoderStream, - * CborTextDecodedStream, - * CborTextEncoderStream, - * } from "@std/cbor"; - * - * const rawMessage = "a".repeat(100); - * - * for await ( - * const value of ReadableStream.from([rawMessage]) - * .pipeThrough(new CborTextEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(typeof value === "string" || value instanceof CborTextDecodedStream); - * if (value instanceof CborTextDecodedStream) { - * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); - * } else assertEquals(value, rawMessage); - * } - * ``` - */ -export class CborTextDecodedStream extends ReadableStream { - /** - * Constructs a new instance. - * - * @param gen A {@link AsyncGenerator}. - * @param releaseLock A Function that's called when the stream is finished. - */ - constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { - super({ - async pull(controller) { - const { done, value } = await gen.next(); - if (done) { - releaseLock(); - controller.close(); - } else controller.enqueue(value); - }, - async cancel() { - // deno-lint-ignore no-empty - for await (const _ of gen) {} - releaseLock(); - }, - }); - } -} - -/** - * A {@link ReadableStream} that wraps the decoded CBOR "Array". - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * Instances of this class is created from {@link CborSequenceDecoderStream}. - * This class is not designed for you to create instances of it yourself. It is - * merely a way for you to validate the type being returned. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborArrayDecodedStream, - * CborArrayEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage = ["a".repeat(100), "b".repeat(100), "c".repeat(100)]; - * - * for await ( - * const value of ReadableStream.from(rawMessage) - * .pipeThrough(new CborArrayEncoderStream()) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborArrayDecodedStream); - * let i = 0; - * for await (const text of value) { - * assert(typeof text === "string"); - * assertEquals(text, rawMessage[i++]); - * } - * } - * ``` - */ -export class CborArrayDecodedStream extends ReadableStream { - /** - * Constructs a new instance. - * - * @param gen A {@link AsyncGenerator}. - * @param releaseLock A Function that's called when the stream is finished. - */ - constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { - super({ - async pull(controller) { - const { done, value } = await gen.next(); - if (done) { - releaseLock(); - controller.close(); - } else controller.enqueue(value); - }, - async cancel() { - // deno-lint-ignore no-empty - for await (const _ of gen) {} - releaseLock(); - }, - }); - } -} - -/** - * A {@link ReadableStream} that wraps the decoded CBOR "Map". - * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - * - * Instances of this class is created from {@link CborSequenceDecoderStream}. - * This class is not designed for you to create instances of it yourself. It is - * merely a way for you to validate the type being returned. - * - * @example Usage - * ```ts - * import { assert, assertEquals } from "@std/assert"; - * import { - * CborMapDecodedStream, - * CborMapEncoderStream, - * CborSequenceDecoderStream, - * } from "@std/cbor"; - * - * const rawMessage: Record = { - * a: 0, - * b: 1, - * c: 2, - * d: 3, - * }; - * - * for await ( - * const value of ReadableStream.from(Object.entries(rawMessage)) - * .pipeThrough(new CborMapEncoderStream) - * .pipeThrough(new CborSequenceDecoderStream()) - * ) { - * assert(value instanceof CborMapDecodedStream); - * for await (const [k, v] of value) { - * assertEquals(rawMessage[k], v); - * } - * } - * ``` - */ -export class CborMapDecodedStream extends ReadableStream { - /** - * Constructs a new instance. - * - * @param gen A {@link AsyncGenerator}. - * @param releaseLock A Function that's called when the stream is finished. - */ - constructor( - gen: AsyncGenerator, - releaseLock: ReleaseLock, - ) { - super({ - async pull(controller) { - const { done, value } = await gen.next(); - if (done) { - releaseLock(); - controller.close(); - } else controller.enqueue(value); - }, - async cancel() { - // deno-lint-ignore no-empty - for await (const _ of gen) {} - releaseLock(); - }, - }); - } -} +import { + arrayToNumber, + type ReleaseLock, + upgradeStreamFromGen, +} from "./_common.ts"; +import { CborArrayDecodedStream } from "./array_decoded_stream.ts"; +import { CborByteDecodedStream } from "./byte_decoded_stream.ts"; +import { CborMapDecodedStream } from "./map_decoded_stream.ts"; +import { CborTag } from "./tag.ts"; +import { CborTextDecodedStream } from "./text_decoded_stream.ts"; +import type { CborMapOutputStream, CborOutputStream } from "./types.ts"; /** * A {@link TransformStream} that decodes a CBOR-sequence-encoded diff --git a/cbor/sequence_decoder_stream_test.ts b/cbor/sequence_decoder_stream_test.ts new file mode 100644 index 000000000000..a37c78d1aade --- /dev/null +++ b/cbor/sequence_decoder_stream_test.ts @@ -0,0 +1,55 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals } from "@std/assert"; +import { random } from "./_common_test.ts"; +import { CborArrayDecodedStream } from "./array_decoded_stream.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { encodeCborSequence } from "./encode_cbor_sequence.ts"; +import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; +import { CborTag } from "./tag.ts"; + +Deno.test("CborSequenceDecoderStream() decoding CborPrimitiveType", async () => { + const input = [ + undefined, + null, + true, + false, + Math.random() * 10, + random(0, 24), + -BigInt(random(2 ** 32, 2 ** 64)), + "a".repeat(random(0, 24)), + new Uint8Array(random(0, 24)), + new Date(), + ]; + + assertEquals( + await Array.fromAsync( + ReadableStream.from([encodeCborSequence(input)]).pipeThrough( + new CborSequenceDecoderStream(), + ), + ), + input, + ); +}); + +Deno.test("CborSequenceDecoderStream() decoding CborTag()", async () => { + const tagNumber = 2; // Tag Number needs to be a value that will return a CborTag. + const size = random(0, 24); + + const reader = ReadableStream.from([ + encodeCbor(new CborTag(tagNumber, new Array(size).fill(0))), + ]).pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborTag); + assertEquals(value.tagNumber, tagNumber); + assert(value.tagContent instanceof CborArrayDecodedStream); + assertEquals( + await Array.fromAsync(value.tagContent), + new Array(size).fill(0), + ); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/sequence_encoder_stream.ts b/cbor/sequence_encoder_stream.ts new file mode 100644 index 000000000000..9f7865e6317d --- /dev/null +++ b/cbor/sequence_encoder_stream.ts @@ -0,0 +1,455 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { numberToArray, upgradeStreamFromGen } from "./_common.ts"; +import { CborArrayEncoderStream } from "./array_encoder_stream.ts"; +import { CborByteEncoderStream } from "./byte_encoder_stream.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborMapEncoderStream } from "./map_encoder_stream.ts"; +import { CborTag } from "./tag.ts"; +import { CborTextEncoderStream } from "./text_encoder_stream.ts"; +import type { CborInputStream } from "./types.ts"; + +/** + * A {@link TransformStream} that encodes a + * {@link ReadableStream} into CBOR format sequence. + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * @example Usage + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } + * ``` + */ +export class CborSequenceEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream< + CborInputStream, + CborInputStream + >(); + this.#readable = upgradeStreamFromGen(this.#encodeFromReadable(readable)); + this.#writable = writable; + } + + async *#encodeFromReadable( + readable: ReadableStream, + ): AsyncGenerator { + for await (const x of readable) { + for await (const y of this.#encode(x)) { + yield y; + } + } + } + + async *#encode( + x: CborInputStream, + ): AsyncGenerator { + if ( + x instanceof CborByteEncoderStream || + x instanceof CborTextEncoderStream || + x instanceof CborArrayEncoderStream || + x instanceof CborMapEncoderStream + ) { + for await (const y of x.readable) { + yield y; + } + } else if (x instanceof Array) { + for await (const y of this.#encodeArray(x)) { + yield y; + } + } else if (x instanceof CborTag) { + for await (const y of this.#encodeTag(x)) { + yield y; + } + } else if (typeof x === "object" && x !== null) { + if (x instanceof Date || x instanceof Uint8Array) yield encodeCbor(x); + else { + for await (const y of this.#encodeObject(x)) { + yield y; + } + } + } else yield encodeCbor(x); + } + + async *#encodeArray(x: CborInputStream[]): AsyncGenerator { + if (x.length < 24) yield new Uint8Array([0b100_00000 + x.length]); + else if (x.length < 2 ** 8) yield new Uint8Array([0b100_11000, x.length]); + else if (x.length < 2 ** 16) { + yield new Uint8Array([0b100_11001, ...numberToArray(2, x.length)]); + } else if (x.length < 2 ** 32) { + yield new Uint8Array([0b100_11010, ...numberToArray(4, x.length)]); + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support an `Array` being that large. + else yield new Uint8Array([0b100_11011, ...numberToArray(8, x.length)]); + for (const y of x) { + for await (const z of this.#encode(y)) { + yield z; + } + } + } + + async *#encodeObject( + x: { [k: string]: CborInputStream }, + ): AsyncGenerator { + const len = Object.keys(x).length; + if (len < 24) yield new Uint8Array([0b101_00000 + len]); + else if (len < 2 ** 8) yield new Uint8Array([0b101_11000, len]); + else if (len < 2 ** 16) { + yield new Uint8Array([0b101_11001, ...numberToArray(2, len)]); + } else if (len < 2 ** 32) { + yield new Uint8Array([0b101_11010, ...numberToArray(4, len)]); + } // Can safely assume `len < 2 ** 64` as JavaScript doesn't support an `Object` being that Large. + else yield new Uint8Array([0b101_11011, ...numberToArray(8, len)]); + for (const [k, v] of Object.entries(x)) { + yield encodeCbor(k); + for await (const y of this.#encode(v)) { + yield y; + } + } + } + + async *#encodeTag(x: CborTag): AsyncGenerator { + const tagNumber = BigInt(x.tagNumber); + if (tagNumber < 0n) { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) is less than zero`, + ); + } + if (tagNumber > 2n ** 64n) { + throw new RangeError( + `Cannot encode Tag Item: Tag Number (${x.tagNumber}) exceeds 2 ** 64 - 1`, + ); + } + + const head = encodeCbor(tagNumber); + head[0]! += 0b110_00000; + yield head; + for await (const y of this.#encode(x.tagContent)) { + yield y; + } + } + + /** + * Creates a {@link CborSequenceEncoderStream} instance from an iterable of + * {@link CborInputStream} chunks. + * + * @example Usage + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } + * + * for await ( + * const value of CborSequenceEncoderStream.from(rawMessage) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } + * ``` + * + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or + * {@link Iterable}. + * @returns A {@link CborSequenceEncoderStream} instance of the encoded data. + */ + static from( + asyncIterable: AsyncIterable | Iterable, + ): CborSequenceEncoderStream { + const encoder = new CborSequenceEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. + * + * @example Usage + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } + * ``` + * + * @returns A {@link ReadableStream}. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The {@link WritableStream} associated with the + * instance, which accepts {@link CborInputStream} chunks to be encoded + * into CBOR format. + * + * @example Usage + * ```ts no-assert + * import { encodeBase64Url } from "@std/encoding"; + * import { + * CborArrayDecodedStream, + * CborArrayEncoderStream, + * CborByteDecodedStream, + * CborByteEncoderStream, + * CborMapDecodedStream, + * CborMapEncoderStream, + * type CborOutputStream, + * CborSequenceDecoderStream, + * CborSequenceEncoderStream, + * CborTag, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = [ + * undefined, + * null, + * true, + * false, + * 3.14, + * 5, + * 2n ** 32n, + * "Hello World", + * new Uint8Array(25), + * new Date(), + * new CborTag(33, encodeBase64Url(new Uint8Array(7))), + * ["cake", "carrot"], + * { a: 3, b: "d" }, + * CborByteEncoderStream.from([new Uint8Array(7)]), + * CborTextEncoderStream.from(["Bye!"]), + * CborArrayEncoderStream.from([ + * "Hey!", + * CborByteEncoderStream.from([new Uint8Array(18)]), + * ]), + * CborMapEncoderStream.from([ + * ["a", 0], + * ["b", "potato"], + * ]), + * ]; + * + * async function logValue(value: CborOutputStream) { + * if ( + * value instanceof CborByteDecodedStream || + * value instanceof CborTextDecodedStream + * ) { + * for await (const x of value) console.log(x); + * } else if (value instanceof CborArrayDecodedStream) { + * for await (const x of value) logValue(x); + * } else if (value instanceof CborMapDecodedStream) { + * for await (const [k, v] of value) { + * console.log(k); + * logValue(v); + * } + * } else if (value instanceof CborTag) { + * console.log(value); + * logValue(value.tagContent); + * } else console.log(value); + * } + * + * for await ( + * const value of ReadableStream.from(rawMessage) + * .pipeThrough(new CborSequenceEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * logValue(value); + * } + * ``` + * + * @returns A {@link WritableStream}. + */ + get writable(): WritableStream { + return this.#writable; + } +} diff --git a/cbor/sequence_encoder_stream_test.ts b/cbor/sequence_encoder_stream_test.ts new file mode 100644 index 000000000000..2e3ec47f74b9 --- /dev/null +++ b/cbor/sequence_encoder_stream_test.ts @@ -0,0 +1,127 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { random } from "./_common_test.ts"; +import { CborArrayEncoderStream } from "./array_encoder_stream.ts"; +import { CborByteEncoderStream } from "./byte_encoder_stream.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborMapEncoderStream } from "./map_encoder_stream.ts"; +import { CborSequenceEncoderStream } from "./sequence_encoder_stream.ts"; +import { CborTag } from "./tag.ts"; +import { CborTextEncoderStream } from "./text_encoder_stream.ts"; + +Deno.test("CborSequenceEncoderStream()", async () => { + const input = [ + undefined, + null, + true, + false, + random(0, 24), + BigInt(random(0, 24)), + "a".repeat(random(0, 24)), + new Uint8Array(random(0, 24)), + new Date(), + ]; + + const expectedOutput = concat(input.map((x) => encodeCbor(x))); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(input).pipeThrough(new CborSequenceEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream() accepting the other streams", async () => { + // Inputs should be identical. We need two of them as the contents will be consumed when calculating expectedOutput and actualOutput. + const input1 = [ + CborByteEncoderStream.from([new Uint8Array(10), new Uint8Array(20)]), + CborTextEncoderStream.from(["a".repeat(10), "b".repeat(20)]), + CborArrayEncoderStream.from([10, 20]), + CborMapEncoderStream.from([["a", 0], ["b", 1], ["c", 2], ["d", 3]]), + ]; + const input2 = [ + CborByteEncoderStream.from([new Uint8Array(10), new Uint8Array(20)]), + CborTextEncoderStream.from(["a".repeat(10), "b".repeat(20)]), + CborArrayEncoderStream.from([10, 20]), + CborMapEncoderStream.from([["a", 0], ["b", 1], ["c", 2], ["d", 3]]), + ]; + + const expectedOutput = concat( + await Promise.all( + input1.map(async (stream) => + concat(await Array.fromAsync(stream.readable)) + ), + ), + ); + + const actualOutput = concat( + await Array.fromAsync( + CborSequenceEncoderStream.from(input2).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream() accepting CborInputStream[]", async () => { + const input = [ + new Array(random(0, 24)).fill(0), + new Array(random(24, 2 ** 8)).fill(0), + new Array(random(2 ** 8, 2 ** 16)).fill(0), + new Array(random(2 ** 16, 2 ** 17)).fill(0), + ]; + + const expectedOutput = concat(input.map((x) => encodeCbor(x))); + + const actualOutput = concat( + await Array.fromAsync( + CborSequenceEncoderStream.from(input).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream() accepting { [k: string]: CborInputStream }", async () => { + const input = [ + Object.fromEntries( + new Array(random(10, 20)).fill(0).map(( + _, + i, + ) => [String.fromCharCode(97 + i), false]), + ), + ]; + + const expectedOutput = concat(input.map((x) => encodeCbor(x))); + + const actualOutput = concat( + await Array.fromAsync( + CborSequenceEncoderStream.from(input).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborSequenceEncoderStream() accepting CborTag()", async () => { + const input = [ + new CborTag(0, 0), + new CborTag(1, 1), + new CborTag(2, 2), + new CborTag(3, 3), + ]; + + const expectedOutput = concat(input.map((x) => encodeCbor(x))); + + const actualOutput = concat( + await Array.fromAsync( + CborSequenceEncoderStream.from(input).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); diff --git a/cbor/tag.ts b/cbor/tag.ts new file mode 100644 index 000000000000..254516dd619c --- /dev/null +++ b/cbor/tag.ts @@ -0,0 +1,102 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import type { CborInputStream, CborOutputStream, CborType } from "./types.ts"; + +/** + * Represents a CBOR tag, which pairs a tag number with content, used to convey + * additional semantic information in CBOR-encoded data. + * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; + * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; + * + * const rawMessage = new TextEncoder().encode("Hello World"); + * + * const encodedMessage = encodeCbor( + * new CborTag( + * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". + * encodeBase64Url(rawMessage), + * ), + * ); + * + * const decodedMessage = decodeCbor(encodedMessage); + * + * assert(decodedMessage instanceof CborTag); + * assert(typeof decodedMessage.tagContent === "string"); + * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); + * ``` + * + * @typeParam T The type of the tag's content, which can be a + * {@link CborType}, {@link CborInputStream}, or {@link CborOutputStream}. + */ +export class CborTag { + /** + * A {@link number} or {@link bigint} representing the CBOR tag number, used + * to identify the type of the tagged content. + * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; + * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; + * + * const rawMessage = new TextEncoder().encode("Hello World"); + * + * const encodedMessage = encodeCbor( + * new CborTag( + * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". + * encodeBase64Url(rawMessage), + * ), + * ); + * + * const decodedMessage = decodeCbor(encodedMessage); + * + * assert(decodedMessage instanceof CborTag); + * assert(typeof decodedMessage.tagContent === "string"); + * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); + * ``` + */ + tagNumber: number | bigint; + /** + * The content associated with the tag of type {@link T}. + * [CBOR Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml). + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { CborTag, decodeCbor, encodeCbor } from "@std/cbor"; + * import { decodeBase64Url, encodeBase64Url } from "@std/encoding"; + * + * const rawMessage = new TextEncoder().encode("Hello World"); + * + * const encodedMessage = encodeCbor( + * new CborTag( + * 33, // TagNumber 33 specifies the tagContent must be a valid "base64url" "string". + * encodeBase64Url(rawMessage), + * ), + * ); + * + * const decodedMessage = decodeCbor(encodedMessage); + * + * assert(decodedMessage instanceof CborTag); + * assert(typeof decodedMessage.tagContent === "string"); + * assertEquals(decodeBase64Url(decodedMessage.tagContent), rawMessage); + * ``` + */ + tagContent: T; + /** + * Constructs a new instance. + * + * @param tagNumber A {@link number} or {@link bigint} representing the CBOR + * tag number, used to identify the type of the tagged content. + * @param tagContent The content associated with the tag of type {@link T}. + */ + constructor(tagNumber: number | bigint, tagContent: T) { + this.tagNumber = tagNumber; + this.tagContent = tagContent; + } +} diff --git a/cbor/text_decoded_stream.ts b/cbor/text_decoded_stream.ts new file mode 100644 index 000000000000..c3ee0d21acda --- /dev/null +++ b/cbor/text_decoded_stream.ts @@ -0,0 +1,59 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import type { ReleaseLock } from "./_common.ts"; + +/** + * A {@link ReadableStream} that wraps the decoded CBOR "Text String". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * Instances of this class is created from {@link CborSequenceDecoderStream}. + * This class is not designed for you to create instances of it yourself. It is + * merely a way for you to validate the type being returned. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = "a".repeat(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborTextEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } + * ``` + */ +export class CborTextDecodedStream extends ReadableStream { + /** + * Constructs a new instance. + * + * @param gen A {@link AsyncGenerator}. + * @param releaseLock A Function that's called when the stream is finished. + */ + constructor(gen: AsyncGenerator, releaseLock: ReleaseLock) { + super({ + async pull(controller) { + const { done, value } = await gen.next(); + if (done) { + releaseLock(); + controller.close(); + } else controller.enqueue(value); + }, + async cancel() { + // deno-lint-ignore no-empty + for await (const _ of gen) {} + releaseLock(); + }, + }); + } +} diff --git a/cbor/text_decoded_stream_test.ts b/cbor/text_decoded_stream_test.ts new file mode 100644 index 000000000000..37ef1a6a949d --- /dev/null +++ b/cbor/text_decoded_stream_test.ts @@ -0,0 +1,50 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals } from "@std/assert"; +import { random } from "./_common_test.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; +import { CborTextDecodedStream } from "./text_decoded_stream.ts"; +import { CborTextEncoderStream } from "./text_encoder_stream.ts"; + +Deno.test("CborSequenceDecoderStream() decoding Indefinite Length Text String", async () => { + const inputSize = 10; + + const reader = CborTextEncoderStream.from([ + "a".repeat(inputSize), + "b".repeat(inputSize * 2), + "c".repeat(inputSize * 3), + ]).readable.pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborTextDecodedStream); + assertEquals(await Array.fromAsync(value), [ + "a".repeat(inputSize), + "b".repeat(inputSize * 2), + "c".repeat(inputSize * 3), + ]); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); + +Deno.test("CborSequenceDecoderStream() decoding large Definite Text Byte String", async () => { + // Strings need to be 2 ** 16 bytes+ to be decoded via a CborTextDecodedStream. + const size = random(2 ** 16, 2 ** 17); + + const reader = ReadableStream.from([ + encodeCbor( + new TextDecoder().decode(new Uint8Array(size).fill("a".charCodeAt(0))), + ), + ]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborTextDecodedStream); + assertEquals((await Array.fromAsync(value)).join(""), "a".repeat(size)); + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/text_encoder_stream.ts b/cbor/text_encoder_stream.ts new file mode 100644 index 000000000000..528fc99f6246 --- /dev/null +++ b/cbor/text_encoder_stream.ts @@ -0,0 +1,169 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { numberToArray, upgradeStreamFromGen } from "./_common.ts"; + +/** + * A {@link TransformStream} that encodes a {@link ReadableStream} into + * CBOR "Indefinite Length Text String". + * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) + * + * **Notice:** Each chunk of the {@link ReadableStream} is encoded as + * its own "Definite Length Text String" meaning space can be saved if large + * chunks are pipped through instead of small chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = "a".repeat(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborTextEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } + * ``` + */ +export class CborTextEncoderStream + implements TransformStream { + #readable: ReadableStream; + #writable: WritableStream; + /** + * Constructs a new instance. + */ + constructor() { + const { readable, writable } = new TransformStream(); + this.#readable = upgradeStreamFromGen(async function* () { + yield new Uint8Array([0b011_11111]); + for await (const x of readable.pipeThrough(new TextEncoderStream())) { + if (x.length < 24) yield new Uint8Array([0b011_00000 + x.length]); + else if (x.length < 2 ** 8) { + yield new Uint8Array([0b011_11000, x.length]); + } else if (x.length < 2 ** 16) { + yield new Uint8Array([0b011_11001, ...numberToArray(2, x.length)]); + } else if (x.length < 2 ** 32) { + yield new Uint8Array([0b011_11010, ...numberToArray(4, x.length)]); + } // Can safely assume `x.length < 2 ** 64` as JavaScript doesn't support a `Uint8Array` being that large. + else yield new Uint8Array([0b011_11011, ...numberToArray(8, x.length)]); + yield x; + } + yield new Uint8Array([0b111_11111]); + }()); + this.#writable = writable; + } + + /** + * Creates a {@link CborTextEncoderStream} instance from an iterable of + * {@link string} chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = "a".repeat(100); + * + * for await ( + * const value of CborTextEncoderStream.from([rawMessage]) + * .readable + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } + * ``` + * + * @param asyncIterable The value to encode of type + * {@link AsyncIterable} or {@link Iterable} + * @returns A {@link CborTextEncoderStream} instance of the encoded data. + */ + static from( + asyncIterable: AsyncIterable | Iterable, + ): CborTextEncoderStream { + const encoder = new CborTextEncoderStream(); + ReadableStream.from(asyncIterable).pipeTo(encoder.writable); + return encoder; + } + + /** + * The {@link ReadableStream} associated with the instance, which + * provides the encoded CBOR data as {@link Uint8Array} chunks. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = "a".repeat(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborTextEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } + * ``` + * + * @returns A {@link ReadableStream}. + */ + get readable(): ReadableStream { + return this.#readable; + } + + /** + * The {@link WritableStream} associated with the instance, which + * accepts {@link string} chunks to be encoded into CBOR format. + * + * @example Usage + * ```ts + * import { assert, assertEquals } from "@std/assert"; + * import { + * CborSequenceDecoderStream, + * CborTextDecodedStream, + * CborTextEncoderStream, + * } from "@std/cbor"; + * + * const rawMessage = "a".repeat(100); + * + * for await ( + * const value of ReadableStream.from([rawMessage]) + * .pipeThrough(new CborTextEncoderStream()) + * .pipeThrough(new CborSequenceDecoderStream()) + * ) { + * assert(typeof value === "string" || value instanceof CborTextDecodedStream); + * if (value instanceof CborTextDecodedStream) { + * assertEquals((await Array.fromAsync(value)).join(""), rawMessage); + * } else assertEquals(value, rawMessage); + * } + * ``` + * + * @returns A {@link WritableStream}. + */ + get writable(): WritableStream { + return this.#writable; + } +} diff --git a/cbor/text_encoder_stream_test.ts b/cbor/text_encoder_stream_test.ts new file mode 100644 index 000000000000..e81c44e2e8d5 --- /dev/null +++ b/cbor/text_encoder_stream_test.ts @@ -0,0 +1,53 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert"; +import { concat } from "@std/bytes"; +import { random } from "./_common_test.ts"; +import { encodeCbor } from "./encode_cbor.ts"; +import { CborTextEncoderStream } from "./text_encoder_stream.ts"; + +Deno.test("CborTextEncoderStream()", async () => { + const strings = [ + "a".repeat(random(0, 24)), + "a".repeat(random(24, 2 ** 8)), + "a".repeat(random(2 ** 8, 2 ** 16)), + "a".repeat(random(2 ** 16, 2 ** 17)), + ]; + + const expectedOutput = concat([ + new Uint8Array([0b011_11111]), + ...strings.filter((x) => x).map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + ReadableStream.from(strings).pipeThrough(new CborTextEncoderStream()), + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); + +Deno.test("CborTextEncoderStream.from()", async () => { + const strings = [ + "a".repeat(random(0, 24)), + "a".repeat(random(24, 2 ** 8)), + "a".repeat(random(2 ** 8, 2 ** 16)), + "a".repeat(random(2 ** 16, 2 ** 17)), + ]; + + const expectedOutput = concat([ + new Uint8Array([0b011_11111]), + ...strings.filter((x) => x).map((x) => encodeCbor(x)), + new Uint8Array([0b111_11111]), + ]); + + const actualOutput = concat( + await Array.fromAsync( + CborTextEncoderStream.from(strings).readable, + ), + ); + + assertEquals(actualOutput, expectedOutput); +}); diff --git a/cbor/types.ts b/cbor/types.ts new file mode 100644 index 000000000000..b3f72a1616c0 --- /dev/null +++ b/cbor/types.ts @@ -0,0 +1,70 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import type { CborArrayDecodedStream } from "./array_decoded_stream.ts"; +import type { CborArrayEncoderStream } from "./array_encoder_stream.ts"; +import type { CborByteDecodedStream } from "./byte_decoded_stream.ts"; +import type { CborByteEncoderStream } from "./byte_encoder_stream.ts"; +import type { CborMapDecodedStream } from "./map_decoded_stream.ts"; +import type { CborMapEncoderStream } from "./map_encoder_stream.ts"; +import type { CborTag } from "./tag.ts"; +import type { CborTextDecodedStream } from "./text_decoded_stream.ts"; +import type { CborTextEncoderStream } from "./text_encoder_stream.ts"; + +/** + * This type specifies the primitive types that the implementation can + * encode/decode into/from. + */ +export type CborPrimitiveType = + | undefined + | null + | boolean + | number + | bigint + | string + | Uint8Array + | Date; + +/** + * This type specifies the encodable and decodable values for + * {@link encodeCbor}, {@link decodeCbor}, {@link encodeCborSequence}, and + * {@link decodeCborSequence}. + */ +export type CborType = CborPrimitiveType | CborTag | CborType[] | { + [k: string]: CborType; +}; + +/** + * Specifies the encodable value types for the {@link CborSequenceEncoderStream} + * and {@link CborArrayEncoderStream}. + */ +export type CborInputStream = + | CborPrimitiveType + | CborTag + | CborInputStream[] + | { [k: string]: CborInputStream } + | CborByteEncoderStream + | CborTextEncoderStream + | CborArrayEncoderStream + | CborMapEncoderStream; + +/** + * Specifies the structure of input for the {@link CborMapEncoderStream}. + */ +export type CborMapInputStream = [string, CborInputStream]; + +/** + * Specifies the decodable value types for the {@link CborSequenceDecoderStream} + * and {@link CborMapDecodedStream}. + */ +export type CborOutputStream = + | CborPrimitiveType + | CborTag + | CborByteDecodedStream + | CborTextDecodedStream + | CborArrayDecodedStream + | CborMapDecodedStream; + +/** + * Specifies the structure of the output for the {@link CborMapDecodedStream}. + */ +export type CborMapOutputStream = [string, CborOutputStream]; From a9987c4e70ea1ff145a5e52412307e1d8944034c Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:20:13 +1000 Subject: [PATCH 43/45] fix(cbor): bug where `new Uint8Array(0)` would cause a promise to never resolve --- cbor/_common.ts | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/cbor/_common.ts b/cbor/_common.ts index 7a500e58a451..ddc6fd476468 100644 --- a/cbor/_common.ts +++ b/cbor/_common.ts @@ -50,29 +50,33 @@ export function upgradeStreamFromGen( return new ReadableStream({ type: "bytes", async pull(controller) { - const { done, value } = await gen.next(); - if (done) { - try { - controller.byobRequest?.respond(0); - return controller.close(); - } catch { - controller.close(); - return controller.byobRequest?.respond(0); + while (true) { + const { done, value } = await gen.next(); + if (done) { + try { + controller.byobRequest?.respond(0); + return controller.close(); + } catch { + controller.close(); + return controller.byobRequest?.respond(0); + } } - } - if (controller.byobRequest?.view) { - const buffer = new Uint8Array(controller.byobRequest.view.buffer); - const size = buffer.length; - if (value.length > size) { - buffer.set(value.slice(0, size)); - controller.byobRequest.respond(size); - controller.enqueue(value.slice(size)); - } else { - buffer.set(value); - controller.byobRequest.respond(value.length); + if (controller.byobRequest?.view) { + const buffer = new Uint8Array(controller.byobRequest.view.buffer); + const size = buffer.length; + if (value.length > size) { + buffer.set(value.slice(0, size)); + controller.byobRequest.respond(size); + controller.enqueue(value.slice(size)); + } else { + buffer.set(value); + controller.byobRequest.respond(value.length); + } + break; + } else if (value.length) { + controller.enqueue(value); + break; } - } else if (value.length) { - controller.enqueue(value); } }, }); From 1cb2a8b42e8f93a6accc2666ccf458bb94e32a85 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:56:45 +1000 Subject: [PATCH 44/45] tests(cbor): improved to make them more uniform --- cbor/array_decoded_stream_test.ts | 28 ++++++++++++++- cbor/array_encoder_stream_test.ts | 4 +-- cbor/byte_decoded_stream_test.ts | 51 +++++++++++++++++++++++----- cbor/byte_encoder_stream_test.ts | 4 +-- cbor/decode_cbor_sequence_test.ts | 2 +- cbor/decode_cbor_test.ts | 26 +++++++------- cbor/encode_cbor_sequence_test.ts | 2 +- cbor/map_decoded_stream_test.ts | 32 ++++++++++++++++- cbor/map_encoder_stream_test.ts | 4 +-- cbor/sequence_decoder_stream_test.ts | 4 +-- cbor/sequence_encoder_stream_test.ts | 12 +++---- cbor/text_decoded_stream_test.ts | 51 +++++++++++++++++++++++----- cbor/text_encoder_stream_test.ts | 4 +-- 13 files changed, 173 insertions(+), 51 deletions(-) diff --git a/cbor/array_decoded_stream_test.ts b/cbor/array_decoded_stream_test.ts index 42a807862f2c..42228f5908a8 100644 --- a/cbor/array_decoded_stream_test.ts +++ b/cbor/array_decoded_stream_test.ts @@ -6,7 +6,7 @@ import { CborArrayDecodedStream } from "./array_decoded_stream.ts"; import { encodeCbor } from "./encode_cbor.ts"; import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; -Deno.test("CborSequenceDecoderStream() decoding Arrays", async () => { +Deno.test("CborArrayDecodedStream() being consumed", async () => { const size = random(0, 24); const reader = ReadableStream.from([encodeCbor(new Array(size).fill(0))]) @@ -20,3 +20,29 @@ Deno.test("CborSequenceDecoderStream() decoding Arrays", async () => { assert((await reader.read()).done === true); reader.releaseLock(); }); + +Deno.test("CborArrayDecodedStream() being cancelled", async () => { + const size = random(0, 24); + const reader = ReadableStream.from([ + encodeCbor(new Array(size).fill(0)), + encodeCbor(0), + ]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + { + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborArrayDecodedStream); + await value.cancel(); + } + + { + const { done, value } = await reader.read(); + assert(done === false); + assert(typeof value === "number"); + assertEquals(value, 0); + } + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/array_encoder_stream_test.ts b/cbor/array_encoder_stream_test.ts index a70cafa622c0..7e6911524cb6 100644 --- a/cbor/array_encoder_stream_test.ts +++ b/cbor/array_encoder_stream_test.ts @@ -6,7 +6,7 @@ import { random } from "./_common_test.ts"; import { CborArrayEncoderStream } from "./array_encoder_stream.ts"; import { encodeCbor } from "./encode_cbor.ts"; -Deno.test("CborArrayEncoderStream()", async () => { +Deno.test("CborArrayEncoderStream() correctly encoding", async () => { const arrays = [random(0, 2 ** 32)]; const expectedOutput = concat([ @@ -24,7 +24,7 @@ Deno.test("CborArrayEncoderStream()", async () => { assertEquals(actualOutput, expectedOutput); }); -Deno.test("CborArrayEncoderStream.from()", async () => { +Deno.test("CborArrayEncoderStream.from() correctly encoding", async () => { const arrays = [random(0, 2 ** 32)]; const expectedOutput = concat([ diff --git a/cbor/byte_decoded_stream_test.ts b/cbor/byte_decoded_stream_test.ts index 86b3140bbda9..fd7a369c5387 100644 --- a/cbor/byte_decoded_stream_test.ts +++ b/cbor/byte_decoded_stream_test.ts @@ -7,30 +7,31 @@ import { CborByteDecodedStream } from "./byte_decoded_stream.ts"; import { CborByteEncoderStream } from "./byte_encoder_stream.ts"; import { encodeCbor } from "./encode_cbor.ts"; import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; +import { CborSequenceEncoderStream } from "./sequence_encoder_stream.ts"; -Deno.test("CborSequenceDecoderStream() decoding Indefinite Length Byte String", async () => { - const inputSize = 10; +Deno.test("CborByteDecodedStream() consuming indefinite length byte string", async () => { + const size = random(0, 24); const reader = CborByteEncoderStream.from([ - new Uint8Array(inputSize), - new Uint8Array(inputSize * 2), - new Uint8Array(inputSize * 3), + new Uint8Array(size), + new Uint8Array(size * 2), + new Uint8Array(size * 3), ]).readable.pipeThrough(new CborSequenceDecoderStream()).getReader(); const { done, value } = await reader.read(); assert(done === false); assert(value instanceof CborByteDecodedStream); assertEquals(await Array.fromAsync(value), [ - new Uint8Array(inputSize), - new Uint8Array(inputSize * 2), - new Uint8Array(inputSize * 3), + new Uint8Array(size), + new Uint8Array(size * 2), + new Uint8Array(size * 3), ]); assert((await reader.read()).done === true); reader.releaseLock(); }); -Deno.test("CborSequenceDecoderStream() decoding large Definite Length Byte String", async () => { +Deno.test("CborByteDecodedStream() consuming large definite length byte string", async () => { // Uint8Array needs to be 2 ** 32 bytes+ to be decoded via a CborByteDecodedStream. const size = random(2 ** 32, 2 ** 33); @@ -45,3 +46,35 @@ Deno.test("CborSequenceDecoderStream() decoding large Definite Length Byte Strin assert((await reader.read()).done === true); reader.releaseLock(); }); + +Deno.test("CborByteDecodedStream() being cancelled", async () => { + const size = random(0, 24); + + const reader = ReadableStream.from([ + CborByteEncoderStream.from([ + new Uint8Array(size), + new Uint8Array(size * 2), + new Uint8Array(size * 3), + ]), + 0, + ]) + .pipeThrough(new CborSequenceEncoderStream()) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + { + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborByteDecodedStream); + await value.cancel(); + } + + { + const { done, value } = await reader.read(); + assert(done === false); + assert(typeof value === "number"); + assertEquals(value, 0); + } + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/byte_encoder_stream_test.ts b/cbor/byte_encoder_stream_test.ts index 2ddfdd65e108..74a0c4bb6eae 100644 --- a/cbor/byte_encoder_stream_test.ts +++ b/cbor/byte_encoder_stream_test.ts @@ -6,7 +6,7 @@ import { random } from "./_common_test.ts"; import { CborByteEncoderStream } from "./byte_encoder_stream.ts"; import { encodeCbor } from "./encode_cbor.ts"; -Deno.test("CborByteEncoderStream()", async () => { +Deno.test("CborByteEncoderStream() correctly encoding", async () => { const bytes = [ new Uint8Array(random(0, 24)), new Uint8Array(random(24, 2 ** 8)), @@ -29,7 +29,7 @@ Deno.test("CborByteEncoderStream()", async () => { assertEquals(actualOutput, expectedOutput); }); -Deno.test("CborByteEncoderStream.from()", async () => { +Deno.test("CborByteEncoderStream.from() correctly encoding", async () => { const bytes = [ new Uint8Array(random(0, 24)), new Uint8Array(random(24, 2 ** 8)), diff --git a/cbor/decode_cbor_sequence_test.ts b/cbor/decode_cbor_sequence_test.ts index b69f837a3524..65cf14cb9a4f 100644 --- a/cbor/decode_cbor_sequence_test.ts +++ b/cbor/decode_cbor_sequence_test.ts @@ -3,7 +3,7 @@ import { assertEquals } from "@std/assert"; import { decodeCborSequence } from "./decode_cbor_sequence.ts"; -Deno.test("decodeCborSequence()", () => { +Deno.test("decodeCborSequence() correctly decoding", () => { assertEquals( decodeCborSequence(Uint8Array.from([0b000_00000, 0b000_00000])), [0, 0], diff --git a/cbor/decode_cbor_test.ts b/cbor/decode_cbor_test.ts index a490d123f099..2f6d4c886417 100644 --- a/cbor/decode_cbor_test.ts +++ b/cbor/decode_cbor_test.ts @@ -171,7 +171,7 @@ Deno.test("decodeCbor() rejecting empty encoded data", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 0", () => { +Deno.test("decodeCbor() rejecting majorType 0 due to additional information", () => { assertThrows( () => { decodeCbor( @@ -222,7 +222,7 @@ Deno.test("decodeCbor() rejecting majorType 0", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 1", () => { +Deno.test("decodeCbor() rejecting majorType 1 due to additional information", () => { assertThrows( () => { decodeCbor( @@ -273,7 +273,7 @@ Deno.test("decodeCbor() rejecting majorType 1", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 2 | Reserved Additional Information", () => { +Deno.test("decodeCbor() rejecting majorType 2 due to additional information", () => { assertThrows( () => { decodeCbor( @@ -312,7 +312,7 @@ Deno.test("decodeCbor() rejecting majorType 2 | Reserved Additional Information" ); }); -Deno.test("decodeCbor() rejecting majorType 2 | Indefinite Byte String", () => { +Deno.test("decodeCbor() rejecting majorType 2 due to invalid indefinite length byte string", () => { assertThrows( () => { decodeCbor(Uint8Array.from([0b010_11111])); @@ -343,7 +343,7 @@ Deno.test("decodeCbor() rejecting majorType 2 | Indefinite Byte String", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 3 | Reserved Additional Information", () => { +Deno.test("decodeCbor() rejecting majorType 3 due to additional information", () => { assertThrows( () => { decodeCbor( @@ -382,7 +382,7 @@ Deno.test("decodeCbor() rejecting majorType 3 | Reserved Additional Information" ); }); -Deno.test("decodeCbor() rejecting majorType 3 | Indefinite Text String", () => { +Deno.test("decodeCbor() rejecting majorType 3 due to invalid indefinite length text string", () => { assertThrows( () => { decodeCbor(Uint8Array.from([0b011_11111])); @@ -413,7 +413,7 @@ Deno.test("decodeCbor() rejecting majorType 3 | Indefinite Text String", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 4 | Reserved Additional Information", () => { +Deno.test("decodeCbor() rejecting majorType 4 due to additional information", () => { assertThrows( () => { decodeCbor( @@ -452,7 +452,7 @@ Deno.test("decodeCbor() rejecting majorType 4 | Reserved Additional Information" ); }); -Deno.test("decodeCbor() rejecting majorType 4 | Indefinite Arrays", () => { +Deno.test("decodeCbor() rejecting majorType 4 due to invalid indefinite length arrays", () => { assertThrows( () => { decodeCbor(Uint8Array.from([0b100_11111])); @@ -469,7 +469,7 @@ Deno.test("decodeCbor() rejecting majorType 4 | Indefinite Arrays", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 5 | Reserved Additional Information", () => { +Deno.test("decodeCbor() rejecting majorType 5 due to additional information", () => { assertThrows( () => { decodeCbor( @@ -508,7 +508,7 @@ Deno.test("decodeCbor() rejecting majorType 5 | Reserved Additional Information" ); }); -Deno.test("decodeCbor() rejecting majorType 5 | Invalid Keys", () => { +Deno.test("decodeCbor() rejecting majorType 5 due to maps having invalid keys", () => { assertThrows( () => { decodeCbor(Uint8Array.from([0b101_00001, 0b000_00000, 0b000_00000])); @@ -535,7 +535,7 @@ Deno.test("decodeCbor() rejecting majorType 5 | Invalid Keys", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 5 | Indefinite Maps", () => { +Deno.test("decodeCbor() rejecting majorType 5 due to invalid indefinite length maps", () => { assertThrows( () => { decodeCbor(Uint8Array.from([0b101_11111])); @@ -586,7 +586,7 @@ Deno.test("decodeCbor() rejecting majorType 5 | Indefinite Maps", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 6", () => { +Deno.test("decodeCbor() rejecting majorType 6 due to additional information", () => { assertThrows( () => { decodeCbor( @@ -615,7 +615,7 @@ Deno.test("decodeCbor() rejecting majorType 6", () => { ); }); -Deno.test("decodeCbor() rejecting majorType 7", () => { +Deno.test("decodeCbor() rejecting majorType 7 due to additional information", () => { assertThrows( () => { decodeCbor( diff --git a/cbor/encode_cbor_sequence_test.ts b/cbor/encode_cbor_sequence_test.ts index ac5d29c271b1..b217a3616762 100644 --- a/cbor/encode_cbor_sequence_test.ts +++ b/cbor/encode_cbor_sequence_test.ts @@ -3,7 +3,7 @@ import { assertEquals } from "@std/assert"; import { encodeCborSequence } from "./encode_cbor_sequence.ts"; -Deno.test("encodeCborSequence()", () => { +Deno.test("encodeCborSequence() correctly encoding", () => { assertEquals( encodeCborSequence([0, 0]), Uint8Array.from([0b000_00000, 0b000_00000]), diff --git a/cbor/map_decoded_stream_test.ts b/cbor/map_decoded_stream_test.ts index 3ea49cb8ad8b..f8989612d086 100644 --- a/cbor/map_decoded_stream_test.ts +++ b/cbor/map_decoded_stream_test.ts @@ -7,7 +7,7 @@ import { CborMapDecodedStream } from "./map_decoded_stream.ts"; import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; import type { CborMapOutputStream } from "./types.ts"; -Deno.test("CborSequenceDecoderStream() decoding Objects", async () => { +Deno.test("CborMapDecodedStream() being consumed", async () => { const size = random(0, 24); const entries = new Array(size).fill(0).map((_, i) => [String.fromCharCode(97 + i), i] satisfies CborMapOutputStream @@ -24,3 +24,33 @@ Deno.test("CborSequenceDecoderStream() decoding Objects", async () => { assert((await reader.read()).done === true); reader.releaseLock(); }); + +Deno.test("CborMapDecodedStream() being cancelled", async () => { + const size = random(0, 24); + const entries = new Array(size).fill(0).map((_, i) => + [String.fromCharCode(97 + i), i] satisfies CborMapOutputStream + ); + + const reader = ReadableStream.from([ + encodeCbor(Object.fromEntries(entries)), + encodeCbor(0), + ]) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + { + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborMapDecodedStream); + await value.cancel(); + } + + { + const { done, value } = await reader.read(); + assert(done === false); + assert(typeof value === "number"); + assertEquals(value, 0); + } + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/map_encoder_stream_test.ts b/cbor/map_encoder_stream_test.ts index 3e4e96dc13a2..befed36a5dba 100644 --- a/cbor/map_encoder_stream_test.ts +++ b/cbor/map_encoder_stream_test.ts @@ -7,7 +7,7 @@ import { encodeCbor } from "./encode_cbor.ts"; import { CborMapEncoderStream } from "./map_encoder_stream.ts"; import type { CborMapInputStream, CborType } from "./types.ts"; -Deno.test("CborMapEncoderStream()", async () => { +Deno.test("CborMapEncoderStream() correctly encoding", async () => { const maps: CborMapInputStream[] = [["a", random(0, 2 ** 32)]]; const expectedOutput = concat([ @@ -25,7 +25,7 @@ Deno.test("CborMapEncoderStream()", async () => { assertEquals(actualOutput, expectedOutput); }); -Deno.test("CborMapEncoderStream.from()", async () => { +Deno.test("CborMapEncoderStream.from() correctly encoding", async () => { const maps: CborMapInputStream[] = [["a", random(0, 2 ** 32)]]; const expectedOutput = concat([ diff --git a/cbor/sequence_decoder_stream_test.ts b/cbor/sequence_decoder_stream_test.ts index a37c78d1aade..f91968564ee0 100644 --- a/cbor/sequence_decoder_stream_test.ts +++ b/cbor/sequence_decoder_stream_test.ts @@ -8,7 +8,7 @@ import { encodeCborSequence } from "./encode_cbor_sequence.ts"; import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; import { CborTag } from "./tag.ts"; -Deno.test("CborSequenceDecoderStream() decoding CborPrimitiveType", async () => { +Deno.test("CborSequenceDecoderStream() correctly decoding CborPrimitiveType", async () => { const input = [ undefined, null, @@ -32,7 +32,7 @@ Deno.test("CborSequenceDecoderStream() decoding CborPrimitiveType", async () => ); }); -Deno.test("CborSequenceDecoderStream() decoding CborTag()", async () => { +Deno.test("CborSequenceDecoderStream() correctly decoding CborTag()", async () => { const tagNumber = 2; // Tag Number needs to be a value that will return a CborTag. const size = random(0, 24); diff --git a/cbor/sequence_encoder_stream_test.ts b/cbor/sequence_encoder_stream_test.ts index 2e3ec47f74b9..3e28cb1cb86f 100644 --- a/cbor/sequence_encoder_stream_test.ts +++ b/cbor/sequence_encoder_stream_test.ts @@ -11,7 +11,7 @@ import { CborSequenceEncoderStream } from "./sequence_encoder_stream.ts"; import { CborTag } from "./tag.ts"; import { CborTextEncoderStream } from "./text_encoder_stream.ts"; -Deno.test("CborSequenceEncoderStream()", async () => { +Deno.test("CborSequenceEncoderStream() correctly encoding CborPrimitiveType", async () => { const input = [ undefined, null, @@ -35,7 +35,7 @@ Deno.test("CborSequenceEncoderStream()", async () => { assertEquals(actualOutput, expectedOutput); }); -Deno.test("CborSequenceEncoderStream() accepting the other streams", async () => { +Deno.test("CborSequenceEncoderStream() correctly encoding streams", async () => { // Inputs should be identical. We need two of them as the contents will be consumed when calculating expectedOutput and actualOutput. const input1 = [ CborByteEncoderStream.from([new Uint8Array(10), new Uint8Array(20)]), @@ -67,9 +67,9 @@ Deno.test("CborSequenceEncoderStream() accepting the other streams", async () => assertEquals(actualOutput, expectedOutput); }); -Deno.test("CborSequenceEncoderStream() accepting CborInputStream[]", async () => { +Deno.test("CborSequenceEncoderStream() correctly encoding arrays", async () => { const input = [ - new Array(random(0, 24)).fill(0), + new Array(random(1, 24)).fill(0), new Array(random(24, 2 ** 8)).fill(0), new Array(random(2 ** 8, 2 ** 16)).fill(0), new Array(random(2 ** 16, 2 ** 17)).fill(0), @@ -86,7 +86,7 @@ Deno.test("CborSequenceEncoderStream() accepting CborInputStream[]", async () => assertEquals(actualOutput, expectedOutput); }); -Deno.test("CborSequenceEncoderStream() accepting { [k: string]: CborInputStream }", async () => { +Deno.test("CborSequenceEncoderStream() correctly encoding objects", async () => { const input = [ Object.fromEntries( new Array(random(10, 20)).fill(0).map(( @@ -107,7 +107,7 @@ Deno.test("CborSequenceEncoderStream() accepting { [k: string]: CborInputStream assertEquals(actualOutput, expectedOutput); }); -Deno.test("CborSequenceEncoderStream() accepting CborTag()", async () => { +Deno.test("CborSequenceEncoderStream() correctly encoding CborTag()", async () => { const input = [ new CborTag(0, 0), new CborTag(1, 1), diff --git a/cbor/text_decoded_stream_test.ts b/cbor/text_decoded_stream_test.ts index 37ef1a6a949d..cad3178f09f7 100644 --- a/cbor/text_decoded_stream_test.ts +++ b/cbor/text_decoded_stream_test.ts @@ -6,30 +6,31 @@ import { encodeCbor } from "./encode_cbor.ts"; import { CborSequenceDecoderStream } from "./sequence_decoder_stream.ts"; import { CborTextDecodedStream } from "./text_decoded_stream.ts"; import { CborTextEncoderStream } from "./text_encoder_stream.ts"; +import { CborSequenceEncoderStream } from "./sequence_encoder_stream.ts"; -Deno.test("CborSequenceDecoderStream() decoding Indefinite Length Text String", async () => { - const inputSize = 10; +Deno.test("CborTextDecodedStream() consuming indefinite length text string", async () => { + const size = random(0, 24); const reader = CborTextEncoderStream.from([ - "a".repeat(inputSize), - "b".repeat(inputSize * 2), - "c".repeat(inputSize * 3), + "a".repeat(size), + "b".repeat(size * 2), + "c".repeat(size * 3), ]).readable.pipeThrough(new CborSequenceDecoderStream()).getReader(); const { done, value } = await reader.read(); assert(done === false); assert(value instanceof CborTextDecodedStream); assertEquals(await Array.fromAsync(value), [ - "a".repeat(inputSize), - "b".repeat(inputSize * 2), - "c".repeat(inputSize * 3), + "a".repeat(size), + "b".repeat(size * 2), + "c".repeat(size * 3), ]); assert((await reader.read()).done === true); reader.releaseLock(); }); -Deno.test("CborSequenceDecoderStream() decoding large Definite Text Byte String", async () => { +Deno.test("CborTextDecodedStream() consuming large definite length text string", async () => { // Strings need to be 2 ** 16 bytes+ to be decoded via a CborTextDecodedStream. const size = random(2 ** 16, 2 ** 17); @@ -48,3 +49,35 @@ Deno.test("CborSequenceDecoderStream() decoding large Definite Text Byte String" assert((await reader.read()).done === true); reader.releaseLock(); }); + +Deno.test("CborTextDecodedStream() being cancelled", async () => { + const size = random(0, 24); + + const reader = ReadableStream.from([ + CborTextEncoderStream.from([ + "a".repeat(size), + "b".repeat(size * 2), + "c".repeat(size * 3), + ]), + 0, + ]) + .pipeThrough(new CborSequenceEncoderStream()) + .pipeThrough(new CborSequenceDecoderStream()).getReader(); + + { + const { done, value } = await reader.read(); + assert(done === false); + assert(value instanceof CborTextDecodedStream); + await value.cancel(); + } + + { + const { done, value } = await reader.read(); + assert(done === false); + assert(typeof value === "number"); + assertEquals(value, 0); + } + + assert((await reader.read()).done === true); + reader.releaseLock(); +}); diff --git a/cbor/text_encoder_stream_test.ts b/cbor/text_encoder_stream_test.ts index e81c44e2e8d5..941245a375d9 100644 --- a/cbor/text_encoder_stream_test.ts +++ b/cbor/text_encoder_stream_test.ts @@ -6,7 +6,7 @@ import { random } from "./_common_test.ts"; import { encodeCbor } from "./encode_cbor.ts"; import { CborTextEncoderStream } from "./text_encoder_stream.ts"; -Deno.test("CborTextEncoderStream()", async () => { +Deno.test("CborTextEncoderStream() correctly encoding", async () => { const strings = [ "a".repeat(random(0, 24)), "a".repeat(random(24, 2 ** 8)), @@ -29,7 +29,7 @@ Deno.test("CborTextEncoderStream()", async () => { assertEquals(actualOutput, expectedOutput); }); -Deno.test("CborTextEncoderStream.from()", async () => { +Deno.test("CborTextEncoderStream.from() correctly encoding", async () => { const strings = [ "a".repeat(random(0, 24)), "a".repeat(random(24, 2 ** 8)), From 4ec394e268d0573a6d51244cf80e220e75803880 Mon Sep 17 00:00:00 2001 From: BlackAsLight <44320105+BlackAsLight@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:02:46 +1000 Subject: [PATCH 45/45] refactor(cbor): `upgradeStreamFromGen()` --- cbor/_common.ts | 49 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/cbor/_common.ts b/cbor/_common.ts index ddc6fd476468..adbeee15834c 100644 --- a/cbor/_common.ts +++ b/cbor/_common.ts @@ -50,34 +50,31 @@ export function upgradeStreamFromGen( return new ReadableStream({ type: "bytes", async pull(controller) { - while (true) { - const { done, value } = await gen.next(); - if (done) { - try { - controller.byobRequest?.respond(0); - return controller.close(); - } catch { - controller.close(); - return controller.byobRequest?.respond(0); - } - } - if (controller.byobRequest?.view) { - const buffer = new Uint8Array(controller.byobRequest.view.buffer); - const size = buffer.length; - if (value.length > size) { - buffer.set(value.slice(0, size)); - controller.byobRequest.respond(size); - controller.enqueue(value.slice(size)); - } else { - buffer.set(value); - controller.byobRequest.respond(value.length); - } - break; - } else if (value.length) { - controller.enqueue(value); - break; + const value = await async function () { + while (true) { + const { done, value } = await gen.next(); + if (done) return undefined; + if (value.length) return value; } + }(); + + if (value == undefined) { + controller.close(); + return controller.byobRequest?.respond(0); } + + if (controller.byobRequest?.view) { + const buffer = new Uint8Array(controller.byobRequest.view.buffer); + const size = buffer.length; + if (value.length > size) { + buffer.set(value.slice(0, size)); + controller.byobRequest.respond(size); + controller.enqueue(value.slice(size)); + } else { + buffer.set(value); + controller.byobRequest.respond(value.length); + } + } else controller.enqueue(value); }, }); }