84 lines
3.1 KiB
C#
84 lines
3.1 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace Just.Core;
|
|
|
|
public static class GuidV8
|
|
{
|
|
private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10; // 100-microsecond units
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
[ExcludeFromCodeCoverage]
|
|
public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
|
|
|
|
public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
|
|
{
|
|
var epoch = dateTime.Subtract(DateTime.UnixEpoch);
|
|
var timestamp = epoch.Ticks / TicksPrecision;
|
|
|
|
// Negative timestamps can't be encoded correctly due to unsigned bit operations
|
|
if (timestamp < 0)
|
|
{
|
|
throw new ArgumentException("Timestamp must be on or after UnixEpoch (1970-01-01 UTC).", nameof(dateTime));
|
|
}
|
|
|
|
uint tsHigh = (uint)((timestamp >> 16) & 0xFFFFFFFF);
|
|
ushort tsLow = (ushort)(timestamp & 0x0000FFFF);
|
|
|
|
Span<byte> bytes = stackalloc byte[10];
|
|
|
|
if (entropy == RngEntropy.Strong)
|
|
{
|
|
RandomNumberGenerator.Fill(bytes);
|
|
}
|
|
else
|
|
{
|
|
Random.Shared.NextBytes(bytes);
|
|
}
|
|
|
|
bytes[0] = (byte)((bytes[0] & 0x0F) | 0x80); // Version 8
|
|
bytes[2] = (byte)((bytes[2] & 0x1F) | 0x80); // Variant 0b1000
|
|
|
|
ushort version = (ushort)((bytes[0] << 8) | bytes[1]);
|
|
|
|
return new Guid(
|
|
tsHigh,
|
|
tsLow,
|
|
version,
|
|
bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the timestamp from a UUID v8 generated by <see cref="NewGuid(DateTime, RngEntropy)"/>.
|
|
/// </summary>
|
|
/// <param name="guid">The UUID v8 to extract the timestamp from.</param>
|
|
/// <returns>The <see cref="DateTime"/> (UTC) encoded in the GUID's timestamp fields.</returns>
|
|
/// <exception cref="ArgumentException">
|
|
/// Thrown if <paramref name="guid"/> is not a UUID v8.
|
|
/// </exception>
|
|
/// <remarks>
|
|
/// The returned timestamp has 100-microsecond precision (the resolution used by
|
|
/// <see cref="NewGuid(DateTime, RngEntropy)"/>). Sub-100µs components of the original
|
|
/// <see cref="DateTime"/> are not recoverable.
|
|
/// </remarks>
|
|
[Pure]
|
|
public static DateTime ExtractTimestamp(Guid guid)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[16];
|
|
guid.TryWriteBytes(bytes);
|
|
|
|
// Version is the high nibble of byte 7 (parameter 'c' high byte, little-endian byte 7)
|
|
var version = bytes[7] >> 4;
|
|
if (version != 8)
|
|
throw new ArgumentException($"The provided GUID is not a UUID v8 (version={version}).", nameof(guid));
|
|
|
|
// tsHigh is stored as Int32 at bytes 0-3
|
|
uint tsHigh = MemoryMarshal.Read<uint>(bytes);
|
|
// tsLow is stored as Int16 at bytes 4-5
|
|
ushort tsLow = MemoryMarshal.Read<ushort>(bytes[4..]);
|
|
|
|
long timestamp = ((long)tsHigh << 16) | tsLow;
|
|
return DateTime.UnixEpoch.AddTicks(timestamp * TicksPrecision);
|
|
}
|
|
}
|