feat: CUID & fix: reorder bug
This commit is contained in:
118
CarManagerV3/Util/CUID.cs
Normal file
118
CarManagerV3/Util/CUID.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CarManagerV3.Util
|
||||
{
|
||||
internal class CUID
|
||||
{
|
||||
public const int DefaultLength = 6;
|
||||
|
||||
private static int _counter = RandomNumberGenerator.GetInt32(int.MaxValue);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random CUID (Collision-resistant Unique Identifier) of a specified length.
|
||||
/// The CUID is designed to be unique across different machines and time, making it suitable for use as an identifier for cars in the application.
|
||||
/// The length must be between 4 and 32 characters to ensure a good balance between uniqueness and readability.
|
||||
/// The generated CUID consists of a combination of alphanumeric characters to ensure uniqueness and readability.
|
||||
/// </summary>
|
||||
/// <param name="length">The desired length of the generated CUID</param>
|
||||
/// <param name="prefixWithC">Whether to prefix the CUID with 'c' for better readability and to avoid starting with a digit.</param>"
|
||||
/// <returns></returns>
|
||||
public static string NewCUID(int length = DefaultLength, bool prefixWithC = true)
|
||||
{
|
||||
// CUIDv2 specs allow between 4 and 32 chars.
|
||||
if(length < 4 || length > 32) throw new ArgumentOutOfRangeException("length");
|
||||
|
||||
// We will produce enough encoded chars to satisfy 'length' after prefixing and truncation.
|
||||
// Base64 encodes 3 bytes -> 4 chars. So bytesNeeded ≈ ceil(charsNeeded * 3/4).
|
||||
int charsNeeded = prefixWithC ? (length - 1) : length;
|
||||
int bytesNeeded = (int)Math.Ceiling(charsNeeded * 3.0 / 4.0);
|
||||
|
||||
Span<byte> material = stackalloc byte[32];
|
||||
FillMaterial(material);
|
||||
|
||||
byte[] outputBytes = ExpandWithSha256(material, bytesNeeded);
|
||||
|
||||
string encoded = Base64UrlEncode(outputBytes);
|
||||
|
||||
if (encoded.Length < charsNeeded)
|
||||
{
|
||||
// Extremely unlikely unless length is huge; ensure we have enough by expanding more.
|
||||
// (Kept as a guard; for typical lengths like 24-64, you're fine.)
|
||||
outputBytes = ExpandWithSha256(material, bytesNeeded + 32);
|
||||
encoded = Base64UrlEncode(outputBytes);
|
||||
}
|
||||
|
||||
string body = encoded.Substring(0, charsNeeded);
|
||||
return prefixWithC ? ("c" + body) : body;
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static void FillMaterial(Span<byte> dst32)
|
||||
{
|
||||
// Compose a payload with:
|
||||
// - 16 bytes random
|
||||
// - 8 bytes timestamp (UTC ticks)
|
||||
// - 4 bytes counter
|
||||
// - 4 bytes process/thread noise
|
||||
Span<byte> payload = stackalloc byte[16 + 8 + 4 + 4];
|
||||
|
||||
RandomNumberGenerator.Fill(payload.Slice(0, 16));
|
||||
|
||||
long ticks = DateTime.UtcNow.Ticks;
|
||||
BinaryPrimitives.WriteInt64LittleEndian(payload.Slice(16, 8), ticks);
|
||||
|
||||
int c = Interlocked.Increment(ref _counter);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload.Slice(24, 4), c);
|
||||
|
||||
// Some extra variability (not relied on for security)
|
||||
int noise = Environment.ProcessId ^ Thread.CurrentThread.ManagedThreadId ^ (int)Stopwatch.GetTimestamp();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload.Slice(28, 4), noise);
|
||||
|
||||
// Hash to produce 32 bytes of uniformly distributed output
|
||||
SHA256.HashData(payload, dst32);
|
||||
}
|
||||
|
||||
private static byte[] ExpandWithSha256(ReadOnlySpan<byte> seed32, int bytesNeeded)
|
||||
{
|
||||
if (bytesNeeded <= 0) return Array.Empty<byte>();
|
||||
|
||||
// If <= 32 bytes, we can just take from seed32 by hashing once more for separation.
|
||||
// We'll use SHA256(seed || blockIndex) to generate blocks.
|
||||
int blocks = (int)Math.Ceiling(bytesNeeded / 32.0);
|
||||
byte[] result = new byte[blocks * 32];
|
||||
|
||||
Span<byte> input = stackalloc byte[32 + 4];
|
||||
seed32.CopyTo(input.Slice(0, 32));
|
||||
|
||||
for (int i = 0; i < blocks; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt32LittleEndian(input.Slice(32, 4), i);
|
||||
Span<byte> block = result.AsSpan(i * 32, 32);
|
||||
SHA256.HashData(input, block);
|
||||
}
|
||||
|
||||
if (result.Length == bytesNeeded) return result;
|
||||
|
||||
byte[] trimmed = new byte[bytesNeeded];
|
||||
Buffer.BlockCopy(result, 0, trimmed, 0, bytesNeeded);
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private static string Base64UrlEncode(byte[] bytes)
|
||||
{
|
||||
// Standard base64url without padding per RFC 4648 §5
|
||||
string b64 = Convert.ToBase64String(bytes);
|
||||
return b64.Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user