Skip to content

Commit

Permalink
Implement uuid.MAX, uuid.v1ToV6(), uuid.V6(), uuid.v6ToV1(), uuid.V7() (
Browse files Browse the repository at this point in the history
#524)

* Implement uuid.MAX, uuid.v1ToV6(), uuid.V6(), uuid.v6ToV1(), uuid.V7()

* Add unit tests
  • Loading branch information
kevinmingtarja authored Aug 4, 2024
1 parent 925400a commit fbea672
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 0 deletions.
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);
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);
});
});

0 comments on commit fbea672

Please sign in to comment.