Skip to content

Commit d6b3e0c

Browse files
committed
feat: binary encoder
1 parent b85503f commit d6b3e0c

File tree

6 files changed

+204
-148
lines changed

6 files changed

+204
-148
lines changed

README.md

+85-55
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ utilities provided by `@better-auth/utils`:
2121
| [**Hex**](#hex) | Encode and decode data in hexadecimal format. |
2222
| [**OTP**](#otp) | Generate and verify one-time passwords. |
2323

24+
2425
## Hash
2526

2627
Digest provides a way to hash an input using sha family hash functions. It wraps over `crypto.digest` and provide utilities to encode output in hex or base 64.
@@ -228,61 +229,6 @@ const isValid = await ecdsa.verify(publicKey, {
228229
});
229230
```
230231

231-
## Base64
232-
233-
Base64 utilities provide a simple interface to encode and decode data in base64 format.
234-
235-
### Encoding
236-
237-
Encode data in base64 format. Input can be a string, `ArrayBuffer`, or `TypedArray`.
238-
239-
```ts
240-
import { base64 } from "@better-auth/utils/base64";
241-
242-
const encodedData = base64.encode("Data to encode");
243-
```
244-
245-
options:
246-
- `urlSafe` - URL-safe encoding, replacing `+` with `-` and `/` with `_`.
247-
- `padding` - Include padding characters (`=`) at the end of the encoded string
248-
249-
```ts
250-
const encodedData = base64.encode("Data to encode", { url: true, padding: false });
251-
```
252-
253-
### Decoding
254-
255-
Decode base64-encoded data. Input can be a string or `ArrayBuffer`.
256-
257-
```ts
258-
const decodedData = await base64.decode(encodedData);
259-
```
260-
261-
It automatically detects if the input is URL-safe and includes padding characters.
262-
263-
264-
## Hex
265-
266-
Hex utilities provide a simple interface to encode and decode data in hexadecimal format.
267-
268-
### Encoding
269-
270-
Encode data in hexadecimal format. Input can be a string, `ArrayBuffer`, or `TypedArray`.
271-
272-
```ts
273-
import { hex } from "@better-auth/utils/hex";
274-
275-
const encodedData = hex.encode("Data to encode");
276-
```
277-
278-
### Decoding
279-
280-
Decode hexadecimal-encoded data. Input can be a string or `ArrayBuffer`.
281-
282-
```ts
283-
const decodedData = hex.decode(encodedData);
284-
```
285-
286232
## OTP
287233

288234
The OTP utility provides a simple and secure way to generate and verify one-time passwords (OTPs), commonly used in multi-factor authentication (MFA) systems. It includes support for both HOTP (HMAC-based One-Time Password) and TOTP (Time-based One-Time Password) standards.
@@ -349,6 +295,90 @@ const secret = "my-super-secret-key";
349295
const qrCodeUrl = createOTP(secret).url("my-app", "[email protected]");
350296
```
351297

298+
299+
## Base64
300+
301+
Base64 utilities provide a simple interface to encode and decode data in base64 format.
302+
303+
### Encoding
304+
305+
Encode data in base64 format. Input can be a string, `ArrayBuffer`, or `TypedArray`.
306+
307+
```ts
308+
import { base64 } from "@better-auth/utils/base64";
309+
310+
const encodedData = base64.encode("Data to encode");
311+
```
312+
313+
options:
314+
- `padding` - Include padding characters (`=`) at the end of the encoded string
315+
316+
```ts
317+
const encodedData = base64.encode("Data to encode", { url: true, padding: false });
318+
```
319+
320+
### Decoding
321+
322+
Decode base64-encoded data. Input can be a string or `ArrayBuffer`.
323+
324+
```ts
325+
const decodedData = await base64.decode(encodedData);
326+
```
327+
328+
It automatically detects if the input is URL-safe and includes padding characters.
329+
330+
### Base64Url
331+
332+
Url safe alternative
333+
334+
```ts
335+
import { base64Url } from "@better-auth/utils/base64";
336+
337+
const encodedData = base64Url.encode("Data to encode");
338+
```
339+
340+
## Hex
341+
342+
Hex utilities provide a simple interface to encode and decode data in hexadecimal format.
343+
344+
### Encoding
345+
346+
Encode data in hexadecimal format. Input can be a string, `ArrayBuffer`, or `TypedArray`.
347+
348+
```ts
349+
import { hex } from "@better-auth/utils/hex";
350+
351+
const encodedData = hex.encode("Data to encode");
352+
```
353+
354+
### Decoding
355+
356+
Decode hexadecimal-encoded data. Input can be a string or `ArrayBuffer`.
357+
358+
```ts
359+
const decodedData = hex.decode(encodedData);
360+
```
361+
362+
## Binary
363+
364+
A utilities provide a simple interface to encode and decode data in binary format. It uses `TextEncode` and `TextDecoder` to encode and decode data respectively.
365+
366+
### Encoding
367+
368+
```ts
369+
import { binary } from "@better-auth/util/binary"
370+
371+
const data = binary.encode("Hello World!")
372+
```
373+
374+
### Decoding
375+
376+
```ts
377+
import { binary } from "@better-auth/util/binary"
378+
379+
const data = binary.decode(new Unit8Array([[72, 101, 108, 108, 111]]))
380+
```
381+
352382
## License
353383

354384
MIT

src/base64.test.ts

+13-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect } from "vitest";
2-
import { base64 } from "./base64";
2+
import { base64, base64Url } from "./base64";
3+
import { binary } from "./binary";
34

45
describe("base64", () => {
56
const plainText = "Hello, World!";
@@ -9,45 +10,38 @@ describe("base64", () => {
910

1011
describe("encode", () => {
1112
it("encodes a string to base64 with padding", async () => {
12-
const result = await base64.encode(plainText, { padding: true });
13+
const result = base64.encode(plainText, { padding: true });
1314
expect(result).toBe(base64Encoded);
1415
});
1516

1617
it("encodes a string to base64 without padding", async () => {
17-
const result = await base64.encode(plainText, { padding: false });
18+
const result = base64.encode(plainText, { padding: false });
1819
expect(result).toBe(base64Encoded.replace(/=+$/, ""));
1920
});
2021

2122
it("encodes a string to base64 URL-safe", async () => {
22-
const result = await base64.encode(plainText, {
23-
urlSafe: true,
23+
const result = base64Url.encode(plainText, {
2424
padding: false,
2525
});
2626
expect(result).toBe(base64UrlEncoded);
2727
});
2828

2929
it("encodes an ArrayBuffer to base64", async () => {
30-
const result = await base64.encode(plainBuffer, { padding: true });
30+
const result = base64.encode(plainBuffer, { padding: true });
3131
expect(result).toBe(base64Encoded);
3232
});
3333
});
3434

3535
describe("decode", () => {
36-
it("decodes a base64 string to a Uint8Array", async () => {
37-
const result = await base64.decode(base64Encoded);
38-
expect(result).toBe(plainText);
36+
it("decodes a base64 string", async () => {
37+
const encoded = Buffer.from(plainText).toString("base64");
38+
const result = base64.decode(encoded);
39+
expect(binary.decode(result)).toBe(plainText);
3940
});
4041

41-
it("decodes a base64 URL-safe string to a Uint8Array", async () => {
42-
const result = await base64.decode(base64UrlEncoded);
43-
expect(result).toBe(plainText);
44-
});
45-
46-
it("throws an error for invalid characters", async () => {
47-
const invalidBase64 = "SGVsbG8s#";
48-
await expect(base64.decode(invalidBase64)).rejects.toThrow(
49-
"Invalid character",
50-
);
42+
it("decodes a base64 URL-safe string", async () => {
43+
const result = base64.decode(base64UrlEncoded);
44+
expect(binary.decode(result)).toBe(plainText);
5145
});
5246
});
5347
});

src/base64.ts

+56-55
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@
22

33
import type { TypedArray } from "./type";
44

5-
function createDecodeMap(alphabet: string): Map<string, number> {
6-
const decodeMap = new Map<string, number>();
7-
for (let i = 0; i < alphabet.length; i++) {
8-
decodeMap.set(alphabet[i]!, i);
9-
}
10-
return decodeMap;
11-
}
12-
135
function getAlphabet(urlSafe: boolean): string {
146
return urlSafe
157
? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
@@ -25,82 +17,91 @@ function base64Encode(
2517
let buffer = 0;
2618
let shift = 0;
2719

28-
for (let i = 0; i < data.length; i++) {
29-
buffer = (buffer << 8) | data[i]!;
20+
for (const byte of data) {
21+
buffer = (buffer << 8) | byte;
3022
shift += 8;
3123
while (shift >= 6) {
3224
shift -= 6;
3325
result += alphabet[(buffer >> shift) & 0x3f];
3426
}
3527
}
28+
3629
if (shift > 0) {
3730
result += alphabet[(buffer << (6 - shift)) & 0x3f];
3831
}
32+
3933
if (padding) {
4034
const padCount = (4 - (result.length % 4)) % 4;
4135
result += "=".repeat(padCount);
4236
}
37+
4338
return result;
4439
}
4540

4641
function base64Decode(data: string, alphabet: string): Uint8Array {
47-
const decodeMap = createDecodeMap(alphabet);
42+
const decodeMap = new Map<string, number>();
43+
for (let i = 0; i < alphabet.length; i++) {
44+
decodeMap.set(alphabet[i]!, i);
45+
}
4846
const result: number[] = [];
49-
const chunkCount = Math.ceil(data.length / 4);
47+
let buffer = 0;
48+
let bitsCollected = 0;
5049

51-
for (let i = 0; i < chunkCount; i++) {
52-
let padCount = 0;
53-
let buffer = 0;
54-
for (let j = 0; j < 4; j++) {
55-
const encoded = data[i * 4 + j];
56-
if (encoded === "=") {
57-
padCount += 1;
58-
continue;
59-
}
60-
if (encoded === undefined) {
61-
padCount += 1;
62-
continue;
63-
}
64-
const value = decodeMap.get(encoded) ?? null;
65-
if (value === null) {
66-
throw new Error(`Invalid character: ${encoded}`);
67-
}
68-
buffer += value << (6 * (3 - j));
69-
}
70-
result.push((buffer >> 16) & 0xff);
71-
if (padCount < 2) {
72-
result.push((buffer >> 8) & 0xff);
50+
for (const char of data) {
51+
if (char === "=") break;
52+
const value = decodeMap.get(char);
53+
if (value === undefined) {
54+
throw new Error(`Invalid Base64 character: ${char}`);
7355
}
74-
if (padCount < 1) {
75-
result.push(buffer & 0xff);
56+
buffer = (buffer << 6) | value;
57+
bitsCollected += 6;
58+
59+
if (bitsCollected >= 8) {
60+
bitsCollected -= 8;
61+
result.push((buffer >> bitsCollected) & 0xff);
7662
}
7763
}
64+
7865
return Uint8Array.from(result);
7966
}
8067

8168
export const base64 = {
82-
async encode(
69+
encode(
8370
data: ArrayBuffer | TypedArray | string,
84-
options: {
85-
urlSafe?: boolean;
86-
padding?: boolean;
87-
} = {},
71+
options: { padding?: boolean } = {},
8872
) {
89-
const alphabet = getAlphabet(options.urlSafe ?? false);
90-
if (typeof data === "string") {
91-
const encoder = new TextEncoder();
92-
data = encoder.encode(data);
73+
const alphabet = getAlphabet(false);
74+
const buffer =
75+
typeof data === "string"
76+
? new TextEncoder().encode(data)
77+
: new Uint8Array(data);
78+
return base64Encode(buffer, alphabet, options.padding ?? true);
79+
},
80+
decode(data: string | ArrayBuffer | TypedArray) {
81+
if (typeof data !== "string") {
82+
data = new TextDecoder().decode(data);
9383
}
94-
return base64Encode(
95-
new Uint8Array(data),
96-
alphabet,
97-
options.padding ?? true,
98-
);
84+
const urlSafe = data.includes("-") || data.includes("_");
85+
const alphabet = getAlphabet(urlSafe);
86+
return base64Decode(data, alphabet);
87+
},
88+
};
89+
90+
export const base64Url = {
91+
encode(
92+
data: ArrayBuffer | TypedArray | string,
93+
options: { padding?: boolean } = {},
94+
) {
95+
const alphabet = getAlphabet(true);
96+
const buffer =
97+
typeof data === "string"
98+
? new TextEncoder().encode(data)
99+
: new Uint8Array(data);
100+
return base64Encode(buffer, alphabet, options.padding ?? true);
99101
},
100-
async decode(data: string) {
101-
const isUrlSafe = data.includes("-") || data.includes("_");
102-
const alphabet = getAlphabet(isUrlSafe);
103-
const decoded = base64Decode(data, alphabet);
104-
return new TextDecoder().decode(decoded);
102+
decode(data: string) {
103+
const urlSafe = data.includes("-") || data.includes("_");
104+
const alphabet = getAlphabet(urlSafe);
105+
return base64Decode(data, alphabet);
105106
},
106107
};

src/binary.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type Encoding = "utf-8" | "utf-16" | "iso-8859-1";
2+
3+
type BinaryData = ArrayBuffer | ArrayBufferView;
4+
5+
const decoders = new Map<Encoding, TextDecoder>();
6+
const encoder = new TextEncoder();
7+
8+
export const binary = {
9+
decode: (data: BinaryData, encoding: Encoding = "utf-8") => {
10+
if (!decoders.has(encoding)) {
11+
decoders.set(encoding, new TextDecoder(encoding));
12+
}
13+
const decoder = decoders.get(encoding)!;
14+
return decoder.decode(data);
15+
},
16+
encode: encoder.encode,
17+
};

0 commit comments

Comments
 (0)