Skip to content

Commit f1fc001

Browse files
committed
fix(writeUintSafe,writIntSafe): canonical encoding of too lartge nums
1 parent e3dec20 commit f1fc001

File tree

5 files changed

+60
-23
lines changed

5 files changed

+60
-23
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ This project adheres to [Semantic Versioning][semver].
66
The format of this changelog is [a variant][lib9-versionning] of [Keep a Changelog][keep-changelog].
77
New entries must be placed in a section entitled `Unreleased`.
88

9+
## Unreleased
10+
11+
- Fix `writeUintSafe` and `writeIntSafe` by encoding too large numbers in a canonical way.
12+
13+
Although the library provides assertions that reject valid inputs, it allows users to disable them.
14+
Even when assertions are disabled, the library must still provide valid encoded values.
15+
Previously, `writeUintSafe` and `writeIntSafe` might output a non-canonical encoding for numbers that were too large.
16+
It now robustly handles invalid input numbers that are too large.
17+
918
## 0.6.1 (2026-01-05)
1019

1120
- Fix `writeUintSafe32` that wrongly encoded numbers larger than 16383 (`2e14 - 1`)

src/codec/int.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,18 @@ test("writeIntSafe", () => {
277277
assert.throws(action, { name: "AssertionError" }, "too big")
278278
} else {
279279
action()
280-
// FIXME: should be `0` to ensure canonical encoding
281-
assert.deepEqual(toBytes(bc), [0x80, 0])
280+
assert.deepEqual(toBytes(bc), [0])
281+
}
282+
}
283+
284+
{
285+
const bc = fromBytes()
286+
const action = () => writeIntSafe(bc, Number.MAX_SAFE_INTEGER + 3)
287+
if (DEV) {
288+
assert.throws(action, { name: "AssertionError" }, "too big")
289+
} else {
290+
action()
291+
assert.deepEqual(toBytes(bc), [4])
282292
}
283293
}
284294

src/codec/int.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,22 @@ export function writeIntSafe(bc: ByteCursor, x: number): void {
6060
let zigZag = x < 0 ? -(x + 1) : x
6161
let first7Bits = ((zigZag & 0x3f) << 1) | sign
6262
zigZag = Math.floor(zigZag / /* 2**6 */ 0x40)
63-
if (zigZag > 0) {
64-
if (!Number.isSafeInteger(x)) {
65-
if (DEV) {
66-
assert(false, TOO_LARGE_NUMBER)
67-
}
68-
// keep only the remaining 53 - 6 = 47 bits
63+
if (!Number.isSafeInteger(x)) {
64+
if (DEV) {
65+
assert(false, TOO_LARGE_NUMBER)
66+
}
67+
// keep only the remaining 53 - 6 = 47 bits
68+
// this is useful when assertions are skipped
69+
const low = zigZag & 0x7fff
70+
const high = ((zigZag / 0x8000) >>> 0) * 0x8000
71+
if (first7Bits === 0x7f && low === 0x7fff && high === 0xffff_ffff) {
72+
// maps -2**53 to Number.MIN_SAFE_INTEGER
6973
// this is useful when assertions are skipped
70-
const low = zigZag & 0x7fff
71-
const high = ((zigZag / 0x8000) >>> 0) * 0x8000
72-
if (first7Bits === 0x7f && low === 0x7fff && high === 0xffff_ffff) {
73-
// maps -2**53 to Number.MIN_SAFE_INTEGER
74-
// this is useful when assertions are skipped
75-
first7Bits &= ~0b10
76-
}
77-
zigZag = high + low
74+
first7Bits &= ~0b10
7875
}
76+
zigZag = high + low
77+
}
78+
if (zigZag > 0) {
7979
writeU8(bc, 0x80 | first7Bits)
8080
writeUintSafe(bc, zigZag)
8181
} else {

src/codec/uint.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,22 @@ test("writeUintSafe", () => {
329329
)
330330
} else {
331331
action()
332-
assert.deepEqual(
333-
toBytes(bc),
334-
// FIXME: should be `0` to ensure canonical encoding
335-
[0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0],
332+
assert.deepEqual(toBytes(bc), [0])
333+
}
334+
}
335+
336+
{
337+
const bc = fromBytes()
338+
const action = () => writeUintSafe(bc, Number.MAX_SAFE_INTEGER + 3)
339+
if (DEV) {
340+
assert.throws(
341+
action,
342+
{ name: "AssertionError", message: "too large number" },
343+
"too large number",
336344
)
345+
} else {
346+
action()
347+
assert.deepEqual(toBytes(bc), [2])
337348
}
338349
}
339350
})

src/codec/uint.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,18 @@ export function readUintSafe(bc: ByteCursor): number {
139139
}
140140

141141
export function writeUintSafe(bc: ByteCursor, x: number): void {
142-
if (DEV) {
143-
assert(isU64Safe(x), TOO_LARGE_NUMBER)
142+
let zigZag = x
143+
if (!isU64Safe(x)) {
144+
if (DEV) {
145+
assert(false, TOO_LARGE_NUMBER)
146+
}
147+
// Truncate `zigZag` to 53 bits
148+
// this is useful when assertions are skipped
149+
const low = zigZag & 0x1fffff
150+
const high = ((zigZag / 0x200000) >>> 0) * 0x200000
151+
zigZag = high + low
144152
}
145153
let byteCount = 1
146-
let zigZag = x
147154
while (zigZag >= 0x80 && byteCount < INT_SAFE_MAX_BYTE_COUNT) {
148155
writeU8(bc, 0x80 | (zigZag & 0x7f))
149156
zigZag = Math.floor(zigZag / /* 2**7 */ 0x80)

0 commit comments

Comments
 (0)