News:

Masm32 SDK description, downloads and other helpful links
Message to All Guests
NB: Posting URL's See here: Posted URL Change

Main Menu

Getting file version info: another exercise in frustration

Started by NoCforMe, August 15, 2023, 07:57:50 AM

Previous topic - Next topic

NoCforMe

Another example of just how twisted and ridiculously screwed up some of the stuff inside Windoze is. My quest was pretty simple: I wrote a DLL, and I wanted to keep track of versions, so I started looking into version stuff under Win32. First order of business was looking at other files (DLLs, EXEs) and getting their version information programmatically.

Well, this turned out to be a whole can of worms in itself. To make a long boring story short and cut to the chase, here's what I ended up doing:

  • Using GetFileVersionInfoSizeEx() to get the size of the version info
  • Using GetFileVersionInfoEx() to get the actual version info
  • Using VerQueryValue() to ... do something to that version info so I could display it

OK, so now I had access to what MS calls the "root block" of version info:

typedef struct tagVS_FIXEDFILEINFO {
  DWORD dwSignature;
  DWORD dwStrucVersion;
  DWORD dwFileVersionMS;
  DWORD dwFileVersionLS;
  DWORD dwProductVersionMS;
  DWORD dwProductVersionLS;
  DWORD dwFileFlagsMask;
  DWORD dwFileFlags;
  DWORD dwFileOS;
  DWORD dwFileType;
  DWORD dwFileSubtype;
  DWORD dwFileDateMS;
  DWORD dwFileDateLS;
} VS_FIXEDFILEINFO;

My hopes went up when I was able to verify that the "signature" was, in fact, the expected value (0xFEEF04BD). Hallelujah.

And I was eventually able to use the version fields to display the actual version #s, but not before being totally confused. Because they don't tell you how those fields are interpreted, only that they're the MS and LS of 64-bit values. At first I just displayed them as 32-bit numbers, but of course that was wrong.

Turns out that they are displayed in the form "nn.nn.nn.nn", where each piece is a 16-bit segment of the 64-bit number. Not hard to do, but I didn't see any explanation of this anywhere, but had to figure it out on my own.

Another weirdness is that in every single file I looked at, the file version # and product version # were exactly the same. Why bother to have both values if nobody uses them?

Another really annoying thing: in every single file I looked at, the timestamp value (dwFileDateMS/LS) was zero. Every single one! So apparently when you right-click on a file in Explorer and choose Properties-->Details to get the version info, the file time comes from the regular file timestamp, not from the version info. Ain't that a bitch?

This whole interface is a mess. Is this even the mechanism you're supposed to use these days to get version info, or has it been "deprecated" and replaced by a newer and possibly even more screwed-up scheme?

BTW, you might wonder why I ended up using the "Ex" functions instead of the "regular" ones. That's another can of worms: when I first coded up my testbed, it wouldn't assemble because apparently the MASM SDK doesn't know from any of those GetFileVersionInfoXXX functions. So I fell back on using LoadLibrary() to load directly from the Windows DLL. Except that I didn't have that file in my system. Ended up having to download that file (Api-ms-win-core-version-l1-1-0.dll), and of course hoping and praying that I didn't get some virus-infected copy. After examining that file I found that 1) it only had the Ex functions, not the "regular" ones it was spozed to contain, and 2) it only had the Unicode versions, not the ASCII ones. Aaaargh; so now I have to convert my filenames to Unicode. Not hard to do, but yet another pain in the ass.

I was finally able to use the DLL functions, which seem to work as advertised. (Weird: to get the "root block" with VerQueryValue() you have to pass it a string, "\", which of course must be Unicode (just put 3 zeroes after it). What a strange interface.)

But wait! There's more! Before I realized that all the timestamps were zero, I thought it would be nice to display the file date/time. I soon discovered another gotcha: the timestamps in the "root block" are in time_t format, not the usual FILETIME format that's used practically everywhere else in Windoze! So searching online, I found this completely ridiculous conversion method for turning time_t into FILETIME:

time[FILETIME] = time[time_t] * 1,000,000 + 116,444,736,000,000,000

I'm not kidding you--check it out here. And of course there's no ready-made conversion function for this, which I'm sure I'm not the only person who ever wanted to do this, so I had to then scrounge around online to find out how to do 64*32-bit multiplication in a 32-bit program. Yet another pain in the ass.

So I now have a semi-functional testbed program which shows version info for any file (attached below). It basically does what Explorer shows you with [right-click]Properties-->Details. Whoop-te-do ...

Which still is only part of the problem I set myself to solve. I still have no idea how to get version information into my DLL. I sense that somehow you have to include a version "resource" in the file. I tried using the LINK option /version:n.m, which didn't do anything so far as I could tell.

So if anyone could tell me how to embed version information in a DLL I'd appreciate it very much.
Assembly language programming should be fun. That's why I do it.

Greenhorn

Hi NoCforMe,

in your *.rc file (Pascal syntax example):

//
//  Version Information resources
//
1 VERSIONINFO
    FILEVERSION     0,9,0,19
    PRODUCTVERSION  0,9,0,19
    FILEOS          VOS_NT_WINDOWS32
    FILETYPE        VFT_APP
    FILESUBTYPE     VFT2_UNKNOWN
    FILEFLAGSMASK   0x00000000
    FILEFLAGS       0x00000000
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "04070475"
        BEGIN
            VALUE "Comments", ""
            VALUE "CompanyName", "YourCompanyName"
            VALUE "FileDescription", "SomeDescription"
            VALUE "FileVersion", "0.9.0.19"
            VALUE "InternalName", ""
            VALUE "LegalCopyright", "© 2021 YourCompanyName"
            VALUE "LegalTrademarks", ""
            VALUE "OriginalFilename", "YourFileName.exe"
            VALUE "PrivateBuild", ""
            VALUE "ProductName", "YourProductName"
            VALUE "ProductVersion", "0.9.0.19"
            VALUE "SpecialBuild", ""
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x0407, 0x0475
    END
END

Or without 'StringFileInfo' for just having the version number (C Syntax example):

//
// Version info resources
//
1 VERSIONINFO
    FILEVERSION     0,9,7,22
    PRODUCTVERSION  0,9,7,22
    FILEOS          VOS_NT_WINDOWS32
    FILETYPE        VFT_DLL
    FILESUBTYPE     VFT2_UNKNOWN
    FILEFLAGSMASK   0
    FILEFLAGS       0
{
   
}

Documentation:
https://learn.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
Kole Feut un Nordenwind gift en krusen Büdel un en lütten Pint.

NoCforMe

Hey, thanks for that. Not a big fan of resource files here, but since that's the only to do it I'll use it.

So what do you know: I actually found a file with different file and product version #s. The file is an ancient bitmap font file, probably dating from the later Jurassic:
Assembly language programming should be fun. That's why I do it.

fearless

Quote from: NoCforMe on August 15, 2023, 07:57:50 AMI sense that somehow you have to include a version "resource" in the file.
Thats correct. If you add a resource and compile it with rc.exe and link the resulting res file into the dll it will add the resource to it, so you can manifests and version info as examples.

LINK.EXE /SUBSYSTEM:WINDOWS /RELEASE /DLL /LIBPATH:"C:\Masm32\lib" /OUT:"MyDll.dll", "MyDll.obj" "MyDll.res"
You can include a version block in a resource file, something like the following:

#define VERINF1 1
VERINF1 VERSIONINFO
FILEVERSION 1,0,0,21
PRODUCTVERSION 1,0,0,21
FILEOS 0x00000004
FILETYPE 0x00000002
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
    BLOCK "040904B0"
    BEGIN
      VALUE "CompanyName", "fearless\0"
      VALUE "FileVersion", "1.0.0.21\0"
      VALUE "FileDescription", "MyDll.dll\0"
      VALUE "InternalName", " MyDll.dll\0"
      VALUE "LegalCopyright", "fearless\0"
      VALUE "LegalTrademarks", "fearless\0"
      VALUE "OriginalFilename", " MyDll.dll\0"
      VALUE "ProductName", " MyDll.dll\0"
      VALUE "ProductVersion", "1.0.0.21\0"
    END
  END
  BLOCK "VarFileInfo"
  BEGIN
    VALUE "Translation", 0x0409, 0x04B0
  END
END

Of course its easier to use a resource editor to add a version block and with the gui its easy to change the version numbers. Some IDEs or editors even auto increment the version number when you build. RadASM for example allows you to do that and has its own inbuilt editor for it: Project->Versioninfo menu item.

I use the Version.inc and Version.lib from masm32 sdk to use the GetFileVersionInfoSize and VerQueryValue functions.

here is an example that checks a file for the version number specified and if it matches it returns true, otherwise false:

include user32.inc
include kernel32.inc
include version.inc

includelib user32.lib
includelib kernel32.lib
includelib version.lib

CheckFileVersion       PROTO :DWORD, :DWORD

.data
szMyFilesExeVersion    DB "0.1.0.0",0
szMyFile               DB "c:\temp\test.exe",0
szVerRoot              DB "\\",0
szFileVersion          DB "%d.%d.%d.%d",0
szFileVersionBuffer    DB 64 DUP (0)

.code

;------------------------------------------------------------------------------
; Checks file version of the filename for the correct version
; Returns: eax contains TRUE if version matches, otherwise returns FALSE
; Invoke CheckFileVersion, Addr szMyFile, Addr szMyFilesExeVersion
;------------------------------------------------------------------------------
CheckFileVersion PROC USES EBX szVersionFile:DWORD, szVersion:DWORD
    LOCAL verHandle:DWORD
    LOCAL verData:DWORD
    LOCAL verSize:DWORD
    LOCAL verInfo:DWORD
    LOCAL hHeap:DWORD
    LOCAL pBuffer:DWORD
    LOCAL lenBuffer:DWORD
    LOCAL ver1:DWORD
    LOCAL ver2:DWORD
    LOCAL ver3:DWORD
    LOCAL ver4:DWORD

    Invoke GetFileVersionInfoSize, szVersionFile, Addr verHandle
    .IF eax != 0
        mov verSize, eax
        Invoke GetProcessHeap
        .IF eax != 0
            mov hHeap, eax
            Invoke HeapAlloc, eax, 0, verSize
            .IF eax != 0
                mov verData, eax
                Invoke GetFileVersionInfo, szVersionFile, 0, verSize, verData
                .IF eax != 0
                    Invoke VerQueryValue, verData, Addr szVerRoot, Addr pBuffer, Addr lenBuffer
                    .IF eax != 0 && lenBuffer != 0
                        lea ebx, pBuffer
                        mov eax, [ebx]
                        mov verInfo, eax
                        mov ebx, eax
                        .IF [ebx].VS_FIXEDFILEINFO.dwSignature == 0FEEF04BDh
                            mov ebx, verInfo

                            mov eax, [ebx].VS_FIXEDFILEINFO.dwFileVersionMS
                            shr eax, 16d
                            and eax, 0FFFFh
                            mov ver1, eax
                            mov eax, [ebx].VS_FIXEDFILEINFO.dwFileVersionMS
                            shr eax, 0
                            and eax, 0FFFFh
                            mov ver2, eax
                            mov eax, [ebx].VS_FIXEDFILEINFO.dwFileVersionLS
                            shr eax, 16d
                            and eax, 0FFFFh
                            mov ver3, eax
                            mov eax, [ebx].VS_FIXEDFILEINFO.dwFileVersionLS
                            shr eax, 0
                            and eax, 0FFFFh
                            mov ver4, eax

                            Invoke HeapFree, hHeap, 0, verData

                            Invoke wsprintf, Addr szFileVersionBuffer, Addr szFileVersion, ver1, ver2, ver3, ver4
                            Invoke lstrcmp, szVersion, Addr szFileVersionBuffer
                            .IF eax == 0 ; match
                                mov eax, TRUE
                            .ELSE
                                mov eax, FALSE
                            .ENDIF
                        .ELSE
                            Invoke HeapFree, hHeap, 0, verData
                            mov eax, FALSE
                            ret
                        .ENDIF
                    .ELSE
                        Invoke HeapFree, hHeap, 0, verData
                        mov eax, FALSE
                        ret
                    .ENDIF
                .ELSE
                    Invoke HeapFree, hHeap, 0, verData
                    mov eax, FALSE
                    ret
                .ENDIF
            .ELSE
                mov eax, FALSE
                ret
            .ENDIF
        .ELSE
            mov eax, FALSE
            ret
        .ENDIF
    .ELSE
        mov eax, FALSE
        ret
    .ENDIF
    ret
CheckFileVersion endp


NoCforMe

Quote from: fearless on August 15, 2023, 08:56:28 AMI use the Version.inc and Version.lib from masm32 sdk to use the GetFileVersionInfoSize and VerQueryValue functions.

Thanks, I shoulda checked that. Small quibble, though: those files don't have any of the Ex functions. (I'm sure the "regular" ones will work just fine.)
Assembly language programming should be fun. That's why I do it.

jj2007

Do you want something like this?

 0  Lang                      040904B0
 1  Comments                  ?
 2  InternalName              QE
 3  ProductName               Quick Editor
 4  CompanyName               Steve Hutchesson
 5  LegalCopyright            © 1998-2011 Steve Hutchesson
 6  ProductVersion            4.0h
 7  FileDescription           MASM32 Code Editor
 8  LegalTrademarks           ?
 9  PrivateBuild              ?
10  FileVersion               4.0h
11  OriginalFilename          qeditor.exe
12  SpecialBuild              ?

include \masm32\MasmBasic\MasmBasic.inc
  Init
  GetFileProps "\Masm32\qEditor.exe"
  For_ ecx=0 To FileProp$(?)-1
Print Str$("%_i  ", ecx), FilePropName$(ecx) ; show the predefined names
wPrintLine At(30) wRec$(FileProp$(ecx)) ; descriptions may be Utf8
  Next
EndOfCode

NoCforMe

Thanks; I think I now have enough info to put version info into my li'l DLL. All I really want is basic version # info.; don't need none of them strings.

BTW, thanks for the flashing neon billboard for MasmBasic ...
Assembly language programming should be fun. That's why I do it.

Vortex

Hi NoCforMe,

Here is a small command line tool for you to check file versions :

GetFileVersion.exe \Windows\notepad.exe
6.1.7601.23403


jj2007

The Windows file properties dialog does not show all entries:
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
BLOCK "080904B0"
BEGIN
  VALUE "Comments", "Dedicated Assembly editor"
VALUE "FileDescription", "MasmBasic IDE"
VALUE "ProductName", "Masm32 Tools Collection"
VALUE "ProductVersion", "Release version #D$"
VALUE "CompanyName", "Macrohard"
VALUE "FileVersion", "1.0"
VALUE "InternalName", "AlmostBugFree"
VALUE "LegalCopyright", "© #Y$ jj2007@Masm32"
VALUE "OriginalFilename", "RichMasm.exe"
VALUE "LegalTrademarks", "RM©"
VALUE "PrivateBuild", "for Jochen"
VALUE "SpecialBuild", "testing new features"
END
  END
  BLOCK "VarFileInfo"
  BEGIN
VALUE "Translation", 0x809, 0x4B0
  END
END
 0  Lang                      080904B0
 1  Comments                  Dedicated Assembly editor
 2  InternalName              AlmostBugFree
 3  ProductName               Masm32 Tools Collection
 4  CompanyName               Macrohard
 5  LegalCopyright            © 2023 jj2007@Masm32
 6  ProductVersion            Release version 15 August 2023
 7  FileDescription           MasmBasic IDE
 8  LegalTrademarks           RM©
 9  PrivateBuild              for Jochen
10  FileVersion               1.0
11  OriginalFilename          RichMasm.exe
12  SpecialBuild              testing new features

NoCforMe

Assembly language programming should be fun. That's why I do it.

NoCforMe

OK, here's a more fleshed-out program. Shows all version strings for all language/codepage pairs. (Everything I've looked at only has 1 lang./codepage pair. I wonder if someone out there, maybe in a foreign (i.e., non-English speaking) land could find some examples that have multiple languages? Theoretically both boxes at the bottom should be populated with all possible combinations of lang./codepage and strings.)

Oh, and I ended up using the MASM32 version.inc and version.lib files, so no need to load the stupid DLL I was using. Much simpler ...
Assembly language programming should be fun. That's why I do it.

Vortex

Hi NoCforMe,

Nice work. Maybe, you need to add UNICODE support to your application to process non-English EXEs\DLLs.

NoCforMe

Quote from: Vortex on August 16, 2023, 05:55:21 PMHi NoCforMe,

Nice work. Maybe, you need to add UNICODE support to your application to process non-English EXEs\DLLs.

Yes. Welll, everything here is Unicode except for the front end (the file-selection dialog). Are you saying that this needs to be Unicode as well? Are there file systems with Unicode file names?

After further thought, that was dumb of me; of course there are Unicode file systems. So I need to use the "W" version of GetOpenFileName(). Ugh. That means that all my strings for that function need to be Unicode. That'll take a little while to get to.

I collect the filename from the user in ANSI and convert it to Unicode for the GetFileVersionInfo() functions.
Assembly language programming should be fun. That's why I do it.

Vortex

Hi NoCforMe,

QuoteNTFS stores file names in Unicode. In contrast, the older FAT12, FAT16, and FAT32 file systems use the OEM character set.

https://learn.microsoft.com/en-us/windows/win32/intl/character-sets-used-in-file-names

QuoteSo I need to use the "W" version of GetOpenFileName().

You could have a file named "éditeur-rapide" ( Quick editor in French ) so better to try the UNICODE version of GetOpenFileName.

jj2007