I did this ages ago and around 2009 hit it very well. What bugged me with existing code was:
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.String
s 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.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 VALUE
s:
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.