Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Library/Static Classes/V7/V7 - Const.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ public static partial class V7 {

private static readonly Vector128<Byte> _VersionMask = Format.VersionVariantMaskNot(V7.Version, V7.Variant);
private static readonly Vector128<Byte> _VersionOverlay = Format.VersionVariantOverlayer(V7.Version, V7.Variant);

/// <summary>Unix epoch: January 1, 1970 00:00:00 UTC</summary>
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

/// <summary>Maximum value that can fit in 48 bits (for RFC9562 V7 timestamp)</summary>
private const UInt64 Max48BitValue = 0xFFFFFFFFFFFF;
}
25 changes: 19 additions & 6 deletions Library/Static Classes/V7/V7 - Extract.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
namespace DaanV2.UUID;

public static partial class V7 {
/// <summary>Extracts the UTC from the UUID</summary>
/// <param name="uuid">The <see cref="UUID"/> to extract the UTC from</param>
/// <returns>The UTC value</returns>
/// <summary>Extracts the Unix milliseconds timestamp from the UUID</summary>
/// <param name="uuid">The <see cref="UUID"/> to extract the timestamp from</param>
/// <returns>Unix timestamp in milliseconds</returns>
public static UInt64 ExtractUtc(UUID uuid) {
(UInt64 bits48, _, _) = Format.Extract(uuid);

return bits48;
}

/// <summary>Extracts the datetime from the UUID</summary>
/// <param name="uuid">The <see cref="UUID"/> to extract the UTC from</param>
/// <param name="uuid">The <see cref="UUID"/> to extract the timestamp from</param>
/// <returns>The <see cref="DateTime"/></returns>
public static DateTime Extract(UUID uuid) {
UInt64 fileUTC = ExtractUtc(uuid);
UInt64 unixMs = ExtractUtc(uuid);

return DateTime.FromFileTimeUtc((Int64)fileUTC);
return UnixMillisecondsToDateTime(unixMs);
}

/// <summary>Converts Unix milliseconds to DateTime</summary>
/// <param name="unixMs">Milliseconds since Unix epoch (1970-01-01 00:00:00 UTC)</param>
/// <returns>The corresponding DateTime in UTC</returns>
private static DateTime UnixMillisecondsToDateTime(UInt64 unixMs) {
try {
return UnixEpoch.AddMilliseconds(unixMs);
}
catch (ArgumentOutOfRangeException) {
// If the milliseconds value is too large, return MaxValue
return DateTime.MaxValue;
}
}
}
25 changes: 21 additions & 4 deletions Library/Static Classes/V7/V7 - Generate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,32 @@ public static UUID Generate(UInt64 utc, ReadOnlySpan<Byte> bytes10) {

/// <inheritdoc cref="Generate()"/>
public static UUID Generate(DateTime timestamp, UInt16 randomA, UInt64 randomB) {
UInt64 t = (UInt64)timestamp.ToFileTimeUtc();
// RFC9562: V7 uses Unix epoch timestamp in milliseconds (48 bits)
UInt64 unixMs = DateTimeToUnixMilliseconds(timestamp);

return Generate(t, randomA, randomB);
return Generate(unixMs, randomA, randomB);
}

/// <inheritdoc cref="Generate()"/>
public static UUID Generate(UInt64 utc, UInt16 randomA, UInt64 randomB) {
Vector128<Byte> u = Format.Create(V7.Version, V7.Variant, utc, randomA, randomB);
/// <param name="unixMs">Unix timestamp in milliseconds (48-bit value)</param>
/// <param name="randomA">12 bits of random data</param>
/// <param name="randomB">62 bits of random data</param>
public static UUID Generate(UInt64 unixMs, UInt16 randomA, UInt64 randomB) {
Vector128<Byte> u = Format.Create(V7.Version, V7.Variant, unixMs, randomA, randomB);

return new UUID(u);
}

/// <summary>Converts a DateTime to Unix milliseconds</summary>
/// <param name="timestamp">The DateTime to convert</param>
/// <returns>Milliseconds since Unix epoch (1970-01-01 00:00:00 UTC)</returns>
private static UInt64 DateTimeToUnixMilliseconds(DateTime timestamp) {
var milliseconds = (timestamp.ToUniversalTime() - UnixEpoch).TotalMilliseconds;

// Ensure non-negative and within 48-bit range
if (milliseconds < 0) milliseconds = 0;
if (milliseconds > Max48BitValue) milliseconds = Max48BitValue;

return (UInt64)milliseconds;
}
}
11 changes: 8 additions & 3 deletions Tests/Generation/V7Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ public sealed partial class V7Tests {
// From https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-example-of-a-uuidv7-value
[Fact(DisplayName = "Given a known example, will generate the expected UUID")]
public void TestVector() {
var timestamp = DateTime.FromFileTimeUtc(1645557742000);
// RFC9562 test vector: UUID 017f22e2-79b0-7cc3-98c4-dc0c0c07398f
// First 48 bits are Unix milliseconds: 0x017f22e279b0 = 1645557742000
// This is 2022-02-22 19:22:22 UTC
var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var timestamp = unixEpoch.AddMilliseconds(1645557742000);

UInt16 randA = (UInt16)0xCC3;
UInt64 randB = (UInt64)0x18C4DC0C0C07398F;

UUID u = V7.Generate(timestamp, randA, randB);

Assert.Equal("017f22e2-79b0-7cc3-98c4-dc0c0c07398f", u.ToString());

Int64 extractedUTC = (Int64)V7.ExtractUtc(u);
Assert.Equal(timestamp.ToFileTimeUtc(), extractedUTC);
UInt64 extractedMs = V7.ExtractUtc(u);
Assert.Equal(1645557742000UL, extractedMs);

DateTime extracted = V7.Extract(u);
Assert.Equal(timestamp, extracted);
Expand Down
251 changes: 251 additions & 0 deletions Tests/RFC/RFC4122.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DaanV2.UUID;
using System.Text;

namespace Tests.RFC;

Expand All @@ -12,4 +13,254 @@ public void RFC4122_AppendixB_V3_MD5_DNS_ExampleCom() {
Utility.ValidateUUID(uuid, V3.Version, V3.Variant);
}

// ========== Bit-Level Validation Tests ==========

[Fact(DisplayName = "RFC4122 Section 4.1.3 - Version bits must be in correct position")]
public void RFC4122_VersionBits_CorrectPosition() {
// Version field is in octet 6, bits 12-15 of time_hi_and_version
var v1uuid = V1.Generate();
var v3uuid = V3.Generate("test");
var v4uuid = V4.Generate();
var v5uuid = V5.Generate("test");

// Version should be readable from the UUID
Assert.Equal(DaanV2.UUID.Version.V1, v1uuid.Version);
Assert.Equal(DaanV2.UUID.Version.V3, v3uuid.Version);
Assert.Equal(DaanV2.UUID.Version.V4, v4uuid.Version);
Assert.Equal(DaanV2.UUID.Version.V5, v5uuid.Version);
}

[Fact(DisplayName = "RFC4122 Section 4.1.1 - Variant bits must be 10x for RFC4122 UUIDs")]
public void RFC4122_VariantBits_RFC4122Compliant() {
// Variant field is in octet 8, bits 6-7 of clock_seq_hi_and_reserved
// Must be 10xxxxxx (0x80-0xBF) for RFC4122 compliant UUIDs
var v1uuid = V1.Generate();
var v3uuid = V3.Generate("test");
var v4uuid = V4.Generate();
var v5uuid = V5.Generate("test");

// All should have variant V1 (RFC4122)
Assert.Equal(Variant.V1, v1uuid.Variant);
Assert.Equal(Variant.V1, v3uuid.Variant);
Assert.Equal(Variant.V1, v4uuid.Variant);
Assert.Equal(Variant.V1, v5uuid.Variant);
}

// ========== Nil and Max UUID Tests (RFC4122 Section 4.1.7) ==========

[Fact(DisplayName = "RFC4122 Section 4.1.7 - Nil UUID is all zeros")]
public void RFC4122_NilUUID_AllZeros() {
var nilUuid = UUID.Zero;
Assert.Equal("00000000-0000-0000-0000-000000000000", nilUuid.ToString().ToLower());
}

[Fact(DisplayName = "RFC4122 - Max UUID is all ones")]
public void RFC4122_MaxUUID_AllOnes() {
var maxUuid = UUID.Max;
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", maxUuid.ToString().ToLower());
}

// ========== Determinism Tests for Name-Based UUIDs ==========

[Fact(DisplayName = "RFC4122 Section 4.3 - V3 (MD5) is deterministic")]
public void RFC4122_V3_Deterministic() {
// Same input should always produce the same UUID
var input = "www.example.com";
var uuid1 = V3.Generate(input);
var uuid2 = V3.Generate(input);
var uuid3 = V3.Generate(input);

Assert.Equal(uuid1, uuid2);
Assert.Equal(uuid2, uuid3);
Assert.Equal(uuid1.ToString(), uuid2.ToString());
}

[Fact(DisplayName = "RFC4122 Section 4.3 - V5 (SHA1) is deterministic")]
public void RFC4122_V5_Deterministic() {
// Same input should always produce the same UUID
var input = "www.example.com";
var uuid1 = V5.Generate(input);
var uuid2 = V5.Generate(input);
var uuid3 = V5.Generate(input);

Assert.Equal(uuid1, uuid2);
Assert.Equal(uuid2, uuid3);
Assert.Equal(uuid1.ToString(), uuid2.ToString());
}

[Fact(DisplayName = "RFC4122 - V3 different inputs produce different UUIDs")]
public void RFC4122_V3_DifferentInputs_DifferentUUIDs() {
var uuid1 = V3.Generate("input1");
var uuid2 = V3.Generate("input2");
var uuid3 = V3.Generate("input3");

Assert.NotEqual(uuid1, uuid2);
Assert.NotEqual(uuid2, uuid3);
Assert.NotEqual(uuid1, uuid3);
}

[Fact(DisplayName = "RFC4122 - V5 different inputs produce different UUIDs")]
public void RFC4122_V5_DifferentInputs_DifferentUUIDs() {
var uuid1 = V5.Generate("input1");
var uuid2 = V5.Generate("input2");
var uuid3 = V5.Generate("input3");

Assert.NotEqual(uuid1, uuid2);
Assert.NotEqual(uuid2, uuid3);
Assert.NotEqual(uuid1, uuid3);
}

// ========== Round-Trip Tests ==========

[Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V1")]
public void RFC4122_V1_RoundTrip() {
var original = V1.Generate();
var str = original.ToString();
var parsed = new UUID(str);

Assert.Equal(original, parsed);
Assert.Equal(original.Version, parsed.Version);
Assert.Equal(original.Variant, parsed.Variant);
}

[Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V3")]
public void RFC4122_V3_RoundTrip() {
var original = V3.Generate("test");
var str = original.ToString();
var parsed = new UUID(str);

Assert.Equal(original, parsed);
Assert.Equal(original.Version, parsed.Version);
Assert.Equal(original.Variant, parsed.Variant);
}

[Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V4")]
public void RFC4122_V4_RoundTrip() {
var original = V4.Generate();
var str = original.ToString();
var parsed = new UUID(str);

Assert.Equal(original, parsed);
Assert.Equal(original.Version, parsed.Version);
Assert.Equal(original.Variant, parsed.Variant);
}

[Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V5")]
public void RFC4122_V5_RoundTrip() {
var original = V5.Generate("test");
var str = original.ToString();
var parsed = new UUID(str);

Assert.Equal(original, parsed);
Assert.Equal(original.Version, parsed.Version);
Assert.Equal(original.Variant, parsed.Variant);
}

// ========== Format Tests ==========

[Fact(DisplayName = "RFC4122 Section 3 - UUID format is 8-4-4-4-12 hexadecimal")]
public void RFC4122_Format_Correct() {
var uuid = V4.Generate();
var str = uuid.ToString();

// Check format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Assert.Equal(36, str.Length);
Assert.Equal('-', str[8]);
Assert.Equal('-', str[13]);
Assert.Equal('-', str[18]);
Assert.Equal('-', str[23]);

// All other characters should be valid hex
var hexParts = str.Split('-');
Assert.Equal(5, hexParts.Length);
Assert.Equal(8, hexParts[0].Length);
Assert.Equal(4, hexParts[1].Length);
Assert.Equal(4, hexParts[2].Length);
Assert.Equal(4, hexParts[3].Length);
Assert.Equal(12, hexParts[4].Length);
}

// ========== V1 Time-Based Tests ==========

[Fact(DisplayName = "RFC4122 Section 4.2.1 - V1 contains timestamp")]
public void RFC4122_V1_ContainsTimestamp() {
var beforeGen = DateTime.UtcNow;
var uuid = V1.Generate();
var afterGen = DateTime.UtcNow;

var info = V1.Extract(uuid);

// Extracted timestamp should be within reasonable range
Assert.True(info.Timestamp >= beforeGen.AddSeconds(-1));
Assert.True(info.Timestamp <= afterGen.AddSeconds(1));
}

[Fact(DisplayName = "RFC4122 Section 4.2.1 - V1 contains MAC address")]
public void RFC4122_V1_ContainsMacAddress() {
var uuid = V1.Generate();
var info = V1.Extract(uuid);

// MAC address should be 6 bytes
Assert.NotNull(info.MacAddress);
Assert.Equal(6, info.MacAddress.Length);
}

// ========== Uniqueness Tests ==========

[Fact(DisplayName = "RFC4122 Section 4.4 - V4 generates unique UUIDs")]
public void RFC4122_V4_Uniqueness() {
var uuids = new HashSet<UUID>();
const int count = 10000;

for (int i = 0; i < count; i++) {
var uuid = V4.Generate();
Assert.True(uuids.Add(uuid), $"Duplicate UUID generated: {uuid}");
}

Assert.Equal(count, uuids.Count);
}

[Fact(DisplayName = "RFC4122 - V1 generates unique UUIDs")]
public void RFC4122_V1_Uniqueness() {
var uuids = new HashSet<UUID>();
const int count = 1000;

for (int i = 0; i < count; i++) {
var uuid = V1.Generate();
Assert.True(uuids.Add(uuid), $"Duplicate UUID generated: {uuid}");
}

Assert.Equal(count, uuids.Count);
}

// ========== Encoding Tests ==========

[Fact(DisplayName = "RFC4122 - V3 with different encodings")]
public void RFC4122_V3_DifferentEncodings() {
var input = "test string";
var uuidUtf8 = V3.Generate(input, Encoding.UTF8);
var uuidAscii = V3.Generate(input, Encoding.ASCII);

// Same string with same encoding should produce same UUID
var uuidUtf8_2 = V3.Generate(input, Encoding.UTF8);
Assert.Equal(uuidUtf8, uuidUtf8_2);

// For ASCII-compatible strings, UTF8 and ASCII should be the same
Assert.Equal(uuidUtf8, uuidAscii);
}

[Fact(DisplayName = "RFC4122 - V5 with different encodings")]
public void RFC4122_V5_DifferentEncodings() {
var input = "test string";
var uuidUtf8 = V5.Generate(input, Encoding.UTF8);
var uuidAscii = V5.Generate(input, Encoding.ASCII);

// Same string with same encoding should produce same UUID
var uuidUtf8_2 = V5.Generate(input, Encoding.UTF8);
Assert.Equal(uuidUtf8, uuidUtf8_2);

// For ASCII-compatible strings, UTF8 and ASCII should be the same
Assert.Equal(uuidUtf8, uuidAscii);
}

}
Loading
Loading