From ad3282caf41f3a59bdd01403913b56067e1fff58 Mon Sep 17 00:00:00 2001 From: Burak Date: Sat, 23 Dec 2023 17:35:20 +0000 Subject: [PATCH] Builtin UUID module (#68) * Implement builtin `uuid.parse` method * Implement builtin `uuid.rfc4122` method * Parse timestamps for v2 UUIDs --- Cargo.toml | 4 +- src/builtins/mod.rs | 6 +- src/builtins/uuid.rs | 145 ++++++++++++++++++ .../cases/builtins/uuid/generate.yaml | 45 ++++++ .../cases/builtins/uuid/parse.yaml | 128 ++++++++++++++++ 5 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 src/builtins/uuid.rs create mode 100644 tests/interpreter/cases/builtins/uuid/generate.yaml create mode 100644 tests/interpreter/cases/builtins/uuid/parse.yaml diff --git a/Cargo.toml b/Cargo.toml index b1a0468..410da74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,10 @@ glob = ["dep:wax"] jsonschema = ["dep:jsonschema"] regex = ["dep:regex"] semver = ["dep:semver"] +uuid = ["dep:uuid"] urlquery = ["dep:url"] yaml = ["serde_yaml"] -full-opa = ["base64", "base64url", "crypto", "deprecated", "glob", "hex", "jsonschema", "regex", "semver", "urlquery", "yaml"] +full-opa = ["base64", "base64url", "crypto", "deprecated", "glob", "hex", "jsonschema", "regex", "semver", "uuid", "urlquery", "yaml"] [dependencies] anyhow = {version = "1.0.66", features = ["backtrace"] } @@ -50,6 +51,7 @@ url = { version = "2.5.0", optional = true } dashu-float = { version = "0.4.1", features = ["num-traits"] } num-traits = "0.2.17" dashu-base = "0.4.0" +uuid = { version = "1.6.1", features = ["v4", "fast-rng"], optional = true } [dev-dependencies] diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index af6391c..4d58c14 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -28,6 +28,8 @@ mod tracing; pub mod types; mod units; mod utils; +#[cfg(feature = "uuid")] +mod uuid; use crate::ast::{Expr, Ref}; use crate::lexer::Span; @@ -77,7 +79,8 @@ lazy_static! { //graphql::register(&mut m); //http::register(&mut m); //net::register(&mut m); - //uuid::register(&mut m); + #[cfg(feature = "uuid")] + uuid::register(&mut m); #[cfg(feature = "semver")] semver::register(&mut m); //rego::register(&mut m); @@ -92,6 +95,7 @@ lazy_static! { pub fn must_cache(path: &str) -> Option<&'static str> { match path { "rand.intn" => Some("rand.intn"), + "uuid.rfc4122" => Some("uuid.rfc4122"), _ => None, } } diff --git a/src/builtins/uuid.rs b/src/builtins/uuid.rs new file mode 100644 index 0000000..57c88b8 --- /dev/null +++ b/src/builtins/uuid.rs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::ast::{Expr, Ref}; +use crate::builtins; +use crate::builtins::utils::{ensure_args_count, ensure_string}; +use crate::lexer::Span; +use crate::value::Value; + +use std::collections::{BTreeMap, HashMap}; + +use anyhow::{Ok, Result}; +use uuid::{Timestamp, Uuid}; + +pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { + m.insert("uuid.parse", (parse, 1)); + m.insert("uuid.rfc4122", (rfc4122, 1)); +} + +fn parse(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "uuid.parse"; + ensure_args_count(span, name, params, args, 1)?; + + let val = ensure_string(name, ¶ms[0], &args[0])?; + let Some(uuid) = Uuid::parse_str(&val).ok() else { + return Ok(Value::Undefined); + }; + let version = uuid.get_version_num(); + + let mut result = BTreeMap::new(); + result.insert( + Value::String("version".into()), + Value::Number(version.into()), + ); + result.insert( + Value::String("variant".into()), + Value::String(uuid.get_variant().to_string().into()), + ); + + if let Some(time) = timestamp(&uuid) { + let (sec, nanosec) = time.to_unix(); + let time = sec.wrapping_mul(1_000_000_000).wrapping_add(nanosec as u64); + result.insert(Value::String("time".into()), Value::Number(time.into())); + } + + if version == 1 || version == 2 { + let (f1, _, _, f4) = uuid.as_fields(); + + result.insert( + Value::String("nodeid".into()), + Value::String( + f4[2..] + .iter() + .map(|f| format!("{f:02x}")) + .collect::>() + .join("-") + .into(), + ), + ); + result.insert( + Value::String("macvariables".into()), + Value::String(mac_vars(f4[2]).into()), + ); + + let clock_seq = u16::from_be_bytes([f4[0], f4[1]]) & 0x3fff; + result.insert( + Value::String("clocksequence".into()), + Value::Number((clock_seq as u64).into()), + ); + + if version == 2 { + result.insert( + Value::String("id".into()), + Value::Number((f1 as u64).into()), + ); + result.insert( + Value::String("domain".into()), + Value::String(domain(f4[1]).into()), + ); + } + } + + Ok(Value::from(result)) +} + +fn rfc4122(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "uuid.rfc4122"; + ensure_args_count(span, name, params, args, 1)?; + ensure_string(name, ¶ms[0], &args[0])?; + + let uuid = Uuid::new_v4(); + Ok(Value::String(uuid.to_string().into())) +} + +fn mac_vars(b: u8) -> &'static str { + if b & 0b11 == 0b11 { + return "local:multicast"; + } else if b & 0b01 == 0b01 { + return "global:multicast"; + } else if b & 0b10 == 0b10 { + return "local:unicast"; + } + "global:unicast" +} + +fn domain(b: u8) -> String { + match b { + 0 => "Person".to_string(), + 1 => "Group".to_string(), + 2 => "Org".to_string(), + n => format!("Domain{n}"), + } +} + +fn timestamp(uuid: &Uuid) -> Option { + // We need a special case for v2 UUIDs because `uuid` crate does not support + // parsing timestamps for v2 UUIDs but OPA tests expects them and also + // the original Go implementation parses timestamps for v2 UUIDs. + // This is just a copy of parsing logic for v1 UUIDs: + // https://github.com/uuid-rs/uuid/blob/94ecea893fadac93248f1bd6f47673c09cec5912/src/lib.rs#L900-L904 + if uuid.get_version_num() == 2 { + let (ticks, counter) = decode_rfc4122_timestamp(uuid); + return Some(Timestamp::from_rfc4122(ticks, counter)); + } + + uuid.get_timestamp() +} + +// Copied from https://github.com/uuid-rs/uuid/blob/94ecea893fadac93248f1bd6f47673c09cec5912/src/timestamp.rs#L190-L205 +const fn decode_rfc4122_timestamp(uuid: &Uuid) -> (u64, u16) { + let bytes = uuid.as_bytes(); + + let ticks: u64 = ((bytes[6] & 0x0F) as u64) << 56 + | (bytes[7] as u64) << 48 + | (bytes[4] as u64) << 40 + | (bytes[5] as u64) << 32 + | (bytes[0] as u64) << 24 + | (bytes[1] as u64) << 16 + | (bytes[2] as u64) << 8 + | (bytes[3] as u64); + + let counter: u16 = ((bytes[8] & 0x3F) as u16) << 8 | (bytes[9] as u16); + + (ticks, counter) +} diff --git a/tests/interpreter/cases/builtins/uuid/generate.yaml b/tests/interpreter/cases/builtins/uuid/generate.yaml new file mode 100644 index 0000000..381d2f8 --- /dev/null +++ b/tests/interpreter/cases/builtins/uuid/generate.yaml @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: generate-v4 + data: {} + modules: + - | + package test + + len := count(uuid.rfc4122("1")) + parsed := uuid.parse(uuid.rfc4122("1")) + query: data.test + want_result: + len: 36 + parsed: + variant: "RFC4122" + version: 4 + + - note: consistent-output + data: {} + modules: + - | + package test + + s1 := true { uuid.rfc4122("1") != uuid.rfc4122("2") } + s2 := true { uuid.rfc4122("1") == uuid.rfc4122("1") } + s3 := true { uuid.rfc4122("2") == uuid.rfc4122("2") } + s4 := true { uuid.rfc4122("2") != uuid.rfc4122("3") } + query: data.test + want_result: + s1: true + s2: true + s3: true + s4: true + + - note: invalid-type + data: {} + modules: + - | + package test + + id := uuid.rfc4122(42) + query: data.test + error: '`uuid.rfc4122` expects string argument. Got `42` instead' diff --git a/tests/interpreter/cases/builtins/uuid/parse.yaml b/tests/interpreter/cases/builtins/uuid/parse.yaml new file mode 100644 index 0000000..84bc00b --- /dev/null +++ b/tests/interpreter/cases/builtins/uuid/parse.yaml @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: v1 + data: {} + modules: + - | + package test + + a := uuid.parse("b01d0062-a116-11ee-822b-7ab0b9e5e0c0") + b := uuid.parse("bdf46d2e-a116-11ee-8c90-0242ac120002") + c := uuid.parse("b3dd08c8-a116-11ee-822b-7ab0b9e5e0c0") + d := uuid.parse("b700c334-a128-11ee-bfff-7ab0b9e5e0c0") + query: data.test + want_result: + a: + clocksequence: 555 + macvariables: "local:unicast" + nodeid: "7a-b0-b9-e5-e0-c0" + time: 1703282931110717000 + variant: "RFC4122" + version: 1 + b: + clocksequence: 3216 + macvariables: "local:unicast" + nodeid: "02-42-ac-12-00-02" + time: 1703282954332907000 + variant: "RFC4122" + version: 1 + c: + clocksequence: 555 + macvariables: "local:unicast" + nodeid: "7a-b0-b9-e5-e0-c0" + time: 1703282937402388000 + variant: "RFC4122" + version: 1 + d: + clocksequence: 16383 + macvariables: "local:unicast" + nodeid: "7a-b0-b9-e5-e0-c0" + time: 1703290673610834000 + variant: "RFC4122" + version: 1 + + - note: v2 + data: {} + modules: + - | + package test + + a := uuid.parse("000003e8-a129-21ee-ae00-325096b39f47") + b := uuid.parse("000003e8-a129-21ee-ab00-325096b39f47") + query: data.test + want_result: + a: + clocksequence: 11776 + domain: "Person" + id: 1000 + macvariables: "local:unicast" + nodeid: "32-50-96-b3-9f-47" + time: 1703290796079613600 + variant: "RFC4122" + version: 2 + b: + clocksequence: 11008 + domain: "Person" + id: 1000 + macvariables: "local:unicast" + nodeid: "32-50-96-b3-9f-47" + time: 1703290796079613600 + variant: "RFC4122" + version: 2 + + - note: others + data: {} + modules: + - | + package test + + v3 := uuid.parse("c6db027c-615c-3b4d-959e-1a917747ca5a") + v4 := uuid.parse("a6342df8-7801-469c-b3f3-c5317a0ebdaa") + v5 := uuid.parse("c66bbb60-d62e-5f17-a399-3a0bd237c503") + v6 := uuid.parse("1EC9414C-232A-6B00-B3C8-9E6BDECED846") + v7 := uuid.parse("017F22E2-79B0-7CC3-98C4-DC0C0C07398F") + v8 := uuid.parse("320C3D4D-CC00-875B-8EC9-32D5F69181C0") + query: data.test + want_result: + v3: + variant: "RFC4122" + version: 3 + v4: + variant: "RFC4122" + version: 4 + v5: + variant: "RFC4122" + version: 5 + v6: + variant: "RFC4122" + version: 6 + time: 1645557742000000000 + v7: + variant: "RFC4122" + version: 7 + time: 1645557742000000000 + v8: + variant: "RFC4122" + version: 8 + + - note: invalid-uuid + data: {} + modules: + - | + package test + + id := uuid.parse("not-valid") + query: data.test + want_result: {} + + - note: invalid-type + data: {} + modules: + - | + package test + + id := uuid.parse(42) + query: data.test + error: '`uuid.parse` expects string argument. Got `42` instead'