Introduce Bun.randomUUIDv5 (#20782)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jarred <jarred@bun.sh>
Co-authored-by: Meghan Denny <meghan@bun.sh>
This commit is contained in:
Jarred Sumner
2025-07-02 22:47:14 -07:00
committed by Meghan Denny
parent 4a031804a3
commit aea99c1757
8 changed files with 608 additions and 3 deletions

View File

@@ -7717,6 +7717,56 @@ declare module "bun" {
timestamp?: number | Date,
): Buffer;
/**
* Generate a UUIDv5, which is a name-based UUID based on the SHA-1 hash of a namespace UUID and a name.
*
* @param name The name to use for the UUID
* @param namespace The namespace to use for the UUID
* @param encoding The encoding to use for the UUID
*
*
* @example
* ```js
* import { randomUUIDv5 } from "bun";
* const uuid = randomUUIDv5("www.example.com", "dns");
* console.log(uuid); // "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
* ```
*
* ```js
* import { randomUUIDv5 } from "bun";
* const uuid = randomUUIDv5("www.example.com", "url");
* console.log(uuid); // "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
* ```
*/
function randomUUIDv5(
name: string | BufferSource,
namespace: string | BufferSource | "dns" | "url" | "oid" | "x500",
/**
* @default "hex"
*/
encoding?: "hex" | "base64" | "base64url",
): string;
/**
* Generate a UUIDv5 as a Buffer
*
* @param name The name to use for the UUID
* @param namespace The namespace to use for the UUID
* @param encoding The encoding to use for the UUID
*
* @example
* ```js
* import { randomUUIDv5 } from "bun";
* const uuid = randomUUIDv5("www.example.com", "url", "buffer");
* console.log(uuid); // <Buffer 6b a7 b8 11 9d ad 11 d1 80 b4 00 c0 4f d4 30 c8>
* ```
*/
function randomUUIDv5(
name: string | BufferSource,
namespace: string | BufferSource | "dns" | "url" | "oid" | "x500",
encoding: "buffer",
): Buffer;
/**
* Types for `bun.lock`
*/

View File

@@ -1,4 +1,3 @@
#include "root.h"
#include "JavaScriptCore/HeapProfiler.h"
@@ -71,6 +70,7 @@ BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__cancel);
BUN_DECLARE_HOST_FUNCTION(Bun__fetch);
BUN_DECLARE_HOST_FUNCTION(Bun__fetchPreconnect);
BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv7);
BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv5);
using namespace JSC;
using namespace WebCore;
@@ -758,6 +758,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
peek constructBunPeekObject DontDelete|PropertyCallback
plugin constructPluginObject ReadOnly|DontDelete|PropertyCallback
randomUUIDv7 Bun__randomUUIDv7 DontDelete|Function 2
randomUUIDv5 Bun__randomUUIDv5 DontDelete|Function 3
readableStreamToArray JSBuiltin Builtin|Function 1
readableStreamToArrayBuffer JSBuiltin Builtin|Function 1
readableStreamToBytes JSBuiltin Builtin|Function 1

View File

@@ -205,3 +205,81 @@ pub const UUID7 = struct {
return self.toUUID().format(layout, options, writer);
}
};
/// UUID v5 implementation using SHA-1 hashing
/// This is a name-based UUID that uses SHA-1 for hashing
pub const UUID5 = struct {
bytes: [16]u8,
pub const namespaces = struct {
pub const dns: *const [16]u8 = &.{ 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8 };
pub const url: *const [16]u8 = &.{ 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8 };
pub const oid: *const [16]u8 = &.{ 0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8 };
pub const x500: *const [16]u8 = &.{ 0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8 };
pub fn get(namespace: []const u8) ?*const [16]u8 {
if (bun.strings.eqlCaseInsensitiveASCII(namespace, "dns", true)) {
return dns;
} else if (bun.strings.eqlCaseInsensitiveASCII(namespace, "url", true)) {
return url;
} else if (bun.strings.eqlCaseInsensitiveASCII(namespace, "oid", true)) {
return oid;
} else if (bun.strings.eqlCaseInsensitiveASCII(namespace, "x500", true)) {
return x500;
}
return null;
}
};
/// Generate a UUID v5 from a namespace UUID and name data
pub fn init(namespace: *const [16]u8, name: []const u8) UUID5 {
const hash = brk: {
var sha1_hasher = bun.sha.SHA1.init();
defer sha1_hasher.deinit();
sha1_hasher.update(namespace);
sha1_hasher.update(name);
var hash: [20]u8 = undefined;
sha1_hasher.final(&hash);
break :brk hash;
};
// Take first 16 bytes of the hash
var bytes: [16]u8 = hash[0..16].*;
// Set version to 5 (bits 12-15 of time_hi_and_version)
bytes[6] = (bytes[6] & 0x0F) | 0x50;
// Set variant bits (bits 6-7 of clock_seq_hi_and_reserved)
bytes[8] = (bytes[8] & 0x3F) | 0x80;
return UUID5{
.bytes = bytes,
};
}
pub fn toBytes(self: UUID5) [16]u8 {
return self.bytes;
}
pub fn print(self: UUID5, buf: *[36]u8) void {
return printBytes(&self.toBytes(), buf);
}
pub fn toUUID(self: UUID5) UUID {
const bytes: [16]u8 = self.toBytes();
return .{ .bytes = bytes };
}
pub fn format(
self: UUID5,
comptime layout: []const u8,
options: fmt.FormatOptions,
writer: anytype,
) !void {
return self.toUUID().format(layout, options, writer);
}
};

View File

@@ -163,6 +163,93 @@ pub fn Bun__randomUUIDv7_(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallF
return encoding.encodeWithMaxSize(globalThis, 32, &uuid.bytes);
}
comptime {
const Bun__randomUUIDv5 = JSC.toJSHostFn(Bun__randomUUIDv5_);
@export(&Bun__randomUUIDv5, .{ .name = "Bun__randomUUIDv5" });
}
pub fn Bun__randomUUIDv5_(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments: []const JSC.JSValue = callframe.argumentsUndef(3).slice();
if (arguments.len == 0 or arguments[0].isUndefinedOrNull()) {
return globalThis.ERR(.INVALID_ARG_TYPE, "The \"name\" argument must be specified", .{}).throw();
}
if (arguments.len < 2 or arguments[1].isUndefinedOrNull()) {
return globalThis.ERR(.INVALID_ARG_TYPE, "The \"namespace\" argument must be specified", .{}).throw();
}
const encoding: JSC.Node.Encoding = brk: {
if (arguments.len > 2 and !arguments[2].isUndefined()) {
if (arguments[2].isString()) {
break :brk try JSC.Node.Encoding.fromJS(arguments[2], globalThis) orelse {
return globalThis.ERR(.UNKNOWN_ENCODING, "Encoding must be one of base64, base64url, hex, or buffer", .{}).throw();
};
}
}
break :brk JSC.Node.Encoding.hex;
};
const name_value = arguments[0];
const namespace_value = arguments[1];
const name = brk: {
if (name_value.isString()) {
const name_str = try name_value.toBunString(globalThis);
defer name_str.deref();
const result = name_str.toUTF8(bun.default_allocator);
break :brk result;
} else if (name_value.asArrayBuffer(globalThis)) |array_buffer| {
break :brk JSC.ZigString.Slice.fromUTF8NeverFree(array_buffer.byteSlice());
} else {
return globalThis.ERR(.INVALID_ARG_TYPE, "The \"name\" argument must be of type string or BufferSource", .{}).throw();
}
};
defer name.deinit();
const namespace = brk: {
if (namespace_value.isString()) {
const namespace_str = try namespace_value.toBunString(globalThis);
defer namespace_str.deref();
const namespace_slice = namespace_str.toUTF8(bun.default_allocator);
defer namespace_slice.deinit();
if (namespace_slice.slice().len != 36) {
if (UUID5.namespaces.get(namespace_slice.slice())) |namespace| {
break :brk namespace.*;
}
return globalThis.ERR(.INVALID_ARG_VALUE, "Invalid UUID format for namespace", .{}).throw();
}
const parsed_uuid = UUID.parse(namespace_slice.slice()) catch {
return globalThis.ERR(.INVALID_ARG_VALUE, "Invalid UUID format for namespace", .{}).throw();
};
break :brk parsed_uuid.bytes;
} else if (namespace_value.asArrayBuffer(globalThis)) |*array_buffer| {
const slice = array_buffer.byteSlice();
if (slice.len != 16) {
return globalThis.ERR(.INVALID_ARG_VALUE, "Namespace must be exactly 16 bytes", .{}).throw();
}
break :brk slice[0..16].*;
}
return globalThis.ERR(.INVALID_ARG_TYPE, "The \"namespace\" argument must be a string or buffer", .{}).throw();
};
const uuid = UUID5.init(&namespace, name.slice());
if (encoding == .hex) {
var str, var bytes = bun.String.createUninitialized(.latin1, 36);
uuid.print(bytes[0..36]);
return str.transferToJS(globalThis);
}
return encoding.encodeWithMaxSize(globalThis, 32, &uuid.bytes);
}
pub fn randomUUIDWithoutTypeChecks(
_: *Crypto,
globalThis: *JSC.JSGlobalObject,
@@ -193,6 +280,8 @@ pub export fn CryptoObject__create(globalThis: *JSC.JSGlobalObject) JSC.JSValue
}
const UUID7 = @import("../uuid.zig").UUID7;
const UUID = @import("../uuid.zig");
const UUID5 = @import("../uuid.zig").UUID5;
const std = @import("std");
const bun = @import("bun");

View File

@@ -84,7 +84,8 @@
"typeorm": "0.3.20",
"typescript": "5.0.2",
"undici": "5.20.0",
"unzipper": "^0.12.3",
"unzipper": "0.12.3",
"uuid": "11.1.0",
"v8-heapsnapshot": "1.3.1",
"verdaccio": "6.0.0",
"vitest": "0.32.2",
@@ -2543,7 +2544,7 @@
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"v8-compile-cache": ["v8-compile-cache@2.4.0", "", {}, "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw=="],
@@ -3207,6 +3208,8 @@
"typeorm/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typeorm/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"unbzip2-stream/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"unstorage/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],

View File

@@ -0,0 +1,382 @@
import { describe, expect, test } from "bun:test";
import * as uuid from "uuid";
describe("randomUUIDv5", () => {
const dnsNamespace = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
const urlNamespace = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
test("basic functionality", () => {
const result = Bun.randomUUIDv5("www.example.com", dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
// Check that it's version 5
expect(result[14]).toBe("5");
});
test("deterministic output", () => {
const uuid1 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
// Should always generate the same UUID for the same namespace + name
expect(uuid1).toBe(uuid2);
});
test("compatibility with uuid library", () => {
const name = "www.example.com";
const bunUuid = Bun.randomUUIDv5(name, dnsNamespace);
const uuidLibUuid = uuid.v5(name, dnsNamespace);
expect(bunUuid).toBe(uuidLibUuid);
});
test("predefined namespace strings", () => {
// Test with predefined namespace strings
const uuid1 = Bun.randomUUIDv5("www.example.com", "dns");
const uuid2 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
expect(uuid1).toBe(uuid2);
const uuid3 = Bun.randomUUIDv5("http://example.com", "url");
const uuid4 = Bun.randomUUIDv5("http://example.com", urlNamespace);
expect(uuid3).toBe(uuid4);
});
test("empty name", () => {
const result = Bun.randomUUIDv5("", dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
});
test("long name", () => {
const longName = "a".repeat(1000);
const result = Bun.randomUUIDv5(longName, dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
});
test("unicode name", () => {
const unicodeName = "测试.example.com";
const result = Bun.randomUUIDv5(unicodeName, dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
// Should be deterministic
const uuid2 = Bun.randomUUIDv5(unicodeName, dnsNamespace);
expect(result).toBe(uuid2);
});
test("name as ArrayBuffer", () => {
const nameString = "test";
const nameBuffer = new TextEncoder().encode(nameString);
const uuid1 = Bun.randomUUIDv5(nameString, dnsNamespace);
const uuid2 = Bun.randomUUIDv5(nameBuffer, dnsNamespace);
expect(uuid1).toBe(uuid2);
});
test("name as TypedArray", () => {
const nameString = "test";
const nameArray = new Uint8Array(new TextEncoder().encode(nameString));
const uuid1 = Bun.randomUUIDv5(nameString, dnsNamespace);
const uuid2 = Bun.randomUUIDv5(nameArray, dnsNamespace);
expect(uuid1).toBe(uuid2);
});
test("error handling - invalid namespace", () => {
expect(() => {
Bun.randomUUIDv5("test", "invalid-uuid");
}).toThrow();
});
test("error handling - wrong namespace buffer size", () => {
const wrongSizeBuffer = new Uint8Array(15); // Should be 16 bytes
expect(() => {
Bun.randomUUIDv5("test", wrongSizeBuffer);
}).toThrow();
});
test("error handling - invalid encoding", () => {
expect(() => {
// @ts-expect-error - testing invalid encoding
Bun.randomUUIDv5("test", dnsNamespace, "invalid");
}).toThrow();
});
test("variant bits are correct", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace);
const bytes = result.replace(/-/g, "");
// Extract the variant byte (17th hex character, index 16)
const variantByte = parseInt(bytes.substr(16, 2), 16);
// Variant bits should be 10xxxxxx (0x80-0xBF)
expect(variantByte & 0xc0).toBe(0x80);
});
test("version bits are correct", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace);
const bytes = result.replace(/-/g, "");
// Extract the version byte (13th hex character, index 12)
const versionByte = parseInt(bytes.substr(12, 2), 16);
// Version bits should be 0101xxxx (0x50-0x5F)
expect(versionByte & 0xf0).toBe(0x50);
});
test("case insensitive namespace strings", () => {
const uuid1 = Bun.randomUUIDv5("test", "DNS");
const uuid2 = Bun.randomUUIDv5("test", "dns");
const uuid3 = Bun.randomUUIDv5("test", "Dns");
expect(uuid1).toBe(uuid2);
expect(uuid2).toBe(uuid3);
});
test("all predefined namespaces", () => {
const name = "test";
const dnsUuid = Bun.randomUUIDv5(name, "dns");
const urlUuid = Bun.randomUUIDv5(name, "url");
const oidUuid = Bun.randomUUIDv5(name, "oid");
const x500Uuid = Bun.randomUUIDv5(name, "x500");
// All should be different
expect(dnsUuid).not.toBe(urlUuid);
expect(urlUuid).not.toBe(oidUuid);
expect(oidUuid).not.toBe(x500Uuid);
// All should be valid UUIDs
[dnsUuid, urlUuid, oidUuid, x500Uuid].forEach(result => {
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
expect(result[14]).toBe("5");
});
});
test("different namespaces produce different UUIDs", () => {
const uuid1 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("www.example.com", urlNamespace);
expect(uuid1).not.toBe(uuid2);
expect(uuid.v5("www.example.com", dnsNamespace)).toBe(uuid1);
expect(uuid.v5("www.example.com", urlNamespace)).toBe(uuid2);
});
test("different names produce different UUIDs", () => {
const uuid1 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("api.example.com", dnsNamespace);
expect(uuid1).not.toBe(uuid2);
});
test("hex encoding (default)", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace);
expect(result).toMatch(/^[0-9a-f-]+$/);
expect(result.length).toBe(36); // Standard UUID string length
});
test("buffer encoding", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace, "buffer");
expect(result).toBeInstanceOf(Uint8Array);
expect(result.byteLength).toBe(16);
});
test("base64 encoding", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace, "base64");
expect(result).toBeTypeOf("string");
expect(result).toMatch(/^[A-Za-z0-9+/=]+$/);
});
test("base64url encoding", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace, "base64url");
expect(result).toBeTypeOf("string");
expect(result).toMatch(/^[A-Za-z0-9_-]+$/);
});
test("namespace as Buffer", () => {
// Convert UUID string to buffer
const nsBytes = new Uint8Array(16);
const nsString = dnsNamespace.replace(/-/g, "");
for (let i = 0; i < 16; i++) {
nsBytes[i] = parseInt(nsString.substr(i * 2, 2), 16);
}
const uuid1 = Bun.randomUUIDv5("test", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("test", nsBytes);
expect(uuid1).toBe(uuid2);
});
test("name as Buffer", () => {
const nameBuffer = new TextEncoder().encode("test");
const uuid1 = Bun.randomUUIDv5("test", dnsNamespace);
const uuid2 = Bun.randomUUIDv5(nameBuffer, dnsNamespace);
expect(uuid1).toBe(uuid2);
});
// Ported v5 tests from uuid library test suite
test("v5 - hello.example.com with DNS namespace", () => {
expect(Bun.randomUUIDv5("hello.example.com", dnsNamespace)).toBe("fdda765f-fc57-5604-a269-52a7df8164ec");
});
test("v5 - http://example.com/hello with URL namespace", () => {
expect(Bun.randomUUIDv5("http://example.com/hello", urlNamespace)).toBe("3bbcee75-cecc-5b56-8031-b6641c1ed1f1");
});
test("v5 - hello with custom namespace", () => {
expect(Bun.randomUUIDv5("hello", "0f5abcd1-c194-47f3-905b-2df7263a084b")).toBe(
"90123e1c-7512-523e-bb28-76fab9f2f73d",
);
});
test("v5 namespace.toUpperCase", () => {
expect(Bun.randomUUIDv5("hello.example.com", dnsNamespace.toUpperCase())).toBe(
"fdda765f-fc57-5604-a269-52a7df8164ec",
);
expect(Bun.randomUUIDv5("http://example.com/hello", urlNamespace.toUpperCase())).toBe(
"3bbcee75-cecc-5b56-8031-b6641c1ed1f1",
);
expect(Bun.randomUUIDv5("hello", "0f5abcd1-c194-47f3-905b-2df7263a084b".toUpperCase())).toBe(
"90123e1c-7512-523e-bb28-76fab9f2f73d",
);
});
test("v5 namespace string validation", () => {
expect(() => {
Bun.randomUUIDv5("hello.example.com", "zyxwvuts-rqpo-nmlk-jihg-fedcba000000");
}).toThrow();
expect(() => {
Bun.randomUUIDv5("hello.example.com", "invalid uuid value");
}).toThrow();
expect(Bun.randomUUIDv5("hello.example.com", "00000000-0000-0000-0000-000000000000")).toBeTypeOf("string");
});
test("v5 namespace buffer validation", () => {
expect(() => {
Bun.randomUUIDv5("hello.example.com", new Uint8Array(15));
}).toThrow();
expect(() => {
Bun.randomUUIDv5("hello.example.com", new Uint8Array(17));
}).toThrow();
expect(Bun.randomUUIDv5("hello.example.com", new Uint8Array(16).fill(0))).toBeTypeOf("string");
});
test("v5 fill buffer", () => {
const expectedUuid = Buffer.from([
0xfd, 0xda, 0x76, 0x5f, 0xfc, 0x57, 0x56, 0x04, 0xa2, 0x69, 0x52, 0xa7, 0xdf, 0x81, 0x64, 0xec,
]);
const result = Bun.randomUUIDv5("hello.example.com", dnsNamespace, "buffer");
expect(result.toString("hex")).toEqual(expectedUuid.toString("hex"));
});
test("v5 undefined/null", () => {
// @ts-expect-error testing invalid input
expect(() => Bun.randomUUIDv5()).toThrow();
// @ts-expect-error testing invalid input
expect(() => Bun.randomUUIDv5("hello")).toThrow();
// @ts-expect-error testing invalid input
expect(() => Bun.randomUUIDv5("hello.example.com", undefined)).toThrow();
// @ts-expect-error testing invalid input
expect(() => Bun.randomUUIDv5("hello.example.com", null)).toThrow();
});
test("RFC 4122 test vectors", () => {
// These should be deterministic
const uuid1 = Bun.randomUUIDv5("http://www.example.com/", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("http://www.example.com/", urlNamespace);
// Both should be valid version 5 UUIDs
expect(uuid1).toEqual("b50f73c9-e407-5ea4-8540-70886e8aa2cd");
expect(uuid2).toEqual("fcde3c85-2270-590f-9e7c-ee003d65e0e2");
});
test("error cases", () => {
// Missing namespace
// @ts-expect-error
expect(() => Bun.randomUUIDv5()).toThrow();
// Missing name
// @ts-expect-error
expect(() => Bun.randomUUIDv5(dnsNamespace)).toThrow();
// Invalid namespace format
expect(() => Bun.randomUUIDv5("test", "invalid-uuid")).toThrow();
// Invalid encoding
// @ts-expect-error
expect(() => Bun.randomUUIDv5("test", dnsNamespace, "invalid")).toThrow();
// Namespace buffer wrong size
expect(() => Bun.randomUUIDv5("test", new Uint8Array(10))).toThrow();
});
test("long names", () => {
const longName = "a".repeat(10000);
const result = Bun.randomUUIDv5(longName, dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
});
test("unicode names", () => {
const unicodeName = "测试🌟";
const result = Bun.randomUUIDv5(unicodeName, dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
// Should be deterministic
const uuid2 = Bun.randomUUIDv5(unicodeName, dnsNamespace);
expect(result).toBe(uuid2);
expect(uuid.v5(unicodeName, dnsNamespace)).toBe(result);
});
test("variant bits are set correctly", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace, "buffer");
// Check variant bits (bits 6-7 of clock_seq_hi_and_reserved should be 10)
const variantByte = result[8];
const variantBits = (variantByte & 0xc0) >> 6;
expect(variantBits).toBe(2); // Binary 10
expect(uuid.v5("test", dnsNamespace).replace(/-/g, "")).toEqual(result.toString("hex"));
});
test("url namespace", () => {
const result = Bun.randomUUIDv5("test", "6ba7b811-9dad-11d1-80b4-00c04fd430c8");
expect(result).toBeTypeOf("string");
expect(result).toEqual("da5b8893-d6ca-5c1c-9a9c-91f40a2a3649");
expect(uuid.v5("test", urlNamespace)).toEqual(result);
});
test("dns namespace", () => {
const result = Bun.randomUUIDv5("test", "dns");
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
expect(result).toEqual(uuid.v5("test", uuid.v5.DNS));
});
test("consistent across multiple calls", () => {
const results: string[] = [];
for (let i = 0; i < 100; i++) {
results.push(Bun.randomUUIDv5("consistency-test", dnsNamespace));
}
// All results should be identical
const first = results[0];
for (const result of results) {
expect(result).toBe(first);
}
});
});

View File

@@ -244,6 +244,7 @@ test/js/bun/util/inspect-error.test.js
test/js/bun/util/inspect.test.js
test/js/bun/util/mmap.test.js
test/js/bun/util/password.test.ts
test/js/bun/util/randomUUIDv5.test.ts
test/js/bun/util/readablestreamtoarraybuffer.test.ts
test/js/bun/util/stringWidth.test.ts
test/js/bun/util/text-loader.test.ts

View File

@@ -89,6 +89,7 @@
"typeorm": "0.3.20",
"typescript": "5.0.2",
"undici": "5.20.0",
"uuid": "11.1.0",
"unzipper": "0.12.3",
"v8-heapsnapshot": "1.3.1",
"verdaccio": "6.0.0",