a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm

refactor: DASL spec conformance

mary.my.id 07958065 f2e9aa5a

verified
Changed files
+58 -119
.changeset
packages
+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
··· 1 + // implements github:darobin/dasl.ing@cc66c35 (2025-10-20) 2 + 1 3 export * from './reader.js'; 2 4 export * from './streamed-reader.js'; 3 5
+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
··· 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
··· 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
··· 1 + // implements github:darobin/dasl.ing@cc66c35 (2025-10-20) 2 + 1 3 export { CidLinkWrapper, fromCidLink, isCidLink, toCidLink, type CidLink } from '@atcute/cid'; 2 4 3 5 export { BytesWrapper, fromBytes, isBytes, toBytes, type Bytes } from './bytes.js';
+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
··· 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 /**
+2
packages/utilities/cid/lib/index.ts
··· 1 + // implements github:darobin/dasl.ing@cc66c35 (2025-10-20) 2 + 1 3 export * from './cid-link.js'; 2 4 export * from './codec.js';