chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
Records stored cut short read again, as they did
.changeset/cbor-lenient-reads.mdnew+9−0
| @@ -0,0 +1,9 @@ | ||
--- | ||
'@pdsjs/core': patch | ||
--- | ||
| ||
The decoder reads as it did before 2.5.2-canary.aaff3ed, and reads the | ||
8-byte integers the encoders now write. Refusing bytes that end inside an | ||
item made a record stored cut short by the earlier encoder unreadable, and | ||
every listing of a collection holding one failed: a knotless account's | ||
`dev.knotless.ref`, which every git fetch lists. | ||
packages/core/src/repo.js+27−38
| @@ -263,46 +263,27 @@ export function cborDecodeLinks(bytes) { | ||
export function cborDecodeFirst(bytes, opts = {}) { | ||
let offset = 0; | ||
| ||
/** | ||
* The next `n` bytes, which must all be there. | ||
* @param {number} n | ||
*/ | ||
function take(n) { | ||
if (offset + n > bytes.length) { | ||
throw new RangeError('CBOR ends inside an item'); | ||
} | ||
const start = offset; | ||
offset += n; | ||
return bytes.subarray(start, offset); | ||
} | ||
| ||
/** @param {number} n */ | ||
function uint(n) { | ||
let value = 0; | ||
for (const byte of take(n)) value = value * 0x100 + byte; | ||
return value; | ||
} | ||
| ||
/** @returns {*} */ | ||
function read() { | ||
const initial = take(1)[0]; | ||
const initial = bytes[offset++]; | ||
const major = initial >> 5; | ||
const info = initial & 0x1f; | ||
| ||
if (major === 7 && info >= 25) { | ||
throw new TypeError( | ||
'CBOR holds a float or a reserved value, which atproto data does not', | ||
); | ||
} | ||
let length = info; | ||
if (info === 24) length = uint(1); | ||
else if (info === 25) length = uint(2); | ||
else if (info === 26) length = uint(4); | ||
else if (info === 27) length = uint(8); | ||
else if (info > 27) | ||
throw new TypeError('CBOR item of indefinite or reserved length'); | ||
if (!Number.isSafeInteger(length)) { | ||
throw new RangeError('CBOR integer past Number.MAX_SAFE_INTEGER'); | ||
if (info === 24) length = bytes[offset++]; | ||
else if (info === 25) { | ||
length = (bytes[offset++] << 8) | bytes[offset++]; | ||
} else if (info === 26) { | ||
// Use multiplication instead of bitshift to avoid 32-bit signed integer overflow | ||
length = | ||
bytes[offset++] * 0x1000000 + | ||
bytes[offset++] * 0x10000 + | ||
bytes[offset++] * 0x100 + | ||
bytes[offset++]; | ||
} else if (info === 27) { | ||
// The encoders write an integer of 2^32 or more with an 8-byte head. | ||
length = 0; | ||
for (let i = 0; i < 8; i++) length = length * 0x100 + bytes[offset++]; | ||
} | ||
| ||
switch (major) { | ||
| @@ -310,12 +291,20 @@ export function cborDecodeFirst(bytes, opts = {}) { | ||
return length; // unsigned int | ||
case 1: | ||
return -1 - length; // negative int | ||
case 2: | ||
case 2: { | ||
// byte string | ||
return take(length).slice(); | ||
case 3: | ||
const data = bytes.slice(offset, offset + length); | ||
offset += length; | ||
return data; | ||
} | ||
case 3: { | ||
// text string | ||
return new TextDecoder().decode(take(length)); | ||
const data = new TextDecoder().decode( | ||
bytes.slice(offset, offset + length), | ||
); | ||
offset += length; | ||
return data; | ||
} | ||
case 4: { | ||
// array | ||
const arr = []; | ||
packages/core/test/repo.test.js+7−19
| @@ -550,24 +550,12 @@ describe('CBOR integers past 32 bits', () => { | ||
} | ||
}); | ||
| ||
test('bytes cut short are refused, not read as zeros', () => { | ||
const whole = cborEncodeDagCbor(ref); | ||
expect(() => cborDecode(whole.subarray(0, whole.length - 1))).toThrow( | ||
'CBOR ends inside an item', | ||
); | ||
expect(() => cborDecode(new Uint8Array([0x62, 0x61]))).toThrow( | ||
'CBOR ends inside an item', | ||
); | ||
}); | ||
| ||
test('floats and reserved heads are refused', () => { | ||
expect(() => cborDecode(new Uint8Array([0xf9, 0x3c, 0x00]))).toThrow( | ||
/float/, | ||
); | ||
expect(() => cborDecode(new Uint8Array([0x9f]))).toThrow( | ||
/indefinite or reserved/, | ||
); | ||
const past = new Uint8Array([0x1b, 0x00, 0x20, 0, 0, 0, 0, 0, 0]); | ||
expect(() => cborDecode(past)).toThrow(/MAX_SAFE_INTEGER/); | ||
test('a record stored cut short by an earlier encoder still reads', () => { | ||
// Before 8-byte heads, the encoders wrote nothing for such an integer. | ||
// A read gives 0 for the missing value. Refusing the read made every | ||
// listing of a collection holding one fail. | ||
const whole = cborEncodeDagCbor({ ...ref, generation: 0 }); | ||
const stored = whole.subarray(0, whole.length - 1); | ||
expect(cborDecode(stored)).toEqual({ ...ref, generation: 0 }); | ||
}); | ||
}); | ||