# GENERATED FILE - DO NOT EDIT. # Installs the pinned stable ReRune CLI for the current Windows hardware. #Requires -Version 5.1 & { param([pscustomobject]$InvocationContext) $FileInvocation = [bool]$InvocationContext.FileInvocation $InstallerArguments = @() if ($FileInvocation) { $InstallerArguments = @($InvocationContext.Arguments) } $State = [pscustomobject]@{ ExitCode = 0 FailureMessage = $null LockHandle = $null LockPath = $null LockText = $null PrivateTemp = $null StagedPath = $null BackupPath = $null DisplacedPath = $null BackupExpectedHash = $null ReplacementExpectedHash = $null DestinationPath = $null CurrentVersion = '1.0.13' MaximumFileBytes = 100MB UseOutputColor = $false UseErrorColor = $false } $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 function Test-ColorAllowed { param([bool]$Redirected) if (-not [Environment]::UserInteractive -or $Redirected) { return $false } return $null -eq [Environment]::GetEnvironmentVariable('NO_COLOR') } try { $State.UseOutputColor = Test-ColorAllowed ([Console]::IsOutputRedirected) } catch { $State.UseOutputColor = $false } try { $State.UseErrorColor = Test-ColorAllowed ([Console]::IsErrorRedirected) } catch { $State.UseErrorColor = $false } function Write-ConsoleLine { param( [Parameter(Mandatory = $true)][string]$Text, [Parameter(Mandatory = $true)][bool]$ErrorStream, [Parameter(Mandatory = $true)][ConsoleColor]$Color, [Parameter(Mandatory = $true)][bool]$UseColor ) $writer = if ($ErrorStream) { [Console]::Error } else { [Console]::Out } if (-not $UseColor) { $writer.WriteLine($Text) return } $oldColor = $null try { $oldColor = [Console]::ForegroundColor [Console]::ForegroundColor = $Color $writer.WriteLine($Text) } catch { $writer.WriteLine($Text) } finally { if ($null -ne $oldColor) { try { [Console]::ForegroundColor = $oldColor } catch { } } } } function Write-Normal { param([string]$Text) [Console]::Out.WriteLine($Text) } function Write-InfoLine { param([string]$Text) Write-ConsoleLine $Text $false ([ConsoleColor]::Cyan) $State.UseOutputColor } function Write-SuccessLine { param([string]$Text) Write-ConsoleLine "OK $Text" $false ([ConsoleColor]::Green) $State.UseOutputColor } function Write-WarningLine { param([string]$Text) Write-ConsoleLine "! $Text" $true ([ConsoleColor]::Yellow) $State.UseErrorColor } function Stop-Install { param([string]$Message) throw [InvalidOperationException]::new($Message) } function Show-Help { Write-Normal 'Install the current stable ReRune CLI for this Windows user.' Write-Normal '' Write-Normal 'Usage:' Write-Normal ' irm https://rerune.io/install.ps1 | iex' Write-Normal ' & .\install.ps1' Write-Normal '' Write-Normal 'Options:' Write-Normal ' -Help, --help Show this help.' } function Test-ContainsControlCharacter { param([string]$Text) if ($null -eq $Text) { return $false } foreach ($character in $Text.ToCharArray()) { if ([char]::IsControl($character)) { return $true } } return $false } function Resolve-SafeAbsolutePath { param([string]$Path, [string]$Label) if ([string]::IsNullOrWhiteSpace($Path)) { Stop-Install "$Label is empty." } if (Test-ContainsControlCharacter $Path) { Stop-Install "$Label contains a control character." } if ($Path -notmatch '^[A-Za-z]:[\\/]') { Stop-Install "$Label must be an absolute local Windows path." } try { $fullPath = [IO.Path]::GetFullPath($Path) } catch { Stop-Install "$Label is not a valid absolute Windows path." } if (Test-ContainsControlCharacter $fullPath) { Stop-Install "$Label contains a control character." } $root = [IO.Path]::GetPathRoot($fullPath) if ([string]::IsNullOrEmpty($root)) { Stop-Install "$Label must have a drive root." } while ($fullPath.Length -gt $root.Length -and ($fullPath.EndsWith('\') -or $fullPath.EndsWith('/'))) { $fullPath = $fullPath.Substring(0, $fullPath.Length - 1) } return $fullPath } function Get-ExistingPathItem { param([string]$Path) try { return Get-Item -LiteralPath $Path -Force -ErrorAction Stop } catch [Management.Automation.ItemNotFoundException] { return $null } catch { Stop-Install "Could not inspect path: $Path" } } function Assert-NoReparsePath { param([string]$Path, [string]$Label) $fullPath = Resolve-SafeAbsolutePath $Path $Label $root = [IO.Path]::GetPathRoot($fullPath) $current = $root $rootItem = Get-ExistingPathItem $current if ($null -eq $rootItem) { Stop-Install "$Label has a missing drive root." } if (($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Stop-Install "$Label crosses a Windows reparse point." } $relative = $fullPath.Substring($root.Length) if ([string]::IsNullOrEmpty($relative)) { return $fullPath } foreach ($part in $relative.Split([char[]]@('\', '/'), [StringSplitOptions]::RemoveEmptyEntries)) { $current = Join-Path $current $part $item = Get-ExistingPathItem $current if ($null -ne $item -and ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Stop-Install "$Label crosses a Windows reparse point: $current" } } return $fullPath } function Assert-SafeDirectory { param([string]$Path, [string]$Label) $null = Assert-NoReparsePath $Path $Label $item = Get-ExistingPathItem $Path if ($null -eq $item -or -not $item.PSIsContainer) { Stop-Install "$Label is not a directory." } } function Assert-SafeRegularFile { param([string]$Path, [string]$Label) $null = Assert-NoReparsePath $Path $Label $item = Get-ExistingPathItem $Path if ($null -eq $item -or $item.PSIsContainer) { Stop-Install "$Label is not a regular file." } if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Stop-Install "$Label is a reparse point." } return $item } function New-SafeDirectory { param([string]$Path, [string]$Label) $null = Assert-NoReparsePath $Path $Label try { $null = [IO.Directory]::CreateDirectory($Path) } catch { Stop-Install "Could not create $Label without administrator privileges." } Assert-SafeDirectory $Path $Label } function Open-FileDigestGuard { param([string]$Path, [string]$Label) $null = Assert-SafeRegularFile $Path $Label $stream = $null $algorithm = $null try { # FileShare.Read lets Windows load a reviewed executable while preventing writes, # deletion, or replacement until the caller disposes the returned guard. $stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) $item = Assert-SafeRegularFile $Path $Label $algorithm = [Security.Cryptography.SHA256]::Create() $hashBytes = $algorithm.ComputeHash($stream) $hash = ([BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant() $stream.Position = 0 $result = [pscustomobject]@{ Hash = $hash; Length = $stream.Length; LastWriteUtc = $item.LastWriteTimeUtc; Stream = $stream } $stream = $null return $result } catch { Stop-Install "Could not securely read $Label." } finally { if ($null -ne $algorithm) { $algorithm.Dispose() } if ($null -ne $stream) { $stream.Dispose() } } } function Get-FileDigest { param([string]$Path, [string]$Label) $guard = Open-FileDigestGuard $Path $Label try { return [pscustomobject]@{ Hash = $guard.Hash; Length = $guard.Length; LastWriteUtc = $guard.LastWriteUtc } } finally { $guard.Stream.Dispose() } } function ConvertTo-StrictSemVer { param([string]$Version, [string]$Label) if ($null -eq $Version -or $Version -notmatch '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$') { Stop-Install "$Label is not a strict stable semantic version." } return [pscustomobject]@{ Text = $Version Major = $Matches[1] Minor = $Matches[2] Patch = $Matches[3] } } function Compare-DecimalText { param([string]$Left, [string]$Right) if ($Left.Length -lt $Right.Length) { return -1 } if ($Left.Length -gt $Right.Length) { return 1 } return [Math]::Sign([string]::CompareOrdinal($Left, $Right)) } function Compare-StrictSemVer { param([pscustomobject]$Left, [pscustomobject]$Right) foreach ($property in @('Major', 'Minor', 'Patch')) { $comparison = Compare-DecimalText ([string]$Left.$property) ([string]$Right.$property) if ($comparison -ne 0) { return $comparison } } return 0 } function Get-CurrentArtifact { param([string]$Target) $releaseUrl = $null $archiveSha256 = $null $binarySha256 = $null switch -CaseSensitive ($Target) { 'windows-amd64' { $releaseUrl = 'https://raw.githubusercontent.com/BasalBit/rerune-releases/main/v1.0.13/rerune_1.0.13_windows_amd64.tar.gz' $archiveSha256 = '8196796394e1b87fbd64101f211a2cf3e5962745a1ba60deb038b44c9bff05ca' $binarySha256 = '26459173cbf7f2ffa0872f489e9ae223da3b1ae651045c3ecb3ed913faf89d0e' } 'windows-arm64' { $releaseUrl = 'https://raw.githubusercontent.com/BasalBit/rerune-releases/main/v1.0.13/rerune_1.0.13_windows_arm64.tar.gz' $archiveSha256 = 'fac839c5151a36ef1bdc6396fc10d15e402f30400f3f437ce2922ad5ef72eea4' $binarySha256 = '295cf987c58f5802ae6d5a2f32c248e05299c4c84910cbce616be6cb8e8addf7' } default { return $null } } if ([string]::IsNullOrWhiteSpace($releaseUrl)) { Stop-Install "No current ReRune artifact exists for $Target." } if ($archiveSha256 -notmatch '^[a-fA-F0-9]{64}$' -or $binarySha256 -notmatch '^[a-fA-F0-9]{64}$') { Stop-Install "The generated hashes for $Target are invalid." } try { $uri = [Uri]$releaseUrl } catch { Stop-Install "The generated release URL for $Target is invalid." } if (-not $uri.IsAbsoluteUri -or $uri.Scheme -cne 'https' -or [string]::IsNullOrEmpty($uri.Host) -or -not [string]::IsNullOrEmpty($uri.UserInfo)) { Stop-Install "The generated release URL for $Target must be an absolute HTTPS URL without user information." } return [pscustomobject]@{ Target = $Target Uri = $uri ArchiveSha256 = $archiveSha256.ToLowerInvariant() BinarySha256 = $binarySha256.ToLowerInvariant() } } function Get-KnownBinaryVersion { param([string]$Target, [string]$Hash) if ($Target -notmatch '^windows-(?:amd64|arm64)$' -or $Hash -notmatch '^[a-fA-F0-9]{64}$') { return $null } switch -CaseSensitive ($Hash.ToLowerInvariant()) { 'f5a0ae471676dde986a343cda759447be15e1e7d3a5300f69d823911a0cc8a1b' { return [pscustomobject]@{ Version = '1.0.0'; Target = 'windows-amd64' } } '894255c96b8493808bfd0d49c8cdc7e7104f8bd748ff4cbefc6225c266d9d51e' { return [pscustomobject]@{ Version = '1.0.0'; Target = 'windows-arm64' } } 'd5940d1b248c6fa578487156437109ff29bcc81ddc3364fbaf334b89f90ea973' { return [pscustomobject]@{ Version = '1.0.1'; Target = 'windows-amd64' } } '5713f33e9e95b6c85c9580f567aa7fb87fb35d5551640357ca147e76b273b2a1' { return [pscustomobject]@{ Version = '1.0.1'; Target = 'windows-arm64' } } 'dbd8ccc22cd1d219b307152b69d4c783b00f74089488a27b3252ffcf87c1ef91' { return [pscustomobject]@{ Version = '1.0.3'; Target = 'windows-amd64' } } '80dabc3db122859e916adffcaf25c7db23eef75a6c1f76806e9dc9850d45657d' { return [pscustomobject]@{ Version = '1.0.3'; Target = 'windows-arm64' } } 'd2507073d325e543dc570de757ad11a062a13cd9cd52c17bfd90039a68827782' { return [pscustomobject]@{ Version = '1.0.4'; Target = 'windows-amd64' } } '8cea07abc3dca89f435abdf841456c33dedc2a21430aecc9209af4bd9af92e5d' { return [pscustomobject]@{ Version = '1.0.4'; Target = 'windows-arm64' } } 'd358d097b79960b6cd61c0e3a66d3d4e0cb51b9fd39798d12872bae4eb01c626' { return [pscustomobject]@{ Version = '1.0.5'; Target = 'windows-amd64' } } '2f36eee98d3a44293bf48597f49758bef7cdc947fd90dd671e3118fa9c943433' { return [pscustomobject]@{ Version = '1.0.5'; Target = 'windows-arm64' } } 'b3a89138678d2f93a61140fe8c5aa8707f9326e2f5aeca5d10c0c7768a244dc7' { return [pscustomobject]@{ Version = '1.0.6'; Target = 'windows-amd64' } } 'f42cd2748bb05088acf03e55a1ee03ecea94ece2122eac0649b969276688d605' { return [pscustomobject]@{ Version = '1.0.6'; Target = 'windows-arm64' } } 'aa77ff8583d542bac05acd5ea879f88039a292b4ef26c88da34dbefb93900080' { return [pscustomobject]@{ Version = '1.0.7'; Target = 'windows-amd64' } } '6c66317dc8bd7fea239da1ab1dc9386c0829709f7919c4c1d126e1110fd90a20' { return [pscustomobject]@{ Version = '1.0.7'; Target = 'windows-arm64' } } '926a3e51099059101726ffc8528bf524151edbab52a18c410e2bec51172153ce' { return [pscustomobject]@{ Version = '1.0.8'; Target = 'windows-amd64' } } '6f71ffd0e0c201b19ee48f0ef269995bdc9b297b0fc729dc5b82b1e91b4b840a' { return [pscustomobject]@{ Version = '1.0.8'; Target = 'windows-arm64' } } '05e407e2703ec431ed8e47dd1dec1a71ff60743cedc77c2258e8f254efabc640' { return [pscustomobject]@{ Version = '1.0.9'; Target = 'windows-amd64' } } '8c5772980ef6cf2eeebb2ef54dffdabab0e266a84d61d5c6fd8fc9b719bdf5a7' { return [pscustomobject]@{ Version = '1.0.9'; Target = 'windows-arm64' } } 'fd28f6db459f35757c2c45fe9bc67265099c7e6ffeef46771591e03679a201d4' { return [pscustomobject]@{ Version = '1.0.10'; Target = 'windows-amd64' } } 'aa53655513c955dcdab8a993827da277cc9ef09348af2ad65aef74f6706a245a' { return [pscustomobject]@{ Version = '1.0.10'; Target = 'windows-arm64' } } 'f53ae5afde417f6af0022e348ca8d146c3290e781af06ce8f644de4477e56fd3' { return [pscustomobject]@{ Version = '1.0.11'; Target = 'windows-amd64' } } '0a8958c7fe379884ed063654c06f57ef3300d017eb04f9867d0d2e99f088bf7f' { return [pscustomobject]@{ Version = '1.0.11'; Target = 'windows-arm64' } } 'e0e21ad65e96c494cb4c5c79c1878f69885d61046b553b5037e944656799c3b5' { return [pscustomobject]@{ Version = '1.0.12'; Target = 'windows-amd64' } } '5c069153f4ba7fc0a4867c2d2504dd28827a07d1245dfc260f823633ca0a14da' { return [pscustomobject]@{ Version = '1.0.12'; Target = 'windows-arm64' } } '26459173cbf7f2ffa0872f489e9ae223da3b1ae651045c3ecb3ed913faf89d0e' { return [pscustomobject]@{ Version = '1.0.13'; Target = 'windows-amd64' } } '295cf987c58f5802ae6d5a2f32c248e05299c4c84910cbce616be6cb8e8addf7' { return [pscustomobject]@{ Version = '1.0.13'; Target = 'windows-arm64' } } default { return $null } } } function Get-WindowsTarget { if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { Stop-Install 'This installer only supports Windows.' } $runtimeInformation = 'System.Runtime.InteropServices.RuntimeInformation' -as [type] if ($null -eq $runtimeInformation) { Stop-Install 'This Windows runtime cannot report the native hardware architecture.' } try { $architecture = [string]$runtimeInformation::OSArchitecture } catch { Stop-Install 'Could not read the native Windows hardware architecture.' } switch -Regex ($architecture) { '^(?i:X64|AMD64|x86_64)$' { return 'windows-amd64' } '^(?i:Arm64|aarch64)$' { return 'windows-arm64' } default { Stop-Install "Unsupported native Windows hardware architecture: $architecture" } } } function ConvertTo-NativeArgument { param([string]$Argument) if (Test-ContainsControlCharacter $Argument) { Stop-Install 'A native command argument contains a control character.' } if ($Argument.Length -gt 0 -and $Argument -notmatch '[\s"]') { return $Argument } $builder = New-Object Text.StringBuilder $null = $builder.Append('"') $backslashes = 0 foreach ($character in $Argument.ToCharArray()) { if ($character -eq '\') { $backslashes++ continue } if ($character -eq '"') { $null = $builder.Append([char]'\', (($backslashes * 2) + 1)) $null = $builder.Append('"') $backslashes = 0 continue } if ($backslashes -gt 0) { $null = $builder.Append([char]'\', $backslashes); $backslashes = 0 } $null = $builder.Append($character) } if ($backslashes -gt 0) { $null = $builder.Append([char]'\', ($backslashes * 2)) } $null = $builder.Append('"') return $builder.ToString() } function Join-NativeArguments { param([string[]]$Arguments) return (($Arguments | ForEach-Object { ConvertTo-NativeArgument $_ }) -join ' ') } function New-NativeProcess { param([string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory) $startInfo = New-Object Diagnostics.ProcessStartInfo $startInfo.FileName = $FilePath $startInfo.Arguments = Join-NativeArguments $Arguments $startInfo.WorkingDirectory = $WorkingDirectory $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardInput = $true $startInfo.RedirectStandardOutput = $true $startInfo.RedirectStandardError = $true $process = New-Object Diagnostics.Process $process.StartInfo = $startInfo return $process } function Wait-NativeProcess { param([Diagnostics.Process]$Process, [Threading.Tasks.Task[]]$Tasks, [int]$TimeoutMilliseconds) if (-not $Process.WaitForExit($TimeoutMilliseconds)) { try { $Process.Kill() } catch { } if (-not $Process.WaitForExit(5000)) { throw [TimeoutException]::new('Native command did not stop after its timeout.') } throw [TimeoutException]::new('Native command timed out.') } $Process.WaitForExit() if (-not [Threading.Tasks.Task]::WaitAll($Tasks, 5000)) { throw [TimeoutException]::new('Native command output did not close.') } foreach ($task in $Tasks) { if ($task.IsFaulted) { throw $task.Exception.GetBaseException() } } } function Invoke-NativeCapture { param( [string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory, [int]$TimeoutMilliseconds, [int]$MaximumCharacters ) $process = $null try { $process = New-NativeProcess $FilePath $Arguments $WorkingDirectory if (-not $process.Start()) { throw [InvalidOperationException]::new('Could not start native command.') } $process.StandardInput.Close() $outputTask = $process.StandardOutput.ReadToEndAsync() $errorTask = $process.StandardError.ReadToEndAsync() Wait-NativeProcess $process @($outputTask, $errorTask) $TimeoutMilliseconds $output = $outputTask.Result $errorOutput = $errorTask.Result if ($output.Length -gt $MaximumCharacters -or $errorOutput.Length -gt $MaximumCharacters) { throw [IO.InvalidDataException]::new('Native command output exceeded its limit.') } return [pscustomobject]@{ ExitCode = $process.ExitCode; StandardOutput = $output; StandardError = $errorOutput } } catch { Stop-Install "A required native command failed safely: $($_.Exception.GetBaseException().Message)" } finally { if ($null -ne $process) { $process.Dispose() } } } function Invoke-NativeToFile { param( [string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory, [string]$OutputPath, [int]$TimeoutMilliseconds, [long]$MaximumBytes ) $process = $null $output = $null try { $process = New-NativeProcess $FilePath $Arguments $WorkingDirectory $output = New-Object IO.FileStream($OutputPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) if (-not $process.Start()) { throw [InvalidOperationException]::new('Could not start native command.') } $process.StandardInput.Close() $copyTask = $process.StandardOutput.BaseStream.CopyToAsync($output) $errorTask = $process.StandardError.ReadToEndAsync() Wait-NativeProcess $process @($copyTask, $errorTask) $TimeoutMilliseconds $output.Flush($true) if ($output.Length -gt $MaximumBytes) { throw [IO.InvalidDataException]::new('Extracted file exceeded its limit.') } return [pscustomobject]@{ ExitCode = $process.ExitCode; StandardError = $errorTask.Result } } catch { Stop-Install "A required native command failed safely: $($_.Exception.GetBaseException().Message)" } finally { if ($null -ne $output) { $output.Dispose() } if ($null -ne $process) { $process.Dispose() } } } function Find-SystemTar { # SpecialFolder values come from Windows, not mutable environment variables. $windowsRoot = Resolve-SafeAbsolutePath ([Environment]::GetFolderPath([Environment+SpecialFolder]::Windows)) 'Windows directory' $systemDirectory = Resolve-SafeAbsolutePath ([Environment]::GetFolderPath([Environment+SpecialFolder]::System)) 'Windows system directory' $candidates = @( (Join-Path $windowsRoot 'Sysnative\tar.exe'), (Join-Path $systemDirectory 'tar.exe') ) foreach ($candidate in $candidates) { $item = Get-ExistingPathItem $candidate if ($null -eq $item) { continue } $null = Assert-SafeRegularFile $candidate 'Windows tar.exe' return $candidate } Stop-Install 'tar.exe is required. Install a modern supported Windows release that includes System32\tar.exe.' } function Get-RegistryPackageOwners { $owners = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) $views = @([Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32) foreach ($hive in @([Microsoft.Win32.RegistryHive]::CurrentUser, [Microsoft.Win32.RegistryHive]::LocalMachine)) { foreach ($view in $views) { $base = $null $uninstall = $null try { $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, $view) $uninstall = $base.OpenSubKey('Software\Microsoft\Windows\CurrentVersion\Uninstall', $false) if ($null -eq $uninstall) { continue } $names = @($uninstall.GetSubKeyNames()) if ($names.Count -gt 16384) { Stop-Install 'The installed-package registry is too large to query safely.' } foreach ($name in $names) { $package = $null try { $package = $uninstall.OpenSubKey($name, $false) if ($null -eq $package) { continue } $displayName = [string]$package.GetValue('DisplayName', '') if ($displayName -match '^(?i:ReRune|ReRune CLI)$') { $null = $owners.Add("Windows package registry: $displayName") } } finally { if ($null -ne $package) { $package.Dispose() } } } } catch [UnauthorizedAccessException] { Stop-Install 'Could not safely query installed-package ownership.' } finally { if ($null -ne $uninstall) { $uninstall.Dispose() } if ($null -ne $base) { $base.Dispose() } } } } return @($owners) } function Add-PackageFolderOwner { param( [Collections.Generic.HashSet[string]]$Owners, [string]$Root, [string]$RelativePath, [string]$Owner ) if ([string]::IsNullOrWhiteSpace($Root)) { return } if (Test-ContainsControlCharacter $Root -or $Root -notmatch '^[A-Za-z]:[\\/]') { Stop-Install "$Owner has an unsafe package root." } try { $fullRoot = [IO.Path]::GetFullPath($Root) } catch { Stop-Install "$Owner has an unsafe package root." } $path = Join-Path $fullRoot $RelativePath $item = Get-ExistingPathItem $path if ($null -ne $item) { $null = $Owners.Add($Owner) } } function Get-FilePackageOwners { param([string]$LocalAppData) $owners = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) Add-PackageFolderOwner $owners $env:SCOOP 'apps\rerune' 'Scoop' if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { Add-PackageFolderOwner $owners $env:USERPROFILE 'scoop\apps\rerune' 'Scoop' } Add-PackageFolderOwner $owners $env:ChocolateyInstall 'lib\rerune' 'Chocolatey' if (-not [string]::IsNullOrWhiteSpace($env:ProgramData)) { Add-PackageFolderOwner $owners $env:ProgramData 'chocolatey\lib\rerune' 'Chocolatey' } $wingetRoot = Join-Path $LocalAppData 'Microsoft\WinGet\Packages' $wingetItem = Get-ExistingPathItem $wingetRoot if ($null -ne $wingetItem) { if (-not $wingetItem.PSIsContainer -or ($wingetItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Stop-Install 'The WinGet package root is not a regular directory.' } try { $count = 0 foreach ($directory in [IO.Directory]::EnumerateDirectories($wingetRoot, '*', [IO.SearchOption]::TopDirectoryOnly)) { $count++ if ($count -gt 16384) { Stop-Install 'The WinGet package directory is too large to query safely.' } $name = [IO.Path]::GetFileName($directory) if ($name -match '^(?i:(?:BasalBit\.)?ReRune(?:_|$))') { $null = $owners.Add('WinGet') } } } catch [UnauthorizedAccessException] { Stop-Install 'Could not safely query WinGet package ownership.' } catch [IO.IOException] { Stop-Install 'Could not safely query WinGet package ownership.' } } return @($owners) } function Assert-NoPackageOwnership { param([string]$LocalAppData) # Query package registry and metadata only. Never launch a package manager or package-owned rerune command. $owners = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) foreach ($owner in @(Get-RegistryPackageOwners)) { $null = $owners.Add($owner) } foreach ($owner in @(Get-FilePackageOwners $LocalAppData)) { $null = $owners.Add($owner) } if ($owners.Count -gt 0) { Stop-Install ("A package manager owns a ReRune installation ({0}). Uninstall it with that package manager before using this installer." -f ((@($owners) | Sort-Object) -join ', ')) } } function Assert-NoActiveExternalReRune { param([string]$Destination) $oldAutoLoading = $PSModuleAutoLoadingPreference try { $PSModuleAutoLoadingPreference = 'None' $commands = @(Microsoft.PowerShell.Core\Get-Command rerune -All -ErrorAction SilentlyContinue) } finally { $PSModuleAutoLoadingPreference = $oldAutoLoading } if ($commands.Count -eq 0) { return } $command = $commands[0] if ($command.CommandType -ne [Management.Automation.CommandTypes]::Application) { Stop-Install "The active rerune command is a $($command.CommandType), not the installer-managed executable. Remove that command conflict first." } $commandPath = [string]$command.Source if ([string]::IsNullOrEmpty($commandPath)) { $commandPath = [string]$command.Path } if ([string]::IsNullOrEmpty($commandPath) -or (Test-ContainsControlCharacter $commandPath)) { Stop-Install 'The active rerune command has an unsafe path.' } try { $commandFullPath = [IO.Path]::GetFullPath($commandPath) } catch { Stop-Install 'The active rerune command does not have a valid absolute path.' } if (-not $commandFullPath.Equals($Destination, [StringComparison]::OrdinalIgnoreCase)) { Stop-Install "Another rerune command is active at $commandFullPath. This installer will not shadow or execute it." } } function Set-PrivateDirectoryAcl { param([string]$Path) try { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $sid = $identity.User if ($null -eq $sid) { Stop-Install 'Could not identify the current Windows user.' } $security = New-Object Security.AccessControl.DirectorySecurity $security.SetOwner($sid) $security.SetAccessRuleProtection($true, $false) $inheritance = [Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [Security.AccessControl.InheritanceFlags]::ObjectInherit $rule = New-Object Security.AccessControl.FileSystemAccessRule( $sid, [Security.AccessControl.FileSystemRights]::FullControl, $inheritance, [Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow) $security.AddAccessRule($rule) Set-Acl -LiteralPath $Path -AclObject $security -ErrorAction Stop } catch { Stop-Install 'Could not make the installer temporary directory private to the current user.' } } function New-PrivateTempDirectory { param([string]$InstallRoot) for ($attempt = 0; $attempt -lt 8; $attempt++) { $path = Join-Path $InstallRoot ('.tmp-' + [Guid]::NewGuid().ToString('N')) try { $null = [IO.Directory]::CreateDirectory($path) Set-PrivateDirectoryAcl $path Assert-SafeDirectory $path 'private installer temporary directory' return $path } catch { if ([IO.Directory]::Exists($path)) { try { [IO.Directory]::Delete($path, $true) } catch { } } if ($attempt -eq 7) { throw } } } Stop-Install 'Could not create a private installer temporary directory.' } function Get-LockOwnerSid { param([string]$Path) try { $acl = Get-Acl -LiteralPath $Path -ErrorAction Stop return $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value } catch { Stop-Install 'Could not verify ownership of the installer lock.' } } function New-LockText { param([string]$Nonce) $process = [Diagnostics.Process]::GetCurrentProcess() $startTicks = $process.StartTime.ToUniversalTime().Ticks $createdTicks = [DateTime]::UtcNow.Ticks return "ReRune CLI installer lock v1`npid=$($process.Id)`nprocessStartUtcTicks=$startTicks`ncreatedUtcTicks=$createdTicks`nnonce=$Nonce`n" } function Read-LockRecord { param([IO.FileStream]$Stream) if ($Stream.Length -le 0 -or $Stream.Length -gt 512) { Stop-Install 'The installer lock is not a recognized installer-owned lock.' } $bytes = New-Object byte[] ([int]$Stream.Length) $Stream.Position = 0 $read = 0 while ($read -lt $bytes.Length) { $count = $Stream.Read($bytes, $read, $bytes.Length - $read) if ($count -le 0) { Stop-Install 'Could not read the installer lock.' } $read += $count } foreach ($byte in $bytes) { if (($byte -lt 0x20 -and $byte -ne 0x0A) -or $byte -gt 0x7E) { Stop-Install 'The installer lock has invalid data.' } } $text = [Text.Encoding]::ASCII.GetString($bytes) $match = [regex]::Match($text, '^ReRune CLI installer lock v1\npid=([1-9][0-9]{0,9})\nprocessStartUtcTicks=([1-9][0-9]{0,18})\ncreatedUtcTicks=([1-9][0-9]{0,18})\nnonce=([a-f0-9]{32})\n$') if (-not $match.Success) { Stop-Install 'The installer lock is not a recognized installer-owned lock.' } $pidValue = 0 $startTicks = [long]0 $createdTicks = [long]0 if (-not [int]::TryParse($match.Groups[1].Value, [ref]$pidValue) -or -not [long]::TryParse($match.Groups[2].Value, [ref]$startTicks) -or -not [long]::TryParse($match.Groups[3].Value, [ref]$createdTicks)) { Stop-Install 'The installer lock has invalid PID or time data.' } try { $created = [DateTime]::new($createdTicks, [DateTimeKind]::Utc) $started = [DateTime]::new($startTicks, [DateTimeKind]::Utc) } catch { Stop-Install 'The installer lock has invalid time data.' } $now = [DateTime]::UtcNow if ($created -gt $now.AddMinutes(5) -or $started -gt $created.AddMinutes(5)) { Stop-Install 'The installer lock has inconsistent time data.' } return [pscustomobject]@{ Pid = $pidValue; Started = $started; Created = $created; Nonce = $match.Groups[4].Value; Text = $text } } function Test-LockProcessActive { param([pscustomobject]$Record) try { $process = [Diagnostics.Process]::GetProcessById($Record.Pid) } catch [ArgumentException] { return $false } catch { Stop-Install 'Could not validate the process recorded in the installer lock.' } try { $actualStart = $process.StartTime.ToUniversalTime() return $actualStart.Ticks -eq $Record.Started.Ticks } catch { Stop-Install 'Could not validate the process start time in the installer lock.' } finally { $process.Dispose() } } function Write-LockStream { param([IO.FileStream]$Stream, [string]$Text) $bytes = [Text.Encoding]::ASCII.GetBytes($Text) $Stream.SetLength(0) $Stream.Position = 0 $Stream.Write($bytes, 0, $bytes.Length) $Stream.Flush($true) } function Acquire-InstallerLock { param([string]$Path) $null = Assert-NoReparsePath $Path 'installer lock path' $nonce = [Guid]::NewGuid().ToString('N') $text = New-LockText $nonce $stream = $null try { try { $stream = New-Object IO.FileStream($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) } catch [IO.IOException] { $item = Assert-SafeRegularFile $Path 'installer lock' if ($item.Length -gt 512) { Stop-Install 'The installer lock is not a recognized installer-owned lock.' } $currentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value if ((Get-LockOwnerSid $Path) -cne $currentSid) { Stop-Install 'The installer lock is not owned by the current user.' } try { $stream = New-Object IO.FileStream($Path, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) } catch [IO.IOException] { Stop-Install 'Another ReRune CLI installer is active.' } $record = Read-LockRecord $stream if (Test-LockProcessActive $record) { Stop-Install ("Another ReRune CLI installer is active with PID {0}." -f $record.Pid) } if ($record.Created -gt [DateTime]::UtcNow.AddMinutes(5)) { Stop-Install 'The stale installer lock has an invalid creation time.' } Write-WarningLine ("Recovering installer lock left by PID {0} at {1:u}." -f $record.Pid, $record.Created) } try { Write-LockStream $stream $text } catch { try { $stream.Dispose() } catch { } $stream = $null try { [IO.File]::Delete($Path) } catch { } throw } $State.LockHandle = $stream $State.LockPath = $Path $State.LockText = $text $stream = $null } finally { if ($null -ne $stream) { $stream.Dispose() } } } function Release-InstallerLock { if ($null -eq $State.LockHandle) { return } try { $State.LockHandle.Dispose() } catch { } $State.LockHandle = $null try { $item = Assert-SafeRegularFile $State.LockPath 'installer lock' if ($item.Length -le 512 -and [IO.File]::ReadAllText($State.LockPath, [Text.Encoding]::ASCII) -ceq $State.LockText) { [IO.File]::Delete($State.LockPath) } else { Write-WarningLine "The installer lock changed and was not removed: $($State.LockPath)" } } catch { Write-WarningLine "Could not remove the installer lock: $($State.LockPath)" } $State.LockPath = $null $State.LockText = $null } function Get-VersionFromOutput { param([string]$Output) if ($null -eq $Output -or (Test-ContainsControlCharacter ($Output.Replace("`r", '').Replace("`n", '')))) { return $null } $text = $Output.Trim() $pattern = '^(?:(?:rerune)(?:\.exe)?(?:\s+CLI)?(?:\s+version)?[ :]+)?v?((?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))$' $match = [regex]::Match($text, $pattern, [Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [Text.RegularExpressions.RegexOptions]::CultureInvariant) if (-not $match.Success) { return $null } return $match.Groups[1].Value } function Invoke-ReviewedBinaryVersion { param([string]$Path, [string]$WorkingDirectory) $result = Invoke-NativeCapture $Path @('version') $WorkingDirectory 10000 65536 if ($result.ExitCode -ne 0) { $result = Invoke-NativeCapture $Path @('--version') $WorkingDirectory 10000 65536 } if ($result.ExitCode -ne 0 -or -not [string]::IsNullOrWhiteSpace($result.StandardError)) { return $null } return Get-VersionFromOutput $result.StandardOutput } function Get-DestinationState { param( [string]$Destination, [string]$Target, [pscustomobject]$CurrentSemVer, [string]$WorkingDirectory ) $item = Get-ExistingPathItem $Destination if ($null -eq $item) { return [pscustomobject]@{ State = 'Absent'; Version = $null; Target = $null; Hash = $null; Reason = $null } } if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { return [pscustomobject]@{ State = 'Conflict'; Version = $null; Target = $null; Hash = $null; Reason = 'The install destination is not a regular non-reparse file.' } } $guard = Open-FileDigestGuard $Destination 'existing ReRune destination' try { if ($guard.Length -le 0 -or $guard.Length -gt $State.MaximumFileBytes) { return [pscustomobject]@{ State = 'Conflict'; Version = $null; Target = $null; Hash = $guard.Hash; Reason = 'The existing destination has an unsafe size.' } } $knownBinary = Get-KnownBinaryVersion $Target $guard.Hash if ($null -eq $knownBinary) { return [pscustomobject]@{ State = 'Conflict'; Version = $null; Target = $null; Hash = $guard.Hash; Reason = 'The existing destination hash is not a reviewed ReRune release.' } } $knownVersion = [string]$knownBinary.Version $knownTarget = [string]$knownBinary.Target if ($knownTarget -notmatch '^windows-(?:amd64|arm64)$') { return [pscustomobject]@{ State = 'Conflict'; Version = $knownVersion; Target = $knownTarget; Hash = $guard.Hash; Reason = 'The reviewed destination has an invalid mapped target.' } } try { $knownSemVer = ConvertTo-StrictSemVer $knownVersion 'Known ReRune binary version' } catch { return [pscustomobject]@{ State = 'Conflict'; Version = $knownVersion; Target = $knownTarget; Hash = $guard.Hash; Reason = $_.Exception.Message } } if ($knownTarget -ceq $Target) { # Keep the no-write/no-delete guard open while Windows starts and waits for the reviewed path. $reportedVersion = Invoke-ReviewedBinaryVersion $Destination $WorkingDirectory if ($null -eq $reportedVersion -or $reportedVersion -cne $knownVersion) { return [pscustomobject]@{ State = 'Conflict'; Version = $knownVersion; Target = $knownTarget; Hash = $guard.Hash; Reason = 'The reviewed destination did not report its mapped exact version.' } } } $comparison = Compare-StrictSemVer $knownSemVer $CurrentSemVer if ($comparison -eq 0 -and $knownTarget -ceq $Target) { $state = 'Current' } elseif ($comparison -le 0) { $state = 'Upgrade' } else { $state = 'Newer' } return [pscustomobject]@{ State = $state; Version = $knownVersion; Target = $knownTarget; Hash = $guard.Hash; Reason = $null } } finally { $guard.Stream.Dispose() } } function Test-TransientDownloadFailure { param([Exception]$Exception) $base = $Exception.GetBaseException() if ($base -is [TimeoutException] -or $base -is [IO.IOException]) { return $true } if ($base -isnot [Net.WebException]) { return $false } if ($null -ne $base.Response -and $base.Response -is [Net.HttpWebResponse]) { $status = [int]$base.Response.StatusCode return $status -eq 408 -or $status -eq 429 -or $status -ge 500 } return $base.Status -in @( [Net.WebExceptionStatus]::ConnectFailure, [Net.WebExceptionStatus]::ConnectionClosed, [Net.WebExceptionStatus]::KeepAliveFailure, [Net.WebExceptionStatus]::NameResolutionFailure, [Net.WebExceptionStatus]::ProxyNameResolutionFailure, [Net.WebExceptionStatus]::ReceiveFailure, [Net.WebExceptionStatus]::SendFailure, [Net.WebExceptionStatus]::Timeout) } function Assert-SafeHttpsUri { param([Uri]$Uri) if ($null -eq $Uri -or -not $Uri.IsAbsoluteUri -or $Uri.Scheme -cne 'https' -or [string]::IsNullOrEmpty($Uri.Host) -or -not [string]::IsNullOrEmpty($Uri.UserInfo)) { Stop-Install 'A release download or redirect URL was not safe HTTPS.' } } function Invoke-DownloadAttempt { param([Uri]$Uri, [string]$Destination) $currentUri = $Uri for ($redirects = 0; $redirects -le 3; $redirects++) { Assert-SafeHttpsUri $currentUri $request = [Net.HttpWebRequest]::Create($currentUri) $request.Method = 'GET' $request.AllowAutoRedirect = $false $request.MaximumAutomaticRedirections = 1 $request.Timeout = 15000 $request.ReadWriteTimeout = 15000 $request.UserAgent = 'ReRune-CLI-Installer' $request.AutomaticDecompression = [Net.DecompressionMethods]::None $request.Proxy = [Net.WebRequest]::GetSystemWebProxy() if ($null -ne $request.Proxy) { $request.Proxy.Credentials = [Net.CredentialCache]::DefaultNetworkCredentials } $response = $null $source = $null $destinationStream = $null try { $response = [Net.HttpWebResponse]$request.GetResponse() $status = [int]$response.StatusCode if ($status -in @(301, 302, 303, 307, 308)) { if ($redirects -eq 3) { Stop-Install 'The release download exceeded the redirect limit.' } $location = $response.Headers['Location'] if ([string]::IsNullOrWhiteSpace($location) -or (Test-ContainsControlCharacter $location)) { Stop-Install 'The release server returned an unsafe redirect.' } try { $currentUri = New-Object Uri($currentUri, $location) } catch { Stop-Install 'The release server returned an invalid redirect.' } continue } if ($status -ne 200) { Stop-Install "The release server returned HTTP $status." } if ($response.ContentLength -gt $State.MaximumFileBytes) { Stop-Install 'The release archive exceeds the 100 MiB limit.' } $source = $response.GetResponseStream() $destinationStream = New-Object IO.FileStream($Destination, [IO.FileMode]::Create, [IO.FileAccess]::Write, [IO.FileShare]::None) $buffer = New-Object byte[] 65536 $total = [long]0 $timer = [Diagnostics.Stopwatch]::StartNew() while (($count = $source.Read($buffer, 0, $buffer.Length)) -gt 0) { if ($timer.Elapsed.TotalSeconds -gt 60) { throw [TimeoutException]::new('Release download exceeded its time limit.') } $total += $count if ($total -gt $State.MaximumFileBytes) { Stop-Install 'The release archive exceeds the 100 MiB limit.' } $destinationStream.Write($buffer, 0, $count) } $destinationStream.Flush($true) if ($total -le 0) { Stop-Install 'The release server returned an empty archive.' } return } finally { if ($null -ne $destinationStream) { $destinationStream.Dispose() } if ($null -ne $source) { $source.Dispose() } if ($null -ne $response) { $response.Dispose() } } } } function Download-ReleaseArchive { param([Uri]$Uri, [string]$Destination) $oldProtocols = [Net.ServicePointManager]::SecurityProtocol $protocols = [Net.SecurityProtocolType]::Tls12 if ([Enum]::GetNames([Net.SecurityProtocolType]) -contains 'Tls13') { $protocols = $protocols -bor [Net.SecurityProtocolType]([Enum]::Parse([Net.SecurityProtocolType], 'Tls13')) } $legacyCertificatePolicy = $null $legacyPolicyProperty = [Net.ServicePointManager].GetProperty( 'CertificatePolicy', [Reflection.BindingFlags]::Public -bor [Reflection.BindingFlags]::Static) if ($null -ne $legacyPolicyProperty) { $legacyCertificatePolicy = $legacyPolicyProperty.GetValue($null, $null) } $legacyPolicyIsDefault = $null -eq $legacyCertificatePolicy -or $legacyCertificatePolicy.GetType().FullName -ceq 'System.Net.DefaultCertPolicy' if ($null -ne [Net.ServicePointManager]::ServerCertificateValidationCallback -or -not $legacyPolicyIsDefault) { Stop-Install 'A process-wide TLS certificate override is active. Start a clean PowerShell session before installing.' } try { [Net.ServicePointManager]::SecurityProtocol = $protocols for ($attempt = 1; $attempt -le 3; $attempt++) { try { if ([IO.File]::Exists($Destination)) { [IO.File]::Delete($Destination) } Invoke-DownloadAttempt $Uri $Destination return } catch { $failure = $_.Exception $transient = Test-TransientDownloadFailure $failure $baseFailure = $failure.GetBaseException() if ($baseFailure -is [Net.WebException] -and $null -ne $baseFailure.Response) { try { $baseFailure.Response.Dispose() } catch { } } if ($attempt -eq 3 -or -not $transient) { throw } Write-WarningLine "Release download attempt $attempt failed; retrying." Start-Sleep -Seconds $attempt } } } finally { [Net.ServicePointManager]::SecurityProtocol = $oldProtocols } } function ConvertFrom-TarLines { param([string]$Output, [string]$Label) if ($Output.IndexOf([char]0) -ge 0 -or $Output.Contains("`r") -and -not $Output.Contains("`r`n")) { Stop-Install "$Label contains invalid control data." } $normalized = $Output.Replace("`r`n", "`n") if ($normalized.EndsWith("`n")) { $normalized = $normalized.Substring(0, $normalized.Length - 1) } if ([string]::IsNullOrEmpty($normalized)) { return @() } return @($normalized.Split(@("`n"), [StringSplitOptions]::None)) } function Get-SafeTarBinaryMember { param([string]$TarPath, [string]$ArchivePath, [string]$WorkingDirectory) $list = Invoke-NativeCapture $TarPath @('-tzf', $ArchivePath) $WorkingDirectory 30000 4194304 if ($list.ExitCode -ne 0 -or -not [string]::IsNullOrWhiteSpace($list.StandardError)) { Stop-Install 'tar.exe could not list the release archive.' } $members = @(ConvertFrom-TarLines $list.StandardOutput 'tar member list') if ($members.Count -eq 0 -or $members.Count -gt 4096) { Stop-Install 'The release archive has an unsafe member count.' } $verbose = Invoke-NativeCapture $TarPath @('-tvzf', $ArchivePath) $WorkingDirectory 30000 4194304 if ($verbose.ExitCode -ne 0 -or -not [string]::IsNullOrWhiteSpace($verbose.StandardError)) { Stop-Install 'tar.exe could not inspect release archive member types.' } $typeLines = @(ConvertFrom-TarLines $verbose.StandardOutput 'tar verbose member list') if ($typeLines.Count -ne $members.Count) { Stop-Install 'The release archive member listings disagree.' } $seen = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) $binaryMembers = New-Object 'Collections.Generic.List[string]' for ($index = 0; $index -lt $members.Count; $index++) { $member = $members[$index] if ([string]::IsNullOrEmpty($member) -or $member -notmatch '^[A-Za-z0-9._/-]+$' -or $member.StartsWith('/') -or $member.StartsWith('-') -or $member.Contains('//') -or $member.Contains('\')) { Stop-Install 'The release archive contains an unsafe path.' } $normalized = $member.TrimEnd('/') if ([string]::IsNullOrEmpty($normalized)) { Stop-Install 'The release archive contains an unsafe root path.' } foreach ($part in $normalized.Split('/')) { if ([string]::IsNullOrEmpty($part) -or $part -eq '.' -or $part -eq '..') { Stop-Install 'The release archive contains a traversal path.' } } if (-not $seen.Add($normalized)) { Stop-Install 'The release archive contains duplicate paths.' } $typeLine = $typeLines[$index] if ([string]::IsNullOrEmpty($typeLine) -or ($typeLine[0] -ne '-' -and $typeLine[0] -ne 'd') -or $typeLine -match '(?: -> | link to )') { Stop-Install 'The release archive contains a link or unsupported member type.' } if ($member.EndsWith('/') -and $typeLine[0] -ne 'd') { Stop-Install 'The release archive has an inconsistent directory member.' } if (-not $member.EndsWith('/') -and $typeLine[0] -ne '-') { Stop-Install 'The release archive has an inconsistent file member.' } if ([IO.Path]::GetFileName($normalized).Equals('rerune.exe', [StringComparison]::OrdinalIgnoreCase)) { if ($typeLine[0] -ne '-') { Stop-Install 'The rerune.exe archive member is not a regular file.' } $binaryMembers.Add($normalized) } } if ($binaryMembers.Count -ne 1) { Stop-Install 'The release archive must contain exactly one rerune.exe regular file.' } return $binaryMembers[0] } function Export-TarBinaryMember { param( [string]$TarPath, [string]$ArchivePath, [string]$Member, [string]$Destination, [string]$WorkingDirectory ) $result = Invoke-NativeToFile $TarPath @('-xOzf', $ArchivePath, '--', $Member) $WorkingDirectory $Destination 30000 $State.MaximumFileBytes if ($result.ExitCode -ne 0 -or -not [string]::IsNullOrWhiteSpace($result.StandardError)) { Stop-Install 'tar.exe could not safely extract rerune.exe.' } $digest = Get-FileDigest $Destination 'extracted ReRune candidate' if ($digest.Length -le 0 -or $digest.Length -gt $State.MaximumFileBytes) { Stop-Install 'The extracted ReRune candidate has an unsafe size.' } return $digest } function Copy-ToStagedFile { param([string]$Source, [string]$Destination) $input = $null $output = $null try { $input = [IO.File]::Open($Source, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) $output = [IO.File]::Open($Destination, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) $input.CopyTo($output) $output.Flush($true) } catch { Stop-Install 'Could not create the staged ReRune executable.' } finally { if ($null -ne $output) { $output.Dispose() } if ($null -ne $input) { $input.Dispose() } } } function Restore-Backup { param( [string]$Destination, [string]$Backup, [string]$ExpectedDestinationHash, [string]$ExpectedBackupHash ) try { $backupDigest = Get-FileDigest $Backup 'ReRune rollback backup' if ($backupDigest.Hash -cne $ExpectedBackupHash) { return $false } if ([IO.File]::Exists($Destination)) { $destinationDigest = Get-FileDigest $Destination 'ReRune rollback destination' if ($destinationDigest.Hash -cne $ExpectedDestinationHash) { return $false } [IO.File]::Replace($Backup, $Destination, $null, $true) } else { [IO.File]::Move($Backup, $Destination) } return (Get-FileDigest $Destination 'restored ReRune executable').Hash -ceq $ExpectedBackupHash } catch { return $false } } function Clear-ReplacementState { $State.BackupPath = $null $State.BackupExpectedHash = $null $State.ReplacementExpectedHash = $null } function Install-StagedCandidate { param( [string]$Candidate, [string]$Destination, [string]$ExpectedBinaryHash, [pscustomobject]$ExpectedState, [string]$Target, [pscustomobject]$CurrentSemVer, [string]$WorkingDirectory ) $binDirectory = [IO.Path]::GetDirectoryName($Destination) Assert-SafeDirectory $binDirectory 'ReRune bin directory' $currentState = Get-DestinationState $Destination $Target $CurrentSemVer $WorkingDirectory if ($currentState.State -eq 'Conflict') { Stop-Install $currentState.Reason } if ($currentState.State -ne $ExpectedState.State -or $currentState.Hash -cne $ExpectedState.Hash) { Stop-Install 'The install destination changed while the release was downloading.' } $stage = Join-Path $binDirectory ('.rerune-stage-' + [Guid]::NewGuid().ToString('N') + '.exe') $State.StagedPath = $stage Copy-ToStagedFile $Candidate $stage $stageDigest = Get-FileDigest $stage 'staged ReRune candidate' if ($stageDigest.Hash -cne $ExpectedBinaryHash) { Stop-Install 'The staged ReRune candidate SHA-256 verification failed.' } if ($currentState.State -eq 'Absent') { try { [IO.File]::Move($stage, $Destination); $State.StagedPath = $null } catch { Stop-Install 'Could not install rerune.exe. No previous installer-managed file was changed.' } $failedInstall = Join-Path $binDirectory ('.rerune-displaced-' + [Guid]::NewGuid().ToString('N') + '.exe') try { $installed = Get-FileDigest $Destination 'installed ReRune executable' } catch { try { [IO.File]::Move($Destination, $failedInstall); $State.DisplacedPath = $failedInstall } catch { } Stop-Install 'The fresh install could not be verified.' } if ($installed.Hash -cne $ExpectedBinaryHash) { try { [IO.File]::Move($Destination, $failedInstall); $State.DisplacedPath = $failedInstall } catch { } Stop-Install 'The fresh install failed final verification.' } return } $backup = Join-Path $binDirectory ('.rerune-backup-' + [Guid]::NewGuid().ToString('N') + '.exe') $displaced = Join-Path $binDirectory ('.rerune-displaced-' + [Guid]::NewGuid().ToString('N') + '.exe') $State.BackupPath = $backup $State.BackupExpectedHash = $currentState.Hash $State.ReplacementExpectedHash = $ExpectedBinaryHash Copy-ToStagedFile $Destination $backup $backupDigest = Get-FileDigest $backup 'reviewed ReRune rollback backup' if ($backupDigest.Hash -cne $currentState.Hash) { Stop-Install 'Could not preserve the reviewed ReRune executable before replacement.' } $finalState = Get-DestinationState $Destination $Target $CurrentSemVer $WorkingDirectory if ($finalState.State -eq 'Conflict' -or $finalState.State -ne $currentState.State -or $finalState.Hash -cne $currentState.Hash) { try { [IO.File]::Delete($backup); Clear-ReplacementState } catch { } Stop-Install 'The install destination changed before atomic replacement.' } try { [IO.File]::Replace($stage, $Destination, $displaced, $true) $State.StagedPath = $null } catch { $oldStillPresent = $false try { $oldStillPresent = (Get-FileDigest $Destination 'existing ReRune destination').Hash -ceq $currentState.Hash } catch { } if ($oldStillPresent) { try { [IO.File]::Delete($backup); Clear-ReplacementState } catch { } Stop-Install 'Could not replace rerune.exe. If it is running, close it and retry. The old executable remains installed.' } Stop-Install "Could not replace rerune.exe safely. The reviewed backup remains at $backup." } $displacedDigest = $null try { $displacedDigest = Get-FileDigest $displaced 'atomically displaced ReRune executable' } catch { } if ($null -eq $displacedDigest -or $displacedDigest.Hash -cne $currentState.Hash) { if (Restore-Backup $Destination $backup $ExpectedBinaryHash $currentState.Hash) { Clear-ReplacementState $State.DisplacedPath = $displaced Stop-Install "The destination changed during replacement. The reviewed CLI was restored; the displaced file was preserved at $displaced." } $State.DisplacedPath = $displaced Stop-Install "The destination changed during replacement and rollback failed. The reviewed backup remains at $backup." } $State.DisplacedPath = $displaced try { [IO.File]::Delete($displaced); $State.DisplacedPath = $null } catch { Write-WarningLine "The update succeeded, but a duplicate reviewed backup could not be removed: $displaced" } try { $installed = Get-FileDigest $Destination 'installed ReRune executable' } catch { if (-not (Restore-Backup $Destination $backup $ExpectedBinaryHash $currentState.Hash)) { Stop-Install "Final verification failed and rollback failed. The reviewed backup remains at $backup." } Clear-ReplacementState throw } if ($installed.Hash -cne $ExpectedBinaryHash) { if (-not (Restore-Backup $Destination $backup $ExpectedBinaryHash $currentState.Hash)) { Stop-Install "Final verification failed and rollback failed. The reviewed backup remains at $backup." } Clear-ReplacementState Stop-Install 'Final SHA-256 verification failed. The previous ReRune executable was restored.' } try { [IO.File]::Delete($backup) } catch { Write-WarningLine "Installed successfully, but the reviewed rollback backup could not be removed: $backup" } Clear-ReplacementState } function Test-UserPathContains { param([string]$Directory) $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ([string]::IsNullOrEmpty($userPath)) { return $false } foreach ($entry in $userPath.Split(';')) { $candidate = $entry.Trim().Trim('"') if ([string]::IsNullOrEmpty($candidate) -or (Test-ContainsControlCharacter $candidate)) { continue } try { $expanded = [Environment]::ExpandEnvironmentVariables($candidate) if ($expanded -notmatch '^[A-Za-z]:[\\/]') { continue } $full = [IO.Path]::GetFullPath($expanded).TrimEnd([char[]]@('\', '/')) if ($full.Equals($Directory, [StringComparison]::OrdinalIgnoreCase)) { return $true } } catch { } } return $false } function Write-PathWarningIfNeeded { param([string]$BinDirectory) if (Test-UserPathContains $BinDirectory) { return } Write-WarningLine "The ReRune bin directory is not on your user PATH: $BinDirectory" Write-WarningLine 'The installer did not change PATH. To add it, run this exact PowerShell command and open a new shell:' Write-WarningLine '$reruneBin = Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)) "Programs\ReRune\bin"; [Environment]::SetEnvironmentVariable("Path", $reruneBin + ";" + [Environment]::GetEnvironmentVariable("Path", "User"), "User")' } function Remove-InstallerScratch { if ($null -ne $State.BackupPath -and [IO.File]::Exists($State.BackupPath)) { if ($null -ne $State.ReplacementExpectedHash -and $null -ne $State.BackupExpectedHash -and $null -ne $State.DestinationPath -and (Restore-Backup $State.DestinationPath $State.BackupPath $State.ReplacementExpectedHash $State.BackupExpectedHash)) { Write-WarningLine 'The previous reviewed ReRune executable was restored during installer cleanup.' } else { Write-WarningLine "A replacement backup was preserved for manual recovery: $($State.BackupPath)" } } $State.BackupPath = $null $State.BackupExpectedHash = $null $State.ReplacementExpectedHash = $null if ($null -ne $State.DisplacedPath -and [IO.File]::Exists($State.DisplacedPath)) { Write-WarningLine "A concurrently displaced file was preserved for manual inspection: $($State.DisplacedPath)" } $State.DisplacedPath = $null if ($null -ne $State.StagedPath -and [IO.File]::Exists($State.StagedPath)) { try { [IO.File]::Delete($State.StagedPath) } catch { Write-WarningLine "Could not remove staged file: $($State.StagedPath)" } } $State.StagedPath = $null if ($null -ne $State.PrivateTemp -and [IO.Directory]::Exists($State.PrivateTemp)) { try { $item = Get-Item -LiteralPath $State.PrivateTemp -Force -ErrorAction Stop if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw [IO.IOException]::new('Temporary root became a reparse point.') } [IO.Directory]::Delete($State.PrivateTemp, $true) } catch { Write-WarningLine "Could not remove private installer temporary files: $($State.PrivateTemp)" } } $State.PrivateTemp = $null } function Invoke-ReRuneInstaller { if ($InstallerArguments.Count -gt 0) { if ($InstallerArguments.Count -eq 1 -and ($InstallerArguments[0] -ceq '-Help' -or $InstallerArguments[0] -ceq '--help')) { Show-Help return } Stop-Install 'Unknown installer arguments. Only -Help and --help are supported.' } $currentSemVer = ConvertTo-StrictSemVer $State.CurrentVersion 'Generated current CLI release version' $target = Get-WindowsTarget $artifact = Get-CurrentArtifact $target if ($null -eq $artifact) { Stop-Install "No stable ReRune CLI artifact supports $target." } $localAppDataValue = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) if ([string]::IsNullOrWhiteSpace($localAppDataValue)) { Stop-Install 'Windows did not provide the current user local application-data directory.' } $localAppData = Resolve-SafeAbsolutePath $localAppDataValue 'current user local application-data directory' Assert-SafeDirectory $localAppData 'current user local application-data directory' $installRoot = Join-Path $localAppData 'Programs\ReRune' $binDirectory = Join-Path $installRoot 'bin' $destination = Join-Path $binDirectory 'rerune.exe' $State.DestinationPath = $destination New-SafeDirectory $installRoot 'ReRune install directory' New-SafeDirectory $binDirectory 'ReRune bin directory' $null = Assert-NoReparsePath $destination 'ReRune install destination' Write-Normal 'ReRune CLI installer' Write-Normal " Platform: $target" Write-Normal " Target: ReRune CLI $($State.CurrentVersion)" Write-Normal " Path: $destination" Write-Normal '' # Ownership checks happen before the lock, temporary files, or any network request. Assert-NoActiveExternalReRune $destination Assert-NoPackageOwnership $localAppData $lockPath = Join-Path $installRoot '.install.lock' Acquire-InstallerLock $lockPath $existing = Get-DestinationState $destination $target $currentSemVer $installRoot if ($existing.State -eq 'Conflict') { Stop-Install $existing.Reason } if ($existing.State -eq 'Current') { Write-SuccessLine "ReRune CLI $($existing.Version) is already installed at $destination." Write-PathWarningIfNeeded $binDirectory return } if ($existing.State -eq 'Newer') { Write-WarningLine "A newer reviewed ReRune CLI $($existing.Version) is installed at $destination. It was left unchanged." Write-PathWarningIfNeeded $binDirectory return } $tarPath = Find-SystemTar $temporaryRoot = Resolve-SafeAbsolutePath ([IO.Path]::GetTempPath()) 'Windows temporary directory' Assert-SafeDirectory $temporaryRoot 'Windows temporary directory' $State.PrivateTemp = New-PrivateTempDirectory $temporaryRoot $archivePath = Join-Path $State.PrivateTemp 'rerune.tar.gz' $candidatePath = Join-Path $State.PrivateTemp 'rerune.exe' Write-InfoLine "Downloading ReRune CLI $($State.CurrentVersion) for $target." Download-ReleaseArchive $artifact.Uri $archivePath $archiveDigest = Get-FileDigest $archivePath 'downloaded ReRune release archive' if ($archiveDigest.Length -le 0 -or $archiveDigest.Length -gt $State.MaximumFileBytes) { Stop-Install 'The downloaded release archive has an unsafe size.' } if ($archiveDigest.Hash -cne $artifact.ArchiveSha256) { Stop-Install 'The release archive SHA-256 verification failed.' } Write-SuccessLine 'Download verified.' $binaryMember = Get-SafeTarBinaryMember $tarPath $archivePath $State.PrivateTemp $candidateDigest = Export-TarBinaryMember $tarPath $archivePath $binaryMember $candidatePath $State.PrivateTemp if ($candidateDigest.Hash -cne $artifact.BinarySha256) { Stop-Install 'The extracted ReRune executable SHA-256 verification failed.' } # Keep a no-write/no-delete hash guard open from the final review through version execution. $candidateGuard = Open-FileDigestGuard $candidatePath 'reviewed ReRune candidate' try { if ($candidateGuard.Hash -cne $artifact.BinarySha256) { Stop-Install 'The ReRune candidate changed after extraction.' } $candidateVersion = Invoke-ReviewedBinaryVersion $candidatePath $State.PrivateTemp if ($null -eq $candidateVersion -or $candidateVersion -cne $State.CurrentVersion) { Stop-Install "The reviewed ReRune candidate did not report exact version $($State.CurrentVersion)." } } finally { $candidateGuard.Stream.Dispose() } Install-StagedCandidate $candidatePath $destination $artifact.BinarySha256 $existing $target $currentSemVer $State.PrivateTemp if ($existing.State -eq 'Upgrade' -and $existing.Version -ceq $State.CurrentVersion) { Write-SuccessLine "Updated ReRune CLI $($State.CurrentVersion) to the native $target build at $destination." } elseif ($existing.State -eq 'Upgrade') { Write-SuccessLine "Updated ReRune CLI from $($existing.Version) to $($State.CurrentVersion) at $destination." } else { Write-SuccessLine "Installed ReRune CLI $($State.CurrentVersion) at $destination." } Write-PathWarningIfNeeded $binDirectory } try { Invoke-ReRuneInstaller } catch { $State.ExitCode = 1 $State.FailureMessage = $_.Exception.GetBaseException().Message } finally { Remove-InstallerScratch Release-InstallerLock } $global:LASTEXITCODE = $InvocationContext.OriginalLastExitCode if ($State.ExitCode -ne 0) { throw [InvalidOperationException]::new(("ERROR {0}" -f $State.FailureMessage)) } } ([pscustomobject]@{ Arguments = @($args) FileInvocation = -not [string]::IsNullOrEmpty($PSCommandPath) OriginalLastExitCode = $global:LASTEXITCODE })