Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/bytearray-toi32-overflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@graphprotocol/graph-ts': patch
---

`ByteArray.toI32` and `ByteArray.toI64` now report an overflow for a positive value
that needs the sign bit of the widest byte, instead of returning it as a negative
number. The overflow check only compared the bytes above the target width against the
sign padding, so `0x80000000` carried in five bytes - exactly what
`BigInt.fromUnsignedBytes` produces for a `u32` at or above `2^31` - passed the check
and came back as `-2147483648`.
13 changes: 13 additions & 0 deletions packages/ts/common/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ export class ByteArray extends Uint8Array {
assert(false, 'overflow converting ' + this.toHexString() + ' to i32');
}
}
// Matching sign padding above the window is not enough on its own: the sign bit of the
// last byte inside the window has to agree with it too. Without this check a positive
// value one bit too wide - 0x80000000 carried in five bytes, exactly what
// BigInt.fromUnsignedBytes produces for a u32 >= 2^31 - passes the loop above and is
// returned as a negative i32 instead of throwing.
if (this.length > 4 && this[3] >> 7 != (isNeg ? 1 : 0)) {
assert(false, 'overflow converting ' + this.toHexString() + ' to i32');
}
const paddedBytes = new Bytes(4);
paddedBytes[0] = padding;
paddedBytes[1] = padding;
Expand Down Expand Up @@ -195,6 +203,11 @@ export class ByteArray extends Uint8Array {
assert(false, 'overflow converting ' + this.toHexString() + ' to i64');
}
}
// Same as in toI32: the sign bit of the last byte inside the window has to agree with
// the padding, otherwise a positive value that needs the 64th bit is returned negative.
if (this.length > 8 && this[7] >> 7 != (isNeg ? 1 : 0)) {
assert(false, 'overflow converting ' + this.toHexString() + ' to i64');
}
const paddedBytes = new Bytes(8);
paddedBytes[0] = padding;
paddedBytes[1] = padding;
Expand Down
4 changes: 3 additions & 1 deletion packages/ts/test/bigInt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ export function testBigInt(): void {
longArray[3] = 255;
longArray[4] = 0;
assert(longArray.toU32() == 4_294_705_147);
assert(longArray.toI32() == 4_294_705_147);
// 4_294_705_147 is past i32.MAX_VALUE, so toI32 reports an overflow rather than
// returning the wrapped -262_149. The assert cannot be written here: an overflow
// aborts, and this harness has no way to expect one.

const bytes = Bytes.fromHexString('0x56696b746f726961');
assert((bytes[0] = 0x56));
Expand Down
29 changes: 28 additions & 1 deletion packages/ts/test/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,34 @@ export function testBytesWithByteArray(): void {
longArray[3] = 255;
longArray[4] = 0;
assert(longArray.toU32() == 4_294_705_147);
assert(longArray.toI32() == 4_294_705_147);
// 4_294_705_147 is past i32.MAX_VALUE, so toI32 reports an overflow rather than
// returning the wrapped -262_149. The assert cannot be written here: an overflow
// aborts, and this harness has no way to expect one.

// Widening a value with sign padding must keep converting - these all still fit.
const i32Max = new ByteArray(5);
i32Max[0] = 255;
i32Max[1] = 255;
i32Max[2] = 255;
i32Max[3] = 127;
i32Max[4] = 0;
assert(i32Max.toI32() == 2_147_483_647);

const i32Min = new ByteArray(5);
i32Min[0] = 0;
i32Min[1] = 0;
i32Min[2] = 0;
i32Min[3] = 128;
i32Min[4] = 255;
assert(i32Min.toI32() == -2_147_483_648);

// The same value in exactly four bytes is a plain i32 and is unaffected.
const i32MinNarrow = new ByteArray(4);
i32MinNarrow[0] = 0;
i32MinNarrow[1] = 0;
i32MinNarrow[2] = 0;
i32MinNarrow[3] = 128;
assert(i32MinNarrow.toI32() == -2_147_483_648);

const bytes = Bytes.fromHexString('0x56696b746f726961');
assert((bytes[0] = 0x56));
Expand Down