Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions cbor/_common_encode.ts
Original file line number Diff line number Diff line change
@@ -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<ReadonlyCborType, ReadonlyCborType> {
return x instanceof Map;
}

function calcBytes(x: bigint): number {
let bytes = 0;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -61,7 +68,7 @@ export function calcEncodingSize(x: CborType): number {
}

export function encode(
input: CborType,
input: ReadonlyCborType,
output: Uint8Array,
offset: number,
): number {
Expand All @@ -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);
}
Expand Down Expand Up @@ -202,7 +210,7 @@ function encodeString(
}

function encodeArray(
input: CborType[],
input: readonly ReadonlyCborType[],
output: Uint8Array,
offset: number,
): number {
Expand All @@ -212,7 +220,7 @@ function encodeArray(
}

function encodeObject(
input: { [k: string]: CborType },
input: { readonly [k: string]: ReadonlyCborType },
output: Uint8Array,
offset: number,
): number {
Expand All @@ -231,7 +239,7 @@ function encodeDate(input: Date, output: Uint8Array, offset: number): number {
}

function encodeTag(
input: CborTag<CborType>,
input: CborTag<ReadonlyCborType>,
output: Uint8Array,
offset: number,
): number {
Expand All @@ -255,7 +263,7 @@ function encodeTag(
}

function encodeMap(
input: Map<CborType, CborType>,
input: ReadonlyMap<ReadonlyCborType, ReadonlyCborType>,
output: Uint8Array,
offset: number,
): number {
Expand Down
11 changes: 6 additions & 5 deletions cbor/encode_cbor.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
Expand Down
13 changes: 8 additions & 5 deletions cbor/encode_cbor_sequence.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions cbor/encode_cbor_sequence_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

import { assertEquals } from "@std/assert";
import { encodeCborSequence } from "./encode_cbor_sequence.ts";
import type { CborType } from "./types.ts";

Deno.test("encodeCborSequence() correctly encoding", () => {
assertEquals(
encodeCborSequence([0, 0]),
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),
);
});
50 changes: 50 additions & 0 deletions cbor/encode_cbor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CborType, CborType> = new Map<CborType, CborType>([
[1, 2],
["3", 4],
[[5], { a: 6 }],
]);

assertEquals(
encodeCbor(map),
encodeCbor(
new Map<CborType, CborType>([
[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))),
);
});
6 changes: 4 additions & 2 deletions cbor/sequence_encoder_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ export class CborSequenceEncoderStream
} else yield encodeCbor(x);
}

async *#encodeArray(x: CborStreamInput[]): AsyncGenerator<Uint8Array> {
async *#encodeArray(
x: readonly CborStreamInput[],
): AsyncGenerator<Uint8Array> {
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) {
Expand All @@ -162,7 +164,7 @@ export class CborSequenceEncoderStream
}

async *#encodeObject(
x: { [k: string]: CborStreamInput },
x: { readonly [k: string]: CborStreamInput },
): AsyncGenerator<Uint8Array> {
const len = Object.keys(x).length;
if (len < 24) yield new Uint8Array([0b101_00000 + len]);
Expand Down
14 changes: 11 additions & 3 deletions cbor/tag.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<T extends CborType | CborStreamInput | CborStreamOutput> {
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.
Expand Down
37 changes: 35 additions & 2 deletions cbor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ReadonlyCborType>
| ReadonlyMap<ReadonlyCborType, ReadonlyCborType>
| 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>
| CborStreamInput[]
| { [k: string]: CborStreamInput }
| readonly CborStreamInput[]
| { readonly [k: string]: CborStreamInput }
| CborByteEncoderStream
| CborTextEncoderStream
| CborArrayEncoderStream
Expand Down
Loading