diff --git a/README.md b/README.md index 523e44d..b7aae0f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ utilities provided by `@better-auth/utils`: | [**RSA**](#rsa) | Perform encryption, decryption, signing, and verification with RSA keys. | | [**ECDSA**](#ecdsa) | Perform signing and verification with ECDSA keys. | | [**Base64**](#base64) | Encode and decode data in base64 format. | +| [**Hex**](#hex) | Encode and decode data in hexadecimal format. | | [**OTP**](#otp) | Generate and verify one-time passwords. | | [**Cookies**](#cookies) | Parse, serialize, and manage HTTP cookies. | @@ -254,6 +255,28 @@ 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. diff --git a/package.json b/package.json index ec43672..46a1ddd 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,10 @@ "import": "./dist/ecdsa.mjs", "require": "./dist/ecdsa.cjs" }, + "./hex": { + "import": "./dist/hex.mjs", + "require": "./dist/hex.cjs" + }, "./hmac": { "import": "./dist/hmac.mjs", "require": "./dist/hmac.cjs" diff --git a/src/hex.test.ts b/src/hex.test.ts new file mode 100644 index 0000000..d3b8fef --- /dev/null +++ b/src/hex.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { hex } from './hex'; + +describe('hex', () => { + describe('encode', () => { + it('should encode a string to hexadecimal', () => { + const input = "Hello, World!"; + const expected = "48656c6c6f2c20576f726c6421"; + expect(hex.encode(input)).toBe(expected); + }); + + it('should encode an ArrayBuffer to hexadecimal', () => { + const input = new TextEncoder().encode("Hello").buffer; + const expected = "48656c6c6f"; + expect(hex.encode(input)).toBe(expected); + }); + + it('should encode a TypedArray to hexadecimal', () => { + const input = new Uint8Array([72, 101, 108, 108, 111]); + const expected = "48656c6c6f"; + expect(hex.encode(input)).toBe(expected); + }); + }); + + describe('decode', () => { + it('should decode a hexadecimal string to its original value', () => { + const input = "48656c6c6f2c20576f726c6421"; + const expected = "Hello, World!"; + expect(hex.decode(input)).toBe(expected); + }); + + it('should handle decoding of a hexadecimal string to binary data', () => { + const input = "48656c6c6f"; + const expected = "Hello"; + expect(hex.decode(input)).toBe(expected); + }); + + it('should throw an error for an odd-length string', () => { + const input = "123"; + expect(() => hex.decode(input)).toThrow(Error); + }); + + it('should throw an error for a non-hexadecimal string', () => { + const input = "zzzz"; + expect(() => hex.decode(input)).toThrow(Error); + }); + }); + + describe('round-trip tests', () => { + it('should return the original string after encoding and decoding', () => { + const input = "Hello, Hex!"; + const encoded = hex.encode(input); + const decoded = hex.decode(encoded); + expect(decoded).toBe(input); + }); + + it('should handle empty strings', () => { + const input = ""; + const encoded = hex.encode(input); + const decoded = hex.decode(encoded); + expect(decoded).toBe(input); + }); + }); +}); diff --git a/src/hex.ts b/src/hex.ts new file mode 100644 index 0000000..fce97bc --- /dev/null +++ b/src/hex.ts @@ -0,0 +1,37 @@ +import type { TypedArray } from "./type"; + + +const hexadecimal = "0123456789abcdef"; +export const hex = { + encode: (data: + string | ArrayBuffer | TypedArray) => { + if (typeof data === "string") { + data = new TextEncoder().encode(data); + } + if (data.byteLength === 0) { + return ""; + } + const buffer = new Uint8Array(data); + let result = ""; + for (const byte of buffer) { + result += byte.toString(16).padStart(2, "0"); + } + return result; + }, + decode: (data: string) => { + if (!data) { + return ""; + } + if (data.length % 2 !== 0) { + throw new Error("Invalid hexadecimal string"); + } + if (!new RegExp(`^[${hexadecimal}]+$`).test(data)) { + throw new Error("Invalid hexadecimal string"); + } + const result = new Uint8Array(data.length / 2); + for (let i = 0; i < data.length; i += 2) { + result[i / 2] = parseInt(data.slice(i, i + 2), 16); + } + return new TextDecoder().decode(result); + } +} \ No newline at end of file