Utils/basenc.ps1

Register-GnuUtil @{
    Name         = 'basenc'
    Category     = 'Viewing file contents'
    Synopsis     = 'Encode/decode data and print to standard output.'
    Usage        = @('basenc [OPTION]... [FILE]')
    Description  = @('basenc encode or decode FILE, or standard input, to standard output.', '', 'With no FILE, or when FILE is -, read standard input.')
    FileOperands = $true
    Options      = @(
        @{ Long = 'base64'; Key = 'encoding'; Value = 'base64'; Help = 'same as ''base64'' program (RFC4648 section 4)' }
        @{ Long = 'base64url'; Key = 'encoding'; Value = 'base64url'; Help = 'file- and url-safe base64 (RFC4648 section 5)' }
        @{ Long = 'base32'; Key = 'encoding'; Value = 'base32'; Help = 'same as ''base32'' program (RFC4648 section 6)' }
        @{ Long = 'base32hex'; Key = 'encoding'; Value = 'base32hex'; Help = 'extended hex alphabet base32 (RFC4648 section 7)' }
        @{ Long = 'base16'; Key = 'encoding'; Value = 'base16'; Help = 'hex encoding (RFC4648 section 8)' }
        @{ Long = 'base2msbf'; Key = 'encoding'; Value = 'base2msbf'; Help = 'bit string with most significant bit (msb) first' }
        @{ Long = 'base2lsbf'; Key = 'encoding'; Value = 'base2lsbf'; Help = 'bit string with least significant bit (lsb) first' }
        @{ Short = 'd'; Long = 'decode'; Help = 'decode data' }
        @{ Short = 'i'; Long = 'ignore-garbage'; Help = 'when decoding, ignore non-alphabet characters' }
        @{ Short = 'w'; Long = 'wrap'; Arg = 'COLS'; Help = 'wrap encoded lines after COLS character (default 76). Use 0 to disable line wrapping' }
        @{ Long = 'z85'; Key = 'encoding'; Value = 'z85'; Help = 'ascii85-like encoding (ZeroMQ spec:32/Z85); when encoding, input length must be a multiple of 4; when decoding, input length must be a multiple of 5' }
    )
    Notes        = @(
        'When decoding, the input may contain newlines in addition to the bytes of the',
        'formal alphabet. Use --ignore-garbage to attempt to recover from any other',
        'non-alphabet bytes in the encoded stream.',
        '',
        'PowerShell note: decoded output is written as UTF-8 text lines.'
    )
}

$script:GnuBasencAlphabets = @{
    base64    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
    base64url = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_='
    base32    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567='
    base32hex = '0123456789ABCDEFGHIJKLMNOPQRSTUV='
    base16    = '0123456789ABCDEFabcdef'
    base2msbf = '01'
    base2lsbf = '01'
    z85       = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#'
}



function script:Invoke-GnuBasencCoding {
    <#
    .SYNOPSIS
    Implementation of basenc.
    #>

    param([Parameter(Mandatory)][hashtable]$Context)

    $encoding = [string](Get-GnuOptionValue -Context $Context -Key 'encoding')
    if ([string]::IsNullOrEmpty($encoding)) { Write-GnuUsageError -Context $Context -Message 'missing encoding type'; return }
    $operands = @($Context.Operands)
    if ($operands.Count -gt 1) { Write-GnuUsageError -Context $Context -Message "extra operand '$($operands[1])'"; return }
    $operand = '-'
    if ($operands.Count -eq 1) { $operand = $operands[0] }

    $wrap = 76
    if (Test-GnuOption -Context $Context -Key 'wrap') {
        $wrapText = [string](Get-GnuOptionValue -Context $Context -Key 'wrap')
        if ($wrapText -notmatch '^\d+$') { Write-GnuError -Context $Context -Message "invalid wrap size: '$wrapText'"; return }
        $wrap = [int]$wrapText
    }

    if (Test-GnuOption -Context $Context -Key 'decode') {
        $text = Get-GnuInputText -Context $Context -Operand $operand
        if ($null -eq $text) { return }
        $alphabet = $script:GnuBasencAlphabets[$encoding]
        $ignoreGarbage = Test-GnuOption -Context $Context -Key 'ignore-garbage'
        $clean = New-Object System.Text.StringBuilder
        foreach ($char in $text.ToCharArray()) {
            if ($char -eq "`n" -or $char -eq "`r") { continue }
            if ($ignoreGarbage -and $alphabet.IndexOf($char) -lt 0) { continue }
            [void]$clean.Append($char)
        }
        $bytes = ConvertFrom-GnuBasencText -Text $clean.ToString() -Encoding $encoding
        if ($null -eq $bytes) { Write-GnuError -Context $Context -Message 'invalid input'; return }
        ConvertTo-GnuOutputLine -Text $script:GnuUtf8.GetString($bytes)
        return
    }

    $data = Get-GnuInputBytes -Context $Context -Operand $operand
    if ($null -eq $data) { return }
    if ($encoding -eq 'z85' -and $data.Length % 4 -ne 0) {
        Write-GnuError -Context $Context -Message 'invalid input (length must be multiple of 4 characters)'
        return
    }
    $encoded = ConvertTo-GnuBasencText -Bytes $data -Encoding $encoding
    if ([string]::IsNullOrEmpty($encoded)) { return }
    if ($wrap -eq 0) { return $encoded }
    for ($i = 0; $i -lt $encoded.Length; $i += $wrap) {
        $encoded.Substring($i, [Math]::Min($wrap, $encoded.Length - $i))
    }
}

function script:ConvertTo-GnuBasencText {
    <#
    .SYNOPSIS
    Encodes bytes with one of the basenc encodings.
    #>

    param(
        [Parameter(Mandatory)][AllowEmptyCollection()][byte[]]$Bytes,
        [Parameter(Mandatory)][string]$Encoding
    )

    switch ($Encoding) {
        'base64' { return [System.Convert]::ToBase64String($Bytes) }
        'base64url' { return [System.Convert]::ToBase64String($Bytes).Replace('+', '-').Replace('/', '_') }
        'base32' { return ConvertTo-GnuBase32 -Bytes $Bytes }
        'base32hex' { return ConvertTo-GnuBase32 -Bytes $Bytes -Alphabet $script:GnuBasencAlphabets.base32hex.TrimEnd('=') }
        'base16' { return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join '') }
        'z85' {
            $alphabet = $script:GnuBasencAlphabets.z85
            $builder = New-Object System.Text.StringBuilder
            for ($offset = 0; $offset -lt $Bytes.Length; $offset += 4) {
                $value = ([long]$Bytes[$offset] -shl 24) -bor ([long]$Bytes[$offset + 1] -shl 16) -bor ([long]$Bytes[$offset + 2] -shl 8) -bor [long]$Bytes[$offset + 3]
                $chars = New-Object char[] 5
                for ($k = 4; $k -ge 0; $k--) { $chars[$k] = $alphabet[[int]($value % 85)]; $value = [Math]::Floor($value / 85) }
                [void]$builder.Append($chars)
            }
            return $builder.ToString()
        }
    }
    # base2msbf / base2lsbf
    $builder = New-Object System.Text.StringBuilder
    foreach ($byte in $Bytes) {
        $bits = [Convert]::ToString([int]$byte, 2).PadLeft(8, '0')
        if ($Encoding -eq 'base2lsbf') { $bits = -join ($bits.ToCharArray()[7..0]) }
        [void]$builder.Append($bits)
    }
    $builder.ToString()
}

function script:ConvertFrom-GnuBasencText {
    <#
    .SYNOPSIS
    Decodes basenc text (line breaks removed). Returns one byte[] (not unrolled), or nothing if invalid.
    #>

    param(
        [Parameter(Mandatory)][AllowEmptyString()][string]$Text,
        [Parameter(Mandatory)][string]$Encoding
    )

    switch ($Encoding) {
        { $_ -eq 'base64' -or $_ -eq 'base64url' } {
            $standard = $Text
            if ($Encoding -eq 'base64url') {
                if ($Text.IndexOfAny([char[]]'+/') -ge 0) { return }
                $standard = $Text.Replace('-', '+').Replace('_', '/')
            }
            elseif ($Text.IndexOfAny([char[]]'-_') -ge 0) { return }
            try { return , [System.Convert]::FromBase64String($standard) } catch { return }
        }
        'base32' { return ConvertFrom-GnuBase32 -Text $Text }
        'base32hex' { return ConvertFrom-GnuBase32 -Text $Text -Alphabet $script:GnuBasencAlphabets.base32hex.TrimEnd('=') }
        'base16' {
            if ($Text.Length % 2 -ne 0 -or $Text -notmatch '^[0-9A-Fa-f]*$') { return }
            $bytes = New-Object byte[] ($Text.Length / 2)
            for ($i = 0; $i -lt $bytes.Length; $i++) { $bytes[$i] = [Convert]::ToByte($Text.Substring(2 * $i, 2), 16) }
            return , $bytes
        }
        'z85' {
            if ($Text.Length % 5 -ne 0) { return }
            $alphabet = $script:GnuBasencAlphabets.z85
            $output = New-Object 'System.Collections.Generic.List[byte]'
            for ($offset = 0; $offset -lt $Text.Length; $offset += 5) {
                $value = [long]0
                for ($k = 0; $k -lt 5; $k++) {
                    $digit = $alphabet.IndexOf($Text[$offset + $k])
                    if ($digit -lt 0) { return }
                    $value = $value * 85 + $digit
                }
                if ($value -gt 0xFFFFFFFFL) { return }
                foreach ($shift in @(24, 16, 8, 0)) { $output.Add([byte](($value -shr $shift) -band 255)) }
            }
            return , $output.ToArray()
        }
    }
    # base2msbf / base2lsbf
    if ($Text.Length % 8 -ne 0 -or $Text -notmatch '^[01]*$') { return }
    $bytes = New-Object byte[] ($Text.Length / 8)
    for ($i = 0; $i -lt $bytes.Length; $i++) {
        $bits = $Text.Substring(8 * $i, 8)
        if ($Encoding -eq 'base2lsbf') { $bits = -join ($bits.ToCharArray()[7..0]) }
        $bytes[$i] = [Convert]::ToByte($bits, 2)
    }
    , $bytes
}