mirror of
https://github.com/audacity/winget-pkgs.git
synced 2026-04-16 21:52:15 -05:00
412 lines
18 KiB
PowerShell
412 lines
18 KiB
PowerShell
####
|
|
# 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
|