diff --git a/packages/thirdweb/src/utils/encoding/helpers/assert-size.test.ts b/packages/thirdweb/src/utils/encoding/helpers/assert-size.test.ts new file mode 100644 index 00000000000..450533c583b --- /dev/null +++ b/packages/thirdweb/src/utils/encoding/helpers/assert-size.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { assertSize } from "./assert-size.js"; + +describe("assertSize", () => { + it("should not throw an error for hex strings within the specified size", () => { + expect(() => assertSize("0x1a4", { size: 2 })).not.toThrow(); + expect(() => assertSize("0x1234", { size: 2 })).not.toThrow(); + }); + + it("should throw an error for hex strings exceeding the specified size", () => { + expect(() => assertSize("0x123456", { size: 2 })).toThrow( + "Size overflow: 3 > 2", + ); + expect(() => assertSize("0xabcdef", { size: 2 })).toThrow( + "Size overflow: 3 > 2", + ); + }); + + it("should not throw an error for Uint8Array within the specified size", () => { + expect(() => + assertSize(new Uint8Array([1, 2, 3]), { size: 3 }), + ).not.toThrow(); + expect(() => assertSize(new Uint8Array([]), { size: 0 })).not.toThrow(); + }); + + it("should throw an error for Uint8Array exceeding the specified size", () => { + expect(() => assertSize(new Uint8Array([1, 2, 3, 4]), { size: 3 })).toThrow( + "Size overflow: 4 > 3", + ); + }); + + it("should not throw an error for empty hex strings", () => { + expect(() => assertSize("0x", { size: 0 })).not.toThrow(); + }); + + it("should handle boundary conditions correctly", () => { + expect(() => assertSize("0x12", { size: 1 })).not.toThrow(); + expect(() => assertSize("0x12", { size: 0 })).toThrow( + "Size overflow: 1 > 0", + ); + }); + + it("should not throw an error for hex strings exactly at the specified size", () => { + expect(() => assertSize("0x12", { size: 1 })).not.toThrow(); + expect(() => assertSize("0x1234", { size: 2 })).not.toThrow(); + }); +}); diff --git a/packages/thirdweb/src/utils/encoding/helpers/byte-size.test.ts b/packages/thirdweb/src/utils/encoding/helpers/byte-size.test.ts new file mode 100644 index 00000000000..b54fe46b11a --- /dev/null +++ b/packages/thirdweb/src/utils/encoding/helpers/byte-size.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { byteSize } from "./byte-size.js"; + +describe("byteSize", () => { + it('should calculate the byte size for a valid hex string with "0x" prefix', () => { + expect(byteSize("0x1a4")).toBe(2); // "1a4" is 1 byte + 1 nibble = 2 bytes + expect(byteSize("0x123456")).toBe(3); // "123456" is 3 bytes + }); + + it("should return 0 for an empty hex string", () => { + expect(byteSize("0x")).toBe(0); + }); + + it("should calculate the byte size for a Uint8Array", () => { + expect(byteSize(new Uint8Array([1, 2, 3]))).toBe(3); + expect(byteSize(new Uint8Array([]))).toBe(0); + }); + + it('should handle a single byte hex string with "0x" prefix', () => { + expect(byteSize("0x1")).toBe(1); // "1" is half a byte, treated as 1 byte + expect(byteSize("0x12")).toBe(1); // "12" is 1 byte + }); + + it("should handle longer hex strings", () => { + expect(byteSize("0x1234567890abcdef")).toBe(8); // "1234567890abcdef" is 8 bytes + expect(byteSize("0xabcdef")).toBe(3); // "abcdef" is 3 bytes + }); + + it("should return 0 for non-hex string inputs in Uint8Array form", () => { + expect(byteSize(new Uint8Array([]))).toBe(0); // Empty Uint8Array + }); + + it('should handle strings that are not valid hex but start with "0x"', () => { + expect(byteSize("0xg")).toBe(1); // Not a valid hex string + }); +}); diff --git a/packages/thirdweb/src/utils/encoding/helpers/charcode-to-base-16.test.ts b/packages/thirdweb/src/utils/encoding/helpers/charcode-to-base-16.test.ts new file mode 100644 index 00000000000..e6e567f9c8c --- /dev/null +++ b/packages/thirdweb/src/utils/encoding/helpers/charcode-to-base-16.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { charCodeToBase16 } from "./charcode-to-base-16.js"; + +describe("charCodeToBase16", () => { + it("should return correct values for digits (0-9)", () => { + expect(charCodeToBase16("0".charCodeAt(0))).toBe(0); + expect(charCodeToBase16("5".charCodeAt(0))).toBe(5); + expect(charCodeToBase16("9".charCodeAt(0))).toBe(9); + }); + + it("should return correct values for uppercase letters (A-F)", () => { + expect(charCodeToBase16("A".charCodeAt(0))).toBe(10); + expect(charCodeToBase16("C".charCodeAt(0))).toBe(12); + expect(charCodeToBase16("F".charCodeAt(0))).toBe(15); + }); + + it("should return correct values for lowercase letters (a-f)", () => { + expect(charCodeToBase16("a".charCodeAt(0))).toBe(10); + expect(charCodeToBase16("c".charCodeAt(0))).toBe(12); + expect(charCodeToBase16("f".charCodeAt(0))).toBe(15); + }); + + // Test cases for invalid inputs + it("should return undefined for invalid inputs", () => { + expect(charCodeToBase16("G".charCodeAt(0))).toBeUndefined(); + expect(charCodeToBase16("z".charCodeAt(0))).toBeUndefined(); + expect(charCodeToBase16(" ".charCodeAt(0))).toBeUndefined(); + expect(charCodeToBase16("!".charCodeAt(0))).toBeUndefined(); + }); + + it("should handle edge cases correctly", () => { + expect(charCodeToBase16("0".charCodeAt(0) - 1)).toBeUndefined(); // Just below '0' + expect(charCodeToBase16("9".charCodeAt(0) + 1)).toBeUndefined(); // Just above '9' + expect(charCodeToBase16("A".charCodeAt(0) - 1)).toBeUndefined(); // Just below 'A' + expect(charCodeToBase16("F".charCodeAt(0) + 1)).toBeUndefined(); // Just above 'F' + expect(charCodeToBase16("a".charCodeAt(0) - 1)).toBeUndefined(); // Just below 'a' + expect(charCodeToBase16("f".charCodeAt(0) + 1)).toBeUndefined(); // Just above 'f' + }); +}); diff --git a/packages/thirdweb/src/utils/encoding/helpers/is-hext.test.ts b/packages/thirdweb/src/utils/encoding/helpers/is-hext.test.ts new file mode 100644 index 00000000000..171850520f1 --- /dev/null +++ b/packages/thirdweb/src/utils/encoding/helpers/is-hext.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { isHex } from "./is-hex.js"; + +describe("isHex", () => { + it('should return true for valid hex strings with "0x" prefix in strict mode', () => { + expect(isHex("0x1a4", { strict: true })).toBe(true); + expect(isHex("0xABCDEF", { strict: true })).toBe(true); + }); + + it('should return false for hex strings without "0x" prefix in strict mode', () => { + expect(isHex("1a4", { strict: true })).toBe(false); + expect(isHex("abcdef", { strict: true })).toBe(false); + }); + + it('should return true for valid hex strings with or without "0x" prefix in non-strict mode', () => { + expect(isHex("0x1a4", { strict: false })).toBe(true); + expect(isHex("1a4", { strict: false })).toBe(false); + }); + + it("should return false for invalid hex strings in strict mode", () => { + expect(isHex("0x1g4", { strict: true })).toBe(false); + expect(isHex("0xZXY", { strict: true })).toBe(false); + }); + + it("should return false for invalid hex strings in non-strict mode", () => { + expect(isHex("0x1g4", { strict: false })).toBe(true); + expect(isHex("0xZXY", { strict: false })).toBe(true); + }); + + it("should return false for non-string inputs", () => { + expect(isHex(123, { strict: true })).toBe(false); + expect(isHex(null, { strict: true })).toBe(false); + expect(isHex(undefined, { strict: true })).toBe(false); + expect(isHex({}, { strict: true })).toBe(false); + }); + + it("should return false for empty strings", () => { + expect(isHex("", { strict: true })).toBe(false); + expect(isHex("", { strict: false })).toBe(false); + }); + + it("should return true for valid empty hex prefix in non-strict mode", () => { + expect(isHex("0x", { strict: false })).toBe(true); + }); + + it("should return true for valid empty hex prefix in strict mode", () => { + expect(isHex("0x", { strict: true })).toBe(true); + }); + + it("should use strict mode by default", () => { + expect(isHex("0x1a4")).toBe(true); + expect(isHex("1a4")).toBe(false); + }); +}); diff --git a/packages/thirdweb/src/utils/encoding/to-bytes.test.ts b/packages/thirdweb/src/utils/encoding/to-bytes.test.ts new file mode 100644 index 00000000000..05b7e433442 --- /dev/null +++ b/packages/thirdweb/src/utils/encoding/to-bytes.test.ts @@ -0,0 +1,233 @@ +import { describe, expect, it } from "vitest"; +import { numberToHex } from "./hex.js"; +import { + boolToBytes, + hexToBytes, + numberToBytes, + padBytes, + stringToBytes, + toBytes, +} from "./to-bytes.js"; + +describe("to-bytes.js", () => { + describe("padBytes", () => { + it("should pad bytes to the left by default to the specified size", () => { + const result = padBytes(new Uint8Array([1, 2, 3]), { size: 5 }); + expect(result).toEqual(new Uint8Array([0, 0, 1, 2, 3])); + }); + + it('should pad bytes to the right if dir is "right"', () => { + const result = padBytes(new Uint8Array([1, 2, 3]), { + dir: "right", + size: 5, + }); + expect(result).toEqual(new Uint8Array([1, 2, 3, 0, 0])); + }); + + it("should not pad if the byte array is already the specified size", () => { + const result = padBytes(new Uint8Array([1, 2, 3, 4, 5]), { size: 5 }); + expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5])); + }); + + it("should throw an error if the byte array exceeds the specified size", () => { + expect(() => + padBytes(new Uint8Array([1, 2, 3, 4, 5, 6]), { size: 5 }), + ).toThrow("Size overflow: 6 > 5"); + }); + + it("should return the same byte array if size is null", () => { + const bytes = new Uint8Array([1, 2, 3]); + const result = padBytes(bytes, { size: null }); + expect(result).toEqual(bytes); + }); + + it("should pad to the default size of 32 if size is not specified", () => { + const result = padBytes(new Uint8Array([1, 2, 3])); + expect(result).toStrictEqual( + new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 2, 3, + ]), + ); + }); + + it("should handle an empty byte array and pad to the specified size", () => { + const result = padBytes(new Uint8Array([]), { size: 3 }); + expect(result).toEqual(new Uint8Array([0, 0, 0])); + }); + }); + + describe("toBytes", () => { + it("should convert a number to bytes", () => { + expect(toBytes(123)).toEqual(new Uint8Array([123])); + }); + + it("should convert a bigint to bytes", () => { + expect(toBytes(BigInt(123))).toEqual(new Uint8Array([123])); + }); + + it("should convert a boolean to bytes", () => { + expect(toBytes(true)).toEqual(new Uint8Array([1])); + expect(toBytes(false)).toEqual(new Uint8Array([0])); + }); + + it("should convert a hex string to bytes", () => { + expect(toBytes("0x1a")).toEqual(new Uint8Array([26])); // 0x1a = 26 in decimal + expect(toBytes("0xabcd")).toEqual(new Uint8Array([171, 205])); + }); + + it("should convert a regular string to bytes", () => { + expect(toBytes("abc")).toEqual(new Uint8Array([97, 98, 99])); // ASCII values for 'a', 'b', 'c' + }); + + it("should handle empty hex string", () => { + expect(toBytes("0x")).toEqual(new Uint8Array([])); + }); + + it("should handle empty string", () => { + expect(toBytes("")).toEqual(new Uint8Array([])); + }); + }); + + describe("boolToBytes", () => { + it("should convert true to a Uint8Array with a value of 1", () => { + expect(boolToBytes(true)).toEqual(new Uint8Array([1])); + }); + + it("should convert false to a Uint8Array with a value of 0", () => { + expect(boolToBytes(false)).toEqual(new Uint8Array([0])); + }); + + it("should pad the byte array to the specified size with zeros on the left when size is provided", () => { + expect(boolToBytes(true, { size: 4 })).toEqual( + new Uint8Array([0, 0, 0, 1]), + ); + expect(boolToBytes(false, { size: 4 })).toEqual( + new Uint8Array([0, 0, 0, 0]), + ); + }); + + it("should throw an error if the specified size is less than the byte size", () => { + expect(() => boolToBytes(true, { size: 0 })).toThrow( + "Size overflow: 1 > 0", + ); + }); + + it("should not pad if no size is provided", () => { + expect(boolToBytes(true)).toEqual(new Uint8Array([1])); + expect(boolToBytes(false)).toEqual(new Uint8Array([0])); + }); + + it("should handle size of 1 correctly", () => { + expect(boolToBytes(true, { size: 1 })).toEqual(new Uint8Array([1])); + expect(boolToBytes(false, { size: 1 })).toEqual(new Uint8Array([0])); + }); + }); + + describe("hexToBytes", () => { + it("should convert a valid hex string to Uint8Array", () => { + expect(hexToBytes("0x1a4")).toEqual(new Uint8Array([1, 164])); + expect(hexToBytes("0xabcdef")).toEqual(new Uint8Array([171, 205, 239])); + }); + + it("should handle odd-length hex strings by prepending a zero", () => { + expect(hexToBytes("0xabc")).toEqual(new Uint8Array([10, 188])); + }); + + it("should throw an error for invalid hex strings", () => { + expect(() => hexToBytes("0x1g")).toThrow( + 'Invalid byte sequence ("1g" in "1g").', + ); + expect(() => hexToBytes("0xzz")).toThrow( + 'Invalid byte sequence ("zz" in "zz").', + ); + }); + + it("should pad the hex string to the specified size by adding extra bytes", () => { + expect(hexToBytes("0x1a", { size: 4 })).toEqual( + new Uint8Array([26, 0, 0, 0]), + ); + }); + + it("should throw an error if the hex string exceeds the specified size", () => { + expect(() => hexToBytes("0x123456", { size: 2 })).toThrow( + "Size overflow", + ); + }); + + it("should correctly convert hex strings with even lengths without additional padding", () => { + expect(hexToBytes("0x1234")).toEqual(new Uint8Array([18, 52])); + }); + }); + + describe("numberToBytes", () => { + it("should convert a number to a Uint8Array", () => { + const result = numberToBytes(420); + expect(result).toEqual(new Uint8Array([1, 164])); + }); + + it("should convert a bigint to a Uint8Array", () => { + const result = numberToBytes(BigInt(420)); + expect(result).toEqual(new Uint8Array([1, 164])); + }); + + it("should handle zero correctly", () => { + const result = numberToBytes(0); + expect(result).toEqual(new Uint8Array([0])); + }); + + it("should handle large numbers correctly", () => { + const largeNumber = BigInt("12345678901234567890"); + const result = numberToBytes(largeNumber); + expect(result).toEqual(hexToBytes(numberToHex(largeNumber))); + }); + + it("should apply options correctly when converting numbers", () => { + const opts = { size: 4 }; + const result = numberToBytes(420, opts); + expect(result).toEqual(hexToBytes(numberToHex(420, opts))); + }); + }); + + describe("stringToBytes", () => { + it("should convert a string to a Uint8Array of bytes", () => { + const result = stringToBytes("Hello, world!"); + expect(result).toEqual( + new Uint8Array([ + 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33, + ]), + ); + }); + + it("should handle an empty string", () => { + const result = stringToBytes(""); + expect(result).toEqual(new Uint8Array([])); + }); + + it("should pad the byte array to the specified size with zeros on the right", () => { + const result = stringToBytes("Hi", { size: 5 }); + expect(result).toEqual(new Uint8Array([72, 105, 0, 0, 0])); + }); + + it("should throw an error if the byte array exceeds the specified size", () => { + expect(() => stringToBytes("Hello", { size: 3 })).toThrow( + "Size overflow: 5 > 3", + ); + }); + + it("should not pad if no size is provided", () => { + const result = stringToBytes("Test"); + expect(result).toEqual(new Uint8Array([84, 101, 115, 116])); + }); + + it("should handle unicode characters correctly", () => { + const result = stringToBytes("こんにちは"); + expect(result).toEqual( + new Uint8Array([ + 227, 129, 147, 227, 130, 147, 227, 129, 171, 227, 129, 161, 227, 129, + 175, + ]), + ); + }); + }); +}); diff --git a/packages/thirdweb/src/utils/encoding/to-bytes.ts b/packages/thirdweb/src/utils/encoding/to-bytes.ts index 8f59a69ece8..a3841d6055e 100644 --- a/packages/thirdweb/src/utils/encoding/to-bytes.ts +++ b/packages/thirdweb/src/utils/encoding/to-bytes.ts @@ -9,7 +9,13 @@ type PadOptions = { size?: number | null; }; -function padBytes(bytes: Uint8Array, { dir, size = 32 }: PadOptions = {}) { +/** + * @internal Exported for test + */ +export function padBytes( + bytes: Uint8Array, + { dir, size = 32 }: PadOptions = {}, +) { if (size === null) { return bytes; }