windowsdelphiembedded-resourcestring-table

Determine which STRINGTABLE is included in EXE


My Delphi application uses the Microsoft Resource Compiler (rc.exe) to compile a list of strings (text format in a .rc file) like this:

Language LANG_KOREAN, SUBLANG_KOREAN
STRINGTABLE
BEGIN
    cszLanguageName "Korean"
    <etc>
END

into a .res file. All tables contains the same IDs (such as cszLanguageName). I maintain two separate resource string files. One contains mostly European languages (English, Czech, etc) which I call "Standard.rc". The other is called "Alternate.rc" and contains all other languages like Korean, Thai, etc.

A compile-time switch determines which file is linked into the EXE:

{$IFDEF ALT_LANG}
    {$R 'source\Alternate.res'}
{$ELSE}
    {$R 'source\Standard.res'}
{$ENDIF}

That was background, now to the question!

Is it possible to determine, given the path to an EXE and using something like the Windows GetFileVersionInfo method, which STRINGTABLE resources are in the EXE? If it were possible to determine whether:

Language LANG_KOREAN, SUBLANG_KOREAN

or

Language LANG_CZECH, SUBLANG_DEFAULT

was included, then the EXE could be identified as "standard" or "alternate".

Currently, without actually executing the EXE, the only difference is size in bytes which is an unreliable heuristic. C++ or C# is fine. I can adapt to Delphi or write a separate utility which is called from Delphi.

UPDATE

Based on the comment from LU RD, I created the following Version.rc file:

// Version information resource file
VS_VERSION_INFO VERSIONINFO
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "080904b0"
        BEGIN
        VALUE "InternalName", "Standard"
        END
    END
END

This was then compiled (with the Microsoft Resource Compiler) into Version.res and linked into the application using {$R 'source\Version.res'}. This compiles as expected, but when I try to read the InternalName field from the EXE it is blank. If I manually set InternalName in the project properties to "Test", then it is set out as "Test". What am I doing wrong and how do I overwrite what has been manually entered in the project properties?


Solution

  • NOTE: This is not an answer to the question of how to determine whether a specific STRINGTABLE resource is included in the final EXE, but an alternate method to resolve the problem posed.

    This question is partially based on this answer.

    Step 1: Enter your project properties and the Version Info tab. If version information (such as version numbers or CompanyName) have been set, copy down all the relevant information. Now turn off Include version information in project.

    Step 2: Create a Version.rc and Version_Alt.rc file. Your exact version may differ, but this is a good starting template:

    // Version information resource file
    #include "windows.h"
    #include "VersionCommon.txt"
    
    1 VERSIONINFO
    FILEVERSION VER_NUM_MAJOR, VER_NUM_MINOR, VER_NUM_RELEASE, VER_NUM_BUILD
    FILEOS VER_FILEOS
    FILETYPE VFT_APP
    {
        BLOCK "VarFileInfo"
        {
            VALUE "Translation", TRANSLATION_LANG_ID, TRANSLATION_CHARSET
        }
    
        BLOCK "StringFileInfo"
        {
            // Note: The block address is a concatenation of the locale ID and code page. This would be "040904E4" for English (US).
            BLOCK "080904E4"
            {
                VALUE "CompanyName",        STRING_COMPANY_NAME
                VALUE "FileDescription",    STRING_FILE_DESCRIPTION
                VALUE "InternalName",       STRING_INTERNAL_NAME_STD
                VALUE "LegalCopyright",     STRING_LEGAL_COPYRIGHT
                VALUE "LegalTrademarks",    STRING_LEGAL_TRADEMARKS
                VALUE "ProductName",        STRING_PRODUCT_NAME
                VALUE "ProductVersion",     STRING_PRODUCT_VERSION
                VALUE "Comments",           STRING_COMMENTS
                VALUE "Homepage",           STRING_HOME_PAGE
            }
        }
    }
    

    This is for the Version.rc file. InternalName will be STRING_INTERNAL_NAME_ALT for Version_Alt.rc, otherwise everything else is the same.

    Step 3: Create VersionCommon.txt which might look like this:

    // Common version information (between standard and alternate language)
    
    // Version info
    #define VER_NUM_MAJOR       1
    #define VER_NUM_MINOR       2
    #define VER_NUM_RELEASE     3
    #define VER_NUM_BUILD       4
    #define VER_FILEOS          0x00000004L     // 32-bit Windows
    
    // Translation info
    #define TRANSLATION_LANG_ID     0x0809      // Locale: English (UK)
    #define TRANSLATION_CHARSET     0x04E4      // Code Page: 1252
    
    // String file info
    #define STRING_COMPANY_NAME         "YOUR-COMPANY\0"
    #define STRING_FILE_DESCRIPTION     "Software to do amazing things\0"
    #define STRING_INTERNAL_NAME_STD    "Standard\0"    // ALT_LANG not defined
    #define STRING_INTERNAL_NAME_ALT    "Alternate\0"   // ALT_LANG is defined
    #define STRING_LEGAL_COPYRIGHT      "Copyright (C) YOUR-COMPANY\0"
    #define STRING_LEGAL_TRADEMARKS     "LEGALISE STATEMENT?\0"
    #define STRING_PRODUCT_NAME         "Groovy\0"
    #define STRING_PRODUCT_VERSION      "SOME-VERSION-INFO\0"
    #define STRING_COMMENTS             "SOME-COMMENTS\0"
    #define STRING_HOME_PAGE            "OPTIONAL-YOUR-WEBSITE\0"
    

    Step 4: Write a batch file that compiles your resource scripts. Note that later versions of Delphi can be configured to compile the resources for you. The batch file might look like this:

    @echo off
    rem Version.rc and Version_Alt.rc are used to create version information that is linked into
    rem the main Delphi application. "InternalName" is used to indicate whether ALT_LANG is defined.
    
    echo Setting the program path (change this if your path is different)
    set SOURCE_PATH=<PATH-TO-FOLDER-CONTAINING-RC-FILES>
    
    echo .
    echo Use Visual Studio tools to generate the version .RES files
    @echo on
    cd <PATH-TO-VISUAL-STUDIO-BIN-FOLDER-CONTAINING-RC.EXE>
    call vcvars32
    rc /r %SOURCE_PATH%\Version.rc
    rc /r %SOURCE_PATH%\Version_Alt.rc
    echo .
    @echo off
    echo .
    
    rem pause <- uncomment this to debug errors
    exit
    

    Step 5: Open your Delphi project (.dpr) in a text editor and link these .RES files into the final EXE:

    {$IFDEF ALT_LANG}
        {$R 'source\Strings_Alt.res'}
        {$R 'source\Version\Version_Alt.res'}
    {$ELSE}
        {$R 'source\Strings.res'}
        {$R 'source\Version\Version.res'}
    {$ENDIF}
    

    Step 6: You've now got version information included in your file and just have to read InternalName (which will be "Standard" or "Alternate"). This can be done as follows:

    strLanguage := GetSpecificFileVersionInfo('YOUR-EXE.exe', 'InternalName');
    

    and the code for GetSpecificFileVersionInfo is:

    function GetSpecificFileVersionInfo(szFile: PChar; strInfo: String) : String;
    var
        pstrBuffer: PChar;
        dwSize, dwLength: DWORD;
        pVersion: pointer;
        strKey: String;
    begin
        // Return the specified file version information
        // Adapted from: http://www.swissdelphicenter.com/en/showcode.php?id=1047
    
        // Typical values in the VERSIONINFO resource of the application include:
        // * CompanyName
        // * FileDescription
        // * InternalName
        // * LegalCopyright
        // * LegalTrademarks
        // * ProductName
        // * ProductVersion
        // * Comments
    
        // Additional fields may be available if the version information has been updated
        Result := '';
        dwSize := GetFileVersionInfoSize(szFile, dwSize);
        if (dwSize > 0) then
            begin
            pstrBuffer := AllocMem(dwSize);
            try
                if (    (GetFileVersionInfo(szFile, 0, dwSize, pstrBuffer)) and
                        (VerQueryValue(pstrBuffer, '\VarFileInfo\Translation', pVersion, dwLength))) then
                    begin
                    strKey := Format('\StringFileInfo\%.4x%.4x\%s', [
                        LoWord(Integer(pVersion^)),
                        HiWord(Integer(pVersion^)), strInfo]);
                    if (VerQueryValue(pstrBuffer, PChar(strKey), pVersion, dwLength)) then
                        Result := StrPas(pVersion);
                    end;
            finally
                FreeMem(pstrBuffer, dwSize);
            end;
            end;
    end;
    

    Job done. Now you've solved two problems. The first is your original one which is to identify how the EXE was compiled (without actually running the EXE). The second is one you probably didn't realise which is your manually entered version information was error-prone...now you have included more automated version information.