240 lines
7.1 KiB
C#
240 lines
7.1 KiB
C#
using System.Buffers.Binary;
|
|
using System.Security.Cryptography;
|
|
using SPTarkov.Server.Core.Extensions;
|
|
|
|
namespace SPTarkov.Server.Core.Models.Common;
|
|
|
|
/// <summary>
|
|
/// Represents a 12-byte MongoDB-style ObjectId, consisting of:
|
|
/// <list type="bullet">
|
|
/// <item><description>4-byte timestamp (seconds since Unix epoch, big-endian)</description></item>
|
|
/// <item><description>3-byte machine identifier</description></item>
|
|
/// <item><description>2-byte process identifier (big-endian)</description></item>
|
|
/// <item><description>3-byte incrementing counter (big-endian)</description></item>
|
|
/// </list>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This struct stores the ObjectId in two packed fields for efficient memory usage
|
|
/// and comparison:
|
|
/// <list type="bullet">
|
|
/// <item><see cref="_timestampAndMachine"/><description>: First 8 bytes (timestamp + machine ID)</description></item>
|
|
/// <item><see cref="_pidAndIncrement"/><description>: Last 4 bytes (process ID + counter)</description></item>
|
|
/// </list>
|
|
/// </para>
|
|
/// <para>
|
|
/// The struct is immutable and implements <see cref="IEquatable{MongoId}"/> for fast comparisons.
|
|
/// </para>
|
|
/// </remarks>
|
|
public readonly struct MongoId : IEquatable<MongoId>, IComparable<MongoId>
|
|
{
|
|
/// <summary>
|
|
/// The first 8 bytes: 4-byte timestamp + 3-byte machine ID + 1 byte of PID.
|
|
/// </summary>
|
|
private readonly long _timestampAndMachine;
|
|
|
|
/// <summary>
|
|
/// The last 4 bytes: remaining 1 byte of PID + 3-byte counter.
|
|
/// </summary>
|
|
private readonly int _pidAndIncrement;
|
|
|
|
private static readonly int _machine = BitConverter.ToInt32(RandomNumberGenerator.GetBytes(4), 0) & 0xFFFFFF;
|
|
private static readonly short _pid = (short)Environment.ProcessId;
|
|
private static int _increment = RandomNumberGenerator.GetInt32(0, 0xFFFFFF);
|
|
|
|
public bool IsEmpty
|
|
{
|
|
get { return _timestampAndMachine == 0 && _pidAndIncrement == 0; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new <see cref="MongoId"/> with a generated value
|
|
/// based on the current time, machine ID, process ID, and counter.
|
|
/// </summary>
|
|
public MongoId()
|
|
{
|
|
var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
Span<byte> bytes = stackalloc byte[12];
|
|
|
|
// timestamp (4 bytes, big-endian)
|
|
BinaryPrimitives.WriteInt32BigEndian(bytes, timestamp);
|
|
|
|
// machine ID (3 bytes)
|
|
bytes[4] = (byte)(_machine >> 16);
|
|
bytes[5] = (byte)(_machine >> 8);
|
|
bytes[6] = (byte)_machine;
|
|
|
|
// PID (2 bytes)
|
|
BinaryPrimitives.WriteInt16BigEndian(bytes[7..9], _pid);
|
|
|
|
// increment (3 bytes, big-endian)
|
|
var inc = Interlocked.Increment(ref _increment) & 0xFFFFFF;
|
|
bytes[9] = (byte)(inc >> 16);
|
|
bytes[10] = (byte)(inc >> 8);
|
|
bytes[11] = (byte)inc;
|
|
|
|
// pack into fields (avoids array allocations later)
|
|
_timestampAndMachine = BitConverter.ToInt64(bytes);
|
|
_pidAndIncrement = BitConverter.ToInt32(bytes[8..]);
|
|
}
|
|
|
|
public MongoId(string? hex)
|
|
{
|
|
if (string.IsNullOrEmpty(hex) || hex == "000000000000000000000000")
|
|
{
|
|
_timestampAndMachine = 0;
|
|
_pidAndIncrement = 0;
|
|
return;
|
|
}
|
|
|
|
if (hex.Length != 24)
|
|
{
|
|
throw new ArgumentException("ObjectId must be a 24-character hex string.", hex);
|
|
}
|
|
|
|
Span<byte> bytes = stackalloc byte[12];
|
|
Span<char> chars = stackalloc char[24];
|
|
hex.AsSpan().CopyTo(chars);
|
|
|
|
for (var i = 0; i < 12; i++)
|
|
{
|
|
var hi = HexCharToValue(hex[2 * i]);
|
|
var lo = HexCharToValue(hex[2 * i + 1]);
|
|
|
|
if (hi == -1 || lo == -1)
|
|
{
|
|
throw new FormatException("ObjectId contains invalid hex characters.");
|
|
}
|
|
|
|
bytes[i] = (byte)((hi << 4) | lo);
|
|
}
|
|
|
|
_timestampAndMachine = BitConverter.ToInt64(bytes);
|
|
_pidAndIncrement = BitConverter.ToInt32(bytes[8..]);
|
|
}
|
|
|
|
private static int HexCharToValue(char c)
|
|
{
|
|
return c >= '0' && c <= '9' ? c - '0'
|
|
: c >= 'a' && c <= 'f' ? c - 'a' + 10
|
|
: c >= 'A' && c <= 'F' ? c - 'A' + 10
|
|
: -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the MongoId as a 24-character lowercase hexadecimal string.
|
|
/// </summary>
|
|
public override string ToString()
|
|
{
|
|
if (_timestampAndMachine == 0 && _pidAndIncrement == 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
Span<byte> bytes = stackalloc byte[12];
|
|
BitConverter.TryWriteBytes(bytes, _timestampAndMachine);
|
|
BitConverter.TryWriteBytes(bytes[8..], _pidAndIncrement);
|
|
return Convert.ToHexString(bytes).ToLowerInvariant();
|
|
}
|
|
|
|
public bool Equals(MongoId? other)
|
|
{
|
|
if (other is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return _timestampAndMachine == other.Value._timestampAndMachine && _pidAndIncrement == other.Value._pidAndIncrement;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool Equals(MongoId other)
|
|
{
|
|
return _timestampAndMachine == other._timestampAndMachine && _pidAndIncrement == other._pidAndIncrement;
|
|
}
|
|
|
|
public bool Equals(string? other)
|
|
{
|
|
if (other == null || other.Length != 24)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Span<byte> bytes = stackalloc byte[12];
|
|
for (var i = 0; i < 12; i++)
|
|
{
|
|
var hi = HexCharToValue(other[2 * i]);
|
|
var lo = HexCharToValue(other[2 * i + 1]);
|
|
if (hi == -1 || lo == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bytes[i] = (byte)((hi << 4) | lo);
|
|
}
|
|
|
|
var a = BitConverter.ToInt64(bytes);
|
|
var b = BitConverter.ToInt32(bytes[8..]);
|
|
|
|
return _timestampAndMachine == a && _pidAndIncrement == b;
|
|
}
|
|
|
|
public static bool IsValidMongoId(string stringToCheck)
|
|
{
|
|
return stringToCheck.IsValidMongoId();
|
|
}
|
|
|
|
public static implicit operator string(MongoId mongoId)
|
|
{
|
|
return mongoId.ToString();
|
|
}
|
|
|
|
public static implicit operator MongoId(string mongoId)
|
|
{
|
|
return new MongoId(mongoId);
|
|
}
|
|
|
|
public int CompareTo(MongoId other)
|
|
{
|
|
var compare = _timestampAndMachine.CompareTo(other._timestampAndMachine);
|
|
return compare != 0 ? compare : _pidAndIncrement.CompareTo(other._pidAndIncrement);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override bool Equals(object? obj)
|
|
{
|
|
return obj is MongoId other && Equals(other);
|
|
}
|
|
|
|
public static bool operator ==(MongoId left, MongoId right)
|
|
{
|
|
return left.Equals(right);
|
|
}
|
|
|
|
public static bool operator !=(MongoId left, MongoId? right)
|
|
{
|
|
return !left.Equals(right);
|
|
}
|
|
|
|
public static bool operator ==(MongoId left, MongoId? right)
|
|
{
|
|
return left.Equals(right);
|
|
}
|
|
|
|
public static bool operator !=(MongoId left, MongoId right)
|
|
{
|
|
return !left.Equals(right);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine(_timestampAndMachine, _pidAndIncrement);
|
|
}
|
|
|
|
public static MongoId Empty()
|
|
{
|
|
return new MongoId("000000000000000000000000");
|
|
}
|
|
}
|