Search code examples
c++delphifileversioninfo

Extract VERSIONINFO::StringFileInfo blocks


If you are generating a Windows binary, you can write a .rc file similar to the following:

// Version information resource file
#include "windows.h"

1 VERSIONINFO
FILEVERSION 1, 2, 3, 4
FILEOS 0x00000004L  // VOS_WINDOWS32 (32-bit Windows)
FILETYPE VFT_APP
{
    BLOCK "VarFileInfo"
    {
        VALUE "Translation", 0x0809, 0x04E4
            // 0x0809 = Locale ID: English (United Kingdom)
            // 0x04E4 = Code page: 1252 (Multilingual)
    }

    BLOCK "StringFileInfo"
    {
        // Note: The block address is a concatenation of the locale ID and code page
        BLOCK "080904E4"
        {
            VALUE "CompanyName", "MyCompanyName\0"
        }
    }
}

This gets compiled into a .res file which is linked into the final binary. On Windows, you might right-click the binary to view version information.

The following Delphi code demonstrates extracting a version string. This could be adapted to another language like C++ or C#.

procedure TMyApp.GetFileVersionString();
var
    path, versionString: String;
    versionSize, versionLength: DWORD;
    pstrBuffer, pstrValue: PAnsiChar;
begin
    // Extract Windows version information from a binary
    versionString := '';
    path := 'c:\path\to\someBinary.exe';
    if (FileExists(path)) then
        begin
        versionSize := GetFileVersionInfoSize(PAnsiChar(path), versionSize);
        if (versionSize > 0) then
            begin
            pstrBuffer := AllocMem(versionSize);
            try
                // Query values in StringFileInfo block "080904E4":
                // * Locale = English (United Kingdom) [0x0809]
                // * Code page = 1252 (Multilingual) [0x04E4]
                // Note: The default "English (United States)" locale would be "040904E4"
                GetFileVersionInfo(PAnsiChar(path), 0, versionSize, pstrBuffer);
                if (VerQueryValue(
                        pstrBuffer,
                        PAnsiChar('StringFileInfo\080904E4\CompanyName'),
                        Pointer(pstrValue), versionLength)) then
                    begin
                        pstrValue := PAnsiChar(Trim(pstrValue));
                        if (Length(pstrValue) > 0) then
                            versionString := pstrValue // "MyCompanyName"
                        else
                            ; // Version info string is empty (i.e. not set)
                    end
                else
                    begin
                    // Version info string not present in VersionInfo::StringFileInfo block
                    // or you are looking at the wrong block
                    end;
            finally
                FreeMem(pstrBuffer, dwSize);
            end;

            end
        else
            ; // File does not contain FileVersionInfo resource
        end;
end;

The default locale is "English (United States)" when the block to look for would be "040904E4" instead.

My question is how to determine, without advance knowledge, which blocks (like "080904E4" or "040904E4") are available in the VersionInfo::StringFileInfo section? C/C++, Delphi or any other language is fine.


Solution

  • I did this ages ago and around 2009 hit it very well. What bugged me with existing code was:

    • Almost everybody expected only exactly 1 Translation. However, there may be multiple ones and that's also the whole sense of having them: so a localized Windows installation can pick the appropriate one.
    • There is no WinAPI function to list all Strings that actually occur in a block. Everybody (and even modern Windows versions) only expect predefined keys like FileDescription and CompanyName. If you have custom entries (f.e. Compiler) Explorer won't display it anymore. One needs to parse the whole resource to actually get all of those key=value pairs.
    • I also encountered 16-bit executables that have an older resource format (ASCII instead of UCS-2) and can parse it, too. Currently I cannot find the specs for that anymore. Note that even the WinAPI can do this: VerQueryValueW() states:

      This function works on 16-, 32-, and 64-bit file images.

    The following program does both: WinAPI approach and listing all translations (not only one), and later parses the whole resource itself.

    program FileVersionRes;
    
    {$APPTYPE CONSOLE}
    
    uses
      SysUtils, Math, Windows;
    
    // A whole 32-bit/Unicode VERSIONINFO block of memory that was returned
    // by GetFileVersionInfoW().
    // https://learn.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
    function Parse
    ( pVerInfo: PByte
    ; iSizeVer: Cardinal
    ): Boolean;
    var
      bOld: Boolean;  // 16-bit resource?
      iRead: Cardinal;  // How many bytes of pVerInfo have been read so far.
    
      // Advance memory pointer and count "iRead" accordingly.
      function ReadBytes( iAmount: Word ): Pointer;
      begin
        result:= pVerInfo;
        Inc( pVerInfo, iAmount );
        Inc( iRead, iAmount );
      end;
    
      // The docs were talking of padding because of 16-bit alignment. But they
      // never meant the VERSION block - they meant the position of bytes in
      // the WHOLE file! So we have to check our position instead of the size
      // we already read.
      function ReadPadding(): Boolean;
      begin
        result:= FALSE;
    
        // Unicode also only pads 1 byte, so we don't have to distinguish between old and new struct.
        while iRead mod 4> 0 do begin
          if iRead>= iSizeVer then exit;
          ReadBytes( 1 );
        end;
    
        result:= TRUE;
      end;
    
      // Read either ASCII (old 16-bit resource) or UNICODE (32-bit) text,
      // which is always ended by a NUL byte/word. Keys of "String"s don't
      // have any length indicator, only the values have (since they don't
      // need to be text always).
      function ReadText
      ( var sText: Widestring  // Characters to be read and returned.
      ; iMinLength: Cardinal= 0  // In characters, not bytes.
      ; iMaxLength: Cardinal= MAXDWORD
      ): Boolean;
      var
        c: WideChar;  // Read letter.
        iPlus: Cardinal;  // Either 0 (ASCII) or 1 (UCS-2).
      begin
        result:= FALSE;
        sText:= '';
        if bOld then iPlus:= 0 else iPlus:= 1;  // 16-bit: octets. 32-bit: words.
    
        while iMaxLength> 0 do begin
          // Is it even possible to read (anymore)?
          if iRead+ iPlus>= iSizeVer then exit;
          if bOld then c:= WideChar(PChar(ReadBytes( 1 ))^) else c:= PWidechar(ReadBytes( 2 ))^;
          Dec( iMaxLength );
    
          if iMinLength> 0 then Dec( iMinLength );
          if c= WideChar(#0) then break;  // End of text.
          sText:= sText+ c;
        end;
        while (iMinLength> 0) and (iMaxLength> 0) do begin
          // Is it even possible to read (anymore)?
          if iRead+ iPlus>= iSizeVer then exit;
          ReadBytes( 1+ iPlus );
    
          Dec( iMinLength );
          Dec( iMaxLength );
        end;
    
        if not ReadPadding() then exit;
    
        result:= TRUE;
      end;
    
      // One "String", consisting of length, value length, type, key and padding.
      // https://learn.microsoft.com/en-us/windows/win32/menurc/string-str
      function ReadEntry
      ( var iLenEntry, iLenValue, iType: Word  // Returned.
      ; var sKey: Widestring  // Returned.
      ; bDetectOld: Boolean= FALSE  // We only need/can detect this once at the start of the whole resource.
      ; iLoopMax: Integer= 1  // In "\StringFileInfo\*\*" things can become odd (maybe bad padding of previous entries).
      ): Boolean;
      var
        iHeader: Word;  // How much was read before parsing the key's text.
      begin
        result:= FALSE;
    
        // How big the whole entry is (bytes).
        repeat
          if iRead+ 2> iSizeVer then exit;
          iLenEntry:= PWord(ReadBytes( 2 ))^;
          Dec( iLoopMax );
        until (iLenEntry> 0) or (iLoopMax<= 0);  // Normally only one iteration.
        if iLenEntry> iSizeVer- iRead then exit;  // Impossible value: outside of memory.
    
        // How long the value is (in "words", but actually characters).
        if iRead+ 2> iSizeVer then exit;
        iLenValue:= PWord(ReadBytes( 2 ))^;
        if iLenValue div 2> iSizeVer - iRead then exit;  // Impossible value: outside of memory.
    
        // Only 32-bit resource knows "type".
        if not bOld then begin
          if iRead+ 2> iSizeVer then exit;
          iType:= PWord(ReadBytes( 2 ))^;
          iHeader:= 6;
    
          if bDetectOld then begin
            if iType= $5356 then begin  // Already read "VS" (of "VS_VERSION_INFO")?
              Writeln( '  (Old 16-bit struct detected: no types.)' );
              bOld:= TRUE;
              iType:= 0;
    
              // Unread type.
              Dec( pVerInfo, 2 );
              Dec( iRead, 2 );
              iHeader:= 4;
            end;
          end;
        end else begin
          iType:= 0;
          iHeader:= 4;
        end;
    
        // Keys don't have any length indication, but we always have a maximum.
        if not ReadText( sKey, 0, (iLenEntry- iHeader) div 2 ) then exit;
    
        result:= TRUE;
      end;
    
      // Handles both "\VarFileInfo\" and "\StringFileInfo\", which can come in any
      // order.
      function Read_X_FileInfo(): Boolean;
    
        // Reading "\VarFileInfo\", should only occur once.
        function ReadVar(): Boolean;
        var
          iLenEntry, iLenValue, iType: Word;
          sKey: Widestring;
          iValue, iReadLocal: Cardinal;
        begin
          result:= FALSE;
          iReadLocal:= iRead;
          Writeln( '  \VarFileInfo\:' );
    
          // The key should be "Translation".
          if not ReadEntry( iLenEntry, iLenValue, iType, sKey ) then exit;
    
          // There can be more than one localization.
          while iRead- iReadLocal< Cardinal(iLenEntry- 3) do begin
            iValue:= PDWord(ReadBytes( 4 ))^;
            iValue:= (iValue shr 16)  // Language.
                  or ((iValue and $FFFF) shl 16);  // Charset/Codepage.
    
            Writeln( '  - ', sKey, ': ', IntToHex( iValue, 8 ) );
          end;
    
          result:= TRUE;
        end;
    
        // Reading "\StringFileInfo\", can occur multiple times.
        function ReadStringTable(): Boolean;
    
          // One of the many version key=value pairs like "ProductName" or "FileDescription".
          // Keys can be freely chosen, although nowadays nobody expects those anymore.
          function ReadString(): Boolean;
          var
            iLenEntry, iLenValue, iType: Word;
            sKey, sValue: Widestring;
            iReadLocal: Cardinal;
          begin
            result:= FALSE;
            iReadLocal:= iRead;
    
            // THESE are the "might have"-paddings. We can simply recognize them here since
            // lengths of 0 are not allowed/expected. However, to avoid deadlocks we let
            // this loop only iterate 10 times.
            if not ReadEntry( iLenEntry, iLenValue, iType, sKey, FALSE, 10 ) then exit;
    
            // Zero length values must be detected (although not allowed from specs)!
            if iLenValue> 0 then begin
              if not ReadText( sValue, iLenValue, (iLenEntry- (iRead- iReadLocal)) div 2 ) then exit;
            end else sValue:= '';
            Writeln( '    - (String) ', sKey, ' = ', sValue );
    
            result:= TRUE;
          end;
    
        var
          iLenEntry, iLenValue, iType: Word;
          sKey: Widestring;
          iReadLocal: Cardinal;
        begin
          result:= FALSE;
          iReadLocal:= iRead;
          Writeln( '  \StringFileInfo\:' );
    
          if not ReadEntry( iLenEntry, iLenValue, iType, sKey ) then exit;
          Writeln( '  + Language=', sKey );  // This lang+charset is really a text, like "080904E4".
    
          // There's no indicator how many pairs come...
          while iRead- iReadLocal< iLenEntry do begin
            if not ReadString() then exit;
    
            // Undocumented: "String" values might also have padding trails!
            // We skip 0-length descriptors when reading those.
          end;
    
          result:= TRUE;
        end;
    
      var
        iLenEntry, iLenValue, iType: Word;
        sKey: Widestring;
      begin
        result:= FALSE;
    
        if not ReadEntry( iLenEntry, iLenValue, iType, sKey ) then exit;
    
        // The only 2 known block types.
        if sKey= 'VarFileInfo' then begin
          if not ReadVar() then exit;
        end else
        if sKey= 'StringFileInfo' then begin
          if not ReadStringTable() then exit;
        end else begin
          Writeln( '+ Unexpected FileInfo block: ', sKey );
          exit;
        end;
    
        result:= TRUE;
      end;
    
    var
      iLenEntry, iLenValue, iType: Word;
      sKey: Widestring;
    begin
      result:= FALSE;
      bOld:= FALSE;  // No 16-bit resource recognized yet.
      iRead:= 0;  // Nothing read so far.
    
      if not ReadEntry( iLenEntry, iLenValue, iType, sKey, TRUE ) then exit;
    
      Writeln( '+ ', sKey );  // Should be "VS_VERSION_INFO".
    
      if iLenValue> 0 then begin
        Writeln( '  (Skipping ', iLenValue, ' bytes of "TVSFixedFileInfo".) ');
        if iRead+ iLenValue> iSizeVer then exit;
        ReadBytes( iLenValue );
      end;
    
      if not ReadPadding() then exit;
    
      while iRead< Min( iSizeVer, iLenEntry ) do begin
        if not Read_X_FileInfo() then exit;
      end;
    
      result:= TRUE;
    end;
    
    procedure One( sModule: Widestring );
    var
      iSizeVer, iVoid, iSizeVal, iSizeTrans: Cardinal;
      pVerInfo: Pointer;
      pLangCp: PDWord;
      sSubBlock, sSubVer, sName, sValue: Widestring;
      pText: PWideChar;
    begin
      // Size needed.
      iSizeVer:= GetFileVersionInfoSizeW( PWideChar(sModule), iVoid );
      if iSizeVer> 0 then begin
        GetMem( pVerInfo, iSizeVer );
        try
          // Got version resource?
          if GetFileVersionInfoW( PWideChar(sModule), 0, iSizeVer, pVerInfo ) then try
            // Get all translations.
            sSubBlock:= '\VarFileInfo\Translation';
            if VerQueryValueW( pVerInfo, PWideChar(sSubBlock), Pointer(pLangCp), iSizeTrans ) then begin
              while iSizeTrans>= 4 do begin
                sSubVer:= '\StringFileInfo\'
                        + SysUtils.IntToHex( LoWord(pLangCp^), 4 )
                        + SysUtils.IntToHex( HiWord(pLangCp^), 4 )
                        + '\';
                Writeln( '* Language ', sSubVer, ':' );
    
                // Query one key in that translation which hopefully exists. But this approach is
                // flawed - the WinAPI provides no function that lists all keys that actually
                // exist in this block. And there may be others than only the predefined ones.
                sName:= sSubVer+ 'FileDescription';
                if VerQueryValueW( pVerInfo, PWideChar(sName), Pointer(pText), iSizeVal ) then try
                  SetString( sValue, pText, iSizeVal );
                  Writeln( '  - value = ', sValue );
                except
                end;
    
                // Advance to next translation.
                Inc( pLangCp );
                Dec( iSizeTrans, SizeOf( DWord ) );
              end;
            end;
    
    
            // Now let's parse everything on our own.
            Writeln;
            case Parse( pVerInfo, iSizeVer ) of
              TRUE: Writeln( 'Parsing successfully ended.' );
              FALSE: Writeln( 'Unexpected end of VERSION resource!' );
            end;
          except
          end;
        finally
          FreeMem( pVerInfo, iSizeVer );
        end;
      end;
    end;
    
    begin
      One( 'C:\Windows\System32\Explorer.exe' );  // Well-known executable.
    end.
    

    As sample resource you can compile this one, which has the most important features: multiple translations and custom VALUEs:

    1 VERSIONINFO
    FILEVERSION     4, 55, 0, 0x0000
    PRODUCTVERSION  0, 0, 0, 0
    FILEOS 0x4
    FILETYPE 0x1 {
      BLOCK "StringFileInfo" {
        BLOCK "00000000" {
          VALUE "FileDescription", "Program\000" 
          VALUE "FileVersion", "4.55\000" 
          VALUE "Date", "2024-12-05\000" 
          VALUE "LegalCopyright", "AmigoJack\000" 
          VALUE "Stack Overflow", "https://stackoverflow.com/q/79251337/4299358\000" 
        }
      }
      BLOCK "StringFileInfo" {
        BLOCK "080904E4" {
          VALUE "FileDescription", "other description\000" 
          VALUE "Compiler", "Delphi 7.0\000" 
          VALUE "Come find me", "Which program displays this metadata?\000" 
        }
      }
      BLOCK "VarFileInfo" {
        VALUE "Translation", 0x0000, 0x0000, 0x0809, 0x04E4
      }
    }
    

    The layout of such a resource is explained in VERSIONINFO, which then refers to StringFileInfo and VarFileInfo blocks.