diff --git a/Tools/Modules/VirtualTerminal/VirtualTerminal.psd1 b/Tools/Modules/VirtualTerminal/VirtualTerminal.psd1 new file mode 100644 index 000000000000..b7f5880151dc --- /dev/null +++ b/Tools/Modules/VirtualTerminal/VirtualTerminal.psd1 @@ -0,0 +1,134 @@ +# +# Module manifest for module 'VirtualTerminal' +# +# Generated by: Trenly +# +# Generated on: 7/7/2025 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'VirtualTerminal.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = 'bb4887b3-c05a-448a-983b-c61114dfd1c0' + + # Author of this module + Author = 'Microsoft Open Source Community' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + # Description = '' + + # Minimum version of the PowerShell engine required by this module + # PowerShellVersion = '' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Initialize-VirtualTerminalSequence' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-pkgs/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-pkgs' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} + diff --git a/Tools/Modules/VirtualTerminal/VirtualTerminal.psm1 b/Tools/Modules/VirtualTerminal/VirtualTerminal.psm1 new file mode 100644 index 000000000000..98970314f4e4 --- /dev/null +++ b/Tools/Modules/VirtualTerminal/VirtualTerminal.psm1 @@ -0,0 +1,55 @@ +$vtSupported = (Get-Host).UI.SupportsVirtualTerminal + +#### +# Deglobalion: If Virtual Terminal is supported, convert the operation code to its virtual terminal sequence +# Inputs: Integer. Operation Code +# Outputs: Nullable Virtual Terminal Sequence String +#### +filter Initialize-VirtualTerminalSequence { + if ($vtSupported) { + "$([char]0x001B)[${_}m" + } +} + +$vtBold = 1 | Initialize-VirtualTerminalSequence; $vtBold | Out-Null +$vtNotBold = 22 | Initialize-VirtualTerminalSequence; $vtNotBold | Out-Null +$vtUnderline = 4 | Initialize-VirtualTerminalSequence; $vtUnderline | Out-Null +$vtNotUnderline = 24 | Initialize-VirtualTerminalSequence; $vtNotUnderline | Out-Null +$vtNegative = 7 | Initialize-VirtualTerminalSequence; $vtNegative | Out-Null +$vtPositive = 27 | Initialize-VirtualTerminalSequence; $vtPositive | Out-Null +$vtForegroundBlack = 30 | Initialize-VirtualTerminalSequence; $vtForegroundBlack | Out-Null +$vtForegroundRed = 31 | Initialize-VirtualTerminalSequence; $vtForegroundRed | Out-Null +$vtForegroundGreen = 32 | Initialize-VirtualTerminalSequence; $vtForegroundGreen | Out-Null +$vtForegroundYellow = 33 | Initialize-VirtualTerminalSequence; $vtForegroundYellow | Out-Null +$vtForegroundBlue = 34 | Initialize-VirtualTerminalSequence; $vtForegroundBlue | Out-Null +$vtForegroundMagenta = 35 | Initialize-VirtualTerminalSequence; $vtForegroundMagenta | Out-Null +$vtForegroundCyan = 36 | Initialize-VirtualTerminalSequence; $vtForegroundCyan | Out-Null +$vtForegroundWhite = 37 | Initialize-VirtualTerminalSequence; $vtForegroundWhite | Out-Null +$vtForegroundDefault = 39 | Initialize-VirtualTerminalSequence; $vtForegroundDefault | Out-Null +$vtBackgroundBlack = 40 | Initialize-VirtualTerminalSequence; $vtBackgroundBlack | Out-Null +$vtBackgroundRed = 41 | Initialize-VirtualTerminalSequence; $vtBackgroundRed | Out-Null +$vtBackgroundGreen = 42 | Initialize-VirtualTerminalSequence; $vtBackgroundGreen | Out-Null +$vtBackgroundYellow = 43 | Initialize-VirtualTerminalSequence; $vtBackgroundYellow | Out-Null +$vtBackgroundBlue = 44 | Initialize-VirtualTerminalSequence; $vtBackgroundBlue | Out-Null +$vtBackgroundMagenta = 45 | Initialize-VirtualTerminalSequence; $vtBackgroundMagenta | Out-Null +$vtBackgroundCyan = 46 | Initialize-VirtualTerminalSequence; $vtBackgroundCyan | Out-Null +$vtBackgroundWhite = 47 | Initialize-VirtualTerminalSequence; $vtBackgroundWhite | Out-Null +$vtBackgroundDefault = 49 | Initialize-VirtualTerminalSequence; $vtBackgroundDefault | Out-Null +$vtForegroundBrightBlack = 90 | Initialize-VirtualTerminalSequence; $vtForegroundBrightBlack | Out-Null +$vtForegroundBrightRed = 91 | Initialize-VirtualTerminalSequence; $vtForegroundBrightRed | Out-Null +$vtForegroundBrightGreen = 92 | Initialize-VirtualTerminalSequence; $vtForegroundBrightGreen | Out-Null +$vtForegroundBrightYellow = 93 | Initialize-VirtualTerminalSequence; $vtForegroundBrightYellow | Out-Null +$vtForegroundBrightBlue = 94 | Initialize-VirtualTerminalSequence; $vtForegroundBrightBlue | Out-Null +$vtForegroundBrightMagenta = 95 | Initialize-VirtualTerminalSequence; $vtForegroundBrightMagenta | Out-Null +$vtForegroundBrightCyan = 96 | Initialize-VirtualTerminalSequence; $vtForegroundBrightCyan | Out-Null +$vtForegroundBrightWhite = 97 | Initialize-VirtualTerminalSequence; $vtForegroundBrightWhite | Out-Null +$vtBackgroundBrightRed = 101 | Initialize-VirtualTerminalSequence; $vtBackgroundBrightRed | Out-Null +$vtBackgroundBrightGreen = 102 | Initialize-VirtualTerminalSequence; $vtBackgroundBrightGreen | Out-Null +$vtBackgroundBrightYellow = 103 | Initialize-VirtualTerminalSequence; $vtBackgroundBrightYellow | Out-Null +$vtBackgroundBrightBlue = 104 | Initialize-VirtualTerminalSequence; $vtBackgroundBrightBlue | Out-Null +$vtBackgroundBrightMagenta = 105 | Initialize-VirtualTerminalSequence; $vtBackgroundBrightMagenta | Out-Null +$vtBackgroundBrightCyan = 106 | Initialize-VirtualTerminalSequence; $vtBackgroundBrightCyan | Out-Null +$vtBackgroundBrightWhite = 107 | Initialize-VirtualTerminalSequence; $vtBackgroundBrightWhite | Out-Null + +Export-ModuleMember -Function Initialize-VirtualTerminalSequence +Export-ModuleMember -Variable * diff --git a/Tools/Modules/YamlCreate/YamlCreate.InstallerDetection/YamlCreate.InstallerDetection.psd1 b/Tools/Modules/YamlCreate/YamlCreate.InstallerDetection/YamlCreate.InstallerDetection.psd1 new file mode 100644 index 000000000000..9cb811af8a13 --- /dev/null +++ b/Tools/Modules/YamlCreate/YamlCreate.InstallerDetection/YamlCreate.InstallerDetection.psd1 @@ -0,0 +1,144 @@ +# +# Module manifest for module 'YamlCreate.InstallerDetection' +# +# Generated by: Trenly +# +# Generated on: 7/7/2025 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'YamlCreate.InstallerDetection.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = 'ec62867e-32ba-40c8-89c4-802639209b03' + + # Author of this module + Author = 'Microsoft Open Source Community' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + # Description = '' + + # Minimum version of the PowerShell engine required by this module + # PowerShellVersion = '' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Get-OffsetBytes' + 'Get-PESectionTable' + 'Test-IsZip' + 'Test-IsMsix' + 'Test-IsMsi' + 'Test-IsWix' + 'Test-IsNullsoft' + 'Test-IsInno' + 'Test-IsBurn' + 'Test-IsFont' + 'Resolve-InstallerType' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-pkgs/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-pkgs' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} + diff --git a/Tools/Modules/YamlCreate/YamlCreate.InstallerDetection/YamlCreate.InstallerDetection.psm1 b/Tools/Modules/YamlCreate/YamlCreate.InstallerDetection/YamlCreate.InstallerDetection.psm1 new file mode 100644 index 000000000000..712c050e118e --- /dev/null +++ b/Tools/Modules/YamlCreate/YamlCreate.InstallerDetection/YamlCreate.InstallerDetection.psm1 @@ -0,0 +1,411 @@ +#### +# Description: Returns the values from the Properties menu of a file +# Inputs: Path to file +# Outputs: Dictonary of properties +#### +function Get-FileMetadata { + param ( + [Parameter(Mandatory = $true)] + [string]$FilePath + ) + + if (-not (Test-Path $FilePath)) { + Write-Error "File not found: $FilePath" + return + } + + $shell = New-Object -ComObject Shell.Application + $folder = $shell.Namespace((Split-Path $FilePath)) + $file = $folder.ParseName((Split-Path $FilePath -Leaf)) + + [PSCustomObject] $metadata = @{} + + for ($i = 0; $i -lt 400; $i++) { + $key = $folder.GetDetailsOf($folder.Items, $i) + $value = $folder.GetDetailsOf($file, $i) + + if ($key -and $value) { + $metadata[$key] = $value + } + } + + # Clean up COM objects + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($file) | Out-Null + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($folder) | Out-Null + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell) | Out-Null + + return $metadata +} + +#### +# Description: Gets the specified bytes from a byte array +# Inputs: Array of Bytes, Integer offset, Integer Length +# Outputs: Array of bytes +#### +function Get-OffsetBytes { + param ( + [Parameter(Mandatory = $true)] + [byte[]] $ByteArray, + [Parameter(Mandatory = $true)] + [int] $Offset, + [Parameter(Mandatory = $true)] + [int] $Length, + [Parameter(Mandatory = $false)] + [bool] $LittleEndian = $false # Bool instead of a switch for use with other functions + ) + + if ($Offset -gt $ByteArray.Length) { return @() } # Prevent null exceptions + $Start = if ($LittleEndian) { $Offset + $Length - 1 } else { $Offset } + $End = if ($LittleEndian) { $Offset } else { $Offset + $Length - 1 } + return $ByteArray[$Start..$End] +} + +#### +# Description: Gets the PE Section Table of a file +# Inputs: Path to File +# Outputs: Array of Object if valid PE file, null otherwise +#### +function Get-PESectionTable { + # TODO: Switch to using FileReader to be able to seek through the file instead of reading from the start + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + # https://learn.microsoft.com/en-us/windows/win32/debug/pe-format + # The first 64 bytes of the file contain the DOS header. The first two bytes are the "MZ" signature, and the 60th byte contains the offset to the PE header. + $DOSHeader = Get-Content -Path $Path -AsByteStream -TotalCount 64 -WarningAction 'SilentlyContinue' + $MZSignature = Get-OffsetBytes -ByteArray $DOSHeader -Offset 0 -Length 2 + if (Compare-Object -ReferenceObject $([byte[]](0x4D, 0x5A)) -DifferenceObject $MZSignature ) { return $null } # The MZ signature is invalid + $PESignatureOffsetBytes = Get-OffsetBytes -ByteArray $DOSHeader -Offset 60 -Length 4 + $PESignatureOffset = [BitConverter]::ToInt32($PESignatureOffsetBytes, 0) + + # These are known sizes + $PESignatureSize = 4 # Bytes + $COFFHeaderSize = 20 # Bytes + $SectionTableEntrySize = 40 # Bytes + + # Read 24 bytes past the PE header offset to get the PE Signature and COFF header + $RawBytes = Get-Content -Path $Path -AsByteStream -TotalCount $($PESignatureOffset + $PESignatureSize + $COFFHeaderSize) -WarningAction 'SilentlyContinue' + $PESignature = Get-OffsetBytes -ByteArray $RawBytes -Offset $PESignatureOffset -Length $PESignatureSize + if (Compare-Object -ReferenceObject $([byte[]](0x50, 0x45, 0x00, 0x00)) -DifferenceObject $PESignature ) { return $null } # The PE header is invalid if it is not 'PE\0\0' + + # Parse out information from the header + $COFFHeaderBytes = Get-OffsetBytes -ByteArray $RawBytes -Offset $($PESignatureOffset + $PESignatureSize) -Length $COFFHeaderSize + $MachineTypeBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 0 -Length 2 + $NumberOfSectionsBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 2 -Length 2 + $TimeDateStampBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 4 -Length 4 + $PointerToSymbolTableBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 8 -Length 4 + $NumberOfSymbolsBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 12 -Length 4 + $SizeOfOptionalHeaderBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 16 -Length 2 + $HeaderCharacteristicsBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 18 -Length 2 + + # Convert the data into real numbers + $NumberOfSections = [BitConverter]::ToInt16($NumberOfSectionsBytes, 0) + $TimeDateStamp = [BitConverter]::ToInt32($TimeDateStampBytes, 0) + $SymbolTableOffset = [BitConverter]::ToInt32($PointerToSymbolTableBytes, 0) + $NumberOfSymbols = [BitConverter]::ToInt32($NumberOfSymbolsBytes, 0) + $OptionalHeaderSize = [BitConverter]::ToInt16($SizeOfOptionalHeaderBytes, 0) + + # Read the section table from the file + $SectionTableStart = $PESignatureOffset + $PESignatureSize + $COFFHeaderSize + $OptionalHeaderSize + $SectionTableLength = $NumberOfSections * $SectionTableEntrySize + $RawBytes = Get-Content -Path $Path -AsByteStream -TotalCount $($SectionTableStart + $SectionTableLength) -WarningAction 'SilentlyContinue' + $SectionTableContents = Get-OffsetBytes -ByteArray $RawBytes -Offset $SectionTableStart -Length $SectionTableLength + + $SectionData = @(); + # Parse each of the sections + foreach ($Section in 0..$($NumberOfSections - 1)) { + $SectionTableEntry = Get-OffsetBytes -ByteArray $SectionTableContents -Offset ($Section * $SectionTableEntrySize) -Length $SectionTableEntrySize + + # Get the raw bytes + $SectionNameBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 0 -Length 8 + $VirtualSizeBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 8 -Length 4 + $VirtualAddressBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 12 -Length 4 + $SizeOfRawDataBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 16 -Length 4 + $PointerToRawDataBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 20 -Length 4 + $PointerToRelocationsBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 24 -Length 4 + $PointerToLineNumbersBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 28 -Length 4 + $NumberOfRelocationsBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 32 -Length 2 + $NumberOfLineNumbersBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 34 -Length 2 + $SectionCharacteristicsBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 36 -Length 4 + + # Convert the data into real values + $SectionName = [Text.Encoding]::UTF8.GetString($SectionNameBytes) + $VirtualSize = [BitConverter]::ToInt32($VirtualSizeBytes, 0) + $VirtualAddressOffset = [BitConverter]::ToInt32($VirtualAddressBytes, 0) + $SizeOfRawData = [BitConverter]::ToInt32($SizeOfRawDataBytes, 0) + $RawDataOffset = [BitConverter]::ToInt32($PointerToRawDataBytes, 0) + $RelocationsOffset = [BitConverter]::ToInt32($PointerToRelocationsBytes, 0) + $LineNumbersOffset = [BitConverter]::ToInt32($PointerToLineNumbersBytes, 0) + $NumberOfRelocations = [BitConverter]::ToInt16($NumberOfRelocationsBytes, 0) + $NumberOfLineNumbers = [BitConverter]::ToInt16($NumberOfLineNumbersBytes, 0) + + # Build the object + $SectionEntry = [PSCustomObject]@{ + SectionName = $SectionName + SectionNameBytes = $SectionNameBytes + VirtualSize = $VirtualSize + VirtualAddressOffset = $VirtualAddressOffset + SizeOfRawData = $SizeOfRawData + RawDataOffset = $RawDataOffset + RelocationsOffset = $RelocationsOffset + LineNumbersOffset = $LineNumbersOffset + NumberOfRelocations = $NumberOfRelocations + NumberOfLineNumbers = $NumberOfLineNumbers + SectionCharacteristicsBytes = $SectionCharacteristicsBytes + } + # Add the section to the output + $SectionData += $SectionEntry + } + + return $SectionData +} + +#### +# Description: Checks if a file is a Zip archive +# Inputs: Path to File +# Outputs: Boolean. True if file is a zip file, false otherwise +# Note: This function does not differentiate between other Zipped installer types. Any specific types like MSIX still result in an Zip file. +# Use this function with care, as it may return overly broad results. +#### +function Test-IsZip { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + + # The first 4 bytes of zip files are the same. + # It isn't worth setting up a FileStream and BinaryReader here since only the first 4 bytes are being checked + # https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT section 4.3.7 + $ZipHeader = Get-Content -Path $Path -AsByteStream -TotalCount 4 -WarningAction 'SilentlyContinue' + return $null -eq $(Compare-Object -ReferenceObject $([byte[]](0x50, 0x4B, 0x03, 0x04)) -DifferenceObject $ZipHeader) +} + +#### +# Description: Checks if a file is an MSIX or APPX archive +# Inputs: Path to File +# Outputs: Boolean. True if file is a MSIX or APPX file, false otherwise +#### +function Test-IsMsix { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + if (!(Test-IsZip -Path $Path)) { return $false } # MSIX are really just a special type of Zip file + Write-Debug 'Extracting file contents as a zip archive' + $FileObject = Get-Item -Path $Path + $temporaryFilePath = Join-Path -Path $env:TEMP -ChildPath "$($FileObject.BaseName).zip" # Expand-Archive only works if the file is a zip file + $expandedArchivePath = Join-Path -Path $env:TEMP -ChildPath $(New-Guid) + Copy-Item -Path $Path -Destination $temporaryFilePath + Expand-Archive -Path $temporaryFilePath -DestinationPath $expandedArchivePath + + # There are a few different indicators that a package can be installed with MSIX technology, look for any of these file names + $msixIndicators = @('AppxSignature.p7x'; 'AppxManifest.xml'; 'AppxBundleManifest.xml', 'AppxBlockMap.xml') + $returnValue = $false + foreach ($filename in $msixIndicators) { + if (Get-ChildItem -Path $expandedArchivePath -Recurse -Depth 3 -Filter $filename) { $returnValue = $true } # If any of the files is found, it is an msix + } + + # Cleanup the temporary files right away + Write-Debug 'Removing extracted files' + if (Test-Path $temporaryFilePath) { Remove-Item -Path $temporaryFilePath -Recurse } + if (Test-Path $expandedArchivePath) { Remove-Item -Path $expandedArchivePath -Recurse } + + return $returnValue +} + +#### +# Description: Checks if a file is an MSI installer +# Inputs: Path to File +# Outputs: Boolean. True if file is an MSI installer, false otherwise +# Note: This function does not differentiate between MSI installer types. Any specific packagers like WIX still result in an MSI installer. +# Use this function with care, as it may return overly broad results. +#### +function Test-IsMsi { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + + $MsiTables = Get-MSITable -Path $Path -ErrorAction SilentlyContinue + if ($MsiTables) { return $true } + # If the table names can't be parsed, it is not an MSI + return $false +} + +#### +# Description: Checks if a file is a WIX installer +# Inputs: Path to File +# Outputs: Boolean. True if file is a WIX installer, false otherwise +#### +function Test-IsWix { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + + $MsiTables = Get-MSITable -Path $Path -ErrorAction SilentlyContinue + if (!$MsiTables) { return $false } # If the table names can't be parsed, it is not an MSI and cannot be WIX + if ($MsiTables.Where({ $_.Table -match 'wix' })) { return $true } # If any of the table names match wix + if (Get-MSIProperty -Path $Path -Property '*wix*' -ErrorAction SilentlyContinue) { return $true } # If any of the keys in the property table match wix + + # If we reach here, the metadata has to be checked to see if it is a WIX installer + $FileMetadata = Get-FileMetadata -FilePath $Path + + # Check the created by program name matches WIX or XML, it is likely a WIX installer + if ($FileMetadata.ContainsKey('Program Name') -and $FileMetadata.'Program Name' -match 'WIX|XML') { + return $true + } + + return $false # If none of the checks matched, it is not a WIX installer +} + +#### +# Description: Checks if a file is a Nullsoft installer +# Inputs: Path to File +# Outputs: Boolean. True if file is a Nullsoft installer, false otherwise +#### +function Test-IsNullsoft { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + $SectionTable = Get-PESectionTable -Path $Path + if (!$SectionTable) { return $false } # If the section table is null, it is not an EXE and therefore not nullsoft + $LastSection = $SectionTable | Sort-Object -Property RawDataOffset -Descending | Select-Object -First 1 + $PEOverlayOffset = $LastSection.RawDataOffset + $LastSection.SizeOfRawData + + try { + # Set up a file reader + $fileStream = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $binaryReader = [System.IO.BinaryReader]::new($fileStream) + # Read 8 bytes after the offset + $fileStream.Seek($PEOverlayOffset, [System.IO.SeekOrigin]::Begin) | Out-Null + $RawBytes = $binaryReader.ReadBytes(8) + } catch { + # Set to null as a precaution + $RawBytes = $null + } finally { + if ($binaryReader) { $binaryReader.Close() } + if ($fileStream) { $fileStream.Close() } + } + if (!$RawBytes) { return $false } # The bytes couldn't be read + # From the first 8 bytes, get the Nullsoft header bytes + $PresumedHeaderBytes = Get-OffsetBytes -ByteArray $RawBytes -Offset 4 -Length 4 -LittleEndian $true + + # DEADBEEF -or- DEADBEED + # https://sourceforge.net/p/nsis/code/HEAD/tree/NSIS/branches/WIN64/Source/exehead/fileform.h#l222 + if (!(Compare-Object -ReferenceObject $([byte[]](0xDE, 0xAD, 0xBE, 0xEF)) -DifferenceObject $PresumedHeaderBytes)) { return $true } + if (!(Compare-Object -ReferenceObject $([byte[]](0xDE, 0xAD, 0xBE, 0xED)) -DifferenceObject $PresumedHeaderBytes)) { return $true } + return $false +} + +#### +# Description: Checks if a file is an Inno installer +# Inputs: Path to File +# Outputs: Boolean. True if file is an Inno installer, false otherwise +#### +function Test-IsInno { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + + $Resources = Get-Win32ModuleResource -Path $Path -DontLoadResource -ErrorAction SilentlyContinue + # https://github.com/jrsoftware/issrc/blob/main/Projects/Src/Shared.Struct.pas#L417 + if ($Resources.Name.Value -contains '#11111') { return $true } # If the resource name is #11111, it is an Inno installer + return $false +} + +#### +# Description: Checks if a file is a Burn installer +# Inputs: Path to File +# Outputs: Boolean. True if file is an Burn installer, false otherwise +#### +function Test-IsBurn { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + + $SectionTable = Get-PESectionTable -Path $Path + if (!$SectionTable) { return $false } # If the section table is null, it is not an EXE and therefore not Burn + # https://github.com/wixtoolset/wix/blob/main/src/burn/engine/inc/engine.h#L8 + if ($SectionTable.SectionName -contains '.wixburn') { return $true } + return $false +} + +#### +# Description: Checks if a file is a font which WinGet can install +# Inputs: Path to File +# Outputs: Boolean. True if file is a supported font, false otherwise +# Note: Supported font formats are TTF, TTC, and OTF +#### +function Test-IsFont { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + + # https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font + $TrueTypeFontSignature = [byte[]](0x00, 0x01, 0x00, 0x00) # The first 4 bytes of a TTF file + $OpenTypeFontSignature = [byte[]](0x4F, 0x54, 0x54, 0x4F) # The first 4 bytes of an OTF file + # https://learn.microsoft.com/en-us/typography/opentype/spec/otff#ttc-header + $TrueTypeCollectionSignature = [byte[]](0x74, 0x74, 0x63, 0x66) # The first 4 bytes of a TTC file + + $FontSignatures = @( + $TrueTypeFontSignature, + $OpenTypeFontSignature, + $TrueTypeCollectionSignature + ) + + # It isn't worth setting up a FileStream and BinaryReader here since only the first 4 bytes are being checked + $FontHeader = Get-Content -Path $Path -AsByteStream -TotalCount 4 -WarningAction 'SilentlyContinue' + return $($FontSignatures | ForEach-Object { !(Compare-Object -ReferenceObject $_ -DifferenceObject $FontHeader) }) -contains $true # If any of the signatures match, it is a font + +} + +#### +# Description: Attempts to identify the type of installer from a file path +# Inputs: Path to File +# Outputs: Null if unknown type. String if known type +#### +function Resolve-InstallerType { + param + ( + [Parameter(Mandatory = $true)] + [String] $Path + ) + + # Ordering is important here due to the specificity achievable by each of the detection methods + # if (Test-IsFont -Path $Path) { return 'font' } # Font detection is not implemented yet + if (Test-IsWix -Path $Path) { return 'wix' } + if (Test-IsMsi -Path $Path) { return 'msi' } + if (Test-IsMsix -Path $Path) { return 'msix' } + if (Test-IsZip -Path $Path) { return 'zip' } + if (Test-IsNullsoft -Path $Path) { return 'nullsoft' } + if (Test-IsInno -Path $Path) { return 'inno' } + if (Test-IsBurn -Path $Path) { return 'burn' } + return $null +} + +Export-ModuleMember -Function Get-OffsetBytes +Export-ModuleMember -Function Get-PESectionTable +Export-ModuleMember -Function Test-IsZip +Export-ModuleMember -Function Test-IsMsix +Export-ModuleMember -Function Test-IsMsi +Export-ModuleMember -Function Test-IsWix +Export-ModuleMember -Function Test-IsNullsoft +Export-ModuleMember -Function Test-IsInno +Export-ModuleMember -Function Test-IsBurn +Export-ModuleMember -Function Test-IsFont +Export-ModuleMember -Function Resolve-InstallerType diff --git a/Tools/Modules/YamlCreate/YamlCreate.Menuing/YamlCreate.Menuing.psd1 b/Tools/Modules/YamlCreate/YamlCreate.Menuing/YamlCreate.Menuing.psd1 new file mode 100644 index 000000000000..c3e301679a36 --- /dev/null +++ b/Tools/Modules/YamlCreate/YamlCreate.Menuing/YamlCreate.Menuing.psd1 @@ -0,0 +1,137 @@ +# +# Module manifest for module 'YamlCreate.Menuing' +# +# Generated by: Trenly +# +# Generated on: 7/7/2025 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'YamlCreate.Menuing.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = '51d2182f-5eca-494a-9ddb-64ff39dedb3c' + + # Author of this module + Author = 'Microsoft Open Source Community' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + # Description = '' + + # Minimum version of the PowerShell engine required by this module + # PowerShellVersion = '' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @( + 'VirtualTerminal' # Required for virtual terminal support + ) + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Get-Keypress', + 'Resolve-Keypress' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-pkgs/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-pkgs' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} + diff --git a/Tools/Modules/YamlCreate/YamlCreate.Menuing/YamlCreate.Menuing.psm1 b/Tools/Modules/YamlCreate/YamlCreate.Menuing/YamlCreate.Menuing.psm1 new file mode 100644 index 000000000000..96d90b6dfbe0 --- /dev/null +++ b/Tools/Modules/YamlCreate/YamlCreate.Menuing/YamlCreate.Menuing.psm1 @@ -0,0 +1,78 @@ +[ConsoleKey[]] $Numeric0 = @( [System.ConsoleKey]::D0; [System.ConsoleKey]::NumPad0 ); $Numeric0 | Out-Null +[ConsoleKey[]] $Numeric1 = @( [System.ConsoleKey]::D1; [System.ConsoleKey]::NumPad1 ); $Numeric1 | Out-Null +[ConsoleKey[]] $Numeric2 = @( [System.ConsoleKey]::D2; [System.ConsoleKey]::NumPad2 ); $Numeric2 | Out-Null +[ConsoleKey[]] $Numeric3 = @( [System.ConsoleKey]::D3; [System.ConsoleKey]::NumPad3 ); $Numeric3 | Out-Null +[ConsoleKey[]] $Numeric4 = @( [System.ConsoleKey]::D4; [System.ConsoleKey]::NumPad4 ); $Numeric4 | Out-Null +[ConsoleKey[]] $Numeric5 = @( [System.ConsoleKey]::D5; [System.ConsoleKey]::NumPad5 ); $Numeric5 | Out-Null +[ConsoleKey[]] $Numeric6 = @( [System.ConsoleKey]::D6; [System.ConsoleKey]::NumPad6 ); $Numeric6 | Out-Null +[ConsoleKey[]] $Numeric7 = @( [System.ConsoleKey]::D7; [System.ConsoleKey]::NumPad7 ); $Numeric7 | Out-Null +[ConsoleKey[]] $Numeric8 = @( [System.ConsoleKey]::D8; [System.ConsoleKey]::NumPad8 ); $Numeric8 | Out-Null +[ConsoleKey[]] $Numeric9 = @( [System.ConsoleKey]::D9; [System.ConsoleKey]::NumPad9 ); $Numeric9 | Out-Null + +#### +# Description: Waits for the user to press a key +# Inputs: Boolean for whether to echo the key value to the console +# Outputs: Key which was pressed +#### +function Get-Keypress { + param ( + [Parameter(Mandatory = $false)] + [bool] $EchoKey = $false + ) + + do { + $keyInfo = [Console]::ReadKey(!$EchoKey) + } until ($keyInfo.Key) + return $keyInfo.Key +} + +#### +# Description: Waits for a valid keypress from the user +# Inputs: List of valid keys, Default key to return, Boolean for strict mode +# Outputs: Key which was pressed +#### +function Resolve-Keypress { + param ( + [Parameter(Mandatory = $true)] + [System.ConsoleKey[]] $ValidKeys, + [Parameter(Mandatory = $true)] + [System.ConsoleKey] $DefaultKey, + [Parameter(Mandatory = $false)] + [bool] $UseStrict = $false + ) + + do { + # Get a keypress + $key = Get-Keypress -EchoKey $false + + # If the key pressed is in the valid keys, it doesn't matter if strict mode is enabled or not + if ($ValidKeys -contains $key) { + return $key + } + + # If the key pressed is the default key, it doesn't matter if strict mode is enabled or not + if ($key -eq $DefaultKey) { + return $key + } + + if (!$UseStrict) { + # The key pressed is not in the valid keys, is not the default key, and strict mode is not enabled + # Since strict mode is not enabled, we will return the default key + return $DefaultKey + } + + # If we reach here, the key pressed is not in the valid keys, is not the default key, and strict mode is enabled + # We will inform the user that the key pressed is invalid and prompt them to press a valid key + Write-Information @" +${vtForegroundRed}Invalid key pressed. Please press one of the valid keys: $($ValidKeys -join ', ') +${vtForegroundDefault} +"@ + + } while ( $true ) # Loop until a valid key is pressed + + return $DefaultKey # This line is never reached, but it's here for completeness +} + +Export-ModuleMember -Function Get-Keypress +Export-ModuleMember -Function Resolve-Keypress +Export-ModuleMember -Variable Numeric* diff --git a/Tools/Modules/YamlCreate/YamlCreate.psd1 b/Tools/Modules/YamlCreate/YamlCreate.psd1 new file mode 100644 index 000000000000..2db8ff8c50d8 --- /dev/null +++ b/Tools/Modules/YamlCreate/YamlCreate.psd1 @@ -0,0 +1,140 @@ +# +# Module manifest for module 'YamlCreate.Menuing' +# +# Generated by: Trenly +# +# Generated on: 7/7/2025 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'YamlCreate.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = '7e3c36ac-436b-41fa-a89d-1deb466096cc' + + # Author of this module + Author = 'Microsoft Open Source Community' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + # Description = '' + + # Minimum version of the PowerShell engine required by this module + # PowerShellVersion = '' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @( + 'VirtualTerminal' # Required for virtual terminal support + ) + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + NestedModules = @( + 'YamlCreate.InstallerDetection' + 'YamlCreate.Menuing' + ) + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + ModuleList = @( + 'YamlCreate.InstallerDetection' + 'YamlCreate.Menuing' + ) + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-pkgs/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-pkgs' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} + diff --git a/Tools/Modules/YamlCreate/YamlCreate.psm1 b/Tools/Modules/YamlCreate/YamlCreate.psm1 new file mode 100644 index 000000000000..c0e3115b2d4c --- /dev/null +++ b/Tools/Modules/YamlCreate/YamlCreate.psm1 @@ -0,0 +1,13 @@ + +# Import all sub-modules +$script:moduleRoot = Split-Path -Parent $MyInvocation.MyCommand.Path + +Get-ChildItem -Path $script:moduleRoot -Recurse -Depth 1 -Filter '*.psd1'| ForEach-Object { + if ($_.Name -eq 'YamlCreate.psd1') { + # Skip the main module manifest as it is already handled + return + } + $moduleFolder = Join-Path -Path $script:moduleRoot -ChildPath $_.Directory.Name + $moduleFile = Join-Path -Path $moduleFolder -ChildPath $_.Name + Import-Module $moduleFile -Force -Scope Global -ErrorAction 'Stop' +} diff --git a/Tools/YamlCreate.ps1 b/Tools/YamlCreate.ps1 index 7b93af93fe87..718be04d3791 100644 --- a/Tools/YamlCreate.ps1 +++ b/Tools/YamlCreate.ps1 @@ -27,8 +27,6 @@ Justification = 'Ths function is a wrapper which calls the singular Read-AppsAndFeaturesEntry as many times as necessary. It corresponds exactly to a pluralized manifest field')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Scope = 'Function', Target = '*Metadata', Justification = 'Metadata is used as a mass noun and is therefore singular in the cases used in this script')] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Scope = 'Function', Target = 'Get-OffsetBytes', - Justification = 'Ths function both consumes and outputs an array of bytes. The pluralized name is required to adequately describe the functions purpose')] Param ( @@ -238,7 +236,7 @@ if ($Settings) { exit } -$ScriptHeader = '# Created with YamlCreate.ps1 v2.4.7' +$ScriptHeader = '# Created with YamlCreate.ps1 v2.5.0' $ManifestVersion = '1.10.0' $PSDefaultParameterValues = @{ '*:Encoding' = 'UTF8' } $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False @@ -253,6 +251,13 @@ $RunHash = $(Get-FileHash -InputStream $([IO.MemoryStream]::new([byte[]][char[]] $script:UserAgent = 'Microsoft-Delivery-Optimization/10.1' $script:CleanupPaths = @() +$script:OriginalPSModulePath = $env:PSModulePath +Write-Debug 'Setting up module paths for YamlCreate' +Write-Debug "Adding $(Join-Path -Path $PSScriptRoot -ChildPath 'Modules') to PSModulePath" +$env:PSModulePath = $env:PSModulePath + ';' + (Join-Path -Path $PSScriptRoot -ChildPath 'Modules') # Add the local modules to the PSModulePath + +Import-Module -Name 'YamlCreate' -Scope Global -Force -ErrorAction 'Stop' # Parent module that loads the rest of the modules required for the script + $_wingetVersion = 1.0.0 $_appInstallerVersion = (Get-AppxPackage Microsoft.DesktopAppInstaller).version if (Get-Command 'winget' -ErrorAction SilentlyContinue) { $_wingetVersion = (winget -v).TrimStart('v') } @@ -416,6 +421,7 @@ Function Invoke-CleanExit { Write-Host [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture + $env:PSModulePath = $script:OriginalPSModulePath exit } @@ -485,25 +491,6 @@ Function Get-EffectiveInstallerType { return $Installer.NestedInstallerType } -# Takes an array of strings and an array of colors then writes one line of text composed of each string being its respective color -Function Write-MulticolorLine { - Param - ( - [Parameter(Mandatory = $true, Position = 0)] - [string[]] $TextStrings, - [Parameter(Mandatory = $true, Position = 1)] - [string[]] $Colors - ) - If ($TextStrings.Count -ne $Colors.Count) { - throw [System.ArgumentException]::new('Invalid Function Parameters. Arguments must be of equal length') - } - $_index = 0 - Foreach ($String in $TextStrings) { - Write-Host -ForegroundColor $Colors[$_index] -NoNewline $String - $_index++ - } -} - # Checks a URL and returns the status code received from the URL Function Test-Url { Param @@ -655,349 +642,6 @@ Function Get-UserSavePreference { return $_Preference } -#### -# Description: Gets the specified bytes from a byte array -# Inputs: Array of Bytes, Integer offset, Integer Length -# Outputs: Array of bytes -#### -function Get-OffsetBytes { - param ( - [Parameter(Mandatory = $true)] - [byte[]] $ByteArray, - [Parameter(Mandatory = $true)] - [int] $Offset, - [Parameter(Mandatory = $true)] - [int] $Length, - [Parameter(Mandatory = $false)] - [bool] $LittleEndian = $false # Bool instead of a switch for use with other functions - ) - - if ($Offset -gt $ByteArray.Length) { return @() } # Prevent null exceptions - $Start = if ($LittleEndian) { $Offset + $Length - 1 } else { $Offset } - $End = if ($LittleEndian) { $Offset } else { $Offset + $Length - 1 } - return $ByteArray[$Start..$End] -} - -#### -# Description: Gets the PE Section Table of a file -# Inputs: Path to File -# Outputs: Array of Object if valid PE file, null otherwise -#### -function Get-PESectionTable { - # TODO: Switch to using FileReader to be able to seek through the file instead of reading from the start - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - # https://learn.microsoft.com/en-us/windows/win32/debug/pe-format - # The first 64 bytes of the file contain the DOS header. The first two bytes are the "MZ" signature, and the 60th byte contains the offset to the PE header. - $DOSHeader = Get-Content -Path $Path -AsByteStream -TotalCount 64 -WarningAction 'SilentlyContinue' - $MZSignature = Get-OffsetBytes -ByteArray $DOSHeader -Offset 0 -Length 2 - if (Compare-Object -ReferenceObject $([byte[]](0x4D, 0x5A)) -DifferenceObject $MZSignature ) { return $null } # The MZ signature is invalid - $PESignatureOffsetBytes = Get-OffsetBytes -ByteArray $DOSHeader -Offset 60 -Length 4 - $PESignatureOffset = [BitConverter]::ToInt32($PESignatureOffsetBytes, 0) - - # These are known sizes - $PESignatureSize = 4 # Bytes - $COFFHeaderSize = 20 # Bytes - $SectionTableEntrySize = 40 # Bytes - - # Read 24 bytes past the PE header offset to get the PE Signature and COFF header - $RawBytes = Get-Content -Path $Path -AsByteStream -TotalCount $($PESignatureOffset + $PESignatureSize + $COFFHeaderSize) -WarningAction 'SilentlyContinue' - $PESignature = Get-OffsetBytes -ByteArray $RawBytes -Offset $PESignatureOffset -Length $PESignatureSize - if (Compare-Object -ReferenceObject $([byte[]](0x50, 0x45, 0x00, 0x00)) -DifferenceObject $PESignature ) { return $null } # The PE header is invalid if it is not 'PE\0\0' - - # Parse out information from the header - $COFFHeaderBytes = Get-OffsetBytes -ByteArray $RawBytes -Offset $($PESignatureOffset + $PESignatureSize) -Length $COFFHeaderSize - # $MachineTypeBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 0 -Length 2 - $NumberOfSectionsBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 2 -Length 2 - # $TimeDateStampBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 4 -Length 4 - # $PointerToSymbolTableBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 8 -Length 4 - # $NumberOfSymbolsBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 12 -Length 4 - $SizeOfOptionalHeaderBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 16 -Length 2 - # $HeaderCharacteristicsBytes = Get-OffsetBytes -ByteArray $COFFHeaderBytes -Offset 18 -Length 2 - - # Convert the data into real numbers - $NumberOfSections = [BitConverter]::ToInt16($NumberOfSectionsBytes, 0) - # $TimeDateStamp = [BitConverter]::ToInt32($TimeDateStampBytes, 0) - # $SymbolTableOffset = [BitConverter]::ToInt32($PointerToSymbolTableBytes, 0) - # $NumberOfSymbols = [BitConverter]::ToInt32($NumberOfSymbolsBytes, 0) - $OptionalHeaderSize = [BitConverter]::ToInt16($SizeOfOptionalHeaderBytes, 0) - - # Read the section table from the file - $SectionTableStart = $PESignatureOffset + $PESignatureSize + $COFFHeaderSize + $OptionalHeaderSize - $SectionTableLength = $NumberOfSections * $SectionTableEntrySize - $RawBytes = Get-Content -Path $Path -AsByteStream -TotalCount $($SectionTableStart + $SectionTableLength) -WarningAction 'SilentlyContinue' - $SectionTableContents = Get-OffsetBytes -ByteArray $RawBytes -Offset $SectionTableStart -Length $SectionTableLength - - $SectionData = @(); - # Parse each of the sections - foreach ($Section in 0..$($NumberOfSections - 1)) { - $SectionTableEntry = Get-OffsetBytes -ByteArray $SectionTableContents -Offset ($Section * $SectionTableEntrySize) -Length $SectionTableEntrySize - - # Get the raw bytes - $SectionNameBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 0 -Length 8 - $VirtualSizeBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 8 -Length 4 - $VirtualAddressBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 12 -Length 4 - $SizeOfRawDataBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 16 -Length 4 - $PointerToRawDataBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 20 -Length 4 - $PointerToRelocationsBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 24 -Length 4 - $PointerToLineNumbersBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 28 -Length 4 - $NumberOfRelocationsBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 32 -Length 2 - $NumberOfLineNumbersBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 34 -Length 2 - $SectionCharacteristicsBytes = Get-OffsetBytes -ByteArray $SectionTableEntry -Offset 36 -Length 4 - - # Convert the data into real values - $SectionName = [Text.Encoding]::UTF8.GetString($SectionNameBytes) - $VirtualSize = [BitConverter]::ToInt32($VirtualSizeBytes, 0) - $VirtualAddressOffset = [BitConverter]::ToInt32($VirtualAddressBytes, 0) - $SizeOfRawData = [BitConverter]::ToInt32($SizeOfRawDataBytes, 0) - $RawDataOffset = [BitConverter]::ToInt32($PointerToRawDataBytes, 0) - $RelocationsOffset = [BitConverter]::ToInt32($PointerToRelocationsBytes, 0) - $LineNumbersOffset = [BitConverter]::ToInt32($PointerToLineNumbersBytes, 0) - $NumberOfRelocations = [BitConverter]::ToInt16($NumberOfRelocationsBytes, 0) - $NumberOfLineNumbers = [BitConverter]::ToInt16($NumberOfLineNumbersBytes, 0) - - # Build the object - $SectionEntry = [PSCustomObject]@{ - SectionName = $SectionName - SectionNameBytes = $SectionNameBytes - VirtualSize = $VirtualSize - VirtualAddressOffset = $VirtualAddressOffset - SizeOfRawData = $SizeOfRawData - RawDataOffset = $RawDataOffset - RelocationsOffset = $RelocationsOffset - LineNumbersOffset = $LineNumbersOffset - NumberOfRelocations = $NumberOfRelocations - NumberOfLineNumbers = $NumberOfLineNumbers - SectionCharacteristicsBytes = $SectionCharacteristicsBytes - } - # Add the section to the output - $SectionData += $SectionEntry - } - - return $SectionData -} - -#### -# Description: Checks if a file is a Zip archive -# Inputs: Path to File -# Outputs: Boolean. True if file is a zip file, false otherwise -# Note: This function does not differentiate between other Zipped installer types. Any specific types like MSIX still result in an Zip file. -# Use this function with care, as it may return overly broad results. -#### -function Test-IsZip { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - - # The first 4 bytes of zip files are the same. - # It isn't worth setting up a FileStream and BinaryReader here since only the first 4 bytes are being checked - # https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT section 4.3.7 - $ZipHeader = Get-Content -Path $Path -AsByteStream -TotalCount 4 -WarningAction 'SilentlyContinue' - return $null -eq $(Compare-Object -ReferenceObject $([byte[]](0x50, 0x4B, 0x03, 0x04)) -DifferenceObject $ZipHeader) -} - -#### -# Description: Checks if a file is an MSIX or APPX archive -# Inputs: Path to File -# Outputs: Boolean. True if file is a MSIX or APPX file, false otherwise -#### -function Test-IsMsix { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - if (!(Test-IsZip -Path $Path)) { return $false } # MSIX are really just a special type of Zip file - Write-Debug 'Extracting file contents as a zip archive' - $FileObject = Get-Item -Path $Path - $temporaryFilePath = Join-Path -Path $env:TEMP -ChildPath "$($FileObject.BaseName).zip" # Expand-Archive only works if the file is a zip file - $expandedArchivePath = Join-Path -Path $env:TEMP -ChildPath $(New-Guid) - Copy-Item -Path $Path -Destination $temporaryFilePath - Expand-Archive -Path $temporaryFilePath -DestinationPath $expandedArchivePath - Write-Debug 'Marking extracted files for cleanup' - $script:CleanupPaths += @($temporaryFilePath; $expandedArchivePath) - - # There are a few different indicators that a package can be installed with MSIX technology, look for any of these file names - $msixIndicators = @('AppxSignature.p7x'; 'AppxManifest.xml'; 'AppxBundleManifest.xml', 'AppxBlockMap.xml') - foreach ($filename in $msixIndicators) { - if (Get-ChildItem -Path $expandedArchivePath -Recurse -Depth 3 -Filter $filename) { return $true } # If any of the files is found, it is an msix - } - return $false -} - -#### -# Description: Checks if a file is an MSI installer -# Inputs: Path to File -# Outputs: Boolean. True if file is an MSI installer, false otherwise -# Note: This function does not differentiate between MSI installer types. Any specific packagers like WIX still result in an MSI installer. -# Use this function with care, as it may return overly broad results. -#### -function Test-IsMsi { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - - $MsiTables = Get-MSITable -Path $Path -ErrorAction SilentlyContinue - if ($MsiTables) { return $true } - # If the table names can't be parsed, it is not an MSI - return $false -} - -#### -# Description: Checks if a file is a WIX installer -# Inputs: Path to File -# Outputs: Boolean. True if file is a WIX installer, false otherwise -#### -function Test-IsWix { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - - $MsiTables = Get-MSITable -Path $Path -ErrorAction SilentlyContinue - if (!$MsiTables) { return $false } # If the table names can't be parsed, it is not an MSI and cannot be WIX - if ($MsiTables.Where({ $_.Table -match 'wix' })) { return $true } # If any of the table names match wix - if (Get-MSIProperty -Path $Path -Property '*wix*' -ErrorAction SilentlyContinue) { return $true } # If any of the keys in the property table match wix - # TODO: Also Check the Metadata of the file -} - -#### -# Description: Checks if a file is a Nullsoft installer -# Inputs: Path to File -# Outputs: Boolean. True if file is a Nullsoft installer, false otherwise -#### -function Test-IsNullsoft { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - $SectionTable = Get-PESectionTable -Path $Path - if (!$SectionTable) { return $false } # If the section table is null, it is not an EXE and therefore not nullsoft - $LastSection = $SectionTable | Sort-Object -Property RawDataOffset -Descending | Select-Object -First 1 - $PEOverlayOffset = $LastSection.RawDataOffset + $LastSection.SizeOfRawData - - try { - # Set up a file reader - $fileStream = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) - $binaryReader = [System.IO.BinaryReader]::new($fileStream) - # Read 8 bytes after the offset - $fileStream.Seek($PEOverlayOffset, [System.IO.SeekOrigin]::Begin) | Out-Null - $RawBytes = $binaryReader.ReadBytes(8) - } catch { - # Set to null as a precaution - $RawBytes = $null - } finally { - if ($binaryReader) { $binaryReader.Close() } - if ($fileStream) { $fileStream.Close() } - } - if (!$RawBytes) { return $false } # The bytes couldn't be read - # From the first 8 bytes, get the Nullsoft header bytes - $PresumedHeaderBytes = Get-OffsetBytes -ByteArray $RawBytes -Offset 4 -Length 4 -LittleEndian $true - - # DEADBEEF -or- DEADBEED - # https://sourceforge.net/p/nsis/code/HEAD/tree/NSIS/branches/WIN64/Source/exehead/fileform.h#l222 - if (!(Compare-Object -ReferenceObject $([byte[]](0xDE, 0xAD, 0xBE, 0xEF)) -DifferenceObject $PresumedHeaderBytes)) { return $true } - if (!(Compare-Object -ReferenceObject $([byte[]](0xDE, 0xAD, 0xBE, 0xED)) -DifferenceObject $PresumedHeaderBytes)) { return $true } - return $false -} - -#### -# Description: Checks if a file is an Inno installer -# Inputs: Path to File -# Outputs: Boolean. True if file is an Inno installer, false otherwise -#### -function Test-IsInno { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - - $Resources = Get-Win32ModuleResource -Path $Path -DontLoadResource -ErrorAction SilentlyContinue - # https://github.com/jrsoftware/issrc/blob/main/Projects/Src/Shared.Struct.pas#L417 - if ($Resources.Name.Value -contains '#11111') { return $true } # If the resource name is #11111, it is an Inno installer - return $false -} - -#### -# Description: Checks if a file is a Burn installer -# Inputs: Path to File -# Outputs: Boolean. True if file is an Burn installer, false otherwise -#### -function Test-IsBurn { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - - $SectionTable = Get-PESectionTable -Path $Path - if (!$SectionTable) { return $false } # If the section table is null, it is not an EXE and therefore not Burn - # https://github.com/wixtoolset/wix/blob/main/src/burn/engine/inc/engine.h#L8 - if ($SectionTable.SectionName -contains '.wixburn') { return $true } - return $false -} - -#### -# Description: Checks if a file is a font which WinGet can install -# Inputs: Path to File -# Outputs: Boolean. True if file is a supported font, false otherwise -# Note: Supported font formats are TTF, TTC, and OTF -#### -function Test-IsFont { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - - # https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font - $TrueTypeFontSignature = [byte[]](0x00, 0x01, 0x00, 0x00) # The first 4 bytes of a TTF file - $OpenTypeFontSignature = [byte[]](0x4F, 0x54, 0x54, 0x4F) # The first 4 bytes of an OTF file - # https://learn.microsoft.com/en-us/typography/opentype/spec/otff#ttc-header - $TrueTypeCollectionSignature = [byte[]](0x74, 0x74, 0x63, 0x66) # The first 4 bytes of a TTC file - - $FontSignatures = @( - $TrueTypeFontSignature, - $OpenTypeFontSignature, - $TrueTypeCollectionSignature - ) - - # It isn't worth setting up a FileStream and BinaryReader here since only the first 4 bytes are being checked - $FontHeader = Get-Content -Path $Path -AsByteStream -TotalCount 4 -WarningAction 'SilentlyContinue' - return $($FontSignatures | ForEach-Object { !(Compare-Object -ReferenceObject $_ -DifferenceObject $FontHeader) }) -contains $true # If any of the signatures match, it is a font - -} - - -Function Get-PathInstallerType { - param - ( - [Parameter(Mandatory = $true)] - [String] $Path - ) - - # Ordering is important here due to the specificity achievable by each of the detection methods - # if (Test-IsFont -Path $Path) { return 'font' } # Font detection is not implemented yet - if (Test-IsWix -Path $Path) { return 'wix' } - if (Test-IsMsi -Path $Path) { return 'msi' } - if (Test-IsMsix -Path $Path) { return 'msix' } - if (Test-IsZip -Path $Path) { return 'zip' } - if (Test-IsNullsoft -Path $Path) { return 'nullsoft' } - if (Test-IsInno -Path $Path) { return 'inno' } - if (Test-IsBurn -Path $Path) { return 'burn' } - return $null -} - Function Get-UriArchitecture { Param ( @@ -1354,7 +998,7 @@ Function Read-InstallerEntry { } Write-Host "Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)" -ForegroundColor Green $_Installer['InstallerSha256'] = (Get-FileHash -Path $script:dest -Algorithm SHA256).Hash - Get-PathInstallerType -Path $script:dest -OutVariable _ | Out-Null + Resolve-InstallerType -Path $script:dest -OutVariable _ | Out-Null if ($_) { $_Installer['InstallerType'] = $_ | Select-Object -First 1 } Get-UriArchitecture -URI $_Installer['InstallerUrl'] -OutVariable _ | Out-Null if ($_) { $_Installer['Architecture'] = $_ | Select-Object -First 1 } @@ -1633,7 +1277,7 @@ Function Read-InstallerEntry { $_Installer['AppsAndFeaturesEntries'] = @($AppsAndFeaturesEntries) } - if ($script:SaveOption -eq '1' -and (Test-Path -Path $script:dest)) { Remove-Item -Path $script:dest } + if ($script:SaveOption -eq '1' -and (Test-Path -Path $script:dest)) { $script:CleanupPaths += $script:dest } # If the installers array is empty, create it if (!$script:Installers) { @@ -1719,7 +1363,7 @@ Function Read-QuickInstallerEntry { # Check that MSI's aren't actually WIX, and EXE's aren't NSIS, INNO or BURN Write-Host -ForegroundColor 'Green' "Installer Downloaded!`nProcessing installer data. . . " if ($_NewInstaller['InstallerType'] -in @('msi'; 'exe')) { - $DetectedType = Get-PathInstallerType $script:dest + $DetectedType = Resolve-InstallerType $script:dest if ($DetectedType -in @('msi'; 'wix'; 'nullsoft'; 'inno'; 'burn')) { $_NewInstaller['InstallerType'] = $DetectedType } } # Get the Sha256 @@ -2802,15 +2446,18 @@ if (!$script:UsingAdvancedOption) { if ($Mode -in 1..6) { $UserChoice = $Mode } else { - Write-Host -ForegroundColor 'Yellow' "Select Mode:`n" - Write-MulticolorLine ' [', '1', "] New Manifest or Package Version`n" 'DarkCyan', 'White', 'DarkCyan' - Write-MulticolorLine ' [', '2', '] Quick Update Package Version ', "(Note: Must be used only when previous version`'s metadata is complete.)`n" 'DarkCyan', 'White', 'DarkCyan', 'Green' - Write-MulticolorLine ' [', '3', "] Update Package Metadata`n" 'DarkCyan', 'White', 'DarkCyan' - Write-MulticolorLine ' [', '4', "] New Locale`n" 'DarkCyan', 'White', 'DarkCyan' - Write-MulticolorLine ' [', '5', "] Remove a manifest`n" 'DarkCyan', 'White', 'DarkCyan' - Write-MulticolorLine ' [', '6', "] Move package to a new identifier`n" 'DarkCyan', 'White', 'DarkCyan' - Write-MulticolorLine ' [', 'Q', ']', " Any key to quit`n" 'DarkCyan', 'White', 'DarkCyan', 'Red' - Write-MulticolorLine "`nSelection: " 'White' + Write-Host @" +${vtForegroundYellow} Select Mode: + ${vtForegroundCyan}[${vtForegroundWhite}1${vtForegroundCyan}] New Manifest or Package Version + ${vtForegroundCyan}[${vtForegroundWhite}2${vtForegroundCyan}] Quick Update Package Version + ${vtForegroundCyan}[${vtForegroundWhite}3${vtForegroundCyan}] Update Package Metadata ${vtForegroundGreen}(Note: Must be used only when previous version's metadata is complete.) + ${vtForegroundCyan}[${vtForegroundWhite}4${vtForegroundCyan}] New Locale + ${vtForegroundCyan}[${vtForegroundWhite}5${vtForegroundCyan}] Remove a manifest + ${vtForegroundCyan}[${vtForegroundWhite}6${vtForegroundCyan}] Move package to a new identifier + ${vtForegroundCyan}[${vtForegroundWhite}Q${vtForegroundCyan}] ${vtForegroundRed}Any key to quit + ${vtForegroundDefault} +"@ + Write-Host "Selection: " -NoNewLine # Listen for keypress and set operation mode based on keypress $Keys = @{ @@ -3317,7 +2964,7 @@ Switch ($script:Option) { } # Check that MSI's aren't actually WIX, and EXE's aren't NSIS, INNO or BURN if ($_Installer['InstallerType'] -in @('msi'; 'exe')) { - $DetectedType = Get-PathInstallerType $script:dest + $DetectedType = Resolve-InstallerType $script:dest if ($DetectedType -in @('msi'; 'wix'; 'nullsoft'; 'inno'; 'burn')) { $_Installer['InstallerType'] = $DetectedType } } # Get the Sha256