1/**
2 * Hexadecimal encoding, decoding, and random value helpers.
3 *
4 * @since 4.0.0
5 */
6import * as Result from "../Result.ts"
7import { EncodingError } from "./EncodingError.ts"
8
9/**
10 * Encodes the given value into a hex `string`.
11 *
12 * **When to use**
13 *
14 * Use to encode text or bytes as lowercase hexadecimal text.
15 *
16 * **Example** (Encoding hex strings and bytes)
17 *
18 * ```ts import.meta.vitest
19 * import { Hex } from "effect/encoding"
20 *
21 * // Encode a string to hex
22 * Hex.encode("hello") // => "68656c6c6f"
23 *
24 * // Encode binary data to hex
25 * const bytes = new Uint8Array([72, 101, 108, 108, 111])
26 * Hex.encode(bytes) // => "48656c6c6f"
27 * ```
28 *
29 * @category encoding
30 * @since 4.0.0
31 */
32export const encode: (input: Uint8Array | string) => string = (input) =>
33 typeof input === "string" ? hexEncodeUint8Array(encoder.encode(input)) : hexEncodeUint8Array(input)
34
35/**
36 * Generates a random lowercase hexadecimal string, optimized for lengths that
37 * are multiples of 8.
38 *
39 * **Details**
40 *
41 * `length` is not validated. The function generates `length >>> 3` random
42 * 8-character words, so non-negative lengths below `2 ** 32` are rounded down
43 * to a multiple of 8 and other values follow JavaScript's unsigned 32-bit
44 * coercion rules.
45 *
46 * This function uses `Math.random()` and is not cryptographically secure. For
47 * security-sensitive values, use the `Crypto.Crypto` service's `randomBytes`
48 * method and encode the result with {@link encode}.
49 *
50 * @category encoding
51 * @since 4.0.0
52 */
53export const random = (length: number): string => {
54 switch (length) {
55 case 16:
56 return random16()
57 case 32:
58 return random32()
59 default: {
60 let result = ""
61 for (let i = length >>> 3; i > 0; i--) {
62 result += random8()
63 }
64 return result
65 }
66 }
67}
68
69const hexCharCodes = Uint8Array.from("0123456789abcdef", (c) => c.charCodeAt(0))
70
71const randomWord = (): number => (Math.random() * 0x100000000) >>> 0
72
73// Trace and span identifiers are the common lengths. A single
74// String.fromCharCode call produces a flat string, which avoids rope
75// flattening when the identifier is later serialized.
76const random8 = (): string => {
77 const a = randomWord()
78 return String.fromCharCode(
79 hexCharCodes[a >>> 28],
80 hexCharCodes[(a >>> 24) & 15],
81 hexCharCodes[(a >>> 20) & 15],
82 hexCharCodes[(a >>> 16) & 15],
83 hexCharCodes[(a >>> 12) & 15],
84 hexCharCodes[(a >>> 8) & 15],
85 hexCharCodes[(a >>> 4) & 15],
86 hexCharCodes[a & 15]
87 )
88}
89
90const random16 = (): string => {
91 const a = randomWord()
92 const b = randomWord()
93 return String.fromCharCode(
94 hexCharCodes[a >>> 28],
95 hexCharCodes[(a >>> 24) & 15],
96 hexCharCodes[(a >>> 20) & 15],
97 hexCharCodes[(a >>> 16) & 15],
98 hexCharCodes[(a >>> 12) & 15],
99 hexCharCodes[(a >>> 8) & 15],
100 hexCharCodes[(a >>> 4) & 15],
101 hexCharCodes[a & 15],
102 hexCharCodes[b >>> 28],
103 hexCharCodes[(b >>> 24) & 15],
104 hexCharCodes[(b >>> 20) & 15],
105 hexCharCodes[(b >>> 16) & 15],
106 hexCharCodes[(b >>> 12) & 15],
107 hexCharCodes[(b >>> 8) & 15],
108 hexCharCodes[(b >>> 4) & 15],
109 hexCharCodes[b & 15]
110 )
111}
112
113const random32 = (): string => {
114 const a = randomWord()
115 const b = randomWord()
116 const c = randomWord()
117 const d = randomWord()
118 return String.fromCharCode(
119 hexCharCodes[a >>> 28],
120 hexCharCodes[(a >>> 24) & 15],
121 hexCharCodes[(a >>> 20) & 15],
122 hexCharCodes[(a >>> 16) & 15],
123 hexCharCodes[(a >>> 12) & 15],
124 hexCharCodes[(a >>> 8) & 15],
125 hexCharCodes[(a >>> 4) & 15],
126 hexCharCodes[a & 15],
127 hexCharCodes[b >>> 28],
128 hexCharCodes[(b >>> 24) & 15],
129 hexCharCodes[(b >>> 20) & 15],
130 hexCharCodes[(b >>> 16) & 15],
131 hexCharCodes[(b >>> 12) & 15],
132 hexCharCodes[(b >>> 8) & 15],
133 hexCharCodes[(b >>> 4) & 15],
134 hexCharCodes[b & 15],
135 hexCharCodes[c >>> 28],
136 hexCharCodes[(c >>> 24) & 15],
137 hexCharCodes[(c >>> 20) & 15],
138 hexCharCodes[(c >>> 16) & 15],
139 hexCharCodes[(c >>> 12) & 15],
140 hexCharCodes[(c >>> 8) & 15],
141 hexCharCodes[(c >>> 4) & 15],
142 hexCharCodes[c & 15],
143 hexCharCodes[d >>> 28],
144 hexCharCodes[(d >>> 24) & 15],
145 hexCharCodes[(d >>> 20) & 15],
146 hexCharCodes[(d >>> 16) & 15],
147 hexCharCodes[(d >>> 12) & 15],
148 hexCharCodes[(d >>> 8) & 15],
149 hexCharCodes[(d >>> 4) & 15],
150 hexCharCodes[d & 15]
151 )
152}
153
154/**
155 * Decodes a hexadecimal string into bytes safely.
156 *
157 * **When to use**
158 *
159 * Use to decode hexadecimal text into bytes without throwing on invalid input.
160 *
161 * **Details**
162 *
163 * Returns `Result.succeed` with a `Uint8Array` when decoding succeeds, or
164 * `Result.fail` with an `EncodingError` when the input has an odd length or
165 * contains invalid hex characters.
166 *
167 * **Example** (Decoding hex bytes)
168 *
169 * ```ts import.meta.vitest
170 * import { Result } from "effect"
171 * import { Hex } from "effect/encoding"
172 *
173 * Hex.decode("48656c6c6f") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111]))
174 * ```
175 *
176 * @category decoding
177 * @since 4.0.0
178 */
179export const decode = (str: string): Result.Result<Uint8Array, EncodingError> => {
180 const bytes = new TextEncoder().encode(str)
181 if (bytes.length % 2 !== 0) {
182 return Result.fail(
183 new EncodingError({
184 module: "Hex",
185 kind: "Decode",
186 input: str,
187 message: `Length must be a multiple of 2, but is ${bytes.length}`
188 })
189 )
190 }
191
192 try {
193 const length = bytes.length / 2
194 const result = new Uint8Array(length)
195 for (let i = 0; i < length; i++) {
196 const a = fromHexChar(bytes[i * 2])
197 const b = fromHexChar(bytes[i * 2 + 1])
198 result[i] = (a << 4) | b
199 }
200
201 return Result.succeed(result)
202 } catch (e) {
203 return Result.fail(
204 new EncodingError({
205 module: "Hex",
206 kind: "Decode",
207 input: str,
208 message: e instanceof Error ? e.message : "Invalid input"
209 })
210 )
211 }
212}
213
214/**
215 * Decodes a hexadecimal string into a UTF-8 string safely.
216 *
217 * **When to use**
218 *
219 * Use to decode hexadecimal text into UTF-8 text without throwing on invalid
220 * input.
221 *
222 * **Details**
223 *
224 * Returns `Result.succeed` with the decoded text when decoding succeeds, or
225 * `Result.fail` with an `EncodingError` when the input is not valid hex.
226 *
227 * **Example** (Decoding hex strings)
228 *
229 * ```ts import.meta.vitest
230 * import { Result } from "effect"
231 * import { Hex } from "effect/encoding"
232 *
233 * Hex.decodeString("68656c6c6f") // => Result.succeed("hello")
234 * ```
235 *
236 * @category decoding
237 * @since 4.0.0
238 */
239export const decodeString = (str: string) => Result.map(decode(str), (_) => decoder.decode(_))
240
241const encoder = new TextEncoder()
242const decoder = new TextDecoder()
243
244const byteToHex: Array<string> = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"))
245
246const hexEncodeUint8Array = (bytes: Uint8Array): string => {
247 let result = ""
248 for (let i = 0; i < bytes.length; i++) {
249 result += byteToHex[bytes[i]]
250 }
251 return result
252}
253
254const fromHexChar = (byte: number) => {
255 if (48 <= byte && byte <= 57) {
256 return byte - 48
257 }
258
259 if (97 <= byte && byte <= 102) {
260 return byte - 97 + 10
261 }
262
263 if (65 <= byte && byte <= 70) {
264 return byte - 65 + 10
265 }
266
267 throw new TypeError("Invalid input")
268}