+11
.changeset/brave-dots-learn.md
+11
.changeset/brave-dots-learn.md
···
1
+
---
2
+
'@atcute/cid': minor
3
+
'@atcute/car': minor
4
+
'@atcute/cbor': minor
5
+
---
6
+
7
+
update to DASL spec 2025-10-20
8
+
9
+
- remove support for empty CIDs (zero-length digests), which were removed from the spec
10
+
- reject `Infinity` values in CBOR encoder (in addition to `NaN`)
11
+
- optimize streamed CAR reader by removing buffer concatenation for CID reads
+2
packages/utilities/car/lib/index.ts
+2
packages/utilities/car/lib/index.ts
+8
-13
packages/utilities/car/lib/reader.ts
+8
-13
packages/utilities/car/lib/reader.ts
···
132
132
};
133
133
134
134
const readCid = (reader: SyncByteReader): CID.Cid => {
135
-
const head = reader.exactly(4, false);
135
+
const bytes = reader.exactly(36, true);
136
136
137
-
const version = head[0];
138
-
const codec = head[1];
139
-
const digestType = head[2];
140
-
const digestSize = head[3];
137
+
const version = bytes[0];
138
+
const codec = bytes[1];
139
+
const digestType = bytes[2];
140
+
const digestSize = bytes[3];
141
141
142
142
if (version !== CID.CID_VERSION) {
143
143
throw new RangeError(`incorrect cid version (got v${version})`);
···
151
151
throw new RangeError(`incorrect cid digest type (got 0x${digestType.toString(16)})`);
152
152
}
153
153
154
-
if (digestSize !== 32 && digestSize !== 0) {
154
+
if (digestSize !== 32) {
155
155
throw new RangeError(`incorrect cid digest size (got ${digestSize})`);
156
156
}
157
157
158
-
const bytes = reader.exactly(4 + digestSize, true);
159
-
const digest = bytes.subarray(4, 4 + digestSize);
160
-
161
-
const cid: CID.Cid = {
158
+
return {
162
159
version: version,
163
160
codec: codec,
164
161
digest: {
165
162
codec: digestType,
166
-
contents: digest,
163
+
contents: bytes.subarray(4, 36),
167
164
},
168
165
bytes: bytes,
169
166
};
170
-
171
-
return cid;
172
167
};
+8
-15
packages/utilities/car/lib/streamed-reader.ts
+8
-15
packages/utilities/car/lib/streamed-reader.ts
···
1
1
import * as CBOR from '@atcute/cbor';
2
2
import type { Cid, CidLink } from '@atcute/cid';
3
3
import * as CID from '@atcute/cid';
4
-
import { concat } from '@atcute/uint8array';
5
4
6
5
import { isCarV1Header, type CarEntry, type CarHeader } from './types.js';
7
6
···
112
111
};
113
112
114
113
const readCid = async (): Promise<Cid> => {
115
-
const head = await readExact(4);
114
+
const bytes = await readExact(36);
116
115
117
-
const version = head[0];
118
-
const codec = head[1];
119
-
const digestType = head[2];
120
-
const digestSize = head[3];
116
+
const version = bytes[0];
117
+
const codec = bytes[1];
118
+
const digestType = bytes[2];
119
+
const digestSize = bytes[3];
121
120
122
121
if (version !== CID.CID_VERSION) {
123
122
throw new RangeError(`incorrect cid version (got v${version})`);
···
131
130
throw new RangeError(`incorrect cid digest type (got 0x${digestType.toString(16)})`);
132
131
}
133
132
134
-
if (digestSize !== 32 && digestSize !== 0) {
133
+
if (digestSize !== 32) {
135
134
throw new RangeError(`incorrect cid digest size (got ${digestSize})`);
136
135
}
137
136
138
-
// concatenate and have digest refer back to this buffer
139
-
const bytes = concat([head, await readExact(digestSize)]);
140
-
const digest = bytes.subarray(4, 4 + digestSize);
141
-
142
-
const cid: Cid = {
137
+
return {
143
138
version: version,
144
139
codec: codec,
145
140
digest: {
146
141
codec: digestType,
147
-
contents: digest,
142
+
contents: bytes.subarray(4, 36),
148
143
},
149
144
bytes: bytes,
150
145
};
151
-
152
-
return cid;
153
146
};
154
147
155
148
return {
+3
-3
packages/utilities/cbor/lib/encode.ts
+3
-3
packages/utilities/cbor/lib/encode.ts
···
17
17
const _max = Math.max;
18
18
19
19
const _isInteger = Number.isInteger;
20
-
const _isNaN = Number.isNaN;
20
+
const _isFinite = Number.isFinite;
21
21
22
22
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
23
23
const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER;
···
135
135
};
136
136
137
137
const writeNumber = (state: State, val: number): void => {
138
-
if (_isNaN(val)) {
139
-
throw new RangeError(`NaN values not supported`);
138
+
if (!_isFinite(val)) {
139
+
throw new RangeError(`NaN and Infinity values not supported`);
140
140
}
141
141
142
142
if (val > MAX_SAFE_INTEGER || val < MIN_SAFE_INTEGER) {
+2
packages/utilities/cbor/lib/index.ts
+2
packages/utilities/cbor/lib/index.ts
+11
-38
packages/utilities/cid/lib/codec.test.ts
+11
-38
packages/utilities/cid/lib/codec.test.ts
···
1
1
import { describe, expect, it } from 'vitest';
2
2
3
-
import { create, createEmpty, decode, fromString, toString } from './codec.js';
3
+
import { create, decode, fromString, toString } from './codec.js';
4
4
5
5
describe('fromString', () => {
6
6
it('parses a CIDv1 string', () => {
···
23
23
});
24
24
});
25
25
26
-
it('parses an empty CIDv1 string', () => {
27
-
const cid = fromString('bafyreaa');
28
-
29
-
expect(cid).toEqual({
30
-
version: 1,
31
-
codec: 113,
32
-
digest: {
33
-
codec: 18,
34
-
contents: Uint8Array.from([]),
35
-
},
36
-
bytes: Uint8Array.from([1, 113, 18, 0]),
37
-
});
38
-
});
39
-
40
26
it('fails on non-v1 CID string', () => {
41
27
expect(() => fromString('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n')).toThrow(
42
-
`not a multibase base32 string`,
28
+
`not a valid cid string`,
43
29
);
30
+
});
31
+
32
+
it('fails on empty CID string', () => {
33
+
expect(() => fromString('bafyreaa')).toThrow(`not a valid cid string`);
44
34
});
45
35
});
46
36
···
70
60
});
71
61
});
72
62
73
-
it('decodes an empty CIDv1', () => {
63
+
it('fails on empty CIDv1', () => {
74
64
const buf = Uint8Array.from([1, 113, 18, 0]);
75
-
76
-
const cid = decode(buf);
77
-
78
-
expect(cid).toEqual({
79
-
version: 1,
80
-
codec: 113,
81
-
digest: {
82
-
codec: 18,
83
-
contents: Uint8Array.from([]),
84
-
},
85
-
bytes: Uint8Array.from([1, 113, 18, 0]),
86
-
});
65
+
expect(() => decode(buf)).toThrow(`cid too short`);
87
66
});
88
67
});
89
68
90
-
describe('create', () => [
69
+
describe('create', () => {
91
70
it('creates a CIDv1 string', async () => {
92
71
const contents = new TextEncoder().encode('abc');
93
72
const cid = await create(113, contents);
94
73
95
74
expect(toString(cid)).toBe('bafyreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu');
96
-
}),
97
-
98
-
it('creates an empty CIDv1 string', () => {
99
-
const cid = createEmpty(113);
100
-
101
-
expect(toString(cid)).toBe('bafyreaa');
102
-
}),
103
-
]);
75
+
});
76
+
});
+11
-50
packages/utilities/cid/lib/codec.ts
+11
-50
packages/utilities/cid/lib/codec.ts
···
15
15
export const CID_STRINGIFY_CACHE = new WeakMap<Cid, string>();
16
16
17
17
/**
18
-
* Represents a Content Identifier (CID), in particular, a limited subset of
18
+
* represents a Content Identifier (CID), in particular, a limited subset of
19
19
* CIDv1 as described by DASL specifications.
20
20
* https://dasl.ing/cid.html
21
21
*/
···
81
81
};
82
82
83
83
/**
84
-
* creates an empty CID with a zero-length digest
85
-
* @param codec multicodec type for the data
86
-
* @returns CID object with empty digest
87
-
*/
88
-
export const createEmpty = (codec: 0x55 | 0x71): Cid => {
89
-
const bytes = Uint8Array.from([CID_VERSION, codec, HASH_SHA256, 0]);
90
-
const digest = bytes.subarray(4);
91
-
92
-
const cid: Cid = {
93
-
version: CID_VERSION,
94
-
codec: codec,
95
-
digest: {
96
-
codec: HASH_SHA256,
97
-
contents: digest,
98
-
},
99
-
bytes: bytes,
100
-
};
101
-
102
-
return cid;
103
-
};
104
-
105
-
/**
106
84
* decodes a CID from bytes, returning the CID and any remaining bytes
107
85
* @param bytes raw CID bytes
108
86
* @returns tuple of decoded CID and remainder bytes
109
87
* @throws {RangeError} if the bytes are too short or contain invalid values
110
88
*/
111
89
export const decodeFirst = (bytes: Uint8Array): [decoded: Cid, remainder: Uint8Array] => {
112
-
const length = bytes.length;
113
-
114
-
if (length < 4) {
90
+
if (bytes.length < 36) {
115
91
throw new RangeError(`cid too short`);
116
92
}
117
93
···
132
108
throw new RangeError(`incorrect cid digest codec (got 0x${digestType.toString(16)})`);
133
109
}
134
110
135
-
if (digestSize !== 32 && digestSize !== 0) {
111
+
if (digestSize !== 32) {
136
112
throw new RangeError(`incorrect cid digest size (got ${digestSize})`);
137
113
}
138
114
139
-
if (length < 4 + digestSize) {
140
-
throw new RangeError(`cid too short`);
141
-
}
142
-
143
115
const cid: Cid = {
144
116
version: CID_VERSION,
145
117
codec: codec,
146
118
digest: {
147
119
codec: digestType,
148
-
contents: bytes.subarray(4, 4 + digestSize),
120
+
contents: bytes.subarray(4, 36),
149
121
},
150
-
bytes: bytes.subarray(0, 4 + digestSize),
122
+
bytes: bytes.subarray(0, 36),
151
123
};
152
124
153
-
return [cid, bytes.subarray(4 + digestSize)];
125
+
return [cid, bytes.subarray(36)];
154
126
};
155
127
156
128
/**
···
177
149
* @throws {RangeError} if the string length is invalid
178
150
*/
179
151
export const fromString = (input: string): Cid => {
180
-
if (input.length < 2 || input[0] !== 'b') {
181
-
throw new SyntaxError(`not a multibase base32 string`);
182
-
}
183
-
184
-
// 4 bytes in base32 = 7 characters + 1 character for the prefix
185
152
// 36 bytes in base32 = 58 characters + 1 character for the prefix
186
-
if (input.length !== 59 && input.length !== 8) {
187
-
throw new RangeError(`cid too short`);
153
+
if (input.length !== 59 || input[0] !== 'b') {
154
+
throw new SyntaxError(`not a valid cid string`);
188
155
}
189
156
190
157
const bytes = fromBase32(input.slice(1));
···
218
185
* @throws {SyntaxError} if the prefix byte is not 0x00
219
186
*/
220
187
export const fromBinary = (input: Uint8Array): Cid => {
221
-
// 4 bytes + 1 byte for the 0x00 prefix
222
188
// 36 bytes + 1 byte for the 0x00 prefix
223
-
if (input.length !== 37 && input.length !== 5) {
224
-
throw new RangeError(`cid bytes too short`);
225
-
}
226
-
227
-
if (input[0] !== 0) {
228
-
throw new SyntaxError(`incorrect binary cid`);
189
+
if (input.length !== 37 || input[0] !== 0) {
190
+
throw new SyntaxError(`invalid binary cid`);
229
191
}
230
192
231
-
const bytes = input.subarray(1);
232
-
return decode(bytes);
193
+
return decode(input.subarray(1));
233
194
};
234
195
235
196
/**