diff options
| author | gingerBill <gingerBill@users.noreply.github.com> | 2024-06-28 09:49:23 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-28 09:49:23 +0100 |
| commit | 4824050c999542d6ae4bd7f67f301a6694dcd556 (patch) | |
| tree | b07b4113aae2ee997c417c6e6a1afed38e822df1 /core/encoding | |
| parent | 35651cfc173bc42f1afc19c4e2cec46501808dd0 (diff) | |
| parent | ca58d7771b3964e47779b1aad7bfb4c29a17f43a (diff) | |
Merge pull request #3792 from Feoramund/core-uuid
Add `core:encoding/uuid`
Diffstat (limited to 'core/encoding')
| -rw-r--r-- | core/encoding/uuid/LICENSE | 28 | ||||
| -rw-r--r-- | core/encoding/uuid/definitions.odin | 67 | ||||
| -rw-r--r-- | core/encoding/uuid/doc.odin | 46 | ||||
| -rw-r--r-- | core/encoding/uuid/generation.odin | 333 | ||||
| -rw-r--r-- | core/encoding/uuid/legacy/legacy.odin | 146 | ||||
| -rw-r--r-- | core/encoding/uuid/reading.odin | 242 | ||||
| -rw-r--r-- | core/encoding/uuid/stamping.odin | 89 | ||||
| -rw-r--r-- | core/encoding/uuid/writing.odin | 131 |
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, +} |