manifest engine: resolve installer paths without the PS provider

Join-Path routes a drive-qualified path through the PowerShell provider, so
it returns null when that drive has gone away mid-cycle, and every consumer
then bound the null straight into Test-Path -LiteralPath and crashed the
entry with a message that names neither the entry nor the path.

Replace the six Join-Path $InstallerRoot sites with Join-InstallerPath, which
does plain string math, and have all six callers treat a null resolve as
"not found" and log it. Only the PS1 branch had a guard before, and it
covered a null Script value, not a null resolved path.

The helper deliberately avoids [IO.Path]::Combine: its separator and
rooted-path rules follow the host platform, so behaviour cannot be verified
off-Windows. It also rejects rooted values ("\x", "D:\x", "\\server\share"),
which Combine would have resolved outside the share root. Checked all 36
path values across the share manifests: none are rooted today.

Behaviour verified against the real manifest values (forward slashes,
backslashes, trailing-slash root, null, whitespace, rooted, UNC).
This commit is contained in:
cproudlock
2026-08-03 17:23:00 -04:00
parent 66c24b5d59
commit 27e76467a8

View File

@@ -241,6 +241,24 @@ function Test-AppInstalled {
# ---------------------------------------------------------------------------
# Action dispatch
# ---------------------------------------------------------------------------
function Join-InstallerPath {
# Plain string math, NOT Join-Path. Join-Path resolves the drive qualifier
# through the PS provider and emits NOTHING (null) if that drive vanished
# mid-cycle - the null then binds into Test-Path -LiteralPath and throws
# "Cannot bind argument to parameter 'LiteralPath' because it is null".
# [IO.Path]::Combine is no good either: its separator and rooted-path rules
# follow the host platform, so it cannot be validated off-Windows.
# Returns $null when the entry is unusable; every caller treats null as
# "not found" and logs, instead of crashing the entry.
param([string]$Root, [string]$Rel)
if ([string]::IsNullOrWhiteSpace($Root) -or [string]::IsNullOrWhiteSpace($Rel)) { return $null }
$clean = ($Rel -replace '/', '\').Trim()
# Reject rooted values ('\x', 'D:\x', '\\server\share'): a manifest entry
# must resolve UNDER the share root, never escape it.
if ($clean -match '^(\\|[A-Za-z]:)') { return $null }
return ($Root.TrimEnd('\') + '\' + $clean)
}
function Invoke-InstallerAction {
param($App)
@@ -251,8 +269,8 @@ function Invoke-InstallerAction {
switch ($App.Type) {
'MSI' {
$installerPath = Join-Path $InstallerRoot $App.Installer
if (-not (Test-Path -LiteralPath $installerPath)) {
$installerPath = Join-InstallerPath $InstallerRoot $App.Installer
if ([string]::IsNullOrWhiteSpace($installerPath) -or -not (Test-Path -LiteralPath $installerPath)) {
Write-InstallLog " MSI not found: $installerPath" 'ERROR'
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
}
@@ -270,8 +288,8 @@ function Invoke-InstallerAction {
return [pscustomobject]@{ ExitCode = $proc.ExitCode; LogRef = $msiLog }
}
'EXE' {
$installerPath = Join-Path $InstallerRoot $App.Installer
if (-not (Test-Path -LiteralPath $installerPath)) {
$installerPath = Join-InstallerPath $InstallerRoot $App.Installer
if ([string]::IsNullOrWhiteSpace($installerPath) -or -not (Test-Path -LiteralPath $installerPath)) {
Write-InstallLog " EXE not found: $installerPath" 'ERROR'
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
}
@@ -346,8 +364,8 @@ function Invoke-InstallerAction {
return [pscustomobject]@{ ExitCode = $exitCode; LogRef = $App.LogFile }
}
{ $_ -eq 'CMD' -or $_ -eq 'BAT' } {
$installerPath = Join-Path $InstallerRoot $App.Installer
if (-not (Test-Path -LiteralPath $installerPath)) {
$installerPath = Join-InstallerPath $InstallerRoot $App.Installer
if ([string]::IsNullOrWhiteSpace($installerPath) -or -not (Test-Path -LiteralPath $installerPath)) {
Write-InstallLog " CMD/BAT not found: $installerPath" 'ERROR'
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
}
@@ -369,8 +387,8 @@ function Invoke-InstallerAction {
Write-InstallLog (" PS1 entry '{0}' has no Script/Installer value (Script={1}, Installer={2}) - skipping" -f $App.Name, $App.Script, $App.Installer) 'ERROR'
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
}
$scriptPath = Join-Path $InstallerRoot $rel
if (-not (Test-Path -LiteralPath $scriptPath)) {
$scriptPath = Join-InstallerPath $InstallerRoot $rel
if ([string]::IsNullOrWhiteSpace($scriptPath) -or -not (Test-Path -LiteralPath $scriptPath)) {
Write-InstallLog " PS1 not found: $scriptPath (from rel '$rel')" 'ERROR'
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
}
@@ -384,8 +402,8 @@ function Invoke-InstallerAction {
return [pscustomobject]@{ ExitCode = $proc.ExitCode; LogRef = $null }
}
'INF' {
$infPath = Join-Path $InstallerRoot $App.Installer
if (-not (Test-Path -LiteralPath $infPath)) {
$infPath = Join-InstallerPath $InstallerRoot $App.Installer
if ([string]::IsNullOrWhiteSpace($infPath) -or -not (Test-Path -LiteralPath $infPath)) {
Write-InstallLog " INF not found: $infPath" 'ERROR'
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
}
@@ -397,8 +415,8 @@ function Invoke-InstallerAction {
}
'File' {
# Copy a file from the share (configs/*) to an absolute on-PC path.
$source = Join-Path $InstallerRoot $App.Source
if (-not (Test-Path -LiteralPath $source)) {
$source = Join-InstallerPath $InstallerRoot $App.Source
if ([string]::IsNullOrWhiteSpace($source) -or -not (Test-Path -LiteralPath $source)) {
Write-InstallLog " File source not found: $source" 'ERROR'
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
}