How to extract version information using the Windows API
Introduction
The Windows API provides a method to extract version information from an executable file. The API is rather archane and some knowledge of how version information is stored in an executable file helps to understand how to use it
This article starts with a brief overview of how version information is laid out in a file. It then goes on to develop some code that tests whether version information is present and extracts it if so. We then wrap the code up in a class before finally looking at some of the limitations of the approach we have taken.
Version Information Overview
Version information is stored in an executable file's resources. The binary format of this data is complex and is not described here. Conceptually though, version information contains a record that provides a machine-readable binary description of a file (fixed file information) and one or more string tables that provide human-readable information. String tables can be localised – so there can be a string table for each supported language or code page. Because of this, version information also contains a table of supported languages and associated code pages (the translation table) that effectively provides an index into the string tables.
Although version information supports multiple languages it is rare to find executables that take advantage of this – there is usually only one string table. Most of the source code I have seen only handles one string table, but the code we will discuss here can handle multiple string tables.
Reflecting the organisation discussed above, version information is structured in three main parts.
Fixed file information
This is a data structure that contains binary information about the executable file. Windows declares this structure as VS_FIXEDFILEINFO and the Delphi equivalent is TVSFixedFileInfo, defined as follows:
1type
2 tagVS_FIXEDFILEINFO = packed record
3 dwSignature: DWORD;
4 dwStrucVersion: DWORD;
5 dwFileVersionMS: DWORD;
6 dwFileVersionLS: DWORD;
7 dwProductVersionMS: DWORD;
8 dwProductVersionLS: DWORD;
9 dwFileFlagsMask: DWORD;
10 dwFileFlags: DWORD;
11 dwFileOS: DWORD;
12 dwFileType: DWORD;
13 dwFileSubtype: DWORD;
14 dwFileDateMS: DWORD;
15 dwFileDateLS: DWORD;
16 end;
17
18 TVSFixedFileInfo = tagVS_FIXEDFILEINFO;
Listing 1
The fields of this record are described the Windows documentation so we won't discuss them further here.
Variable file information
According to the Windows documentation, this section can be user-defined. However, in practise it always contains a table of "translation" information. Each entry in the table is a pair of language and character set identifiers that together provide a key that can be used to access a string table. We can define an entry in this table with the following record (not defined by Windows):
1type
2 TTransRec = packed record
3 Lang,
4 CharSet: Word;
5 end;
Listing 2
String file information
This section stores a string table for each "translation" listed in the variable file information section. The entries in string tables are name / value pairs – i.e. the string values are accessed by specifying a string name. Windows defines several standard names:
- Comments
- CompanyName
- FileDescription
- FileVersion
- InternalName
- LegalCopyright
- LegalTrademarks
- OriginalFilename
- PrivateBuild
- ProductName
- ProductVersion
- SpecialBuild
The intended purpose and restrictions on use of these names are set out in the Windows documentation for the "VERSIONINFO resource" topic. User defined names are also permitted.
Accessing the data
These different sections of the resource are accessed using an addressing system that is similar to a file path. The "paths" are:
\
-
Accesses fixed file information.
\VarFileInfo\Translation
-
Accesses the "translation" table. The table is a sequence of TTransRec records.
\StringFileInfo\<translation-code>\<string-name>
-
Accesses a named entry in a given string table, where
<translation-code>
is a concatenation the hexadecimal representations of the translation's language code and the associated character set code.
<string-name>
is the name of the desired string value.
For example to access the "ProductName" string in the string table associated with translation "040904E4" the "path" is \StringFileInfo\040904E4\ProductName
.
This all looks rather horrendous – and if you write code to access the raw version information binary it is (believe me, I've done it!). However the Windows API provides three functions that assist in extracting the code. In the next section we'll review the API calls and write some Delphi wrappers to make them easier to use before finally developing our version information class.
Writing the code
Windows API functions
Windows provides the following functions to use to access version information.
- GetFileVersionInfoSize
-
1function GetFileVersionInfoSize(lptstrFilename: PChar;
2 var lpdwHandle: DWORD): DWORD; stdcall;
Listing 3
This function returns the size of the given executable file's version information. It returns 0 if there is no version information present. We have to pass a dummy variable as lpdwHandle – the function just sets it to 0. Who comes up with this stuff?!
-
GetFileVersionInfo
-
1function GetFileVersionInfo(lptstrFilename: PChar;
2 dwHandle, dwLen: DWORD; lpData: Pointer): BOOL; stdcall;
Listing 4
Once we know the size of the version information we have to create a buffer the same size and then call this function to copy the version information from the executable file into the buffer. We pass the name of the file, a dummy 0 value to dwHandle, followed by the size of the buffer and the buffer pointer itself. The function returns true on success and false on failure.
-
VerQueryValue
-
1function VerQueryValue(pBlock: Pointer; lpSubBlock: PChar;
2 var lplpBuffer: Pointer; var puLen: UINT): BOOL; stdcall;
Listing 5
This function is used to read data from the version information buffer we filled using GetFileVersionInfo. We pass the buffer as the first parameter. The next parameter takes a pointer to the "path" that specifies the information we need (as described above). The function sets the pointer variable passed as lplpBuffer to point to the requested data. puLen is set to the length of the data. The function returns true on success and false on failure. If the function fails lplpBuffer has an indeterminate value and can cause a GPF if read.
A full description of these functions can be read in the Windows API documentation. As can be seen the calls we need to make are complex and require a lot of pointer manipulation. In the next section we hide some of this complexity in wrapper routines.
Delphi wrapper routines
Let us now define some wrapper functions that (a) hide the API routines and (b) give easy access to the fixed file information, string values and translation table.
We will create five routines:
- GetVerInfoSize – a thin wrapper round the GetFileVersionInfoSize API function.
- GetVerInfo – a thin wrapper round the GetFileVersionInfo API function.
- GetFFI – gets the fixed file information from the version information buffer using VerQueryValue.
- GetTransTable – uses VerQueryValue to get and return the translation table as a dynamic array of TTransRec.
- GetVerInfoStr – returns the value of a string table entry using VerQueryValue.
GetVerInfoSize
1function GetVerInfoSize(const FileName: string): Integer;
2var
3 Dummy: DWORD;
4begin
5 Result := GetFileVersionInfoSize(PChar(FileName), Dummy);
6end;
Listing 6
This function simply prevents us from having to provide the dummy parameter that is required when calling GetFileVersionInfoSize.
GetVerInfo
1procedure GetVerInfo(const FileName: string; const Size: Integer;
2 const Buffer: Pointer);
3begin
4 if not GetFileVersionInfo(PChar(FileName), 0, Size, Buffer) then
5 raise Exception.Create('Can''t load version information');
6end;
Listing 7
This wrapper function just calls down into GetFileVersionInfo and raises an exception if the call fails.
GetFFI
1function GetFFI(const Buffer: Pointer): TVSFixedFileInfo;
2var
3 Size: DWORD;
4 Ptr: Pointer;
5begin
6
7 if not VerQueryValue(Buffer, '\', Ptr, Size) then
8 raise Exception.Create('Can''t read fixed file information');
9
10 if Size <> SizeOf(TVSFixedFileInfo) then
11 raise Exception.Create('Fixed file information record wrong size');
12 Result := PVSFixedFileInfo(Ptr)^;
13end;
Listing 8
GetFFI gets fixed file information from the version information data. We call VerQueryValue with a "path" of \ and then check if the call succeeds and that the returned data is the correct size. Exceptions are raised if any error is detected.
GetTransTable
1type
2 TTransRec = packed record
3 Lang,
4 CharSet: Word;
5 end;
6 PTransRec = ^TTransRec;
7 TTransRecArray = array of TTransRec;
8
9function GetTransTable(const Buffer: Pointer): TTransRecArray;
10var
11 TransRec: PTransRec;
12 Size: DWORD;
13 RecCount: Integer;
14 Idx: Integer;
15begin
16
17 VerQueryValue(
18 Buffer, '\VarFileInfo\Translation', Pointer(TransRec), Size
19 );
20
21 RecCount := Size div SizeOf(TTransRec);
22 SetLength(Result, RecCount);
23
24 for Idx := 0 to Pred(RecCount) do
25 begin
26 Result[Idx] := TransRec^;
27 Inc(TransRec);
28 end;
29end;
Listing 9
This routine fetches the translation table from the version information. It gets the raw data using VarFileInfo then calculates the number of translation records by dividing the size of the raw data by the size of a translation record. We then size the dynamic array appropriately and copy each record into the array.
We also need to define the TTransRec, PTransRec and TTransRecArray types required by the routine.
GetVerInfoStr
1function GetVerInfoStr(const Buffer: Pointer;
2 const Trans, StrName: string): string;
3var
4 Data: PChar;
5 Dummy: DWORD;
6 Path: string;
7begin
8
9 Path := '\StringFileInfo\' + Trans + '\' + StrName;
10
11 if VerQueryValue(Buffer, PChar(Path), Pointer(Data), Dummy) then
12 Result := Data
13 else
14 Result := '';
15end;
Listing 10
This function returns the value of given string from the string table associated with a given translation. We build the "path" needed to access the string value and use VerQueryValue to get the string value. If the string exists we return the data, cast to a string. Otherwise we return the empty string.
At first sight it may seem strange that we don't allocate memory for Data before calling VerQueryValue, but there's no need because VerQueryValue sets Data to point to a part of the given Buffer. What is important is that we make a copy of the memory pointed to by Data before returning from our function, which happens implicitly in line 12.
The demo program accompanying this article shows how to use the routines.
The next step is to encapsulate the whole process in a class.
Object oriented solution
The class we will develop is rather simple but has all the needed functionality. It can detect whether a file contains version information and can extract fixed file information, the translation table and strings from a given string table. Here's the class declaration:
1type
2 TVerInfo = class(TObject)
3 private
4 fFixedFileInfo: TVSFixedFileInfo;
5 fTransTable: TTransRecArray;
6 fHasVerInfo: Boolean;
7 fVerInfo: Pointer;
8 function GetString(const Trans, Name: string): string;
9 function GetTranslation(Idx: Integer): string;
10 function GetTranslationCount: Integer;
11 public
12 constructor Create(const FileName: string);
13 destructor Destroy; override;
14 property HasVerInfo: Boolean read fHasVerInfo;
15 property FixedFileInfo: TVSFixedFileInfo read fFixedFileInfo;
16 property Translations[Idx: Integer]: string read GetTranslation;
17 property TranslationCount: Integer read GetTranslationCount;
18 property Strings[const Trans, Name: string]: string read GetString;
19 end;
Listing 11
The constructor receives the name of the file to be examined. The HasVerInfo property is true if the file contains version information. The FixedFileInfo property contains a copy of the fixed file info record. TranslationCount records the number of translations in the file while Translations is a zero based array property of string values corresponding to the translations. Strings is a two dimensional array property that returns the value of a string table entry with name Name in the string table specified by Trans.
Internally we store the translation table in a dynamic array of TTransRec records. Both the translation and the fixed file information are recorded when the class is created. String file information is read from version information as values are requested from the Strings property.
Most of the work is done in the constructor. We use the routines developed above to simplify the code. So, let us start by looking at the constructor:
1constructor TVerInfo.Create(const FileName: string);
2var
3 BufSize: Integer;
4begin
5 inherited Create;
6
7 BufSize := GetVerInfoSize(FileName);
8 fHasVerInfo := BufSize > 0;
9 if fHasVerInfo then
10 begin
11
12 GetMem(fVerInfo, BufSize);
13 GetVerInfo(FileName, BufSize, fVerInfo);
14
15 fFixedFileInfo := GetFFI(fVerInfo);
16 fTransTable := GetTransTable(fVerInfo);
17 end;
18end;
Listing 12
We first use GetVerInfoSize to find the size of the version information data. We set the HasVerInfo property to record if there is version information (by checking the size of the data), and if there is none, we skip the rest of the code.
If we do have version information we allocate a buffer to hold it then load the version information from the file using the GetVerInfo routine. We next use the GetFFI and GetTransTable routines to store the fixed file information and translation table in fields.
The destructor is very simple: we just free the version info buffer:
1destructor TVerInfo.Destroy;
2begin
3
4 FreeMem(fVerInfo);
5 inherited;
6end;
Listing 13
We'll look at the GetTranslation method next. This simple method returns a string representation of the translation at a given index in the table. The string representation is simply the concatenation of the translation's language code and character set / code page code in hexadecimal:
1function TVerInfo.GetTranslation(Idx: Integer): string;
2begin
3 Assert(fHasVerInfo);
4 Assert((Idx >= 0) and (Idx < fTranslationCount));
5
6 Result := Format(
7 '%4.4x%4.4x', [fTransTable[Idx].Lang, fTransTable[Idx].CharSet]
8 );
9end;
Listing 14
Since this method should not be called when there is no version information available we use an assertion to guard against this. The other assertion simply checks that the requested index is in range.
The GetTranslationCount method simply returns the size of the dynamic array that stores the translation table:
1function TVerInfo.GetTranslationCount: Integer;
2begin
3 Result := Length(fTransTable);
4end;
Listing 15
There is no need to check whether version information is available since this method will return 0 in that case.
This leaves just the GetString method, which is trivial:
1function TVerInfo.GetString(const Trans, Name: string): string;
2begin
3 Assert(fHasVerInfo);
4 Result := GetVerInfoStr(fVerInfo, Trans, Name);
5end;
Listing 16
The heavy lifting is done by the GetVerInfoStr routine. We simply check we have version information and then return the result of calling GetVerInfoStr. Recall that GetVerInfoStr returns the empty string if there is no such string name in the given translation.
That's the class completed. Again this can be tested using the demo code (below).
It's not the best of practise to leave much of the functionality of this class in routines outside the class. Normally I would move those routines inside the class and adjust them accordingly. But that would lengthen this article considerably while adding no new pertinent information. So I'll leave that tidying up as an exercise!
Demo Code
A demo program to accompany this article can be found in the delphidabbler/article-demos
Git repository on GitHub.
You can view the code in the article-20
sub-directory. Alternatively download a zip file containing all the demos by going to the repository's landing page and clicking the Clone or download button and selecting Download ZIP.
See the demo's README.md file for details.
This source code is merely a proof of concept and is intended only to illustrate this article. It is not designed for use in its current form in finished applications. The code is provided on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.
The demo is open source. See the demo's LICENSE.md file for licensing details.
For a more comprehensive solution to the problem of extracting version information see my Version Information Component.
Limitations
While the solution works well in the majority of cases, it is by no means perfect. There are two major shortcomings:
- While standard and non-standard string information can be accessed, the user must know in advance what non-standard string names are present in the version information. The code cannot enumerate the string names. This can't be done using the official API: more advanced methods are required.
- Several programs contain version information that does not follow the Microsoft guidelines – i.e. the structure of the binary version information is non-standard. The API functions can fail on some of these programs
I have written a DLL, VIBinData, that solves most of these problems by parsing the version information binary resource data and being tolerant of variations from the standard. So if you need to handle badly behaved programs, or want to enumerate all the strings in a string table, feel free to uses it. It is quite well documented. To see how to parse the raw version information data take a look at the source code.
Note that VIBinData.dll
is a 32 bit only DLL.
Summary
In this article we have reviewed how Windows handles version information and makes it accessible to programmers via the API. We noted that the API is rather cumbersome and so went on to develop wrapper routines to hide some of the complexity. We then wrote a class that encapsulates the version information and provides a simpler interface for the developer. A demo program was provided that implements and tests the code. Finally we reviewed some of the limitations of the code.
Feedback
I hope you found this article useful.
If you have any observations, comments, or have found any errors there are two places you can report them.
- For anything to do with the article content, but not the downloadable demo code, please use this website's Issues page on GitHub. Make sure you mention that the issue relates to "article #20".
- For bugs in the demo code see the
article-demo
project's README.md
file for details of how to report them.