Skip to content

fix: add validation for alphabet random, fix performance issue and more#2

Merged
dryfish09 merged 1 commit into
mainfrom
fix/fix
Jun 20, 2026
Merged

fix: add validation for alphabet random, fix performance issue and more#2
dryfish09 merged 1 commit into
mainfrom
fix/fix

Conversation

@dryfish09

Copy link
Copy Markdown
Owner

No description provided.

@dryfish09 dryfish09 merged commit 2d7a4aa into main Jun 20, 2026
12 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces performance optimizations and new features to the ILibRandom class, including thread-static byte buffers, .NET 6+ specific optimizations for random integer and long generation, stricter character range validation, new random decimal generation methods, and random enum generation. The review feedback highlights several critical issues: potential integer overflows in IRandomInt and IRandomLong when dealing with maximum bounds, a minor bias in the rejection sampling logic of IRandomLong, potential overflow in IRandomDecimal with high precision, and performance overhead and infinite loop risks in the new IRandomEnum methods.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/main/ILibRandom.cs
Comment on lines +72 to +74
#if NET6_0_OR_GREATER
return System.Random.Shared.Next(min, max + 1);
#else

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If max is int.MaxValue, max + 1 will overflow to int.MinValue. This causes System.Random.Shared.Next(min, max + 1) to throw an ArgumentOutOfRangeException because the upper bound is less than the lower bound.

By casting to long, we can safely calculate the range (long)max - min + 1 without any overflow, and then use NextInt64 to get a uniform random value.

#if NET6_0_OR_GREATER
            return (int)(min + System.Random.Shared.NextInt64((long)max - min + 1));
#else

Comment thread src/main/ILibRandom.cs
Comment on lines +181 to +208
#if NET6_0_OR_GREATER
// .NET 6+ has built-in method for long range
return System.Random.Shared.NextInt64(min, max + 1);
#else
// For older frameworks, use rejection sampling to avoid bias
ulong range = (ulong)(max - min);

// Handle full range (ulong.MaxValue)
if (range == ulong.MaxValue)
{
byte[] buf = new byte[8];
_random.NextBytes(buf);
return BitConverter.ToInt64(buf, 0);
byte[] buffer = GetByteBuffer();
_random.NextBytes(buffer);
return BitConverter.ToInt64(buffer, 0);
}

byte[] bytes = new byte[8];
_random.NextBytes(bytes);
ulong uval = BitConverter.ToUInt64(bytes, 0);

// Rejection sampling to eliminate bias
ulong limit = ulong.MaxValue - ulong.MaxValue % (range + 1);
byte[] bytes = GetByteBuffer();
ulong uval;

do
{
_random.NextBytes(bytes);
uval = BitConverter.ToUInt64(bytes, 0);
} while (uval > limit);

return min + (long)(uval % (range + 1));
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This method has two issues:

  1. If max is long.MaxValue, max + 1 will overflow to long.MinValue, causing System.Random.Shared.NextInt64 to throw an ArgumentOutOfRangeException.
  2. The rejection sampling loop condition uval > limit accepts limit + 1 values, which is not a multiple of range + 1, introducing a tiny bias. Changing it to uval >= limit ensures perfect uniformity.

For .NET 6+, we can handle the max == long.MaxValue case using Span<byte> and stackalloc to avoid any thread-static buffer access or array overhead.

#if NET6_0_OR_GREATER
            if (max < long.MaxValue)
            {
                return System.Random.Shared.NextInt64(min, max + 1);
            }

            ulong range = (ulong)(max - min);
            if (range == ulong.MaxValue)
            {
                Span<byte> buffer = stackalloc byte[8];
                System.Random.Shared.NextBytes(buffer);
                return BitConverter.ToInt64(buffer);
            }

            ulong limit = ulong.MaxValue - ulong.MaxValue % (range + 1);
            Span<byte> bytes = stackalloc byte[8];
            ulong uval;
            do
            {
                System.Random.Shared.NextBytes(bytes);
                uval = BitConverter.ToUInt64(bytes);
            } while (uval >= limit);

            return min + (long)(uval % (range + 1));
#else
            ulong range = (ulong)(max - min);
            
            if (range == ulong.MaxValue)
            {
                byte[] buffer = GetByteBuffer();
                _random.NextBytes(buffer);
                return BitConverter.ToInt64(buffer, 0);
            }
            
            ulong limit = ulong.MaxValue - ulong.MaxValue % (range + 1);
            byte[] bytes = GetByteBuffer();
            ulong uval;
            
            do
            {
                _random.NextBytes(bytes);
                uval = BitConverter.ToUInt64(bytes, 0);
            } while (uval >= limit);
            
            return min + (long)(uval % (range + 1));
#endif

Comment thread src/main/ILibRandom.cs
Comment on lines +257 to +263
// Generate random integer with specified precision
long multiplier = (long)Math.Pow(10, precision);
long minScaled = (long)(min * multiplier);
long maxScaled = (long)(max * multiplier);

long randomScaled = IRandomLong(minScaled, maxScaled);
return randomScaled / (decimal)multiplier;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Math.Pow(10, precision) and casting to long will overflow when precision >= 19 (since 10^19 exceeds long.MaxValue). Additionally, min * multiplier or max * multiplier can easily overflow long even for smaller precisions if the values are large.

A much safer and simpler approach is to generate the random decimal using IRandomDecimal(min, max) and then round it to the desired precision using Math.Round.

            decimal randomValue = IRandomDecimal(min, max);
            return Math.Round(randomValue, precision, MidpointRounding.AwayFromZero);

Comment thread src/main/ILibRandom.cs
Comment on lines +301 to +322
/// <summary>
/// Returns a random element from an enumeration
/// </summary>
public static T IRandomEnum<T>() where T : Enum
{
var values = Enum.GetValues(typeof(T));
return (T)values.GetValue(_random.Next(values.Length))!;
}

/// <summary>
/// Returns a random element from an enumeration with exclusion
/// </summary>
public static T IRandomEnum<T>(T exclude) where T : Enum
{
var values = Enum.GetValues(typeof(T));
T result;
do
{
result = (T)values.GetValue(_random.Next(values.Length))!;
} while (result.Equals(exclude));
return result;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation has two main issues:

  1. Calling Enum.GetValues(typeof(T)) on every invocation is very slow because it uses reflection and allocates a new array each time. We can cache the values in a generic static class EnumCache<T> to avoid allocations and reflection overhead.
  2. In IRandomEnum<T>(T exclude), if the enum has only one value and it is excluded (or if all values are excluded), the do-while loop will run infinitely, causing a denial of service. We should validate that there is at least one non-excluded value before looping.
        private static class EnumCache<T> where T : Enum
        {
            public static readonly T[] Values = (T[])Enum.GetValues(typeof(T));
        }

        /// <summary>
        /// Returns a random element from an enumeration
        /// </summary>
        public static T IRandomEnum<T>() where T : Enum
        {
            var values = EnumCache<T>.Values;
            if (values.Length == 0)
                throw new ArgumentException("Enum has no values");
            return values[_random.Next(values.Length)];
        }

        /// <summary>
        /// Returns a random element from an enumeration with exclusion
        /// </summary>
        public static T IRandomEnum<T>(T exclude) where T : Enum
        {
            var values = EnumCache<T>.Values;
            if (values.Length == 0)
                throw new ArgumentException("Enum has no values");

            bool hasOther = false;
            foreach (var val in values)
            {
                if (!val.Equals(exclude))
                {
                    hasOther = true;
                    break;
                }
            }
            if (!hasOther)
                throw new ArgumentException("All values in the enum are excluded");

            T result;
            do
            {
                result = values[_random.Next(values.Length)];
            } while (result.Equals(exclude));
            return result;
        }

@dryfish09 dryfish09 deleted the fix/fix branch June 20, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant