function Resolve-ShadowFile { param([Parameter(Mandatory = $true)][string]$Path) $item = Get-Item -LiteralPath $Path -ErrorAction Stop if ($item.PSIsContainer) { throw "FILE_REQUIRED:$Path" } return $item.FullName } function Resolve-ShadowDirectory { param( [Parameter(Mandatory = $true)][string]$Path, [switch]$Create ) if ($Create -and -not (Test-Path -LiteralPath $Path)) { New-Item -ItemType Directory -Path $Path -Force | Out-Null } $item = Get-Item -LiteralPath $Path -ErrorAction Stop if (-not $item.PSIsContainer) { throw "DIRECTORY_REQUIRED:$Path" } return $item.FullName } function Write-ShadowJson { param( [Parameter(Mandatory = $true)]$Value, [Parameter(Mandatory = $true)][string]$Path, [int]$Depth = 12 ) $parent = Split-Path -Parent ([IO.Path]::GetFullPath($Path)) if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } $Value | ConvertTo-Json -Depth $Depth | Set-Content -LiteralPath $Path ` -Encoding UTF8 } function Read-ShadowJson { param([Parameter(Mandatory = $true)][string]$Path) $resolved = Resolve-ShadowFile $Path return Get-Content -LiteralPath $resolved -Raw | ConvertFrom-Json } function Get-ShadowSha256 { param([Parameter(Mandatory = $true)][string]$Path) return (Get-FileHash -LiteralPath (Resolve-ShadowFile $Path) ` -Algorithm SHA256).Hash.ToLowerInvariant() } function Get-ShadowCommandLine { param([Parameter(Mandatory = $true)][int]$ProcessId) try { $row = Get-CimInstance Win32_Process -Filter "ProcessId=$ProcessId" ` -ErrorAction Stop return $row.CommandLine } catch { return $null } } function Get-ShadowListeningPorts { param([Parameter(Mandatory = $true)][int]$ProcessId) try { return @(Get-NetTCPConnection -OwningProcess $ProcessId -State Listen ` -ErrorAction Stop | Sort-Object LocalPort | ForEach-Object { [pscustomobject]@{ address = $_.LocalAddress port = [int]$_.LocalPort state = $_.State.ToString() } }) } catch { return @() } } function Test-ShadowPathWithin { param( [Parameter(Mandatory = $true)][string]$Candidate, [Parameter(Mandatory = $true)][string]$Directory ) $candidatePath = [IO.Path]::GetFullPath($Candidate) $directoryPath = [IO.Path]::GetFullPath($Directory).TrimEnd('\', '/') if ($candidatePath.Equals($directoryPath, [StringComparison]::OrdinalIgnoreCase)) { return $true } $prefix = $directoryPath + [IO.Path]::DirectorySeparatorChar return $candidatePath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) } function Get-ShadowFreeBytes { param([Parameter(Mandatory = $true)][string]$Path) $root = [IO.Path]::GetPathRoot([IO.Path]::GetFullPath($Path)) return ([IO.DriveInfo]::new($root)).AvailableFreeSpace } function Resolve-ShadowScalarPlaceholder { param([AllowNull()][string]$Value) if ($null -eq $Value) { return $null } if ($Value -match '^\$\{([A-Za-z_][A-Za-z0-9_]*):(.*)\}$') { $environmentValue = [Environment]::GetEnvironmentVariable($matches[1]) return if ($null -eq $environmentValue) { $matches[2] } ` else { $environmentValue } } return $Value } function Read-ShadowYamlScalars { param([Parameter(Mandatory = $true)][string]$Path) $resolved = Resolve-ShadowFile $Path $parents = @{} $values = @{} foreach ($line in Get-Content -LiteralPath $resolved) { if ($line -match '^\s*(#.*)?$') { continue } if ($line -notmatch '^(\s*)([A-Za-z0-9_-]+):(?:\s*(.*))?$') { continue } $indent = $matches[1].Length $key = $matches[2] $raw = if ($null -eq $matches[3]) { '' } else { $matches[3].Trim() } foreach ($level in @($parents.Keys | Where-Object { [int]$_ -ge $indent })) { $parents.Remove($level) } if ([string]::IsNullOrWhiteSpace($raw)) { $parents[$indent] = $key continue } $segments = @($parents.Keys | Sort-Object { [int]$_ } | ForEach-Object { $parents[$_] }) + $key if (($raw.StartsWith('"') -and $raw.EndsWith('"')) -or ` ($raw.StartsWith("'") -and $raw.EndsWith("'"))) { $raw = $raw.Substring(1, $raw.Length - 2) } $values[($segments -join '.')] = Resolve-ShadowScalarPlaceholder $raw } return $values } function Get-ShadowSqlServerJdbcIdentity { param([Parameter(Mandatory = $true)][string]$JdbcUrl) if ($JdbcUrl -notmatch '(?i)^jdbc:sqlserver://([^;]+)(?:;(.*))?$') { throw 'SQLSERVER_JDBC_URL_REQUIRED' } $server = $matches[1].Trim() $properties = @{} foreach ($part in @($matches[2] -split ';')) { if ($part -match '^\s*([^=]+)=(.*)$') { $properties[$matches[1].Trim().ToLowerInvariant()] = $matches[2].Trim() } } $database = $properties['databasename'] if ([string]::IsNullOrWhiteSpace($database)) { $database = $properties['database'] } if ([string]::IsNullOrWhiteSpace($server) -or ` [string]::IsNullOrWhiteSpace($database)) { throw 'SQLSERVER_JDBC_IDENTITY_INCOMPLETE' } return [pscustomobject]@{ server = $server; database = $database } }