Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
250 changes: 250 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,253 @@ 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() {
const int count = 1000;

// Use batch function which properly handles timestamp incrementing
var uuids = V1.Batch(count);
var uniqueUuids = new HashSet<UUID>(uuids);

// All UUIDs should be unique when using batch generation
Assert.Equal(count, uniqueUuids.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