Command line manifest replacement tool (PB source, Windows only)

Applications, Games, Tools, User libs and useful stuff coded in PureBasic
User avatar
Kukulkan
Addict
Addict
Posts: 1348
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Command line manifest replacement tool (PB source, Windows only)

Post by Kukulkan »

[UPDATE]
05. Dec. 2023: Fixed bugs regarding the UpdateResource_ Windows API call. Added automatic trimming of imported manifest files (remove any leading or trailing CR, LF, TAB or space). Add compiler warning if not compiled in console mode.

Please use the updated code from this post.
________________________________________________________________________________________________

Hi,

due to recent discussions, I wrote a commandline tool which is able to replace the manifest of a given executable file (.exe). It is also able to show the current manifest. The source code is below.

Thanks to ChrisR for providing a very good starting point to me.

Usage:

Code: Select all

manifest_replace.exe - replace manifest in windows executables
Usage:
  manifest_replace.exe filename.exe [newManifest.xml] [newExeFilename.exe]

  - If only filename.exe is given, it only outputs existing contained manifest.
  - If newManifest.xml is given without newExeFilename.exe, it replaces the manifest in given exe.
  - If all three parameters are given, it creates a copy of exe with replaced manifest.
Please note that you have to provide the new manifest template (Microsoft manifest reference). It does not edit the current one. It replaces it completely with the new one!

The tool retrieves the following attributes from <assemblyIdentity /> out of the existing manifest of the input executable and offers them as placeholder for the new manifest template:

version → %version%
name → %name%
type → %type%
processorArchitecture → %processorArchitecture%
language → %language%
publicKeyToken → %publicKeyToken%

Also, it determines the architecture from the given executable and offers %arch% as placeholder.

This is a possible manifest template to be used as new/replaced manifest (for a non GUI tool):

Code: Select all

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
      <application>
        <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
        <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
        <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
        <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
        <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
      </application>
  </compatibility>
  <assemblyIdentity
    version="%version%"
    processorArchitecture="%arch%"
    name="%name%"
    type="%type%"
    language="%language%" />
  <description></description>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
        type="%type%"
        name="Microsoft.Windows.Common-Controls"
        version="6.0.0.0"
        processorArchitecture="%arch%"
        publicKeyToken="6595b64144ccf1df"
        language="*" />
    </dependentAssembly>
  </dependency>

</assembly>
This is a great start to compile your own manifest for whatever needs. Search this forum for DPI settings etc.

Source:

Code: Select all

; manifest replacement tool
; @license: Public Domain
;
; Tested on PB 6.03 LTS (x86 & x64)

EnableExplicit

CompilerIf #PB_Compiler_ExecutableFormat <> #PB_Compiler_Console
  CompilerWarning "You should compile with option 'Executable format: Console'!"
CompilerEndIf

; GetBinaryType_ constants
#SCS_32BIT_BINARY = 0
#SCS_64BIT_BINARY = 6
#SCS_DOS_BINARY   = 1
#SCS_OS216_BINARY = 5
#SCS_PIF_BINARY   = 3
#SCS_POSIX_BINARY = 4
#SCS_WOW_BINARY   = 2

; helper TRIM function to trim all replaceChar.s characters from in.s
; works from both right and left!
; Example: Debug betterTrim(#CRLF$+" "+#TAB$+"1234567890987654321 "+#CRLF$, " 12"+#CRLF$+#TAB$)
Procedure.s betterTrim(in.s, replaceChars.s)
  Protected l.i = Len(in.s)
  Protected x.i = 0
  ; from left
  For x.i = 1 To l.i
    Protected c.s = Mid(in.s, x.i, 1)
    If FindString(replaceChars.s, c.s) = 0: Break: EndIf
  Next
  in.s = Mid(in.s, x.i)
  ; from right
  Repeat
    c.s = Right(in.s, 1)
    If FindString(replaceChars.s, c.s) = 0: Break: EndIf
    in.s = Left(in.s, Len(in.s) - 1)
  ForEver  
  ProcedureReturn in.s
EndProcedure

; Examines given xml and returns attribute
Procedure.s getManifestVal(manifest.s, path.s, attribute.s, defaultValue.s = "")
  Protected xml.i = ParseXML(#PB_Any, manifest.s)
  If XMLStatus(xml.i) <> #PB_XML_Success
    Print("Failed to parse exe manifest xml! Replacement values will be default!" + #CRLF$)
    Print("XML-Error: " + XMLError(xml.i) + " in line " + XMLErrorLine(xml.i) + #CRLF$)
    ProcedureReturn defaultValue.s
  EndIf
  Protected *main = MainXMLNode(xml.i)
  Protected *myNode = XMLNodeFromPath(*main, path.s)
  If *myNode = 0
    Print("Failed to parse exe manifest node "+path.s+"! Replacement value will be default!" + #CRLF$)
    FreeXML(xml.i)
    ProcedureReturn defaultValue.s
  EndIf
  Protected result.s = GetXMLAttribute(*myNode, attribute.s)
  FreeXML(xml.i)
  If result.s = "": ProcedureReturn defaultValue.s: EndIf
  ProcedureReturn result.s
EndProcedure

; returns manifest of given executable
Procedure.s loadManifest(fileName.s)
  
  ; load to memory
  Protected handle.i = LoadLibraryEx_(@FileName, 0, #LOAD_LIBRARY_AS_DATAFILE)
  If handle.i = 0
    Print("Error reading manifest from existing exe. Is there no manifest yet?" + #CRLF$)
    ProcedureReturn ""
  EndIf
  
  ; extract manifest
  Protected ressource.i = FindResource_(handle.i, 1, #RT_MANIFEST)
  If ressource.i = 0
    Print("Error reading manifest from existing exe. Is there no manifest yet?" + #CRLF$)
    ProcedureReturn ""
  EndIf
  
  Protected load.i = LoadResource_(handle.i, ressource.i)
  Protected size.i = SizeofResource_(handle.i, ressource.i)
  
  Protected manifest.s = PeekS(load.i, size.i, #PB_UTF8)
  
  FreeLibrary_(handle.i)
  
  ProcedureReturn manifest.s
EndProcedure

Procedure.b updateManifest(fileName.s, newManifestFile.s, newExeFile.s)
  Protected lpBinaryType.i
  
  ; get original manifest from exe file
  Protected originalManifest.s = loadManifest(fileName.s)
  If originalManifest.s = ""
    ProcedureReturn #False
  EndIf
  
  ; output manifest if no input manifest was given
  If newManifestFile.s = ""
    Print("Manifest of " + fileName.s + ":" + #CRLF$ + #CRLF$)
    Print(originalManifest.s)
    Print(#CRLF$ + #CRLF$)
    ProcedureReturn #True
  EndIf
  
  ; retrieve replacement values
  ; - mandatory values
  Protected version.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "version", "1.0.0.0")
  Protected name.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "name")
  Protected type.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "type")
  ; - optional values
  Protected language.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "language", "*")
  Protected processorArchitecture.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "processorArchitecture")
  Protected publicKeyToken.s = getManifestVal(originalManifest.s, "/assembly/assemblyIdentity", "publicKeyToken")
  
  ; load new manifest
  Protected file.i = ReadFile(#PB_Any, newManifestFile.s)
  If file.i = 0
    Print("Error opening new manifest file" + #CRLF$)
    ProcedureReturn #False
  EndIf
  Protected manifest.s = ReadString(file.i, #PB_UTF8 | #PB_File_IgnoreEOL)
  CloseFile(file.i)
  
  ; determine binary type from exe
  Protected res.i = GetBinaryType_(@fileName, @lpBinaryType)
  If res.i = 0
    Print("Error determining executable type. Is it really a exe file?" + #CRLF$)
    ProcedureReturn #False
  EndIf
  
  Protected arch.s = ""
  Select lpBinaryType
    Case #SCS_64BIT_BINARY : arch.s = "amd64"
    Case #SCS_32BIT_BINARY : arch.s = "X86"
  EndSelect
  
  ; make a copy and replace there if second exe was given
  If newExeFile.s <> ""
    ; use new exe filename
    If FileSize(newExeFile.s) >= 0: DeleteFile(newExeFile.s): EndIf ; cleanup
    CopyFile(fileName.s, newExeFile.s)
    fileName.s = newExeFile.s
  EndIf
  
  ; open exe file
  Protected handle.i = BeginUpdateResource_(@fileName, #False) 
  If handle.i = 0
    Print("Error opening executable file" + #CRLF$)
    ProcedureReturn #False
  EndIf
  
  ; update manifest replacement placeholders with retrieved values
  manifest.s = ReplaceString(manifest.s, "%arch%", arch.s)
  manifest.s = ReplaceString(manifest.s, "%version%", version.s)
  manifest.s = ReplaceString(manifest.s, "%language%", language.s)
  manifest.s = ReplaceString(manifest.s, "%name%", name.s)
  manifest.s = ReplaceString(manifest.s, "%type%", type.s)
  manifest.s = ReplaceString(manifest.s, "%processorArchitecture%", processorArchitecture.s)
  manifest.s = ReplaceString(manifest.s, "%publicKeyToken%", publicKeyToken.s)
  
  ; clean start and ending of manifest (no linebreaks, no spaces, no tabs allowed at the ends)
  manifest.s = betterTrim(manifest.s, #CRLF$+#TAB$+" ")
  
  ; prepare utf8 memory buffer
  Protected *buffer = AllocateMemory(StringByteLength(manifest.s, #PB_UTF8))
  Protected lenNewManifest.i = PokeS(*buffer, manifest.s, -1, #PB_UTF8)
  
  ; update ressource
  UpdateResource_(handle.i, #RT_MANIFEST, 1, 1033, *buffer, lenNewManifest.i)
  
  EndUpdateResource_(handle.i, #False)
  
  FreeMemory(*buffer); cleanup
  
  Print("Manifest successful replaced." + #CRLF$)
  ProcedureReturn #True
EndProcedure

Procedure main()
  Protected filename.s = ProgramParameter(0)
  Protected manifest.s = ProgramParameter(1)
  Protected newFilename.s = ProgramParameter(2)
  
  If filename.s = "" Or filename.s = "-h" Or filename.s = "--help" Or filename.s = "/?"
    Print("manifest_replace.exe - replace manifest in windows executables" + #CRLF$)
    Print("Usage:" + #CRLF$)
    Print("  manifest_replace.exe filename.exe [newManifest.xml] [newExeFilename.exe]" + #CRLF$ + #CRLF$)
    Print("  - If only filename.exe is given, it only outputs existing contained manifest." + #CRLF$)
    Print("  - If newManifest.xml is given without newExeFilename.exe, it replaces the manifest in given exe." + #CRLF$)
    Print("  - If all three parameters are given, it creates a copy of exe with replaced manifest." + #CRLF$ + #CRLF$)
    ProcedureReturn
  EndIf
  
  updateManifest(filename.s, manifest.s, newFilename.s)
  
EndProcedure

OpenConsole() 
main()
CloseConsole()
If you found errors or enhanced it, please feel free to post it here below.