Skip to content

Commit

Permalink
feat: binary encoder
Browse files Browse the repository at this point in the history
  • Loading branch information
Bekacru committed Dec 11, 2024
1 parent b85503f commit d6b3e0c
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 148 deletions.
140 changes: 85 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ utilities provided by `@better-auth/utils`:
| [**Hex**](#hex) | Encode and decode data in hexadecimal format. |
| [**OTP**](#otp) | Generate and verify one-time passwords. |


## Hash

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.
Expand Down Expand Up @@ -228,61 +229,6 @@ const isValid = await ecdsa.verify(publicKey, {
});
```

## Base64

Base64 utilities provide a simple interface to encode and decode data in base64 format.

### Encoding

Encode data in base64 format. Input can be a string, `ArrayBuffer`, or `TypedArray`.

```ts
import { base64 } from "@better-auth/utils/base64";

const encodedData = base64.encode("Data to encode");
```

options:
- `urlSafe` - URL-safe encoding, replacing `+` with `-` and `/` with `_`.
- `padding` - Include padding characters (`=`) at the end of the encoded string

```ts
const encodedData = base64.encode("Data to encode", { url: true, padding: false });
```

### Decoding

Decode base64-encoded data. Input can be a string or `ArrayBuffer`.

```ts
const decodedData = await base64.decode(encodedData);
```

It automatically detects if the input is URL-safe and includes padding characters.


## Hex

Hex utilities provide a simple interface to encode and decode data in hexadecimal format.

### Encoding

Encode data in hexadecimal format. Input can be a string, `ArrayBuffer`, or `TypedArray`.

```ts
import { hex } from "@better-auth/utils/hex";

const encodedData = hex.encode("Data to encode");
```

### Decoding

Decode hexadecimal-encoded data. Input can be a string or `ArrayBuffer`.

```ts
const decodedData = hex.decode(encodedData);
```

## OTP

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.
Expand Down Expand Up @@ -349,6 +295,90 @@ const secret = "my-super-secret-key";
const qrCodeUrl = createOTP(secret).url("my-app", "user@email.com");
```


## Base64

Base64 utilities provide a simple interface to encode and decode data in base64 format.

### Encoding

Encode data in base64 format. Input can be a string, `ArrayBuffer`, or `TypedArray`.

```ts
import { base64 } from "@better-auth/utils/base64";

const encodedData = base64.encode("Data to encode");
```

options:
- `padding` - Include padding characters (`=`) at the end of the encoded string

```ts
const encodedData = base64.encode("Data to encode", { url: true, padding: false });
```

### Decoding

Decode base64-encoded data. Input can be a string or `ArrayBuffer`.

```ts
const decodedData = await base64.decode(encodedData);
```

It automatically detects if the input is URL-safe and includes padding characters.

### Base64Url

Url safe alternative

```ts
import { base64Url } from "@better-auth/utils/base64";

const encodedData = base64Url.encode("Data to encode");
```

## Hex

Hex utilities provide a simple interface to encode and decode data in hexadecimal format.

### Encoding

Encode data in hexadecimal format. Input can be a string, `ArrayBuffer`, or `TypedArray`.

```ts
import { hex } from "@better-auth/utils/hex";

const encodedData = hex.encode("Data to encode");
```

### Decoding

Decode hexadecimal-encoded data. Input can be a string or `ArrayBuffer`.

```ts
const decodedData = hex.decode(encodedData);
```

## Binary

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.

### Encoding

```ts
import { binary } from "@better-auth/util/binary"

const data = binary.encode("Hello World!")
```

### Decoding

```ts
import { binary } from "@better-auth/util/binary"

const data = binary.decode(new Unit8Array([[72, 101, 108, 108, 111]]))
```

## License

MIT
32 changes: 13 additions & 19 deletions src/base64.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { base64 } from "./base64";
import { base64, base64Url } from "./base64";
import { binary } from "./binary";

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

describe("encode", () => {
it("encodes a string to base64 with padding", async () => {
const result = await base64.encode(plainText, { padding: true });
const result = base64.encode(plainText, { padding: true });
expect(result).toBe(base64Encoded);
});

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

it("encodes a string to base64 URL-safe", async () => {
const result = await base64.encode(plainText, {
urlSafe: true,
const result = base64Url.encode(plainText, {
padding: false,
});
expect(result).toBe(base64UrlEncoded);
});

it("encodes an ArrayBuffer to base64", async () => {
const result = await base64.encode(plainBuffer, { padding: true });
const result = base64.encode(plainBuffer, { padding: true });
expect(result).toBe(base64Encoded);
});
});

describe("decode", () => {
it("decodes a base64 string to a Uint8Array", async () => {
const result = await base64.decode(base64Encoded);
expect(result).toBe(plainText);
it("decodes a base64 string", async () => {
const encoded = Buffer.from(plainText).toString("base64");
const result = base64.decode(encoded);
expect(binary.decode(result)).toBe(plainText);
});

it("decodes a base64 URL-safe string to a Uint8Array", async () => {
const result = await base64.decode(base64UrlEncoded);
expect(result).toBe(plainText);
});

it("throws an error for invalid characters", async () => {
const invalidBase64 = "SGVsbG8s#";
await expect(base64.decode(invalidBase64)).rejects.toThrow(
"Invalid character",
);
it("decodes a base64 URL-safe string", async () => {
const result = base64.decode(base64UrlEncoded);
expect(binary.decode(result)).toBe(plainText);
});
});
});
111 changes: 56 additions & 55 deletions src/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@

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

function createDecodeMap(alphabet: string): Map<string, number> {
const decodeMap = new Map<string, number>();
for (let i = 0; i < alphabet.length; i++) {
decodeMap.set(alphabet[i]!, i);
}
return decodeMap;
}

function getAlphabet(urlSafe: boolean): string {
return urlSafe
? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
Expand All @@ -25,82 +17,91 @@ function base64Encode(
let buffer = 0;
let shift = 0;

for (let i = 0; i < data.length; i++) {
buffer = (buffer << 8) | data[i]!;
for (const byte of data) {
buffer = (buffer << 8) | byte;
shift += 8;
while (shift >= 6) {
shift -= 6;
result += alphabet[(buffer >> shift) & 0x3f];
}
}

if (shift > 0) {
result += alphabet[(buffer << (6 - shift)) & 0x3f];
}

if (padding) {
const padCount = (4 - (result.length % 4)) % 4;
result += "=".repeat(padCount);
}

return result;
}

function base64Decode(data: string, alphabet: string): Uint8Array {
const decodeMap = createDecodeMap(alphabet);
const decodeMap = new Map<string, number>();
for (let i = 0; i < alphabet.length; i++) {
decodeMap.set(alphabet[i]!, i);
}
const result: number[] = [];
const chunkCount = Math.ceil(data.length / 4);
let buffer = 0;
let bitsCollected = 0;

for (let i = 0; i < chunkCount; i++) {
let padCount = 0;
let buffer = 0;
for (let j = 0; j < 4; j++) {
const encoded = data[i * 4 + j];
if (encoded === "=") {
padCount += 1;
continue;
}
if (encoded === undefined) {
padCount += 1;
continue;
}
const value = decodeMap.get(encoded) ?? null;
if (value === null) {
throw new Error(`Invalid character: ${encoded}`);
}
buffer += value << (6 * (3 - j));
}
result.push((buffer >> 16) & 0xff);
if (padCount < 2) {
result.push((buffer >> 8) & 0xff);
for (const char of data) {
if (char === "=") break;
const value = decodeMap.get(char);
if (value === undefined) {
throw new Error(`Invalid Base64 character: ${char}`);
}
if (padCount < 1) {
result.push(buffer & 0xff);
buffer = (buffer << 6) | value;
bitsCollected += 6;

if (bitsCollected >= 8) {
bitsCollected -= 8;
result.push((buffer >> bitsCollected) & 0xff);
}
}

return Uint8Array.from(result);
}

export const base64 = {
async encode(
encode(
data: ArrayBuffer | TypedArray | string,
options: {
urlSafe?: boolean;
padding?: boolean;
} = {},
options: { padding?: boolean } = {},
) {
const alphabet = getAlphabet(options.urlSafe ?? false);
if (typeof data === "string") {
const encoder = new TextEncoder();
data = encoder.encode(data);
const alphabet = getAlphabet(false);
const buffer =
typeof data === "string"
? new TextEncoder().encode(data)
: new Uint8Array(data);
return base64Encode(buffer, alphabet, options.padding ?? true);
},
decode(data: string | ArrayBuffer | TypedArray) {
if (typeof data !== "string") {
data = new TextDecoder().decode(data);
}
return base64Encode(
new Uint8Array(data),
alphabet,
options.padding ?? true,
);
const urlSafe = data.includes("-") || data.includes("_");
const alphabet = getAlphabet(urlSafe);
return base64Decode(data, alphabet);
},
};

export const base64Url = {
encode(
data: ArrayBuffer | TypedArray | string,
options: { padding?: boolean } = {},
) {
const alphabet = getAlphabet(true);
const buffer =
typeof data === "string"
? new TextEncoder().encode(data)
: new Uint8Array(data);
return base64Encode(buffer, alphabet, options.padding ?? true);
},
async decode(data: string) {
const isUrlSafe = data.includes("-") || data.includes("_");
const alphabet = getAlphabet(isUrlSafe);
const decoded = base64Decode(data, alphabet);
return new TextDecoder().decode(decoded);
decode(data: string) {
const urlSafe = data.includes("-") || data.includes("_");
const alphabet = getAlphabet(urlSafe);
return base64Decode(data, alphabet);
},
};
17 changes: 17 additions & 0 deletions src/binary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type Encoding = "utf-8" | "utf-16" | "iso-8859-1";

type BinaryData = ArrayBuffer | ArrayBufferView;

const decoders = new Map<Encoding, TextDecoder>();
const encoder = new TextEncoder();

export const binary = {
decode: (data: BinaryData, encoding: Encoding = "utf-8") => {
if (!decoders.has(encoding)) {
decoders.set(encoding, new TextDecoder(encoding));
}
const decoder = decoders.get(encoding)!;
return decoder.decode(data);
},
encode: encoder.encode,
};
Loading

0 comments on commit d6b3e0c

Please sign in to comment.