79253026

Date: 2024-12-04 23:49:58
Score: 0.5
Natty:
Report link

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

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.

Reasons:
  • Blacklisted phrase (1.5): I cannot find
  • Blacklisted phrase (1): stackoverflow
  • Blacklisted phrase (0.5): I cannot
  • Long answer (-1):
  • Has code block (-0.5):
  • High reputation (-1):
Posted by: AmigoJack