microcbor 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,24 +2,26 @@
2
2
 
3
3
  [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg)](https://github.com/RichardLitt/standard-readme) [![license](https://img.shields.io/github/license/joeltg/microcbor)](https://opensource.org/licenses/MIT) [![NPM version](https://img.shields.io/npm/v/microcbor)](https://www.npmjs.com/package/microcbor) ![TypeScript types](https://img.shields.io/npm/types/microcbor) ![lines of code](https://img.shields.io/tokei/lines/github/joeltg/microcbor)
4
4
 
5
- Encode JSON values as canonical CBOR.
5
+ Encode JavaScript values as canonical CBOR.
6
6
 
7
- microcbor is a [CBOR](https://cbor.io/) implementation that only supports the subset of CBOR that corresponds to JSON. This includes `null`, `true`, `false`, numbers, strings, objects (string keys only), and arrays. You can use microcbor to serialize JSON values to CBOR, and to deserialize them back into JSON values again. microcbor doesn't support decoding items that don't correspond to JSON values: no tags, byte strings, typed arrays, `undefined`, or indefinite length collections.
7
+ microcbor is a minimal JavaScript [CBOR](https://cbor.io/) implementation featuring
8
8
 
9
- microcbor also follows the [deterministic CBOR encoding requirements](https://www.rfc-editor.org/rfc/rfc8949.html#core-det) - all floating-point numbers are serialized in the smallest possible size without losing precision, and object entries are always sorted by key in byte-wise lexicographic order. `NaN` is always serialized as `0xf97e00`.
9
+ - a small footprint,
10
+ - fast performance, and
11
+ - an async iterable streaming API
10
12
 
11
- This library is TypeScript-native, ESM-only, and has just one dependency ([joeltg/fp16](https://github.com/joeltg/fp16) for half-precision floats). It works in Node, the browser, and Deno.
13
+ microcbor follows the [deterministic CBOR encoding requirements](https://www.rfc-editor.org/rfc/rfc8949.html#core-det) - all floating-point numbers are serialized in the smallest possible size without losing precision, and object entries are always sorted by key in byte-wise lexicographic order. `NaN` is always serialized as `0xf97e00`. **microcbor doesn't support tags, bigints, typed arrays, non-string keys, or indefinite-length collections.**
14
+
15
+ This library is TypeScript-native, ESM-only, and has just one dependency [joeltg/fp16](https://github.com/joeltg/fp16) for half-precision floats. It works in Node, the browser, and Deno.
12
16
 
13
17
  ## Table of Contents
14
18
 
15
19
  - [Install](#install)
16
20
  - [Usage](#usage)
17
21
  - [API](#api)
18
- - [Encoding](#encoding)
19
- - [Decoding](#decoding)
20
- - [Strict mode](#strict-mode)
21
- - [Limitations](#limitations)
22
+ - [Value mapping](#value-mapping)
22
23
  - [Testing](#testing)
24
+ - [Benchmarks](#benchmarks)
23
25
  - [Contributing](#contributing)
24
26
  - [License](#license)
25
27
 
@@ -55,105 +57,108 @@ console.log(decode(data))
55
57
 
56
58
  ## API
57
59
 
58
- ### Encoding
59
-
60
- ```typescript
61
- interface EncodeOptions {
62
- strict?: true
63
- chunkSize?: number
60
+ ```ts
61
+ declare type CBORValue =
62
+ | undefined
63
+ | null
64
+ | boolean
65
+ | number
66
+ | string
67
+ | Uint8Array
68
+ | CBORArray
69
+ | CBORMap
70
+
71
+ interface CBORArray extends Array<CBORValue> {}
72
+ interface CBORMap {
73
+ [key: string]: CBORValue
64
74
  }
65
75
 
66
- declare function encode(value: any, options: EncodeOptions = {}): Uint8Array
76
+ // If not provided, chunkSize defaults to 512 bytes.
77
+ // It's only a guideline; `encodeStream` won't break up
78
+ // individual CBOR values like strings or byte arrays
79
+ // that are larger than the provided chunk size.
80
+ declare function encode(
81
+ value: CBORValue,
82
+ options?: { chunkSize?: number }
83
+ ): Uint8Array
84
+
85
+ declare function encodeStream(
86
+ source: AsyncIterable<CBORValue>,
87
+ options?: { chunkSize?: number }
88
+ ): AsyncIterable<Uint8Array>
89
+
90
+ declare function decode(data: Uint8Array): CBORValue
91
+
92
+ declare function decodeStream(
93
+ source: AsyncIterable<Uint8Array>
94
+ ): AsyncIterable<CBORValue>
95
+
96
+ // You can measure the byte length that a given value will
97
+ // serialize to without actually allocating anything.
98
+ declare function encodingLength(value: CBORValue): number
67
99
  ```
68
100
 
69
- `encode` accepts an `options` object that can have these properties:
101
+ ## Unsafe integer handling
70
102
 
71
- | property | type | default |
72
- | ------------ | ------- | ------- |
73
- | `strictJSON` | boolean | false |
74
- | `chunkSize` | number | 512 |
75
-
76
- ### Decoding
103
+ - JavaScript integers below `Number.MIN_SAFE_INTEGER` or greater than `Number.MAX_SAFE_INTEGER` will encode as CBOR floating-point numbers, as per the [suggestion in the CBOR spec](https://www.rfc-editor.org/rfc/rfc8949.html#name-converting-from-json-to-cbo).
104
+ - decoding **CBOR integers** less than `Number.MIN_SAFE_INTEGER` (major type 1 with uint64 argument greater than `2^53-2`) or greater than `Number.MAX_SAFE_INTEGER` (major type 0 with uint64 argument greater than `2^53-1`) **will throw an error**. The error will be an instance of `UnsafeIntegerError` and will have the out-of-range value as a readonly `.value: bigint` property.
77
105
 
78
106
  ```typescript
79
- interface DecodeOptions {
80
- strict?: boolean
107
+ declare class UnsafeIntegerError extends RangeError {
108
+ readonly value: bigint
109
+ constructor(message: string, value: bigint)
81
110
  }
82
-
83
- declare function decode<T = any>(
84
- data: Uint8Array,
85
- decode: DecodeOptions = {}
86
- ): T
87
111
  ```
88
112
 
89
- `decode` accepts an `options` object that can have these properties:
113
+ ## Value mapping
114
+
115
+ | CBOR major type | JavaScript | notes |
116
+ | ---------------------------- | --------------- | -------------------------------------------------------- |
117
+ | `0` (non-negative integer) | `number` | decoding throws an `UnsafeIntegerError` on unsafe values |
118
+ | `1` (negative integer) | `number` | decoding throws an `UnsafeIntegerError` on unsafe values |
119
+ | `2` (byte string) | `Uint8Array` | |
120
+ | `3` (UTF-8 string) | `string` | |
121
+ | `4` (array) | `Array` | |
122
+ | `5` (map) | `Object` | decoding throws an error on non-string keys |
123
+ | `6` (tagged item) | **Unsupported** | |
124
+ | `7` (floating-point numbers) | `number` | |
125
+ | `7` (booleans) | `boolean` | |
126
+ | `7` (null) | `null` | |
127
+ | `7` (undefined) | `undefined` | |
90
128
 
91
- | property | type | default |
92
- | ------------ | ------- | ------- |
93
- | `strictJSON` | boolean | false |
94
-
95
- ### Strict mode
96
-
97
- Technically, neither `NaN` nor `+/- Infinity` are valid JSON numbers. But microcbor functions as a direct interface between JavaScript and CBOR, so by default, you can encode them...
129
+ ## Testing
98
130
 
99
- ```javascript
100
- import { encode } from "microcbor"
131
+ Tests use [AVA](https://github.com/avajs/ava) and live in the [test](./test/) directory. Tests use [node-cbor](https://github.com/hildjj/node-cbor/) to validate encoding results. More tests are always welcome!
101
132
 
102
- encode(NaN) // Uint8Array(3) [ 249, 126, 0 ]
103
- encode(Infinity) // Uint8Array(3) [ 249, 124, 0 ]
104
- encode(-Infinity) // Uint8Array(3) [ 249, 252, 0 ]
105
133
  ```
106
-
107
- ... and decode them...
108
-
109
- ```javascript
110
- import { decode } from "microcbor"
111
-
112
- decode(new Uint8Array([249, 126, 0])) // NaN
113
- decode(new Uint8Array([249, 124, 0])) // Infinity
114
- decode(new Uint8Array([249, 252, 0])) // -Infinity
134
+ npm run test
115
135
  ```
116
136
 
117
- If you don't want this behavior - for example, if it's important to validate that all of the values you decode are JSON-serializable - you can set `options.strict` to `true`, and microcbor will throw an error if it encounters `NaN` or `+/- Infinity`.
118
-
119
- ```javascript
120
- import { encode } from "microcbor"
137
+ ## Comparison to node-cbor
121
138
 
122
- encode(NaN, { strictJSON: true })
123
- // Uncaught Error: cannot encode NaN when strict mode is enabled
124
- ```
125
-
126
- ```javascript
127
- import { decode } from "microcbor"
139
+ - microcbor runs isomorphically on the web, in Node, and in Deno. node-cbor ships a separate cbor-web package.
140
+ - microcbor encodes `Uint8Array` values as CBOR byte strings (major type 2). node-cbor encodes `Uint8Array` values as tagged type arrays (major type 6 / RFC 8746), and encodes NodeJS `Buffer` values as CBOR byte strings (major type 2).
141
+ - microcbor uses async iterables for its streaming API. node-cbor uses NodeJS streams.
142
+ - microcbor is about **2x faster** than node-cbor at encoding and about **1.5x faster** than node-cbor at decoding.
128
143
 
129
- decode(new Uint8Array([249, 126, 0]), { strictJSON: true })
130
- // Uncaught Error: cannot decode NaN when strict mode is enabled
131
144
  ```
145
+ microcbor % npm run test -- test/benchmarks.test.js
132
146
 
133
- For reference, `JSON.stringify` returns `"null"` when called with `NaN` or `+/- Infinity`.
134
-
135
- ## Limitations
147
+ > microcbor@0.2.0 test
148
+ > ava
136
149
 
137
- JSON numbers can be arbitrarly large and have unlimited decimal precision. CBOR has explicit types for the standard fixed-size integer and float formats, and separate tags for [arbitrarily large integers](https://www.rfc-editor.org/rfc/rfc8949.html#name-bignums) and [unlimited-precision decimal values](https://www.rfc-editor.org/rfc/rfc8949.html#name-decimal-fractions-and-bigfl). Meanwhile, JavaScript can only represent numbers as 64-bit floats or BigInts. This means there are always tradeoffs in interfacing between JSON, CBOR, and JavaScript.
138
150
 
139
- microcbor takes an opinionated, minimal stance:
140
-
141
- - JavaScript integers below `Number.MIN_SAFE_INTEGER` or greater than `Number.MAX_SAFE_INTEGER` will encode as CBOR floating-point numbers, as per the [suggestion in the CBOR spec](https://www.rfc-editor.org/rfc/rfc8949.html#name-converting-from-json-to-cbo).
142
- - decoding CBOR integers less than `Number.MIN_SAFE_INTEGER` (major type 1 with uint64 argument greater than `2^53-2`) or greater than `Number.MAX_SAFE_INTEGER` (major type 0 with uint64 argument greater than `2^53-1`) **will throw an error**. The error will be an instance of `UnsafeIntegerError` and will have the out-of-range value as a readonly `.value: bigint` property.
151
+ time encode() (390ms)
152
+ ℹ microcbor: 66.47262525558472 (ms)
153
+ ℹ node-cbor: 155.0249171257019 (ms)
154
+ JSON.stringify: 5.56374979019165 (ms)
155
+ ✔ time decode() (161ms)
156
+ ℹ microcbor: 64.23729228973389 (ms)
157
+ ℹ node-cbor: 91.34658432006836 (ms)
158
+ ℹ JSON.parse: 2.7592921257019043 (ms)
159
+
143
160
 
144
- ```typescript
145
- declare class UnsafeIntegerError extends RangeError {
146
- readonly value: bigint
147
- constructor(message: string, value: bigint)
148
- }
149
- ```
150
-
151
- ## Testing
152
-
153
- Tests use [AVA 4](https://github.com/avajs/ava) (currently in alpha) and live in the [test](./test/) directory. Tests use [node-cbor](https://github.com/hildjj/node-cbor/) to validate encoding results. More tests are always welcome!
154
-
155
- ```
156
- npm run test
161
+ 2 tests passed
157
162
  ```
158
163
 
159
164
  ## Contributing
package/lib/decode.d.ts CHANGED
@@ -1,8 +1,2 @@
1
- export interface DecodeOptions {
2
- strictJSON?: boolean;
3
- }
4
- export declare class UnsafeIntegerError extends RangeError {
5
- readonly value: bigint;
6
- constructor(message: string, value: bigint);
7
- }
8
- export declare function decode<T = any>(data: Uint8Array, options?: DecodeOptions): T;
1
+ import type { CBORValue } from "./types.js";
2
+ export declare function decode(data: Uint8Array): CBORValue;
package/lib/decode.js CHANGED
@@ -1,174 +1,141 @@
1
1
  import { getFloat16 } from "fp16";
2
- const maxSafeInteger = BigInt(Number.MAX_SAFE_INTEGER);
3
- const minSafeInteger = BigInt(Number.MIN_SAFE_INTEGER);
4
- function validateFloat(state, value) {
5
- if (state.options.strictJSON) {
6
- if (isNaN(value)) {
7
- throw new Error("cannot decode NaN when strict mode is enabled");
8
- }
9
- else if (value === Infinity || value === -Infinity) {
10
- throw new Error("cannot decode +/- Infinity when strict mode is enabled");
11
- }
12
- }
13
- }
14
- const constants = {
15
- float16(state) {
16
- const value = getFloat16(state.view, state.offset);
17
- validateFloat(state, value);
18
- state.offset += 2;
19
- return value;
20
- },
21
- float32(state) {
22
- const value = state.view.getFloat32(state.offset);
23
- validateFloat(state, value);
24
- state.offset += 4;
25
- return value;
26
- },
27
- float64(state) {
28
- const value = state.view.getFloat64(state.offset);
29
- validateFloat(state, value);
30
- state.offset += 8;
31
- return value;
32
- },
33
- uint8(state) {
34
- const value = state.view.getUint8(state.offset);
35
- state.offset += 1;
36
- return value;
37
- },
38
- uint16(state) {
39
- const value = state.view.getUint16(state.offset);
40
- state.offset += 2;
41
- return value;
42
- },
43
- uint32(state) {
44
- const value = state.view.getUint32(state.offset);
45
- state.offset += 4;
46
- return value;
47
- },
48
- uint64(state) {
49
- const value = state.view.getBigUint64(state.offset);
50
- state.offset += 8;
2
+ import { UnsafeIntegerError, maxSafeInteger, minSafeInteger } from "./utils.js";
3
+ class Decoder {
4
+ constructor(data) {
5
+ this.data = data;
6
+ this.constant = (size, f) => () => {
7
+ const value = f();
8
+ this.offset += size;
9
+ return value;
10
+ };
11
+ this.float16 = this.constant(2, () => getFloat16(this.view, this.offset));
12
+ this.float32 = this.constant(4, () => this.view.getFloat32(this.offset));
13
+ this.float64 = this.constant(8, () => this.view.getFloat64(this.offset));
14
+ this.uint8 = this.constant(1, () => this.view.getUint8(this.offset));
15
+ this.uint16 = this.constant(2, () => this.view.getUint16(this.offset));
16
+ this.uint32 = this.constant(4, () => this.view.getUint32(this.offset));
17
+ this.uint64 = this.constant(8, () => this.view.getBigUint64(this.offset));
18
+ this.offset = 0;
19
+ this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
20
+ }
21
+ decodeBytes(length) {
22
+ const value = new Uint8Array(length);
23
+ value.set(this.data.subarray(this.offset, this.offset + length), 0);
24
+ this.offset += length;
51
25
  return value;
52
- },
53
- };
54
- function decodeString(state, length) {
55
- const view = new DataView(state.view.buffer, state.view.byteOffset + state.offset, length);
56
- state.offset += length;
57
- return new TextDecoder().decode(view);
58
- }
59
- function getArgument(state, additionalInformation) {
60
- if (additionalInformation < 24) {
61
- return { value: additionalInformation };
62
- }
63
- else if (additionalInformation === 24) {
64
- return { value: constants.uint8(state) };
65
26
  }
66
- else if (additionalInformation === 25) {
67
- return { value: constants.uint16(state) };
68
- }
69
- else if (additionalInformation === 26) {
70
- return { value: constants.uint32(state) };
71
- }
72
- else if (additionalInformation === 27) {
73
- const uint64 = constants.uint64(state);
74
- const value = maxSafeInteger < uint64 ? Infinity : Number(uint64);
75
- return { value, uint64 };
76
- }
77
- else if (additionalInformation === 31) {
78
- throw new Error("microcbor does not support decoding indefinite-length items");
79
- }
80
- else {
81
- throw new Error("invalid argument encoding");
82
- }
83
- }
84
- export class UnsafeIntegerError extends RangeError {
85
- constructor(message, value) {
86
- super(message);
87
- this.value = value;
27
+ decodeString(length) {
28
+ const value = new TextDecoder().decode(this.data.subarray(this.offset, this.offset + length));
29
+ this.offset += length;
30
+ return value;
88
31
  }
89
- }
90
- function decodeValue(state) {
91
- const initialByte = constants.uint8(state);
92
- const majorType = initialByte >> 5;
93
- const additionalInformation = initialByte & 0x1f;
94
- if (majorType === 0) {
95
- const { value, uint64 } = getArgument(state, additionalInformation);
96
- if (uint64 !== undefined && maxSafeInteger < uint64) {
97
- throw new UnsafeIntegerError("cannot decode integers greater than 2^53-1", uint64);
32
+ getArgument(additionalInformation) {
33
+ if (additionalInformation < 24) {
34
+ return { value: additionalInformation };
98
35
  }
99
- else {
100
- return value;
36
+ else if (additionalInformation === 24) {
37
+ return { value: this.uint8() };
101
38
  }
102
- }
103
- else if (majorType === 1) {
104
- const { value, uint64 } = getArgument(state, additionalInformation);
105
- if (uint64 !== undefined && -1n - uint64 < minSafeInteger) {
106
- throw new UnsafeIntegerError("cannot decode integers less than -2^53+1", -1n - uint64);
39
+ else if (additionalInformation === 25) {
40
+ return { value: this.uint16() };
41
+ }
42
+ else if (additionalInformation === 26) {
43
+ return { value: this.uint32() };
44
+ }
45
+ else if (additionalInformation === 27) {
46
+ const uint64 = this.uint64();
47
+ const value = maxSafeInteger < uint64 ? Infinity : Number(uint64);
48
+ return { value, uint64 };
49
+ }
50
+ else if (additionalInformation === 31) {
51
+ throw new Error("microcbor does not support decoding indefinite-length items");
107
52
  }
108
53
  else {
109
- return -1 - value;
54
+ throw new Error("invalid argument encoding");
110
55
  }
111
56
  }
112
- else if (majorType === 2) {
113
- throw new Error("microcbor does not support byte strings");
114
- }
115
- else if (majorType === 3) {
116
- const { value: length } = getArgument(state, additionalInformation);
117
- return decodeString(state, length);
118
- }
119
- else if (majorType === 4) {
120
- const { value: length } = getArgument(state, additionalInformation);
121
- const value = new Array(length);
122
- for (let i = 0; i < length; i++) {
123
- value[i] = decodeValue(state);
57
+ decodeValue() {
58
+ const initialByte = this.uint8();
59
+ const majorType = initialByte >> 5;
60
+ const additionalInformation = initialByte & 0x1f;
61
+ if (majorType === 0) {
62
+ const { value, uint64 } = this.getArgument(additionalInformation);
63
+ if (uint64 !== undefined && maxSafeInteger < uint64) {
64
+ throw new UnsafeIntegerError("cannot decode integers greater than 2^53-1", uint64);
65
+ }
66
+ else {
67
+ return value;
68
+ }
124
69
  }
125
- return value;
126
- }
127
- else if (majorType === 5) {
128
- const { value: length } = getArgument(state, additionalInformation);
129
- const value = {};
130
- for (let i = 0; i < length; i++) {
131
- const key = decodeValue(state);
132
- if (typeof key !== "string") {
133
- throw new Error("microcbor only supports string keys in objects");
70
+ else if (majorType === 1) {
71
+ const { value, uint64 } = this.getArgument(additionalInformation);
72
+ if (uint64 !== undefined && -1n - uint64 < minSafeInteger) {
73
+ throw new UnsafeIntegerError("cannot decode integers less than -2^53+1", -1n - uint64);
74
+ }
75
+ else {
76
+ return -1 - value;
134
77
  }
135
- value[key] = decodeValue(state);
136
78
  }
137
- return value;
138
- }
139
- else if (majorType === 6) {
140
- throw new Error("microcbor does not support tagged data items");
141
- }
142
- else if (majorType === 7) {
143
- switch (additionalInformation) {
144
- case 20:
145
- return false;
146
- case 21:
147
- return true;
148
- case 22:
149
- return null;
150
- case 23:
151
- throw new Error("microcbor does not support the undefined value");
152
- case 24:
153
- throw new Error("microcbor does not support decoding unassigned simple values");
154
- case 25:
155
- return constants.float16(state);
156
- case 26:
157
- return constants.float32(state);
158
- case 27:
159
- return constants.float64(state);
160
- case 31:
161
- throw new Error("microcbor does not support decoding indefinite-length items");
162
- default:
163
- throw new Error("invalid simple value");
79
+ else if (majorType === 2) {
80
+ const { value: length } = this.getArgument(additionalInformation);
81
+ return this.decodeBytes(length);
82
+ }
83
+ else if (majorType === 3) {
84
+ const { value: length } = this.getArgument(additionalInformation);
85
+ return this.decodeString(length);
86
+ }
87
+ else if (majorType === 4) {
88
+ const { value: length } = this.getArgument(additionalInformation);
89
+ const value = new Array(length);
90
+ for (let i = 0; i < length; i++) {
91
+ value[i] = this.decodeValue();
92
+ }
93
+ return value;
94
+ }
95
+ else if (majorType === 5) {
96
+ const { value: length } = this.getArgument(additionalInformation);
97
+ const value = {};
98
+ for (let i = 0; i < length; i++) {
99
+ const key = this.decodeValue();
100
+ if (typeof key !== "string") {
101
+ throw new Error("microcbor only supports string keys in objects");
102
+ }
103
+ value[key] = this.decodeValue();
104
+ }
105
+ return value;
106
+ }
107
+ else if (majorType === 6) {
108
+ throw new Error("microcbor does not support tagged data items");
109
+ }
110
+ else if (majorType === 7) {
111
+ switch (additionalInformation) {
112
+ case 20:
113
+ return false;
114
+ case 21:
115
+ return true;
116
+ case 22:
117
+ return null;
118
+ case 23:
119
+ return undefined;
120
+ case 24:
121
+ throw new Error("microcbor does not support decoding unassigned simple values");
122
+ case 25:
123
+ return this.float16();
124
+ case 26:
125
+ return this.float32();
126
+ case 27:
127
+ return this.float64();
128
+ case 31:
129
+ throw new Error("microcbor does not support decoding indefinite-length items");
130
+ default:
131
+ throw new Error("invalid simple value");
132
+ }
133
+ }
134
+ else {
135
+ throw new Error("invalid major type");
164
136
  }
165
- }
166
- else {
167
- throw new Error("invalid major type");
168
137
  }
169
138
  }
170
- export function decode(data, options = {}) {
171
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
172
- const state = { options, offset: 0, view };
173
- return decodeValue(state);
139
+ export function decode(data) {
140
+ return new Decoder(data).decodeValue();
174
141
  }
@@ -0,0 +1,2 @@
1
+ import type { CBORValue } from "./types.js";
2
+ export declare function decodeStream(source: AsyncIterable<Uint8Array>): AsyncIterable<CBORValue>;