Skip to content

Commit 312acc2

Browse files
committed
cleanup
1 parent 203450f commit 312acc2

File tree

1 file changed

+60
-49
lines changed

1 file changed

+60
-49
lines changed

src/common/utils/leb128.ts

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@
1010
* - No dependencies on Node.js Buffer or other runtime-specific APIs
1111
*/
1212

13-
const MAX_UINT32 = 0xffffffff; // 4294967295
14-
const CONTINUATION_BIT = 0x80; // 10000000 in binary
15-
const DATA_BITS_MASK = 0x7f; // 01111111 in binary
13+
const MAX_UINT32 = 0xffffffff;
14+
const CONTINUATION_BIT = 0x80;
15+
const DATA_BITS_MASK = 0x7f;
16+
const DATA_BITS_PER_BYTE = 7;
17+
const MAX_BYTES_FOR_UINT32 = 5; // ceil(32 / 7) = 5
1618

1719
/**
1820
* Encodes an unsigned 32-bit integer into LEB128 format.
1921
*
2022
* @param value - The unsigned 32-bit integer to encode (0 to 4,294,967,295)
2123
* @returns Uint8Array containing the encoded bytes (1-5 bytes)
22-
* @throws Error if value is negative, non-integer, NaN, or exceeds MAX_UINT32
24+
* @throws Error if value is invalid
2325
*
2426
* @example
2527
* encodeUInt32(0) // Uint8Array[0x00] (1 byte)
@@ -28,43 +30,25 @@ const DATA_BITS_MASK = 0x7f; // 01111111 in binary
2830
* encodeUInt32(300) // Uint8Array[0xAC, 0x02] (2 bytes)
2931
*/
3032
export function encodeUInt32(value: number): Uint8Array {
31-
// Validate input
32-
if (!Number.isFinite(value)) {
33-
throw new Error('Value must be a finite number');
34-
}
35-
if (!Number.isInteger(value)) {
36-
throw new Error('Value must be an integer');
37-
}
38-
if (value < 0) {
39-
throw new Error('Value must be non-negative');
40-
}
41-
if (value > MAX_UINT32) {
42-
throw new Error(`Value must not exceed ${MAX_UINT32} (MAX_UINT32)`);
43-
}
33+
validateUInt32(value);
4434

45-
// Fast path for zero
35+
// Handle zero directly (most common small value)
4636
if (value === 0) {
4737
return new Uint8Array([0]);
4838
}
4939

50-
// Calculate the maximum number of bytes needed (uint32 needs at most 5 bytes)
5140
const bytes: number[] = [];
5241

53-
// Encode the value
54-
while (value > 0) {
55-
// Extract the lowest 7 bits
42+
do {
5643
let byte = value & DATA_BITS_MASK;
44+
value >>>= DATA_BITS_PER_BYTE;
5745

58-
// Shift right by 7 bits for next iteration
59-
value >>>= 7;
60-
61-
// If there are more bytes to encode, set the continuation bit
62-
if (value > 0) {
46+
if (value !== 0) {
6347
byte |= CONTINUATION_BIT;
6448
}
6549

6650
bytes.push(byte);
67-
}
51+
} while (value !== 0);
6852

6953
return new Uint8Array(bytes);
7054
}
@@ -75,7 +59,7 @@ export function encodeUInt32(value: number): Uint8Array {
7559
* @param data - Uint8Array containing LEB128 encoded data
7660
* @param offset - Starting position in the buffer (defaults to 0)
7761
* @returns Object with decoded value and the index after the last byte read
78-
* @throws Error if offset is out of bounds or encoding is truncated
62+
* @throws Error if decoding fails
7963
*
8064
* @example
8165
* decodeUInt32(new Uint8Array([0x00])) // { value: 0, nextIndex: 1 }
@@ -87,38 +71,65 @@ export function decodeUInt32(
8771
data: Uint8Array,
8872
offset = 0,
8973
): { value: number; nextIndex: number } {
90-
// Validate offset
91-
if (offset < 0 || offset >= data.length) {
92-
throw new Error(`Offset ${offset} is out of bounds (buffer length: ${data.length})`);
93-
}
74+
validateOffset(data, offset);
9475

9576
let result = 0;
9677
let shift = 0;
9778
let index = offset;
79+
let bytesRead = 0;
9880

99-
// Decode bytes until we hit a byte without the continuation bit
10081
while (index < data.length) {
101-
const byte = data[index];
102-
103-
// Extract the lower 7 bits and shift into position
104-
result |= (byte & DATA_BITS_MASK) << shift;
82+
const byte = data[index++];
83+
bytesRead++;
10584

106-
index++;
107-
108-
// If continuation bit is not set, we're done
109-
if ((byte & CONTINUATION_BIT) === 0) {
110-
return { value: result >>> 0, nextIndex: index }; // >>> 0 ensures unsigned 32-bit
85+
// Check for overflow before processing
86+
if (bytesRead > MAX_BYTES_FOR_UINT32) {
87+
throw new Error('LEB128 sequence exceeds maximum length for uint32');
11188
}
11289

113-
// Move to next 7-bit chunk
114-
shift += 7;
90+
result |= (byte & DATA_BITS_MASK) << shift;
11591

116-
// Safety check: uint32 should never need more than 5 bytes (5 * 7 = 35 bits)
117-
if (shift > 28) {
118-
throw new Error('LEB128 sequence exceeds maximum length for uint32');
92+
if (!hasContinuationBit(byte)) {
93+
// Convert to unsigned 32-bit integer
94+
return { value: result >>> 0, nextIndex: index };
11995
}
96+
97+
shift += DATA_BITS_PER_BYTE;
12098
}
12199

122-
// If we exit the loop, the encoding was truncated
123100
throw new Error('Truncated LEB128 encoding');
124101
}
102+
103+
/**
104+
* Validates that a value is a valid unsigned 32-bit integer.
105+
*/
106+
function validateUInt32(value: number): void {
107+
if (!Number.isFinite(value)) {
108+
throw new Error('Value must be a finite number');
109+
}
110+
if (!Number.isInteger(value)) {
111+
throw new Error('Value must be an integer');
112+
}
113+
if (value < 0) {
114+
throw new Error('Value must be non-negative');
115+
}
116+
if (value > MAX_UINT32) {
117+
throw new Error(`Value must not exceed ${MAX_UINT32} (MAX_UINT32)`);
118+
}
119+
}
120+
121+
/**
122+
* Validates that an offset is within bounds.
123+
*/
124+
function validateOffset(data: Uint8Array, offset: number): void {
125+
if (offset < 0 || offset >= data.length) {
126+
throw new Error(`Offset ${offset} is out of bounds (buffer length: ${data.length})`);
127+
}
128+
}
129+
130+
/**
131+
* Checks if a byte has the continuation bit set.
132+
*/
133+
function hasContinuationBit(byte: number): boolean {
134+
return (byte & CONTINUATION_BIT) !== 0;
135+
}

0 commit comments

Comments
 (0)