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.
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.