Skip to content

Commit afa043d

Browse files
committed
Minor tweaks
1 parent ba413ea commit afa043d

File tree

4 files changed

+127
-8
lines changed

4 files changed

+127
-8
lines changed

index.d.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,25 @@ try {
4848
*/
4949
export function assertUint8Array(value: unknown): asserts value is Uint8Array;
5050

51+
/**
52+
Throw a `TypeError` if the given value is not an instance of `Uint8Array` or `ArrayBuffer`.
53+
54+
Useful as a guard for functions that accept either a `Uint8Array` or `ArrayBuffer`.
55+
56+
@example
57+
```
58+
import {assertUint8ArrayOrArrayBuffer} from 'uint8array-extras';
59+
60+
assertUint8ArrayOrArrayBuffer(new Uint8Array());
61+
assertUint8ArrayOrArrayBuffer(new ArrayBuffer(8));
62+
```
63+
*/
64+
export function assertUint8ArrayOrArrayBuffer(value: unknown): asserts value is Uint8Array | ArrayBuffer;
65+
5166
/**
5267
Convert a value to a `Uint8Array` without copying its data.
5368
54-
This can be useful for converting a `Buffer` to a pure `Uint8Array`. `Buffer` is already an `Uint8Array` subclass, but [`Buffer` alters some behavior](https://sindresorhus.com/blog/goodbye-nodejs-buffer), so it can be useful to cast it to a pure `Uint8Array` before returning it.
69+
This can be useful for converting a `Buffer` to a pure `Uint8Array`. `Buffer` is already a `Uint8Array` subclass, but [`Buffer` alters some behavior](https://sindresorhus.com/blog/goodbye-nodejs-buffer), so it can be useful to cast it to a pure `Uint8Array` before returning it.
5570
5671
Tip: If you want a copy, just call `.slice()` on the return value.
5772
*/
@@ -183,6 +198,8 @@ export function uint8ArrayToBase64(array: Uint8Array, options?: {urlSafe: boolea
183198
/**
184199
Convert a Base64-encoded or [Base64URL](https://base64.guru/standards/base64url)-encoded string to a `Uint8Array`.
185200
201+
Accepts Base64URL with or without padding.
202+
186203
Replacement for [`Buffer.from('SGVsbG8=', 'base64')`](https://nodejs.org/api/buffer.html#static-method-bufferfromstring-encoding).
187204
188205
@example
@@ -196,7 +213,7 @@ console.log(base64ToUint8Array('SGVsbG8='));
196213
export function base64ToUint8Array(string: string): Uint8Array<ArrayBuffer>;
197214

198215
/**
199-
Encode a string to Base64-encoded string.
216+
Encode a string to a Base64-encoded string.
200217
201218
Specify `{urlSafe: true}` to get a [Base64URL](https://base64.guru/standards/base64url)-encoded string.
202219
@@ -215,6 +232,8 @@ export function stringToBase64(string: string, options?: {urlSafe: boolean}): st
215232
/**
216233
Decode a Base64-encoded or [Base64URL](https://base64.guru/standards/base64url)-encoded string to a string.
217234
235+
Accepts Base64URL with or without padding.
236+
218237
Replacement for `Buffer.from('SGVsbG8=', 'base64').toString()` and [`atob()`](https://developer.mozilla.org/en-US/docs/Web/API/atob).
219238
220239
@example
@@ -262,7 +281,7 @@ export function hexToUint8Array(hexString: string): Uint8Array<ArrayBuffer>;
262281
/**
263282
Read `DataView#byteLength` number of bytes from the given view, up to 48-bit.
264283
265-
Replacement for [`Buffer#readUintBE`](https://nodejs.org/api/buffer.html#bufreadintbeoffset-bytelength)
284+
Replacement for [`Buffer#readUIntBE`](https://nodejs.org/api/buffer.html#bufreaduintbeoffset-bytelength)
266285
267286
@example
268287
```

index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,13 @@ function base64ToBase64Url(base64) {
137137
}
138138

139139
function base64UrlToBase64(base64url) {
140-
return base64url.replaceAll('-', '+').replaceAll('_', '/');
140+
const base64 = base64url.replaceAll('-', '+').replaceAll('_', '/');
141+
const padding = (4 - (base64.length % 4)) % 4;
142+
return base64 + '='.repeat(padding);
141143
}
142144

143145
// Reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/
146+
// Important: Keep this value divisible by 3 so intermediate chunks produce no Base64 padding.
144147
const MAX_BLOCK_SIZE = 65_535;
145148

146149
export function uint8ArrayToBase64(array, {urlSafe = false} = {}) {
@@ -151,7 +154,7 @@ export function uint8ArrayToBase64(array, {urlSafe = false} = {}) {
151154
for (let index = 0; index < array.length; index += MAX_BLOCK_SIZE) {
152155
const chunk = array.subarray(index, index + MAX_BLOCK_SIZE);
153156
// Required as `btoa` and `atob` don't properly support Unicode: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
154-
base64 += globalThis.btoa(String.fromCodePoint.apply(this, chunk));
157+
base64 += globalThis.btoa(String.fromCodePoint.apply(undefined, chunk));
155158
}
156159

157160
return urlSafe ? base64ToBase64Url(base64) : base64;

readme.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,24 @@ try {
6565
}
6666
```
6767

68+
### `assertUint8ArrayOrArrayBuffer(value: unknown)`
69+
70+
Throw a `TypeError` if the given value is not an instance of `Uint8Array` or `ArrayBuffer`.
71+
72+
Useful as a guard for functions that accept either a `Uint8Array` or `ArrayBuffer`.
73+
74+
```js
75+
import {assertUint8ArrayOrArrayBuffer} from 'uint8array-extras';
76+
77+
assertUint8ArrayOrArrayBuffer(new Uint8Array());
78+
assertUint8ArrayOrArrayBuffer(new ArrayBuffer(8));
79+
```
80+
6881
### `toUint8Array(value: TypedArray | ArrayBuffer | DataView): Uint8Array`
6982

7083
Convert a value to a `Uint8Array` without copying its data.
7184

72-
This can be useful for converting a `Buffer` to a pure `Uint8Array`. `Buffer` is already an `Uint8Array` subclass, but [`Buffer` alters some behavior](https://sindresorhus.com/blog/goodbye-nodejs-buffer), so it can be useful to cast it to a pure `Uint8Array` before returning it.
85+
This can be useful for converting a `Buffer` to a pure `Uint8Array`. `Buffer` is already a `Uint8Array` subclass, but [`Buffer` alters some behavior](https://sindresorhus.com/blog/goodbye-nodejs-buffer), so it can be useful to cast it to a pure `Uint8Array` before returning it.
7386

7487
Tip: If you want a copy, just call `.slice()` on the return value.
7588

@@ -188,6 +201,8 @@ console.log(uint8ArrayToBase64(byteArray));
188201

189202
Convert a Base64-encoded or [Base64URL](https://base64.guru/standards/base64url)-encoded string to a `Uint8Array`.
190203

204+
Accepts Base64URL with or without padding.
205+
191206
Replacement for [`Buffer.from('SGVsbG8=', 'base64')`](https://nodejs.org/api/buffer.html#static-method-bufferfromstring-encoding).
192207

193208
```js
@@ -199,7 +214,7 @@ console.log(base64ToUint8Array('SGVsbG8='));
199214

200215
### `stringToBase64(string: string, options?: {urlSafe: boolean}): string`
201216

202-
Encode a string to Base64-encoded string.
217+
Encode a string to a Base64-encoded string.
203218

204219
Specify `{urlSafe: true}` to get a [Base64URL](https://base64.guru/standards/base64url)-encoded string.
205220

@@ -216,6 +231,8 @@ console.log(stringToBase64('Hello'));
216231

217232
Decode a Base64-encoded or [Base64URL](https://base64.guru/standards/base64url)-encoded string to a string.
218233

234+
Accepts Base64URL with or without padding.
235+
219236
Replacement for `Buffer.from('SGVsbG8=', 'base64').toString()` and [`atob()`](https://developer.mozilla.org/en-US/docs/Web/API/atob).
220237

221238
```js
@@ -257,7 +274,7 @@ console.log(hexToUint8Array('48656c6c6f'));
257274

258275
Read `DataView#byteLength` number of bytes from the given view, up to 48-bit.
259276

260-
Replacement for [`Buffer#readUintBE`](https://nodejs.org/api/buffer.html#bufreadintbeoffset-bytelength)
277+
Replacement for [`Buffer#readUIntBE`](https://nodejs.org/api/buffer.html#bufreaduintbeoffset-bytelength)
261278

262279
```js
263280
import {getUintBE} from 'uint8array-extras';

test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import test from 'ava';
22
import {
33
isUint8Array,
44
assertUint8Array,
5+
assertUint8ArrayOrArrayBuffer,
56
toUint8Array,
67
concatUint8Arrays,
78
areUint8ArraysEqual,
@@ -49,6 +50,12 @@ test('toUint8Array - DataView', t => {
4950
t.true(isUint8ArrayStrict(toUint8Array(fixture)));
5051
});
5152

53+
test('toUint8Array - unsupported value throws', t => {
54+
t.throws(() => {
55+
toUint8Array(123);
56+
}, {instanceOf: TypeError});
57+
});
58+
5259
test('concatUint8Arrays - combining multiple Uint8Arrays', t => {
5360
const array1 = new Uint8Array([1, 2, 3]);
5461
const array2 = new Uint8Array([4, 5, 6]);
@@ -141,6 +148,21 @@ test('uint8ArrayToString with ArrayBuffer', t => {
141148
t.is(uint8ArrayToString(fixture), 'Hello');
142149
});
143150

151+
test('assertUint8ArrayOrArrayBuffer', t => {
152+
const u8 = new Uint8Array(0);
153+
const ab = new ArrayBuffer(0);
154+
// Should not throw
155+
assertUint8ArrayOrArrayBuffer(u8);
156+
assertUint8ArrayOrArrayBuffer(ab);
157+
// Should throw on invalid types
158+
t.throws(() => {
159+
assertUint8ArrayOrArrayBuffer(null);
160+
}, {instanceOf: TypeError});
161+
t.throws(() => {
162+
assertUint8ArrayOrArrayBuffer({});
163+
}, {instanceOf: TypeError});
164+
});
165+
144166
test('uint8ArrayToBase64 and base64ToUint8Array', t => {
145167
const fixture = stringToUint8Array('Hello');
146168
const base64 = uint8ArrayToBase64(fixture);
@@ -181,6 +203,18 @@ test('hexToUint8Array', t => {
181203
t.deepEqual(hexToUint8Array(fixtureHex), new Uint8Array(Buffer.from(fixtureHex, 'hex'))); // eslint-disable-line n/prefer-global/buffer
182204
});
183205

206+
test('hexToUint8Array - invalid length throws', t => {
207+
t.throws(() => {
208+
hexToUint8Array('A');
209+
}, {message: 'Invalid Hex string length.'});
210+
});
211+
212+
test('hexToUint8Array - invalid character throws', t => {
213+
t.throws(() => {
214+
hexToUint8Array('0G');
215+
}, {message: /Invalid Hex character encountered at position 0|position 1/});
216+
});
217+
184218
test('getUintBE', t => {
185219
const fixture = [0x12, 0x34, 0x56, 0x78, 0x90, 0xab]; // eslint-disable-line unicorn/number-literal-case
186220

@@ -240,3 +274,49 @@ test('includes', t => {
240274
t.true(includes(new Uint8Array(fixture), new Uint8Array([0x78, 0x90])));
241275
t.false(includes(new Uint8Array(fixture), new Uint8Array([0x90, 0x78])));
242276
});
277+
278+
test('uint8ArrayToBase64 - empty input returns empty string', t => {
279+
const empty = new Uint8Array(0);
280+
t.is(uint8ArrayToBase64(empty), '');
281+
// And decode back
282+
t.deepEqual(base64ToUint8Array(''), empty);
283+
});
284+
285+
test('uint8ArrayToBase64 - chunk boundaries and padding are correct', t => {
286+
const make = length => new Uint8Array(Array.from({length}, () => 0x41)); // 0x41 = 'A'
287+
const sizes = [
288+
65_534, // Chunk size - 1 (length % 3 === 2)
289+
65_535, // Exact chunk size (length % 3 === 0)
290+
65_536, // Chunk size + 1 (length % 3 === 1)
291+
(2 * 65_535) - 2,
292+
(2 * 65_535) - 1,
293+
(2 * 65_535),
294+
(2 * 65_535) + 1,
295+
];
296+
297+
for (const size of sizes) {
298+
const u8 = make(size);
299+
const b64 = uint8ArrayToBase64(u8);
300+
const roundTrip = base64ToUint8Array(b64);
301+
t.deepEqual(roundTrip, u8);
302+
}
303+
});
304+
305+
test('base64ToUint8Array - accepts Base64URL without padding', t => {
306+
const cases = [
307+
['f', 'Zg'],
308+
['fo', 'Zm8'],
309+
['foo', 'Zm9v'],
310+
['foob', 'Zm9vYg'],
311+
['fooba', 'Zm9vYmE'],
312+
['foobar', 'Zm9vYmFy'],
313+
];
314+
315+
for (const [plain, base64url] of cases) {
316+
const decoded = base64ToString(base64url);
317+
t.is(decoded, plain);
318+
// And Uint8Array path
319+
const u8 = base64ToUint8Array(base64url);
320+
t.is(uint8ArrayToString(u8), plain);
321+
}
322+
});

0 commit comments

Comments
 (0)