diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommand.cs index a8ad9deb22..c77c25c5f4 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommand.cs @@ -1,199 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Numerics; -using System.Security.Cryptography; -using System.Threading; - -using Debug = System.Management.Automation.Diagnostics; namespace Microsoft.PowerShell.Commands { /// - /// This class implements get-random cmdlet. + /// This class implements `Get-Random` cmdlet. /// - /// - [Cmdlet(VerbsCommon.Get, "Random", DefaultParameterSetName = GetRandomCommand.RandomNumberParameterSet, + [Cmdlet(VerbsCommon.Get, "Random", DefaultParameterSetName = GetRandomCommandBase.RandomNumberParameterSet, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2097016", RemotingCapability = RemotingCapability.None)] [OutputType(typeof(int), typeof(long), typeof(double))] - public class GetRandomCommand : PSCmdlet + public sealed class GetRandomCommand : GetRandomCommandBase { - #region Parameter set handling - - private const string RandomNumberParameterSet = "RandomNumberParameterSet"; - private const string RandomListItemParameterSet = "RandomListItemParameterSet"; - private const string ShuffleParameterSet = "ShuffleParameterSet"; - - private static readonly object[] _nullInArray = new object[] { null }; - - private enum MyParameterSet - { - Unknown, - RandomNumber, - RandomListItem - } - - private MyParameterSet _effectiveParameterSet; - - private MyParameterSet EffectiveParameterSet - { - get - { - // cache MyParameterSet enum instead of doing string comparison every time - if (_effectiveParameterSet == MyParameterSet.Unknown) - { - if ((MyInvocation.ExpectingInput) && (Maximum == null) && (Minimum == null)) - { - _effectiveParameterSet = MyParameterSet.RandomListItem; - } - else if (ParameterSetName == GetRandomCommand.RandomListItemParameterSet - || ParameterSetName == GetRandomCommand.ShuffleParameterSet) - { - _effectiveParameterSet = MyParameterSet.RandomListItem; - } - else if (ParameterSetName.Equals(GetRandomCommand.RandomNumberParameterSet, StringComparison.OrdinalIgnoreCase)) - { - if ((Maximum != null) && (Maximum.GetType().IsArray)) - { - InputObject = (object[])Maximum; - _effectiveParameterSet = MyParameterSet.RandomListItem; - } - else - { - _effectiveParameterSet = MyParameterSet.RandomNumber; - } - } - else - { - Debug.Assert(false, "Unrecognized parameter set"); - } - } - - return _effectiveParameterSet; - } - } - - #endregion Parameter set handling - - #region Error handling - - private void ThrowMinGreaterThanOrEqualMax(object minValue, object maxValue) - { - if (minValue == null) - { - throw PSTraceSource.NewArgumentNullException("min"); - } - - if (maxValue == null) - { - throw PSTraceSource.NewArgumentNullException("max"); - } - - ErrorRecord errorRecord = new( - new ArgumentException(string.Format( - CultureInfo.InvariantCulture, GetRandomCommandStrings.MinGreaterThanOrEqualMax, minValue, maxValue)), - "MinGreaterThanOrEqualMax", - ErrorCategory.InvalidArgument, - null); - - ThrowTerminatingError(errorRecord); - } - - #endregion - - #region Random generator state - - private static readonly ReaderWriterLockSlim s_runspaceGeneratorMapLock = new(); - - // 1-to-1 mapping of runspaces and random number generators - private static readonly Dictionary s_runspaceGeneratorMap = new(); - - private static void CurrentRunspace_StateChanged(object sender, RunspaceStateEventArgs e) - { - switch (e.RunspaceStateInfo.State) - { - case RunspaceState.Broken: - case RunspaceState.Closed: - try - { - GetRandomCommand.s_runspaceGeneratorMapLock.EnterWriteLock(); - GetRandomCommand.s_runspaceGeneratorMap.Remove(((Runspace)sender).InstanceId); - } - finally - { - GetRandomCommand.s_runspaceGeneratorMapLock.ExitWriteLock(); - } - - break; - } - } - - private PolymorphicRandomNumberGenerator _generator; - - /// - /// Gets and sets generator associated with the current runspace. - /// - private PolymorphicRandomNumberGenerator Generator - { - get - { - if (_generator == null) - { - Guid runspaceId = Context.CurrentRunspace.InstanceId; - - bool needToInitialize = false; - try - { - GetRandomCommand.s_runspaceGeneratorMapLock.EnterReadLock(); - needToInitialize = !GetRandomCommand.s_runspaceGeneratorMap.TryGetValue(runspaceId, out _generator); - } - finally - { - GetRandomCommand.s_runspaceGeneratorMapLock.ExitReadLock(); - } - - if (needToInitialize) - { - Generator = new PolymorphicRandomNumberGenerator(); - } - } - - return _generator; - } - - set - { - _generator = value; - Runspace myRunspace = Context.CurrentRunspace; - - try - { - GetRandomCommand.s_runspaceGeneratorMapLock.EnterWriteLock(); - if (!GetRandomCommand.s_runspaceGeneratorMap.ContainsKey(myRunspace.InstanceId)) - { - // make sure we won't leave the generator around after runspace exits - myRunspace.StateChanged += CurrentRunspace_StateChanged; - } - - GetRandomCommand.s_runspaceGeneratorMap[myRunspace.InstanceId] = _generator; - } - finally - { - GetRandomCommand.s_runspaceGeneratorMapLock.ExitWriteLock(); - } - } - } - - #endregion - - #region Common parameters - /// /// Seed used to reinitialize random numbers generator. /// @@ -201,194 +20,6 @@ namespace Microsoft.PowerShell.Commands [ValidateNotNull] public int? SetSeed { get; set; } - #endregion Common parameters - - #region Parameters for RandomNumberParameterSet - - /// - /// Maximum number to generate. - /// - [Parameter(ParameterSetName = RandomNumberParameterSet, Position = 0)] - public object Maximum { get; set; } - - /// - /// Minimum number to generate. - /// - [Parameter(ParameterSetName = RandomNumberParameterSet)] - public object Minimum { get; set; } - - private static bool IsInt(object o) - { - if (o == null || o is int) - { - return true; - } - - return false; - } - - private static bool IsInt64(object o) - { - if (o == null || o is long) - { - return true; - } - - return false; - } - - private static object ProcessOperand(object o) - { - if (o == null) - { - return null; - } - - PSObject pso = PSObject.AsPSObject(o); - object baseObject = pso.BaseObject; - - if (baseObject is string) - { - // The type argument passed in does not decide the number type we want to convert to. ScanNumber will return - // int/long/double based on the string form number passed in. - baseObject = System.Management.Automation.Language.Parser.ScanNumber((string)baseObject, typeof(int)); - } - - return baseObject; - } - - private static double ConvertToDouble(object o, double defaultIfNull) - { - if (o == null) - { - return defaultIfNull; - } - - double result = (double)LanguagePrimitives.ConvertTo(o, typeof(double), CultureInfo.InvariantCulture); - return result; - } - - #endregion - - #region Parameters and variables for RandomListItemParameterSet - - private List _chosenListItems; - private int _numberOfProcessedListItems; - - /// - /// List from which random elements are chosen. - /// - [Parameter(ParameterSetName = RandomListItemParameterSet, ValueFromPipeline = true, Position = 0, Mandatory = true)] - [Parameter(ParameterSetName = ShuffleParameterSet, ValueFromPipeline = true, Position = 0, Mandatory = true)] - [System.Management.Automation.AllowNull] - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] - public object[] InputObject { get; set; } - - /// - /// Number of items to output (number of list items or of numbers). - /// - [Parameter(ParameterSetName = RandomNumberParameterSet)] - [Parameter(ParameterSetName = RandomListItemParameterSet)] - [ValidateRange(1, int.MaxValue)] - public int Count { get; set; } = 1; - - #endregion - - #region Shuffle parameter - - /// - /// Gets or sets whether the command should return all input objects in randomized order. - /// - [Parameter(ParameterSetName = ShuffleParameterSet, Mandatory = true)] - public SwitchParameter Shuffle { get; set; } - - #endregion - - #region Cmdlet processing methods - - private double GetRandomDouble(double minValue, double maxValue) - { - double randomNumber; - double diff = maxValue - minValue; - - // I couldn't find a better fix for bug #216893 then - // to test and retry if a random number falls outside the bounds - // because of floating-point-arithmetic inaccuracies. - // - // Performance in the normal case is not impacted much. - // In low-precision situations we should converge to a solution quickly - // (diff gets smaller at a quick pace). - - if (double.IsInfinity(diff)) - { - do - { - double r = Generator.NextDouble(); - randomNumber = minValue + r * maxValue - r * minValue; - } - while (randomNumber >= maxValue); - } - else - { - do - { - double r = Generator.NextDouble(); - randomNumber = minValue + r * diff; - diff *= r; - } - while (randomNumber >= maxValue); - } - - return randomNumber; - } - - /// - /// Get a random Int64 type number. - /// - /// - /// - /// - private long GetRandomInt64(long minValue, long maxValue) - { - // Randomly generate eight bytes and convert the byte array to UInt64 - var buffer = new byte[sizeof(ulong)]; - ulong randomUint64; - - BigInteger bigIntegerDiff = (BigInteger)maxValue - (BigInteger)minValue; - - // When the difference is less than int.MaxValue, use Random.Next(int, int) - if (bigIntegerDiff <= int.MaxValue) - { - int randomDiff = Generator.Next(0, (int)(maxValue - minValue)); - return minValue + randomDiff; - } - - // The difference of two Int64 numbers would not exceed UInt64.MaxValue, so it can be represented by a UInt64 number. - ulong uint64Diff = (ulong)bigIntegerDiff; - - // Calculate the number of bits to represent the diff in type UInt64 - int bitsToRepresentDiff = 0; - ulong diffCopy = uint64Diff; - for (; diffCopy != 0; bitsToRepresentDiff++) - { - diffCopy >>= 1; - } - // Get the mask for the number of bits - ulong mask = (0xffffffffffffffff >> (64 - bitsToRepresentDiff)); - do - { - // Randomly fill the buffer - Generator.NextBytes(buffer); - randomUint64 = BitConverter.ToUInt64(buffer, 0); - - // Get the last 'bitsToRepresentDiff' number of random bits - randomUint64 &= mask; - } while (uint64Diff <= randomUint64); - - double randomNumber = minValue * 1.0 + randomUint64 * 1.0; - return (long)randomNumber; - } - /// /// This method implements the BeginProcessing method for get-random command. /// @@ -399,335 +30,7 @@ namespace Microsoft.PowerShell.Commands Generator = new PolymorphicRandomNumberGenerator(SetSeed.Value); } - if (EffectiveParameterSet == MyParameterSet.RandomNumber) - { - object maxOperand = ProcessOperand(Maximum); - object minOperand = ProcessOperand(Minimum); - - if (IsInt(maxOperand) && IsInt(minOperand)) - { - int minValue = minOperand != null ? (int)minOperand : 0; - int maxValue = maxOperand != null ? (int)maxOperand : int.MaxValue; - - if (minValue >= maxValue) - { - ThrowMinGreaterThanOrEqualMax(minValue, maxValue); - } - - for (int i = 0; i < Count; i++) - { - int randomNumber = Generator.Next(minValue, maxValue); - Debug.Assert(minValue <= randomNumber, "lower bound <= random number"); - Debug.Assert(randomNumber < maxValue, "random number < upper bound"); - - WriteObject(randomNumber); - } - } - else if ((IsInt64(maxOperand) || IsInt(maxOperand)) && (IsInt64(minOperand) || IsInt(minOperand))) - { - long minValue = minOperand != null ? ((minOperand is long) ? (long)minOperand : (int)minOperand) : 0; - long maxValue = maxOperand != null ? ((maxOperand is long) ? (long)maxOperand : (int)maxOperand) : long.MaxValue; - - if (minValue >= maxValue) - { - ThrowMinGreaterThanOrEqualMax(minValue, maxValue); - } - - for (int i = 0; i < Count; i++) - { - long randomNumber = GetRandomInt64(minValue, maxValue); - Debug.Assert(minValue <= randomNumber, "lower bound <= random number"); - Debug.Assert(randomNumber < maxValue, "random number < upper bound"); - - WriteObject(randomNumber); - } - } - else - { - double minValue = (minOperand is double) ? (double)minOperand : ConvertToDouble(Minimum, 0.0); - double maxValue = (maxOperand is double) ? (double)maxOperand : ConvertToDouble(Maximum, double.MaxValue); - - if (minValue >= maxValue) - { - ThrowMinGreaterThanOrEqualMax(minValue, maxValue); - } - - for (int i = 0; i < Count; i++) - { - double randomNumber = GetRandomDouble(minValue, maxValue); - Debug.Assert(minValue <= randomNumber, "lower bound <= random number"); - Debug.Assert(randomNumber < maxValue, "random number < upper bound"); - - WriteObject(randomNumber); - } - } - } - else if (EffectiveParameterSet == MyParameterSet.RandomListItem) - { - _chosenListItems = new List(); - _numberOfProcessedListItems = 0; - } - } - - // rough proof that when choosing random K items out of N items - // each item has got K/N probability of being included in the final list - // - // probability that a particular item in chosenListItems is NOT going to be replaced - // when processing I-th input item [assumes I > K]: - // P_one_step(I) = 1 - ((K / I) * ((K - 1) / K) + ((I - K) / I) = (I - 1) / I - // <--A--> <-----B-----> <-----C-----> - // A - probability that I-th element is going to be replacing an element from chosenListItems - // (see (1) in the code below) - // B - probability that a particular element from chosenListItems is NOT going to be replaced - // (see (2) in the code below) - // C - probability that I-th element is NOT going to be replacing an element from chosenListItems - // (see (1) in the code below) - // - // probability that a particular item in chosenListItems is NOT going to be replaced - // when processing input items J through N [assumes J > K] - // P_removal(J) = Multiply(for I = J to N) P(I) = - // = ((J - 1) / J) * (J / (J + 1)) * ... * ((N - 2) / (N - 1)) * ((N - 1) / N) = - // = (J - 1) / N - // - // probability that when processing an element it is going to be put into chosenListItems - // P_insertion(I) = 1.0 when I <= K - see (3) in the code below - // P_insertion(I) = K/N otherwise - see (1) in the code below - // - // probability that a given element is going to be a part of the final list - // P_final(I) = P_insertion(I) * P_removal(max(I + 1, K + 1)) - // [for I <= K] = 1.0 * ((K + 1) - 1) / N = K / N - // [otherwise] = (K / I) * ((I + 1) - 1) / N = K / N - // - // which proves that P_final(I) = K / N for all values of I. QED. - - /// - /// This method implements the ProcessRecord method for get-random command. - /// - protected override void ProcessRecord() - { - if (EffectiveParameterSet == MyParameterSet.RandomListItem) - { - if (ParameterSetName == ShuffleParameterSet) - { - // this allows for $null to be in an array passed to InputObject - foreach (object item in InputObject ?? _nullInArray) - { - _chosenListItems.Add(item); - } - } - else - { - foreach (object item in InputObject ?? _nullInArray) - { - // (3) - if (_numberOfProcessedListItems < Count) - { - Debug.Assert(_chosenListItems.Count == _numberOfProcessedListItems, "Initial K elements should all be included in chosenListItems"); - _chosenListItems.Add(item); - } - else - { - Debug.Assert(_chosenListItems.Count == Count, "After processing K initial elements, the length of chosenItems should stay equal to K"); - - // (1) - if (Generator.Next(_numberOfProcessedListItems + 1) < Count) - { - // (2) - int indexToReplace = Generator.Next(_chosenListItems.Count); - _chosenListItems[indexToReplace] = item; - } - } - - _numberOfProcessedListItems++; - } - } - } - } - - /// - /// This method implements the EndProcessing method for get-random command. - /// - protected override void EndProcessing() - { - if (EffectiveParameterSet == MyParameterSet.RandomListItem) - { - // make sure the order is truly random - // (all permutations with the same probability) - // O(n) time - int n = _chosenListItems.Count; - for (int i = 0; i < n; i++) - { - // randomly choose j from [i...n) - int j = Generator.Next(i, n); - - WriteObject(_chosenListItems[j]); - - // remove the output object from consideration in the next iteration. - if (i != j) - { - _chosenListItems[j] = _chosenListItems[i]; - } - } - } - } - - #endregion Processing methods - } - - /// - /// Provides an adapter API for random numbers that may be either cryptographically random, or - /// generated with the regular pseudo-random number generator. Re-implementations of - /// methods using the NextBytes() primitive based on the CLR implementation: - /// https://referencesource.microsoft.com/#mscorlib/system/random.cs. - /// - internal class PolymorphicRandomNumberGenerator - { - /// - /// Initializes a new instance of the class. - /// - public PolymorphicRandomNumberGenerator() - { - _cryptographicGenerator = RandomNumberGenerator.Create(); - _pseudoGenerator = null; - } - - internal PolymorphicRandomNumberGenerator(int seed) - { - _cryptographicGenerator = null; - _pseudoGenerator = new Random(seed); - } - - private readonly Random _pseudoGenerator = null; - private readonly RandomNumberGenerator _cryptographicGenerator = null; - - /// - /// Generates a random floating-point number that is greater than or equal to 0.0, and less than 1.0. - /// - /// A random floating-point number that is greater than or equal to 0.0, and less than 1.0. - internal double NextDouble() - { - // According to the CLR source: - // "Including this division at the end gives us significantly improved random number distribution." - return Next() * (1.0 / int.MaxValue); - } - - /// - /// Generates a non-negative random integer. - /// - /// A non-negative random integer. - internal int Next() - { - int randomNumber; - - // The CLR implementation just fudges - // Int32.MaxValue down to (Int32.MaxValue - 1). This implementation - // errs on the side of correctness. - do - { - randomNumber = InternalSample(); - } - while (randomNumber == int.MaxValue); - - if (randomNumber < 0) - { - randomNumber += int.MaxValue; - } - - return randomNumber; - } - - /// - /// Returns a random integer that is within a specified range. - /// - /// The exclusive upper bound of the random number returned. - /// - internal int Next(int maxValue) - { - if (maxValue < 0) - { - throw new ArgumentOutOfRangeException(nameof(maxValue), GetRandomCommandStrings.MaxMustBeGreaterThanZeroApi); - } - - return Next(0, maxValue); - } - - /// - /// Returns a random integer that is within a specified range. - /// - /// The inclusive lower bound of the random number returned. - /// The exclusive upper bound of the random number returned. maxValue must be greater than or equal to minValue. - /// - public int Next(int minValue, int maxValue) - { - if (minValue > maxValue) - { - throw new ArgumentOutOfRangeException(nameof(minValue), GetRandomCommandStrings.MinGreaterThanOrEqualMaxApi); - } - - int randomNumber = 0; - - long range = (long)maxValue - (long)minValue; - if (range <= int.MaxValue) - { - randomNumber = ((int)(NextDouble() * range) + minValue); - } - else - { - double largeSample = InternalSampleLargeRange() * (1.0 / (2 * ((uint)int.MaxValue))); - randomNumber = (int)((long)(largeSample * range) + minValue); - } - - return randomNumber; - } - - /// - /// Fills the elements of a specified array of bytes with random numbers. - /// - /// The array to be filled. - internal void NextBytes(byte[] buffer) - { - if (_cryptographicGenerator != null) - { - _cryptographicGenerator.GetBytes(buffer); - } - else - { - _pseudoGenerator.NextBytes(buffer); - } - } - - /// - /// Samples a random integer. - /// - /// A random integer, using the full range of Int32. - private int InternalSample() - { - int randomNumber; - byte[] data = new byte[sizeof(int)]; - - NextBytes(data); - randomNumber = BitConverter.ToInt32(data, 0); - - return randomNumber; - } - - /// - /// Samples a random int when the range is large. This does - /// not need to be in the range of -Double.MaxValue .. Double.MaxValue, - /// just 0.. (2 * Int32.MaxValue) - 1 . - /// - /// - private double InternalSampleLargeRange() - { - double randomNumber; - - do - { - randomNumber = InternalSample(); - } while (randomNumber == int.MaxValue); - - randomNumber += int.MaxValue; - return randomNumber; + base.BeginProcessing(); } } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommandBase.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommandBase.cs new file mode 100644 index 0000000000..7d6e4e71e6 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommandBase.cs @@ -0,0 +1,719 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Numerics; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading; + +using Debug = System.Management.Automation.Diagnostics; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// This class implements base class for `Get-Random` and `Get-SecureRandom` cmdlets. + /// + public class GetRandomCommandBase : PSCmdlet + { + #region Parameter set handling + + internal const string RandomNumberParameterSet = "RandomNumberParameterSet"; + private const string RandomListItemParameterSet = "RandomListItemParameterSet"; + private const string ShuffleParameterSet = "ShuffleParameterSet"; + + private static readonly object[] _nullInArray = new object[] { null }; + + private enum MyParameterSet + { + Unknown, + RandomNumber, + RandomListItem + } + + private MyParameterSet _effectiveParameterSet; + + private MyParameterSet EffectiveParameterSet + { + get + { + // cache MyParameterSet enum instead of doing string comparison every time + if (_effectiveParameterSet == MyParameterSet.Unknown) + { + if (MyInvocation.ExpectingInput && (Maximum == null) && (Minimum == null)) + { + _effectiveParameterSet = MyParameterSet.RandomListItem; + } + else if (ParameterSetName == GetRandomCommandBase.RandomListItemParameterSet + || ParameterSetName == GetRandomCommandBase.ShuffleParameterSet) + { + _effectiveParameterSet = MyParameterSet.RandomListItem; + } + else if (ParameterSetName.Equals(GetRandomCommandBase.RandomNumberParameterSet, StringComparison.OrdinalIgnoreCase)) + { + if ((Maximum != null) && Maximum.GetType().IsArray) + { + InputObject = (object[])Maximum; + _effectiveParameterSet = MyParameterSet.RandomListItem; + } + else + { + _effectiveParameterSet = MyParameterSet.RandomNumber; + } + } + else + { + Debug.Assert(false, "Unrecognized parameter set"); + } + } + + return _effectiveParameterSet; + } + } + + #endregion Parameter set handling + + #region Error handling + + private void ThrowMinGreaterThanOrEqualMax(object minValue, object maxValue) + { + if (minValue == null) + { + throw PSTraceSource.NewArgumentNullException("min"); + } + + if (maxValue == null) + { + throw PSTraceSource.NewArgumentNullException("max"); + } + + ErrorRecord errorRecord = new( + new ArgumentException(string.Format( + CultureInfo.InvariantCulture, GetRandomCommandStrings.MinGreaterThanOrEqualMax, minValue, maxValue)), + "MinGreaterThanOrEqualMax", + ErrorCategory.InvalidArgument, + null); + + ThrowTerminatingError(errorRecord); + } + + #endregion + + #region Random generator state + + private static readonly ReaderWriterLockSlim s_runspaceGeneratorMapLock = new(); + + // 1-to-1 mapping of cmdlet + runspacesId and random number generators + private static readonly Dictionary s_runspaceGeneratorMap = new(); + + private static void CurrentRunspace_StateChanged(object sender, RunspaceStateEventArgs e) + { + switch (e.RunspaceStateInfo.State) + { + case RunspaceState.Broken: + case RunspaceState.Closed: + try + { + GetRandomCommandBase.s_runspaceGeneratorMapLock.EnterWriteLock(); + GetRandomCommandBase.s_runspaceGeneratorMap.Remove(MethodBase.GetCurrentMethod().DeclaringType.Name + ((Runspace)sender).InstanceId.ToString()); + } + finally + { + GetRandomCommandBase.s_runspaceGeneratorMapLock.ExitWriteLock(); + } + + break; + } + } + + private PolymorphicRandomNumberGenerator _generator; + + /// + /// Gets and sets generator associated with the current cmdlet and runspace. + /// + internal PolymorphicRandomNumberGenerator Generator + { + get + { + if (_generator == null) + { + string runspaceId = Context.CurrentRunspace.InstanceId.ToString(); + + bool needToInitialize = false; + try + { + GetRandomCommandBase.s_runspaceGeneratorMapLock.EnterReadLock(); + needToInitialize = !GetRandomCommandBase.s_runspaceGeneratorMap.TryGetValue(this.GetType().Name + runspaceId, out _generator); + } + finally + { + GetRandomCommandBase.s_runspaceGeneratorMapLock.ExitReadLock(); + } + + if (needToInitialize) + { + Generator = new PolymorphicRandomNumberGenerator(); + } + } + + return _generator; + } + + set + { + _generator = value; + Runspace myRunspace = Context.CurrentRunspace; + + try + { + GetRandomCommandBase.s_runspaceGeneratorMapLock.EnterWriteLock(); + if (!GetRandomCommandBase.s_runspaceGeneratorMap.ContainsKey(this.GetType().Name + myRunspace.InstanceId.ToString())) + { + // make sure we won't leave the generator around after runspace exits + myRunspace.StateChanged += CurrentRunspace_StateChanged; + } + + GetRandomCommandBase.s_runspaceGeneratorMap[this.GetType().Name + myRunspace.InstanceId.ToString()] = _generator; + } + finally + { + GetRandomCommandBase.s_runspaceGeneratorMapLock.ExitWriteLock(); + } + } + } + + #endregion + + #region Parameters for RandomNumberParameterSet + + /// + /// Gets or sets the maximum number to generate. + /// + [Parameter(ParameterSetName = RandomNumberParameterSet, Position = 0)] + public object Maximum { get; set; } + + /// + /// Gets or sets the minimum number to generate. + /// + [Parameter(ParameterSetName = RandomNumberParameterSet)] + public object Minimum { get; set; } + + private static bool IsInt(object o) + { + if (o == null || o is int) + { + return true; + } + + return false; + } + + private static bool IsInt64(object o) + { + if (o == null || o is long) + { + return true; + } + + return false; + } + + private static object ProcessOperand(object o) + { + if (o == null) + { + return null; + } + + PSObject pso = PSObject.AsPSObject(o); + object baseObject = pso.BaseObject; + + if (baseObject is string) + { + // The type argument passed in does not decide the number type we want to convert to. ScanNumber will return + // int/long/double based on the string form number passed in. + baseObject = System.Management.Automation.Language.Parser.ScanNumber((string)baseObject, typeof(int)); + } + + return baseObject; + } + + private static double ConvertToDouble(object o, double defaultIfNull) + { + if (o == null) + { + return defaultIfNull; + } + + double result = (double)LanguagePrimitives.ConvertTo(o, typeof(double), CultureInfo.InvariantCulture); + return result; + } + + #endregion + + #region Parameters and variables for RandomListItemParameterSet + + private List _chosenListItems; + private int _numberOfProcessedListItems; + + /// + /// Gets or sets the list from which random elements are chosen. + /// + [Parameter(ParameterSetName = RandomListItemParameterSet, ValueFromPipeline = true, Position = 0, Mandatory = true)] + [Parameter(ParameterSetName = ShuffleParameterSet, ValueFromPipeline = true, Position = 0, Mandatory = true)] + [System.Management.Automation.AllowNull] + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public object[] InputObject { get; set; } + + /// + /// Gets or sets the number of items to output (number of list items or of numbers). + /// + [Parameter(ParameterSetName = RandomNumberParameterSet)] + [Parameter(ParameterSetName = RandomListItemParameterSet)] + [ValidateRange(1, int.MaxValue)] + public int Count { get; set; } = 1; + + #endregion + + #region Shuffle parameter + + /// + /// Gets or sets whether the command should return all input objects in randomized order. + /// + [Parameter(ParameterSetName = ShuffleParameterSet, Mandatory = true)] + public SwitchParameter Shuffle { get; set; } + + #endregion + + #region Cmdlet processing methods + + private double GetRandomDouble(double minValue, double maxValue) + { + double randomNumber; + double diff = maxValue - minValue; + + // I couldn't find a better fix for bug #216893 then + // to test and retry if a random number falls outside the bounds + // because of floating-point-arithmetic inaccuracies. + // + // Performance in the normal case is not impacted much. + // In low-precision situations we should converge to a solution quickly + // (diff gets smaller at a quick pace). + if (double.IsInfinity(diff)) + { + do + { + double r = Generator.NextDouble(); + randomNumber = minValue + (r * maxValue) - (r * minValue); + } + while (randomNumber >= maxValue); + } + else + { + do + { + double r = Generator.NextDouble(); + randomNumber = minValue + (r * diff); + diff *= r; + } + while (randomNumber >= maxValue); + } + + return randomNumber; + } + + /// + /// Get a random Int64 type number. + /// + /// Minimum value. + /// Maximum value. + /// Rnadom long. + private long GetRandomInt64(long minValue, long maxValue) + { + // Randomly generate eight bytes and convert the byte array to UInt64 + var buffer = new byte[sizeof(ulong)]; + ulong randomUint64; + + BigInteger bigIntegerDiff = (BigInteger)maxValue - (BigInteger)minValue; + + // When the difference is less than int.MaxValue, use Random.Next(int, int) + if (bigIntegerDiff <= int.MaxValue) + { + int randomDiff = Generator.Next(0, (int)(maxValue - minValue)); + return minValue + randomDiff; + } + + // The difference of two Int64 numbers would not exceed UInt64.MaxValue, so it can be represented by a UInt64 number. + ulong uint64Diff = (ulong)bigIntegerDiff; + + // Calculate the number of bits to represent the diff in type UInt64 + int bitsToRepresentDiff = 0; + ulong diffCopy = uint64Diff; + for (; diffCopy != 0; bitsToRepresentDiff++) + { + diffCopy >>= 1; + } + + // Get the mask for the number of bits + ulong mask = 0xffffffffffffffff >> (64 - bitsToRepresentDiff); + do + { + // Randomly fill the buffer + Generator.NextBytes(buffer); + randomUint64 = BitConverter.ToUInt64(buffer, 0); + + // Get the last 'bitsToRepresentDiff' number of random bits + randomUint64 &= mask; + } while (uint64Diff <= randomUint64); + + double randomNumber = (minValue * 1.0) + (randomUint64 * 1.0); + return (long)randomNumber; + } + + /// + /// This method implements the BeginProcessing method for derived cmdlets. + /// + protected override void BeginProcessing() + { + if (EffectiveParameterSet == MyParameterSet.RandomNumber) + { + object maxOperand = ProcessOperand(Maximum); + object minOperand = ProcessOperand(Minimum); + + if (IsInt(maxOperand) && IsInt(minOperand)) + { + int minValue = minOperand != null ? (int)minOperand : 0; + int maxValue = maxOperand != null ? (int)maxOperand : int.MaxValue; + + if (minValue >= maxValue) + { + ThrowMinGreaterThanOrEqualMax(minValue, maxValue); + } + + for (int i = 0; i < Count; i++) + { + int randomNumber = Generator.Next(minValue, maxValue); + Debug.Assert(minValue <= randomNumber, "lower bound <= random number"); + Debug.Assert(randomNumber < maxValue, "random number < upper bound"); + + WriteObject(randomNumber); + } + } + else if ((IsInt64(maxOperand) || IsInt(maxOperand)) && (IsInt64(minOperand) || IsInt(minOperand))) + { + long minValue = minOperand != null ? ((minOperand is long) ? (long)minOperand : (int)minOperand) : 0; + long maxValue = maxOperand != null ? ((maxOperand is long) ? (long)maxOperand : (int)maxOperand) : long.MaxValue; + + if (minValue >= maxValue) + { + ThrowMinGreaterThanOrEqualMax(minValue, maxValue); + } + + for (int i = 0; i < Count; i++) + { + long randomNumber = GetRandomInt64(minValue, maxValue); + Debug.Assert(minValue <= randomNumber, "lower bound <= random number"); + Debug.Assert(randomNumber < maxValue, "random number < upper bound"); + + WriteObject(randomNumber); + } + } + else + { + double minValue = (minOperand is double) ? (double)minOperand : ConvertToDouble(Minimum, 0.0); + double maxValue = (maxOperand is double) ? (double)maxOperand : ConvertToDouble(Maximum, double.MaxValue); + + if (minValue >= maxValue) + { + ThrowMinGreaterThanOrEqualMax(minValue, maxValue); + } + + for (int i = 0; i < Count; i++) + { + double randomNumber = GetRandomDouble(minValue, maxValue); + Debug.Assert(minValue <= randomNumber, "lower bound <= random number"); + Debug.Assert(randomNumber < maxValue, "random number < upper bound"); + + WriteObject(randomNumber); + } + } + } + else if (EffectiveParameterSet == MyParameterSet.RandomListItem) + { + _chosenListItems = new List(); + _numberOfProcessedListItems = 0; + } + } + + // rough proof that when choosing random K items out of N items + // each item has got K/N probability of being included in the final list + // + // probability that a particular item in chosenListItems is NOT going to be replaced + // when processing I-th input item [assumes I > K]: + // P_one_step(I) = 1 - ((K / I) * ((K - 1) / K) + ((I - K) / I) = (I - 1) / I + // <--A--> <-----B-----> <-----C-----> + // A - probability that I-th element is going to be replacing an element from chosenListItems + // (see (1) in the code below) + // B - probability that a particular element from chosenListItems is NOT going to be replaced + // (see (2) in the code below) + // C - probability that I-th element is NOT going to be replacing an element from chosenListItems + // (see (1) in the code below) + // + // probability that a particular item in chosenListItems is NOT going to be replaced + // when processing input items J through N [assumes J > K] + // P_removal(J) = Multiply(for I = J to N) P(I) = + // = ((J - 1) / J) * (J / (J + 1)) * ... * ((N - 2) / (N - 1)) * ((N - 1) / N) = + // = (J - 1) / N + // + // probability that when processing an element it is going to be put into chosenListItems + // P_insertion(I) = 1.0 when I <= K - see (3) in the code below + // P_insertion(I) = K/N otherwise - see (1) in the code below + // + // probability that a given element is going to be a part of the final list + // P_final(I) = P_insertion(I) * P_removal(max(I + 1, K + 1)) + // [for I <= K] = 1.0 * ((K + 1) - 1) / N = K / N + // [otherwise] = (K / I) * ((I + 1) - 1) / N = K / N + // + // which proves that P_final(I) = K / N for all values of I. QED. + + /// + /// This method implements the ProcessRecord method for derived cmdlets. + /// + protected override void ProcessRecord() + { + if (EffectiveParameterSet == MyParameterSet.RandomListItem) + { + if (ParameterSetName == ShuffleParameterSet) + { + // this allows for $null to be in an array passed to InputObject + foreach (object item in InputObject ?? _nullInArray) + { + _chosenListItems.Add(item); + } + } + else + { + foreach (object item in InputObject ?? _nullInArray) + { + // (3) + if (_numberOfProcessedListItems < Count) + { + Debug.Assert(_chosenListItems.Count == _numberOfProcessedListItems, "Initial K elements should all be included in chosenListItems"); + _chosenListItems.Add(item); + } + else + { + Debug.Assert(_chosenListItems.Count == Count, "After processing K initial elements, the length of chosenItems should stay equal to K"); + + // (1) + if (Generator.Next(_numberOfProcessedListItems + 1) < Count) + { + // (2) + int indexToReplace = Generator.Next(_chosenListItems.Count); + _chosenListItems[indexToReplace] = item; + } + } + + _numberOfProcessedListItems++; + } + } + } + } + + /// + /// This method implements the EndProcessing method for derived cmdlets. + /// + protected override void EndProcessing() + { + if (EffectiveParameterSet == MyParameterSet.RandomListItem) + { + // make sure the order is truly random + // (all permutations with the same probability) + // O(n) time + int n = _chosenListItems.Count; + for (int i = 0; i < n; i++) + { + // randomly choose j from [i...n) + int j = Generator.Next(i, n); + + WriteObject(_chosenListItems[j]); + + // remove the output object from consideration in the next iteration. + if (i != j) + { + _chosenListItems[j] = _chosenListItems[i]; + } + } + } + } + + #endregion Processing methods + } + + /// + /// Provides an adapter API for random numbers that may be either cryptographically random, or + /// generated with the regular pseudo-random number generator. Re-implementations of + /// methods using the NextBytes() primitive based on the CLR implementation: + /// https://referencesource.microsoft.com/#mscorlib/system/random.cs. + /// + internal class PolymorphicRandomNumberGenerator + { + /// + /// Initializes a new instance of the class. + /// + public PolymorphicRandomNumberGenerator() + { + _cryptographicGenerator = RandomNumberGenerator.Create(); + _pseudoGenerator = null; + } + + /// + /// Initializes a new instance of the using pseudorandom generator instead of the cryptographic one. + /// + /// The seed value. + internal PolymorphicRandomNumberGenerator(int seed) + { + _cryptographicGenerator = null; + _pseudoGenerator = new Random(seed); + } + + private readonly Random _pseudoGenerator = null; + private readonly RandomNumberGenerator _cryptographicGenerator = null; + + /// + /// Generates a random floating-point number that is greater than or equal to 0.0, and less than 1.0. + /// + /// A random floating-point number that is greater than or equal to 0.0, and less than 1.0. + internal double NextDouble() + { + // According to the CLR source: + // "Including this division at the end gives us significantly improved random number distribution." + return Next() * (1.0 / int.MaxValue); + } + + /// + /// Generates a non-negative random integer. + /// + /// A non-negative random integer. + internal int Next() + { + int randomNumber; + + // The CLR implementation just fudges + // Int32.MaxValue down to (Int32.MaxValue - 1). This implementation + // errs on the side of correctness. + do + { + randomNumber = InternalSample(); + } + while (randomNumber == int.MaxValue); + + if (randomNumber < 0) + { + randomNumber += int.MaxValue; + } + + return randomNumber; + } + + /// + /// Returns a random integer that is within a specified range. + /// + /// The exclusive upper bound of the random number returned. + /// Next random integer. + internal int Next(int maxValue) + { + if (maxValue < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxValue), GetRandomCommandStrings.MaxMustBeGreaterThanZeroApi); + } + + return Next(0, maxValue); + } + + /// + /// Returns a random integer that is within a specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. maxValue must be greater than or equal to minValue. + /// Next random integer. + public int Next(int minValue, int maxValue) + { + if (minValue > maxValue) + { + throw new ArgumentOutOfRangeException(nameof(minValue), GetRandomCommandStrings.MinGreaterThanOrEqualMaxApi); + } + + int randomNumber = 0; + + long range = (long)maxValue - (long)minValue; + if (range <= int.MaxValue) + { + randomNumber = (int)(NextDouble() * range) + minValue; + } + else + { + double largeSample = InternalSampleLargeRange() * (1.0 / (2 * ((uint)int.MaxValue))); + randomNumber = (int)((long)(largeSample * range) + minValue); + } + + return randomNumber; + } + + /// + /// Fills the elements of a specified array of bytes with random numbers. + /// + /// The array to be filled. + internal void NextBytes(byte[] buffer) + { + if (_cryptographicGenerator != null) + { + _cryptographicGenerator.GetBytes(buffer); + } + else + { + _pseudoGenerator.NextBytes(buffer); + } + } + + /// + /// Samples a random integer. + /// + /// A random integer, using the full range of Int32. + private int InternalSample() + { + int randomNumber; + byte[] data = new byte[sizeof(int)]; + + NextBytes(data); + randomNumber = BitConverter.ToInt32(data, 0); + + return randomNumber; + } + + /// + /// Samples a random int when the range is large. This does + /// not need to be in the range of -Double.MaxValue .. Double.MaxValue, + /// just 0.. (2 * Int32.MaxValue) - 1 . + /// + /// A random double. + private double InternalSampleLargeRange() + { + double randomNumber; + + do + { + randomNumber = InternalSample(); + } + while (randomNumber == int.MaxValue); + + randomNumber += int.MaxValue; + return randomNumber; + } + } +} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetSecureRandomCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetSecureRandomCommand.cs new file mode 100644 index 0000000000..e0ea7e68db --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetSecureRandomCommand.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// This class implements `Get-SecureRandom` cmdlet. + /// + [Cmdlet(VerbsCommon.Get, "SecureRandom", DefaultParameterSetName = GetRandomCommandBase.RandomNumberParameterSet, + HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2235055", RemotingCapability = RemotingCapability.None)] + [OutputType(typeof(int), typeof(long), typeof(double))] + public sealed class GetSecureRandomCommand : GetRandomCommandBase + { + // nothing unique from base class + } +} diff --git a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index 5761b3c745..b17465abc5 100644 --- a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -19,7 +19,7 @@ CmdletsToExport = @( 'New-Object', 'Select-Object', 'Sort-Object', 'Tee-Object', 'Register-ObjectEvent', 'Write-Output', 'Import-PowerShellDataFile', 'Write-Progress', 'Disable-PSBreakpoint', 'Enable-PSBreakpoint', 'Get-PSBreakpoint', 'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession', - 'Import-PSSession', 'Get-Random', 'Invoke-RestMethod', 'Debug-Runspace', 'Get-Runspace', + 'Import-PSSession', 'Get-Random', 'Get-SecureRandom', 'Invoke-RestMethod', 'Debug-Runspace', 'Get-Runspace', 'Disable-RunspaceDebug', 'Enable-RunspaceDebug', 'Get-RunspaceDebug', 'Start-Sleep', 'Join-String', 'Out-String', 'Select-String', 'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryFile', 'New-TimeSpan', 'Get-TraceSource', 'Set-TraceSource', 'Add-Type', 'Get-TypeData', 'Remove-TypeData', 'Update-TypeData', diff --git a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index 7d0e7a91e1..396ed51a8f 100644 --- a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -17,7 +17,7 @@ CmdletsToExport = @( 'Show-Markdown', 'Get-MarkdownOption', 'Set-MarkdownOption', 'Add-Member', 'Get-Member', 'Compare-Object', 'Group-Object', 'Measure-Object', 'New-Object', 'Select-Object', 'Sort-Object', 'Tee-Object', 'Register-ObjectEvent', 'Write-Output', 'Import-PowerShellDataFile', 'Write-Progress', 'Disable-PSBreakpoint', 'Enable-PSBreakpoint', 'Get-PSBreakpoint', - 'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession', 'Import-PSSession', 'Get-Random', + 'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession', 'Import-PSSession', 'Get-Random', 'Get-SecureRandom' 'Invoke-RestMethod', 'Debug-Runspace', 'Get-Runspace', 'Disable-RunspaceDebug', 'Enable-RunspaceDebug', 'Get-RunspaceDebug', 'ConvertFrom-SddlString', 'Start-Sleep', 'Join-String', 'Out-String', 'Select-String', 'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryFile', 'New-TimeSpan', 'Get-TraceSource', 'Set-TraceSource', diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-SecureRandom.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-SecureRandom.Tests.ps1 new file mode 100644 index 0000000000..acc560abdb --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-SecureRandom.Tests.ps1 @@ -0,0 +1,183 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +Describe "Get-SecureRandom DRT Unit Tests" -Tags "CI" { + $testData = @( + @{ Name = 'no params'; Maximum = $null; Minimum = $null; GreaterThan = -1; LessThan = ([int32]::MaxValue); Type = 'System.Int32' } + @{ Name = 'only positive maximum number'; Maximum = 100; Minimum = $null; GreaterThan = -1; LessThan = 100; Type = 'System.Int32' } + @{ Name = 'maximum set to 0, Minimum to a negative number'; Maximum = 0; Minimum = -100; GreaterThan = -101; LessThan = 0; Type = 'System.Int32' } + @{ Name = 'positive maximum, negative Minimum'; Maximum = 100; Minimum = -100; GreaterThan = -101; LessThan = 100; Type = 'System.Int32' } + @{ Name = 'both negative'; Maximum = -100; Minimum = -200; GreaterThan = -201; LessThan = -100; Type = 'System.Int32' } + @{ Name = 'both negative with parentheses'; Maximum = (-100); Minimum = (-200); GreaterThan = -201; LessThan = -100; Type = 'System.Int32' } + @{ Name = 'maximum enclosed in quote'; Maximum = '8'; Minimum = 5; GreaterThan = 4; LessThan = 8; Type = 'System.Int32' } + @{ Name = 'minimum enclosed in quote'; Maximum = 8; Minimum = '5'; GreaterThan = 4; LessThan = 8; Type = 'System.Int32' } + @{ Name = 'maximum with plus sign'; Maximum = +100; Minimum = 0; GreaterThan = -1; LessThan = 100; Type = 'System.Int32' } + @{ Name = 'maximum with plus sign and quote'; Maximum = '+100'; Minimum = 0; GreaterThan = -1; LessThan = 100; Type = 'System.Int32' } + @{ Name = 'both with quote'; Maximum = '+100'; Minimum = '-100'; GreaterThan = -101; LessThan = 100; Type = 'System.Int32' } + @{ Name = 'maximum set to kb'; Maximum = '1kb'; Minimum = 0; GreaterThan = -1; LessThan = 1024; Type = 'System.Int32' } + @{ Name = 'maximum is Int64.MaxValue'; Maximum = ([int64]::MaxValue); Minimum = $null; GreaterThan = ([int64]-1); LessThan = ([int64]::MaxValue); Type = 'System.Int64' } + @{ Name = 'maximum is a 64-bit integer'; Maximum = ([int64]100); Minimum = $null; GreaterThan = ([int64]-1); LessThan = ([int64]100); Type = 'System.Int64' } + @{ Name = 'maximum set to a large integer greater than int32.MaxValue'; Maximum = 100000000000; Minimum = $null; GreaterThan = ([int64]-1); LessThan = ([int64]100000000000); Type = 'System.Int64' } + @{ Name = 'maximum set to 0, Minimum set to a negative 64-bit integer'; Maximum = ([int64]0); Minimum = ([int64]-100); GreaterThan = ([int64]-101); LessThan = ([int64]0); Type = 'System.Int64' } + @{ Name = 'maximum set to positive 64-bit number, min set to negative 64-bit number'; Maximum = ([int64]100); Minimum = ([int64]-100); GreaterThan = ([int64]-101); LessThan = ([int64]100); Type = 'System.Int64' } + @{ Name = 'both are negative 64-bit number'; Maximum = ([int64]-100); Minimum = ([int64]-200); GreaterThan = ([int64]-201); LessThan = ([int64]-100); Type = 'System.Int64' } + @{ Name = 'both are negative 64-bit number with parentheses'; Maximum = ([int64](-100)); Minimum = ([int64](-200)); GreaterThan = ([int64]-201); LessThan = ([int64]-100); Type = 'System.Int64' } + @{ Name = 'max is 32-bit, min is 64-bit integer'; Maximum = '8'; Minimum = ([int64]5); GreaterThan = ([int64]4); LessThan = ([int64]8); Type = 'System.Int64' } + @{ Name = 'max is 64-bit, min is 32-bit integer'; Maximum = ([int64]8); Minimum = '5'; GreaterThan = ([int64]4); LessThan = ([int64]8); Type = 'System.Int64' } + @{ Name = 'max set to a 32-bit integer, min set to [int64]0'; Maximum = +100; Minimum = ([int64]0); GreaterThan = ([int64]-1); LessThan = ([int64]100); Type = 'System.Int64' } + @{ Name = 'max set to a 32-bit integer with quote'; Maximum = '+100'; Minimum = ([int64]0); GreaterThan = ([int64]-1); LessThan = ([int64]100); Type = 'System.Int64' } + @{ Name = 'max is [int64]0, min is a 32-bit integer'; Maximum = ([int64]0); Minimum = '-100'; GreaterThan = ([int64]-101); LessThan = ([int64]0); Type = 'System.Int64' } + @{ Name = 'min set to 1MB, max set to a 64-bit integer greater than min'; Maximum = ([int64]1048585); Minimum = '1mb'; GreaterThan = ([int64]1048575); LessThan = ([int64]1048585); Type = 'System.Int64' } + @{ Name = 'max set to 1tb, min set to 10 mb'; Maximum = '1tb'; Minimum = '10mb'; GreaterThan = ([int64]10485759); LessThan = ([int64]1099511627776); Type = 'System.Int64' } + @{ Name = 'max is int64.MaxValue, min is Int64.MinValue'; Maximum = ([int64]::MaxValue); Minimum = ([int64]::MinValue); GreaterThan = ([int64]::MinValue); LessThan = ([int64]::MaxValue); Type = 'System.Int64' } + @{ Name = 'both are int64.MaxValue plus a 32-bit integer'; Maximum = ([int64](([int]::MaxValue)+15)); Minimum = ([int64](([int]::MaxValue)+10)); GreaterThan = ([int64](([int]::MaxValue)+9)); LessThan = ([int64](([int]::MaxValue)+15)); Type = 'System.Int64' } + @{ Name = 'both are greater than int32.MaxValue without specified type, and max with quote'; Maximum = '100099000001'; Minimum = 100000000001; GreaterThan = ([int64]10000000000); LessThan = ([int64]100099000001); Type = 'System.Int64' } + @{ Name = 'both are greater than int32.MaxValue without specified type, and min with quote'; Maximum = 100000002230; Minimum = '100000002222'; GreaterThan = ([int64]100000002221); LessThan = ([int64]100000002230); Type = 'System.Int64' } + @{ Name = 'max is greater than int32.MaxValue without specified type'; Maximum = 90000000000; Minimum = 4; GreaterThan = ([int64]3); LessThan = ([int64]90000000000); Type = 'System.Int64' } + @{ Name = 'max is a double-precision number'; Maximum = 100.0; Minimum = $null; GreaterThan = -1.0; LessThan = 100.0; Type = 'System.Double' } + @{ Name = 'both are double-precision numbers, min is negative.'; Maximum = 0.0; Minimum = -100.0; GreaterThan = -101.0; LessThan = 0.0; Type = 'System.Double' } + @{ Name = 'both are double-precision number, max is positive, min is negative.'; Maximum = 100.0; Minimum = -100.0; GreaterThan = -101.0; LessThan = 100.0; Type = 'System.Double' } + @{ Name = 'max is a double-precision number, min is int32'; Maximum = 8.0; Minimum = 5; GreaterThan = 4.0; LessThan = 8.0; Type = 'System.Double' } + @{ Name = 'min is a double-precision number, max is int32'; Maximum = 8; Minimum = 5.0; GreaterThan = 4.0; LessThan = 8.0; Type = 'System.Double' } + @{ Name = 'max set to a special double number'; Maximum = 20.; Minimum = 0.0; GreaterThan = -1.0; LessThan = 20.0; Type = 'System.Double' } + @{ Name = 'max is double with quote'; Maximum = '20.'; Minimum = 0.0; GreaterThan = -1.0; LessThan = 20.0; Type = 'System.Double' } + @{ Name = 'max is double with plus sign'; Maximum = +100.0; Minimum = 0; GreaterThan = -1.0; LessThan = 100.0; Type = 'System.Double' } + @{ Name = 'max is double with plus sign and enclosed in quote'; Maximum = '+100.0'; Minimum = 0; GreaterThan = -1.0; LessThan = 100.0; Type = 'System.Double' } + @{ Name = 'both set to the special numbers as 1.0e+xx '; Maximum = $null; Minimum = 1.0e+100; GreaterThan = 1.0e+99; LessThan = ([double]::MaxValue); Type = 'System.Double' } + @{ Name = 'max is Double.MaxValue, min is Double.MinValue'; Maximum = ([double]::MaxValue); Minimum = ([double]::MinValue); GreaterThan = ([double]::MinValue); LessThan = ([double]::MaxValue); Type = 'System.Double' } + ) + + $testDataForError = @( + @{ Name = 'Min is greater than max and all are positive 32-bit integer'; Maximum = 10; Minimum = 20} + @{ Name = 'Min and Max are same and all are positive 32-bit integer'; Maximum = 20; Minimum = 20} + @{ Name = 'Min is greater than max and all are negative 32-bit integer'; Maximum = -20; Minimum = -10} + @{ Name = 'Min and Max are same and all are negative 32-bit integer'; Maximum = -20; Minimum = -20} + @{ Name = 'Min is greater than max and all are positive double-precision number'; Maximum = 10.0; Minimum = 20.0} + @{ Name = 'Min and Max are same and all are positive double-precision number'; Maximum = 20.0; Minimum = 20.0} + @{ Name = 'Min is greater than max and all are negative double-precision number'; Maximum = -20.0; Minimum = -10.0} + @{ Name = 'Min and Max are same and all are negative double-precision number'; Maximum = -20.0; Minimum = -20.0} + @{ Name = 'Max is a negative number, min is the default number '; Maximum = -10; Minimum = $null} + ) + + # minimum is always set to the actual low end of the range, details refer to closed issue #887. + It "Should return a correct random number for ''" -TestCases $testData { + param($maximum, $minimum, $greaterThan, $lessThan, $type) + + $result = Get-SecureRandom -Maximum $maximum -Minimum $minimum + $result | Should -BeGreaterThan $greaterThan + $result | Should -BeLessThan $lessThan + $result | Should -BeOfType $type + } + + It "Should return correct random numbers for '' with Count specified" -TestCases $testData { + param($maximum, $minimum, $greaterThan, $lessThan, $type) + + $result = Get-SecureRandom -Maximum $maximum -Minimum $minimum -Count 1 + $result | Should -BeGreaterThan $greaterThan + $result | Should -BeLessThan $lessThan + $result | Should -BeOfType $type + + $result = Get-SecureRandom -Maximum $maximum -Minimum $minimum -Count 3 + foreach ($randomNumber in $result) { + $randomNumber | Should -BeGreaterThan $greaterThan + $randomNumber | Should -BeLessThan $lessThan + $randomNumber | Should -BeOfType $type + } + } + + It "Should be able to throw error when ''" -TestCases $testDataForError { + param($maximum, $minimum) + { Get-SecureRandom -Minimum $minimum -Maximum $maximum } | Should -Throw -ErrorId "MinGreaterThanOrEqualMax,Microsoft.PowerShell.Commands.GetSecureRandomCommand" + } +} + +Describe "Get-SecureRandom" -Tags "CI" { + It "Should return a random number greater than -1" { + Get-SecureRandom | Should -BeGreaterThan -1 + } + + It "Should return a random number less than 100" { + Get-SecureRandom -Maximum 100 | Should -BeLessThan 100 + Get-SecureRandom -Maximum 100 | Should -BeGreaterThan -1 + } + + It "Should return a random number less than 100 and greater than -100 " { + $randomNumber = Get-SecureRandom -Minimum -100 -Maximum 100 + $randomNumber | Should -BeLessThan 100 + $randomNumber | Should -BeGreaterThan -101 + } + + It "Should return a random number less than 20.93 and greater than 10.7 " { + $randomNumber = Get-SecureRandom -Minimum 10.7 -Maximum 20.93 + $randomNumber | Should -BeLessThan 20.93 + $randomNumber | Should -BeGreaterThan 10.7 + } + + It "Should return a number from 1,2,3,5,8,13 " { + $randomNumber = Get-SecureRandom -InputObject 1, 2, 3, 5, 8, 13 + $randomNumber | Should -BeIn 1, 2, 3, 5, 8, 13 + } + + It "Should return an array " { + $randomNumber = Get-SecureRandom -InputObject 1, 2, 3, 5, 8, 13 -Count 3 + $randomNumber.Count | Should -Be 3 + ,$randomNumber | Should -BeOfType System.Array + } + + It "Should return three random numbers for array of 1,2,3,5,8,13 " { + $randomNumber = Get-SecureRandom -InputObject 1, 2, 3, 5, 8, 13 -Count 3 + $randomNumber.Count | Should -Be 3 + $randomNumber[0] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[1] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[2] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[3] | Should -BeNullOrEmpty + } + + It "Should return all the numbers for array of 1,2,3,5,8,13 in no particular order" { + $randomNumber = Get-SecureRandom -InputObject 1, 2, 3, 5, 8, 13 -Count ([int]::MaxValue) + $randomNumber.Count | Should -Be 6 + $randomNumber[0] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[1] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[2] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[3] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[4] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[5] | Should -BeIn 1, 2, 3, 5, 8, 13 + $randomNumber[6] | Should -BeNullOrEmpty + } + + It "Should return all the numbers for array of 1,2,3,5,8,13 in randomized order when the Shuffle switch is used" { + $randomNumber = Get-SecureRandom -InputObject 1, 2, 3, 5, 8, 13 -Shuffle + $randomNumber.Count | Should -Be 6 + $randomNumber | Should -BeIn 1, 2, 3, 5, 8, 13 + } + + It "Should return for a string collection " { + $randomNumber = Get-SecureRandom -InputObject "red", "yellow", "blue" + $randomNumber | Should -Be ("red" -or "yellow" -or "blue") + } + + It "Should return a number for hexadecimal " { + $randomNumber = Get-SecureRandom 0x07FFFFFFFFF + $randomNumber | Should -BeLessThan 549755813887 + $randomNumber | Should -BeGreaterThan 0 + } + + It "Should throw an error because the hexadecimal number is to large " { + { Get-SecureRandom 0x07FFFFFFFFFFFFFFFF } | Should -Throw "Value was either too large or too small for a UInt32" + } + + It "Should accept collection containing empty string for -InputObject" { + 1..10 | ForEach-Object { + Get-SecureRandom -InputObject @('a','b','') | Should -BeIn 'a','b','' + } + } + + It "Should accept `$null in collection for -InputObject" { + 1..10 | ForEach-Object { + Get-SecureRandom -InputObject @('a','b',$null) | Should -BeIn 'a','b',$null + } + } + + It 'Should not have a -SetSeed parameter' { + (Get-Command Get-SecureRandom).Parameters['SetSeed'] | Should -BeNullOrEmpty + } +} diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index 27be013ec6..193b16cf2c 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -333,6 +333,7 @@ Describe "Verify aliases and cmdlets" -Tags "CI" { "Cmdlet", "Get-Random", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "Get-Runspace", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "Get-RunspaceDebug", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" +"Cmdlet", "Get-SecureRandom", "", $( $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "Get-Service", "", $($FullCLR -or $CoreWindows ), "", "", "None" "Cmdlet", "Get-TimeZone", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "Get-TraceSource", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"