From 8e3894e9ad5383ce507fb7ddcea538021b56ec75 Mon Sep 17 00:00:00 2001 From: Lacyway <20912169+Lacyway@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:17:39 +0200 Subject: [PATCH] MongoID improvements (#437) * MongoID improvements - Added extension to check whether a MongoID is valid, 33% faster than old method - Cut down generation speed by 2/3 * Fix method used * Add test --- .../Extensions/MongoIDExtensions.cs | 71 +++++++++++++++++++ .../Models/Common/MongoId.cs | 39 +++++----- UnitTests/Tests/Utils/MongoIdTests.cs | 35 +++++++++ 3 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 Libraries/SPTarkov.Server.Core/Extensions/MongoIDExtensions.cs create mode 100644 UnitTests/Tests/Utils/MongoIdTests.cs diff --git a/Libraries/SPTarkov.Server.Core/Extensions/MongoIDExtensions.cs b/Libraries/SPTarkov.Server.Core/Extensions/MongoIDExtensions.cs new file mode 100644 index 00000000..6b742e0e --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Extensions/MongoIDExtensions.cs @@ -0,0 +1,71 @@ +using SPTarkov.Server.Core.Models.Common; + +namespace SPTarkov.Server.Core.Extensions +{ + public static class MongoIDExtensions + { + /// + /// Determines whether the specified is a valid 24-character hexadecimal string, + /// which is the standard format for MongoDB ObjectIds. + /// + /// The to validate. + /// if the is a valid MongoDB ObjectId; otherwise, . + public static bool IsValidMongoId(this MongoId mongoId) + { + var span = mongoId.ToString().AsSpan(); + + if (span.Length != 24) + { + return false; + } + + for (var i = 0; i < 24; i++) + { + var c = span[i]; + var isHex = + (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); + + if (!isHex) + { + return false; + } + } + + return true; + } + + /// + /// Determines whether the specified string is a valid 24-character hexadecimal representation + /// of a MongoDB ObjectId. + /// + /// The string to validate as a MongoDB ObjectId. + /// if the is a valid MongoDB ObjectId; otherwise, . + public static bool IsValidMongoId(this string mongoId) + { + var span = mongoId.AsSpan(); + + if (span.Length != 24) + { + return false; + } + + for (var i = 0; i < 24; i++) + { + var c = span[i]; + var isHex = + (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); + + if (!isHex) + { + return false; + } + } + + return true; + } + } +} diff --git a/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs b/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs index 313e2514..48acc184 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using SPTarkov.Server.Core.Extensions; namespace SPTarkov.Server.Core.Models.Common; @@ -35,29 +36,26 @@ public readonly partial struct MongoId : IEquatable /// 24 character objectId private static string Generate() { - // Allocate a span directly onto the stack, will dispose whenever we finished running - // Span is recommended to work with stackalloc + we use stackalloc here because we don't do anything with this afterwards Span objectId = stackalloc byte[12]; - // Time stamp (4 bytes) - var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - // Convert to big-endian - objectId[0] = (byte)(timestamp >> 24); - objectId[1] = (byte)(timestamp >> 16); - objectId[2] = (byte)(timestamp >> 8); - objectId[3] = (byte)timestamp; + // 4 bytes: current timestamp (big endian) + var timestamp = (int) DateTimeOffset.UtcNow + .ToUnixTimeSeconds(); + objectId[0] = (byte) (timestamp >> 24); + objectId[1] = (byte) (timestamp >> 16); + objectId[2] = (byte) (timestamp >> 8); + objectId[3] = (byte) timestamp; - // Random value (5 bytes) - var rand = new Random(); - rand.NextBytes(objectId.Slice(4, 5)); + // 5 bytes: random machine/process identifier + Random.Shared.NextBytes(objectId.Slice(4, 5)); - // Incrementing counter (3 bytes) - // 24-bit counter - var counter = rand.Next(0, 16777215); - objectId[9] = (byte)(counter >> 16); - objectId[10] = (byte)(counter >> 8); - objectId[11] = (byte)counter; + // 3 bytes: random counter fallback (no static state) + var counter = Random.Shared.Next(0, 0xFFFFFF); + objectId[9] = (byte) (counter >> 16); + objectId[10] = (byte) (counter >> 8); + objectId[11] = (byte) counter; + // Convert to lowercase hex string (24 chars) return Convert.ToHexStringLower(objectId); } @@ -88,7 +86,7 @@ public readonly partial struct MongoId : IEquatable public static bool IsValidMongoId(string stringToCheck) { - return MongoIdRegex().IsMatch(stringToCheck); + return stringToCheck.IsValidMongoId(); } public static implicit operator string(MongoId mongoId) @@ -140,7 +138,4 @@ public readonly partial struct MongoId : IEquatable { return new MongoId("000000000000000000000000"); } - - [GeneratedRegex("^[a-fA-F0-9]{24}$")] - private static partial Regex MongoIdRegex(); } diff --git a/UnitTests/Tests/Utils/MongoIdTests.cs b/UnitTests/Tests/Utils/MongoIdTests.cs new file mode 100644 index 00000000..f5e4072b --- /dev/null +++ b/UnitTests/Tests/Utils/MongoIdTests.cs @@ -0,0 +1,35 @@ +using SPTarkov.Server.Core.Extensions; +using SPTarkov.Server.Core.Models.Common; + +namespace UnitTests.Tests.Utils +{ + /// + /// Unit tests for the struct. + /// + [TestClass] + public class MongoIdTests + { + /// + /// Test that generates 1000 and ensures they are all valid.
+ /// Validity is checked by ensuring the ID is non-empty, exactly 24 characters, and matches the expected format. + ///
+ [TestMethod] + public void Generate_ShouldProduceValidMongoIds() + { + var invalidIds = new List(); + + for (var i = 0; i < 1000; i++) + { + var id = new MongoId(); + var idStr = id.ToString(); + + if (string.IsNullOrWhiteSpace(idStr) || idStr.Length != 24 || !id.IsValidMongoId()) + { + invalidIds.Add(idStr); + } + } + + Assert.AreEqual(0, invalidIds.Count, $"Invalid MongoIds found: {string.Join(", ", invalidIds)}"); + } + } +}