Repository: pyinstaller/pyinstaller

Issue:
Weird issue when adding product version information.
<!--
Welcome to the PyInstaller issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to PyInstaller
    - For questions and general support, use the mailing list, see
	  <http://www.pyinstaller.org/support.html#mailing-list>
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
   maintainers to reproduce, the faster it'll be fixed.
-->

<!-- +++ ONLY TEXT +++ DO NOT POST IMAGES +++ -->

## Description of the issue

When building I try to set product information, name and version, and although everything is displayed fine in the file properties, NVDA, the screen reader I use, also reads this information, but for some reason it is wrong, although this does not happen with any other applications.
As a result, the product name looks something like this:
'Myprogram\x00\x004\x10\x01Prod'

I tried to manually create the resource file, compiled it using visual studio, and manually replaced it. After that, the problem went away.


### Context information (for bug reports)

* Output of `pyinstaller --version`: 4.5.1, 5.0.dev0
* Version of Python: 3.7.9, 32bit
* Platform: Windows 7
* How you installed Python: python.org/downloads
* Did you also try this on another platform? Does it work there?
No, I haven't tried it.


* try the latest development version, using the following command:

```shell
pip install https://github.com/pyinstaller/pyinstaller/archive/develop.zip
```

* follow *all* the instructions in our "If Things Go Wrong" Guide
  (https://github.com/pyinstaller/pyinstaller/wiki/If-Things-Go-Wrong) and

### Make sure [everything is packaged correctly](https://github.com/pyinstaller/pyinstaller/wiki/How-to-Report-Bugs#make-sure-everything-is-packaged-correctly)

  * [ ] start with clean installation
  * [ ] use the latest development version
  * [ ] Run your frozen program **from a command window (shell)** — instead of double-clicking on it
  * [ ] Package your program in **--onedir mode**
  * [ ] Package **without UPX**, say: use the option `--noupx` or set `upx=False` in your .spec-file
  * [ ] Repackage you application in **verbose/debug mode**. For this, pass the option `--debug` to `pyi-makespec` or `pyinstaller` or use `EXE(..., debug=1, ...)` in your .spec file.


### A minimal example program which shows the error

```
(paste text here)
“Minimal“ means: remove everything from your code which is not relevant for this bug,
esp. don't use external programs, remote requests, etc.
A very good example is https://gist.github.com/ronen/024cdae9ff2d50488438. This one helped
us reproducing and fixing a quite complex problem within approx 1 hour.
```

### Stacktrace / full error message


```
(paste text here)
```

Please also see <https://github.com/pyinstaller/pyinstaller/wiki/How-to-Report-Bugs>
for more about what would use to solve the issue.



Discussion:
> When building I try to set product information, name and version, and although everything is displayed fine in the file properties, NVDA, the screen reader I use, also reads this information, but for some reason it is wrong, although this does not happen with any other applications.

Any chance you can provide the file-version file that you used to build the program?

When you say "this does not happen with any other application", do you mean that product info is correctly displayed (read out) for other (non-pyinstaller-built) applications? Or that applications other than NVDA correctly display the product information of your built application?

> As a result, the product name looks something like this:
> 'Myprogram\x00\x004\x10\x01Prod'

Is this what NVDA reads out? If so, when (e.g., when you hover mouse over executable, or something else)? Or is this string displayed somewhere?


> 
> Any chance you can provide the file-version file that you used to build the program?
> 

yes, I used the following file:

VSVersionInfo(
ffi=FixedFileInfo(
filevers=(0, 1, 0, 2),
prodvers=(0, 1, 0, 2),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)),
kids=[StringFileInfo([StringTable(
u'040904B0',[
StringStruct(u'FileDescription', u'Myprogram'),
StringStruct(u'FileVersion', u'0.1.0.2'),
StringStruct(u'InternalName', u'Myprogram'),
StringStruct(u'LegalCopyright', u'Copyright'),
StringStruct(u'OriginalFilename', u'myprogram.exe'),
StringStruct(u'ProductName', u'Myprogram'),
StringStruct(u'ProductVersion', u'0.1.0.2'),
StringStruct(u'Language', u'Language Neutral'),
StringStruct(u'LegalTrademarks', u'Myprogram')])]),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])])

> When you say "this does not happen with any other application", do you mean that product info is correctly displayed (read out) for other (non-pyinstaller-built) applications? 

Yes, NVDA displays information about other applications correctly, even when I was building them with py2exe or cx_freeze.> 

> Is this what NVDA reads out? If so, when (e.g., when you hover mouse over executable, or something else)? Or is this string displayed somewhere?

Usually keyboard shortcuts are used to get this information when the application is in the foreground, but it's easier to look at it in the developer information by pressing INSERT + F1, this is what is displayed for my application:

...
appModule.productName: 'Myprogram\x004\x10\x01Produc'
appModule.productVersion: '0.1.0.2\x00:"\x01Lang'
...

And this is how it is displayed in other applications, for example Github desktop

...
appModule.productName: 'GitHub Desktop'
appModule.productVersion: '2.9.3'
...



Thanks for the additional info! I've reproduced the problem with a self-contained example based on the NVDA code.

Resolving patch:
diff --git a/PyInstaller/utils/win32/versioninfo.py b/PyInstaller/utils/win32/versioninfo.py
--- a/PyInstaller/utils/win32/versioninfo.py
+++ b/PyInstaller/utils/win32/versioninfo.py
@@ -519,13 +519,13 @@ def toRaw(self):
         raw_name = getRaw(self.name)
         raw_val = getRaw(self.val)
         # TODO: document the size of vallen and sublen.
-        vallen = len(raw_val) + 2
+        vallen = len(self.val) + 1  # Number of (wide-)characters, not bytes!
         typ = 1
         sublen = 6 + len(raw_name) + 2
         pad = b''
         if sublen % 4:
             pad = b'\000\000'
-        sublen = sublen + len(pad) + vallen
+        sublen = sublen + len(pad) + (vallen * 2)
         return struct.pack('hhh', sublen, vallen, typ) + raw_name + b'\000\000' + pad + raw_val + b'\000\000'
 
     def __eq__(self, other):


Test patch:
diff --git a/tests/unit/test_miscutils.py b/tests/unit/test_miscutils.py
--- a/tests/unit/test_miscutils.py
+++ b/tests/unit/test_miscutils.py
@@ -9,8 +9,13 @@
 # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
 #-----------------------------------------------------------------------------
 
+import os
+import array
+import shutil
+
 import pytest
 
+from PyInstaller import HOMEPATH, PLATFORM
 from PyInstaller.utils.misc import load_py_data_struct, save_py_data_struct
 
 
@@ -73,3 +78,88 @@ def test_versioninfo_str(tmp_path):
     vsinfo2 = eval(vs_info_str)
 
     assert vsinfo == vsinfo2
+
+
+@pytest.mark.win32
+def test_versioninfo_written_to_exe(tmp_path):
+    from PyInstaller.utils.win32 import versioninfo
+    from PyInstaller.utils.win32.versioninfo import VSVersionInfo, \
+        FixedFileInfo, StringFileInfo, StringTable, StringStruct, \
+        VarFileInfo, VarStruct
+
+    # Test/expected values
+    FILE_DESCRIPTION = 'versioninfo test'
+    PRODUCT_NAME = 'Test Product'
+    PRODUCT_VERSION = '2021.09.18.00'
+
+    # Create a version info structure...
+    vsinfo = VSVersionInfo(
+        ffi=FixedFileInfo(
+            filevers=(1, 2, 3, 4),
+            prodvers=(5, 6, 7, 8),
+            mask=0x3f,
+            flags=0x1,
+            OS=0x40004,
+            fileType=0x42,
+            subtype=0x42,
+            date=(0, 0)
+        ),
+        kids=[
+            StringFileInfo([
+                StringTable(
+                    '040904b0', [
+                        StringStruct('FileDescription', FILE_DESCRIPTION),
+                        StringStruct('ProductName', PRODUCT_NAME),
+                        StringStruct('ProductVersion', PRODUCT_VERSION)
+                    ]
+                )
+            ]),
+            VarFileInfo([VarStruct('Translation', [1033, 1200])])
+        ]
+    )
+
+    # Locate bootloader executable
+    bootloader_file = os.path.join(HOMEPATH, 'PyInstaller', 'bootloader', PLATFORM, 'run.exe')
+
+    # Create a local copy
+    test_file = str(tmp_path / 'test_file.exe')
+    shutil.copyfile(bootloader_file, test_file)
+
+    # Embed version info
+    versioninfo.SetVersion(test_file, vsinfo)
+
+    # Read back the values from the string table.
+    def read_file_version_info(filename, *attributes):
+        import ctypes
+        winver = ctypes.WinDLL('version.dll')
+
+        # Read the version information.
+        info_len = winver.GetFileVersionInfoSizeW(filename, None)
+        assert info_len, "No version information found!"
+        info_buf = ctypes.create_string_buffer(info_len)
+        rc = winver.GetFileVersionInfoW(filename, None, info_len, info_buf)
+        assert rc, "Failed to read version information!"
+
+        # Look for the codepages and take the first one.
+        entry_ptr = ctypes.c_void_p()
+        entry_len = ctypes.c_uint()
+        winver.VerQueryValueW(info_buf, r'\VarFileInfo\Translation', ctypes.byref(entry_ptr), ctypes.byref(entry_len))
+        assert entry_len.value, "No codepages found!"
+        codepage = array.array('H', ctypes.string_at(entry_ptr.value, 4))
+        codepage = "%04x%04x" % tuple(codepage)
+
+        # Query attributes.
+        attr_values = []
+        for attr in attributes:
+            entry_name = fr'\StringFileInfo\{codepage}\{attr}'
+            rc = winver.VerQueryValueW(info_buf, entry_name, ctypes.byref(entry_ptr), ctypes.byref(entry_len))
+            if not rc:
+                entry_value = None
+            else:
+                entry_value = ctypes.wstring_at(entry_ptr.value, entry_len.value - 1)
+            attr_values.append(entry_value)
+
+        return attr_values
+
+    values = read_file_version_info(test_file, 'FileDescription', 'ProductName', 'ProductVersion')
+    assert values == [FILE_DESCRIPTION, PRODUCT_NAME, PRODUCT_VERSION]