Skip to content

Commit e8bc13e

Browse files
author
Michael Taylor
committed
feat: add subdomain encoder
1 parent e8b28b2 commit e8bc13e

File tree

1 file changed

+220
-143
lines changed

1 file changed

+220
-143
lines changed

src/utils/Subdomain.ts

Lines changed: 220 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,236 @@
11
// SubDomain.ts
22

3-
import baseX from "base-x";
4-
53
/**
64
* SubDomain Class
75
*
86
* Encapsulates the logic for encoding and decoding a combination of
97
* chain and storeId into a DNS-friendly identifier using Base62 encoding.
108
*/
119
class SubDomain {
12-
// Define the Base62 character set
13-
private static readonly BASE62_CHARSET =
14-
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
15-
16-
// Initialize the Base62 encoder/decoder
17-
private static base62 = baseX(SubDomain.BASE62_CHARSET);
18-
19-
// Define expected byte length for storeId
20-
private static readonly STORE_ID_LENGTH = 32; // bytes
21-
22-
// Properties
23-
public readonly chain: string;
24-
public readonly storeId: string;
25-
public readonly encodedId: string;
26-
27-
/**
28-
* Constructor for SubDomain
29-
*
30-
* @param chain - The chain name (e.g., "CHAIN1234").
31-
* @param storeId - The store ID as a 64-character hexadecimal string.
32-
* @throws Will throw an error if inputs are invalid or encoding exceeds DNS limits.
33-
*/
34-
constructor(chain: string, storeId: string) {
35-
this.chain = chain;
36-
this.storeId = storeId;
37-
this.encodedId = this.encode();
38-
}
39-
40-
/**
41-
* Encodes the provided chain and storeId into a DNS-friendly identifier.
42-
*
43-
* @returns The Base62-encoded identifier.
44-
* @throws Will throw an error if encoding fails.
45-
*/
46-
private encode(): string {
47-
// Validate inputs
48-
if (!this.chain || typeof this.chain !== "string") {
49-
throw new Error("Invalid chain: Chain must be a non-empty string.");
50-
}
51-
52-
if (
53-
!this.storeId ||
54-
typeof this.storeId !== "string" ||
55-
!/^[0-9a-fA-F]{64}$/.test(this.storeId)
56-
) {
57-
throw new Error(
58-
"Invalid storeId: StoreId must be a 64-character hexadecimal string."
59-
);
60-
}
61-
62-
// Ensure the chain length is within 1-255 characters to fit in one byte
63-
const chainLength = this.chain.length;
64-
if (chainLength < 1 || chainLength > 255) {
65-
throw new Error(
66-
"Invalid chain: Length must be between 1 and 255 characters."
67-
);
68-
}
69-
70-
// Convert chain length to a single byte Buffer
71-
const chainLengthBuffer = Buffer.from([chainLength]);
72-
73-
// Convert chain to a Buffer (UTF-8)
74-
const chainBuffer = Buffer.from(this.chain, "utf8");
75-
76-
// Convert storeId from hex string to Buffer
77-
const storeIdBuffer = Buffer.from(this.storeId, "hex");
78-
79-
// Validate storeId byte length
80-
if (storeIdBuffer.length !== SubDomain.STORE_ID_LENGTH) {
81-
throw new Error(
82-
`Invalid storeId length: Expected ${SubDomain.STORE_ID_LENGTH} bytes, got ${storeIdBuffer.length} bytes.`
83-
);
10+
// Define the Base62 character set
11+
private static readonly BASE62_CHARSET =
12+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
13+
14+
// Define expected byte length for storeId
15+
private static readonly STORE_ID_LENGTH = 32; // bytes
16+
17+
// Properties
18+
public readonly chain: string;
19+
public readonly storeId: string;
20+
public readonly encodedId: string;
21+
22+
/**
23+
* Constructor for SubDomain
24+
*
25+
* @param chain - The chain name (e.g., "CHAIN1234").
26+
* @param storeId - The store ID as a 64-character hexadecimal string.
27+
* @throws Will throw an error if inputs are invalid or encoding exceeds DNS limits.
28+
*/
29+
constructor(chain: string, storeId: string) {
30+
this.chain = chain;
31+
this.storeId = storeId;
32+
this.encodedId = this.encode();
8433
}
85-
86-
// Concatenate chain_length, chain, and storeId buffers
87-
const dataBuffer = Buffer.concat([
88-
chainLengthBuffer,
89-
chainBuffer,
90-
storeIdBuffer,
91-
]);
92-
93-
// Encode the data buffer using Base62
94-
const encodedId = SubDomain.base62.encode(dataBuffer);
95-
96-
// Ensure DNS label length does not exceed 63 characters
97-
if (encodedId.length > 63) {
98-
throw new Error(
99-
`Encoded identifier length (${encodedId.length}) exceeds DNS label limit of 63 characters.`
100-
);
34+
35+
/**
36+
* Encodes the provided chain and storeId into a DNS-friendly identifier.
37+
*
38+
* @returns The Base62-encoded identifier.
39+
* @throws Will throw an error if encoding fails.
40+
*/
41+
private encode(): string {
42+
// Validate inputs
43+
if (!this.chain || typeof this.chain !== "string") {
44+
throw new Error("Invalid chain: Chain must be a non-empty string.");
45+
}
46+
47+
if (
48+
!this.storeId ||
49+
typeof this.storeId !== "string" ||
50+
!/^[0-9a-fA-F]{64}$/.test(this.storeId)
51+
) {
52+
throw new Error(
53+
"Invalid storeId: StoreId must be a 64-character hexadecimal string."
54+
);
55+
}
56+
57+
// Ensure the chain length is within 1-255 characters to fit in one byte
58+
const chainLength = this.chain.length;
59+
if (chainLength < 1 || chainLength > 255) {
60+
throw new Error(
61+
"Invalid chain: Length must be between 1 and 255 characters."
62+
);
63+
}
64+
65+
// Convert chain length to a single byte
66+
const chainLengthBuffer = Buffer.from([chainLength]);
67+
68+
// Convert chain to a Buffer (UTF-8)
69+
const chainBuffer = Buffer.from(this.chain, "utf8");
70+
71+
// Convert storeId from hex string to Buffer
72+
const storeIdBuffer = Buffer.from(this.storeId, "hex");
73+
74+
// Validate storeId byte length
75+
if (storeIdBuffer.length !== SubDomain.STORE_ID_LENGTH) {
76+
throw new Error(
77+
`Invalid storeId length: Expected ${SubDomain.STORE_ID_LENGTH} bytes, got ${storeIdBuffer.length} bytes.`
78+
);
79+
}
80+
81+
// Concatenate chain_length, chain, and storeId buffers
82+
const dataBuffer = Buffer.concat([
83+
chainLengthBuffer,
84+
chainBuffer,
85+
storeIdBuffer,
86+
]);
87+
88+
// Encode the data buffer using Base62
89+
const encodedId = SubDomain.encodeBase62(dataBuffer);
90+
91+
// Ensure DNS label length does not exceed 63 characters
92+
if (encodedId.length > 63) {
93+
throw new Error(
94+
`Encoded identifier length (${encodedId.length}) exceeds DNS label limit of 63 characters.`
95+
);
96+
}
97+
98+
return encodedId;
10199
}
102-
103-
return encodedId;
104-
}
105-
106-
/**
107-
* Decodes the provided identifier back into the original chain and storeId.
108-
*
109-
* @param encodedId - The Base62-encoded identifier.
110-
* @returns An object containing the original chain and storeId.
111-
* @throws Will throw an error if decoding fails or data lengths mismatch.
112-
*/
113-
public static decode(encodedId: string): { chain: string; storeId: string } {
114-
// Validate input
115-
if (!encodedId || typeof encodedId !== "string") {
116-
throw new Error(
117-
"Invalid encodedId: encodedId must be a non-empty string."
100+
101+
/**
102+
* Decodes the provided identifier back into the original chain and storeId.
103+
*
104+
* @param encodedId - The Base62-encoded identifier.
105+
* @returns An object containing the original chain and storeId.
106+
* @throws Will throw an error if decoding fails or data lengths mismatch.
107+
*/
108+
public static decode(encodedId: string): { chain: string; storeId: string } {
109+
// Validate input
110+
if (!encodedId || typeof encodedId !== "string") {
111+
throw new Error(
112+
"Invalid encodedId: encodedId must be a non-empty string."
113+
);
114+
}
115+
116+
// Decode the Base62 string back to a Buffer
117+
const decodedBuffer = SubDomain.decodeBase62(encodedId);
118+
119+
if (!decodedBuffer) {
120+
throw new Error("Failed to decode Base62 string.");
121+
}
122+
123+
// Ensure there's at least 1 byte for chain_length and STORE_ID_LENGTH bytes for storeId
124+
if (decodedBuffer.length < 1 + SubDomain.STORE_ID_LENGTH) {
125+
throw new Error("Decoded data is too short to contain required fields.");
126+
}
127+
128+
// Extract chain_length (1 byte)
129+
const chain_length = decodedBuffer.readUInt8(0);
130+
131+
// Extract chain
132+
const chain = decodedBuffer.slice(1, 1 + chain_length).toString("utf8");
133+
134+
// Extract storeId
135+
const storeIdBuffer = decodedBuffer.slice(
136+
1 + chain_length,
137+
1 + chain_length + SubDomain.STORE_ID_LENGTH
118138
);
139+
140+
// Convert storeId buffer to hex string
141+
const storeId = storeIdBuffer.toString("hex");
142+
143+
return { chain, storeId };
119144
}
120-
121-
// Decode the Base62 string back to a Buffer
122-
const decodedBuffer = SubDomain.base62.decode(encodedId);
123-
124-
if (!decodedBuffer) {
125-
throw new Error("Failed to decode Base62 string.");
145+
146+
/**
147+
* Encodes a Buffer into a Base62 string.
148+
*
149+
* @param buffer - The Buffer to encode.
150+
* @returns The Base62-encoded string.
151+
*/
152+
private static encodeBase62(buffer: Buffer): string {
153+
if (buffer.length === 0) return "";
154+
155+
// Convert Buffer to BigInt
156+
let num = BigInt(0);
157+
for (let i = 0; i < buffer.length; i++) {
158+
num = (num << BigInt(8)) + BigInt(buffer[i]);
159+
}
160+
161+
// Base62 encoding
162+
let encoded = "";
163+
const base = BigInt(62);
164+
165+
if (num === BigInt(0)) {
166+
encoded = SubDomain.BASE62_CHARSET[0];
167+
} else {
168+
while (num > 0) {
169+
const remainder = num % base;
170+
encoded = SubDomain.BASE62_CHARSET[Number(remainder)] + encoded;
171+
num = num / base;
172+
}
173+
}
174+
175+
// Handle leading zero bytes
176+
let leadingZeros = 0;
177+
for (let i = 0; i < buffer.length; i++) {
178+
if (buffer[i] === 0) {
179+
leadingZeros++;
180+
} else {
181+
break;
182+
}
183+
}
184+
for (let i = 0; i < leadingZeros; i++) {
185+
encoded = SubDomain.BASE62_CHARSET[0] + encoded;
186+
}
187+
188+
return encoded;
126189
}
127-
128-
// Ensure there's at least 1 byte for chain_length and STORE_ID_LENGTH bytes for storeId
129-
if (decodedBuffer.length < 1 + SubDomain.STORE_ID_LENGTH) {
130-
throw new Error("Decoded data is too short to contain required fields.");
190+
191+
/**
192+
* Decodes a Base62 string into a Buffer.
193+
*
194+
* @param str - The Base62 string to decode.
195+
* @returns The decoded Buffer.
196+
*/
197+
private static decodeBase62(str: string): Buffer {
198+
if (str.length === 0) return Buffer.alloc(0);
199+
200+
// Handle leading '0's
201+
let leadingZeros = 0;
202+
while (leadingZeros < str.length && str[leadingZeros] === SubDomain.BASE62_CHARSET[0]) {
203+
leadingZeros++;
204+
}
205+
206+
// Decode the Base62 string to BigInt
207+
let num = BigInt(0);
208+
const base = BigInt(62);
209+
for (let i = leadingZeros; i < str.length; i++) {
210+
const char = str[i];
211+
const value = SubDomain.BASE62_CHARSET.indexOf(char);
212+
if (value === -1) {
213+
throw new Error(`Invalid character in Base62 string: ${char}`);
214+
}
215+
num = num * base + BigInt(value);
216+
}
217+
218+
// Convert BigInt to Buffer
219+
let hex = num.toString(16);
220+
if (hex.length % 2 !== 0) {
221+
hex = "0" + hex;
222+
}
223+
let decoded = Buffer.from(hex, "hex");
224+
225+
// Prepend leading zero bytes
226+
if (leadingZeros > 0) {
227+
const zeroBuffer = Buffer.alloc(leadingZeros, 0);
228+
decoded = Buffer.concat([zeroBuffer, decoded]);
229+
}
230+
231+
return decoded;
131232
}
132-
133-
// Extract chain_length (1 byte)
134-
const chain_length = Buffer.from(decodedBuffer).readUInt8(0);
135-
136-
// Define the expected total length
137-
const expected_length = 1 + chain_length + SubDomain.STORE_ID_LENGTH;
138-
139-
if (decodedBuffer.length !== expected_length) {
140-
throw new Error(
141-
`Decoded data length mismatch: expected ${expected_length} bytes, got ${decodedBuffer.length} bytes.`
142-
);
143-
}
144-
145-
// Extract chain and storeId from the buffer
146-
const chain = Buffer.from(decodedBuffer.slice(1, 1 + chain_length)).toString("utf8");
147-
const storeIdBuffer = decodedBuffer.slice(
148-
1 + chain_length,
149-
1 + chain_length + SubDomain.STORE_ID_LENGTH
150-
);
151-
152-
// Convert storeId buffer to hex string
153-
const storeId = Buffer.from(storeIdBuffer).toString("hex");
154-
155-
return { chain, storeId };
156233
}
157-
}
158-
159-
export { SubDomain };
234+
235+
export { SubDomain };
236+

0 commit comments

Comments
 (0)