mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
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:
committed by
Meghan Denny
parent
4a031804a3
commit
aea99c1757
50
packages/bun-types/bun.d.ts
vendored
50
packages/bun-types/bun.d.ts
vendored
@@ -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`
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
382
test/js/bun/util/randomUUIDv5.test.ts
Normal file
382
test/js/bun/util/randomUUIDv5.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user