microcbor 0.1.0 → 0.2.0

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,11 +2,11 @@
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. You can use microcbor to serialize JavaScript values to CBOR, and to deserialize them back into JavaScript values again. **microcbor doesn't support tags, bigints, typed arrays, non-string keys, or indefinite-length collections.**
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
+ 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`.
10
10
 
11
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.
12
12
 
@@ -15,11 +15,13 @@ This library is TypeScript-native, ESM-only, and has just one dependency ([joelt
15
15
  - [Install](#install)
16
16
  - [Usage](#usage)
17
17
  - [API](#api)
18
+ - [Value types](#value-types)
18
19
  - [Encoding](#encoding)
19
20
  - [Decoding](#decoding)
20
- - [Strict mode](#strict-mode)
21
- - [Limitations](#limitations)
21
+ - [Encoding length](#encoding-length)
22
+ - [Support](#support)
22
23
  - [Testing](#testing)
24
+ - [Benchmarks](#benchmarks)
23
25
  - [Contributing](#contributing)
24
26
  - [License](#license)
25
27
 
@@ -32,7 +34,15 @@ npm i microcbor
32
34
  Or in Deno:
33
35
 
34
36
  ```typescript
35
- import { encode, decode } from "https://cdn.skypack.dev/microcbor"
37
+ import {
38
+ encode,
39
+ decode,
40
+ encodeStream,
41
+ decodeStream,
42
+ encodingLength,
43
+ CBORValue,
44
+ UnsafeIntegerError,
45
+ } from "https://cdn.skypack.dev/microcbor"
36
46
  ```
37
47
 
38
48
  ## Usage
@@ -55,91 +65,63 @@ console.log(decode(data))
55
65
 
56
66
  ## API
57
67
 
58
- ### Encoding
59
-
60
- ```typescript
61
- interface EncodeOptions {
62
- strict?: true
63
- chunkSize?: number
68
+ ### Value types
69
+
70
+ ```ts
71
+ declare type CBORValue =
72
+ | undefined
73
+ | null
74
+ | boolean
75
+ | number
76
+ | string
77
+ | Uint8Array
78
+ | CBORArray
79
+ | CBORMap
80
+
81
+ interface CBORArray extends Array<CBORValue> {}
82
+ interface CBORMap {
83
+ [key: string]: CBORValue
64
84
  }
65
-
66
- declare function encode(value: any, options: EncodeOptions = {}): Uint8Array
67
85
  ```
68
86
 
69
- `encode` accepts an `options` object that can have these properties:
70
-
71
- | property | type | default |
72
- | ------------ | ------- | ------- |
73
- | `strictJSON` | boolean | false |
74
- | `chunkSize` | number | 512 |
75
-
76
- ### Decoding
87
+ ### Encoding
77
88
 
78
89
  ```typescript
79
- interface DecodeOptions {
80
- strict?: boolean
81
- }
82
-
83
- declare function decode<T = any>(
84
- data: Uint8Array,
85
- decode: DecodeOptions = {}
86
- ): T
90
+ declare function encode(
91
+ value: CBORValue,
92
+ options: { chunkSize?: number } = {}
93
+ ): Uint8Array
94
+
95
+ declare function encodeStream(
96
+ source: AsyncIterable<CBORValue>,
97
+ options?: { chunkSize?: number }
98
+ ): AsyncIterable<Uint8Array>
87
99
  ```
88
100
 
89
- `decode` accepts an `options` object that can have these properties:
90
-
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...
101
+ If not provided, `chunkSize` defaults to 512 bytes. It's only a guideline; `encodeStream` won't break up individual CBOR values like strings or byte arrays that are larger than the provided chunk size.
98
102
 
99
- ```javascript
100
- import { encode } from "microcbor"
101
-
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
- ```
106
-
107
- ... and decode them...
103
+ ### Decoding
108
104
 
109
- ```javascript
110
- import { decode } from "microcbor"
105
+ ```typescript
106
+ declare function decode(data: Uint8Array): CBORValue
111
107
 
112
- decode(new Uint8Array([249, 126, 0])) // NaN
113
- decode(new Uint8Array([249, 124, 0])) // Infinity
114
- decode(new Uint8Array([249, 252, 0])) // -Infinity
108
+ declare function decodeStream(
109
+ source: AsyncIterable<Uint8Array>
110
+ ): AsyncIterable<CBORValue>
115
111
  ```
116
112
 
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`.
113
+ ### Encoding length
118
114
 
119
- ```javascript
120
- import { encode } from "microcbor"
115
+ You can measure the byte length that a given value will serialize to without actually allocating anything.
121
116
 
122
- encode(NaN, { strictJSON: true })
123
- // Uncaught Error: cannot encode NaN when strict mode is enabled
117
+ ```ts
118
+ declare function encodingLength(value: CBORValue): number
124
119
  ```
125
120
 
126
- ```javascript
127
- import { decode } from "microcbor"
128
-
129
- decode(new Uint8Array([249, 126, 0]), { strictJSON: true })
130
- // Uncaught Error: cannot decode NaN when strict mode is enabled
131
- ```
132
-
133
- For reference, `JSON.stringify` returns `"null"` when called with `NaN` or `+/- Infinity`.
134
-
135
- ## Limitations
136
-
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
-
139
- microcbor takes an opinionated, minimal stance:
121
+ ## Unsafe integer handling
140
122
 
141
123
  - 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.
124
+ - 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.
143
125
 
144
126
  ```typescript
145
127
  declare class UnsafeIntegerError extends RangeError {
@@ -148,14 +130,52 @@ declare class UnsafeIntegerError extends RangeError {
148
130
  }
149
131
  ```
150
132
 
133
+ ## Value mapping
134
+
135
+ | CBOR major type | JavaScript | notes |
136
+ | ---------------------------- | -------------- | -------------------------------------------------------- |
137
+ | `0` (non-negative integer) | `number` | decoding throws an `UnsafeIntegerError` on unsafe values |
138
+ | `1` (negative integer) | `number` | decoding throws an `UnsafeIntegerError` on unsafe values |
139
+ | `2` (byte string) | `Uint8Array` | |
140
+ | `3` (UTF-8 string) | `string` | |
141
+ | `4` (array) | `Array` | |
142
+ | `5` (map) | `Object` | decoding throws an error on non-string keys |
143
+ | `6` (tagged item) | Unsupported ❌ | decoding throws an error on non-string keys |
144
+ | `7` (floating-point numbers) | `number` | |
145
+ | `7` (booleans) | `boolean` | |
146
+ | `7` (null) | `null` | |
147
+ | `7` (undefined) | `undefined` | |
148
+
151
149
  ## Testing
152
150
 
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!
151
+ 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!
154
152
 
155
153
  ```
156
154
  npm run test
157
155
  ```
158
156
 
157
+ ## Benchmarks
158
+
159
+ Basic testing in [src/benchmarks.test.js](src/benchmarks.test.js) indicate that microcbor is about **2x as fast** as node-cbor at encoding and about **1.5x as fast** as node-cbor at decoding.
160
+
161
+ ```
162
+ microcbor % npm run test -- test/benchmarks.test.js
163
+
164
+ > microcbor@0.2.0 test
165
+ > ava
166
+
167
+
168
+ ✔ time encode() (382ms)
169
+ ℹ microcbor: 63.44141721725464 (ms)
170
+ ℹ node-cbor: 152.31466674804688 (ms)
171
+ ✔ time decode() (164ms)
172
+ ℹ microcbor: 72.13012504577637 (ms)
173
+ ℹ node-cbor: 87.16287469863892 (ms)
174
+
175
+
176
+ 2 tests passed
177
+ ```
178
+
159
179
  ## Contributing
160
180
 
161
181
  I don't expect to add any additional features to this library. But if you have suggestions for better interfaces, find a bug, or would like to add more tests, please open an issue to discuss it!
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>;