~ knotless Search/
↑↓ move↵ open⇧↵ all results

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

Merged chadtmiller.com asked to merge into main from chadtmiller.com/pds.js:core/cbor-lenient-reads
.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 });
});
});