Skip to content

Commit

Permalink
Merge pull request #23 from hildjj/cde-dcbor
Browse files Browse the repository at this point in the history
Add explicit CDE and dCBOR support
  • Loading branch information
hildjj authored Jan 23, 2024
2 parents 5670495 + 1c9ca84 commit 682021e
Show file tree
Hide file tree
Showing 18 changed files with 498 additions and 153 deletions.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@ format ([RFC8949](https://www.rfc-editor.org/rfc/rfc8949.html)).

This package supersedes [node-cbor](https://github.com/hildjj/node-cbor/tree/main/packages/cbor), with the following goals:

- Web-first. Usable in node.
- Web-first. Usable in Node and Deno.
- Simpler where possible. Remove non-core features in favor of extensibility.
- Synchronous decoding. Removes streaming capabilities that are rarely used.
- Complete break of API compatibility, allowing use of newer JavaScript constructs.
- No work-arounds for older environments.

## Supported Node.js versions

This project now only supports versions of Node that the Node team is
This project now only supports versions of Node.js that the Node team is
[currently supporting](https://github.com/nodejs/Release#release-schedule).
Ava's [support
statement](https://github.com/avajs/ava/blob/main/docs/support-statement.md)
is what we will be using as well. Since the first release will not be soon,
that means Node `18`+ is required.
Currently, that means Node `18`+ is required.

## Installation

Expand Down
5 changes: 5 additions & 0 deletions src/box.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-redeclare */
import {SYMS} from './constants.js';
import {Tag} from './tag.js';

Expand Down Expand Up @@ -37,6 +38,10 @@ export function saveEncoded(obj: OriginalEncoding, orig: Uint8Array): void {
});
}

export function box(value: bigint, orig: Uint8Array): BigInt;
export function box(value: string, orig: Uint8Array): String;
export function box(value: number, orig: Uint8Array): Number;

/**
* Put an object wrapper around a primitive value, such as a number, boolean,
* or bigint, storing the original CBOR encoding of the value for later
Expand Down
2 changes: 1 addition & 1 deletion src/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ function output(
}

const CommentOptionsDefault: RequiredCommentOptions = {
...CBORcontainer.defaultOptions,
...CBORcontainer.defaultDecodeOptions,
initialDepth: 0,
noPrefixHex: false,
minCol: 0,
Expand Down
83 changes: 54 additions & 29 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type {DecodeOptions, MtAiValue, Parent, RequiredDecodeOptions} from './options.js';
import {type KeyValueEncoded, sortCoreDeterministic} from './sorts.js';
import {MT, NUMBYTES} from './constants.js';
import type {MtAiValue, Parent, RequiredDecodeOptions} from './options.js';
import {box, getEncoded, saveEncoded} from './box.js';
import {u8concat, u8toHex} from './utils.js';
import {DecodeStream} from './decodeStream.js';
import type {KeyValueEncoded} from './sorts.js';
import {Simple} from './simple.js';
import {Tag} from './tag.js';
import {checkSubnormal} from './float.js';
import {encode} from './encoder.js';

const LENGTH_FOR_AI = new Map([
Expand All @@ -18,17 +19,6 @@ const LENGTH_FOR_AI = new Map([

const EMPTY_BUF = new Uint8Array(0);

// Decide on dCBOR approach
// export const dCBORdecodeOptions: ContainerOptions = {
// rejectLargeNegatives: true,
// rejectLongLoundNaN: true,
// rejectLongNumbers: true,
// rejectNegativeZero: true,
// rejectSimple: true,
// rejectStreaming: true,
// sortKeys: sortCoreDeterministic,
// };

/**
* A CBOR data item that can contain other items. One of:
*
Expand All @@ -40,25 +30,66 @@ const EMPTY_BUF = new Uint8Array(0);
* This is used in various decoding applications to keep track of state.
*/
export class CBORcontainer {
public static defaultOptions: RequiredDecodeOptions = {
public static defaultDecodeOptions: RequiredDecodeOptions = {
...DecodeStream.defaultOptions,
ParentType: CBORcontainer,
boxed: false,
cde: false,
dcbor: false,
rejectLargeNegatives: false,
rejectBigInts: false,
rejectDuplicateKeys: false,
rejectFloats: false,
rejectInts: false,
rejectLongLoundNaN: false,
rejectLongNumbers: false,
rejectLongFloats: false,
rejectNegativeZero: false,
rejectSimple: false,
rejectStreaming: false,
rejectSubnormals: false,
rejectUndefined: false,
saveOriginal: false,
sortKeys: null,
};

/**
* Throw errors when decoding for bytes that were not encoded with {@link
* https://www.ietf.org/archive/id/draft-ietf-cbor-cde-01.html CBOR Common
* Deterministic Encoding Profile}.
*
* CDE does not mandate this checking, so it is up to the application
* whether it wants to ensure that inputs were not encoded incompetetently
* or maliciously. To turn all of these on at once, set the cbor option to
* true.
*/
public static cdeDecodeOptions: DecodeOptions = {
cde: true,
rejectStreaming: true,
requirePreferred: true,
sortKeys: sortCoreDeterministic,
};

/**
* Throw errors when decoding for bytes that were not encoded with {@link
* https://www.ietf.org/archive/id/draft-mcnally-deterministic-cbor-07.html
* dCBOR: A Deterministic CBOR Application Profile}.
*
* The dCBOR spec mandates that these errors be thrown when decoding dCBOR.
* Turn this on by setting the `dcbor` option to true, which also enables
* `cde` mode.
*/
public static dcborDecodeOptions: DecodeOptions = {
...this.cdeDecodeOptions,
dcbor: true,
rejectDuplicateKeys: true,
rejectLargeNegatives: true,
rejectLongLoundNaN: true,
rejectLongFloats: true,
rejectNegativeZero: true,
rejectSimple: true,
rejectUndefined: true,
};

public parent: Parent | undefined;
public mt: number;
public ai: number;
Expand Down Expand Up @@ -124,22 +155,12 @@ export class CBORcontainer {
if (opts.rejectInts) {
throw new Error(`Unexpected integer: ${value}`);
}
if (opts.rejectLongNumbers && (ai > NUMBYTES.ZERO)) {
// No opts needed
const buf = encode(value, {chunkSize: 9});

// Known safe:
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (buf.length < LENGTH_FOR_AI.get(ai)!) {
throw new Error(`Int should have been encoded shorter: ${value}`);
}
}
if (opts.rejectLargeNegatives &&
(value as number < -0x8000000000000000n)) {
(value as bigint < -0x8000000000000000n)) {
throw new Error(`Invalid 65bit negative number: ${value}`);
}
if (opts.boxed) {
return box(value, stream.toHere(offset));
return box(value as number, stream.toHere(offset));
}
return value;
case MT.SIMPLE_FLOAT:
Expand All @@ -156,7 +177,11 @@ export class CBORcontainer {
throw new Error(`Invalid NaN encoding: "${u8toHex(buf)}"`);
}
}
if (opts.rejectLongNumbers) {
if (opts.rejectSubnormals) {
// Skip the size byte
checkSubnormal(stream.toHere(offset + 1));
}
if (opts.rejectLongFloats) {
// No opts needed.
const buf = encode(value, {chunkSize: 9});
if ((buf[0] >> 5) !== mt) {
Expand Down Expand Up @@ -190,7 +215,7 @@ export class CBORcontainer {
return new opts.ParentType(mav, Infinity, parent, opts);
}
if (opts.boxed) {
return box(value, stream.toHere(offset));
return box(value as string, stream.toHere(offset));
}
return value;
case MT.ARRAY:
Expand Down
12 changes: 12 additions & 0 deletions src/decodeStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class DecodeStream implements Sliceable {
public static defaultOptions: Required<DecodeStreamOptions> = {
maxDepth: 1024,
encoding: 'hex',
requirePreferred: false,
};

#src;
Expand Down Expand Up @@ -115,6 +116,8 @@ export class DecodeStream implements Sliceable {
throw new Error(`Invalid simple encoding in extra byte: ${val}`);
}
simple = true;
} else if (this.#opts.requirePreferred && (val < 24)) {
throw new Error(`Unexpectedly long integer encoding (1) for ${val}`);
}
break;
case NUMBYTES.TWO:
Expand All @@ -123,6 +126,9 @@ export class DecodeStream implements Sliceable {
val = parseHalf(this.#src, this.#offset);
} else {
val = this.#view.getUint16(this.#offset, false);
if (this.#opts.requirePreferred && (val <= 0xff)) {
throw new Error(`Unexpectedly long integer encoding (2) for ${val}`);
}
}
break;
case NUMBYTES.FOUR:
Expand All @@ -131,6 +137,9 @@ export class DecodeStream implements Sliceable {
val = this.#view.getFloat32(this.#offset, false);
} else {
val = this.#view.getUint32(this.#offset, false);
if (this.#opts.requirePreferred && (val <= 0xffff)) {
throw new Error(`Unexpectedly long integer encoding (4) for ${val}`);
}
}
break;
case NUMBYTES.EIGHT: {
Expand All @@ -142,6 +151,9 @@ export class DecodeStream implements Sliceable {
if (val <= Number.MAX_SAFE_INTEGER) {
val = Number(val);
}
if (this.#opts.requirePreferred && (val <= 0xffffffff)) {
throw new Error(`Unexpectedly long integer encoding (8) for ${val}`);
}
}
break;
}
Expand Down
18 changes: 13 additions & 5 deletions src/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ import {SYMS} from './constants.js';
*/
export function decode<T = unknown>(
src: Uint8Array | string,
options?: DecodeOptions
options: DecodeOptions = {}
): T {
const opts: Required<DecodeOptions> = {
...CBORcontainer.defaultOptions,
...options,
};
const opts = {...CBORcontainer.defaultDecodeOptions};
if (options.dcbor) {
Object.assign(opts, CBORcontainer.dcborDecodeOptions);
} else if (options.cde) {
Object.assign(opts, CBORcontainer.cdeDecodeOptions);
}
Object.assign(opts, options);

if (Object.hasOwn(opts, 'rejectLongNumbers')) {
throw new TypeError('rejectLongNumbers has changed to requirePreferred');
}

if (opts.boxed) {
opts.saveOriginal = true;
}
Expand Down
2 changes: 1 addition & 1 deletion src/diagnostic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function diagnose(
options?: DecodeOptions
): string {
const opts: Required<DecodeOptions> = {
...CBORcontainer.defaultOptions,
...CBORcontainer.defaultDecodeOptions,
...options,
ParentType: DiagContainer,
};
Expand Down
77 changes: 56 additions & 21 deletions src/encoder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-use-before-define */

import type {EncodeOptions, RequiredEncodeOptions} from './options.js';
import {type KeyValueEncoded, sortCoreDeterministic} from './sorts.js';
import {MT, NUMBYTES, SIMPLE, SYMS, TAG} from './constants.js';
import {type TagNumber, type TaggedValue, type ToCBOR, Writer} from './writer.js';
import type {KeyValueEncoded} from './sorts.js';
import {halfToUint} from './float.js';
import {flushToZero, halfToUint} from './float.js';
import {hexToU8} from './utils.js';

export const {
Expand All @@ -20,23 +20,14 @@ const UNDEFINED = (MT.SIMPLE_FLOAT << 5) | SIMPLE.UNDEFINED;
const NULL = (MT.SIMPLE_FLOAT << 5) | SIMPLE.NULL;
const TE = new TextEncoder();

// Decide on dCBOR approach
// export const dCBORencodeOptions: EncodeOptions = {
// // Default: collapseBigInts: true,
// ignoreOriginalEncoding: true,
// largeNegativeAsBigInt: true,
// rejectCustomSimples: true,
// rejectDuplicateKeys: true,
// rejectUndefined: true,
// simplifyNegativeZero: true,
// sortKeys: sortCoreDeterministic,
// };

export const EncodeOptionsDefault: RequiredEncodeOptions = {
export const defaultEncodeOptions: RequiredEncodeOptions = {
...Writer.defaultOptions,
avoidInts: false,
cde: false,
collapseBigInts: true,
dcbor: false,
float64: false,
flushToZero: false,
forceEndian: null,
ignoreOriginalEncoding: false,
largeNegativeAsBigInt: false,
Expand All @@ -49,6 +40,41 @@ export const EncodeOptionsDefault: RequiredEncodeOptions = {
sortKeys: null,
};

/**
* Encode with CDE ({@link
* https://www.ietf.org/archive/id/draft-ietf-cbor-cde-01.html CBOR Common
* Deterministic Encoding Profile}). Eable this set of options by setting
* `cde` to true.
*
* Since cbor2 always uses preferred encoding, this option only sets the
* sort algorithm for map/object keys, and ensures that any original
* encoding information (from decoding with saveOriginal) is ignored.
*/
export const cdeEncodeOptions: EncodeOptions = {
cde: true,
ignoreOriginalEncoding: true,
sortKeys: sortCoreDeterministic,
};

/**
* Encode with CDE and dCBOR ({@link
* https://www.ietf.org/archive/id/draft-mcnally-deterministic-cbor-07.html
* dCBOR: A Deterministic CBOR Application Profile}). Enable this set of
* options by setting `dcbor` to true.
*
* Several of these options can cause errors to be thrown for inputs that
* would have otherwise generated valid CBOR (e.g. `undefined`).
*/
export const dcborEncodeOptions: EncodeOptions = {
...cdeEncodeOptions,
dcbor: true,
largeNegativeAsBigInt: true,
rejectCustomSimples: true,
rejectDuplicateKeys: true,
rejectUndefined: true,
simplifyNegativeZero: true,
};

/**
* Any class. Ish.
*/
Expand Down Expand Up @@ -245,9 +271,14 @@ export function writeBigInt(
* @param opts Encoding options.
*/
export function writeNumber(
val: number, w: Writer,
val: number,
w: Writer,
opts: RequiredEncodeOptions
): void {
if (opts.flushToZero) {
val = flushToZero(val);
}

if (Object.is(val, -0)) {
if (opts.simplifyNegativeZero) {
if (opts.avoidInts) {
Expand Down Expand Up @@ -429,11 +460,15 @@ export function writeUnknown(
* @param options Tweak the conversion process.
* @returns Bytes in a Uint8Array buffer.
*/
export function encode(val: unknown, options?: EncodeOptions): Uint8Array {
const opts: RequiredEncodeOptions = {
...EncodeOptionsDefault,
...options,
};
export function encode(val: unknown, options: EncodeOptions = {}): Uint8Array {
const opts: RequiredEncodeOptions = {...defaultEncodeOptions};
if (options.dcbor) {
Object.assign(opts, dcborEncodeOptions);
} else if (options.cde) {
Object.assign(opts, cdeEncodeOptions);
}
Object.assign(opts, options);

const w = new Writer(opts);
writeUnknown(val, w, opts);
return w.read();
Expand Down
Loading

0 comments on commit 682021e

Please sign in to comment.