From b513f10ed68a950e125c249fe7acd3c0206ab1be Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Sat, 20 Jul 2024 04:32:11 -0400 Subject: [PATCH] Use uuidv7 algorithm from javascript --- src/Web/Common/GuidExtensions.cs | 108 ++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/src/Web/Common/GuidExtensions.cs b/src/Web/Common/GuidExtensions.cs index d02c4a7..0c71da8 100644 --- a/src/Web/Common/GuidExtensions.cs +++ b/src/Web/Common/GuidExtensions.cs @@ -1,30 +1,94 @@ -using System.Buffers.Binary; -using System.Security.Cryptography; - -namespace Hutopy.Web.Common; +namespace Hutopy.Web.Common; +/// +/// Adapted from https://raw.githubusercontent.com/uuidjs/uuid/main/src/v7.ts. +/// to match the uuidv7 generated on the client +/// public static class GuidHelper { - // TODO: Delete when NET9 is release! + private class V7State + { + public long Msecs { get; set; } = long.MinValue; + public int Seq { get; set; } + } + + private static readonly V7State State = new(); + private static readonly Random Random = new(); + public static Guid GenerateUuidV7() { - Span uuidv7 = stackalloc byte[16]; - ulong unixTimeTicks = (ulong)DateTimeOffset.UtcNow.Subtract(DateTimeOffset.UnixEpoch).Ticks; - ulong unixTsMs = (unixTimeTicks & 0x0FFFFFFFFFFFF000) << 4; - ulong unixTsMsVer = unixTsMs | 0b0111UL << 12; - ulong randA = unixTimeTicks & 0x0000000000000FFF; - // merge "unix_ts_ms", "ver" and "rand_a" - ulong hi = unixTsMsVer | randA; - BinaryPrimitives.WriteUInt64BigEndian(uuidv7, hi); - // fill "rand_b" and "var" - RandomNumberGenerator.Fill(uuidv7[8..]); - // set "var" - byte varOctet = uuidv7[8]; - varOctet = (byte)(varOctet & 0b00111111); - varOctet = (byte)(varOctet | 0b10111111); - uuidv7[8] = varOctet; + byte[] randomValues = new byte[16]; + Random.NextBytes(randomValues); + + UpdateV7State( + State, + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + randomValues); + + var values = V7Bytes(randomValues, State.Msecs, State.Seq); + + return new(values); + } + + private static void UpdateV7State(V7State state, long now, byte[] rnds) + { + if (now > state.Msecs) + { + state.Seq = (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9]; + state.Msecs = now; + } + else + { + state.Seq = (state.Seq + 1) | 0; + if (state.Seq == 0) + { + state.Msecs++; + } + } + } + + private static byte[] V7Bytes(byte[] rnds, long? msecs = null, int? seq = null, byte[] buf = null, int offset = 0) + { + if (buf == null) + { + buf = new byte[16]; + offset = 0; + } + + // Defaults + msecs ??= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + seq ??= ((rnds[6] & 0x7f) << 24) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9]; + + // byte 0-5: timestamp (48 bits) + buf[offset++] = (byte)((msecs.Value / 0x10000000000) & 0xff); + buf[offset++] = (byte)((msecs.Value / 0x100000000) & 0xff); + buf[offset++] = (byte)((msecs.Value / 0x1000000) & 0xff); + buf[offset++] = (byte)((msecs.Value / 0x10000) & 0xff); + buf[offset++] = (byte)((msecs.Value / 0x100) & 0xff); + buf[offset++] = (byte)(msecs.Value & 0xff); - var value = Convert.ToHexString(uuidv7); - return Guid.Parse(value); + // byte 6: `version` (4 bits) | sequence bits 28-31 (4 bits) + buf[offset++] = (byte)(0x70 | ((seq.Value >> 28) & 0x0f)); + + // byte 7: sequence bits 20-27 (8 bits) + buf[offset++] = (byte)((seq.Value >> 20) & 0xff); + + // byte 8: `variant` (2 bits) | sequence bits 14-19 (6 bits) + buf[offset++] = (byte)(0x80 | ((seq.Value >> 14) & 0x3f)); + + // byte 9: sequence bits 6-13 (8 bits) + buf[offset++] = (byte)((seq.Value >> 6) & 0xff); + + // byte 10: sequence bits 0-5 (6 bits) | random (2 bits) + buf[offset++] = (byte)(((seq.Value << 2) & 0xff) | (rnds[10] & 0x03)); + + // bytes 11-15: random (40 bits) + buf[offset++] = rnds[11]; + buf[offset++] = rnds[12]; + buf[offset++] = rnds[13]; + buf[offset++] = rnds[14]; + buf[offset++] = rnds[15]; + + return buf; } }