5

I'm looking to change the build number (low-order of VersionLS) in the version info of an executable. So, I should read the VS_VERSIONINFO structure, change the build number and then update it back to the PE.

I'm working with this code as base: https://stackoverflow.com/a/7999813/1970843. This code works really well to change the VS_FIXEDFILEINFO data, but it doesn't changes (nor access) the StringFileInfo information.

I'm pretty sure I should include something in the VERSIONHEADER packed record to add the Children entry of VS_VERSIONINFO, but I don't know exactly how to do this. This is what I have so far:

type
    StringStruc = Packed Record
        wLength: Word;
        wValueLength: Word;
        wType: Word;
        //szKey: ?;
        //Value: ?;
    End;

    StringTable = Packed Record
        wLength: Word;
        wValueLength: Word;
        wType: Word;
        szKey: Array[0..8] Of WideChar;
        Children: StringStruc;
    End;

    StringFileInfo = Packed Record
        wLength: Word;
        wValueLength: Word;
        wType: Word;
        szKey: Array[0..14] Of WideChar;   // 'STRINGFILEINFO'
        Children: StringTable;
    End;

    VERSIONHEADER = Packed Record
        wLength: Word;
        wValueLength: Word;
        wType: Word;
        szKey: Array[0..16] Of WideChar;   // 'VS_VERSION_INFO'
        Version: VS_FIXEDFILEINFO;
        Children: StringFileInfo;
    End;

...

var VersionHandle, VersionRes: THandle;
    VersionSize: Cardinal;
    Version: Array Of AnsiChar;
    Ver: ^VERSIONHEADER;
Begin
    VersionSize := GetFileVersionInfoSize(PChar(sExe), VersionHandle);

    SetLength(Version, VersionSize);
    Ver := Pointer(Version);
    GetFileVersionInfo(PChar(sExe), 0, VersionSize, Ver);

So, the information seems to be coming correctly up to the first StringStruc. But since both szKey and Value aren't fixed size, I don't know how to correctly define my Packed Record (is it even possible?) to get those values. I'm also having troubles with the arrays... how can I define them? The way I'm doing, I'm just getting the first Children on each Struc. Notice that I'm ignoring the paddings... is this ok?

Any help is appreciated. Most of what I've done here was by trial and error, so I don't really understand what's going on.

PS: I'm still working on this, so I might update this post frequently.

Community
  • 1
  • 1
GabrielF
  • 2,071
  • 1
  • 17
  • 29
  • Maybe I should just try to read the memory as raw binary data? When I find what I want to change, I need to update the information and the sizes from each of the parents, and reallocate memory as necessary... sounds pretty hard. – GabrielF Feb 12 '14 at 16:54
  • 1
    For a project, I tried to change the StringFileInfo, but it was far from obvious, AFAIR. As a result, I sticked to change the numerical information (code already linked in your question :) ), and put a generic text in the string field. What matters is what is displayed in the "About" box and splash screen (extracted from numerical fields), and if the file properties give the exact (numerical) "File Version" (e.g. 2.1.231), but something more generic for "Product Version" (e.g. "2" or "2.1") - sounds enough to me. – Arnaud Bouchez Feb 12 '14 at 17:12
  • Can't you do it with `BeginUpdateResource` etc.? – David Heffernan Feb 12 '14 at 17:40
  • @DavidHeffernan No, he sadly can't. This is the point. See his link about BeginUpdateResource - which works easily with numerical version, but is not easy to work with for string version: allocating such a resource structure is not obvious. – Arnaud Bouchez Feb 12 '14 at 19:29
  • @GabrielF If `BeginUpdateResource` API is difficult to work with, why not just use a pattern by default in the executable resource (e.g. some Unicode characters with a complex pattern), then a simple binary search & replace on the executable file content? Just ensure that the pattern occurs only once before replacement, fill with some spaces if you have less characters than expected, and it's done. – Arnaud Bouchez Feb 12 '14 at 19:32
  • You're wrong about you've gotta find out what the non-obvious members are. The docs doesn't say *"not a true struct"*, *"does not appear in any header"* for nothing. Parse it. See `ExtractData` of `TJclFileVersionInfo` in 'jclfileutils.pas' for how it can be done. – Sertac Akyuz Feb 12 '14 at 21:33
  • 1
    Rather than reinventing the wheel, why not shell out to, e.g., StampVer and let it do the hard work for you. (It's an *old* app., but still works fine for us.) http://www.codeguru.com/tools/standalonetools/article.php/c1403/StampVer-Command-line-version-updater.htm – Erik Knowles Feb 12 '14 at 21:14

1 Answers1

1

I appreciate your attention and help. I found a ready solution here on SO. In fact, it's in the comments to the very question I've linked to (shame on me!).

It's based on a library by Colin Wilson. It uses pointer arithmetic to extract and write the information, so, the hard (maybe only) way. There's also a nice example on how to use the library by Jason Penny: SetVersion. Since I'm using D7, I downloaded Colin Wilson's library from here (under Resource Utilities), but a more recent version, using UnicodeString and nicer pointer arithmetic, is available here.

This is the actual implementation I have now:

uses ..., unitResourceVersionInfo, unitPEFile;

...

var VersionInfo: TVersionInfoResourceDetails;
    PEResModule: TPEResourceModule;
    VersionNumber: ULARGE_INTEGER;
    sVersion: String;
    I: Integer;
Begin
    PEResModule := TPEResourceModule.Create;
    Try
        PEResModule.LoadFromFile(sExe);

        For I := 0 To PEResModule.ResourceCount - 1 Do Begin
            If PEResModule.ResourceDetails[I] Is TVersionInfoResourceDetails Then Begin
                VersionInfo := (PEResModule.ResourceDetails[I] As TVersionInfoResourceDetails);
                Break;
            End;
        End;

        VersionNumber.LowPart := MakeLong(NewBuildNumber, HiWord(VersionInfo.FileVersion.LowPart));
        VersionNumber.HighPart := VersionInfo.FileVersion.HighPart;
        VersionInfo.FileVersion := VersionNumber;
        VersionInfo.ProductVersion := VersionNumber;

        VersionInfo.CodePage := $04e4;

        sVersion := Format('%d.%d.%d.%d', [HiWord(VersionInfo.FileVersion.HighPart), LoWord(VersionInfo.FileVersion.HighPart), HiWord(VersionInfo.FileVersion.LowPart), LoWord(VersionInfo.FileVersion.LowPart)]);
        VersionInfo.SetKeyValue('FileVersion', sVersion);

        PEResModule.SaveToFile(ChangeFileExt(sExe, '.exe2'));

    Finally
        FreeAndNil(PEResModule);
    End;
End;

The codepage line is due to a bug (I believe it is) in the library. It doesn't reads the codepage (comes 0), so, when you save back, it appears as 0.

GabrielF
  • 2,071
  • 1
  • 17
  • 29