diff --git a/llrt_core/Cargo.toml b/llrt_core/Cargo.toml index fba0af4ce8..93ffe39883 100644 --- a/llrt_core/Cargo.toml +++ b/llrt_core/Cargo.toml @@ -39,6 +39,8 @@ uuid = { version = "1.10.0", default-features = false, features = [ "v3", "v4", "v5", + "v6", + "v7", "fast-rng", ] } once_cell = "1.19.0" diff --git a/llrt_core/src/modules/llrt/uuid.rs b/llrt_core/src/modules/llrt/uuid.rs index 45bd2fcda9..05ca581ca1 100644 --- a/llrt_core/src/modules/llrt/uuid.rs +++ b/llrt_core/src/modules/llrt/uuid.rs @@ -21,6 +21,8 @@ use crate::{ pub struct LlrtUuidModule; +const MAX_UUID: &str = "ffffffff-ffff-ffff-ffff-ffffffffffff"; + static ERROR_MESSAGE: &str = "Not a valid UUID"; static NODE_ID: Lazy<[u8; 6]> = Lazy::new(|| { @@ -60,6 +62,64 @@ pub fn uuidv4() -> String { Uuid::new_v4().format_hyphenated().to_string() } +fn uuidv6() -> String { + Uuid::now_v6(&NODE_ID).format_hyphenated().to_string() +} + +fn uuidv7() -> String { + Uuid::now_v7().format_hyphenated().to_string() +} + +fn uuidv1_to_v6<'js>(ctx: Ctx<'js>, v1_value: Value<'js>) -> Result { + let v1_uuid = from_value(&ctx, v1_value)?; + let v1_bytes = v1_uuid.as_bytes(); + let mut v6_bytes = [0u8; 16]; + + // time_high + v6_bytes[0] = ((v1_bytes[6] & 0x0f) << 4) | ((v1_bytes[7] & 0xf0) >> 4); + v6_bytes[1] = ((v1_bytes[7] & 0x0f) << 4) | ((v1_bytes[4] & 0xf0) >> 4); + v6_bytes[2] = ((v1_bytes[4] & 0x0f) << 4) | ((v1_bytes[5] & 0xf0) >> 4); + v6_bytes[3] = ((v1_bytes[5] & 0x0f) << 4) | ((v1_bytes[0] & 0xf0) >> 4); + + // time_mid + v6_bytes[4] = ((v1_bytes[0] & 0x0f) << 4) | ((v1_bytes[1] & 0xf0) >> 4); + v6_bytes[5] = ((v1_bytes[1] & 0x0f) << 4) | ((v1_bytes[2] & 0xf0) >> 4); + + // version and time_low + v6_bytes[6] = 0x60 | (v1_bytes[2] & 0x0f); + v6_bytes[7] = v1_bytes[3]; + + // clock_seq and node + v6_bytes[8..16].copy_from_slice(&v1_bytes[8..16]); + + Ok(Uuid::from_bytes(v6_bytes).format_hyphenated().to_string()) +} + +fn uuidv6_to_v1<'js>(ctx: Ctx<'js>, v6_value: Value<'js>) -> Result { + let v6_uuid = from_value(&ctx, v6_value)?; + let v6_bytes: &[u8; 16] = v6_uuid.as_bytes(); + let mut v1_bytes = [0u8; 16]; + + // time_low + v1_bytes[0] = (v6_bytes[3] & 0x0f) << 4 | (v6_bytes[4] & 0xf0) >> 4; + v1_bytes[1] = (v6_bytes[4] & 0x0f) << 4 | (v6_bytes[5] & 0xf0) >> 4; + v1_bytes[2] = (v6_bytes[5] & 0x0f) << 4 | (v6_bytes[6] & 0x0f); + v1_bytes[3] = v6_bytes[7]; + + // time_mid + v1_bytes[4] = (v6_bytes[1] & 0x0f) << 4 | (v6_bytes[2] & 0xf0) >> 4; + v1_bytes[5] = (v6_bytes[2] & 0x0f) << 4 | (v6_bytes[3] & 0xf0) >> 4; + + // version and time_high + v1_bytes[6] = 0x10 | (v6_bytes[0] & 0xf0) >> 4; + v1_bytes[7] = (v6_bytes[0] & 0x0f) << 4 | (v6_bytes[1] & 0xf0) >> 4; + + // clock_seq and node + v1_bytes[8..16].copy_from_slice(&v6_bytes[8..16]); + + Ok(Uuid::from_bytes(v1_bytes).format_hyphenated().to_string()) +} + fn parse(ctx: Ctx<'_>, value: String) -> Result> { let uuid = Uuid::try_parse(&value).or_throw_msg(&ctx, ERROR_MESSAGE)?; let bytes = uuid.as_bytes(); @@ -88,6 +148,11 @@ fn validate(value: String) -> bool { } fn version(ctx: Ctx<'_>, value: String) -> Result { + // the Node.js uuid package returns 15 for the version of MAX + // https://github.com/uuidjs/uuid?tab=readme-ov-file#uuidversionstr + if value == MAX_UUID { + return Ok(15); + } let uuid = Uuid::parse_str(&value).or_throw_msg(&ctx, ERROR_MESSAGE)?; Ok(uuid.get_version().map(|v| v as u8).unwrap_or(0)) } @@ -98,11 +163,16 @@ impl ModuleDef for LlrtUuidModule { declare.declare("v3")?; declare.declare("v4")?; declare.declare("v5")?; + declare.declare("v6")?; + declare.declare("v7")?; + declare.declare("v1ToV6")?; + declare.declare("v6ToV1")?; declare.declare("parse")?; declare.declare("validate")?; declare.declare("stringify")?; declare.declare("version")?; declare.declare("NIL")?; + declare.declare("MAX")?; declare.declare("default")?; Ok(()) @@ -128,7 +198,12 @@ impl ModuleDef for LlrtUuidModule { default.set("v3", v3_func)?; default.set("v4", Func::from(uuidv4))?; default.set("v5", v5_func)?; + default.set("v6", Func::from(uuidv6))?; + default.set("v7", Func::from(uuidv7))?; + default.set("v1ToV6", Func::from(uuidv1_to_v6))?; + default.set("v6ToV1", Func::from(uuidv6_to_v1))?; default.set("NIL", "00000000-0000-0000-0000-000000000000")?; + default.set("MAX", MAX_UUID)?; default.set("parse", Func::from(parse))?; default.set("stringify", Func::from(stringify))?; default.set("validate", Func::from(validate))?; diff --git a/tests/unit/uuid.test.ts b/tests/unit/uuid.test.ts index 5b32c2ff1e..35eadb15c1 100644 --- a/tests/unit/uuid.test.ts +++ b/tests/unit/uuid.test.ts @@ -4,10 +4,15 @@ import { v3 as uuidv3, v4 as uuidv4, v5 as uuidv5, + v6 as uuidv6, + v7 as uuidv7, + v1ToV6 as uuidv1ToV6, + v6ToV1 as uuidv6ToV1, parse, stringify, validate, NIL, + MAX, version, } from "llrt:uuid"; @@ -47,6 +52,35 @@ describe("UUID Generation", () => { expect(version(uuid)).toEqual(5); }); + it("should generate a valid v6 UUID", () => { + const uuid = uuidv6(); + expect(typeof uuid).toEqual("string"); + expect(uuid.length).toEqual(36); + expect(uuid).toMatch(UUID_PATTERN); + expect(version(uuid)).toEqual(6); + }) + + it("should generate a valid v7 UUID", () => { + const uuid = uuidv7(); + expect(typeof uuid).toEqual("string"); + expect(uuid.length).toEqual(36); + expect(uuid).toMatch(UUID_PATTERN); + expect(version(uuid)).toEqual(7); + }) + + it("should convert v1 -> v6 and vice versa", () => { + const v1 = "f4df6856-5238-11ef-a311-d4807f27f0c6" + const v6 = "1ef5238f-4df6-6856-a311-d4807f27f0c6" + + const convertedv6 = uuidv1ToV6(v1) + expect(convertedv6).toEqual(v6) + expect(version(convertedv6)).toEqual(6) + + const convertedv1 = uuidv6ToV1(convertedv6) + expect(convertedv1).toEqual(v1) + expect(version(convertedv1)).toEqual(1) + }) + it("should parse and stringify a UUID", () => { const uuid = uuidv1(); const parsedUuid = parse(uuid); @@ -71,15 +105,26 @@ describe("UUID Generation", () => { expect(version(nilUuid)).toEqual(0); }); + it("should generate a MAX UUID", () => { + const maxUuid = MAX; + expect(maxUuid).toEqual("ffffffff-ffff-ffff-ffff-ffffffffffff"); + expect(version(maxUuid)).toEqual(15); + }); + it("should return correct versions", () => { const v1 = uuidv1(); const v3 = uuidv3("hello", uuidv3.URL); const v4 = uuidv4(); const v5 = uuidv5("hello", uuidv3.URL); + const v6 = uuidv6(); + const v7 = uuidv7(); expect(version(v1)).toEqual(1); expect(version(v3)).toEqual(3); expect(version(v4)).toEqual(4); expect(version(v5)).toEqual(5); + expect(version(v6)).toEqual(6); + expect(version(v7)).toEqual(7); expect(version(NIL)).toEqual(0); + expect(version(MAX)).toEqual(15); }); });