diff options
| author | Feoramund <161657516+Feoramund@users.noreply.github.com> | 2024-06-20 22:57:55 -0400 |
|---|---|---|
| committer | Feoramund <161657516+Feoramund@users.noreply.github.com> | 2024-06-22 18:21:27 -0400 |
| commit | 4dacddd85e07af165df5093e14f3f1a767cf63d1 (patch) | |
| tree | f386ad01cb964f967f2dbc43b09b314f80e84f02 /core/encoding/uuid | |
| parent | 3af9d31bd52c4714beddd2eaf154eadaf81d14b1 (diff) | |
Add `core:encoding/uuid`
Diffstat (limited to 'core/encoding/uuid')
| -rw-r--r-- | core/encoding/uuid/LICENSE | 28 | ||||
| -rw-r--r-- | core/encoding/uuid/definitions.odin | 59 | ||||
| -rw-r--r-- | core/encoding/uuid/doc.odin | 15 | ||||
| -rw-r--r-- | core/encoding/uuid/generation.odin | 159 | ||||
| -rw-r--r-- | core/encoding/uuid/reading.odin | 97 | ||||
| -rw-r--r-- | core/encoding/uuid/writing.odin | 61 |
6 files changed, 419 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..5bb104cb2 --- /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 000000000..a05698955 --- /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 000000000..fe05d3ebd --- /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 000000000..7f3d30ab2 --- /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 000000000..c13d700a8 --- /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 +} |