aboutsummaryrefslogtreecommitdiff
path: root/core/encoding
diff options
context:
space:
mode:
authorgingerBill <gingerBill@users.noreply.github.com>2024-06-28 09:49:23 +0100
committerGitHub <noreply@github.com>2024-06-28 09:49:23 +0100
commit4824050c999542d6ae4bd7f67f301a6694dcd556 (patch)
treeb07b4113aae2ee997c417c6e6a1afed38e822df1 /core/encoding
parent35651cfc173bc42f1afc19c4e2cec46501808dd0 (diff)
parentca58d7771b3964e47779b1aad7bfb4c29a17f43a (diff)
Merge pull request #3792 from Feoramund/core-uuid
Add `core:encoding/uuid`
Diffstat (limited to 'core/encoding')
-rw-r--r--core/encoding/uuid/LICENSE28
-rw-r--r--core/encoding/uuid/definitions.odin67
-rw-r--r--core/encoding/uuid/doc.odin46
-rw-r--r--core/encoding/uuid/generation.odin333
-rw-r--r--core/encoding/uuid/legacy/legacy.odin146
-rw-r--r--core/encoding/uuid/reading.odin242
-rw-r--r--core/encoding/uuid/stamping.odin89
-rw-r--r--core/encoding/uuid/writing.odin131
8 files changed, 1082 insertions, 0 deletions
diff --git a/core/encoding/uuid/LICENSE b/core/encoding/uuid/LICENSE
new file mode 100644
index 000000000..e4e21e62d
--- /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 000000000..fe13ca99a
--- /dev/null
+++ b/core/encoding/uuid/definitions.odin
@@ -0,0 +1,67 @@
+package uuid
+
+// A RFC 4122 Universally Unique Identifier
+Identifier :: distinct [16]u8
+
+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
+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,
+ 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 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,
+}
diff --git a/core/encoding/uuid/doc.odin b/core/encoding/uuid/doc.odin
new file mode 100644
index 000000000..6fa375b72
--- /dev/null
+++ b/core/encoding/uuid/doc.odin
@@ -0,0 +1,46 @@
+/*
+package uuid implements Universally Unique Identifiers according to the
+standard originally outlined in RFC 4122 with additions from RFC 9562.
+
+The UUIDs are textually represented and read in the following string format:
+`00000000-0000-v000-V000-000000000000`
+
+`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.
+ }
+
+
+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
diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin
new file mode 100644
index 000000000..cf29df378
--- /dev/null
+++ b/core/encoding/uuid/generation.odin
@@ -0,0 +1,333 @@
+package uuid
+
+import "base:runtime"
+import "core:crypto/hash"
+import "core:math/rand"
+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.
+- 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, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) {
+ 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)
+ uuid_timestamp_octets := transmute([8]u8)uuid_timestamp
+
+ 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
+ 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.")
+ }
+
+ 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 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) {
+ 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.")
+
+ result[VERSION_BYTE_INDEX] &= 0x0F
+ result[VERSION_BYTE_INDEX] |= 0x40
+
+ result[VARIANT_BYTE_INDEX] &= 0x3F
+ result[VARIANT_BYTE_INDEX] |= 0x80
+
+ return
+}
+
+/*
+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.
+- 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, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) {
+ unix_time_in_hns_intervals := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 100
+
+ uuid_timestamp := cast(u128be)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals)
+
+ 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, BIG_CLOCK_ERROR)
+ 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.")
+ result[8] |= cast(u8)temporary[0] & 0x3F
+ result[9] = cast(u8)temporary[1]
+ }
+
+ if realized_node, ok := node.?; ok {
+ mutable_node := realized_node
+ 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.")
+ }
+
+ result[VERSION_BYTE_INDEX] |= 0x60
+ result[VARIANT_BYTE_INDEX] |= 0x80
+
+ return
+}
+
+/*
+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.
+
+Inputs:
+- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time.
+
+Returns:
+- result: The generated UUID.
+*/
+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(timestamp.? or_else time.now()) / 1e6
+
+ 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[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 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.
+
+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 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:
+- result: The generated UUID.
+*/
+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, VERSION_7_BIG_COUNTER_ERROR)
+ unix_time_in_milliseconds := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 1e6
+
+ 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[VERSION_BYTE_INDEX] &= 0x0F
+ result[VERSION_BYTE_INDEX] |= 0x70
+
+ result[VARIANT_BYTE_INDEX] &= 0x3F
+ result[VARIANT_BYTE_INDEX] |= 0x80
+
+ return
+}
+
+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/core/encoding/uuid/legacy/legacy.odin b/core/encoding/uuid/legacy/legacy.odin
new file mode 100644
index 000000000..d5f3df617
--- /dev/null
+++ b/core/encoding/uuid/legacy/legacy.odin
@@ -0,0 +1,146 @@
+/*
+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 with a MD5 hash of a name and a namespace.
+
+Inputs:
+- 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 which will be hashed with 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 with a MD5 hash of a name and a namespace.
+
+Inputs:
+- 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 which will be hashed with 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 with a SHA1 hash of a name and a namespace.
+
+Inputs:
+- 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 which will be hashed with 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 with a SHA1 hash of a name and a namespace.
+
+Inputs:
+- 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 which will be hashed with 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 000000000..c91c82465
--- /dev/null
+++ b/core/encoding/uuid/reading.odin
@@ -0,0 +1,242 @@
+package uuid
+
+import "base:runtime"
+import "core:time"
+
+/*
+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[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[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[VARIANT_BYTE_INDEX] & 0x80 == 0:
+ return .Reserved_Apollo_NCS
+ case id[VARIANT_BYTE_INDEX] & 0xC0 == 0x80:
+ return .RFC_4122
+ case id[VARIANT_BYTE_INDEX] & 0xE0 == 0xC0:
+ return .Reserved_Microsoft_COM
+ case id[VARIANT_BYTE_INDEX] & 0xF0 == 0xE0:
+ return .Reserved_Future
+ case:
+ return .Unknown
+ }
+}
+
+/*
+Get the clock sequence of a version 1 or version 6 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 or version 6 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 raw timestamp of a version 1 UUID.
+
+Inputs:
+- id: The identifier.
+
+Returns:
+- timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15.
+*/
+raw_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 1 UUID.
+
+Inputs:
+- id: The identifier.
+
+Returns:
+- timestamp: The timestamp of the UUID.
+*/
+time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) {
+ return time.from_nanoseconds(cast(i64)(raw_time_v1(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100)
+}
+
+/*
+Get the raw timestamp of a version 6 UUID.
+
+Inputs:
+- id: The identifier.
+
+Returns:
+- timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15.
+*/
+raw_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 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) {
+ return time.from_nanoseconds(cast(i64)(raw_time_v6(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100)
+}
+
+/*
+Get the raw timestamp of a version 7 UUID.
+
+Inputs:
+- id: The identifier.
+
+Returns:
+- timestamp: The timestamp, in milliseconds since the UNIX epoch.
+*/
+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.from_nanoseconds(cast(i64)raw_time_v7(id) * 1e6)
+}
+
+/*
+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/core/encoding/uuid/stamping.odin b/core/encoding/uuid/stamping.odin
new file mode 100644
index 000000000..0c07725c3
--- /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/core/encoding/uuid/writing.odin b/core/encoding/uuid/writing.odin
new file mode 100644
index 000000000..499cba72b
--- /dev/null
+++ b/core/encoding/uuid/writing.odin
@@ -0,0 +1,131 @@
+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.
+
+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.
+*/
+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
+
+ 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[index]) }
+ io.write_byte(w, '-')
+ for index in 4 ..< 6 { write_octet(w, id[index]) }
+ io.write_byte(w, '-')
+ for index in 6 ..< 8 { write_octet(w, id[index]) }
+ io.write_byte(w, '-')
+ for index in 8 ..< 10 { write_octet(w, id[index]) }
+ io.write_byte(w, '-')
+ for index in 10 ..< 16 { write_octet(w, id[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_allocated :: 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[:])
+ unsafe_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)
+ unsafe_write(strings.to_writer(&builder), id)
+ return strings.to_string(builder)
+}
+
+to_string :: proc {
+ to_string_allocated,
+ to_string_buffer,
+}