nanoid-ps.psm1

using namespace System.Numerics

$ErrorActionPreference = 'Stop'

[string] $DefaultAlphabet = '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
[CryptoRandom] $Random = [CryptoRandom]::new()

function New-NanoId {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Random]$Random = $script:Random, 
        [ValidateNotNullOrEmpty()]
        [string] $Alphabet = $script:DefaultAlphabet, 
        [ValidateRange(1, 255)]
        [int] $Size = 21) 

    if ($Alphabet.Length -le 0 -or $Alphabet.Length -ge 256) {
        throw [ArgumentOutOfRangeException]("Alphabet must contain between 1 and 255 symbols. Received $($Alphabet.Length) chars.");
    }

    # See https://github.com/ai/nanoid/blob/master/format.js for explanation why masking
    # is used (`random % alphabet` is a common mistake security-wise).
    $Mask = (2 -shl 31 - [BitOperations]::LeadingZeroCount(($Alphabet.Length - 1) -bor 1)) - 1
    $Step = [int][Math]::Ceiling(1.6 * $Mask * $Size / $Alphabet.Length)
    Write-Verbose "Step: $Step" #-Verbose
    $IdBuilder = [char[]]::new($Size)
    $Buffer = [byte[]]::new($Step)
    $cnt = 0;
    
    for ($cnt; $cnt -lt $size ; $cnt++ ) {
        $Random.NextBytes($Buffer);

        for ($i = 0; $i -lt $Step; $i++) {
            $AlphabetIndex = $Buffer[$i] -band $Mask;

            if ($AlphabetIndex -ge $Alphabet.Length) { 
                continue
            }

            $IdBuilder[$cnt] = $Alphabet[$AlphabetIndex]
        }
    }

    $RetVal = [string]::new($IdBuilder)
    Write-Verbose "NanoId: $RetVal" #-Verbose
    return $RetVal
}