Przeglądaj źródła

Add `core:encoding/uuid`

Feoramund 1 rok temu
rodzic
commit
4dacddd85e

+ 28 - 0
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.

+ 59 - 0
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,
+	},
+}

+ 15 - 0
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

+ 159 - 0
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,
+}

+ 97 - 0
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
+	}
+}

+ 61 - 0
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
+}

+ 2 - 0
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

+ 118 - 0
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)
+    }
+}

+ 1 - 0
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"