@nakanoaas/uuid58 1.2.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,57 +16,37 @@
16
16
  * ```
17
17
  */
18
18
  const UUID58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
19
+ // internal.ts
20
+ const MAX_PREVIEW_LENGTH = 40;
19
21
  /**
20
22
  * @internal
21
- * The length of the Base58 alphabet as BigInt for encoding/decoding operations.
22
- * This is exported for internal use but not part of the public API.
23
+ * 58^9, the largest power of 58 below 2^53. A 9-digit Base58 chunk therefore
24
+ * fits in a Number, so most digit arithmetic can avoid slow BigInt division.
23
25
  */
24
- const UUID58_LENGTH = BigInt(58);
25
- // check.ts
26
- const MAX_UUID58 = "YcVfxkQb6JRzqk5kF2tNLv";
26
+ const BASE58_CHUNK = 7427658739644928n;
27
+ /** Appends `digits` Base58 digits of `value` (most significant first). */
28
+ function appendDigits(out, value, digits) {
29
+ let chunk = "";
30
+ for (let i = 0; i < digits; i++) {
31
+ const quotient = Math.floor(value / 58);
32
+ chunk = UUID58_ALPHABET[value - quotient * 58] + chunk;
33
+ value = quotient;
34
+ }
35
+ return out + chunk;
36
+ }
27
37
  /**
28
- * Regular expression pattern for validating UUID58 strings.
29
- *
30
- * This regex ensures that a string:
31
- * - Contains exactly 22 characters
32
- * - Uses only characters from the Base58 alphabet
33
- *
34
- * **Note:** This regex does NOT validate that the decoded value fits within
35
- * 128 bits (UUID size). For complete validation including 128-bit range
36
- * checking, use {@link isUuid58} instead.
38
+ * @internal
39
+ * Encodes an unsigned 128-bit integer as a 22-character Base58 string.
37
40
  *
38
- * @example
39
- * ```typescript
40
- * const isValid = UUID58_REGEX.test("XDY9dmBbcMBXqcRvYw8xJ2"); // true
41
- * const isValid = UUID58_REGEX.test("invalid"); // false
42
- * const isValid = UUID58_REGEX.test("123"); // false (too short)
43
- * ```
41
+ * The value is split into 4 + 9 + 9 digit chunks with two BigInt divisions,
42
+ * instead of 22 BigInt divisions (one per digit).
44
43
  */
45
- const UUID58_REGEX = /^[1-9A-HJ-NP-Za-km-z]{22}$/u;
46
- /**
47
- * Checks if a given string is a valid UUID58-encodable string.
48
- * This function efficiently validates without performing full decoding.
49
- *
50
- * A valid UUID58 string must:
51
- * - Be exactly 22 characters long
52
- * - Contain only characters from the Base58 alphabet
53
- * - Decode to a value that fits within 128 bits (UUID size)
54
- *
55
- * @param uuid58 - The string to check
56
- * @returns `true` if the string is a valid UUID58 string, `false` otherwise
57
- *
58
- * @example
59
- * ```typescript
60
- * isUuid58("XDY9dmBbcMBXqcRvYw8xJ2"); // true
61
- * isUuid58("invalid"); // false
62
- * isUuid58("O0lI"); // false (contains invalid characters)
63
- * ```
64
- */
65
- function isUuid58(uuid58) {
66
- return UUID58_REGEX.test(uuid58) && uuid58 <= MAX_UUID58;
44
+ function encodeUint128(num) {
45
+ const low = Number(num % BASE58_CHUNK);
46
+ num /= BASE58_CHUNK;
47
+ const mid = Number(num % BASE58_CHUNK);
48
+ return appendDigits(appendDigits(appendDigits("", Number(num / BASE58_CHUNK), 4), mid, 9), low, 9);
67
49
  }
68
- // internal.ts
69
- const MAX_PREVIEW_LENGTH = 40;
70
50
  /**
71
51
  * @internal
72
52
  * Formats untrusted input for inclusion in an error message.
@@ -80,16 +60,48 @@ function previewInput(input) {
80
60
  }
81
61
  // decode.ts
82
62
  var _Uuid58DecodeError;
83
- const BASE58_MAP = {};
84
- for (let i = 0; i < 58; i++) BASE58_MAP[UUID58_ALPHABET[i]] = BigInt(i);
63
+ const BASE58_INDEX = (/* @__PURE__ */ new Int8Array(128)).fill(-1);
64
+ for (let i = 0; i < 58; i++) BASE58_INDEX[UUID58_ALPHABET.charCodeAt(i)] = i;
65
+ const MAX_UINT128 = (1n << 128n) - 1n;
85
66
  /**
86
- * Error thrown when an invalid Base58 string is provided for decoding.
87
- * This includes strings containing characters not in the Base58 alphabet.
67
+ * Error thrown when an invalid uuid58 string is provided for decoding.
68
+ * Its {@link Uuid58DecodeError.kind | kind} tells why.
88
69
  */
89
- var Uuid58DecodeError = class extends Error {};
70
+ var Uuid58DecodeError = class extends Error {
71
+ constructor(message, kind) {
72
+ super(message);
73
+ this.kind = kind;
74
+ }
75
+ };
90
76
  _Uuid58DecodeError = Uuid58DecodeError;
91
77
  _Uuid58DecodeError.prototype.name = "Uuid58DecodeError";
92
78
  /**
79
+ * @internal
80
+ * Decodes a uuid58 string to an unsigned 128-bit integer, checking length,
81
+ * then characters, then range (specification, Section 4).
82
+ */
83
+ function decodeUint128(uuid58) {
84
+ if (uuid58.length !== 22) return new Uuid58DecodeError(`Expected Base58 string of length 22, but received string of length ${uuid58.length}: ${previewInput(uuid58)}`, "invalid_length");
85
+ let num = 0n;
86
+ for (let start = 0; start < 22;) {
87
+ const end = start === 0 ? 4 : start + 9;
88
+ let chunk = 0;
89
+ for (let i = start; i < end; i++) {
90
+ const code = uuid58.charCodeAt(i);
91
+ const index = code < 128 ? BASE58_INDEX[code] : -1;
92
+ if (index < 0) {
93
+ const char = String.fromCodePoint(uuid58.codePointAt(i));
94
+ return new Uuid58DecodeError(`Invalid Base58 character ${JSON.stringify(char)} found in input: ${previewInput(uuid58)}`, "invalid_character");
95
+ }
96
+ chunk = chunk * 58 + index;
97
+ }
98
+ num = num * BASE58_CHUNK + BigInt(chunk);
99
+ start = end;
100
+ }
101
+ if (num > MAX_UINT128) return new Uuid58DecodeError(`Decoded value exceeds 128 bits of UUID: ${previewInput(uuid58)}`, "overflow");
102
+ return num;
103
+ }
104
+ /**
93
105
  * Converts a 22-character Base58-encoded string back to a standard UUID format, but instead of
94
106
  * throwing an error for invalid input, it returns an `Uuid58DecodeError`
95
107
  * instance.
@@ -102,19 +114,13 @@ _Uuid58DecodeError.prototype.name = "Uuid58DecodeError";
102
114
  * @example
103
115
  * ```typescript
104
116
  * const uuid = uuid58DecodeSafe("XDY9dmBbcMBXqcRvYw8xJ2"); // returns "f4b247fd-1f87-45d4-aa06-1c6fc0a8dfaf"
105
- * const error = uuid58DecodeSafe("invalid"); // returns Uuid58DecodeError
117
+ * const error = uuid58DecodeSafe("invalid"); // returns Uuid58DecodeError (kind: "invalid_length")
106
118
  * ```
107
119
  */
108
120
  function uuid58DecodeSafe(uuid58) {
109
- if (uuid58.length !== 22) return new Uuid58DecodeError(`Expected Base58 string of length 22, but received string of length ${uuid58.length}: ${previewInput(uuid58)}`);
110
- let num = 0n;
111
- for (const char of uuid58) {
112
- const index = BASE58_MAP[char];
113
- if (index === void 0) return new Uuid58DecodeError(`Invalid Base58 character ${JSON.stringify(char)} found in input: ${previewInput(uuid58)}`);
114
- num = num * UUID58_LENGTH + index;
115
- }
121
+ const num = decodeUint128(uuid58);
122
+ if (typeof num !== "bigint") return num;
116
123
  const hex = num.toString(16).padStart(32, "0");
117
- if (hex.length !== 32) return new Uuid58DecodeError(`Decoded value exceeds 128 bits of UUID: ${previewInput(uuid58)}`);
118
124
  return hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-" + hex.slice(12, 16) + "-" + hex.slice(16, 20) + "-" + hex.slice(20);
119
125
  }
120
126
  /**
@@ -137,18 +143,44 @@ function uuid58Decode(uuid58) {
137
143
  // encode.ts
138
144
  var _Uuid58EncodeError;
139
145
  const HEX32_REGEX = /^[0-9a-f]{32}$/i;
146
+ const HYPHENATED_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
140
147
  /**
141
- * Error thrown when an invalid UUID string is provided for encoding.
142
- * This includes strings that don't match the standard UUID format (32 hexadecimal characters with optional hyphens).
148
+ * Error thrown when an invalid UUID string or byte array is provided for
149
+ * encoding. Its {@link Uuid58EncodeError.kind | kind} tells why.
143
150
  */
144
- var Uuid58EncodeError = class extends Error {};
151
+ var Uuid58EncodeError = class extends Error {
152
+ constructor(message, kind) {
153
+ super(message);
154
+ this.kind = kind;
155
+ }
156
+ };
145
157
  _Uuid58EncodeError = Uuid58EncodeError;
146
158
  _Uuid58EncodeError.prototype.name = "Uuid58EncodeError";
147
159
  /**
160
+ * @internal
161
+ * Parses a UUID string in one of the two forms of the specification
162
+ * (Section 5.1) into an unsigned 128-bit integer.
163
+ */
164
+ function parseUuidString(uuid) {
165
+ let hex;
166
+ if (uuid.length === 32) {
167
+ if (!HEX32_REGEX.test(uuid)) return new Uuid58EncodeError(`Invalid UUID: ${previewInput(uuid)} must be 32 hexadecimal digits`, "invalid_character");
168
+ hex = uuid;
169
+ } else if (uuid.length === 36) {
170
+ if (!HYPHENATED_REGEX.test(uuid)) return new Uuid58EncodeError(`Invalid UUID: ${previewInput(uuid)} must be in the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, "invalid_character");
171
+ hex = uuid.slice(0, 8) + uuid.slice(9, 13) + uuid.slice(14, 18) + uuid.slice(19, 23) + uuid.slice(24);
172
+ } else return new Uuid58EncodeError(`Invalid UUID length: expected 32 or 36 characters, got ${uuid.length} in ${previewInput(uuid)}`, "invalid_length");
173
+ return BigInt("0x" + hex);
174
+ }
175
+ /**
148
176
  * Converts a standard UUID string to a 22-character Base58-encoded format, but instead of
149
177
  * throwing an error for invalid input, it returns an `Uuid58EncodeError` instance.
150
178
  *
151
- * @param uuid - The UUID string to encode (with or without hyphens)
179
+ * Only two forms are accepted, in any letter case: the hyphenated form
180
+ * (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) and 32 hexadecimal digits. Braces,
181
+ * `urn:uuid:`, whitespace, and hyphens in other positions are rejected.
182
+ *
183
+ * @param uuid - The UUID string to encode (hyphenated, or 32 hexadecimal digits)
152
184
  * @returns A 22-character Base58-encoded string, or an `Uuid58EncodeError` if the input is
153
185
  * not a valid UUID
154
186
  * @note This function does not throw; it returns the error object instead.
@@ -162,27 +194,20 @@ _Uuid58EncodeError.prototype.name = "Uuid58EncodeError";
162
194
  * // Uppercase UUID is also accepted
163
195
  * const base58 = uuid58EncodeSafe("F4B247FD-1F87-45D4-AA06-1C6FC0A8DFAF"); // returns "XDY9dmBbcMBXqcRvYw8xJ2"
164
196
  * // Invalid UUID format
165
- * const error = uuid58EncodeSafe("invalid"); // returns Uuid58EncodeError
197
+ * const error = uuid58EncodeSafe("invalid"); // returns Uuid58EncodeError (kind: "invalid_length")
166
198
  * ```
167
199
  */
168
200
  function uuid58EncodeSafe(uuid) {
169
- const hex = uuid.replace(/-/g, "");
170
- if (hex.length !== 32) return new Uuid58EncodeError(`Invalid UUID length: expected 32 characters (excluding hyphens), got ${hex.length} characters in ${previewInput(uuid)}`);
171
- if (!HEX32_REGEX.test(hex)) return new Uuid58EncodeError(`Invalid UUID format: ${previewInput(uuid)} contains non-hexadecimal characters`);
172
- let num = BigInt("0x" + hex);
173
- let encoded = "";
174
- do {
175
- encoded = UUID58_ALPHABET[Number(num % UUID58_LENGTH)] + encoded;
176
- num /= UUID58_LENGTH;
177
- } while (num > 0n);
178
- return encoded.padStart(22, UUID58_ALPHABET[0]);
201
+ const num = parseUuidString(uuid);
202
+ return typeof num === "bigint" ? encodeUint128(num) : num;
179
203
  }
180
204
  /**
181
205
  * Converts a standard UUID string to a 22-character Base58-encoded format.
182
206
  *
183
- * @param uuid - The UUID string to encode. Can be provided with or without hyphens
184
- * (format: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" or "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").
185
- * The input is case-insensitive, so both uppercase and lowercase hexadecimal characters are accepted.
207
+ * @param uuid - The UUID string to encode, either hyphenated
208
+ * ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") or as 32 hexadecimal digits
209
+ * ("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"). Hexadecimal digits may be in any case.
210
+ * Other forms, including hyphens in other positions, are rejected.
186
211
  * @returns A 22-character Base58-encoded string representation of the UUID
187
212
  * @throws {Uuid58EncodeError} If the input string is not a valid UUID format
188
213
  *
@@ -201,7 +226,131 @@ function uuid58Encode(uuid) {
201
226
  if (result instanceof Uuid58EncodeError) throw result;
202
227
  return result;
203
228
  }
229
+ // bytes.ts
230
+ /**
231
+ * Encodes 16 bytes, in big-endian (RFC 9562) order, as a 22-character uuid58
232
+ * string, but instead of throwing an error for invalid input, it returns an
233
+ * `Uuid58EncodeError` instance.
234
+ *
235
+ * @param bytes - The 16 bytes of the UUID
236
+ * @returns A 22-character uuid58 string, or an `Uuid58EncodeError` (kind
237
+ * `"invalid_length"`) if `bytes` is not exactly 16 bytes long
238
+ * @note This function does not throw; it returns the error object instead.
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const bytes = new Uint8Array(16); // nil UUID
243
+ * uuid58EncodeBytesSafe(bytes); // returns "1111111111111111111111"
244
+ * ```
245
+ */
246
+ function uuid58EncodeBytesSafe(bytes) {
247
+ if (bytes.length !== 16) return new Uuid58EncodeError(`Invalid byte length: expected 16 bytes, got ${bytes.length}`, "invalid_length");
248
+ const view = new DataView(bytes.buffer, bytes.byteOffset, 16);
249
+ return encodeUint128(view.getBigUint64(0) << 64n | view.getBigUint64(8));
250
+ }
251
+ /**
252
+ * Encodes 16 bytes, in big-endian (RFC 9562) order, as a 22-character uuid58
253
+ * string.
254
+ *
255
+ * @param bytes - The 16 bytes of the UUID
256
+ * @returns A 22-character uuid58 string
257
+ * @throws {Uuid58EncodeError} If `bytes` is not exactly 16 bytes long
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * uuid58EncodeBytes(new Uint8Array(16)); // returns "1111111111111111111111"
262
+ * ```
263
+ */
264
+ function uuid58EncodeBytes(bytes) {
265
+ const result = uuid58EncodeBytesSafe(bytes);
266
+ if (result instanceof Uuid58EncodeError) throw result;
267
+ return result;
268
+ }
269
+ /**
270
+ * Decodes a uuid58 string to 16 bytes in big-endian (RFC 9562) order, but
271
+ * instead of throwing an error for invalid input, it returns an
272
+ * `Uuid58DecodeError` instance.
273
+ *
274
+ * @param uuid58 - The 22-character uuid58 string to decode
275
+ * @returns A new 16-byte `Uint8Array`, or an `Uuid58DecodeError`
276
+ * @note This function does not throw; it returns the error object instead.
277
+ *
278
+ * @example
279
+ * ```typescript
280
+ * uuid58DecodeBytesSafe("1111111111111111111111"); // returns Uint8Array(16) of zeros
281
+ * ```
282
+ */
283
+ function uuid58DecodeBytesSafe(uuid58) {
284
+ const num = decodeUint128(uuid58);
285
+ if (typeof num !== "bigint") return num;
286
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
287
+ const view = new DataView(bytes.buffer);
288
+ view.setBigUint64(0, num >> 64n);
289
+ view.setBigUint64(8, num & 18446744073709551615n);
290
+ return bytes;
291
+ }
292
+ /**
293
+ * Decodes a uuid58 string to 16 bytes in big-endian (RFC 9562) order.
294
+ *
295
+ * @param uuid58 - The 22-character uuid58 string to decode
296
+ * @returns A new 16-byte `Uint8Array`
297
+ * @throws {Uuid58DecodeError} If the input is not a valid uuid58 string
298
+ *
299
+ * @example
300
+ * ```typescript
301
+ * const bytes = uuid58DecodeBytes("XDY9dmBbcMBXqcRvYw8xJ2");
302
+ * ```
303
+ */
304
+ function uuid58DecodeBytes(uuid58) {
305
+ const result = uuid58DecodeBytesSafe(uuid58);
306
+ if (result instanceof Uuid58DecodeError) throw result;
307
+ return result;
308
+ }
309
+ // check.ts
310
+ const MAX_UUID58 = "YcVfxkQb6JRzqk5kF2tNLv";
311
+ /**
312
+ * Regular expression pattern for validating UUID58 strings.
313
+ *
314
+ * This regex ensures that a string:
315
+ * - Contains exactly 22 characters
316
+ * - Uses only characters from the Base58 alphabet
317
+ *
318
+ * **Note:** This regex does NOT validate that the decoded value fits within
319
+ * 128 bits (UUID size). For complete validation including 128-bit range
320
+ * checking, use {@link isUuid58} instead.
321
+ *
322
+ * @example
323
+ * ```typescript
324
+ * const isValid = UUID58_REGEX.test("XDY9dmBbcMBXqcRvYw8xJ2"); // true
325
+ * const isValid = UUID58_REGEX.test("invalid"); // false
326
+ * const isValid = UUID58_REGEX.test("123"); // false (too short)
327
+ * ```
328
+ */
329
+ const UUID58_REGEX = /^[1-9A-HJ-NP-Za-km-z]{22}$/u;
330
+ /**
331
+ * Checks if a given string is a valid UUID58-encodable string.
332
+ * This function efficiently validates without performing full decoding.
333
+ *
334
+ * A valid UUID58 string must:
335
+ * - Be exactly 22 characters long
336
+ * - Contain only characters from the Base58 alphabet
337
+ * - Decode to a value that fits within 128 bits (UUID size)
338
+ *
339
+ * @param uuid58 - The string to check
340
+ * @returns `true` if the string is a valid UUID58 string, `false` otherwise
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * isUuid58("XDY9dmBbcMBXqcRvYw8xJ2"); // true
345
+ * isUuid58("invalid"); // false
346
+ * isUuid58("O0lI"); // false (contains invalid characters)
347
+ * ```
348
+ */
349
+ function isUuid58(uuid58) {
350
+ return UUID58_REGEX.test(uuid58) && uuid58 <= MAX_UUID58;
351
+ }
204
352
  // generate.ts
353
+ const random = /* @__PURE__ */ new BigUint64Array(2);
205
354
  /**
206
355
  * Generates a new Base58-encoded UUID (always 22 characters).
207
356
  *
@@ -216,15 +365,9 @@ function uuid58Encode(uuid) {
216
365
  * ```
217
366
  */
218
367
  function uuid58() {
219
- const bytes = crypto.getRandomValues(/* @__PURE__ */ new BigUint64Array(2));
220
- bytes[0] = bytes[0] & 18446744073709490175n | 16384n;
221
- bytes[1] = bytes[1] & 4611686018427387903n | 9223372036854775808n;
222
- let num = bytes[0] << 64n | bytes[1];
223
- let encoded = "";
224
- do {
225
- encoded = UUID58_ALPHABET[Number(num % UUID58_LENGTH)] + encoded;
226
- num /= UUID58_LENGTH;
227
- } while (num > 0n);
228
- return encoded.padStart(22, UUID58_ALPHABET[0]);
368
+ crypto.getRandomValues(random);
369
+ const high = random[0] & 18446744073709490175n | 16384n;
370
+ const low = random[1] & 4611686018427387903n | 9223372036854775808n;
371
+ return encodeUint128(high << 64n | low);
229
372
  }
230
- export { UUID58_ALPHABET, UUID58_REGEX, Uuid58DecodeError, Uuid58EncodeError, isUuid58, uuid58, uuid58Decode, uuid58DecodeSafe, uuid58Encode, uuid58EncodeSafe };
373
+ export { UUID58_ALPHABET, UUID58_REGEX, Uuid58DecodeError, Uuid58EncodeError, isUuid58, uuid58, uuid58Decode, uuid58DecodeBytes, uuid58DecodeBytesSafe, uuid58DecodeSafe, uuid58Encode, uuid58EncodeBytes, uuid58EncodeBytesSafe, uuid58EncodeSafe };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nakanoaas/uuid58",
3
- "version": "1.2.2",
3
+ "version": "2.0.0",
4
4
  "description": "A tiny, zero-dependency library for generating and converting UUIDs to Base58-encoded strings.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/nakanoasaservice/uuid58/issues"
@@ -8,7 +8,8 @@
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/nakanoasaservice/uuid58.git"
11
+ "url": "git+https://github.com/nakanoasaservice/uuid58.git",
12
+ "directory": "js"
12
13
  },
13
14
  "files": [
14
15
  "dist"
@@ -35,7 +36,8 @@
35
36
  "build": "vp pack",
36
37
  "check": "vp check",
37
38
  "test": "vp test run",
38
- "bench": "vp test bench --run",
39
+ "bench": "vp pack && vp test bench --run --reporter=default",
40
+ "bench:baseline": "sh bench/save-baseline.sh",
39
41
  "jsr:dry-run": "vp pack && jsr publish --dry-run"
40
42
  },
41
43
  "devDependencies": {