Add core:encoding/uuid

This commit is contained in:
Feoramund
2024-06-20 22:57:55 -04:00
parent 3af9d31bd5
commit 4dacddd85e
9 changed files with 540 additions and 0 deletions

View File

@@ -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.

View File

@@ -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,
},
}

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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"