Last active
August 21, 2025 09:00
-
-
Save stdStudent/03f1a7942a8a636d576a179ab1d60276 to your computer and use it in GitHub Desktop.
A PowerShell script to determine a VST version (VST2, VST3).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
# License: MPL-2.0 | |
# Text: https://www.mozilla.org/en-US/MPL/2.0/ | |
#> | |
<# | |
.SYNOPSIS | |
Determines the VST plugin type by analyzing the DLL exports. | |
.DESCRIPTION | |
Analyzes a DLL file to determine if it is a VST2 or VST3 plugin by examining its exports. | |
.PARAMETER Path | |
The path to the VST DLL file to analyze. | |
.LINK | |
https://www.mozilla.org/en-US/MPL/2.0/ | |
.EXAMPLE | |
.\Get-VstVersion.ps1 'C:\Program Files\VSTPlugins\MyPlugin.dll' | |
.EXAMPLE | |
PS> . .\Get-VstVersion.ps1 . | |
PS> Get-VstVersion -Path 'C:\Program Files\VSTPlugins\MyPlugin.dll' | |
#> | |
[CmdletBinding()] | |
param ( | |
[Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] | |
[string]$Path | |
) | |
function Get-PEHeaders { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[byte[]]$Bytes | |
) | |
function Get-Int32 ($offset) { [BitConverter]::ToInt32($Bytes, $offset) } | |
function Get-UInt32 ($offset) { [BitConverter]::ToUInt32($Bytes, $offset) } | |
function Get-UInt16 ($offset) { [BitConverter]::ToUInt16($Bytes, $offset) } | |
# Validate DOS header | |
if ($Bytes[0] -ne 77 -or $Bytes[1] -ne 90) { | |
throw "Not a valid PE file (missing MZ signature)." | |
} | |
$e_lfanew = Get-Int32 0x3C | |
# Validate PE signature | |
if ($Bytes[$e_lfanew] -ne 80 -or $Bytes[$e_lfanew + 1] -ne 69 -or | |
$Bytes[$e_lfanew + 2] -ne 0 -or $Bytes[$e_lfanew + 3] -ne 0) { | |
throw "Not a valid PE file (missing PE signature)." | |
} | |
$opt_offset = $e_lfanew + 24 | |
$magic = Get-UInt16 $opt_offset | |
if ($magic -eq 0x10b) { # PE32 | |
$data_dir_offset = $opt_offset + 96 | |
} elseif ($magic -eq 0x20b) { # PE32+ | |
$data_dir_offset = $opt_offset + 112 | |
} else { | |
throw "Unknown PE optional header magic value." | |
} | |
$numberOfSections = Get-UInt16 ($e_lfanew + 6) | |
$sizeOfOptionalHeader = Get-UInt16 ($e_lfanew + 20) | |
$section_offset = $opt_offset + $sizeOfOptionalHeader | |
# Parse sections | |
$sections = @() | |
for ($i = 0; $i -lt $numberOfSections; $i++) { | |
$sec_start = $section_offset + $i * 40 | |
$nameBytes = $Bytes[$sec_start..($sec_start + 7)] | |
$name = [System.Text.Encoding]::ASCII.GetString($nameBytes).TrimEnd("`0") | |
$virtualSize = Get-UInt32 ($sec_start + 8) | |
$virtualAddress = Get-UInt32 ($sec_start + 12) | |
$sizeOfRawData = Get-UInt32 ($sec_start + 16) | |
$pointerToRawData = Get-UInt32 ($sec_start + 20) | |
$sections += [PSCustomObject]@{ | |
Name = $name | |
VirtualAddress = $virtualAddress | |
VirtualSize = $virtualSize | |
PointerToRawData = $pointerToRawData | |
SizeOfRawData = $sizeOfRawData | |
} | |
} | |
return @{ | |
E_LFANEW = $e_lfanew | |
DataDirOffset = $data_dir_offset | |
Sections = $sections | |
} | |
} | |
function Get-AsciiString { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[byte[]]$Bytes, | |
[Parameter(Mandatory=$true)] | |
[int]$Offset | |
) | |
$strBytes = @() | |
$j = 0 | |
while ($Bytes[$Offset + $j] -ne 0 -and ($Offset + $j) -lt $Bytes.Length) { | |
$strBytes += $Bytes[$Offset + $j] | |
$j++ | |
} | |
return [System.Text.Encoding]::ASCII.GetString($strBytes) | |
} | |
function ConvertTo-FileOffset { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[int]$Rva, | |
[Parameter(Mandatory=$true)] | |
[array]$Sections | |
) | |
foreach ($sec in $Sections) { | |
if ($Rva -ge $sec.VirtualAddress -and $Rva -lt ($sec.VirtualAddress + $sec.VirtualSize)) { | |
return $sec.PointerToRawData + ($Rva - $sec.VirtualAddress) | |
} | |
} | |
throw "RVA $Rva not mapped to any section." | |
} | |
function Get-PEExports { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$Path | |
) | |
$bytes = [IO.File]::ReadAllBytes($Path) | |
$headers = Get-PEHeaders -Bytes $bytes | |
function Get-UInt32 ($offset) { [BitConverter]::ToUInt32($bytes, $offset) } | |
# Get export directory | |
$export_rva = Get-UInt32 $headers.DataDirOffset | |
$export_size = Get-UInt32 ($headers.DataDirOffset + 4) | |
if ($export_rva -eq 0 -or $export_size -eq 0) { return @() } | |
$export_offset = ConvertTo-FileOffset -Rva $export_rva -Sections $headers.Sections | |
$num_names = Get-UInt32 ($export_offset + 24) | |
$address_names_rva = Get-UInt32 ($export_offset + 32) | |
$exports = @() | |
if ($num_names -gt 0) { | |
$names_offset = ConvertTo-FileOffset -Rva $address_names_rva -Sections $headers.Sections | |
for ($i = 0; $i -lt $num_names; $i++) { | |
$name_rva = Get-UInt32 ($names_offset + $i * 4) | |
$name_offset = ConvertTo-FileOffset -Rva $name_rva -Sections $headers.Sections | |
$name = Get-AsciiString -Bytes $bytes -Offset $name_offset | |
$exports += $name | |
} | |
} | |
return $exports | |
} | |
function Get-PEImports { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$Path | |
) | |
$bytes = [IO.File]::ReadAllBytes($Path) | |
$headers = Get-PEHeaders -Bytes $bytes | |
function Get-UInt32 ($offset) { [BitConverter]::ToUInt32($bytes, $offset) } | |
# Get import directory (data dir index 1) | |
$import_rva = Get-UInt32 ($headers.DataDirOffset + 8) # after export (index 0), import is +8 | |
if ($import_rva -eq 0) { return @() } | |
$import_offset = ConvertTo-FileOffset -Rva $import_rva -Sections $headers.Sections | |
$imports = @() | |
$descriptor_offset = $import_offset | |
while ($true) { | |
$name_rva = Get-UInt32 ($descriptor_offset + 12) | |
if ($name_rva -eq 0) { break } # end of descriptors | |
$name_offset = ConvertTo-FileOffset -Rva $name_rva -Sections $headers.Sections | |
$dllName = Get-AsciiString -Bytes $bytes -Offset $name_offset | |
$imports += $dllName | |
$descriptor_offset += 20 # size of IMAGE_IMPORT_DESCRIPTOR | |
} | |
return $imports | Sort-Object -Unique | |
} | |
function Get-VstVersion { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$Path | |
) | |
try { | |
$exports = Get-PEExports -Path $Path | |
if ($exports -contains "GetPluginFactory") { | |
return "VST3" | |
} elseif ($exports -contains "VSTPluginMain") { | |
return "VST2" | |
} else { | |
return "Not a recognized VST plugin" | |
} | |
} catch { | |
return "Failed to parse PE file: $($_.Exception.Message)" | |
} | |
} | |
# Entry Point | |
if ($MyInvocation.InvocationName -ne '.') { | |
$result = Get-VstVersion -Path $Path | |
Write-Output $result | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment