Finish improvements.
This commit is contained in:
@@ -93,25 +93,23 @@ param(
|
||||
[string]$Module = 'ECU',
|
||||
[string]$VIN,
|
||||
[string]$Notes,
|
||||
[string]$Technician
|
||||
[string]$Technician,
|
||||
[switch]$PromptForRestart,
|
||||
[switch]$AllowAutomaticRestart
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$moduleRoot = Join-Path $PSScriptRoot 'Modules'
|
||||
Import-Module (Join-Path $moduleRoot 'Configuration.psm1') -Force
|
||||
Import-Module (Join-Path $moduleRoot 'Checkpoint.psm1') -Force
|
||||
Import-Module (Join-Path $moduleRoot 'Download.psm1') -Force
|
||||
Import-Module (Join-Path $moduleRoot 'Workspace.psm1') -Force
|
||||
|
||||
function Import-SetupConfiguration {
|
||||
param([Parameter(Mandatory)][string]$Name)
|
||||
$path = Join-Path $PSScriptRoot (Join-Path 'Config' $Name)
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
throw "Required configuration file is missing: $path"
|
||||
}
|
||||
try {
|
||||
return Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
throw "Configuration file is invalid: $path. $($_.Exception.Message)"
|
||||
}
|
||||
return Import-WorkstationJsonConfiguration -BasePath $PSScriptRoot -Name $Name
|
||||
}
|
||||
|
||||
$Configuration = @{
|
||||
@@ -263,8 +261,7 @@ function Test-SetupPhaseCompleted {
|
||||
if (-not $ResumeFromCheckpoint -or -not (Test-Path -LiteralPath $CheckpointFile -PathType Leaf)) {
|
||||
return $false
|
||||
}
|
||||
$checkpoint = Load-ExecutionCheckpoint
|
||||
return @($checkpoint.CompletedPhases | Where-Object { $_.Phase -eq $Phase -and $_.Status -eq 'Completed' }).Count -gt 0
|
||||
return Test-WorkstationCheckpointPhase -Path $CheckpointFile -Phase $Phase
|
||||
}
|
||||
|
||||
function Detect-PendingRestart {
|
||||
@@ -279,6 +276,24 @@ function Complete-SetupPhase {
|
||||
Save-ExecutionCheckpoint -Phase $Phase -Status 'Completed' -Details $Details
|
||||
}
|
||||
|
||||
function Request-SetupRestart {
|
||||
param([Parameter(Mandatory)][string]$Reason)
|
||||
Save-ExecutionCheckpoint -Phase 'RestartRequired' -Status 'AwaitingRestart' -Details $Reason
|
||||
if ($AllowAutomaticRestart) {
|
||||
Write-Log "Restarting Windows automatically: $Reason" 'WARN'
|
||||
Restart-Computer -Force
|
||||
return
|
||||
}
|
||||
if ($PromptForRestart) {
|
||||
$answer = Read-Host "$Reason Restart now? (Y/N)"
|
||||
if ($answer -match '^(?i)y(?:es)?$') {
|
||||
Restart-Computer -Force
|
||||
return
|
||||
}
|
||||
}
|
||||
Write-Log "Restart required before continuing. Resume with -ResumeFromCheckpoint after restarting." 'WARN'
|
||||
}
|
||||
|
||||
function Invoke-Safe {
|
||||
param([string]$Category, [string]$Name, [scriptblock]$Action)
|
||||
try {
|
||||
@@ -529,11 +544,7 @@ function Set-WindowsProfileConfiguration {
|
||||
|
||||
function ConvertTo-ProjectSegment {
|
||||
param([Parameter(Mandatory)][string]$Value, [Parameter(Mandatory)][string]$Name)
|
||||
$segment = $Value.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($segment) -or $segment -match '[\\/:*?"<>|]' -or $segment -eq '.' -or $segment -eq '..') {
|
||||
throw "$Name contains an invalid path segment: '$Value'."
|
||||
}
|
||||
return ($segment -replace '\s+', '_')
|
||||
return ConvertTo-WorkstationProjectSegment -Value $Value -Name $Name
|
||||
}
|
||||
|
||||
function New-AutomotiveProject {
|
||||
@@ -726,13 +737,10 @@ function Test-DownloadUri {
|
||||
[Parameter(Mandatory)][string]$Uri,
|
||||
[switch]$AllowInsecureHttp
|
||||
)
|
||||
$parsedUri = [Uri]$Uri
|
||||
if ($parsedUri.Scheme -eq 'https') { return }
|
||||
if ($parsedUri.Scheme -eq 'http' -and $AllowInsecureHttp) {
|
||||
Test-WorkstationDownloadUri -Uri $Uri -AllowInsecureHttp:$AllowInsecureHttp
|
||||
if (([Uri]$Uri).Scheme -eq 'http' -and $AllowInsecureHttp) {
|
||||
Write-Log "Using explicitly approved HTTP download: $Uri" 'WARN'
|
||||
return
|
||||
}
|
||||
throw "Insecure download blocked: $Uri. Use HTTPS or explicitly approve this documented vendor exception."
|
||||
}
|
||||
|
||||
function Test-DownloadedFile {
|
||||
@@ -1046,6 +1054,13 @@ if ($EffectiveMode -eq 'Audit' -or $EffectiveMode -eq 'HealthCheck') {
|
||||
return
|
||||
}
|
||||
|
||||
if ($EffectiveMode -eq 'Inventory') {
|
||||
Invoke-Safe 'Inventory' 'Export workstation inventory' { Export-WorkstationInventory } | Out-Null
|
||||
$Results | Export-Csv -LiteralPath $SummaryFile -NoTypeInformation -Encoding UTF8
|
||||
if ($EnableTranscript) { Stop-Transcript | Out-Null }
|
||||
return
|
||||
}
|
||||
|
||||
if ($EffectiveMode -eq 'Plan') {
|
||||
$windowsPlan = @(Get-WindowsConfigurationPlan)
|
||||
$planFile = Join-Path $Paths.Logs ("{0}_Plan_{1}.csv" -f $Profile.Folder, $TimeStamp)
|
||||
@@ -1084,6 +1099,18 @@ if ($EffectiveMode -eq 'UpdatePortable') {
|
||||
$InstallOptionalApps = $false
|
||||
}
|
||||
|
||||
if ($EffectiveMode -eq 'Repair') {
|
||||
if (-not (Test-Path -LiteralPath $CheckpointFile -PathType Leaf)) {
|
||||
throw "Repair requires an existing checkpoint: $CheckpointFile"
|
||||
}
|
||||
$ResumeFromCheckpoint = $true
|
||||
$repairCheckpoint = Load-ExecutionCheckpoint
|
||||
$ExecutionId = $repairCheckpoint.ExecutionId
|
||||
$ExecutionState.StartTime = $repairCheckpoint.StartTime
|
||||
$ExecutionState.CompletedPhases = @($repairCheckpoint.CompletedPhases)
|
||||
Write-Log 'Repair mode will revalidate completed phases and retry failed or incomplete work.' 'INFO'
|
||||
}
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
throw 'Run this script from Windows PowerShell or PowerShell as Administrator.'
|
||||
}
|
||||
@@ -1120,20 +1147,21 @@ if ($RestoreConfiguration) {
|
||||
return
|
||||
}
|
||||
|
||||
# 1. Create local structure.
|
||||
$Paths.Values | ForEach-Object {
|
||||
if (-not (Test-SetupPhaseCompleted -Phase 'LocalStructure')) {
|
||||
# 1. Create local structure.
|
||||
$Paths.Values | ForEach-Object {
|
||||
$null = New-Item -ItemType Directory -Path $_ -Force
|
||||
}
|
||||
}
|
||||
|
||||
$Subfolders = @($Configuration.Folders.Local)
|
||||
foreach ($relative in $Subfolders) {
|
||||
$Subfolders = @($Configuration.Folders.Local)
|
||||
foreach ($relative in $Subfolders) {
|
||||
$path = Join-Path $LocalRoot $relative
|
||||
$null = New-Item -ItemType Directory -Path $path -Force
|
||||
}
|
||||
Add-Result 'Folders' 'Local automotive structure' 'Success' $LocalRoot
|
||||
Complete-SetupPhase -Phase 'LocalStructure' -Details $LocalRoot
|
||||
}
|
||||
Add-Result 'Folders' 'Local automotive structure' 'Success' $LocalRoot
|
||||
Complete-SetupPhase -Phase 'LocalStructure' -Details $LocalRoot
|
||||
|
||||
Invoke-Safe 'Configuration' 'Workstation profile metadata' {
|
||||
Invoke-Safe 'Configuration' 'Workstation profile metadata' {
|
||||
@"
|
||||
Profile: $WorkstationProfile
|
||||
Focus: $($Profile.Focus)
|
||||
@@ -1145,7 +1173,8 @@ Shared installers: $ProfileInstallerRoot
|
||||
This Windows installation uses only C:, S: (AUTO_DATA), and P: (PORTABLE_APPS).
|
||||
Do not assign drive letters to other Windows installations from this workstation.
|
||||
"@ | Set-Content -LiteralPath (Join-Path $Paths.Config 'Workstation-Profile.txt') -Encoding UTF8
|
||||
} | Out-Null
|
||||
} | Out-Null
|
||||
}
|
||||
|
||||
if ($BackupConfiguration) {
|
||||
Invoke-Safe 'Recovery' 'Backup workstation configuration' {
|
||||
@@ -1154,31 +1183,32 @@ if ($BackupConfiguration) {
|
||||
} | Out-Null
|
||||
}
|
||||
|
||||
# 2. Configure both shared partitions and expose them inside C:\Automotive.
|
||||
# S: holds data shared by all three Windows installations.
|
||||
$SharedDataFolders = @($Configuration.Folders.SharedData)
|
||||
if (-not (Test-SetupPhaseCompleted -Phase 'SharedPartitions')) {
|
||||
# 2. Configure both shared partitions and expose them inside C:\Automotive.
|
||||
# S: holds data shared by all three Windows installations.
|
||||
$SharedDataFolders = @($Configuration.Folders.SharedData)
|
||||
|
||||
# P: holds portable applications, shared tool configuration, installers, and manifests.
|
||||
$SharedPortableFolders = @($Configuration.Folders.SharedPortable)
|
||||
$SharedPortableFolders = @($Configuration.Folders.SharedPortable)
|
||||
|
||||
Invoke-Safe 'SharedPartition' 'Configure S: shared automotive data' {
|
||||
Invoke-Safe 'SharedPartition' 'Configure S: shared automotive data' {
|
||||
Initialize-SharedPartition -Drive 'S:' -ExpectedLabel $SharedDataLabel -Folders $SharedDataFolders
|
||||
} | Out-Null
|
||||
} | Out-Null
|
||||
|
||||
Invoke-Safe 'SharedPartition' 'Configure P: shared portable applications' {
|
||||
Invoke-Safe 'SharedPartition' 'Configure P: shared portable applications' {
|
||||
Initialize-SharedPartition -Drive 'P:' -ExpectedLabel $SharedPortableLabel -Folders $SharedPortableFolders
|
||||
} | Out-Null
|
||||
} | Out-Null
|
||||
|
||||
if ($CreateSharedLinks) {
|
||||
if ($CreateSharedLinks) {
|
||||
Invoke-Safe 'Link' 'Link C:\Automotive\SharedData to S:' {
|
||||
New-DirectoryLink -Link (Join-Path $LocalRoot 'SharedData') -Target $SharedDataRoot
|
||||
} | Out-Null
|
||||
Invoke-Safe 'Link' 'Link C:\Automotive\SharedPortableApps to P:' {
|
||||
New-DirectoryLink -Link (Join-Path $LocalRoot 'SharedPortableApps') -Target $SharedPortableRoot
|
||||
} | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
if ((Test-Path -LiteralPath $SharedPortableRoot) -and (Test-Path -LiteralPath $SharedDataRoot)) {
|
||||
if ((Test-Path -LiteralPath $SharedPortableRoot) -and (Test-Path -LiteralPath $SharedDataRoot)) {
|
||||
Invoke-Safe 'AutomotiveTooling' 'Commercial automotive tool workspaces' {
|
||||
foreach ($toolName in $Configuration.Shortcuts.CommercialAutomotiveTools) {
|
||||
$portableToolRoot = Join-Path $SharedPortableRoot "Automotive\CommercialTools\$toolName"
|
||||
@@ -1186,9 +1216,10 @@ if ((Test-Path -LiteralPath $SharedPortableRoot) -and (Test-Path -LiteralPath $S
|
||||
$null = New-Item -ItemType Directory -Path $portableToolRoot, (Join-Path $portableToolRoot 'Config'), (Join-Path $portableToolRoot 'Shortcuts'), $dataToolRoot -Force
|
||||
}
|
||||
} | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Complete-SetupPhase -Phase 'SharedPartitions' -Details "DataRoot=$SharedDataRoot; PortableRoot=$SharedPortableRoot"
|
||||
Complete-SetupPhase -Phase 'SharedPartitions' -Details "DataRoot=$SharedDataRoot; PortableRoot=$SharedPortableRoot"
|
||||
}
|
||||
|
||||
# 3. Portable applications are installed after WinGet so archive prerequisites are available.
|
||||
|
||||
@@ -1205,6 +1236,7 @@ $OptionalPackages = if ($catalogPackages.Count -gt 0) {
|
||||
@($Configuration.OptionalApps.Packages)
|
||||
}
|
||||
|
||||
if (-not (Test-SetupPhaseCompleted -Phase 'WinGetPackages')) {
|
||||
if (-not $SkipWinget) {
|
||||
Invoke-Safe 'WinGet' 'Refresh package sources' {
|
||||
Update-WingetSources
|
||||
@@ -1219,9 +1251,15 @@ if (-not $SkipWinget) {
|
||||
}
|
||||
}
|
||||
|
||||
if (Detect-PendingRestart) {
|
||||
Request-SetupRestart -Reason 'WinGet reports that Windows needs to restart before setup can continue.'
|
||||
if ($AllowAutomaticRestart -or $PromptForRestart) { return }
|
||||
}
|
||||
Complete-SetupPhase -Phase 'WinGetPackages' -Details "Skipped=$SkipWinget; Optional=$InstallOptionalApps"
|
||||
}
|
||||
|
||||
# 5. Download, install/update, validate, and configure portable applications.
|
||||
if (-not (Test-SetupPhaseCompleted -Phase 'PortableApplications')) {
|
||||
if (-not $SkipDownloads) {
|
||||
$PortableRoot = Get-PortableInstallRoot
|
||||
$PortableDefinitions = [System.Collections.Generic.List[object]]::new()
|
||||
@@ -1889,9 +1927,11 @@ according to the vendor instructions. Do not use a third-party mirror.
|
||||
}
|
||||
|
||||
Complete-SetupPhase -Phase 'PortableApplications' -Details "Skipped=$SkipDownloads"
|
||||
}
|
||||
|
||||
# 6. Convenience shortcuts.
|
||||
|
||||
if (-not (Test-SetupPhaseCompleted -Phase 'Shortcuts')) {
|
||||
if (-not $SkipShortcuts) {
|
||||
$desktopGroup = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Automotive Workstation'
|
||||
$startGroup = Join-Path ([Environment]::GetFolderPath('Programs')) 'Automotive Workstation'
|
||||
@@ -1945,6 +1985,7 @@ if (-not $SkipShortcuts) {
|
||||
}
|
||||
|
||||
Complete-SetupPhase -Phase 'Shortcuts' -Details "Skipped=$SkipShortcuts"
|
||||
}
|
||||
|
||||
# 7. Environment and safety optimizations.
|
||||
Invoke-Safe 'Configuration' 'Local TEMP workspace' {
|
||||
@@ -1982,6 +2023,7 @@ Invoke-Safe 'Report' 'Export workstation inventory' { Export-WorkstationInventor
|
||||
$Results | Export-Csv -LiteralPath $SummaryFile -NoTypeInformation -Encoding UTF8
|
||||
Invoke-Safe 'Report' 'Export setup report' { Export-SetupReport } | Out-Null
|
||||
if (Detect-PendingRestart) {
|
||||
Request-SetupRestart -Reason 'Setup completed but Windows reports a pending restart.'
|
||||
Save-ExecutionCheckpoint -Phase 'Complete' -Status 'AwaitingRestart' -Details 'Windows reports a pending restart.'
|
||||
} else {
|
||||
Save-ExecutionCheckpoint -Phase 'Complete' -Status 'Completed' -Details 'Setup flow completed.'
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Deployment
|
||||
|
||||
## Build a release package
|
||||
|
||||
Run from an elevated Windows PowerShell session only when the target machine will be configured. Packaging itself does not require elevation.
|
||||
|
||||
```powershell
|
||||
.\Deploy\Package-WorkstationSetup.ps1 -Version '2026.09.09' -IncludeTests
|
||||
```
|
||||
|
||||
The package is written to `Releases\\Automotive-Workstation-Setup-<version>.zip` and includes:
|
||||
|
||||
- Main setup script
|
||||
- JSON configuration files
|
||||
- User and troubleshooting documentation
|
||||
- Validation checklist
|
||||
- SHA256 release manifests
|
||||
- Tests when `-IncludeTests` is specified
|
||||
|
||||
Do not distribute the temporary staging directory. Verify the generated archive hash from the accompanying `.release.json` file before copying it to a deployment share.
|
||||
|
||||
## Rollout sequence
|
||||
|
||||
1. Verify the release archive SHA256.
|
||||
2. Extract it to a versioned folder on the deployment share.
|
||||
3. Boot the intended isolated Windows installation.
|
||||
4. Confirm only the intended `C:`, `S:`, and `P:` volumes are mounted.
|
||||
5. Run `-Mode Plan` and review the Windows/application plan.
|
||||
6. Run `-Mode Audit` and save the generated reports.
|
||||
7. Create a configuration backup.
|
||||
8. Run `-Mode Apply` from an elevated Windows PowerShell window.
|
||||
9. Restart when the checkpoint reports `AwaitingRestart`.
|
||||
10. Resume with `-ResumeFromCheckpoint`.
|
||||
11. Run `-Mode HealthCheck` and review the setup report.
|
||||
12. Install licensed OEM suites and communication drivers separately through vendor installers.
|
||||
|
||||
## Production gates
|
||||
|
||||
- All Pester tests pass.
|
||||
- The archive hash is verified.
|
||||
- The selected profile matches the isolated Windows installation.
|
||||
- OEM software licensing and driver compatibility are confirmed.
|
||||
- Original ECU, EEPROM, and coding data have independent backups.
|
||||
- Elevated Apply testing has completed on a clean test workstation before production rollout.
|
||||
@@ -0,0 +1,57 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$OutputRoot = (Join-Path (Split-Path $PSScriptRoot -Parent) 'Releases'),
|
||||
[string]$Version = (Get-Date -Format 'yyyy.MM.dd'),
|
||||
[switch]$IncludeTests
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sourceRoot = Split-Path $PSScriptRoot -Parent
|
||||
$packageName = "Automotive-Workstation-Setup-$Version"
|
||||
$stagingRoot = Join-Path ([IO.Path]::GetTempPath()) $packageName
|
||||
$packageRoot = Join-Path $stagingRoot 'Workstation-Setup'
|
||||
|
||||
Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Path $packageRoot, $OutputRoot -Force | Out-Null
|
||||
|
||||
$files = @(
|
||||
'Automotive-Workstation-Setup.ps1',
|
||||
'COMPLETION-SUMMARY.md',
|
||||
'USER-GUIDE.md',
|
||||
'TROUBLESHOOTING.md',
|
||||
'VALIDATION-CHECKLIST.md',
|
||||
'Config'
|
||||
)
|
||||
if ($IncludeTests) { $files += 'Tests' }
|
||||
|
||||
foreach ($relativePath in $files) {
|
||||
$sourcePath = Join-Path $sourceRoot $relativePath
|
||||
if (-not (Test-Path -LiteralPath $sourcePath)) { throw "Release input is missing: $relativePath" }
|
||||
Copy-Item -LiteralPath $sourcePath -Destination (Join-Path $packageRoot $relativePath) -Recurse -Force
|
||||
}
|
||||
|
||||
$manifest = Get-ChildItem -LiteralPath $packageRoot -File -Recurse | ForEach-Object {
|
||||
$hash = Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256
|
||||
[pscustomobject]@{
|
||||
Path = $_.FullName.Substring($packageRoot.Length).TrimStart('\\')
|
||||
SizeBytes = $_.Length
|
||||
SHA256 = $hash.Hash
|
||||
}
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $packageRoot 'RELEASE-MANIFEST.json') -Encoding UTF8
|
||||
$manifest | Export-Csv -LiteralPath (Join-Path $packageRoot 'RELEASE-MANIFEST.csv') -NoTypeInformation -Encoding UTF8
|
||||
|
||||
$archivePath = Join-Path $OutputRoot "$packageName.zip"
|
||||
Compress-Archive -Path (Join-Path $packageRoot '*') -DestinationPath $archivePath -Force
|
||||
$archiveHash = Get-FileHash -LiteralPath $archivePath -Algorithm SHA256
|
||||
[pscustomobject]@{
|
||||
Package = $packageName
|
||||
Archive = $archivePath
|
||||
SHA256 = $archiveHash.Hash
|
||||
IncludeTests = [bool]$IncludeTests
|
||||
Created = Get-Date
|
||||
} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $OutputRoot "$packageName.release.json") -Encoding UTF8
|
||||
|
||||
Remove-Item -LiteralPath $stagingRoot -Recurse -Force
|
||||
Write-Output $archivePath
|
||||
@@ -0,0 +1,14 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Test-WorkstationCheckpointPhase {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$Phase
|
||||
)
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
|
||||
$state = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
|
||||
return @($state.CompletedPhases | Where-Object { $_.Phase -eq $Phase -and $_.Status -eq 'Completed' }).Count -gt 0
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Test-WorkstationCheckpointPhase
|
||||
@@ -0,0 +1,21 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Import-WorkstationJsonConfiguration {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$BasePath,
|
||||
[Parameter(Mandatory)][string]$Name
|
||||
)
|
||||
$path = Join-Path $BasePath (Join-Path 'Config' $Name)
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
throw "Required configuration file is missing: $path"
|
||||
}
|
||||
try {
|
||||
Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
throw "Configuration file is invalid: $path. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Import-WorkstationJsonConfiguration
|
||||
@@ -0,0 +1,31 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Test-WorkstationDownloadUri {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Uri,
|
||||
[switch]$AllowInsecureHttp
|
||||
)
|
||||
$parsedUri = [Uri]$Uri
|
||||
if ($parsedUri.Scheme -eq 'https') { return }
|
||||
if ($parsedUri.Scheme -eq 'http' -and $AllowInsecureHttp) { return }
|
||||
throw "Insecure download blocked: $Uri"
|
||||
}
|
||||
|
||||
function Test-WorkstationDownloadedFile {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][int64]$MinimumBytes,
|
||||
[string]$ExpectedSHA256
|
||||
)
|
||||
$file = Get-Item -LiteralPath $Path -ErrorAction Stop
|
||||
if ($file.Length -lt $MinimumBytes) { throw "Downloaded file is smaller than the required minimum: $Path" }
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExpectedSHA256)) {
|
||||
$actual = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
|
||||
if ($actual -ne $ExpectedSHA256.ToUpperInvariant()) { throw "SHA256 mismatch for $Path" }
|
||||
}
|
||||
return $file
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Test-WorkstationDownloadUri, Test-WorkstationDownloadedFile
|
||||
@@ -0,0 +1,16 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function ConvertTo-WorkstationProjectSegment {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Value,
|
||||
[Parameter(Mandatory)][string]$Name
|
||||
)
|
||||
$segment = $Value.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($segment) -or $segment -match '[\\/:*?"<>|]' -or $segment -in @('.', '..')) {
|
||||
throw "$Name contains an invalid path segment: '$Value'."
|
||||
}
|
||||
return ($segment -replace '\s+', '_')
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function ConvertTo-WorkstationProjectSegment
|
||||
Reference in New Issue
Block a user