diff --git a/cbor/_common_encode.ts b/cbor/_common_encode.ts index b038bd86eb4a..ab8c299a67cd 100644 --- a/cbor/_common_encode.ts +++ b/cbor/_common_encode.ts @@ -1,7 +1,14 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { CborTag } from "./tag.ts"; -import type { CborType } from "./types.ts"; +import type { ReadonlyCborType } from "./types.ts"; + +// Narrows to ReadonlyMap (which TS's instanceof Map doesn't do on its own). +function isReadonlyMap( + x: unknown, +): x is ReadonlyMap { + return x instanceof Map; +} function calcBytes(x: bigint): number { let bytes = 0; @@ -20,7 +27,7 @@ function calcHeaderSize(x: number | bigint): number { return 9; } -export function calcEncodingSize(x: CborType): number { +export function calcEncodingSize(x: ReadonlyCborType): number { if (x == undefined || typeof x === "boolean") return 1; if (typeof x === "number") { return x % 1 === 0 ? calcHeaderSize(x < 0 ? -x - 1 : x) : 9; @@ -46,7 +53,7 @@ export function calcEncodingSize(x: CborType): number { for (const y of x) size += calcEncodingSize(y); return size; } - if (x instanceof Map) { + if (isReadonlyMap(x)) { let size = 3 + calcHeaderSize(x.size); for (const y of x) size += calcEncodingSize(y[0]) + calcEncodingSize(y[1]); return size; @@ -61,7 +68,7 @@ export function calcEncodingSize(x: CborType): number { } export function encode( - input: CborType, + input: ReadonlyCborType, output: Uint8Array, offset: number, ): number { @@ -86,8 +93,9 @@ export function encode( return encodeDate(input, output, offset); } else if (input instanceof CborTag) { return encodeTag(input, output, offset); - } else if (input instanceof Map) return encodeMap(input, output, offset); - else if (input instanceof Array) { + } else if (isReadonlyMap(input)) { + return encodeMap(input, output, offset); + } else if (input instanceof Array) { return encodeArray(input, output, offset); } else return encodeObject(input, output, offset); } @@ -202,7 +210,7 @@ function encodeString( } function encodeArray( - input: CborType[], + input: readonly ReadonlyCborType[], output: Uint8Array, offset: number, ): number { @@ -212,7 +220,7 @@ function encodeArray( } function encodeObject( - input: { [k: string]: CborType }, + input: { readonly [k: string]: ReadonlyCborType }, output: Uint8Array, offset: number, ): number { @@ -231,7 +239,7 @@ function encodeDate(input: Date, output: Uint8Array, offset: number): number { } function encodeTag( - input: CborTag, + input: CborTag, output: Uint8Array, offset: number, ): number { @@ -255,7 +263,7 @@ function encodeTag( } function encodeMap( - input: Map, + input: ReadonlyMap, output: Uint8Array, offset: number, ): number { diff --git a/cbor/encode_cbor.ts b/cbor/encode_cbor.ts index 8483d3a410a9..3de64d8f7a26 100644 --- a/cbor/encode_cbor.ts +++ b/cbor/encode_cbor.ts @@ -1,11 +1,11 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { calcEncodingSize, encode } from "./_common_encode.ts"; -import type { CborType } from "./types.ts"; +import type { CborType, ReadonlyCborType } from "./types.ts"; /** - * Encodes a {@link CborType} value into a CBOR format represented as a - * {@link Uint8Array}. + * Encodes a {@link CborType} or {@link ReadonlyCborType} 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 @@ -32,10 +32,11 @@ import type { CborType } from "./types.ts"; * assertEquals(decodedMessage, rawMessage); * ``` * - * @param value The value to encode of type {@link CborType}. + * @param value The value to encode of type {@link CborType} or + * {@link ReadonlyCborType}. * @returns A {@link Uint8Array} representing the encoded data. */ -export function encodeCbor(value: CborType): Uint8Array { +export function encodeCbor(value: CborType | ReadonlyCborType): Uint8Array { const output = new Uint8Array(calcEncodingSize(value)); const o = encode(value, output, 0); if (o !== output.length) return output.subarray(0, o); diff --git a/cbor/encode_cbor_sequence.ts b/cbor/encode_cbor_sequence.ts index 2a3f3661f8af..e9051976ad8e 100644 --- a/cbor/encode_cbor_sequence.ts +++ b/cbor/encode_cbor_sequence.ts @@ -1,11 +1,11 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { calcEncodingSize, encode } from "./_common_encode.ts"; -import type { CborType } from "./types.ts"; +import type { CborType, ReadonlyCborType } from "./types.ts"; /** - * Encodes an array of {@link CborType} values into a CBOR format sequence - * represented as a {@link Uint8Array}. + * Encodes an array of {@link CborType} or {@link ReadonlyCborType} 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 @@ -31,10 +31,13 @@ import type { CborType } from "./types.ts"; * assertEquals(decodedMessage, rawMessage); * ``` * - * @param values An array of values to encode of type {@link CborType} + * @param values An array of values to encode, each of type {@link CborType} + * or {@link ReadonlyCborType}. * @returns A {@link Uint8Array} representing the encoded data. */ -export function encodeCborSequence(values: CborType[]): Uint8Array { +export function encodeCborSequence( + values: readonly (CborType | ReadonlyCborType)[], +): Uint8Array { let o = 0; for (const value of values) o += calcEncodingSize(value); const output = new Uint8Array(o); diff --git a/cbor/encode_cbor_sequence_test.ts b/cbor/encode_cbor_sequence_test.ts index 24200c4264a7..425f5309c62e 100644 --- a/cbor/encode_cbor_sequence_test.ts +++ b/cbor/encode_cbor_sequence_test.ts @@ -2,6 +2,7 @@ import { assertEquals } from "@std/assert"; import { encodeCborSequence } from "./encode_cbor_sequence.ts"; +import type { CborType } from "./types.ts"; Deno.test("encodeCborSequence() correctly encoding", () => { assertEquals( @@ -9,3 +10,12 @@ Deno.test("encodeCborSequence() correctly encoding", () => { Uint8Array.from([0b000_00000, 0b000_00000]), ); }); + +Deno.test("encodeCborSequence() accepting readonly array input", () => { + const values = [1, "two", { three: 3n }] as const; + const mutable: CborType[] = [1, "two", { three: 3n }]; + assertEquals( + encodeCborSequence(values), + encodeCborSequence(mutable), + ); +}); diff --git a/cbor/encode_cbor_test.ts b/cbor/encode_cbor_test.ts index 0e71085be55d..4e408dd0e75f 100644 --- a/cbor/encode_cbor_test.ts +++ b/cbor/encode_cbor_test.ts @@ -498,3 +498,53 @@ Deno.test("encodeCbor() rejecting CborTag()", () => { `Cannot encode Tag Item: Tag Number (${num}) exceeds 2 ** 64 - 1`, ); }); + +Deno.test("encodeCbor() accepting `as const` (deeply readonly) input", () => { + const data = { + a: 1, + b: { c: 2n }, + d: [3, { e: 4 }], + } as const; + + assertEquals(encodeCbor(data), encodeCbor(structuredClone(data))); +}); + +Deno.test("encodeCbor() accepting readonly array literals", () => { + const tuple = ["hello", 42, { nested: "value" }] as const; + assertEquals( + encodeCbor(tuple), + encodeCbor(["hello", 42, { nested: "value" }]), + ); +}); + +Deno.test("encodeCbor() accepting ReadonlyMap input", () => { + const map: ReadonlyMap = new Map([ + [1, 2], + ["3", 4], + [[5], { a: 6 }], + ]); + + assertEquals( + encodeCbor(map), + encodeCbor( + new Map([ + [1, 2], + ["3", 4], + [[5], { a: 6 }], + ]), + ), + ); +}); + +Deno.test("encodeCbor() accepting readonly index signature", () => { + const obj: { readonly [k: string]: number } = { x: 1, y: 2, z: 3 }; + assertEquals(encodeCbor(obj), encodeCbor({ x: 1, y: 2, z: 3 })); +}); + +Deno.test("encodeCbor() accepting CborTag with readonly content", () => { + const data = [1, { inner: 2 }] as const; + assertEquals( + encodeCbor(new CborTag(1, data)), + encodeCbor(new CborTag(1, structuredClone(data))), + ); +}); diff --git a/cbor/sequence_encoder_stream.ts b/cbor/sequence_encoder_stream.ts index 990c57c4d166..9f59c5c4ff80 100644 --- a/cbor/sequence_encoder_stream.ts +++ b/cbor/sequence_encoder_stream.ts @@ -145,7 +145,9 @@ export class CborSequenceEncoderStream } else yield encodeCbor(x); } - async *#encodeArray(x: CborStreamInput[]): AsyncGenerator { + async *#encodeArray( + x: readonly CborStreamInput[], + ): 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) { @@ -162,7 +164,7 @@ export class CborSequenceEncoderStream } async *#encodeObject( - x: { [k: string]: CborStreamInput }, + x: { readonly [k: string]: CborStreamInput }, ): AsyncGenerator { const len = Object.keys(x).length; if (len < 24) yield new Uint8Array([0b101_00000 + len]); diff --git a/cbor/tag.ts b/cbor/tag.ts index 497ba62482cf..ee3b7f4faf41 100644 --- a/cbor/tag.ts +++ b/cbor/tag.ts @@ -1,6 +1,11 @@ // Copyright 2018-2026 the Deno authors. MIT license. -import type { CborStreamInput, CborStreamOutput, CborType } from "./types.ts"; +import type { + CborStreamInput, + CborStreamOutput, + CborType, + ReadonlyCborType, +} from "./types.ts"; /** * Represents a CBOR tag, which pairs a tag number with content, used to convey @@ -30,9 +35,12 @@ import type { CborStreamInput, CborStreamOutput, CborType } from "./types.ts"; * ``` * * @typeParam T The type of the tag's content, which can be a - * {@link CborType}, {@link CborStreamInput}, or {@link CborStreamOutput}. + * {@link CborType}, {@link ReadonlyCborType}, {@link CborStreamInput}, or + * {@link CborStreamOutput}. */ -export class CborTag { +export class CborTag< + T extends CborType | ReadonlyCborType | CborStreamInput | CborStreamOutput, +> { /** * A {@link number} or {@link bigint} representing the CBOR tag number, used * to identify the type of the tagged content. diff --git a/cbor/types.ts b/cbor/types.ts index d75b773650a9..dc7d83cc03b4 100644 --- a/cbor/types.ts +++ b/cbor/types.ts @@ -28,6 +28,9 @@ export type CborPrimitiveType = * This type specifies the encodable and decodable values for * {@link encodeCbor}, {@link decodeCbor}, {@link encodeCborSequence}, and * {@link decodeCborSequence}. + * + * The encoder functions also accept {@link ReadonlyCborType}, which permits + * `readonly` arrays, `ReadonlyMap`, and `readonly` index signatures. */ export type CborType = | CborPrimitiveType @@ -38,15 +41,45 @@ export type CborType = [k: string]: CborType; }; +/** + * Readonly variant of {@link CborType} accepted by {@link encodeCbor} and + * {@link encodeCborSequence}. Lets you pass `as const` literals, + * `ReadonlyMap`s, and frozen arrays without casting. + * + * @example Usage + * ```ts + * import { encodeCbor } from "@std/cbor"; + * + * const data = { + * a: 1, + * b: { c: 2n }, + * d: [3, { e: 4 }], + * } as const; + * + * encodeCbor(data); + * ``` + */ +export type ReadonlyCborType = + | CborPrimitiveType + | CborTag + | ReadonlyMap + | readonly ReadonlyCborType[] + | { + readonly [k: string]: ReadonlyCborType; + }; + /** * Specifies the encodable value types for the {@link CborSequenceEncoderStream} * and {@link CborArrayEncoderStream}. + * + * Arrays and index signatures are `readonly` so `as const` literals work + * without casting. */ export type CborStreamInput = | CborPrimitiveType | CborTag - | CborStreamInput[] - | { [k: string]: CborStreamInput } + | readonly CborStreamInput[] + | { readonly [k: string]: CborStreamInput } | CborByteEncoderStream | CborTextEncoderStream | CborArrayEncoderStream