Skip over navigation

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; // always $feef04bd
  4    dwStrucVersion: DWORD; // version of structure
  5    dwFileVersionMS: DWORD; // most significant file version
  6    dwFileVersionLS: DWORD; // least significant file version
  7    dwProductVersionMS: DWORD; // most significant product version
  8    dwProductVersionLS: DWORD; // last significant product version
  9    dwFileFlagsMask: DWORD; // masks valid flags in dwFileFlags
 10    dwFileFlags: DWORD; // bit mask of attributes of file
 11    dwFileOS: DWORD; // flags describing target OS
 12    dwFileType: DWORD; // flag describing type of file
 13    dwFileSubtype: DWORD; // sub type for file: keyboard driver
 14    dwFileDateMS: DWORD; // most significant part of file date
 15    dwFileDateLS: DWORD; // least significant part of file date
 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, // language code
  4    CharSet: Word; // character set (code page)
  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:

  1. GetVerInfoSize – a thin wrapper round the GetFileVersionInfoSize API function.
  2. GetVerInfo – a thin wrapper round the GetFileVersionInfo API function.
  3. GetFFI – gets the fixed file information from the version information buffer using VerQueryValue.
  4. GetTransTable – uses VerQueryValue to get and return the translation table as a dynamic array of TTransRec.
  5. GetVerInfoStr – returns the value of a string table entry using VerQueryValue.

GetVerInfoSize

  1function GetVerInfoSize(const FileName: string): Integer;
  2var
  3  Dummy: DWORD; // Dummy handle parameter
  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; // Size of fixed file info read
  4  Ptr: Pointer; // Pointer to FFI data
  5begin
  6  // Read the fixed file info
  7  if not VerQueryValue(Buffer, '\', Ptr, Size) then
  8    raise Exception.Create('Can''t read fixed file information');
  9  // Check that data read is correct size
 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, // language code
  4    CharSet: Word; // character set (code page)
  5  end;
  6  PTransRec = ^TTransRec; // pointer to TTransRec
  7  TTransRecArray = array of TTransRec; // translation table
  8
  9function GetTransTable(const Buffer: Pointer): TTransRecArray;
 10var
 11  TransRec: PTransRec; // pointer to a translation record
 12  Size: DWORD; // size of data read
 13  RecCount: Integer; // number of translation records
 14  Idx: Integer; // loops thru translation records
 15begin
 16  // Read translation data
 17  VerQueryValue(
 18    Buffer, '\VarFileInfo\Translation', Pointer(TransRec), Size
 19  );
 20  // Get record count and set length of array
 21  RecCount := Size div SizeOf(TTransRec);
 22  SetLength(Result, RecCount);
 23  // Loop thru table storing records in array
 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; // the string value data
  5  Dummy: DWORD; // size of value data (unused)
  6  Path: string; // "path" to string value
  7begin
  8  // Build "path" from translation and string name
  9  Path := '\StringFileInfo\' + Trans + '\' + StrName;
 10  // Read the string: return '' if string doesn't exist
 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; // fixed file info record
  5    fTransTable: TTransRecArray; // translation table
  6    fHasVerInfo: Boolean; // whether file contains ver info
  7    fVerInfo: Pointer; // buffer storing ver info
  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; // size of ver info buffer
  4begin
  5  inherited Create;
  6  // Get size of buffer: no ver info if size = 0
  7  BufSize := GetVerInfoSize(FileName);
  8  fHasVerInfo := BufSize > 0;
  9  if fHasVerInfo then
 10  begin
 11    // Read ver info into buffer
 12    GetMem(fVerInfo, BufSize);
 13    GetVerInfo(FileName, BufSize, fVerInfo);
 14    // Read fixed file info and translation table
 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  // Free ver info buffer
  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  // Return string representation of translation at given index
  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:

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

  1. 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".
  2. For bugs in the demo code see the article-demo project's README.md file for details of how to report them.