diff --git a/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 b/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 index 34a2618..fe7e101 100644 --- a/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 +++ b/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 @@ -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 } }