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
106 changes: 80 additions & 26 deletions Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace SPTarkov.Server.Core.Models.Common;

/// <summary>
/// Represents a 12-byte MongoDB-style ObjectId, consisting of:
/// Represents a 12-<see cref="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>
Expand Down Expand Up @@ -39,7 +39,7 @@ namespace SPTarkov.Server.Core.Models.Common;
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 readonly short _pid = (short) Environment.ProcessId;
private static int _increment = RandomNumberGenerator.GetInt32(0, 0xFFFFFF);

public bool IsEmpty
Expand All @@ -53,25 +53,25 @@ public bool IsEmpty
/// </summary>
public MongoId()
{
var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
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;
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;
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);
Expand All @@ -82,8 +82,7 @@ public MongoId(string? hex)
{
if (string.IsNullOrEmpty(hex) || hex == "000000000000000000000000")
{
_timestampAndMachine = 0;
_pidAndIncrement = 0;
this = default;
return;
}

Expand All @@ -93,26 +92,28 @@ public MongoId(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]);
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);
bytes[i] = (byte) ((hi << 4) | lo);
}

_timestampAndMachine = BitConverter.ToInt64(bytes);
_pidAndIncrement = BitConverter.ToInt32(bytes[8..]);
}

/// <summary>
/// Converts a hexadecimal character into its corresponding integer nibble value.
/// </summary>
/// <param name="c">The hex character to evaluate (0-9, a-f, A-F).</param>
/// <returns>An integer value from 0 to 15 if the character is valid hex; otherwise, -1.</returns>
private static int HexCharToValue(char c)
{
return c >= '0' && c <= '9' ? c - '0'
Expand All @@ -121,30 +122,74 @@ private static int HexCharToValue(char c)
: -1;
}

/// <summary>
/// Converts an integer nibble value into its corresponding lowercase hexadecimal character representation.
/// </summary>
/// <param name="value">The nibble value to convert (0-15).</param>
/// <returns>A lowercase hexadecimal character representing the specified value.</returns>
private static char HexValueToChar(int value)
{
return (char) (value < 10 ? value + '0' : value - 10 + 'a');
}

/// <summary>
/// Returns the MongoId as a 24-character lowercase hexadecimal string.
/// </summary>
public override string ToString()
{
if (_timestampAndMachine == 0 && _pidAndIncrement == 0)
if (IsEmpty)
{
return string.Empty;
}

Span<byte> bytes = stackalloc byte[12];
BitConverter.TryWriteBytes(bytes, _timestampAndMachine);
BitConverter.TryWriteBytes(bytes[8..], _pidAndIncrement);
return Convert.ToHexString(bytes).ToLowerInvariant();
return string.Create(24, this, static (chars, state) =>
{
Span<byte> bytes = stackalloc byte[12];
BitConverter.TryWriteBytes(bytes, state._timestampAndMachine);
BitConverter.TryWriteBytes(bytes[8..], state._pidAndIncrement);

for (var i = 0; i < 12; i++)
{
var b = bytes[i];
chars[i * 2] = HexValueToChar(b >> 4);
chars[(i * 2) + 1] = HexValueToChar(b & 0x0F);
}
});
}

public bool Equals(MongoId? other)
/// <summary>
/// Tries to format the current <see cref="MongoId"/> instance into the provided character span.
/// </summary>
/// <param name="destination">The destination span. Must be at least 24 characters long.</param>
/// <param name="charsWritten">When this method returns, contains the number of characters written.</param>
/// <returns><see langword="true"/> if the formatting was successful; otherwise, <see langword="false"/>.</returns>
public bool TryFormat(Span<char> destination, out int charsWritten)
{
if (other is null)
if (destination.Length < 24)
{
charsWritten = 0;
return false;
}

return _timestampAndMachine == other.Value._timestampAndMachine && _pidAndIncrement == other.Value._pidAndIncrement;
if (IsEmpty)
{
charsWritten = 0;
return true;
}

Span<byte> bytes = stackalloc byte[12];
BitConverter.TryWriteBytes(bytes, _timestampAndMachine);
BitConverter.TryWriteBytes(bytes[8..], _pidAndIncrement);

for (var i = 0; i < 12; i++)
{
var b = bytes[i];
destination[i * 2] = HexValueToChar(b >> 4);
destination[(i * 2) + 1] = HexValueToChar(b & 0x0F);
}

charsWritten = 24;
return true;
}

/// <inheritdoc/>
Expand All @@ -170,7 +215,7 @@ public bool Equals(string? other)
return false;
}

bytes[i] = (byte)((hi << 4) | lo);
bytes[i] = (byte) ((hi << 4) | lo);
}

var a = BitConverter.ToInt64(bytes);
Expand All @@ -179,6 +224,11 @@ public bool Equals(string? other)
return _timestampAndMachine == a && _pidAndIncrement == b;
}

/// <summary>
/// Validates whether the specified string represents a valid MongoDB ObjectId format.
/// </summary>
/// <param name="stringToCheck">The string representation of the identifier to validate.</param>
/// <returns><see langword="true"/> if the string satisfies format constraints; otherwise, <see langword="false"/>.</returns>
public static bool IsValidMongoId(string stringToCheck)
{
return stringToCheck.IsValidMongoId();
Expand Down Expand Up @@ -232,8 +282,12 @@ public override int GetHashCode()
return HashCode.Combine(_timestampAndMachine, _pidAndIncrement);
}

/// <summary>
/// Returns an empty <see cref="MongoId"/> instance with all internal bits initialized to zero.
/// </summary>
/// <returns>A default initialized <see cref="MongoId"/>.</returns>
public static MongoId Empty()
{
return new MongoId("000000000000000000000000");
return default;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace SPTarkov.Server.Core.Utils.Json.Converters;

public class StringToMongoIdConverter : JsonConverter<MongoId>
public sealed class StringToMongoIdConverter : JsonConverter<MongoId>
{
public override MongoId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Expand All @@ -13,12 +13,20 @@ public override MongoId Read(ref Utf8JsonReader reader, Type typeToConvert, Json
return new MongoId(reader.GetString());
}

throw new JsonException();
throw new JsonException($"The JsonTokenType was not of type string, it was: {reader.TokenType}");
}

public override void Write(Utf8JsonWriter writer, MongoId mongoId, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, mongoId.ToString(), options);
Span<char> buffer = stackalloc char[24];
if (mongoId.TryFormat(buffer, out var charsWritten))
{
writer.WriteStringValue(buffer[..charsWritten]);
}
else
{
throw new JsonException("Failed to format MongoId to stack buffer.");
}
}

// Deserialize MongoId as a dictionary key
Expand All @@ -30,6 +38,21 @@ public override MongoId ReadAsPropertyName(ref Utf8JsonReader reader, Type typeT
// Serialize MongoId as a dictionary key
public override void WriteAsPropertyName(Utf8JsonWriter writer, MongoId value, JsonSerializerOptions options)
{
writer.WritePropertyName(value.ToString());
Span<char> buffer = stackalloc char[24];
if (value.TryFormat(buffer, out var charsWritten))
{
if (charsWritten == 0)
{
writer.WritePropertyName(string.Empty);
}
else
{
writer.WritePropertyName(buffer[..charsWritten]);
}
}
else
{
throw new JsonException("Failed to format MongoId to stack buffer.");
}
}
}
Loading
Loading