[CmdletBinding()] param( [string]$DsViewPath = (Join-Path $env:ProgramFiles 'DSView'), [switch]$CheckOnly ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2 # Resolve source paths relative to templates, never the caller working directory. # Standalone DSView and SETGUI use the same adapters and parsing cores. $templateRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..')) $targetRoot = [IO.Path]::GetFullPath($DsViewPath) $decoderRoot = Join-Path $targetRoot 'decoders' # Use .NET for compatibility with PowerShell lacking Get-FileHash. # Release both stream and hashing provider even if reading fails. function Get-ContentHash([string]$Path) { $algorithm = [Security.Cryptography.SHA256]::Create() $stream = [IO.File]::OpenRead($Path) try { return [BitConverter]::ToString($algorithm.ComputeHash($stream)) } finally { $stream.Dispose() $algorithm.Dispose() } } # Check all existing ancestors: a junction under decoders/common could # redirect a local-looking update outside the chosen installation. function Assert-PlainPath([string]$Path) { $cursor = [IO.Path]::GetFullPath($Path) $first = $true while ($cursor) { if (Test-Path -LiteralPath $cursor) { $item = Get-Item -LiteralPath $cursor -Force if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Symbolic links and junctions are not supported: $cursor" } if (-not $first -and -not $item.PSIsContainer) { throw "Expected a parent directory: $cursor" } } $parent = [IO.Path]::GetDirectoryName($cursor) if ($parent -eq $cursor) { break } $cursor = $parent $first = $false } } Assert-PlainPath $targetRoot $exe = Join-Path $targetRoot 'DSView.exe' if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw "DSView.exe not found in '$targetRoot'. Pass -DsViewPath with the installed DSView directory." } if (-not (Test-Path -LiteralPath (Join-Path $decoderRoot 'common') -PathType Container)) { throw "DSView decoders/common is missing. Select a complete DSView installation." } # An explicit allowlist excludes tests, bytecode and unrelated modules. # The private common/setgui_decoders layout matches the embedded build. $files = @() foreach ($name in @('gate_driver_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart')) { foreach ($leaf in @('__init__.py', 'pd.py')) { $files += [pscustomobject]@{ Source = Join-Path $PSScriptRoot "decoders/$name/$leaf" Relative = "$name/$leaf" } } } foreach ($name in @('__init__', 'gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart')) { $files += [pscustomobject]@{ Source = Join-Path $templateRoot "python/logic_analyzer/decoders/$name.py" Relative = "common/setgui_decoders/$name.py" } } # Validate the entire payload and destination before changing any decoder. foreach ($file in $files) { if (-not (Test-Path -LiteralPath $file.Source -PathType Leaf)) { throw "Missing templates source: $($file.Source). Initialize the templates submodule." } $destination = Join-Path $decoderRoot $file.Relative Assert-PlainPath $destination if (Test-Path -LiteralPath $destination -PathType Container) { throw "Expected a file, found a directory: $destination" } if ([IO.Path]::GetFullPath($file.Source) -eq [IO.Path]::GetFullPath($destination)) { throw 'The installation destination must differ from the templates source.' } } # Read-only validation does not create backups or check elevated write access. # Real installation performs its own write-access check before copying. if ($CheckOnly) { Write-Output "Validated $($files.Count) files for $targetRoot. No files changed." return } # A mapped/running Windows EXE cannot be opened for writing. Never terminate DSView. try { $handle = [IO.File]::Open($exe, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read) $handle.Dispose() } catch { throw "Close DSView and check write permissions for '$targetRoot'. For Program Files, run PowerShell as administrator. $($_.Exception.Message)" } # Keep each previous version outside decoder discovery directories. # Retain backups after success for manual recovery, not only after errors. $backup = Join-Path $targetRoot ('set-decoder-backups/' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N')) Assert-PlainPath $backup # Back up all existing files before writing the first replacement. foreach ($file in $files) { $destination = Join-Path $decoderRoot $file.Relative if (Test-Path -LiteralPath $destination -PathType Leaf) { $saved = Join-Path $backup $file.Relative [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($saved)) | Out-Null Copy-Item -LiteralPath $destination -Destination $saved } } # Track files before copying: a failed copy may leave a partial destination. # Rollback must include that file as well as completed replacements. $attempted = @() try { foreach ($file in $files) { $destination = Join-Path $decoderRoot $file.Relative [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null $attempted += $file Copy-Item -LiteralPath $file.Source -Destination $destination -Force if ((Get-ContentHash $file.Source) -ne (Get-ContentHash $destination)) { throw "Copy verification failed: $destination" } } } catch { $failure = $_ # Restore overwritten files and remove only new files from our allowlist. # Never recursively delete decoder folders or remove unrelated analyzers. foreach ($file in $attempted) { $destination = Join-Path $decoderRoot $file.Relative $saved = Join-Path $backup $file.Relative try { if (Test-Path -LiteralPath $saved -PathType Leaf) { Copy-Item -LiteralPath $saved -Destination $destination -Force } elseif (Test-Path -LiteralPath $destination -PathType Leaf) { Remove-Item -LiteralPath $destination -Force } } catch { Write-Warning "Could not restore $destination. Retained backup: $backup" } } throw $failure } Write-Output "Installed: IGBT 1SP/1SD, SET UART, SET CAN, PM35 UART in $decoderRoot" if (Test-Path -LiteralPath $backup) { Write-Output "Previous files retained in: $backup" } Write-Output 'Restart DSView. Add UART/CAN first, then stack the corresponding SET decoder. No external Python is required.'