From 4dacddd85e07af165df5093e14f3f1a767cf63d1 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:57:55 -0400 Subject: [PATCH 01/23] Add `core:encoding/uuid` --- core/encoding/uuid/LICENSE | 28 ++++ core/encoding/uuid/definitions.odin | 59 +++++++ core/encoding/uuid/doc.odin | 15 ++ core/encoding/uuid/generation.odin | 159 +++++++++++++++++++ core/encoding/uuid/reading.odin | 97 +++++++++++ core/encoding/uuid/writing.odin | 61 +++++++ examples/all/all_main.odin | 2 + tests/core/encoding/uuid/test_core_uuid.odin | 118 ++++++++++++++ tests/core/normal.odin | 1 + 9 files changed, 540 insertions(+) create mode 100644 core/encoding/uuid/LICENSE create mode 100644 core/encoding/uuid/definitions.odin create mode 100644 core/encoding/uuid/doc.odin create mode 100644 core/encoding/uuid/generation.odin create mode 100644 core/encoding/uuid/reading.odin create mode 100644 core/encoding/uuid/writing.odin create mode 100644 tests/core/encoding/uuid/test_core_uuid.odin diff --git a/core/encoding/uuid/LICENSE b/core/encoding/uuid/LICENSE new file mode 100644 index 00000000000..e4e21e62d03 --- /dev/null +++ b/core/encoding/uuid/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Feoramund + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin new file mode 100644 index 00000000000..5bb104cb2a7 --- /dev/null +++ b/core/encoding/uuid/definitions.odin @@ -0,0 +1,59 @@ +package uuid + +// A RFC 4122 Universally Unique Identifier +Identifier :: struct #raw_union { + integer: u128be, + bytes: [16]u8, +} + +EXPECTED_LENGTH :: 8 + 4 + 4 + 4 + 12 + 4 + +VERSION_BYTE_INDEX :: 6 +VARIANT_BYTE_INDEX :: 8 + +Read_Error :: enum { + None, + Invalid_Length, + Invalid_Hexadecimal, + Invalid_Separator, +} + +Variant_Type :: enum { + Unknown, + Reserved_Apollo_NCS, // 0b0xx + RFC_4122, // 0b10x + Reserved_Microsoft_COM, // 0b110 + Reserved_Future, // 0b111 +} + +// Name string is a URL. +Namespace_DNS := Identifier { + bytes = { + 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, + }, +} + +// Name string is a fully-qualified domain name. +Namespace_URL := Identifier { + bytes = { + 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, + }, +} + +// Name string is an ISO OID. +Namespace_OID := Identifier { + bytes = { + 0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, + }, +} + +// Name string is an X.500 DN (in DER or a text output format). +Namespace_X500 := Identifier { + bytes = { + 0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, + }, +} diff --git a/core/encoding/uuid/doc.odin b/core/encoding/uuid/doc.odin new file mode 100644 index 00000000000..a05698955da --- /dev/null +++ b/core/encoding/uuid/doc.odin @@ -0,0 +1,15 @@ +/* +package uuid implements Universally Unique Identifiers according to the +standard outlined in RFC 4122. + +See here for more information: https://www.rfc-editor.org/rfc/rfc4122.html + +Generation of versions 1 and 2 (the MAC address-based versions) are not yet +implemented. + +The UUIDs are textually represented and read in the following string format: +`00000000-0000-4000-8000-000000000000` + +Outside of string representations, they are represented in memory by a 128-bit structure. +*/ +package uuid diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin new file mode 100644 index 00000000000..fe05d3ebdec --- /dev/null +++ b/core/encoding/uuid/generation.odin @@ -0,0 +1,159 @@ +package uuid + +import "core:crypto/legacy/md5" +import "core:crypto/legacy/sha1" +import "core:math/rand" +import "core:mem" + +/* +Generate a version 3 UUID. + +This UUID is generated from a name within a namespace. +MD5 is used to hash the name with the namespace to produce the UUID. + +Inputs: +- namespace: Another `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in this package. +- name: The byte slice used to generate the name on top of the namespace. + +Returns: +- result: The generated UUID. +*/ +generate_v3_bytes :: proc( + namespace: Identifier, + name: []byte, +) -> ( + result: Identifier, +) { + namespace := namespace + + ctx: md5.Context + md5.init(&ctx) + md5.update(&ctx, namespace.bytes[:]) + md5.update(&ctx, name) + md5.final(&ctx, result.bytes[:]) + + result.bytes[VERSION_BYTE_INDEX] &= 0x0F + result.bytes[VERSION_BYTE_INDEX] |= 0x30 + + result.bytes[VARIANT_BYTE_INDEX] &= 0x3F + result.bytes[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Generate a version 3 UUID. + +This UUID is generated from a name within a namespace. +MD5 is used to hash the name with the namespace to produce the UUID. + +Inputs: +- namespace: Another `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in this package. +- name: The string used to generate the name on top of the namespace. + +Returns: +- result: The generated UUID. +*/ +generate_v3_string :: proc( + namespace: Identifier, + name: string, +) -> ( + result: Identifier, +) { + return generate_v3_bytes(namespace, transmute([]byte)name) +} + +generate_v3 :: proc { + generate_v3_bytes, + generate_v3_string, +} + +/* +Generate a version 4 UUID. + +This UUID will be pseudorandom, save for 6 pre-determined version and variant bits. + +Returns: +- result: The generated UUID. +*/ +generate_v4 :: proc() -> (result: Identifier) { + result.integer = transmute(u128be)rand.uint128() + + result.bytes[VERSION_BYTE_INDEX] &= 0x0F + result.bytes[VERSION_BYTE_INDEX] |= 0x40 + + result.bytes[VARIANT_BYTE_INDEX] &= 0x3F + result.bytes[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Generate a version 5 UUID. + +This UUID is generated from a name within a namespace. +SHA1 is used to hash the name with the namespace to produce the UUID. + +Inputs: +- namespace: Another `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in this package. +- name: The byte slice used to generate the name on top of the namespace. + +Returns: +- result: The generated UUID. +*/ +generate_v5_bytes :: proc( + namespace: Identifier, + name: []byte, +) -> ( + result: Identifier, +) { + namespace := namespace + digest: [sha1.DIGEST_SIZE]byte + + ctx: sha1.Context + sha1.init(&ctx) + sha1.update(&ctx, namespace.bytes[:]) + sha1.update(&ctx, name) + sha1.final(&ctx, digest[:]) + + mem.copy_non_overlapping(&result.bytes, &digest, 16) + + result.bytes[VERSION_BYTE_INDEX] &= 0x0F + result.bytes[VERSION_BYTE_INDEX] |= 0x50 + + result.bytes[VARIANT_BYTE_INDEX] &= 0x3F + result.bytes[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Generate a version 5 UUID. + +This UUID is generated from a name within a namespace. +SHA1 is used to hash the name with the namespace to produce the UUID. + +Inputs: +- namespace: Another `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in this package. +- name: The string used to generate the name on top of the namespace. + +Returns: +- result: The generated UUID. +*/ +generate_v5_string :: proc( + namespace: Identifier, + name: string, +) -> ( + result: Identifier, +) { + return generate_v5_bytes(namespace, transmute([]byte)name) +} + +generate_v5 :: proc { + generate_v5_bytes, + generate_v5_string, +} diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin new file mode 100644 index 00000000000..7f3d30ab27f --- /dev/null +++ b/core/encoding/uuid/reading.odin @@ -0,0 +1,97 @@ +package uuid + +/* +Convert a string to a UUID. + +Inputs: +- str: A string in the 8-4-4-4-12 format. + +Returns: +- id: The converted identifier, or `nil` if there is an error. +- error: A description of the error, or `nil` if successful. +*/ +read :: proc "contextless" (str: string) -> (id: Identifier, error: Read_Error) #no_bounds_check { + // Only exact-length strings are acceptable. + if len(str) != EXPECTED_LENGTH { + return {}, .Invalid_Length + } + + // Check ahead to see if the separators are in the right places. + if str[8] != '-' || str[13] != '-' || str[18] != '-' || str[23] != '-' { + return {}, .Invalid_Separator + } + + read_nibble :: proc "contextless" (nibble: u8) -> u8 { + switch nibble { + case '0' ..= '9': + return nibble - '0' + case 'A' ..= 'F': + return nibble - 'A' + 10 + case 'a' ..= 'f': + return nibble - 'a' + 10 + case: + // Return an error value. + return 0xFF + } + } + + index := 0 + octet_index := 0 + + CHUNKS :: [5]int{8, 4, 4, 4, 12} + + for chunk in CHUNKS { + for i := index; i < index + chunk; i += 2 { + high := read_nibble(str[i]) + low := read_nibble(str[i + 1]) + + if high | low > 0xF { + return {}, .Invalid_Hexadecimal + } + + id.bytes[octet_index] = low | high << 4 + octet_index += 1 + } + + index += chunk + 1 + } + + return +} + +/* +Get the version of a UUID. + +Inputs: +- id: The identifier. + +Returns: +- number: The version number. +*/ +version :: proc "contextless" (id: Identifier) -> (number: int) #no_bounds_check { + return cast(int)(id.bytes[VERSION_BYTE_INDEX] & 0xF0 >> 4) +} + +/* +Get the variant of a UUID. + +Inputs: +- id: The identifier. + +Returns: +- variant: The variant type. +*/ +variant :: proc "contextless" (id: Identifier) -> (variant: Variant_Type) #no_bounds_check { + switch { + case id.bytes[VARIANT_BYTE_INDEX] & 0x80 == 0: + return .Reserved_Apollo_NCS + case id.bytes[VARIANT_BYTE_INDEX] & 0xC0 == 0x80: + return .RFC_4122 + case id.bytes[VARIANT_BYTE_INDEX] & 0xE0 == 0xC0: + return .Reserved_Microsoft_COM + case id.bytes[VARIANT_BYTE_INDEX] & 0xF0 == 0xE0: + return .Reserved_Future + case: + return .Unknown + } +} diff --git a/core/encoding/uuid/writing.odin b/core/encoding/uuid/writing.odin new file mode 100644 index 00000000000..c13d700a890 --- /dev/null +++ b/core/encoding/uuid/writing.odin @@ -0,0 +1,61 @@ +package uuid + +import "base:runtime" +import "core:io" +import "core:strconv" +import "core:strings" + +/* +Write a UUID in the 8-4-4-4-12 format. + +Inputs: +- w: A writable stream. +- id: The identifier to convert. +*/ +write :: proc(w: io.Writer, id: Identifier) #no_bounds_check { + write_octet :: proc (w: io.Writer, octet: u8) { + high_nibble := octet >> 4 + low_nibble := octet & 0xF + + io.write_byte(w, strconv.digits[high_nibble]) + io.write_byte(w, strconv.digits[low_nibble]) + } + + for index in 0 ..< 4 { write_octet(w, id.bytes[index]) } + io.write_byte(w, '-') + for index in 4 ..< 6 { write_octet(w, id.bytes[index]) } + io.write_byte(w, '-') + for index in 6 ..< 8 { write_octet(w, id.bytes[index]) } + io.write_byte(w, '-') + for index in 8 ..< 10 { write_octet(w, id.bytes[index]) } + io.write_byte(w, '-') + for index in 10 ..< 16 { write_octet(w, id.bytes[index]) } +} + +/* +Convert a UUID to a string in the 8-4-4-4-12 format. + +*Allocates Using Provided Allocator* + +Inputs: +- id: The identifier to convert. +- allocator: (default: context.allocator) +- loc: The caller location for debugging purposes (default: #caller_location) + +Returns: +- str: The allocated and converted string. +- error: An optional allocator error if one occured, `nil` otherwise. +*/ +to_string :: proc( + id: Identifier, + allocator := context.allocator, + loc := #caller_location, +) -> ( + str: string, + error: runtime.Allocator_Error, +) #optional_allocator_error { + buf := make([]byte, EXPECTED_LENGTH, allocator, loc) or_return + builder := strings.builder_from_bytes(buf[:]) + write(strings.to_writer(&builder), id) + return strings.to_string(builder), nil +} diff --git a/examples/all/all_main.odin b/examples/all/all_main.odin index 5202d72be8b..d39fbe79f2d 100644 --- a/examples/all/all_main.odin +++ b/examples/all/all_main.odin @@ -62,6 +62,7 @@ import varint "core:encoding/varint" import xml "core:encoding/xml" import endian "core:encoding/endian" import cbor "core:encoding/cbor" +import uuid "core:encoding/uuid" import fmt "core:fmt" import hash "core:hash" @@ -237,6 +238,7 @@ _ :: datetime _ :: flags _ :: sysinfo _ :: unicode +_ :: uuid _ :: utf8 _ :: utf8string _ :: utf16 diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin new file mode 100644 index 00000000000..3d1eb1db086 --- /dev/null +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -0,0 +1,118 @@ +package test_core_uuid + +import "core:testing" +import "core:encoding/uuid" + +@(test) +test_version_and_variant :: proc(t: ^testing.T) { + v3 := uuid.generate_v3(uuid.Namespace_DNS, "") + v4 := uuid.generate_v4() + v5 := uuid.generate_v5(uuid.Namespace_DNS, "") + + testing.expect_value(t, uuid.version(v3), 3) + testing.expect_value(t, uuid.variant(v3), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v4), 4) + testing.expect_value(t, uuid.variant(v4), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v5), 5) + testing.expect_value(t, uuid.variant(v5), uuid.Variant_Type.RFC_4122) +} + +@(test) +test_namespaced_uuids :: proc(t: ^testing.T) { + TEST_NAME :: "0123456789ABCDEF0123456789ABCDEF" + + Expected_Result :: struct { + namespace: uuid.Identifier, + v3, v5: string, + } + + Expected_Results := [?]Expected_Result { + { uuid.Namespace_DNS, "80147f37-36db-3b82-b78f-810c3c6504ba", "18394c41-13a2-593f-abf2-a63e163c2860" }, + { uuid.Namespace_URL, "8136789b-8e16-3fbd-800b-1587e2f22521", "07337422-eb77-5fd3-99af-c7f59e641e13" }, + { uuid.Namespace_OID, "adbb95bc-ea50-3226-9a75-20c34a6030f8", "24db9b0f-70b8-53c4-a301-f695ce17276d" }, + { uuid.Namespace_X500, "a8965ad1-0e54-3d65-b933-8b7cca8e8313", "3012bf2d-fac4-5187-9825-493e6636b936" }, + } + + for exp in Expected_Results { + v3 := uuid.generate_v3(exp.namespace, TEST_NAME) + v5 := uuid.generate_v5(exp.namespace, TEST_NAME) + + v3_str := uuid.to_string(v3) + defer delete(v3_str) + + v5_str := uuid.to_string(v5) + defer delete(v5_str) + + testing.expect_value(t, v3_str, exp.v3) + testing.expect_value(t, v5_str, exp.v5) + } +} + +@(test) +test_writing :: proc(t: ^testing.T) { + id: uuid.Identifier + + for &b, i in id.bytes { + b = u8(i) + } + + s := uuid.to_string(id) + defer delete(s) + + testing.expect_value(t, s, "00010203-0405-0607-0809-0a0b0c0d0e0f") +} + +@(test) +test_reading :: proc(t: ^testing.T) { + id, err := uuid.read("00010203-0405-0607-0809-0a0b0c0d0e0f") + testing.expect_value(t, err, nil) + + for b, i in id.bytes { + testing.expect_value(t, b, u8(i)) + } +} + +@(test) +test_reading_errors :: proc(t: ^testing.T) { + { + BAD_STRING :: "|.......@....@....@....@............" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Separator) + } + + { + BAD_STRING :: "|.......-....-....-....-............" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Hexadecimal) + } + + { + BAD_STRING :: ".......-....-....-....-............" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) + } + + { + BAD_STRING :: "|.......-....-....-....-............|" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) + } + + { + BAD_STRING :: "00000000-0000-0000-0000-0000000000001" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) + } + + { + BAD_STRING :: "00000000000000000000000000000000" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) + } + + { + OK_STRING :: "00000000-0000-0000-0000-000000000000" + _, err := uuid.read(OK_STRING) + testing.expect_value(t, err, nil) + } +} diff --git a/tests/core/normal.odin b/tests/core/normal.odin index a84420cca3b..065090be387 100644 --- a/tests/core/normal.odin +++ b/tests/core/normal.odin @@ -17,6 +17,7 @@ download_assets :: proc() { @(require) import "encoding/hex" @(require) import "encoding/hxa" @(require) import "encoding/json" +@(require) import "encoding/uuid" @(require) import "encoding/varint" @(require) import "encoding/xml" @(require) import "flags" From 31873ed46625823bae8d7f9b1883597ea1e1c351 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:44:39 -0400 Subject: [PATCH 02/23] Fix wrong comments --- core/encoding/uuid/definitions.odin | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin index 5bb104cb2a7..a1862a3fd18 100644 --- a/core/encoding/uuid/definitions.odin +++ b/core/encoding/uuid/definitions.odin @@ -26,7 +26,7 @@ Variant_Type :: enum { Reserved_Future, // 0b111 } -// Name string is a URL. +// Name string is a fully-qualified domain name. Namespace_DNS := Identifier { bytes = { 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, @@ -34,7 +34,7 @@ Namespace_DNS := Identifier { }, } -// Name string is a fully-qualified domain name. +// Name string is a URL. Namespace_URL := Identifier { bytes = { 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, From 6da99b888a4974fbdd2fcb7d04f12342fc1c8bb2 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:56:41 -0400 Subject: [PATCH 03/23] Make UUID `Identfier` only a distinct byte array --- core/encoding/uuid/definitions.odin | 29 +++++----------- core/encoding/uuid/generation.odin | 35 ++++++++++---------- core/encoding/uuid/reading.odin | 12 +++---- core/encoding/uuid/writing.odin | 10 +++--- tests/core/encoding/uuid/test_core_uuid.odin | 4 +-- 5 files changed, 40 insertions(+), 50 deletions(-) diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin index a1862a3fd18..a5cbf9c3806 100644 --- a/core/encoding/uuid/definitions.odin +++ b/core/encoding/uuid/definitions.odin @@ -1,10 +1,7 @@ package uuid // A RFC 4122 Universally Unique Identifier -Identifier :: struct #raw_union { - integer: u128be, - bytes: [16]u8, -} +Identifier :: distinct [16]u8 EXPECTED_LENGTH :: 8 + 4 + 4 + 4 + 12 + 4 @@ -28,32 +25,24 @@ Variant_Type :: enum { // Name string is a fully-qualified domain name. Namespace_DNS := Identifier { - bytes = { - 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, - 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, - }, + 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, } // Name string is a URL. Namespace_URL := Identifier { - bytes = { - 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, - 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, - }, + 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, } // Name string is an ISO OID. Namespace_OID := Identifier { - bytes = { - 0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, - 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, - }, + 0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, } // Name string is an X.500 DN (in DER or a text output format). Namespace_X500 := Identifier { - bytes = { - 0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, - 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, - }, + 0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, } diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index fe05d3ebdec..9b790714cf2 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -29,15 +29,15 @@ generate_v3_bytes :: proc( ctx: md5.Context md5.init(&ctx) - md5.update(&ctx, namespace.bytes[:]) + md5.update(&ctx, namespace[:]) md5.update(&ctx, name) - md5.final(&ctx, result.bytes[:]) + md5.final(&ctx, result[:]) - result.bytes[VERSION_BYTE_INDEX] &= 0x0F - result.bytes[VERSION_BYTE_INDEX] |= 0x30 + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x30 - result.bytes[VARIANT_BYTE_INDEX] &= 0x3F - result.bytes[VARIANT_BYTE_INDEX] |= 0x80 + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 return } @@ -79,13 +79,14 @@ Returns: - result: The generated UUID. */ generate_v4 :: proc() -> (result: Identifier) { - result.integer = transmute(u128be)rand.uint128() + bytes_generated := rand.read(result[:]) + assert(bytes_generated == 16, "RNG failed to generate 16 bytes for UUID v4.") - result.bytes[VERSION_BYTE_INDEX] &= 0x0F - result.bytes[VERSION_BYTE_INDEX] |= 0x40 + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x40 - result.bytes[VARIANT_BYTE_INDEX] &= 0x3F - result.bytes[VARIANT_BYTE_INDEX] |= 0x80 + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 return } @@ -115,17 +116,17 @@ generate_v5_bytes :: proc( ctx: sha1.Context sha1.init(&ctx) - sha1.update(&ctx, namespace.bytes[:]) + sha1.update(&ctx, namespace[:]) sha1.update(&ctx, name) sha1.final(&ctx, digest[:]) - mem.copy_non_overlapping(&result.bytes, &digest, 16) + mem.copy_non_overlapping(&result, &digest, 16) - result.bytes[VERSION_BYTE_INDEX] &= 0x0F - result.bytes[VERSION_BYTE_INDEX] |= 0x50 + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x50 - result.bytes[VARIANT_BYTE_INDEX] &= 0x3F - result.bytes[VARIANT_BYTE_INDEX] |= 0x80 + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 return } diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index 7f3d30ab27f..0c0274e534a 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -49,7 +49,7 @@ read :: proc "contextless" (str: string) -> (id: Identifier, error: Read_Error) return {}, .Invalid_Hexadecimal } - id.bytes[octet_index] = low | high << 4 + id[octet_index] = low | high << 4 octet_index += 1 } @@ -69,7 +69,7 @@ Returns: - number: The version number. */ version :: proc "contextless" (id: Identifier) -> (number: int) #no_bounds_check { - return cast(int)(id.bytes[VERSION_BYTE_INDEX] & 0xF0 >> 4) + return cast(int)(id[VERSION_BYTE_INDEX] & 0xF0 >> 4) } /* @@ -83,13 +83,13 @@ Returns: */ variant :: proc "contextless" (id: Identifier) -> (variant: Variant_Type) #no_bounds_check { switch { - case id.bytes[VARIANT_BYTE_INDEX] & 0x80 == 0: + case id[VARIANT_BYTE_INDEX] & 0x80 == 0: return .Reserved_Apollo_NCS - case id.bytes[VARIANT_BYTE_INDEX] & 0xC0 == 0x80: + case id[VARIANT_BYTE_INDEX] & 0xC0 == 0x80: return .RFC_4122 - case id.bytes[VARIANT_BYTE_INDEX] & 0xE0 == 0xC0: + case id[VARIANT_BYTE_INDEX] & 0xE0 == 0xC0: return .Reserved_Microsoft_COM - case id.bytes[VARIANT_BYTE_INDEX] & 0xF0 == 0xE0: + case id[VARIANT_BYTE_INDEX] & 0xF0 == 0xE0: return .Reserved_Future case: return .Unknown diff --git a/core/encoding/uuid/writing.odin b/core/encoding/uuid/writing.odin index c13d700a890..45096e84f0a 100644 --- a/core/encoding/uuid/writing.odin +++ b/core/encoding/uuid/writing.odin @@ -21,15 +21,15 @@ write :: proc(w: io.Writer, id: Identifier) #no_bounds_check { io.write_byte(w, strconv.digits[low_nibble]) } - for index in 0 ..< 4 { write_octet(w, id.bytes[index]) } + for index in 0 ..< 4 { write_octet(w, id[index]) } io.write_byte(w, '-') - for index in 4 ..< 6 { write_octet(w, id.bytes[index]) } + for index in 4 ..< 6 { write_octet(w, id[index]) } io.write_byte(w, '-') - for index in 6 ..< 8 { write_octet(w, id.bytes[index]) } + for index in 6 ..< 8 { write_octet(w, id[index]) } io.write_byte(w, '-') - for index in 8 ..< 10 { write_octet(w, id.bytes[index]) } + for index in 8 ..< 10 { write_octet(w, id[index]) } io.write_byte(w, '-') - for index in 10 ..< 16 { write_octet(w, id.bytes[index]) } + for index in 10 ..< 16 { write_octet(w, id[index]) } } /* diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 3d1eb1db086..717c5599d09 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -52,7 +52,7 @@ test_namespaced_uuids :: proc(t: ^testing.T) { test_writing :: proc(t: ^testing.T) { id: uuid.Identifier - for &b, i in id.bytes { + for &b, i in id { b = u8(i) } @@ -67,7 +67,7 @@ test_reading :: proc(t: ^testing.T) { id, err := uuid.read("00010203-0405-0607-0809-0a0b0c0d0e0f") testing.expect_value(t, err, nil) - for b, i in id.bytes { + for b, i in id { testing.expect_value(t, b, u8(i)) } } From fee81985b4b7ade3cddc7038360efcd32555d879 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:59:57 -0400 Subject: [PATCH 04/23] Make UUID namespaces `@(rodata)` --- core/encoding/uuid/definitions.odin | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin index a5cbf9c3806..1208e26f47f 100644 --- a/core/encoding/uuid/definitions.odin +++ b/core/encoding/uuid/definitions.odin @@ -24,24 +24,28 @@ Variant_Type :: enum { } // Name string is a fully-qualified domain name. +@(rodata) Namespace_DNS := Identifier { 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, } // Name string is a URL. +@(rodata) Namespace_URL := Identifier { 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, } // Name string is an ISO OID. +@(rodata) Namespace_OID := Identifier { 0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, } // Name string is an X.500 DN (in DER or a text output format). +@(rodata) Namespace_X500 := Identifier { 0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8, From 4cfbd83b10ff17b6c7ca4b01dac249bfbea0da84 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:04:45 -0400 Subject: [PATCH 05/23] Add version 7 UUID generation --- core/encoding/uuid/definitions.odin | 5 ++ core/encoding/uuid/generation.odin | 84 ++++++++++++++++++++ core/encoding/uuid/reading.odin | 31 ++++++++ tests/core/encoding/uuid/test_core_uuid.odin | 32 +++++++- 4 files changed, 151 insertions(+), 1 deletion(-) diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin index 1208e26f47f..b54965e2343 100644 --- a/core/encoding/uuid/definitions.odin +++ b/core/encoding/uuid/definitions.odin @@ -8,6 +8,11 @@ EXPECTED_LENGTH :: 8 + 4 + 4 + 4 + 12 + 4 VERSION_BYTE_INDEX :: 6 VARIANT_BYTE_INDEX :: 8 +VERSION_7_TIME_MASK :: 0xffffffff_ffff0000_00000000_00000000 +VERSION_7_TIME_SHIFT :: 80 +VERSION_7_COUNTER_MASK :: 0x00000000_00000fff_00000000_00000000 +VERSION_7_COUNTER_SHIFT :: 64 + Read_Error :: enum { None, Invalid_Length, diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 9b790714cf2..7fe0bbd1305 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -4,6 +4,7 @@ import "core:crypto/legacy/md5" import "core:crypto/legacy/sha1" import "core:math/rand" import "core:mem" +import "core:time" /* Generate a version 3 UUID. @@ -158,3 +159,86 @@ generate_v5 :: proc { generate_v5_bytes, generate_v5_string, } + +/* +Generate a version 7 UUID. + +This UUID will be pseudorandom, save for 6 pre-determined version and variant +bits and a 48 bit timestamp. + +It is designed with time-based sorting in mind, such as for database usage, as +the highest bits are allocated from the timestamp of when it is created. + +Returns: +- result: The generated UUID. +*/ +generate_v7 :: proc() -> (result: Identifier) { + unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 + + temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT + + bytes_generated := rand.read(result[6:]) + assert(bytes_generated == 10, "RNG failed to generate 10 bytes for UUID v7.") + + result |= transmute(Identifier)temporary + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x70 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Generate a version 7 UUID with an incremented counter. + +This UUID will be pseudorandom, save for 6 pre-determined version and variant +bits, a 48 bit timestamp, and 12 bits of counter state. + +It is designed with time-based sorting in mind, such as for database usage, as +the highest bits are allocated from the timestamp of when it is created. + +This procedure is preferable if you are generating hundreds or thousands of +UUIDs as a batch within the span of a millisecond. Do note that the counter +only has 12 bits of state, thus `counter` cannot exceed the number 4,095. + +Example: + + import "core:uuid" + + // Create a batch of UUIDs all at once. + batch: [dynamic]uuid.Identifier + + for i: u16 = 0; i < 1000; i += 1 { + my_uuid := uuid.generate_v7_counter(i) + append(&batch, my_uuid) + } + +Inputs: +- counter: A 12-bit value, incremented each time a UUID is generated in a batch. + +Returns: +- result: The generated UUID. +*/ +generate_v7_counter :: proc(counter: u16) -> (result: Identifier) { + assert(counter <= 0x0fff, "This implementation of the version 7 UUID does not support counters in excess of 12 bits (4,095).") + unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 + + temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT + temporary |= cast(u128be)counter << VERSION_7_COUNTER_SHIFT + + bytes_generated := rand.read(result[8:]) + assert(bytes_generated == 8, "RNG failed to generate 8 bytes for UUID v7.") + + result |= transmute(Identifier)temporary + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x70 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index 0c0274e534a..c72f5791e84 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -95,3 +95,34 @@ variant :: proc "contextless" (id: Identifier) -> (variant: Variant_Type) #no_bo return .Unknown } } + +/* +Get the timestamp of a version 7 UUID. + +Inputs: +- id: The identifier. + +Returns: +- timestamp: The timestamp, in milliseconds since the UNIX epoch. +*/ +time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { + time_bits := transmute(u128be)id & VERSION_7_TIME_MASK + return cast(u64)(time_bits >> VERSION_7_TIME_SHIFT) +} + +/* +Get the 12-bit counter value of a version 7 UUID. + +The UUID must have been generated with a counter, otherwise this procedure will +return random bits. + +Inputs: +- id: The identifier. + +Returns: +- counter: The 12-bit counter value. +*/ +counter_v7 :: proc "contextless" (id: Identifier) -> (counter: u16) { + counter_bits := transmute(u128be)id & VERSION_7_COUNTER_MASK + return cast(u16)(counter_bits >> VERSION_7_COUNTER_SHIFT) +} diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 717c5599d09..b5e8e90cc1c 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -1,13 +1,16 @@ package test_core_uuid -import "core:testing" import "core:encoding/uuid" +import "core:log" +import "core:testing" +import "core:time" @(test) test_version_and_variant :: proc(t: ^testing.T) { v3 := uuid.generate_v3(uuid.Namespace_DNS, "") v4 := uuid.generate_v4() v5 := uuid.generate_v5(uuid.Namespace_DNS, "") + v7 := uuid.generate_v7() testing.expect_value(t, uuid.version(v3), 3) testing.expect_value(t, uuid.variant(v3), uuid.Variant_Type.RFC_4122) @@ -15,6 +18,8 @@ test_version_and_variant :: proc(t: ^testing.T) { testing.expect_value(t, uuid.variant(v4), uuid.Variant_Type.RFC_4122) testing.expect_value(t, uuid.version(v5), 5) testing.expect_value(t, uuid.variant(v5), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v7), 7) + testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122) } @(test) @@ -48,6 +53,31 @@ test_namespaced_uuids :: proc(t: ^testing.T) { } } +@(test) +test_v7 :: proc(t: ^testing.T) { + v7_a := uuid.generate_v7() + time.sleep(10 * time.Millisecond) + v7_b := uuid.generate_v7() + time.sleep(10 * time.Millisecond) + v7_c := uuid.generate_v7() + + time_bits_a := uuid.time_v7(v7_a) + time_bits_b := uuid.time_v7(v7_b) + time_bits_c := uuid.time_v7(v7_c) + + log.debugf("A: %02x, %i", v7_a, time_bits_a) + log.debugf("B: %02x, %i", v7_b, time_bits_b) + log.debugf("C: %02x, %i", v7_c, time_bits_c) + + testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") + testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") + testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") + + v7_with_counter := uuid.generate_v7_counter(0x555) + log.debugf("D: %02x", v7_with_counter) + testing.expect_value(t, uuid.counter_v7(v7_with_counter), 0x555) +} + @(test) test_writing :: proc(t: ^testing.T) { id: uuid.Identifier From 525bfca4ef53916ca99f0c5cd4a1371ba6756c95 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:55:27 -0400 Subject: [PATCH 06/23] Add version 1 UUID generation --- core/encoding/uuid/definitions.odin | 3 ++ core/encoding/uuid/generation.odin | 44 ++++++++++++++++ core/encoding/uuid/reading.odin | 55 ++++++++++++++++++++ tests/core/encoding/uuid/test_core_uuid.odin | 31 +++++++++++ 4 files changed, 133 insertions(+) diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin index b54965e2343..a63e72693d5 100644 --- a/core/encoding/uuid/definitions.odin +++ b/core/encoding/uuid/definitions.odin @@ -8,6 +8,9 @@ EXPECTED_LENGTH :: 8 + 4 + 4 + 4 + 12 + 4 VERSION_BYTE_INDEX :: 6 VARIANT_BYTE_INDEX :: 8 +// The number of 100-nanosecond intervals between 1582-10-15 and 1970-01-01. +HNS_INTERVALS_BETWEEN_GREG_AND_UNIX :: 141427 * 24 * 60 * 60 * 1000 * 1000 * 10 + VERSION_7_TIME_MASK :: 0xffffffff_ffff0000_00000000_00000000 VERSION_7_TIME_SHIFT :: 80 VERSION_7_COUNTER_MASK :: 0x00000000_00000fff_00000000_00000000 diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 7fe0bbd1305..29944dcb577 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -6,6 +6,50 @@ import "core:math/rand" import "core:mem" import "core:time" +/* +Generate a version 1 UUID. + +Inputs: +- clock_seq: The clock sequence, a number which must be initialized to a random number once in the lifetime of a system. +- node: An optional 48-bit spatially unique identifier, specified to be the IEEE 802 address of the system. + If one is not provided or available, 48 bits of random state will take its place. + +Returns: +- result: The generated UUID. +*/ +generate_v1 :: proc(clock_seq: u16, node: Maybe([6]u8) = nil) -> (result: Identifier) { + assert(clock_seq <= 0x3FFF, "The clock sequence can only hold 14 bits of data; no number greater than 16,383.") + unix_time_in_hns_intervals := time.to_unix_nanoseconds(time.now()) / 100 + timestamp := cast(u64le)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) + timestamp_octets := transmute([8]u8)timestamp + + result[0] = timestamp_octets[0] + result[1] = timestamp_octets[1] + result[2] = timestamp_octets[2] + result[3] = timestamp_octets[3] + result[4] = timestamp_octets[4] + result[5] = timestamp_octets[5] + + result[6] = timestamp_octets[6] >> 4 + result[7] = timestamp_octets[6] << 4 | timestamp_octets[7] + + if realized_node, ok := node.?; ok { + mutable_node := realized_node + mem.copy_non_overlapping(&result[10], &mutable_node[0], 6) + } else { + bytes_generated := rand.read(result[10:]) + assert(bytes_generated == 6, "RNG failed to generate 6 bytes for UUID v1.") + } + + result[VERSION_BYTE_INDEX] |= 0x10 + result[VARIANT_BYTE_INDEX] |= 0x80 + + result[8] |= cast(u8)(clock_seq & 0x3F00 >> 8) + result[9] = cast(u8)clock_seq + + return +} + /* Generate a version 3 UUID. diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index c72f5791e84..f31ae2bcd27 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -1,5 +1,7 @@ package uuid +import "base:runtime" + /* Convert a string to a UUID. @@ -96,6 +98,59 @@ variant :: proc "contextless" (id: Identifier) -> (variant: Variant_Type) #no_bo } } +/* +Get the clock sequence of a version 1 UUID. + +Inputs: +- id: The identifier. + +Returns: +- clock_seq: The 14-bit clock sequence field. +*/ +clock_seq :: proc "contextless" (id: Identifier) -> (clock_seq: u16) { + return cast(u16)id[9] | cast(u16)id[8] & 0x3F << 8 +} + +/* +Get the node of a version 1 UUID. + +Inputs: +- id: The identifier. + +Returns: +- node: The 48-bit spatially unique identifier. +*/ +node :: proc "contextless" (id: Identifier) -> (node: [6]u8) { + mutable_id := id + runtime.mem_copy_non_overlapping(&node, &mutable_id[10], 6) + return +} + +/* +Get the timestamp of a version 1 UUID. + +Inputs: +- id: The identifier. + +Returns: +- timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. +*/ +time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { + timestamp_octets: [8]u8 + + timestamp_octets[0] = id[0] + timestamp_octets[1] = id[1] + timestamp_octets[2] = id[2] + timestamp_octets[3] = id[3] + timestamp_octets[4] = id[4] + timestamp_octets[5] = id[5] + + timestamp_octets[6] = id[6] << 4 | id[7] >> 4 + timestamp_octets[7] = id[7] & 0xF + + return cast(u64)transmute(u64le)timestamp_octets +} + /* Get the timestamp of a version 7 UUID. diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index b5e8e90cc1c..f934d13dca1 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -7,11 +7,14 @@ import "core:time" @(test) test_version_and_variant :: proc(t: ^testing.T) { + v1 := uuid.generate_v1(0) v3 := uuid.generate_v3(uuid.Namespace_DNS, "") v4 := uuid.generate_v4() v5 := uuid.generate_v5(uuid.Namespace_DNS, "") v7 := uuid.generate_v7() + testing.expect_value(t, uuid.version(v1), 1) + testing.expect_value(t, uuid.variant(v1), uuid.Variant_Type.RFC_4122) testing.expect_value(t, uuid.version(v3), 3) testing.expect_value(t, uuid.variant(v3), uuid.Variant_Type.RFC_4122) testing.expect_value(t, uuid.version(v4), 4) @@ -53,6 +56,34 @@ test_namespaced_uuids :: proc(t: ^testing.T) { } } +@(test) +test_v1 :: proc(t: ^testing.T) { + CLOCK :: 0x3A1A + v1_a := uuid.generate_v1(CLOCK) + time.sleep(10 * time.Millisecond) + v1_b := uuid.generate_v1(CLOCK) + time.sleep(10 * time.Millisecond) + v1_c := uuid.generate_v1(CLOCK) + + testing.expect_value(t, uuid.clock_seq(v1_a), CLOCK) + + time_bits_a := uuid.time_v1(v1_a) + time_bits_b := uuid.time_v1(v1_b) + time_bits_c := uuid.time_v1(v1_c) + + time_a := time.Time { _nsec = cast(i64)((time_bits_a - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } + time_b := time.Time { _nsec = cast(i64)((time_bits_b - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } + time_c := time.Time { _nsec = cast(i64)((time_bits_c - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } + + log.debugf("A: %02x, %i, %v", v1_a, time_bits_a, time_a) + log.debugf("B: %02x, %i, %v", v1_b, time_bits_b, time_b) + log.debugf("C: %02x, %i, %v", v1_c, time_bits_c, time_c) + + testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.") + testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.") + testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.") +} + @(test) test_v7 :: proc(t: ^testing.T) { v7_a := uuid.generate_v7() From 9866b54d59721d62137f2a8279f015aeef0f7a4f Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:54:39 -0400 Subject: [PATCH 07/23] Add version 6 UUID generation --- core/encoding/uuid/generation.odin | 46 ++++++++++++++++++++ core/encoding/uuid/reading.odin | 20 ++++++++- tests/core/encoding/uuid/test_core_uuid.odin | 31 +++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 29944dcb577..08fd84c0c37 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -204,6 +204,52 @@ generate_v5 :: proc { generate_v5_string, } +/* +Generate a version 6 UUID. + +Inputs: +- clock_seq: The clock sequence from version 1, now made optional. + If unspecified, it will be replaced with random bits. +- node: An optional 48-bit spatially unique identifier, specified to be the IEEE 802 address of the system. + If one is not provided or available, 48 bits of random state will take its place. + +Returns: +- result: The generated UUID. +*/ +generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil) -> (result: Identifier) { + unix_time_in_hns_intervals := time.to_unix_nanoseconds(time.now()) / 100 + + timestamp := cast(u128be)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) + + result |= transmute(Identifier)(timestamp & 0x0FFFFFFF_FFFFF000 << 68) + result |= transmute(Identifier)(timestamp & 0x00000000_00000FFF << 64) + + if realized_clock_seq, ok := clock_seq.?; ok { + assert(realized_clock_seq <= 0x3FFF, "The clock sequence can only hold 14 bits of data, therefore no number greater than 16,383.") + result[8] |= cast(u8)(realized_clock_seq & 0x3F00 >> 8) + result[9] = cast(u8)realized_clock_seq + } else { + temporary: [2]u8 + bytes_generated := rand.read(temporary[:]) + assert(bytes_generated == 2, "RNG failed to generate 2 bytes for UUID v1.") + result[8] |= cast(u8)temporary[0] & 0x3F + result[9] = cast(u8)temporary[1] + } + + if realized_node, ok := node.?; ok { + mutable_node := realized_node + mem.copy_non_overlapping(&result[10], &mutable_node[0], 6) + } else { + bytes_generated := rand.read(result[10:]) + assert(bytes_generated == 6, "RNG failed to generate 6 bytes for UUID v1.") + } + + result[VERSION_BYTE_INDEX] |= 0x60 + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + /* Generate a version 7 UUID. diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index f31ae2bcd27..3b2c480117c 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -99,7 +99,7 @@ variant :: proc "contextless" (id: Identifier) -> (variant: Variant_Type) #no_bo } /* -Get the clock sequence of a version 1 UUID. +Get the clock sequence of a version 1 or version 6 UUID. Inputs: - id: The identifier. @@ -151,6 +151,24 @@ time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { return cast(u64)transmute(u64le)timestamp_octets } +/* +Get the timestamp of a version 6 UUID. + +Inputs: +- id: The identifier. + +Returns: +- timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. +*/ +time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { + temporary := transmute(u128be)id + + timestamp |= cast(u64)(temporary & 0xFFFFFFFF_FFFF0000_00000000_00000000 >> 68) + timestamp |= cast(u64)(temporary & 0x00000000_00000FFF_00000000_00000000 >> 64) + + return timestamp +} + /* Get the timestamp of a version 7 UUID. diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index f934d13dca1..90e299143e4 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -11,6 +11,7 @@ test_version_and_variant :: proc(t: ^testing.T) { v3 := uuid.generate_v3(uuid.Namespace_DNS, "") v4 := uuid.generate_v4() v5 := uuid.generate_v5(uuid.Namespace_DNS, "") + v6 := uuid.generate_v6() v7 := uuid.generate_v7() testing.expect_value(t, uuid.version(v1), 1) @@ -21,6 +22,8 @@ test_version_and_variant :: proc(t: ^testing.T) { testing.expect_value(t, uuid.variant(v4), uuid.Variant_Type.RFC_4122) testing.expect_value(t, uuid.version(v5), 5) testing.expect_value(t, uuid.variant(v5), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v6), 6) + testing.expect_value(t, uuid.variant(v6), uuid.Variant_Type.RFC_4122) testing.expect_value(t, uuid.version(v7), 7) testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122) } @@ -84,6 +87,34 @@ test_v1 :: proc(t: ^testing.T) { testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.") } +@(test) +test_v6 :: proc(t: ^testing.T) { + CLOCK :: 0x3A1A + v6_a := uuid.generate_v6(CLOCK) + time.sleep(10 * time.Millisecond) + v6_b := uuid.generate_v6(CLOCK) + time.sleep(10 * time.Millisecond) + v6_c := uuid.generate_v6(CLOCK) + + testing.expect_value(t, uuid.clock_seq(v6_a), CLOCK) + + time_bits_a := uuid.time_v6(v6_a) + time_bits_b := uuid.time_v6(v6_b) + time_bits_c := uuid.time_v6(v6_c) + + time_a := time.Time { _nsec = cast(i64)((time_bits_a - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } + time_b := time.Time { _nsec = cast(i64)((time_bits_b - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } + time_c := time.Time { _nsec = cast(i64)((time_bits_c - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } + + log.debugf("A: %02x, %i, %v", v6_a, time_bits_a, time_a) + log.debugf("B: %02x, %i, %v", v6_b, time_bits_b, time_b) + log.debugf("C: %02x, %i, %v", v6_c, time_bits_c, time_c) + + testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.") + testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.") + testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.") +} + @(test) test_v7 :: proc(t: ^testing.T) { v7_a := uuid.generate_v7() From 3aa232a894340ea13d2fc57090c8ad149f5c482a Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 13:04:21 -0400 Subject: [PATCH 08/23] Move v3 and v5 UUID procs to `uuid/legacy` --- core/encoding/uuid/generation.odin | 135 ----------------- core/encoding/uuid/legacy/legacy.odin | 150 +++++++++++++++++++ examples/all/all_main.odin | 2 + tests/core/encoding/uuid/test_core_uuid.odin | 11 +- 4 files changed, 158 insertions(+), 140 deletions(-) create mode 100644 core/encoding/uuid/legacy/legacy.odin diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 08fd84c0c37..2ca5d0e5f3f 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -1,7 +1,5 @@ package uuid -import "core:crypto/legacy/md5" -import "core:crypto/legacy/sha1" import "core:math/rand" import "core:mem" import "core:time" @@ -50,71 +48,6 @@ generate_v1 :: proc(clock_seq: u16, node: Maybe([6]u8) = nil) -> (result: Identi return } -/* -Generate a version 3 UUID. - -This UUID is generated from a name within a namespace. -MD5 is used to hash the name with the namespace to produce the UUID. - -Inputs: -- namespace: Another `Identifier` that is used to represent the underlying namespace. - This can be any one of the `Namespace_*` values provided in this package. -- name: The byte slice used to generate the name on top of the namespace. - -Returns: -- result: The generated UUID. -*/ -generate_v3_bytes :: proc( - namespace: Identifier, - name: []byte, -) -> ( - result: Identifier, -) { - namespace := namespace - - ctx: md5.Context - md5.init(&ctx) - md5.update(&ctx, namespace[:]) - md5.update(&ctx, name) - md5.final(&ctx, result[:]) - - result[VERSION_BYTE_INDEX] &= 0x0F - result[VERSION_BYTE_INDEX] |= 0x30 - - result[VARIANT_BYTE_INDEX] &= 0x3F - result[VARIANT_BYTE_INDEX] |= 0x80 - - return -} - -/* -Generate a version 3 UUID. - -This UUID is generated from a name within a namespace. -MD5 is used to hash the name with the namespace to produce the UUID. - -Inputs: -- namespace: Another `Identifier` that is used to represent the underlying namespace. - This can be any one of the `Namespace_*` values provided in this package. -- name: The string used to generate the name on top of the namespace. - -Returns: -- result: The generated UUID. -*/ -generate_v3_string :: proc( - namespace: Identifier, - name: string, -) -> ( - result: Identifier, -) { - return generate_v3_bytes(namespace, transmute([]byte)name) -} - -generate_v3 :: proc { - generate_v3_bytes, - generate_v3_string, -} - /* Generate a version 4 UUID. @@ -136,74 +69,6 @@ generate_v4 :: proc() -> (result: Identifier) { return } -/* -Generate a version 5 UUID. - -This UUID is generated from a name within a namespace. -SHA1 is used to hash the name with the namespace to produce the UUID. - -Inputs: -- namespace: Another `Identifier` that is used to represent the underlying namespace. - This can be any one of the `Namespace_*` values provided in this package. -- name: The byte slice used to generate the name on top of the namespace. - -Returns: -- result: The generated UUID. -*/ -generate_v5_bytes :: proc( - namespace: Identifier, - name: []byte, -) -> ( - result: Identifier, -) { - namespace := namespace - digest: [sha1.DIGEST_SIZE]byte - - ctx: sha1.Context - sha1.init(&ctx) - sha1.update(&ctx, namespace[:]) - sha1.update(&ctx, name) - sha1.final(&ctx, digest[:]) - - mem.copy_non_overlapping(&result, &digest, 16) - - result[VERSION_BYTE_INDEX] &= 0x0F - result[VERSION_BYTE_INDEX] |= 0x50 - - result[VARIANT_BYTE_INDEX] &= 0x3F - result[VARIANT_BYTE_INDEX] |= 0x80 - - return -} - -/* -Generate a version 5 UUID. - -This UUID is generated from a name within a namespace. -SHA1 is used to hash the name with the namespace to produce the UUID. - -Inputs: -- namespace: Another `Identifier` that is used to represent the underlying namespace. - This can be any one of the `Namespace_*` values provided in this package. -- name: The string used to generate the name on top of the namespace. - -Returns: -- result: The generated UUID. -*/ -generate_v5_string :: proc( - namespace: Identifier, - name: string, -) -> ( - result: Identifier, -) { - return generate_v5_bytes(namespace, transmute([]byte)name) -} - -generate_v5 :: proc { - generate_v5_bytes, - generate_v5_string, -} - /* Generate a version 6 UUID. diff --git a/core/encoding/uuid/legacy/legacy.odin b/core/encoding/uuid/legacy/legacy.odin new file mode 100644 index 00000000000..44057f1f874 --- /dev/null +++ b/core/encoding/uuid/legacy/legacy.odin @@ -0,0 +1,150 @@ +/* +package uuid/legacy implements versions 3 and 5 of UUID generation, both of +which are using hashing algorithms (MD5 and SHA1, respectively) that are known +these days to no longer be secure. +*/ +package uuid_legacy + +import "base:runtime" +import "core:crypto/legacy/md5" +import "core:crypto/legacy/sha1" +import "core:encoding/uuid" + +Identifier :: uuid.Identifier +VERSION_BYTE_INDEX :: uuid.VERSION_BYTE_INDEX +VARIANT_BYTE_INDEX :: uuid.VARIANT_BYTE_INDEX + + +/* +Generate a version 3 UUID. + +This UUID is generated from a name within a namespace. +MD5 is used to hash the name with the namespace to produce the UUID. + +Inputs: +- namespace: Another `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in the `uuid` package. +- name: The byte slice used to generate the name on top of the namespace. + +Returns: +- result: The generated UUID. +*/ +generate_v3_bytes :: proc( + namespace: Identifier, + name: []byte, +) -> ( + result: Identifier, +) { + namespace := namespace + + ctx: md5.Context + md5.init(&ctx) + md5.update(&ctx, namespace[:]) + md5.update(&ctx, name) + md5.final(&ctx, result[:]) + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x30 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Generate a version 3 UUID. + +This UUID is generated from a name within a namespace. +MD5 is used to hash the name with the namespace to produce the UUID. + +Inputs: +- namespace: Another `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in the `uuid` package. +- name: The string used to generate the name on top of the namespace. + +Returns: +- result: The generated UUID. +*/ +generate_v3_string :: proc( + namespace: Identifier, + name: string, +) -> ( + result: Identifier, +) { + return generate_v3_bytes(namespace, transmute([]byte)name) +} + +generate_v3 :: proc { + generate_v3_bytes, + generate_v3_string, +} + +/* +Generate a version 5 UUID. + +This UUID is generated from a name within a namespace. +SHA1 is used to hash the name with the namespace to produce the UUID. + +Inputs: +- namespace: Another `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in the `uuid` package. +- name: The byte slice used to generate the name on top of the namespace. + +Returns: +- result: The generated UUID. +*/ +generate_v5_bytes :: proc( + namespace: Identifier, + name: []byte, +) -> ( + result: Identifier, +) { + namespace := namespace + digest: [sha1.DIGEST_SIZE]byte + + ctx: sha1.Context + sha1.init(&ctx) + sha1.update(&ctx, namespace[:]) + sha1.update(&ctx, name) + sha1.final(&ctx, digest[:]) + + runtime.mem_copy_non_overlapping(&result, &digest, 16) + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x50 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Generate a version 5 UUID. + +This UUID is generated from a name within a namespace. +SHA1 is used to hash the name with the namespace to produce the UUID. + +Inputs: +- namespace: Another `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in the `uuid` package. +- name: The string used to generate the name on top of the namespace. + +Returns: +- result: The generated UUID. +*/ +generate_v5_string :: proc( + namespace: Identifier, + name: string, +) -> ( + result: Identifier, +) { + return generate_v5_bytes(namespace, transmute([]byte)name) +} + +generate_v5 :: proc { + generate_v5_bytes, + generate_v5_string, +} + diff --git a/examples/all/all_main.odin b/examples/all/all_main.odin index d39fbe79f2d..9c0d28a91e8 100644 --- a/examples/all/all_main.odin +++ b/examples/all/all_main.odin @@ -63,6 +63,7 @@ import xml "core:encoding/xml" import endian "core:encoding/endian" import cbor "core:encoding/cbor" import uuid "core:encoding/uuid" +import uuid_legacy "core:encoding/uuid/legacy" import fmt "core:fmt" import hash "core:hash" @@ -239,6 +240,7 @@ _ :: flags _ :: sysinfo _ :: unicode _ :: uuid +_ :: uuid_legacy _ :: utf8 _ :: utf8string _ :: utf16 diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 90e299143e4..fc2615fe094 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -1,6 +1,7 @@ package test_core_uuid import "core:encoding/uuid" +import uuid_legacy "core:encoding/uuid/legacy" import "core:log" import "core:testing" import "core:time" @@ -8,9 +9,9 @@ import "core:time" @(test) test_version_and_variant :: proc(t: ^testing.T) { v1 := uuid.generate_v1(0) - v3 := uuid.generate_v3(uuid.Namespace_DNS, "") + v3 := uuid_legacy.generate_v3(uuid.Namespace_DNS, "") v4 := uuid.generate_v4() - v5 := uuid.generate_v5(uuid.Namespace_DNS, "") + v5 := uuid_legacy.generate_v5(uuid.Namespace_DNS, "") v6 := uuid.generate_v6() v7 := uuid.generate_v7() @@ -29,7 +30,7 @@ test_version_and_variant :: proc(t: ^testing.T) { } @(test) -test_namespaced_uuids :: proc(t: ^testing.T) { +test_legacy_namespaced_uuids :: proc(t: ^testing.T) { TEST_NAME :: "0123456789ABCDEF0123456789ABCDEF" Expected_Result :: struct { @@ -45,8 +46,8 @@ test_namespaced_uuids :: proc(t: ^testing.T) { } for exp in Expected_Results { - v3 := uuid.generate_v3(exp.namespace, TEST_NAME) - v5 := uuid.generate_v5(exp.namespace, TEST_NAME) + v3 := uuid_legacy.generate_v3(exp.namespace, TEST_NAME) + v5 := uuid_legacy.generate_v5(exp.namespace, TEST_NAME) v3_str := uuid.to_string(v3) defer delete(v3_str) From fcdba334ea764edb5fe1e325cc5527e9495a6f55 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 13:21:48 -0400 Subject: [PATCH 09/23] Require CSPRNG in UUID generation where applicable --- core/encoding/uuid/definitions.odin | 3 +++ core/encoding/uuid/generation.odin | 12 +++++++++--- tests/core/encoding/uuid/test_core_uuid.odin | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin index a63e72693d5..6810b1cb608 100644 --- a/core/encoding/uuid/definitions.odin +++ b/core/encoding/uuid/definitions.odin @@ -16,6 +16,9 @@ VERSION_7_TIME_SHIFT :: 80 VERSION_7_COUNTER_MASK :: 0x00000000_00000fff_00000000_00000000 VERSION_7_COUNTER_SHIFT :: 64 +@(private) +NO_CSPRNG_ERROR :: "The context random generator is not cryptographic. See the documentation for an example of how to set one up." + Read_Error :: enum { None, Invalid_Length, diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 2ca5d0e5f3f..1afa810a21f 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -1,7 +1,7 @@ package uuid +import "base:runtime" import "core:math/rand" -import "core:mem" import "core:time" /* @@ -33,8 +33,9 @@ generate_v1 :: proc(clock_seq: u16, node: Maybe([6]u8) = nil) -> (result: Identi if realized_node, ok := node.?; ok { mutable_node := realized_node - mem.copy_non_overlapping(&result[10], &mutable_node[0], 6) + runtime.mem_copy_non_overlapping(&result[10], &mutable_node[0], 6) } else { + assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) bytes_generated := rand.read(result[10:]) assert(bytes_generated == 6, "RNG failed to generate 6 bytes for UUID v1.") } @@ -57,6 +58,7 @@ Returns: - result: The generated UUID. */ generate_v4 :: proc() -> (result: Identifier) { + assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) bytes_generated := rand.read(result[:]) assert(bytes_generated == 16, "RNG failed to generate 16 bytes for UUID v4.") @@ -94,6 +96,7 @@ generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil) -> (r result[8] |= cast(u8)(realized_clock_seq & 0x3F00 >> 8) result[9] = cast(u8)realized_clock_seq } else { + assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) temporary: [2]u8 bytes_generated := rand.read(temporary[:]) assert(bytes_generated == 2, "RNG failed to generate 2 bytes for UUID v1.") @@ -103,8 +106,9 @@ generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil) -> (r if realized_node, ok := node.?; ok { mutable_node := realized_node - mem.copy_non_overlapping(&result[10], &mutable_node[0], 6) + runtime.mem_copy_non_overlapping(&result[10], &mutable_node[0], 6) } else { + assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) bytes_generated := rand.read(result[10:]) assert(bytes_generated == 6, "RNG failed to generate 6 bytes for UUID v1.") } @@ -128,6 +132,7 @@ Returns: - result: The generated UUID. */ generate_v7 :: proc() -> (result: Identifier) { + assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT @@ -178,6 +183,7 @@ Returns: - result: The generated UUID. */ generate_v7_counter :: proc(counter: u16) -> (result: Identifier) { + assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) assert(counter <= 0x0fff, "This implementation of the version 7 UUID does not support counters in excess of 12 bits (4,095).") unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index fc2615fe094..a8f563e13e9 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -1,5 +1,6 @@ package test_core_uuid +import "core:crypto" import "core:encoding/uuid" import uuid_legacy "core:encoding/uuid/legacy" import "core:log" @@ -8,6 +9,8 @@ import "core:time" @(test) test_version_and_variant :: proc(t: ^testing.T) { + context.random_generator = crypto.random_generator() + v1 := uuid.generate_v1(0) v3 := uuid_legacy.generate_v3(uuid.Namespace_DNS, "") v4 := uuid.generate_v4() @@ -62,6 +65,8 @@ test_legacy_namespaced_uuids :: proc(t: ^testing.T) { @(test) test_v1 :: proc(t: ^testing.T) { + context.random_generator = crypto.random_generator() + CLOCK :: 0x3A1A v1_a := uuid.generate_v1(CLOCK) time.sleep(10 * time.Millisecond) @@ -90,6 +95,8 @@ test_v1 :: proc(t: ^testing.T) { @(test) test_v6 :: proc(t: ^testing.T) { + context.random_generator = crypto.random_generator() + CLOCK :: 0x3A1A v6_a := uuid.generate_v6(CLOCK) time.sleep(10 * time.Millisecond) @@ -118,6 +125,8 @@ test_v6 :: proc(t: ^testing.T) { @(test) test_v7 :: proc(t: ^testing.T) { + context.random_generator = crypto.random_generator() + v7_a := uuid.generate_v7() time.sleep(10 * time.Millisecond) v7_b := uuid.generate_v7() From ea771d0cb77254f3dd78e2c13230390b50cb2228 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 13:30:06 -0400 Subject: [PATCH 10/23] Update `uuid` package documentation --- core/encoding/uuid/doc.odin | 45 +++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/core/encoding/uuid/doc.odin b/core/encoding/uuid/doc.odin index a05698955da..6fa375b7212 100644 --- a/core/encoding/uuid/doc.odin +++ b/core/encoding/uuid/doc.odin @@ -1,15 +1,46 @@ /* package uuid implements Universally Unique Identifiers according to the -standard outlined in RFC 4122. +standard originally outlined in RFC 4122 with additions from RFC 9562. -See here for more information: https://www.rfc-editor.org/rfc/rfc4122.html +The UUIDs are textually represented and read in the following string format: +`00000000-0000-v000-V000-000000000000` -Generation of versions 1 and 2 (the MAC address-based versions) are not yet -implemented. +`v` is where the version bits reside, and `V` is where the variant bits reside. +The meaning of the other bits is version-dependent. + +Outside of string representations, UUIDs are represented in memory by a 128-bit +structure organized as an array of 16 bytes. + + +Of the UUID versions which may make use of random number generation, a +requirement is placed upon them that the underlying generator be +cryptographically-secure, per RFC 9562's suggestion. + +- Version 1 without a node argument. +- Version 4 in all cases. +- Version 6 without either a clock or node argument. +- Version 7 in all cases. + +Here's an example of how to set up one: + + import "core:crypto" + import "core:encoding/uuid" + + main :: proc() { + my_uuid: uuid.Identifier + + { + // This scope will have a CSPRNG. + context.random_generator = crypto.random_generator() + my_uuid = uuid.generate_v7() + } + + // Back to the default random number generator. + } -The UUIDs are textually represented and read in the following string format: -`00000000-0000-4000-8000-000000000000` -Outside of string representations, they are represented in memory by a 128-bit structure. +For more information on the specifications, see here: +- https://www.rfc-editor.org/rfc/rfc4122.html +- https://www.rfc-editor.org/rfc/rfc9562.html */ package uuid From 9b265b2309089656250fd001a61a379e160fa64f Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 14:29:16 -0400 Subject: [PATCH 11/23] Improve time-related API in `uuid` package - Let timestamps be specified by the user. - Change `time_v*` to `raw_time_v*` and implement an API that returns timestamps from the `time` package. --- core/encoding/uuid/generation.odin | 50 +++++---- core/encoding/uuid/reading.odin | 55 ++++++++-- tests/core/encoding/uuid/test_core_uuid.odin | 101 +++++++++---------- 3 files changed, 130 insertions(+), 76 deletions(-) diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 1afa810a21f..bf41b4a8c16 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -11,25 +11,27 @@ Inputs: - clock_seq: The clock sequence, a number which must be initialized to a random number once in the lifetime of a system. - node: An optional 48-bit spatially unique identifier, specified to be the IEEE 802 address of the system. If one is not provided or available, 48 bits of random state will take its place. +- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time. Returns: - result: The generated UUID. */ -generate_v1 :: proc(clock_seq: u16, node: Maybe([6]u8) = nil) -> (result: Identifier) { +generate_v1 :: proc(clock_seq: u16, node: Maybe([6]u8) = nil, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) { assert(clock_seq <= 0x3FFF, "The clock sequence can only hold 14 bits of data; no number greater than 16,383.") - unix_time_in_hns_intervals := time.to_unix_nanoseconds(time.now()) / 100 - timestamp := cast(u64le)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) - timestamp_octets := transmute([8]u8)timestamp + unix_time_in_hns_intervals := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 100 - result[0] = timestamp_octets[0] - result[1] = timestamp_octets[1] - result[2] = timestamp_octets[2] - result[3] = timestamp_octets[3] - result[4] = timestamp_octets[4] - result[5] = timestamp_octets[5] + uuid_timestamp := cast(u64le)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) + uuid_timestamp_octets := transmute([8]u8)uuid_timestamp - result[6] = timestamp_octets[6] >> 4 - result[7] = timestamp_octets[6] << 4 | timestamp_octets[7] + result[0] = uuid_timestamp_octets[0] + result[1] = uuid_timestamp_octets[1] + result[2] = uuid_timestamp_octets[2] + result[3] = uuid_timestamp_octets[3] + result[4] = uuid_timestamp_octets[4] + result[5] = uuid_timestamp_octets[5] + + result[6] = uuid_timestamp_octets[6] >> 4 + result[7] = uuid_timestamp_octets[6] << 4 | uuid_timestamp_octets[7] if realized_node, ok := node.?; ok { mutable_node := realized_node @@ -79,12 +81,13 @@ Inputs: If unspecified, it will be replaced with random bits. - node: An optional 48-bit spatially unique identifier, specified to be the IEEE 802 address of the system. If one is not provided or available, 48 bits of random state will take its place. +- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time. Returns: - result: The generated UUID. */ -generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil) -> (result: Identifier) { - unix_time_in_hns_intervals := time.to_unix_nanoseconds(time.now()) / 100 +generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) { + unix_time_in_hns_intervals := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 100 timestamp := cast(u128be)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) @@ -128,12 +131,15 @@ bits and a 48 bit timestamp. It is designed with time-based sorting in mind, such as for database usage, as the highest bits are allocated from the timestamp of when it is created. +Inputs: +- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time. + Returns: - result: The generated UUID. */ -generate_v7 :: proc() -> (result: Identifier) { +generate_v7_basic :: proc(timestamp: Maybe(time.Time) = nil) -> (result: Identifier) { assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) - unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 + unix_time_in_milliseconds := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 1e6 temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT @@ -152,7 +158,7 @@ generate_v7 :: proc() -> (result: Identifier) { } /* -Generate a version 7 UUID with an incremented counter. +Generate a version 7 UUID that has an incremented counter. This UUID will be pseudorandom, save for 6 pre-determined version and variant bits, a 48 bit timestamp, and 12 bits of counter state. @@ -178,14 +184,15 @@ Example: Inputs: - counter: A 12-bit value, incremented each time a UUID is generated in a batch. +- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time. Returns: - result: The generated UUID. */ -generate_v7_counter :: proc(counter: u16) -> (result: Identifier) { +generate_v7_with_counter :: proc(counter: u16, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) { assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) assert(counter <= 0x0fff, "This implementation of the version 7 UUID does not support counters in excess of 12 bits (4,095).") - unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 + unix_time_in_milliseconds := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 1e6 temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT temporary |= cast(u128be)counter << VERSION_7_COUNTER_SHIFT @@ -203,3 +210,8 @@ generate_v7_counter :: proc(counter: u16) -> (result: Identifier) { return } + +generate_v7 :: proc { + generate_v7_basic, + generate_v7_with_counter, +} diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index 3b2c480117c..c32f69eb773 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -1,6 +1,7 @@ package uuid import "base:runtime" +import "core:time" /* Convert a string to a UUID. @@ -127,7 +128,7 @@ node :: proc "contextless" (id: Identifier) -> (node: [6]u8) { } /* -Get the timestamp of a version 1 UUID. +Get the raw timestamp of a version 1 UUID. Inputs: - id: The identifier. @@ -135,7 +136,7 @@ Inputs: Returns: - timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. */ -time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { +raw_time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { timestamp_octets: [8]u8 timestamp_octets[0] = id[0] @@ -151,8 +152,23 @@ time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { return cast(u64)transmute(u64le)timestamp_octets } + /* -Get the timestamp of a version 6 UUID. +Get the timestamp of a version 1 UUID. + +Inputs: +- id: The identifier. + +Returns: +- timestamp: The timestamp of the UUID. +*/ +time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { + delta := cast(time.Duration)(raw_time_v1(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100 + return time.time_add({}, delta) +} + +/* +Get the raw timestamp of a version 6 UUID. Inputs: - id: The identifier. @@ -160,7 +176,7 @@ Inputs: Returns: - timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. */ -time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { +raw_time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { temporary := transmute(u128be)id timestamp |= cast(u64)(temporary & 0xFFFFFFFF_FFFF0000_00000000_00000000 >> 68) @@ -170,7 +186,21 @@ time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { } /* -Get the timestamp of a version 7 UUID. +Get the timestamp of a version 6 UUID. + +Inputs: +- id: The identifier. + +Returns: +- timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. +*/ +time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { + delta := cast(time.Duration)(raw_time_v6(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100 + return time.time_add({}, delta) +} + +/* +Get the raw timestamp of a version 7 UUID. Inputs: - id: The identifier. @@ -178,11 +208,24 @@ Inputs: Returns: - timestamp: The timestamp, in milliseconds since the UNIX epoch. */ -time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { +raw_time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { time_bits := transmute(u128be)id & VERSION_7_TIME_MASK return cast(u64)(time_bits >> VERSION_7_TIME_SHIFT) } +/* +Get the timestamp of a version 7 UUID. + +Inputs: +- id: The identifier. + +Returns: +- timestamp: The timestamp, in milliseconds since the UNIX epoch. +*/ +time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { + return time.time_add({}, cast(time.Duration)(raw_time_v7(id) * 1e6)) +} + /* Get the 12-bit counter value of a version 7 UUID. diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index a8f563e13e9..c7beb67d37a 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -67,87 +67,86 @@ test_legacy_namespaced_uuids :: proc(t: ^testing.T) { test_v1 :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() + point_a := time.time_add({}, 1 * time.Second) + point_b := time.time_add({}, 3 * time.Second) + point_c := time.time_add({}, 5 * time.Second) + CLOCK :: 0x3A1A - v1_a := uuid.generate_v1(CLOCK) - time.sleep(10 * time.Millisecond) - v1_b := uuid.generate_v1(CLOCK) - time.sleep(10 * time.Millisecond) - v1_c := uuid.generate_v1(CLOCK) + v1_a := uuid.generate_v1(CLOCK, nil, point_a) + v1_b := uuid.generate_v1(CLOCK, nil, point_b) + v1_c := uuid.generate_v1(CLOCK, nil, point_c) testing.expect_value(t, uuid.clock_seq(v1_a), CLOCK) - time_bits_a := uuid.time_v1(v1_a) - time_bits_b := uuid.time_v1(v1_b) - time_bits_c := uuid.time_v1(v1_c) - - time_a := time.Time { _nsec = cast(i64)((time_bits_a - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } - time_b := time.Time { _nsec = cast(i64)((time_bits_b - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } - time_c := time.Time { _nsec = cast(i64)((time_bits_c - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } + time_a := uuid.time_v1(v1_a) + time_b := uuid.time_v1(v1_b) + time_c := uuid.time_v1(v1_c) - log.debugf("A: %02x, %i, %v", v1_a, time_bits_a, time_a) - log.debugf("B: %02x, %i, %v", v1_b, time_bits_b, time_b) - log.debugf("C: %02x, %i, %v", v1_c, time_bits_c, time_c) + log.debugf("A: %02x, %v", v1_a, time_a) + log.debugf("B: %02x, %v", v1_b, time_b) + log.debugf("C: %02x, %v", v1_c, time_c) - testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.") - testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.") - testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.") + testing.expect(t, time.diff(time_a, time_b) > 0, "The time on the later-generated v1 UUID is earlier than its successor.") + testing.expect(t, time.diff(time_b, time_c) > 0, "The time on the later-generated v1 UUID is earlier than its successor.") + testing.expect(t, time.diff(time_a, time_c) > 0, "The time on the later-generated v1 UUID is earlier than its successor.") } @(test) test_v6 :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() + point_a := time.time_add({}, 1 * time.Second) + point_b := time.time_add({}, 3 * time.Second) + point_c := time.time_add({}, 5 * time.Second) + CLOCK :: 0x3A1A - v6_a := uuid.generate_v6(CLOCK) - time.sleep(10 * time.Millisecond) - v6_b := uuid.generate_v6(CLOCK) - time.sleep(10 * time.Millisecond) - v6_c := uuid.generate_v6(CLOCK) + v6_a := uuid.generate_v6(CLOCK, nil, point_a) + v6_b := uuid.generate_v6(CLOCK, nil, point_b) + v6_c := uuid.generate_v6(CLOCK, nil, point_c) testing.expect_value(t, uuid.clock_seq(v6_a), CLOCK) - time_bits_a := uuid.time_v6(v6_a) - time_bits_b := uuid.time_v6(v6_b) - time_bits_c := uuid.time_v6(v6_c) + time_a := uuid.time_v6(v6_a) + time_b := uuid.time_v6(v6_b) + time_c := uuid.time_v6(v6_c) - time_a := time.Time { _nsec = cast(i64)((time_bits_a - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } - time_b := time.Time { _nsec = cast(i64)((time_bits_b - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } - time_c := time.Time { _nsec = cast(i64)((time_bits_c - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } + log.debugf("A: %02x, %v", v6_a, time_a) + log.debugf("B: %02x, %v", v6_b, time_b) + log.debugf("C: %02x, %v", v6_c, time_c) - log.debugf("A: %02x, %i, %v", v6_a, time_bits_a, time_a) - log.debugf("B: %02x, %i, %v", v6_b, time_bits_b, time_b) - log.debugf("C: %02x, %i, %v", v6_c, time_bits_c, time_c) - - testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.") - testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.") - testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.") + testing.expect(t, time.diff(time_a, time_b) > 0, "The time on the later-generated v6 UUID is earlier than its successor.") + testing.expect(t, time.diff(time_b, time_c) > 0, "The time on the later-generated v6 UUID is earlier than its successor.") + testing.expect(t, time.diff(time_a, time_c) > 0, "The time on the later-generated v6 UUID is earlier than its successor.") } @(test) test_v7 :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() - v7_a := uuid.generate_v7() - time.sleep(10 * time.Millisecond) - v7_b := uuid.generate_v7() - time.sleep(10 * time.Millisecond) - v7_c := uuid.generate_v7() + point_a := time.time_add({}, 1 * time.Second) + point_b := time.time_add({}, 3 * time.Second) + point_c := time.time_add({}, 5 * time.Second) + + v7_a := uuid.generate_v7(point_a) + v7_b := uuid.generate_v7(point_b) + v7_c := uuid.generate_v7(point_c) - time_bits_a := uuid.time_v7(v7_a) - time_bits_b := uuid.time_v7(v7_b) - time_bits_c := uuid.time_v7(v7_c) + time_a := uuid.time_v7(v7_a) + time_b := uuid.time_v7(v7_b) + time_c := uuid.time_v7(v7_c) - log.debugf("A: %02x, %i", v7_a, time_bits_a) - log.debugf("B: %02x, %i", v7_b, time_bits_b) - log.debugf("C: %02x, %i", v7_c, time_bits_c) + log.debugf("A: %02x, %v", v7_a, time_a) + log.debugf("B: %02x, %v", v7_b, time_b) + log.debugf("C: %02x, %v", v7_c, time_c) - testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") - testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") - testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") + testing.expect(t, time.diff(time_a, time_b) > 0, "The time on the later-generated v7 UUID is earlier than its successor.") + testing.expect(t, time.diff(time_b, time_c) > 0, "The time on the later-generated v7 UUID is earlier than its successor.") + testing.expect(t, time.diff(time_a, time_c) > 0, "The time on the later-generated v7 UUID is earlier than its successor.") - v7_with_counter := uuid.generate_v7_counter(0x555) + v7_with_counter := uuid.generate_v7(0x555) log.debugf("D: %02x", v7_with_counter) testing.expect_value(t, uuid.counter_v7(v7_with_counter), 0x555) + } @(test) From 9b3a104640f518f21a83ae2c9784cd8601589880 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 14:38:45 -0400 Subject: [PATCH 12/23] Add buffer-based `to_string` to `uuid` package --- core/encoding/uuid/writing.odin | 31 +++++++++++++++++++- tests/core/encoding/uuid/test_core_uuid.odin | 11 +++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/core/encoding/uuid/writing.odin b/core/encoding/uuid/writing.odin index 45096e84f0a..27dc789c2ad 100644 --- a/core/encoding/uuid/writing.odin +++ b/core/encoding/uuid/writing.odin @@ -46,7 +46,7 @@ Returns: - str: The allocated and converted string. - error: An optional allocator error if one occured, `nil` otherwise. */ -to_string :: proc( +to_string_allocated :: proc( id: Identifier, allocator := context.allocator, loc := #caller_location, @@ -59,3 +59,32 @@ to_string :: proc( write(strings.to_writer(&builder), id) return strings.to_string(builder), nil } + +/* +Convert a UUID to a string in the 8-4-4-4-12 format. + +Inputs: +- id: The identifier to convert. +- buffer: A byte buffer to store the result. Must be at least 32 bytes large. +- loc: The caller location for debugging purposes (default: #caller_location) + +Returns: +- str: The converted string which will be stored in `buffer`. +*/ +to_string_buffer :: proc( + id: Identifier, + buffer: []byte, + loc := #caller_location, +) -> ( + str: string, +) { + assert(len(buffer) >= EXPECTED_LENGTH, "The buffer provided is not at least 32 bytes large.", loc) + builder := strings.builder_from_bytes(buffer) + write(strings.to_writer(&builder), id) + return strings.to_string(builder) +} + +to_string :: proc { + to_string_allocated, + to_string_buffer, +} diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index c7beb67d37a..460e4259bb0 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -157,10 +157,15 @@ test_writing :: proc(t: ^testing.T) { b = u8(i) } - s := uuid.to_string(id) - defer delete(s) + buf: [uuid.EXPECTED_LENGTH]u8 - testing.expect_value(t, s, "00010203-0405-0607-0809-0a0b0c0d0e0f") + s_alloc := uuid.to_string(id) + defer delete(s_alloc) + + s_buf := uuid.to_string(id, buf[:]) + + testing.expect_value(t, s_alloc, "00010203-0405-0607-0809-0a0b0c0d0e0f") + testing.expect_value(t, s_buf, "00010203-0405-0607-0809-0a0b0c0d0e0f") } @(test) From 95a9c9b016ab36110f7313105915ab45fea75ca3 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 14:40:09 -0400 Subject: [PATCH 13/23] Fix indentation --- tests/core/encoding/uuid/test_core_uuid.odin | 188 +++++++++---------- 1 file changed, 94 insertions(+), 94 deletions(-) diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 460e4259bb0..f50aca963ec 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -11,56 +11,56 @@ import "core:time" test_version_and_variant :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() - v1 := uuid.generate_v1(0) - v3 := uuid_legacy.generate_v3(uuid.Namespace_DNS, "") - v4 := uuid.generate_v4() - v5 := uuid_legacy.generate_v5(uuid.Namespace_DNS, "") - v6 := uuid.generate_v6() - v7 := uuid.generate_v7() - - testing.expect_value(t, uuid.version(v1), 1) - testing.expect_value(t, uuid.variant(v1), uuid.Variant_Type.RFC_4122) - testing.expect_value(t, uuid.version(v3), 3) - testing.expect_value(t, uuid.variant(v3), uuid.Variant_Type.RFC_4122) - testing.expect_value(t, uuid.version(v4), 4) - testing.expect_value(t, uuid.variant(v4), uuid.Variant_Type.RFC_4122) - testing.expect_value(t, uuid.version(v5), 5) - testing.expect_value(t, uuid.variant(v5), uuid.Variant_Type.RFC_4122) - testing.expect_value(t, uuid.version(v6), 6) - testing.expect_value(t, uuid.variant(v6), uuid.Variant_Type.RFC_4122) - testing.expect_value(t, uuid.version(v7), 7) - testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122) + v1 := uuid.generate_v1(0) + v3 := uuid_legacy.generate_v3(uuid.Namespace_DNS, "") + v4 := uuid.generate_v4() + v5 := uuid_legacy.generate_v5(uuid.Namespace_DNS, "") + v6 := uuid.generate_v6() + v7 := uuid.generate_v7() + + testing.expect_value(t, uuid.version(v1), 1) + testing.expect_value(t, uuid.variant(v1), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v3), 3) + testing.expect_value(t, uuid.variant(v3), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v4), 4) + testing.expect_value(t, uuid.variant(v4), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v5), 5) + testing.expect_value(t, uuid.variant(v5), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v6), 6) + testing.expect_value(t, uuid.variant(v6), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v7), 7) + testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122) } @(test) test_legacy_namespaced_uuids :: proc(t: ^testing.T) { - TEST_NAME :: "0123456789ABCDEF0123456789ABCDEF" + TEST_NAME :: "0123456789ABCDEF0123456789ABCDEF" - Expected_Result :: struct { - namespace: uuid.Identifier, - v3, v5: string, - } + Expected_Result :: struct { + namespace: uuid.Identifier, + v3, v5: string, + } - Expected_Results := [?]Expected_Result { - { uuid.Namespace_DNS, "80147f37-36db-3b82-b78f-810c3c6504ba", "18394c41-13a2-593f-abf2-a63e163c2860" }, - { uuid.Namespace_URL, "8136789b-8e16-3fbd-800b-1587e2f22521", "07337422-eb77-5fd3-99af-c7f59e641e13" }, - { uuid.Namespace_OID, "adbb95bc-ea50-3226-9a75-20c34a6030f8", "24db9b0f-70b8-53c4-a301-f695ce17276d" }, - { uuid.Namespace_X500, "a8965ad1-0e54-3d65-b933-8b7cca8e8313", "3012bf2d-fac4-5187-9825-493e6636b936" }, - } + Expected_Results := [?]Expected_Result { + { uuid.Namespace_DNS, "80147f37-36db-3b82-b78f-810c3c6504ba", "18394c41-13a2-593f-abf2-a63e163c2860" }, + { uuid.Namespace_URL, "8136789b-8e16-3fbd-800b-1587e2f22521", "07337422-eb77-5fd3-99af-c7f59e641e13" }, + { uuid.Namespace_OID, "adbb95bc-ea50-3226-9a75-20c34a6030f8", "24db9b0f-70b8-53c4-a301-f695ce17276d" }, + { uuid.Namespace_X500, "a8965ad1-0e54-3d65-b933-8b7cca8e8313", "3012bf2d-fac4-5187-9825-493e6636b936" }, + } - for exp in Expected_Results { - v3 := uuid_legacy.generate_v3(exp.namespace, TEST_NAME) - v5 := uuid_legacy.generate_v5(exp.namespace, TEST_NAME) + for exp in Expected_Results { + v3 := uuid_legacy.generate_v3(exp.namespace, TEST_NAME) + v5 := uuid_legacy.generate_v5(exp.namespace, TEST_NAME) - v3_str := uuid.to_string(v3) - defer delete(v3_str) + v3_str := uuid.to_string(v3) + defer delete(v3_str) - v5_str := uuid.to_string(v5) - defer delete(v5_str) + v5_str := uuid.to_string(v5) + defer delete(v5_str) - testing.expect_value(t, v3_str, exp.v3) - testing.expect_value(t, v5_str, exp.v5) - } + testing.expect_value(t, v3_str, exp.v3) + testing.expect_value(t, v5_str, exp.v5) + } } @(test) @@ -151,74 +151,74 @@ test_v7 :: proc(t: ^testing.T) { @(test) test_writing :: proc(t: ^testing.T) { - id: uuid.Identifier + id: uuid.Identifier - for &b, i in id { - b = u8(i) - } + for &b, i in id { + b = u8(i) + } buf: [uuid.EXPECTED_LENGTH]u8 - s_alloc := uuid.to_string(id) - defer delete(s_alloc) + s_alloc := uuid.to_string(id) + defer delete(s_alloc) s_buf := uuid.to_string(id, buf[:]) - testing.expect_value(t, s_alloc, "00010203-0405-0607-0809-0a0b0c0d0e0f") - testing.expect_value(t, s_buf, "00010203-0405-0607-0809-0a0b0c0d0e0f") + testing.expect_value(t, s_alloc, "00010203-0405-0607-0809-0a0b0c0d0e0f") + testing.expect_value(t, s_buf, "00010203-0405-0607-0809-0a0b0c0d0e0f") } @(test) test_reading :: proc(t: ^testing.T) { - id, err := uuid.read("00010203-0405-0607-0809-0a0b0c0d0e0f") - testing.expect_value(t, err, nil) + id, err := uuid.read("00010203-0405-0607-0809-0a0b0c0d0e0f") + testing.expect_value(t, err, nil) - for b, i in id { - testing.expect_value(t, b, u8(i)) - } + for b, i in id { + testing.expect_value(t, b, u8(i)) + } } @(test) test_reading_errors :: proc(t: ^testing.T) { - { - BAD_STRING :: "|.......@....@....@....@............" - _, err := uuid.read(BAD_STRING) - testing.expect_value(t, err, uuid.Read_Error.Invalid_Separator) - } - - { - BAD_STRING :: "|.......-....-....-....-............" - _, err := uuid.read(BAD_STRING) - testing.expect_value(t, err, uuid.Read_Error.Invalid_Hexadecimal) - } - - { - BAD_STRING :: ".......-....-....-....-............" - _, err := uuid.read(BAD_STRING) - testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) - } - - { - BAD_STRING :: "|.......-....-....-....-............|" - _, err := uuid.read(BAD_STRING) - testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) - } - - { - BAD_STRING :: "00000000-0000-0000-0000-0000000000001" - _, err := uuid.read(BAD_STRING) - testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) - } - - { - BAD_STRING :: "00000000000000000000000000000000" - _, err := uuid.read(BAD_STRING) - testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) - } - - { - OK_STRING :: "00000000-0000-0000-0000-000000000000" - _, err := uuid.read(OK_STRING) - testing.expect_value(t, err, nil) - } + { + BAD_STRING :: "|.......@....@....@....@............" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Separator) + } + + { + BAD_STRING :: "|.......-....-....-....-............" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Hexadecimal) + } + + { + BAD_STRING :: ".......-....-....-....-............" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) + } + + { + BAD_STRING :: "|.......-....-....-....-............|" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) + } + + { + BAD_STRING :: "00000000-0000-0000-0000-0000000000001" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) + } + + { + BAD_STRING :: "00000000000000000000000000000000" + _, err := uuid.read(BAD_STRING) + testing.expect_value(t, err, uuid.Read_Error.Invalid_Length) + } + + { + OK_STRING :: "00000000-0000-0000-0000-000000000000" + _, err := uuid.read(OK_STRING) + testing.expect_value(t, err, nil) + } } From 8b8f8c7f7d8fd9184e376a119730ad392d3aa4d1 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 15:07:29 -0400 Subject: [PATCH 14/23] Address minor organizational issues --- core/encoding/uuid/definitions.odin | 4 ++++ core/encoding/uuid/generation.odin | 32 ++++++++++++++--------------- core/encoding/uuid/reading.odin | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin index 6810b1cb608..fe13ca99a56 100644 --- a/core/encoding/uuid/definitions.odin +++ b/core/encoding/uuid/definitions.odin @@ -18,6 +18,10 @@ VERSION_7_COUNTER_SHIFT :: 64 @(private) NO_CSPRNG_ERROR :: "The context random generator is not cryptographic. See the documentation for an example of how to set one up." +@(private) +BIG_CLOCK_ERROR :: "The clock sequence can only hold 14 bits of data, therefore no number greater than 16,383 (0x3FFF)." +@(private) +VERSION_7_BIG_COUNTER_ERROR :: "This implementation of the version 7 UUID counter can only hold 12 bits of data, therefore no number greater than 4,095 (0xFFF)." Read_Error :: enum { None, diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index bf41b4a8c16..427d5243e9c 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -17,7 +17,7 @@ Returns: - result: The generated UUID. */ generate_v1 :: proc(clock_seq: u16, node: Maybe([6]u8) = nil, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) { - assert(clock_seq <= 0x3FFF, "The clock sequence can only hold 14 bits of data; no number greater than 16,383.") + assert(clock_seq <= 0x3FFF, BIG_CLOCK_ERROR) unix_time_in_hns_intervals := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 100 uuid_timestamp := cast(u64le)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) @@ -89,13 +89,15 @@ Returns: generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) { unix_time_in_hns_intervals := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 100 - timestamp := cast(u128be)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) + uuid_timestamp := cast(u128be)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) - result |= transmute(Identifier)(timestamp & 0x0FFFFFFF_FFFFF000 << 68) - result |= transmute(Identifier)(timestamp & 0x00000000_00000FFF << 64) + result = transmute(Identifier)( + uuid_timestamp & 0x0FFFFFFF_FFFFF000 << 68 | + uuid_timestamp & 0x00000000_00000FFF << 64 + ) if realized_clock_seq, ok := clock_seq.?; ok { - assert(realized_clock_seq <= 0x3FFF, "The clock sequence can only hold 14 bits of data, therefore no number greater than 16,383.") + assert(realized_clock_seq <= 0x3FFF, BIG_CLOCK_ERROR) result[8] |= cast(u8)(realized_clock_seq & 0x3F00 >> 8) result[9] = cast(u8)realized_clock_seq } else { @@ -126,7 +128,7 @@ generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil, times Generate a version 7 UUID. This UUID will be pseudorandom, save for 6 pre-determined version and variant -bits and a 48 bit timestamp. +bits and a 48-bit timestamp. It is designed with time-based sorting in mind, such as for database usage, as the highest bits are allocated from the timestamp of when it is created. @@ -141,13 +143,11 @@ generate_v7_basic :: proc(timestamp: Maybe(time.Time) = nil) -> (result: Identif assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) unix_time_in_milliseconds := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 1e6 - temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT + result = transmute(Identifier)(cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT) bytes_generated := rand.read(result[6:]) assert(bytes_generated == 10, "RNG failed to generate 10 bytes for UUID v7.") - result |= transmute(Identifier)temporary - result[VERSION_BYTE_INDEX] &= 0x0F result[VERSION_BYTE_INDEX] |= 0x70 @@ -161,7 +161,7 @@ generate_v7_basic :: proc(timestamp: Maybe(time.Time) = nil) -> (result: Identif Generate a version 7 UUID that has an incremented counter. This UUID will be pseudorandom, save for 6 pre-determined version and variant -bits, a 48 bit timestamp, and 12 bits of counter state. +bits, a 48-bit timestamp, and 12 bits of counter state. It is designed with time-based sorting in mind, such as for database usage, as the highest bits are allocated from the timestamp of when it is created. @@ -183,7 +183,7 @@ Example: } Inputs: -- counter: A 12-bit value, incremented each time a UUID is generated in a batch. +- counter: A 12-bit value which should be incremented each time a UUID is generated in a batch. - timestamp: A timestamp from the `core:time` package, or `nil` to use the current time. Returns: @@ -191,17 +191,17 @@ Returns: */ generate_v7_with_counter :: proc(counter: u16, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) { assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) - assert(counter <= 0x0fff, "This implementation of the version 7 UUID does not support counters in excess of 12 bits (4,095).") + assert(counter <= 0x0fff, VERSION_7_BIG_COUNTER_ERROR) unix_time_in_milliseconds := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 1e6 - temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT - temporary |= cast(u128be)counter << VERSION_7_COUNTER_SHIFT + result = transmute(Identifier)( + cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT | + cast(u128be)counter << VERSION_7_COUNTER_SHIFT + ) bytes_generated := rand.read(result[8:]) assert(bytes_generated == 8, "RNG failed to generate 8 bytes for UUID v7.") - result |= transmute(Identifier)temporary - result[VERSION_BYTE_INDEX] &= 0x0F result[VERSION_BYTE_INDEX] |= 0x70 diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index c32f69eb773..d41606f63de 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -113,7 +113,7 @@ clock_seq :: proc "contextless" (id: Identifier) -> (clock_seq: u16) { } /* -Get the node of a version 1 UUID. +Get the node of a version 1 or version 6 UUID. Inputs: - id: The identifier. From 339b2b23f695fb22d2b91fc79f75a288b27a9572 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 15:14:14 -0400 Subject: [PATCH 15/23] Add `unsafe_write` to `uuid` package --- core/encoding/uuid/writing.odin | 49 ++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/core/encoding/uuid/writing.odin b/core/encoding/uuid/writing.odin index 27dc789c2ad..499cba72b4e 100644 --- a/core/encoding/uuid/writing.odin +++ b/core/encoding/uuid/writing.odin @@ -8,12 +8,53 @@ import "core:strings" /* Write a UUID in the 8-4-4-4-12 format. +This procedure performs error checking with every byte written. + +If you can guarantee beforehand that your stream has enough space to hold the +UUID (32 bytes), then it is better to use `unsafe_write` instead as that will +be faster. + +Inputs: +- w: A writable stream. +- id: The identifier to convert. + +Returns: +- error: An `io` error, if one occurred, otherwise `nil`. +*/ +write :: proc(w: io.Writer, id: Identifier) -> (error: io.Error) #no_bounds_check { + write_octet :: proc (w: io.Writer, octet: u8) -> io.Error #no_bounds_check { + high_nibble := octet >> 4 + low_nibble := octet & 0xF + + io.write_byte(w, strconv.digits[high_nibble]) or_return + io.write_byte(w, strconv.digits[low_nibble]) or_return + return nil + } + + for index in 0 ..< 4 { write_octet(w, id[index]) or_return } + io.write_byte(w, '-') or_return + for index in 4 ..< 6 { write_octet(w, id[index]) or_return } + io.write_byte(w, '-') or_return + for index in 6 ..< 8 { write_octet(w, id[index]) or_return } + io.write_byte(w, '-') or_return + for index in 8 ..< 10 { write_octet(w, id[index]) or_return } + io.write_byte(w, '-') or_return + for index in 10 ..< 16 { write_octet(w, id[index]) or_return } + + return nil +} + +/* +Write a UUID in the 8-4-4-4-12 format. + +This procedure performs no error checking on the underlying stream. + Inputs: - w: A writable stream. - id: The identifier to convert. */ -write :: proc(w: io.Writer, id: Identifier) #no_bounds_check { - write_octet :: proc (w: io.Writer, octet: u8) { +unsafe_write :: proc(w: io.Writer, id: Identifier) #no_bounds_check { + write_octet :: proc (w: io.Writer, octet: u8) #no_bounds_check { high_nibble := octet >> 4 low_nibble := octet & 0xF @@ -56,7 +97,7 @@ to_string_allocated :: proc( ) #optional_allocator_error { buf := make([]byte, EXPECTED_LENGTH, allocator, loc) or_return builder := strings.builder_from_bytes(buf[:]) - write(strings.to_writer(&builder), id) + unsafe_write(strings.to_writer(&builder), id) return strings.to_string(builder), nil } @@ -80,7 +121,7 @@ to_string_buffer :: proc( ) { assert(len(buffer) >= EXPECTED_LENGTH, "The buffer provided is not at least 32 bytes large.", loc) builder := strings.builder_from_bytes(buffer) - write(strings.to_writer(&builder), id) + unsafe_write(strings.to_writer(&builder), id) return strings.to_string(builder) } From f6344577d3cf89bf1be6675bf34e8dd03752c9e1 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 15:30:47 -0400 Subject: [PATCH 16/23] Add UUID sorting tests --- tests/core/encoding/uuid/test_core_uuid.odin | 125 +++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index f50aca963ec..9f2d9c31285 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -4,6 +4,7 @@ import "core:crypto" import "core:encoding/uuid" import uuid_legacy "core:encoding/uuid/legacy" import "core:log" +import "core:slice" import "core:testing" import "core:time" @@ -146,7 +147,131 @@ test_v7 :: proc(t: ^testing.T) { v7_with_counter := uuid.generate_v7(0x555) log.debugf("D: %02x", v7_with_counter) testing.expect_value(t, uuid.counter_v7(v7_with_counter), 0x555) +} + +@(test) +test_sorting_v1 :: proc(t: ^testing.T) { + // This test is to make sure that the v1 UUIDs do _not_ sort. + // They are incapable of sorting properly by the nature their time bit ordering. + // + // Something is very strange if they do sort correctly. + point_a := time.time_add({}, 1 * time.Second) + point_b := time.time_add({}, 3 * time.Second) + point_c := time.time_add({}, 5 * time.Second) + point_d := time.time_add({}, 7 * time.Second) + point_e := time.time_add({}, 11 * time.Second) + + mac: [6]byte + v1_a := uuid.generate_v1(0, mac, point_a) + v1_b := uuid.generate_v1(0, mac, point_b) + v1_c := uuid.generate_v1(0, mac, point_c) + v1_d := uuid.generate_v1(0, mac, point_d) + v1_e := uuid.generate_v1(0, mac, point_e) + + sort_test := [5]u128be { + transmute(u128be)v1_e, + transmute(u128be)v1_a, + transmute(u128be)v1_d, + transmute(u128be)v1_b, + transmute(u128be)v1_c, + } + + log.debugf("Before: %x", sort_test) + slice.sort(sort_test[:]) + log.debugf("After: %x", sort_test) + + ERROR :: "v1 UUIDs are sorting by time, despite this not being possible." + + testing.expect(t, sort_test[0] != transmute(u128be)v1_a, ERROR) + testing.expect(t, sort_test[1] != transmute(u128be)v1_b, ERROR) + testing.expect(t, sort_test[2] != transmute(u128be)v1_c, ERROR) + testing.expect(t, sort_test[3] != transmute(u128be)v1_d, ERROR) + testing.expect(t, sort_test[4] != transmute(u128be)v1_e, ERROR) +} + +@(test) +test_sorting_v6 :: proc(t: ^testing.T) { + context.random_generator = crypto.random_generator() + + point_a := time.time_add({}, 1 * time.Second) + point_b := time.time_add({}, 3 * time.Second) + point_c := time.time_add({}, 5 * time.Second) + point_d := time.time_add({}, 7 * time.Second) + point_e := time.time_add({}, 11 * time.Second) + + mac: [6]byte + v6_a := uuid.generate_v6(0, mac, point_a) + v6_b := uuid.generate_v6(0, mac, point_b) + v6_c := uuid.generate_v6(0, mac, point_c) + v6_d := uuid.generate_v6(0, mac, point_d) + v6_e := uuid.generate_v6(0, mac, point_e) + + sort_test := [5]u128be { + transmute(u128be)v6_e, + transmute(u128be)v6_a, + transmute(u128be)v6_d, + transmute(u128be)v6_b, + transmute(u128be)v6_c, + } + + log.debugf("Before: %x", sort_test) + slice.sort(sort_test[:]) + log.debugf("After: %x", sort_test) + + ERROR :: "v6 UUIDs are failing to sort properly." + + testing.expect(t, sort_test[0] < sort_test[1], ERROR) + testing.expect(t, sort_test[1] < sort_test[2], ERROR) + testing.expect(t, sort_test[2] < sort_test[3], ERROR) + testing.expect(t, sort_test[3] < sort_test[4], ERROR) + + testing.expect(t, sort_test[0] == transmute(u128be)v6_a, ERROR) + testing.expect(t, sort_test[1] == transmute(u128be)v6_b, ERROR) + testing.expect(t, sort_test[2] == transmute(u128be)v6_c, ERROR) + testing.expect(t, sort_test[3] == transmute(u128be)v6_d, ERROR) + testing.expect(t, sort_test[4] == transmute(u128be)v6_e, ERROR) +} + +@(test) +test_sorting_v7 :: proc(t: ^testing.T) { + context.random_generator = crypto.random_generator() + + point_a := time.time_add({}, 1 * time.Second) + point_b := time.time_add({}, 3 * time.Second) + point_c := time.time_add({}, 5 * time.Second) + point_d := time.time_add({}, 7 * time.Second) + point_e := time.time_add({}, 11 * time.Second) + + v7_a := uuid.generate_v7(point_a) + v7_b := uuid.generate_v7(point_b) + v7_c := uuid.generate_v7(point_c) + v7_d := uuid.generate_v7(point_d) + v7_e := uuid.generate_v7(point_e) + + sort_test := [5]u128be { + transmute(u128be)v7_e, + transmute(u128be)v7_a, + transmute(u128be)v7_d, + transmute(u128be)v7_b, + transmute(u128be)v7_c, + } + + log.debugf("Before: %x", sort_test) + slice.sort(sort_test[:]) + log.debugf("After: %x", sort_test) + + ERROR :: "v7 UUIDs are failing to sort properly." + + testing.expect(t, sort_test[0] < sort_test[1], ERROR) + testing.expect(t, sort_test[1] < sort_test[2], ERROR) + testing.expect(t, sort_test[2] < sort_test[3], ERROR) + testing.expect(t, sort_test[3] < sort_test[4], ERROR) + testing.expect(t, sort_test[0] == transmute(u128be)v7_a, ERROR) + testing.expect(t, sort_test[1] == transmute(u128be)v7_b, ERROR) + testing.expect(t, sort_test[2] == transmute(u128be)v7_c, ERROR) + testing.expect(t, sort_test[3] == transmute(u128be)v7_d, ERROR) + testing.expect(t, sort_test[4] == transmute(u128be)v7_e, ERROR) } @(test) From d559feb7011fd1c75ae66c9aed38536dcd41a2cf Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 15:49:48 -0400 Subject: [PATCH 17/23] Add `uuid` test for timestamps --- tests/core/encoding/uuid/test_core_uuid.odin | 48 ++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 9f2d9c31285..895de386366 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -33,6 +33,54 @@ test_version_and_variant :: proc(t: ^testing.T) { testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122) } +@(test) +test_timestamps :: proc(t: ^testing.T) { + // This test makes sure that timestamps are recoverable and have not been + // overwritten by neighboring bits, taking into account precision loss. + context.random_generator = crypto.random_generator() + + N :: max(i64) + + max_time := time.Time { N } + + mac: [6]byte + v1 := uuid.generate_v1(0, mac, max_time) + v6 := uuid.generate_v6(0, mac, max_time) + v7 := uuid.generate_v7(max_time) + // The counter version keeps its time in the same place as the basic version, + // this is just for the sake of completeness. + v7_counter := uuid.generate_v7(0, max_time) + + v1_time := uuid.time_v1(v1) + v6_time := uuid.time_v6(v6) + v7_time := uuid.time_v7(v7) + v7_counter_time := uuid.time_v7(v7_counter) + + // I hope the compiler doesn't ever optimize this out. + max_time_hns_resolution := time.Time { N / 100 * 100 } + max_time_ms_resolution := time.Time { N / 1e6 * 1e6 } + + testing.expectf(t, + time.diff(max_time_hns_resolution, v1_time) == 0, + "v1 UUID timestamp is invalid, expected %x, got %x", + max_time_hns_resolution, v1_time) + + testing.expectf(t, + time.diff(max_time_hns_resolution, v6_time) == 0, + "v6 UUID timestamp is invalid, expected %x, got %x", + max_time_hns_resolution, v6_time) + + testing.expectf(t, + time.diff(max_time_ms_resolution, v7_time) == 0, + "v7 UUID timestamp is invalid, expected %x, got %x", + max_time_ms_resolution, v7_time) + + testing.expectf(t, + time.diff(max_time_ms_resolution, v7_counter_time) == 0, + "v7 UUID (with counter) timestamp is invalid, expected %x, got %x", + max_time_ms_resolution, v7_counter_time) +} + @(test) test_legacy_namespaced_uuids :: proc(t: ^testing.T) { TEST_NAME :: "0123456789ABCDEF0123456789ABCDEF" From 859cbf7d72ffc080f6e0224980bbed7458f1d07e Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 15:54:15 -0400 Subject: [PATCH 18/23] Test if v1 and v6 UUID `node` is set correctly --- tests/core/encoding/uuid/test_core_uuid.odin | 26 +++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 895de386366..3a0d621ca5f 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -121,12 +121,19 @@ test_v1 :: proc(t: ^testing.T) { point_c := time.time_add({}, 5 * time.Second) CLOCK :: 0x3A1A - v1_a := uuid.generate_v1(CLOCK, nil, point_a) - v1_b := uuid.generate_v1(CLOCK, nil, point_b) - v1_c := uuid.generate_v1(CLOCK, nil, point_c) + mac := [6]u8{0xFF, 0x10, 0xAA, 0x55, 0x01, 0xFF} + + v1_a := uuid.generate_v1(CLOCK, mac, point_a) + v1_b := uuid.generate_v1(CLOCK, mac, point_b) + v1_c := uuid.generate_v1(CLOCK, mac, point_c) testing.expect_value(t, uuid.clock_seq(v1_a), CLOCK) + extracted_mac := uuid.node(v1_a) + for i in 0 ..< len(mac) { + testing.expect(t, mac[i] == extracted_mac[i]) + } + time_a := uuid.time_v1(v1_a) time_b := uuid.time_v1(v1_b) time_c := uuid.time_v1(v1_c) @@ -149,12 +156,19 @@ test_v6 :: proc(t: ^testing.T) { point_c := time.time_add({}, 5 * time.Second) CLOCK :: 0x3A1A - v6_a := uuid.generate_v6(CLOCK, nil, point_a) - v6_b := uuid.generate_v6(CLOCK, nil, point_b) - v6_c := uuid.generate_v6(CLOCK, nil, point_c) + mac := [6]u8{0xFF, 0x10, 0xAA, 0x55, 0x01, 0xFF} + + v6_a := uuid.generate_v6(CLOCK, mac, point_a) + v6_b := uuid.generate_v6(CLOCK, mac, point_b) + v6_c := uuid.generate_v6(CLOCK, mac, point_c) testing.expect_value(t, uuid.clock_seq(v6_a), CLOCK) + extracted_mac := uuid.node(v6_a) + for i in 0 ..< len(mac) { + testing.expect(t, mac[i] == extracted_mac[i]) + } + time_a := uuid.time_v6(v6_a) time_b := uuid.time_v6(v6_b) time_c := uuid.time_v6(v6_c) From e9b882be058f08f014ea0e598c18b5df05a86a8a Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 16:51:37 -0400 Subject: [PATCH 19/23] Add vendor-specific version 8 UUID generation (hashing) --- core/encoding/uuid/generation.odin | 116 +++++++++++++++++++ tests/core/encoding/uuid/test_core_uuid.odin | 14 +++ 2 files changed, 130 insertions(+) diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 427d5243e9c..cf29df37811 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -1,6 +1,7 @@ package uuid import "base:runtime" +import "core:crypto/hash" import "core:math/rand" import "core:time" @@ -215,3 +216,118 @@ generate_v7 :: proc { generate_v7_basic, generate_v7_with_counter, } + +/* +Generate a version 8 UUID using a specific hashing algorithm. + +This UUID is generated by hashing a name with a namespace. + +Note that all version 8 UUIDs are for experimental or vendor-specific use +cases, per the specification. This use case in particular is for offering a +non-legacy alternative to UUID versions 3 and 5. + +Inputs: +- namespace: An `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in this package. +- name: The byte slice which will be hashed with the namespace. +- algorithm: A hashing algorithm from `core:crypto/hash`. + +Returns: +- result: The generated UUID. + +Example: + import "core:crypto/hash" + import "core:encoding/uuid" + import "core:fmt" + + main :: proc() { + my_uuid := uuid.generate_v8_hash(uuid.Namespace_DNS, "www.odin-lang.org", .SHA256) + my_uuid_string := uuid.to_string(my_uuid, context.temp_allocator) + fmt.println(my_uuid_string) + } + +Output: + + 3730f688-4bff-8dce-9cbf-74a3960c5703 + +*/ +generate_v8_hash_bytes :: proc( + namespace: Identifier, + name: []byte, + algorithm: hash.Algorithm, +) -> ( + result: Identifier, +) { + // 128 bytes should be enough for the foreseeable future. + digest: [128]byte + + assert(hash.DIGEST_SIZES[algorithm] >= 16, "Per RFC 9562, the hashing algorithm used must generate a digest of 128 bits or larger.") + assert(hash.DIGEST_SIZES[algorithm] < len(digest), "Digest size is too small for this algorithm. The buffer must be increased.") + + hash_context: hash.Context + hash.init(&hash_context, algorithm) + + mutable_namespace := namespace + hash.update(&hash_context, mutable_namespace[:]) + hash.update(&hash_context, name[:]) + hash.final(&hash_context, digest[:]) + + runtime.mem_copy_non_overlapping(&result, &digest, 16) + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x80 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Generate a version 8 UUID using a specific hashing algorithm. + +This UUID is generated by hashing a name with a namespace. + +Note that all version 8 UUIDs are for experimental or vendor-specific use +cases, per the specification. This use case in particular is for offering a +non-legacy alternative to UUID versions 3 and 5. + +Inputs: +- namespace: An `Identifier` that is used to represent the underlying namespace. + This can be any one of the `Namespace_*` values provided in this package. +- name: The string which will be hashed with the namespace. +- algorithm: A hashing algorithm from `core:crypto/hash`. + +Returns: +- result: The generated UUID. + +Example: + import "core:crypto/hash" + import "core:encoding/uuid" + import "core:fmt" + + main :: proc() { + my_uuid := uuid.generate_v8_hash(uuid.Namespace_DNS, "www.odin-lang.org", .SHA256) + my_uuid_string := uuid.to_string(my_uuid, context.temp_allocator) + fmt.println(my_uuid_string) + } + +Output: + + 3730f688-4bff-8dce-9cbf-74a3960c5703 + +*/ +generate_v8_hash_string :: proc( + namespace: Identifier, + name: string, + algorithm: hash.Algorithm, +) -> ( + result: Identifier, +) { + return generate_v8_hash_bytes(namespace, transmute([]byte)name, algorithm) +} + +generate_v8_hash :: proc { + generate_v8_hash_bytes, + generate_v8_hash_string, +} diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 3a0d621ca5f..aeb73c841c6 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -18,6 +18,7 @@ test_version_and_variant :: proc(t: ^testing.T) { v5 := uuid_legacy.generate_v5(uuid.Namespace_DNS, "") v6 := uuid.generate_v6() v7 := uuid.generate_v7() + v8 := uuid.generate_v8_hash(uuid.Namespace_DNS, "", .SHA512) testing.expect_value(t, uuid.version(v1), 1) testing.expect_value(t, uuid.variant(v1), uuid.Variant_Type.RFC_4122) @@ -31,6 +32,8 @@ test_version_and_variant :: proc(t: ^testing.T) { testing.expect_value(t, uuid.variant(v6), uuid.Variant_Type.RFC_4122) testing.expect_value(t, uuid.version(v7), 7) testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v8), 8) + testing.expect_value(t, uuid.variant(v8), uuid.Variant_Type.RFC_4122) } @(test) @@ -81,6 +84,17 @@ test_timestamps :: proc(t: ^testing.T) { max_time_ms_resolution, v7_counter_time) } +@(test) +test_v8_hash_implementation :: proc(t: ^testing.T) { + // This example and its results are derived from RFC 9562. + // https://www.rfc-editor.org/rfc/rfc9562.html#name-example-of-a-uuidv8-value-n + + id := uuid.generate_v8_hash(uuid.Namespace_DNS, "www.example.com", .SHA256) + id_str := uuid.to_string(id) + defer delete(id_str) + testing.expect_value(t, id_str, "5c146b14-3c52-8afd-938a-375d0df1fbf6") +} + @(test) test_legacy_namespaced_uuids :: proc(t: ^testing.T) { TEST_NAME :: "0123456789ABCDEF0123456789ABCDEF" From 5a75cac5b9d9173d2d2f23baf189dacb21f8ae83 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 17:06:32 -0400 Subject: [PATCH 20/23] Add API for creating custom version 8 UUIDs --- core/encoding/uuid/stamping.odin | 89 ++++++++++++++++++++ tests/core/encoding/uuid/test_core_uuid.odin | 20 ++++- 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 core/encoding/uuid/stamping.odin diff --git a/core/encoding/uuid/stamping.odin b/core/encoding/uuid/stamping.odin new file mode 100644 index 00000000000..0c07725c35c --- /dev/null +++ b/core/encoding/uuid/stamping.odin @@ -0,0 +1,89 @@ +package uuid + +import "base:runtime" + +/* +Stamp a 128-bit integer as being a valid version 8 UUID. + +Per the specification, all version 8 UUIDs are either for experimental or +vendor-specific purposes. This procedure allows for converting arbitrary data +into custom UUIDs. + +Inputs: +- integer: Any integer type. + +Returns: +- result: A valid version 8 UUID. +*/ +stamp_v8_int :: proc(#any_int integer: u128) -> (result: Identifier) { + result = transmute(Identifier)cast(u128be)integer + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x80 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Stamp an array of 16 bytes as being a valid version 8 UUID. + +Per the specification, all version 8 UUIDs are either for experimental or +vendor-specific purposes. This procedure allows for converting arbitrary data +into custom UUIDs. + +Inputs: +- array: An array of 16 bytes. + +Returns: +- result: A valid version 8 UUID. +*/ +stamp_v8_array :: proc(array: [16]u8) -> (result: Identifier) { + result = transmute(Identifier)array + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x80 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Stamp a slice of bytes as being a valid version 8 UUID. + +If the slice is less than 16 bytes long, the data available will be used. +If it is longer than 16 bytes, only the first 16 will be used. + +This procedure does not modify the underlying slice. + +Per the specification, all version 8 UUIDs are either for experimental or +vendor-specific purposes. This procedure allows for converting arbitrary data +into custom UUIDs. + +Inputs: +- slice: A slice of bytes. + +Returns: +- result: A valid version 8 UUID. +*/ +stamp_v8_slice :: proc(slice: []u8) -> (result: Identifier) { + runtime.mem_copy_non_overlapping(&result, &slice[0], min(16, len(slice))) + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x80 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +stamp_v8 :: proc { + stamp_v8_int, + stamp_v8_array, + stamp_v8_slice, +} diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index aeb73c841c6..9f66b316d25 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -18,7 +18,13 @@ test_version_and_variant :: proc(t: ^testing.T) { v5 := uuid_legacy.generate_v5(uuid.Namespace_DNS, "") v6 := uuid.generate_v6() v7 := uuid.generate_v7() - v8 := uuid.generate_v8_hash(uuid.Namespace_DNS, "", .SHA512) + + _v8_array: [16]u8 = 0xff + v8_int := uuid.stamp_v8(max(u128)) + v8_array := uuid.stamp_v8(_v8_array) + v8_slice := uuid.stamp_v8(_v8_array[:]) + + v8_hash := uuid.generate_v8_hash(uuid.Namespace_DNS, "", .SHA512) testing.expect_value(t, uuid.version(v1), 1) testing.expect_value(t, uuid.variant(v1), uuid.Variant_Type.RFC_4122) @@ -32,8 +38,16 @@ test_version_and_variant :: proc(t: ^testing.T) { testing.expect_value(t, uuid.variant(v6), uuid.Variant_Type.RFC_4122) testing.expect_value(t, uuid.version(v7), 7) testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122) - testing.expect_value(t, uuid.version(v8), 8) - testing.expect_value(t, uuid.variant(v8), uuid.Variant_Type.RFC_4122) + + testing.expect_value(t, uuid.version(v8_int), 8) + testing.expect_value(t, uuid.variant(v8_int), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v8_array), 8) + testing.expect_value(t, uuid.variant(v8_array), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v8_slice), 8) + testing.expect_value(t, uuid.variant(v8_slice), uuid.Variant_Type.RFC_4122) + + testing.expect_value(t, uuid.version(v8_hash), 8) + testing.expect_value(t, uuid.variant(v8_hash), uuid.Variant_Type.RFC_4122) } @(test) From 4481f9c695061782f8ff086fc13c58f13ab43023 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 17:07:18 -0400 Subject: [PATCH 21/23] Clarify some `uuid` legacy documentation --- core/encoding/uuid/legacy/legacy.odin | 28 ++++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/core/encoding/uuid/legacy/legacy.odin b/core/encoding/uuid/legacy/legacy.odin index 44057f1f874..d5f3df61762 100644 --- a/core/encoding/uuid/legacy/legacy.odin +++ b/core/encoding/uuid/legacy/legacy.odin @@ -18,13 +18,12 @@ VARIANT_BYTE_INDEX :: uuid.VARIANT_BYTE_INDEX /* Generate a version 3 UUID. -This UUID is generated from a name within a namespace. -MD5 is used to hash the name with the namespace to produce the UUID. +This UUID is generated with a MD5 hash of a name and a namespace. Inputs: -- namespace: Another `Identifier` that is used to represent the underlying namespace. +- namespace: An `Identifier` that is used to represent the underlying namespace. This can be any one of the `Namespace_*` values provided in the `uuid` package. -- name: The byte slice used to generate the name on top of the namespace. +- name: The byte slice which will be hashed with the namespace. Returns: - result: The generated UUID. @@ -55,13 +54,12 @@ generate_v3_bytes :: proc( /* Generate a version 3 UUID. -This UUID is generated from a name within a namespace. -MD5 is used to hash the name with the namespace to produce the UUID. +This UUID is generated with a MD5 hash of a name and a namespace. Inputs: -- namespace: Another `Identifier` that is used to represent the underlying namespace. +- namespace: An `Identifier` that is used to represent the underlying namespace. This can be any one of the `Namespace_*` values provided in the `uuid` package. -- name: The string used to generate the name on top of the namespace. +- name: The string which will be hashed with the namespace. Returns: - result: The generated UUID. @@ -83,13 +81,12 @@ generate_v3 :: proc { /* Generate a version 5 UUID. -This UUID is generated from a name within a namespace. -SHA1 is used to hash the name with the namespace to produce the UUID. +This UUID is generated with a SHA1 hash of a name and a namespace. Inputs: -- namespace: Another `Identifier` that is used to represent the underlying namespace. +- namespace: An `Identifier` that is used to represent the underlying namespace. This can be any one of the `Namespace_*` values provided in the `uuid` package. -- name: The byte slice used to generate the name on top of the namespace. +- name: The byte slice which will be hashed with the namespace. Returns: - result: The generated UUID. @@ -123,13 +120,12 @@ generate_v5_bytes :: proc( /* Generate a version 5 UUID. -This UUID is generated from a name within a namespace. -SHA1 is used to hash the name with the namespace to produce the UUID. +This UUID is generated with a SHA1 hash of a name and a namespace. Inputs: -- namespace: Another `Identifier` that is used to represent the underlying namespace. +- namespace: An `Identifier` that is used to represent the underlying namespace. This can be any one of the `Namespace_*` values provided in the `uuid` package. -- name: The string used to generate the name on top of the namespace. +- name: The string which will be hashed with the namespace. Returns: - result: The generated UUID. From 8a4a3ed66e9fc2e4ecc398acb616a6c3b756515e Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 17:58:42 -0400 Subject: [PATCH 22/23] Change how `Time` is constructed in `uuid` --- core/encoding/uuid/reading.odin | 8 ++-- tests/core/encoding/uuid/test_core_uuid.odin | 48 ++++++++++---------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index d41606f63de..ab08300b060 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -163,8 +163,7 @@ Returns: - timestamp: The timestamp of the UUID. */ time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { - delta := cast(time.Duration)(raw_time_v1(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100 - return time.time_add({}, delta) + return { _nsec = cast(i64)(raw_time_v1(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100 } } /* @@ -195,8 +194,7 @@ Returns: - timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. */ time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { - delta := cast(time.Duration)(raw_time_v6(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100 - return time.time_add({}, delta) + return { _nsec = cast(i64)(raw_time_v6(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100 } } /* @@ -223,7 +221,7 @@ Returns: - timestamp: The timestamp, in milliseconds since the UNIX epoch. */ time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { - return time.time_add({}, cast(time.Duration)(raw_time_v7(id) * 1e6)) + return { _nsec = cast(i64)raw_time_v7(id) * 1e6 } } /* diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 9f66b316d25..619534a7fb5 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -144,9 +144,9 @@ test_legacy_namespaced_uuids :: proc(t: ^testing.T) { test_v1 :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() - point_a := time.time_add({}, 1 * time.Second) - point_b := time.time_add({}, 3 * time.Second) - point_c := time.time_add({}, 5 * time.Second) + point_a := time.unix(1, 0) + point_b := time.unix(3, 0) + point_c := time.unix(5, 0) CLOCK :: 0x3A1A mac := [6]u8{0xFF, 0x10, 0xAA, 0x55, 0x01, 0xFF} @@ -179,9 +179,9 @@ test_v1 :: proc(t: ^testing.T) { test_v6 :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() - point_a := time.time_add({}, 1 * time.Second) - point_b := time.time_add({}, 3 * time.Second) - point_c := time.time_add({}, 5 * time.Second) + point_a := time.unix(1, 0) + point_b := time.unix(3, 0) + point_c := time.unix(5, 0) CLOCK :: 0x3A1A mac := [6]u8{0xFF, 0x10, 0xAA, 0x55, 0x01, 0xFF} @@ -214,9 +214,9 @@ test_v6 :: proc(t: ^testing.T) { test_v7 :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() - point_a := time.time_add({}, 1 * time.Second) - point_b := time.time_add({}, 3 * time.Second) - point_c := time.time_add({}, 5 * time.Second) + point_a := time.unix(1, 0) + point_b := time.unix(3, 0) + point_c := time.unix(5, 0) v7_a := uuid.generate_v7(point_a) v7_b := uuid.generate_v7(point_b) @@ -245,11 +245,11 @@ test_sorting_v1 :: proc(t: ^testing.T) { // They are incapable of sorting properly by the nature their time bit ordering. // // Something is very strange if they do sort correctly. - point_a := time.time_add({}, 1 * time.Second) - point_b := time.time_add({}, 3 * time.Second) - point_c := time.time_add({}, 5 * time.Second) - point_d := time.time_add({}, 7 * time.Second) - point_e := time.time_add({}, 11 * time.Second) + point_a := time.unix(1, 0) + point_b := time.unix(3, 0) + point_c := time.unix(5, 0) + point_d := time.unix(7, 0) + point_e := time.unix(11, 0) mac: [6]byte v1_a := uuid.generate_v1(0, mac, point_a) @@ -283,11 +283,11 @@ test_sorting_v1 :: proc(t: ^testing.T) { test_sorting_v6 :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() - point_a := time.time_add({}, 1 * time.Second) - point_b := time.time_add({}, 3 * time.Second) - point_c := time.time_add({}, 5 * time.Second) - point_d := time.time_add({}, 7 * time.Second) - point_e := time.time_add({}, 11 * time.Second) + point_a := time.unix(1, 0) + point_b := time.unix(3, 0) + point_c := time.unix(5, 0) + point_d := time.unix(7, 0) + point_e := time.unix(11, 0) mac: [6]byte v6_a := uuid.generate_v6(0, mac, point_a) @@ -326,11 +326,11 @@ test_sorting_v6 :: proc(t: ^testing.T) { test_sorting_v7 :: proc(t: ^testing.T) { context.random_generator = crypto.random_generator() - point_a := time.time_add({}, 1 * time.Second) - point_b := time.time_add({}, 3 * time.Second) - point_c := time.time_add({}, 5 * time.Second) - point_d := time.time_add({}, 7 * time.Second) - point_e := time.time_add({}, 11 * time.Second) + point_a := time.unix(1, 0) + point_b := time.unix(3, 0) + point_c := time.unix(5, 0) + point_d := time.unix(7, 0) + point_e := time.unix(11, 0) v7_a := uuid.generate_v7(point_a) v7_b := uuid.generate_v7(point_b) From ca58d7771b3964e47779b1aad7bfb4c29a17f43a Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Sat, 22 Jun 2024 18:36:42 -0400 Subject: [PATCH 23/23] Use new API `from_nanoseconds` in `uuid` --- core/encoding/uuid/reading.odin | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index ab08300b060..c91c8246507 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -163,7 +163,7 @@ Returns: - timestamp: The timestamp of the UUID. */ time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { - return { _nsec = cast(i64)(raw_time_v1(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100 } + return time.from_nanoseconds(cast(i64)(raw_time_v1(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } /* @@ -194,7 +194,7 @@ Returns: - timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. */ time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { - return { _nsec = cast(i64)(raw_time_v6(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100 } + return time.from_nanoseconds(cast(i64)(raw_time_v6(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } /* @@ -221,7 +221,7 @@ Returns: - timestamp: The timestamp, in milliseconds since the UNIX epoch. */ time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) { - return { _nsec = cast(i64)raw_time_v7(id) * 1e6 } + return time.from_nanoseconds(cast(i64)raw_time_v7(id) * 1e6) } /*