veloci 0.3.0

Veloci Redactor: redact secrets and PII from text and structured files, with stable numbered redactions
Documentation
# Install the latest veloci.exe from GitHub releases for this Windows machine.
#
#   irm https://raw.githubusercontent.com/phayes/velociredactor/master/scripts/install.ps1 | iex
#
# Environment:
#   PREFIX    install directory (default: %LOCALAPPDATA%\Programs\veloci)
#   VERSION   release tag such as v0.1.1 (default: latest)

param(
    [string]$Prefix = $env:PREFIX,
    [string]$Version = $env:VERSION,
    [switch]$AddToPath,
    [switch]$Help
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$Repo = 'phayes/velociredactor'
$Releases = "https://github.com/$Repo/releases"

function Show-Usage {
    @"
Install veloci.exe from GitHub releases.

Usage: install.ps1 [-Prefix DIR] [-Version TAG] [-AddToPath]

  -Prefix DIR    directory to place veloci.exe
                 (default: `$PREFIX, or %LOCALAPPDATA%\Programs\veloci)
  -Version TAG   release tag to install, such as v0.1.1 (default: latest)
  -AddToPath     append the prefix to the user PATH
  -Help          show this help
"@
}

if ($Help) {
    Show-Usage
    return
}

function Get-WindowsTarget {
    $arch = $null
    try {
        $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
    } catch {
        $arch = $env:PROCESSOR_ARCHITECTURE
    }
    switch -Regex ($arch) {
        '^(X64|Amd64|AMD64)$' { return 'x86_64-pc-windows-msvc' }
        '^(Arm64|ARM64)$' { return 'aarch64-pc-windows-msvc' }
        default {
            throw "install.ps1: no release for Windows $arch. Install from source: cargo install veloci-cli"
        }
    }
}

function Get-LatestTag {
    # /releases/latest redirects to /releases/tag/<tag>; that avoids the API
    # rate limit and does not need ConvertFrom-Json.
    $request = [System.Net.WebRequest]::Create("$Releases/latest")
    $request.Method = 'HEAD'
    $request.AllowAutoRedirect = $false
    $response = $null
    try {
        try {
            $response = $request.GetResponse()
        } catch [System.Net.WebException] {
            $response = $_.Exception.Response
            if ($null -eq $response) {
                throw
            }
        }
        $location = $response.Headers['Location']
        if ([string]::IsNullOrWhiteSpace($location)) {
            throw "install.ps1: could not resolve the latest release tag"
        }
        return ($location.TrimEnd('/') -split '/')[-1]
    } finally {
        if ($null -ne $response) {
            $response.Close()
            $response.Dispose()
        }
    }
}

function Test-OnPath([string]$Directory) {
    $root = [System.IO.Path]::GetFullPath($Directory)
    foreach ($entry in ($env:Path -split ';')) {
        if ([string]::IsNullOrWhiteSpace($entry)) {
            continue
        }
        try {
            if ([System.IO.Path]::GetFullPath($entry) -eq $root) {
                return $true
            }
        } catch {
            continue
        }
    }
    return $false
}

function Add-UserPath([string]$Directory) {
    $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
    if ($null -eq $userPath) {
        $userPath = ''
    }
    $parts = @($userPath -split ';' | Where-Object { $_ -ne '' })
    if ($parts -contains $Directory) {
        return
    }
    $parts += $Directory
    [Environment]::SetEnvironmentVariable('Path', ($parts -join ';'), 'User')
    $env:Path = "$Directory;$env:Path"
}

$target = Get-WindowsTarget
$exe = 'veloci.exe'
if ([string]::IsNullOrWhiteSpace($Prefix)) {
    $Prefix = Join-Path $env:LOCALAPPDATA 'Programs\veloci'
}
if ([string]::IsNullOrWhiteSpace($Version)) {
    $Version = Get-LatestTag
}
if ([string]::IsNullOrWhiteSpace($Version) -or $Version -eq 'latest') {
    throw 'install.ps1: could not resolve the latest release tag'
}

$asset = "velociredactor-$Version-$target.zip"
$base = "$Releases/download/$Version"
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("veloci-install-" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $tmp | Out-Null
try {
    $zip = Join-Path $tmp $asset
    $sum = "$zip.sha256"
    Write-Host "Downloading $asset"
    Invoke-WebRequest -Uri "$base/$asset" -OutFile $zip -UseBasicParsing
    Invoke-WebRequest -Uri "$base/$asset.sha256" -OutFile $sum -UseBasicParsing

    $expected = ((Get-Content -Path $sum -Raw).Trim() -split '\s+')[0]
    $actual = (Get-FileHash -Path $zip -Algorithm SHA256).Hash
    if ($expected -ne $actual) {
        throw "install.ps1: sha256 mismatch: expected $expected, got $actual"
    }

    $extract = Join-Path $tmp 'extract'
    Expand-Archive -Path $zip -DestinationPath $extract -Force

    # CI packs README, LICENSE, and the binary in velociredactor-<tag>-<target>/.
    # Older releases named the binary velociredactor.exe; install either as veloci.exe.
    $staged = Join-Path $extract "velociredactor-$Version-$target"
    $bin = Join-Path $staged $exe
    if (-not (Test-Path -LiteralPath $bin -PathType Leaf)) {
        $bin = Join-Path $staged 'velociredactor.exe'
    }
    if (-not (Test-Path -LiteralPath $bin -PathType Leaf)) {
        throw "install.ps1: archive did not contain $exe at the expected path"
    }

    New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
    $dest = Join-Path $Prefix $exe
    Copy-Item -LiteralPath $bin -Destination $dest -Force
    Write-Host "Installed $dest ($Version, $target)"
    & $dest --version
    if ($AddToPath) {
        Add-UserPath $Prefix
        Write-Host "Added $Prefix to the user PATH"
    } elseif (-not (Test-OnPath $Prefix)) {
        Write-Host "Add $Prefix to your PATH, or re-run with -AddToPath"
    }
} finally {
    Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
}