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 */
3032export 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