src/ReadScriptFile.ps1
|
function ShortPath($Path) { $Path = $Path.Replace($env:USERPROFILE, '~').Replace($PWD, '.').Replace('/./', '/') $Path } function BaseReadFile($File, $Encoding = 'UTF8') { $Content = try { if ($File -match "^(https?|ftp)://") { if ($GuestMode) { if ((Invoke-WebRequest $File -Method Head -ErrorAction SilentlyContinue).Headers.'Content-Length' -gt 1mb) { Write-I18n Error GuestModeFileTooLarge $File -Category LimitsExceeded throw } if ($File -match "^ftp://") { Write-I18n Error GuestModeFtpNotSupported -Category ReadError throw } } $result = (Invoke-WebRequest -Uri $File).Content if ($Encoding -ne 'byte') { $result = $result -replace '^[^\u0000-\u007F]+', '' } $result } elseif (-not $GuestMode) { # -Encoding Byte 似乎在某些机器中不起作用:https://github.com/steve02081504/ps12exe/issues/18 if ($Encoding -ne 'Byte') { Get-Content -LiteralPath $File -Encoding $Encoding -Raw } else { [System.IO.File]::ReadAllBytes($File) } } } catch { Write-Verbose $_ } if (-not $Content) { Write-I18n Error ReadFileFailed $(ShortPath $File) -Category ReadError throw } Write-I18n Host ReadingFile @([System.IO.Path]::GetFileName($File), $Content.Length) $Content } function ReadScriptFile($File) { $Content = BaseReadFile $File $Content = $Content -join "`n" -split '\r?\n' $FileName = [System.IO.Path]::GetFileName($File) Write-I18n Verbose ReadingScriptDone $FileName Preprocessor $Content $File Write-I18n Verbose PreprocessScriptDone $FileName } . $PSScriptRoot\predicate.ps1 . $PSScriptRoot\PSObjectToString.ps1 . $PSScriptRoot\AstAnalyze.ps1 function Preprocessor($Content, $FilePath) { $Result = @() $requiredModules = @() $requireFlag = $False # here-string 函数体与完全处于块注释内的行不参与 `#_!!` 使用检查(与 VS Code 插件的 computeSkipMask 一致)。 $OpaqueLines = [bool[]]::new($Content.Count) $HereTerminator = $null $InBlockComment = $false for ($skipIndex = 0; $skipIndex -lt $Content.Count; $skipIndex++) { $skipLine = [string]$Content[$skipIndex] $skipTrimmed = $skipLine.Trim() if ($HereTerminator) { $OpaqueLines[$skipIndex] = $true if ($skipTrimmed.StartsWith($HereTerminator)) { $HereTerminator = $null } continue } if ($InBlockComment) { $OpaqueLines[$skipIndex] = $true if ($skipLine.Contains('#>')) { $InBlockComment = $false } continue } if ($skipLine -match '@(["''])\s*$') { $HereTerminator = $Matches[1] + '@' continue } $rest = $skipLine $hasCode = $false while ($true) { $open = $rest.IndexOf('<#') if ($open -lt 0) { if ($rest.Trim() -ne '') { $hasCode = $true } break } if ($rest.Substring(0, $open).Trim() -ne '') { $hasCode = $true } $close = $rest.IndexOf('#>', $open + 2) if ($close -lt 0) { $InBlockComment = $true; break } $rest = $rest.Substring($close + 2) } if (-not $hasCode) { $OpaqueLines[$skipIndex] = $true } } # 处理#_if <PSEXE/PSScript>、#_else、#_endif(支持嵌套:只有栈上所有分支都为真时才输出) $conditionStack = [System.Collections.Generic.List[hashtable]]::new() for ($index = 0; $index -lt $Content.Count; $index++) { $Line = $Content[$index] if ($Line -match "^\s*#_if\s+(?<condition>\S+)\s*(?!#.*)") { $conditionName = $Matches["condition"] # 外层 #_if 已经决定了内层条件,内层必有一支是死代码 if ($conditionStack.Count -gt 0) { Write-I18n Warning PreprocessNestedIfDeadCode @($conditionName, $conditionStack[$conditionStack.Count - 1].Name) } $conditionStack.Add(@{ Name = $conditionName Active = switch ($conditionName) { 'PSEXE' { $TRUE } 'PSScript' { $False } default { Write-I18n Error PreprocessUnknownIfCondition $conditionName -Category InvalidData; $False } } }) } elseif ($Line -match "^\s*#_else\s*(?!#.*)") { if ($conditionStack.Count -eq 0) { $Result += $Line; continue } $top = $conditionStack[$conditionStack.Count - 1] $top.Active = -not $top.Active } elseif ($Line -match "^\s*#_endif\s*(?!#.*)") { if ($conditionStack.Count -eq 0) { $Result += $Line; continue } $conditionStack.RemoveAt($conditionStack.Count - 1) } else { # `#_if PSEXE` 分支只会编译进 EXE:直接运行脚本时该分支并未被注释掉,因此其中的普通代码也必须带 `#_!!`。 # 反之 `#_if PSScript` 分支只在直接运行时存在:其中的 `#_!!` 会让它在直接运行时变成注释,属于误用。 if ($conditionStack.Count -gt 0 -and -not $OpaqueLines[$index]) { $knownBranch = $true foreach ($conditionEntry in $conditionStack) { if ($conditionEntry.Name -ne 'PSEXE' -and $conditionEntry.Name -ne 'PSScript') { $knownBranch = $false; break } } if ($knownBranch) { # 一行只有在其所有外层分支都于编译期被选中时才会进入 EXE,否则只在直接运行时存在。 $selectedInExe = -not ($conditionStack.Active -contains $false) $trimmedLine = ([string]$Line).TrimStart() if ($selectedInExe) { if ($trimmedLine -ne '' -and -not $trimmedLine.StartsWith('#')) { Write-I18n Warning PreprocessPsexeBranchCode } } elseif ($trimmedLine.StartsWith('#_!!')) { Write-I18n Warning PreprocessPsscriptBranchBang } } } if (-not ($conditionStack.Active -contains $false)) { $Result += $Line } } } if ($conditionStack.Count -ne 0) { Write-I18n Error PreprocessMissingEndif -Category SyntaxError return } # 被处理文件所在目录,用于解析 #_include 里的 $PSScriptRoot。本地路径取绝对目录,避免相对输入路径被反复前缀。 $ScriptRoot = if ($FilePath -match "^(https?|ftp)://") { $FilePath -replace '/[^/]*$', '' } else { [System.IO.Path]::GetDirectoryName($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)) } function GetIncludeFilePath($rest) { if ($rest -match "((\'[^\']*\')+)\s*(?!#.*)") { $file = $Matches[1] $file = $file.Substring(1, $file.Length - 2).Replace("''", "'") } elseif ($rest -match '((\"[^\"]*\")+)\s*(?!#.*)') { $file = $Matches[1] $file = $file.Substring(1, $file.Length - 2).Replace('""', '"') } else { $file = $rest } $file = $file.Replace('$PSScriptRoot', $ScriptRoot) # 仍是相对路径(未引用 $PSScriptRoot)时,基于被处理文件所在目录解析。 if (-not [System.IO.Path]::IsPathRooted($file) -and $file -notmatch "^(https?|ftp)://") { $file = "$ScriptRoot/$file" } $file } # 校验 pragma 子表达式是否只使用白名单内的 path 相关命令/变量。返回:$true 表示安全;否则返回一个含具体原因的字符串数组。 function Test-PragmaExpressionSafe([string]$Expr) { $PragmaSafeCommands = @('gcm', 'get-command', 'join-path', 'split-path', 'resolve-path', 'convert-path', 'get-item', 'test-path', 'get-childitem') if (-not $GuestMode) { $PragmaSafeCommands += 'get-content' } $PragmaSafeVariables = @('PSScriptRoot', 'ScriptRoot', 'HOME', 'PWD', 'PSCommandPath') $Errors = [System.Collections.Generic.List[string]]::new() $Tokens = $null $ParseErrors = $null $Ast = [System.Management.Automation.Language.Parser]::ParseInput($Expr, [ref]$Tokens, [ref]$ParseErrors) if ($ParseErrors) { $Errors.Add("parse: $($ParseErrors[0].Message)") return $Errors.ToArray() } $Result = AstAnalyze $Ast if ($Result.ImporttedExternalScripts) { $Errors.Add('external script invocation') } if ($Result.UsedNonConstTypes) { $Errors.Add("type members: $($Result.UsedNonConstTypes -join ', ')") } if ($Result.UsedInstanceMethods) { $Errors.Add("instance methods: $($Result.UsedInstanceMethods -join ', ')") } foreach ($f in $Result.UsedNonConstFunctions) { if ($PragmaSafeCommands -notcontains $f.ToLowerInvariant()) { $Errors.Add("command: $f") } } foreach ($v in $Result.UsedNonConstVariables) { if ($PragmaSafeVariables -notcontains $v) { $Errors.Add("variable: `$$v") } } return $Errors.ToArray() } # 校验通过后对 pragma 值进行 PowerShell 字符串展开(求值 $(...) 子表达式)。展开时临时把 $PSScriptRoot 指向被编译脚本所在目录,使子表达式内能直接引用它。 function Expand-PragmaExpression([string]$Value, [string]$PragmaName) { $Unsafe = Test-PragmaExpressionSafe ('"' + $Value + '"') if ($Unsafe) { Write-I18n Error PragmaUnsafeExpression $($PragmaName, ($Unsafe -join '; ')) -Category ReadError throw } $PSScriptRootBackup = $PSScriptRoot $PSScriptRoot = $ScriptRoot try { return $ExecutionContext.InvokeCommand.ExpandString($Value) } finally { $PSScriptRoot = $PSScriptRootBackup } } # 解析 pragma 的字符串值:支持双/单引号、$() 白名单子表达式展开、$PSScriptRoot 替换。 function ConvertFrom-PragmaStringValue([string]$Value, [string]$PragmaName) { if ($Value -match '^\"(?<value>[^\"]*)\"\s*(?!#.*)') { $Value = $Matches["value"] if ($Value -match '\$\(') { $Value = Expand-PragmaExpression $Value $PragmaName } else { $Value = $Value.Replace('$PSScriptRoot', $ScriptRoot) } } elseif ($Value -match "^\'(?<value>[^\']*)\'\s*(?!#.*)") { $Value = $Matches["value"] } else { if ($Value -match '\$\(') { $Value = Expand-PragmaExpression $Value $PragmaName } else { $Value = $Value.Replace('$PSScriptRoot', $ScriptRoot) } } return $Value } # 嵌套设置:a.b 或 a.b.c...,根参数必须是哈希表。字符串值会做同样的 pragma 值转换。 function Set-NestedPragma([string]$PragmaName, $Value) { $segments = $PragmaName -split '\.' $root = $segments[0] $rootType = $ParamList[$root].ParameterType if ($rootType -ne [hashtable] -and $rootType -ne [System.Collections.IDictionary]) { Write-I18n Warning UnknownPragma $PragmaName return $false } if ($Value -is [string]) { $Value = ConvertFrom-PragmaStringValue $Value $PragmaName if ($root -eq 'Signing' -and $segments[-1] -eq 'Password') { $Value = ConvertTo-SecureString -String $Value -AsPlainText -Force } } $target = if ($Params.ContainsKey($root) -and $Params[$root] -is [hashtable]) { $Params[$root] } else { @{} } $cursor = $target for ($i = 1; $i -lt $segments.Count - 1; $i++) { if (-not ($cursor[$segments[$i]] -is [hashtable])) { $cursor[$segments[$i]] = @{} } $cursor = $cursor[$segments[$i]] } $cursor[$segments[-1]] = $Value $Params[$root] = $target return $true } $Content = $Result | # 处理#_pragma ForEach-Object { $_ # 对于#_pragma,我们不在预处理时移除它:考虑到它可能被用于$PSEXEscript中 if ($_ -match "^\s*#_pragma\s+(?<pragmaname>[a-zA-Z_][a-zA-Z_0-9]*(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*)\s*(?!#.*)$") { $pragmaname = $Matches["pragmaname"] $value = $true if ($pragmaname.Contains('.')) { # 无值嵌套 pragma(如 #_pragma App.Windowed)等同于打开对应键 Set-NestedPragma $pragmaname $true | Out-Null return } if ($pragmaname.StartsWith("no")) { $pragmaname = $pragmaname.Substring(2) $value = $false } if ($ParamList[$pragmaname].ParameterType -eq [Switch]) { $Params[$pragmaname] = [Switch]$value return } if ($ParamList["no$pragmaname"].ParameterType -eq [Switch]) { $Params["no$pragmaname"] = [Switch]-not $value return } Write-I18n Warning UnknownPragma $($Matches["pragmaname"]) } elseif ($_ -match "^\s*#_pragma\s+(?<pragmaname>[a-zA-Z_][a-zA-Z_0-9]*(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*)\s+(?<rest>.+)\s*$") { $pragmaname = $Matches["pragmaname"] if ($ConstEvalPragmas -contains $pragmaname) { return } $value = $Matches["rest"] if ($pragmaname.Contains('.')) { Set-NestedPragma $pragmaname $value | Out-Null return } if ($ParamList[$pragmaname].ParameterType -eq [Switch] -or $ParamList["no$pragmaname"].ParameterType -eq [Switch]) { if ($value.IndexOf("#") -ge 0) { $value = $value.Substring(0, $value.IndexOf("#")) } $value = $value.Trim() if (IsEnable $value) { $value = $true } elseif (IsDisable $value) { $value = $false } else { Write-I18n Warning UnknownPragmaBoolValue $value return } if ($pragmaname.StartsWith("no")) { $pragmaname = $pragmaname.Substring(2) $value = -not $value } if ($ParamList[$pragmaname].ParameterType -eq [Switch]) { $Params[$pragmaname] = [Switch]$value } if ($ParamList["no$pragmaname"].ParameterType -eq [Switch]) { $Params["no$pragmaname"] = [Switch]-not $value } } elseif ($ParamList[$pragmaname].ParameterType -eq [string] -or $ParamList[$pragmaname + "File"].ParameterType -eq [string]) { $value = ConvertFrom-PragmaStringValue $value $pragmaname if ($ParamList[$pragmaname].ParameterType -eq [string]) { $Params[$pragmaname] = $value } elseif ($ParamList[$pragmaname + "File"].ParameterType -eq [string]) { $Params[$pragmaname + "File"] = $value } } elseif ($ParamList[$pragmaname].ParameterType) { Write-I18n Warning UnknownPragmaBadParameterType $($pragmaname, $ParamList[$pragmaname].ParameterType) } else { Write-I18n Warning UnknownPragma $pragmaname } } } | # 处理#_require ForEach-Object { if ($_ -match "^(\s*)#_require\s+(?<moduleList>[^#]+)\s*(?!#.*)") { $requiredModules += $Matches["moduleList"].Split(', |;、 ') | Where-Object { $_.Trim('"''') -ne '' } if (!$requireFlag) { $requireFlag = $true [bigint]::Parse('72') } } else { $_ } } | # 处理#_DllExport ForEach-Object { $_ # 对于#_DllExport,我们不在预处理时移除它:考虑到它可能被用于$PSEXEscript中 if ($_ -match "^(\s*)(#_DllExport\s+(?<callsign>[^#\(]+)\((?<callsignParams>[^#\)]*)\))\s*(?!#.*)") { $callsign = $Matches["callsign"] -split ' ' | ForEach-Object { $_.Trim() } $callsignParams = $Matches["callsignParams"] -split ',' | ForEach-Object { $_.Trim() } } elseif ($_ -match "^(\s*)(#_DllExport\s+(?<callsign>[^#\(]+))\s*(?!#.*)") { $callsign = $Matches["callsign"] -split ' ' | ForEach-Object { $_.Trim() } $callsignParams = @() } if ($callsign) { if (!$callsign[1]) { $callsign = @('void', $callsign[0]) } $DllExportData = @{ returntype = $callsign[0] funcname = $callsign[1] params = @() } foreach ($param in $callsignParams) { $paramData = $param -split ' ' | ForEach-Object { $_.Trim() } if ($paramData.Count -eq 1) { Write-I18n Warning DllExportDelNoneTypeArg $($Matches[2], $paramData[0]) $paramData = @('string', $paramData[0]) } $DllExportData.params += @{ name = $paramData[1]; type = $paramData[0] } } $DllExportList.Add($DllExportData) | Out-Null Write-I18n Warning DllExportUsing Write-Debug "$($Matches[2]): FuncSign: [$($DllExportData.returntype)]$($DllExportData.funcname)($(($DllExportData.params|ForEach-Object{ $_.type + ' ' + $_.name }) -join ', '))" } } | # 处理#_!!<line>、#_balus <?exitcode> ForEach-Object { if ($_ -match "^(\s*)#_!!(?<line>.*)") { $Matches[1] + $Matches["line"] } elseif ($_ -match "^(\s*)#_balus\s+(?<exitcode>\`$?\w+)") { 'Start-Process powershell @("-NoProfile";"-c";"sleep 1;rm `"$PSCommandPath`"") -WindowStyle hidden;exit ' + $Matches["exitcode"] } elseif ($_ -match "^(\s*)#_balus") { 'Start-Process powershell @("-NoProfile";"-c";"sleep 1;rm `"$PSCommandPath`"") -WindowStyle hidden;exit 0' } else { $_ } } | # 处理#_include <file>、#_include_as_value <valuename> <file>、#_include_as_(base64|bytes) <valuename> <file> ForEach-Object { if ($_ -match "^\s*#_include\s+(?<rest>.+)\s*") { $file = GetIncludeFilePath $Matches["rest"] ReadScriptFile $file } elseif ($_ -match "^\s*#_include_as_value\s+(?<valuename>[a-zA-Z_][a-zA-Z_0-9]+)\s+(?<rest>.+)\s*") { $valuename = $Matches["valuename"] $file = GetIncludeFilePath $Matches["rest"] $IncludeContent = BaseReadFile $file $IncludeContent = $IncludeContent -join "`n" $IncludeContent = $IncludeContent.Replace("'", "''") "`$$valuename = '$IncludeContent'" } elseif ($_ -match "^\s*#_include_as_(?<type>base64|bytes)\s+(?<valuename>[a-zA-Z_][a-zA-Z_0-9]+)\s+(?<rest>.+)\s*") { $valuename = $Matches["valuename"] $file = GetIncludeFilePath $Matches["rest"] $IncludeContent = BaseReadFile $file Byte $IncludeContent = [System.Convert]::ToBase64String($IncludeContent) if ($Matches["type"] -eq 'bytes') { "`$$valuename = [System.Convert]::FromBase64String('$IncludeContent')" } else { "`$$valuename = '$IncludeContent'" } } else { if ($_ -match '^\s*(?<assign>\$\w+\s*\=\s*)?(?<callopt>\.|&)\s*(?<rest>(\"|)\$PSScriptRoot.+)\s*') { $assign = $Matches["assign"] $rest = $Matches["rest"] $callopt = $Matches["callopt"] if ($rest -match "^(?<file>\`"[^\`"]*\`")(?<args>.+)") { $callargs = $Matches["args"] $rest = $Matches["file"] } elseif ($rest.IndexOf(' ') -gt 0) { $callargs = $rest.Substring($rest.IndexOf(' ') + 1) $rest = $rest.Substring(0, $rest.IndexOf(' ')) } $file = GetIncludeFilePath $rest if (Test-Path $file -PathType Leaf -ErrorAction Ignore) { if (!$callargs -and !$assign -and $callopt -eq '.') { return ReadScriptFile $file } return @("$assign$callopt{", $(ReadScriptFile $file), "}$callargs") } } $_ } } $NuGetIniter = "try{Import-PackageProvider NuGet}catch{Install-PackageProvider NuGet -Scope CurrentUser -Force -ea Ignore;Import-PackageProvider NuGet -ea Ignore}" $LoadModuleScript = if ($requiredModules.Count -gt 1) { (PSObjectToString $requiredModules -OneLine) + '|%{if(!(gmo $_ -ListAvailable -ea SilentlyContinue)){' + $NuGetIniter + ';Install-Module $_ -Scope CurrentUser -Force -ea Stop}}' } elseif ($requiredModules.Count -eq 1) { "if(!(gmo $requiredModules -ListAvailable -ea SilentlyContinue)){$NuGetIniter;Install-Module $requiredModules -Scope CurrentUser -Force -ea Stop}" } $LoadModuleScript = $LoadModuleScript -join "`n" if ($LoadModuleScript) { $Content = $Content | ForEach-Object { # 在第一次#_require的前方加入$LoadModuleScript if ($_ -is [bigint]) { $LoadModuleScript } else { $_ } } } $Content -join "`n" } |