@atcute/cbor 2.0.0 → 2.1.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/lib/encode.ts CHANGED
@@ -1,102 +1,142 @@
1
1
  import { CidLinkWrapper, fromString, type CidLink } from '@atcute/cid';
2
+ import { allocUnsafe, concat, encodeUtf8Into } from '@atcute/uint8array';
2
3
 
3
4
  import { BytesWrapper, fromBytes, type Bytes } from './bytes.js';
4
5
 
6
+ const MAX_TYPE_ARG_LEN = 9;
5
7
  const CHUNK_SIZE = 1024;
6
8
 
7
- const utf8e = new TextEncoder();
8
-
9
9
  interface State {
10
10
  c: Uint8Array[];
11
- b: ArrayBuffer;
12
- v: DataView;
11
+ b: Uint8Array;
13
12
  p: number;
13
+ l: number;
14
14
  }
15
15
 
16
+ const _abs = Math.abs;
17
+ const _floor = Math.floor;
18
+ const _log2 = Math.log2;
19
+ const _max = Math.max;
20
+
21
+ const _isInteger = Number.isInteger;
22
+ const _isNaN = Number.isNaN;
23
+
24
+ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
25
+ const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER;
26
+
16
27
  const resizeIfNeeded = (state: State, needed: number): void => {
17
28
  const buf = state.b;
18
29
  const pos = state.p;
19
30
 
20
31
  if (buf.byteLength < pos + needed) {
21
- state.c.push(new Uint8Array(buf, 0, pos));
32
+ state.c.push(buf.subarray(0, pos));
33
+ state.l += pos;
22
34
 
23
- state.b = new ArrayBuffer(Math.max(CHUNK_SIZE, needed));
24
- state.v = new DataView(state.b);
35
+ state.b = allocUnsafe(_max(CHUNK_SIZE, needed));
25
36
  state.p = 0;
26
37
  }
27
38
  };
28
39
 
29
- const getInfo = (arg: number): number => {
30
- if (arg < 24) {
31
- return arg;
32
- } else if (arg < 0x100) {
33
- return 24;
34
- } else if (arg < 0x10000) {
35
- return 25;
36
- } else if (arg < 0x100000000) {
37
- return 26;
38
- } else {
39
- return 27;
40
- }
40
+ const getTypeInfoLength = (arg: number): number => {
41
+ return arg < 24 ? 1 : arg < 0x100 ? 2 : arg < 0x10000 ? 3 : arg < 0x100000000 ? 5 : 9;
41
42
  };
42
43
 
43
44
  const writeFloat64 = (state: State, val: number): void => {
44
- resizeIfNeeded(state, 8);
45
+ let pos = state.p;
46
+ const buf = state.b;
45
47
 
46
- state.v.setFloat64(state.p, val);
47
- state.p += 8;
48
+ const sign = val < 0 ? 1 : 0;
49
+ val = _abs(val);
50
+
51
+ const exp = _floor(_log2(val));
52
+ let frac = val / 2 ** exp - 1;
53
+
54
+ const biasedExp = exp + 1023;
55
+
56
+ buf[pos++] = (sign << 7) | (biasedExp >>> 4);
57
+ buf[pos++] = ((biasedExp & 0xf) << 4) | ((frac * 16) >>> 0);
58
+
59
+ frac *= 16;
60
+ for (let i = 0; i < 6; i++) {
61
+ frac = (frac % 1) * 256;
62
+ buf[pos++] = frac >>> 0;
63
+ }
64
+
65
+ state.p = pos;
48
66
  };
49
67
 
50
68
  const writeUint8 = (state: State, val: number): void => {
51
- resizeIfNeeded(state, 1);
52
-
53
- state.v.setUint8(state.p, val);
54
- state.p += 1;
69
+ state.b[state.p++] = val;
55
70
  };
56
71
 
57
72
  const writeUint16 = (state: State, val: number): void => {
58
- resizeIfNeeded(state, 2);
73
+ let pos = state.p;
74
+
75
+ const buf = state.b;
76
+
77
+ buf[pos++] = val >>> 8;
78
+ buf[pos++] = val & 0xff;
59
79
 
60
- state.v.setUint16(state.p, val);
61
- state.p += 2;
80
+ state.p = pos;
62
81
  };
63
82
 
64
83
  const writeUint32 = (state: State, val: number): void => {
65
- resizeIfNeeded(state, 4);
84
+ let pos = state.p;
66
85
 
67
- state.v.setUint32(state.p, val);
68
- state.p += 4;
86
+ const buf = state.b;
87
+
88
+ buf[pos++] = val >>> 24;
89
+ buf[pos++] = (val >>> 16) & 0xff;
90
+ buf[pos++] = (val >>> 8) & 0xff;
91
+ buf[pos++] = val & 0xff;
92
+
93
+ state.p = pos;
69
94
  };
70
95
 
71
96
  const writeUint64 = (state: State, val: number): void => {
97
+ let pos = state.p;
98
+
99
+ const buf = state.b;
100
+
72
101
  const hi = (val / 2 ** 32) | 0;
73
102
  const lo = val >>> 0;
74
103
 
75
- resizeIfNeeded(state, 8);
104
+ buf[pos++] = hi >>> 24;
105
+ buf[pos++] = (hi >>> 16) & 0xff;
106
+ buf[pos++] = (hi >>> 8) & 0xff;
107
+ buf[pos++] = hi & 0xff;
76
108
 
77
- state.v.setUint32(state.p, hi);
78
- state.v.setUint32(state.p + 4, lo);
79
- state.p += 8;
109
+ buf[pos++] = lo >>> 24;
110
+ buf[pos++] = (lo >>> 16) & 0xff;
111
+ buf[pos++] = (lo >>> 8) & 0xff;
112
+ buf[pos++] = lo & 0xff;
113
+
114
+ state.p = pos;
80
115
  };
81
116
 
82
117
  const writeTypeAndArgument = (state: State, type: number, arg: number): void => {
83
- const info = getInfo(arg);
84
-
85
- writeUint8(state, (type << 5) | info);
86
-
87
- switch (info) {
88
- case 24:
89
- return writeUint8(state, arg);
90
- case 25:
91
- return writeUint16(state, arg);
92
- case 26:
93
- return writeUint32(state, arg);
94
- case 27:
95
- return writeUint64(state, arg);
118
+ if (arg < 24) {
119
+ writeUint8(state, (type << 5) | arg);
120
+ } else if (arg < 0x100) {
121
+ writeUint8(state, (type << 5) | 24);
122
+ writeUint8(state, arg);
123
+ } else if (arg < 0x10000) {
124
+ writeUint8(state, (type << 5) | 25);
125
+ writeUint16(state, arg);
126
+ } else if (arg < 0x100000000) {
127
+ writeUint8(state, (type << 5) | 26);
128
+ writeUint32(state, arg);
129
+ } else {
130
+ writeUint8(state, (type << 5) | 27);
131
+ writeUint64(state, arg);
96
132
  }
97
133
  };
98
134
 
135
+ // --- Functions below MUST be cautious about ensuring there's enough room in the buffer!!
136
+
99
137
  const writeInteger = (state: State, val: number): void => {
138
+ resizeIfNeeded(state, MAX_TYPE_ARG_LEN);
139
+
100
140
  if (val < 0) {
101
141
  writeTypeAndArgument(state, 1, -val - 1);
102
142
  } else {
@@ -105,35 +145,55 @@ const writeInteger = (state: State, val: number): void => {
105
145
  };
106
146
 
107
147
  const writeFloat = (state: State, val: number): void => {
148
+ resizeIfNeeded(state, 9);
149
+
108
150
  writeUint8(state, 0xe0 | 27);
109
151
  writeFloat64(state, val);
110
152
  };
111
153
 
112
154
  const writeNumber = (state: State, val: number): void => {
113
- if (Number.isNaN(val)) {
155
+ if (_isNaN(val)) {
114
156
  throw new RangeError(`NaN values not supported`);
115
157
  }
116
158
 
117
- if (val > Number.MAX_SAFE_INTEGER || val < Number.MIN_SAFE_INTEGER) {
159
+ if (val > MAX_SAFE_INTEGER || val < MIN_SAFE_INTEGER) {
118
160
  throw new RangeError(`can't encode numbers beyond safe integer range`);
119
161
  }
120
162
 
121
- if (Number.isInteger(val)) {
163
+ if (_isInteger(val)) {
122
164
  writeInteger(state, val);
123
165
  } else {
166
+ // Note: https://atproto.com/specs/data-model#:~:text=not%20allowed%20in%20atproto
124
167
  writeFloat(state, val);
125
168
  }
126
169
  };
127
170
 
128
171
  const writeString = (state: State, val: string): void => {
129
- const buf = utf8e.encode(val);
130
- const len = buf.byteLength;
172
+ // JS strings are UTF-16 (ECMA spec)
173
+ // Therefore, worst case length of UTF-8 is length * 3. (plus 9 bytes of CBOR header)
174
+ // Greatly overshoots in practice, but doesn't matter. (alloc is O(1)+ anyway)
175
+ const strLength = val.length;
176
+ resizeIfNeeded(state, strLength * 3 + MAX_TYPE_ARG_LEN);
177
+
178
+ // Credit: method used by cbor-x
179
+ // Rather than allocate a buffer and then copy it back to the destination buffer:
180
+ // - Estimate the length of the header based on the UTF-16 size of the string.
181
+ // Should be accurate most of the time, see last point for when it isn't.
182
+ // - Directly write the string at the estimated location, retrieving with it the actual length.
183
+ // - Write the header now that the length is available.
184
+ // - If the estimation happened to be wrong, correct the placement of the string.
185
+ // While it's costly, it's actually roughly the same cost as if we encoded it separately + copy.
186
+ const estimatedHeaderSize = getTypeInfoLength(strLength);
187
+ const estimatedPosition = state.p + estimatedHeaderSize;
188
+ const len = encodeUtf8Into(state.b, val, estimatedPosition);
189
+
190
+ const headerSize = getTypeInfoLength(len);
191
+ if (estimatedHeaderSize !== headerSize) {
192
+ // Estimation was incorrect, move the bytes to the real place.
193
+ state.b.copyWithin(state.p + headerSize, estimatedPosition, estimatedPosition + len);
194
+ }
131
195
 
132
196
  writeTypeAndArgument(state, 3, len);
133
- resizeIfNeeded(state, len);
134
-
135
- new Uint8Array(state.b, state.p).set(buf);
136
-
137
197
  state.p += len;
138
198
  };
139
199
 
@@ -141,11 +201,10 @@ const writeBytes = (state: State, val: Bytes): void => {
141
201
  const buf = fromBytes(val);
142
202
  const len = buf.byteLength;
143
203
 
144
- writeTypeAndArgument(state, 2, len);
145
- resizeIfNeeded(state, len);
146
-
147
- new Uint8Array(state.b, state.p, len).set(buf);
204
+ resizeIfNeeded(state, len + MAX_TYPE_ARG_LEN);
148
205
 
206
+ writeTypeAndArgument(state, 2, len);
207
+ state.b.set(buf, state.p);
149
208
  state.p += len;
150
209
  };
151
210
 
@@ -155,90 +214,86 @@ const writeCid = (state: State, val: CidLink): void => {
155
214
  const buf = val instanceof CidLinkWrapper ? val.bytes : fromString(val.$link).bytes;
156
215
  const len = buf.byteLength + 1;
157
216
 
217
+ resizeIfNeeded(state, len + 2 * MAX_TYPE_ARG_LEN);
218
+
158
219
  writeTypeAndArgument(state, 6, 42);
159
220
  writeTypeAndArgument(state, 2, len);
160
221
 
161
- resizeIfNeeded(state, len);
162
-
163
- new Uint8Array(state.b, state.p + 1, len - 1).set(buf);
222
+ state.b[state.p] = 0;
223
+ state.b.set(buf, state.p + 1);
164
224
 
165
225
  state.p += len;
166
226
  };
167
227
 
168
228
  const writeValue = (state: State, val: any): void => {
169
- if (val === undefined) {
170
- throw new TypeError(`undefined values not supported`);
171
- }
172
-
173
- if (val === null) {
174
- return writeUint8(state, 0xf6);
175
- }
176
-
177
- if (val === false) {
178
- return writeUint8(state, 0xf4);
179
- }
180
-
181
- if (val === true) {
182
- return writeUint8(state, 0xf5);
183
- }
184
-
185
- if (typeof val === 'number') {
186
- return writeNumber(state, val);
187
- }
188
-
189
- if (typeof val === 'string') {
190
- return writeString(state, val);
191
- }
229
+ switch (typeof val) {
230
+ case 'boolean': {
231
+ resizeIfNeeded(state, 1);
232
+ return writeUint8(state, 0xf4 + +val);
233
+ }
234
+ case 'number': {
235
+ return writeNumber(state, val);
236
+ }
237
+ case 'string': {
238
+ return writeString(state, val);
239
+ }
240
+ case 'object': {
241
+ // case: null
242
+ if (val === null) {
243
+ resizeIfNeeded(state, 1);
244
+ return writeUint8(state, 0xf6);
245
+ }
192
246
 
193
- if (typeof val === 'object') {
194
- if (isArray(val)) {
195
- const len = val.length;
247
+ // case: array
248
+ if (Array.isArray(val)) {
249
+ const len = val.length;
250
+ resizeIfNeeded(state, MAX_TYPE_ARG_LEN);
251
+ writeTypeAndArgument(state, 4, len);
196
252
 
197
- writeTypeAndArgument(state, 4, len);
253
+ for (let idx = 0; idx < len; idx++) {
254
+ writeValue(state, val[idx]);
255
+ }
198
256
 
199
- for (let idx = 0; idx < len; idx++) {
200
- const v = val[idx];
201
- writeValue(state, v);
257
+ return;
202
258
  }
203
259
 
204
- return;
205
- }
260
+ // case: cid-link
261
+ if ('$link' in val) {
262
+ if (val instanceof CidLinkWrapper || typeof val.$link === 'string') {
263
+ writeCid(state, val);
264
+ return;
265
+ }
206
266
 
207
- if ('$link' in val) {
208
- if (val instanceof CidLinkWrapper || typeof val.$link === 'string') {
209
- writeCid(state, val);
210
- return;
267
+ throw new TypeError(`unexpected cid-link value`);
211
268
  }
212
269
 
213
- throw new TypeError(`unexpected cid-link value`);
214
- }
270
+ // case: bytes
271
+ if ('$bytes' in val) {
272
+ if (val instanceof BytesWrapper || typeof val.$bytes === 'string') {
273
+ writeBytes(state, val);
274
+ return;
275
+ }
215
276
 
216
- if ('$bytes' in val) {
217
- if (val instanceof BytesWrapper || typeof val.$bytes === 'string') {
218
- writeBytes(state, val);
219
- return;
277
+ throw new TypeError(`unexpected bytes value`);
220
278
  }
221
279
 
222
- throw new TypeError(`unexpected bytes value`);
223
- }
224
-
225
- if (isPlainObject(val)) {
226
- const keys = Object.keys(val)
227
- .filter((key) => typeof key === 'string' && val[key] !== undefined)
228
- .sort(compareKeys);
280
+ // case: POJO
281
+ if (val.constructor === Object) {
282
+ const keys = getOrderedObjectKeys(val);
283
+ const len = keys.length;
229
284
 
230
- const len = keys.length;
285
+ resizeIfNeeded(state, MAX_TYPE_ARG_LEN);
286
+ writeTypeAndArgument(state, 5, len);
231
287
 
232
- writeTypeAndArgument(state, 5, len);
288
+ for (let idx = 0; idx < len; idx++) {
289
+ const key = keys[idx];
233
290
 
234
- for (let idx = 0; idx < len; idx++) {
235
- const key = keys[idx];
291
+ writeString(state, key);
292
+ writeValue(state, val[key]);
293
+ }
236
294
 
237
- writeString(state, key);
238
- writeValue(state, val[key]);
295
+ return;
239
296
  }
240
-
241
- return;
242
297
  }
243
298
  }
244
299
 
@@ -246,61 +301,53 @@ const writeValue = (state: State, val: any): void => {
246
301
  };
247
302
 
248
303
  const createState = (): State => {
249
- const buf = new ArrayBuffer(CHUNK_SIZE);
304
+ const buf = allocUnsafe(CHUNK_SIZE);
250
305
 
251
306
  return {
252
307
  c: [],
253
308
  b: buf,
254
- v: new DataView(buf),
255
309
  p: 0,
310
+ l: 0,
256
311
  };
257
312
  };
258
313
 
259
314
  export const encode = (value: any): Uint8Array => {
260
315
  const state = createState();
261
- const chunks = state.c;
262
316
 
263
317
  writeValue(state, value);
264
- chunks.push(new Uint8Array(state.b, 0, state.p));
265
-
266
- let size = 0;
267
- let written = 0;
268
-
269
- let len = chunks.length;
270
- let idx: number;
271
-
272
- for (idx = 0; idx < len; idx++) {
273
- size += chunks[idx].byteLength;
274
- }
275
-
276
- const u8 = new Uint8Array(size);
277
318
 
278
- for (idx = 0; idx < len; idx++) {
279
- const chunk = chunks[idx];
280
-
281
- u8.set(chunk, written);
282
- written += chunk.byteLength;
283
- }
284
-
285
- return u8;
319
+ state.c.push(state.b.subarray(0, state.p));
320
+ return concat(state.c, state.l + state.p);
286
321
  };
287
322
 
288
- const isArray = Array.isArray;
289
- const isPlainObject = (v: any): boolean => {
290
- if (typeof v !== 'object' || v === null) {
291
- return false;
292
- }
293
-
294
- const proto = Object.getPrototypeOf(v);
295
- return proto === Object.prototype || proto === null;
296
- };
323
+ /** @internal */
324
+ export const getOrderedObjectKeys = (obj: Record<string, unknown>): string[] => {
325
+ const keys = Object.keys(obj);
326
+ for (let i = 1, len = keys.length, j = 0; i < len; j = i++) {
327
+ const valA = keys[i];
328
+
329
+ // Tuck in undefined value filtering here to avoid extra iterations.
330
+ if (obj[valA] === undefined) {
331
+ // A lot of things are tucked in here xd
332
+ // - Pull the currently last item in the keys array at the current place
333
+ // - Update saved value of array length
334
+ // - Decrease i by 1
335
+ keys[i--] = keys[--len];
336
+ keys.length = len;
337
+ } else {
338
+ for (; j >= 0; j--) {
339
+ const valB = keys[j];
340
+
341
+ // Note: Don't need to check for equality, keys are always distinct.
342
+ const cmp = valA.length - valB.length || +(valA > valB);
343
+ if (cmp > 0) break;
344
+
345
+ keys[j + 1] = valB;
346
+ }
297
347
 
298
- const compareKeys = (a: string, b: string): number => {
299
- if (a.length < b.length) {
300
- return -1;
301
- } else if (b.length < a.length) {
302
- return 1;
303
- } else {
304
- return a < b ? -1 : 1;
348
+ keys[j + 1] = valA;
349
+ }
305
350
  }
351
+
352
+ return keys;
306
353
  };
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@atcute/cbor",
4
- "version": "2.0.0",
5
- "description": "DAG-CBOR codec that deals in AT Protocol's HTTP wire format",
4
+ "version": "2.1.0",
5
+ "description": "lightweight DASL dCBOR42 codec library for AT Protocol",
6
+ "keywords": [
7
+ "atproto",
8
+ "dasl",
9
+ "cbor"
10
+ ],
6
11
  "license": "MIT",
7
12
  "repository": {
8
13
  "url": "https://github.com/mary-ext/atcute",
@@ -19,16 +24,17 @@
19
24
  },
20
25
  "sideEffects": false,
21
26
  "devDependencies": {
22
- "@ipld/dag-cbor": "^9.2.1",
23
- "@types/bun": "^1.1.12",
24
- "mitata": "^1.0.10"
27
+ "@ipld/dag-cbor": "^9.2.2",
28
+ "@types/bun": "^1.1.14",
29
+ "cbor-x": "^1.6.0"
25
30
  },
26
31
  "dependencies": {
27
- "@atcute/cid": "^2.0.0",
28
- "@atcute/multibase": "^1.0.1"
32
+ "@atcute/cid": "^2.1.0",
33
+ "@atcute/uint8array": "^1.0.0",
34
+ "@atcute/multibase": "^1.1.0"
29
35
  },
30
36
  "scripts": {
31
- "build": "tsc --project tsconfig.build.json",
37
+ "build": "rm -rf dist; tsc --project tsconfig.build.json",
32
38
  "test": "bun test --coverage",
33
39
  "prepublish": "rm -rf dist; pnpm run build"
34
40
  }