< Summary

Information
Class: ArturRios.Util.Random.CustomRandom
Assembly: ArturRios.Util
File(s): /home/runner/work/dotnet-util/dotnet-util/src/Random/CustomRandom.cs
Line coverage
96%
Covered lines: 56
Uncovered lines: 2
Coverable lines: 58
Total lines: 230
Line coverage: 96.5%
Branch coverage
89%
Covered branches: 34
Total branches: 38
Branch coverage: 89.4%
Method coverage
100%
Covered methods: 6
Fully covered methods: 4
Total methods: 6
Method coverage: 100%
Full method coverage: 66.6%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
NumberFromRng(...)50%2280%
NumberFromSystemRandom(...)75%4480%
Text(...)100%1818100%
IsExcluded(...)100%22100%
ValidateRange(...)100%44100%
NextInclusive(...)75%88100%

File(s)

/home/runner/work/dotnet-util/dotnet-util/src/Random/CustomRandom.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using ArturRios.Util.Collections;
 3
 4namespace ArturRios.Util.Random;
 5
 6/// <summary>
 7/// Provides helpers for generating random numbers and strings with optional exclusion constraints.
 8/// </summary>
 9/// <remarks>
 10/// Both number helpers treat their bounds as inclusive on each end, so <c>(1, 6)</c> models a die
 11/// whichever source of randomness is chosen. Pick <see cref="NumberFromRng"/> for anything a third party
 12/// must not be able to predict, and <see cref="NumberFromSystemRandom"/> when throughput matters more
 13/// than unpredictability.
 14/// </remarks>
 15public static class CustomRandom
 16{
 17    /// <summary>
 18    /// Caps how many times <see cref="Text"/> redraws while trying to avoid the excluded strings before
 19    /// concluding that the exclusion list has swallowed the whole search space.
 20    /// </summary>
 21    private const int MaxExclusionAttempts = 1000;
 22
 23    /// <summary>
 24    /// Generates a cryptographically strong random integer between <paramref name="start"/> (inclusive) and <paramref n
 25    /// </summary>
 26    /// <param name="start">Minimum value (inclusive).</param>
 27    /// <param name="end">Maximum value (inclusive).</param>
 28    /// <param name="differentFrom">Optional value to avoid returning; regeneration occurs until distinct.</param>
 29    /// <returns>A random integer in the specified range.</returns>
 30    /// <exception cref="ArgumentOutOfRangeException"><paramref name="end"/> is less than <paramref name="start"/>.</exc
 31    /// <exception cref="ArgumentException">
 32    /// <paramref name="differentFrom"/> is the only value the range can produce, which would leave nothing
 33    /// to return.
 34    /// </exception>
 35    /// <remarks>
 36    /// The whole 32-bit range is supported, including an <paramref name="end"/> of
 37    /// <see cref="int.MaxValue"/>: the exclusive bound is computed in 64-bit arithmetic so it cannot wrap.
 38    /// </remarks>
 39    public static int NumberFromRng(int start, int end, int? differentFrom = null)
 40    {
 70641        ValidateRange(start, end, differentFrom);
 42
 70443        var random = NextInclusive(start, end);
 44
 70445        while (random == differentFrom)
 46        {
 047            random = NextInclusive(start, end);
 48        }
 49
 70450        return random;
 51    }
 52
 53    /// <summary>
 54    /// Generates a random integer using <see cref="System.Random"/> between <paramref name="start"/> (inclusive) and <p
 55    /// </summary>
 56    /// <param name="start">Minimum value (inclusive).</param>
 57    /// <param name="end">Maximum value (inclusive).</param>
 58    /// <param name="differentFrom">Optional value to avoid returning; regeneration occurs until distinct.</param>
 59    /// <returns>A random integer in the specified range.</returns>
 60    /// <exception cref="ArgumentOutOfRangeException"><paramref name="end"/> is less than <paramref name="start"/>.</exc
 61    /// <exception cref="ArgumentException">
 62    /// <paramref name="differentFrom"/> is the only value the range can produce, which would leave nothing
 63    /// to return.
 64    /// </exception>
 65    /// <remarks>
 66    /// <paramref name="end"/> is inclusive, matching <see cref="NumberFromRng"/>. This is not suitable for
 67    /// tokens, keys or anything else an adversary must not be able to predict.
 68    /// </remarks>
 69    public static int NumberFromSystemRandom(int start, int end, int? differentFrom = null)
 70    {
 60671        ValidateRange(start, end, differentFrom);
 72
 60473        var random = System.Random.Shared.NextInt64(start, (long)end + 1L);
 74
 60475        while (random == differentFrom)
 76        {
 077            random = System.Random.Shared.NextInt64(start, (long)end + 1L);
 78        }
 79
 60480        return (int)random;
 81    }
 82
 83    /// <summary>
 84    /// Generates a cryptographically strong random string respecting the constraints defined in <paramref name="options
 85    /// </summary>
 86    /// <remarks>
 87    /// Every character is drawn from the union of the character sets enabled in <paramref name="options"/>, and the
 88    /// result is guaranteed to contain at least one character from each enabled set. The result is therefore suitable
 89    /// for security tokens: it is sourced from <see cref="RandomNumberGenerator"/>, not <see cref="System.Random"/>.
 90    /// </remarks>
 91    /// <param name="options">Options controlling length and character inclusion.</param>
 92    /// <param name="differentFrom">Optional collection of strings to exclude from results. Null entries are ignored.</p
 93    /// <returns>A randomly generated string of exactly <see cref="RandomStringOptions.Length"/> characters.</returns>
 94    /// <exception cref="ArgumentNullException"><paramref name="options"/> is <c>null</c>.</exception>
 95    /// <exception cref="ArgumentException">
 96    /// No character set is enabled, or <see cref="RandomStringOptions.Length"/> is smaller than the number of enabled
 97    /// sets — in which case the length and the at-least-one-of-each guarantee cannot both be honored.
 98    /// </exception>
 99    /// <exception cref="InvalidOperationException">
 100    /// <paramref name="differentFrom"/> excluded every candidate produced in
 101    /// <see cref="MaxExclusionAttempts"/> draws, which means the exclusion list covers effectively the whole
 102    /// search space. Widen the alphabet or the length rather than retrying.
 103    /// </exception>
 104    public static string Text(RandomStringOptions options, string[]? differentFrom = null)
 105    {
 651106        ArgumentNullException.ThrowIfNull(options);
 107
 650108        var characterSets = new List<string>(4);
 109
 650110        if (options.IncludeLowercase)
 111        {
 471112            characterSets.Add(Characters.LowerLetters);
 113        }
 114
 650115        if (options.IncludeUppercase)
 116        {
 463117            characterSets.Add(Characters.UpperLetters);
 118        }
 119
 650120        if (options.IncludeSpecialCharacters)
 121        {
 438122            characterSets.Add(Characters.Special);
 123        }
 124
 650125        if (options.IncludeDigits)
 126        {
 463127            characterSets.Add(Characters.Digits);
 128        }
 129
 650130        if (characterSets.Count == 0)
 131        {
 1132            throw new ArgumentException("At least one character set must be included.", nameof(options));
 133        }
 134
 649135        if (options.Length < characterSets.Count)
 136        {
 4137            throw new ArgumentException(
 4138                $"Length must be at least {characterSets.Count} to include one character from each enabled character set
 4139                nameof(options));
 140        }
 141
 645142        var alphabet = string.Concat(characterSets);
 143
 3290144        for (var attempt = 0; attempt < MaxExclusionAttempts; attempt++)
 145        {
 1644146            var characters = new char[options.Length];
 147
 8930148            for (var i = 0; i < characterSets.Count; i++)
 149            {
 2821150                characters[i] = characterSets[i][RandomNumberGenerator.GetInt32(characterSets[i].Length)];
 151            }
 152
 1644153            RandomNumberGenerator.GetItems<char>(alphabet, characters.AsSpan(characterSets.Count));
 1644154            RandomNumberGenerator.Shuffle<char>(characters);
 155
 1644156            var generatedString = new string(characters);
 157
 1644158            if (!IsExcluded(generatedString, differentFrom))
 159            {
 644160                return generatedString;
 161            }
 162        }
 163
 1164        throw new InvalidOperationException(
 1165            $"Could not generate a string outside the excluded set within {MaxExclusionAttempts} attempts. " +
 1166            "The exclusion list covers effectively every string the current options can produce.");
 167    }
 168
 169    /// <summary>
 170    /// Checks a candidate against the exclusion list, tolerating a null entry in it.
 171    /// </summary>
 172    private static bool IsExcluded(string candidate, string[]? differentFrom) =>
 1644173        differentFrom is not null &&
 15055174        Array.Exists(differentFrom, excluded => string.Equals(excluded, candidate, StringComparison.Ordinal));
 175
 176    /// <summary>
 177    /// Rejects ranges that are inverted, or that could only ever return the excluded value.
 178    /// </summary>
 179    private static void ValidateRange(int start, int end, int? differentFrom)
 180    {
 1312181        ArgumentOutOfRangeException.ThrowIfLessThan(end, start);
 182
 1310183        if (start == end && differentFrom == start)
 184        {
 2185            throw new ArgumentException(
 2186                $"The range [{start}, {end}] contains only {differentFrom}, so no different value can be produced.",
 2187                nameof(differentFrom));
 188        }
 1308189    }
 190
 191    /// <summary>
 192    /// Draws a uniform value in [<paramref name="start"/>, <paramref name="end"/>] from
 193    /// <see cref="RandomNumberGenerator"/>.
 194    /// </summary>
 195    /// <remarks>
 196    /// <see cref="RandomNumberGenerator.GetInt32(int, int)"/> takes an exclusive upper bound, which cannot
 197    /// express an inclusive <see cref="int.MaxValue"/>. That single case is served by rejection sampling
 198    /// over 64 random bits, which keeps the distribution uniform.
 199    /// </remarks>
 200    private static int NextInclusive(int start, int end)
 201    {
 704202        if (start == end)
 203        {
 2204            return start;
 205        }
 206
 702207        if (end < int.MaxValue)
 208        {
 552209            return RandomNumberGenerator.GetInt32(start, end + 1);
 210        }
 211
 150212        var range = (ulong)((long)end - start) + 1UL;
 213
 214        // Values in the incomplete final bucket are rejected, so every bucket stays equally likely.
 150215        var remainder = (ulong.MaxValue % range + 1UL) % range;
 216
 150217        Span<byte> buffer = stackalloc byte[sizeof(ulong)];
 218        ulong draw;
 219
 220        do
 221        {
 150222            RandomNumberGenerator.Fill(buffer);
 223
 150224            draw = BitConverter.ToUInt64(buffer);
 225        }
 150226        while (remainder != 0UL && draw > ulong.MaxValue - remainder);
 227
 150228        return (int)(start + (long)(draw % range));
 229    }
 230}