Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement uuid.MAX, uuid.v1ToV6(), uuid.V6(), uuid.v6ToV1(), uuid.V7() #524

Merged
merged 2 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions llrt_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
75 changes: 75 additions & 0 deletions llrt_core/src/modules/llrt/uuid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(|| {
Expand Down Expand Up @@ -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<String> {
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);
richarddavison marked this conversation as resolved.
Show resolved Hide resolved
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<String> {
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<TypedArray<u8>> {
let uuid = Uuid::try_parse(&value).or_throw_msg(&ctx, ERROR_MESSAGE)?;
let bytes = uuid.as_bytes();
Expand Down Expand Up @@ -88,6 +148,11 @@ fn validate(value: String) -> bool {
}

fn version(ctx: Ctx<'_>, value: String) -> Result<u8> {
// 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))
}
Expand All @@ -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(())
Expand All @@ -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))?;
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/uuid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand All @@ -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);
});
});