Builtin UUID module (#68)

* Implement builtin `uuid.parse` method
* Implement builtin `uuid.rfc4122` method
* Parse timestamps for v2 UUIDs
This commit is contained in:
Burak
2023-12-23 17:35:20 +00:00
committed by GitHub
parent 1fb144b145
commit ad3282caf4
5 changed files with 326 additions and 2 deletions

View File

@@ -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]

View File

@@ -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,
}
}

145
src/builtins/uuid.rs Normal file
View File

@@ -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<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "uuid.parse";
ensure_args_count(span, name, params, args, 1)?;
let val = ensure_string(name, &params[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::<Vec<_>>()
.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<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "uuid.rfc4122";
ensure_args_count(span, name, params, args, 1)?;
ensure_string(name, &params[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<Timestamp> {
// 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)
}

View File

@@ -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'

View File

@@ -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'