1# -*- coding: utf-8 -*-
2#-----------------------------------------------------------------------------
3# Copyright (c) 2013-2019, PyInstaller Development Team.
4#
5# Distributed under the terms of the GNU General Public License with exception
6# for distributing bootloader.
7#
8# The full license is in the file COPYING.txt, distributed with this software.
9#-----------------------------------------------------------------------------
10
11
12import codecs
13import struct
14
15from ...compat import is_py3, text_read_mode, win32api
16
17# ::TODO:: #1920 revert to using pypi version
18import pefile
19
20
21# TODO implement read/write version information with pefile library.
22# PE version info doc: http://msdn.microsoft.com/en-us/library/ms646981.aspx
23def pefile_read_version(filename):
24    """
25    Return structure like:
26
27    {
28        # Translation independent information.
29        # VS_FIXEDFILEINFO - Contains version information about a file. This information is language and code page independent.
30        u'FileVersion':      (1, 2, 3, 4),
31        u'ProductVersion':   (9, 10, 11, 12),
32
33        # PE files might contain several translations of version information.
34        # VS_VERSIONINFO - Depicts the organization of data in a file-version resource. It is the root structure that contains all other file-version information structures.
35        u'translations': {
36            'lang_id1' : {
37                u'Comments':         u'日本語, Unicode 対応.',
38                u'CompanyName':      u'your company.',
39                u'FileDescription':  u'your file desc.',
40                u'FileVersion':      u'1, 2, 3, 4',
41                u'InternalName':     u'your internal name.',
42                u'LegalCopyright':   u'your legal copyright.',
43                u'LegalTrademarks':  u'your legal trademarks.',
44                u'OriginalFilename': u'your original filename.',
45                u'PrivateBuild':     u'5, 6, 7, 8',
46                u'ProductName':      u'your product name',
47                u'ProductVersion':   u'9, 10, 11, 12',
48                u'SpecialBuild':     u'13, 14, 15, 16',
49            },
50
51            'lang_id2' : {
52                ...
53            }
54        }
55    }
56
57    Version info can contain multiple languages.
58    """
59    # TODO
60    vers = {
61        'FileVersion': (0, 0, 0, 0),
62        'ProductVersion': (0, 0, 0, 0),
63        'translations': {
64            'lang_id1': {
65                'Comments': '',
66                'CompanyName': '',
67                'FileDescription': '',
68                'FileVersion': '',
69                'InternalName': '',
70                'LegalCopyright': '',
71                'LegalTrademarks': '',
72                'OriginalFilename': '',
73                'PrivateBuild': '',
74                'ProductName': '',
75                'ProductVersion': '',
76                'SpecialBuild': '',
77            }
78        }
79    }
80    pe = pefile.PE(filename)
81    #ffi = pe.VS_FIXEDFILEINFO
82    #vers['FileVersion'] = (ffi.FileVersionMS >> 16, ffi.FileVersionMS & 0xFFFF, ffi.FileVersionLS >> 16, ffi.FileVersionLS & 0xFFFF)
83    #vers['ProductVersion'] = (ffi.ProductVersionMS >> 16, ffi.ProductVersionMS & 0xFFFF, ffi.ProductVersionLS >> 16, ffi.ProductVersionLS & 0xFFFF)
84    #print(pe.VS_FIXEDFILEINFO.FileVersionMS)
85    # TODO Only first available language is used for now.
86    #vers = pe.FileInfo[0].StringTable[0].entries
87    from pprint import pprint
88    pprint(pe.VS_FIXEDFILEINFO)
89    print(dir(pe.VS_FIXEDFILEINFO))
90    print(repr(pe.VS_FIXEDFILEINFO))
91    print(pe.dump_info())
92    pe.close()
93    return vers
94
95
96
97# Ensures no code from the executable is executed.
98LOAD_LIBRARY_AS_DATAFILE = 2
99
100
101if is_py3:
102    def getRaw(text):
103        """
104        Encodes text as UTF-16LE (Microsoft 'Unicode') for use in structs.
105        `bytes` is not allowed on Python 3.
106        """
107        return text.encode('UTF-16LE')
108else:
109    def getRaw(text):
110        """
111        Encodes text as UTF-16LE (Microsoft 'Unicode') for use in structs.
112        `unicode` is encoded to UTF-16LE, and `str` is first decoded from
113        `mbcs` before being re-encoded.
114        """
115        if isinstance(text, str):
116            text = text.decode('mbcs', errors='replace')
117
118        return text.encode('UTF-16LE')
119
120
121def decode(pathnm):
122    h = win32api.LoadLibraryEx(pathnm, 0, LOAD_LIBRARY_AS_DATAFILE)
123    res = win32api.EnumResourceNames(h, pefile.RESOURCE_TYPE['RT_VERSION'])
124    if not len(res):
125        return None
126    data = win32api.LoadResource(h, pefile.RESOURCE_TYPE['RT_VERSION'],
127                                 res[0])
128    vs = VSVersionInfo()
129    j = vs.fromRaw(data)
130    win32api.FreeLibrary(h)
131    return vs
132
133
134def nextDWord(offset):
135    """ Align `offset` to the next 4-byte boundary """
136    return ((offset + 3) >> 2) << 2
137
138if is_py3:
139    def _py3_str_compat(cls):
140        """
141        On Python 3, the special method __str__ is equivalent to the Python 2
142        method __unicode__.
143        """
144        cls.__str__ = cls.__unicode__
145        return cls
146else:
147    def _py3_str_compat(cls):
148        return cls
149
150@_py3_str_compat
151class VSVersionInfo:
152    """
153    WORD  wLength;        // length of the VS_VERSION_INFO structure
154    WORD  wValueLength;   // length of the Value member
155    WORD  wType;          // 1 means text, 0 means binary
156    WCHAR szKey[];        // Contains the Unicode string "VS_VERSION_INFO".
157    WORD  Padding1[];
158    VS_FIXEDFILEINFO Value;
159    WORD  Padding2[];
160    WORD  Children[];     // zero or more StringFileInfo or VarFileInfo
161                          // structures (or both) that are children of the
162                          // current version structure.
163    """
164
165    def __init__(self, ffi=None, kids=None):
166        self.ffi = ffi
167        self.kids = kids or []
168
169    def fromRaw(self, data):
170        i, (sublen, vallen, wType, nm) = parseCommon(data)
171        #vallen is length of the ffi, typ is 0, nm is 'VS_VERSION_INFO'.
172        i = nextDWord(i)
173        # Now a VS_FIXEDFILEINFO
174        self.ffi = FixedFileInfo()
175        j = self.ffi.fromRaw(data, i)
176        i = j
177        while i < sublen:
178            j = i
179            i, (csublen, cvallen, ctyp, nm) = parseCommon(data, i)
180            if nm.strip() == u'StringFileInfo':
181                sfi = StringFileInfo()
182                k = sfi.fromRaw(csublen, cvallen, nm, data, i, j+csublen)
183                self.kids.append(sfi)
184                i = k
185            else:
186                vfi = VarFileInfo()
187                k = vfi.fromRaw(csublen, cvallen, nm, data, i, j+csublen)
188                self.kids.append(vfi)
189                i = k
190            i = j + csublen
191            i = nextDWord(i)
192        return i
193
194    def toRaw(self):
195        raw_name = getRaw(u'VS_VERSION_INFO')
196        rawffi = self.ffi.toRaw()
197        vallen = len(rawffi)
198        typ = 0
199        sublen = 6 + len(raw_name) + 2
200        pad = b''
201        if sublen % 4:
202            pad = b'\000\000'
203        sublen = sublen + len(pad) + vallen
204        pad2 = b''
205        if sublen % 4:
206            pad2 = b'\000\000'
207        tmp = b''.join([kid.toRaw() for kid in self.kids ])
208        sublen = sublen + len(pad2) + len(tmp)
209        return (struct.pack('hhh', sublen, vallen, typ)
210                + raw_name + b'\000\000' + pad + rawffi + pad2 + tmp)
211
212    def __unicode__(self, indent=u''):
213        indent = indent + u'  '
214        tmp = [kid.__unicode__(indent+u'  ')
215               for kid in self.kids]
216        tmp = u', \n'.join(tmp)
217        return (u"""# UTF-8
218#
219# For more details about fixed file info 'ffi' see:
220# http://msdn.microsoft.com/en-us/library/ms646997.aspx
221VSVersionInfo(
222%sffi=%s,
223%skids=[
224%s
225%s]
226)
227""" % (indent, self.ffi.__unicode__(indent), indent, tmp, indent))
228
229
230def parseCommon(data, start=0):
231    i = start + 6
232    (wLength, wValueLength, wType) = struct.unpack('3h', data[start:i])
233    i, text = parseUString(data, i, i+wLength)
234    return i, (wLength, wValueLength, wType, text)
235
236def parseUString(data, start, limit):
237    i = start
238    while i < limit:
239        if data[i:i+2] == b'\000\000':
240            break
241        i += 2
242    text = data[start:i].decode('UTF-16LE')
243    i += 2
244    return i, text
245
246@_py3_str_compat
247class FixedFileInfo:
248    """
249    DWORD dwSignature;        //Contains the value 0xFEEFO4BD
250    DWORD dwStrucVersion;     // binary version number of this structure.
251                              // The high-order word of this member contains
252                              // the major version number, and the low-order
253                              // word contains the minor version number.
254    DWORD dwFileVersionMS;    // most significant 32 bits of the file's binary
255                              // version number
256    DWORD dwFileVersionLS;    //
257    DWORD dwProductVersionMS; // most significant 32 bits of the binary version
258                              // number of the product with which this file was
259                              // distributed
260    DWORD dwProductVersionLS; //
261    DWORD dwFileFlagsMask;    // bitmask that specifies the valid bits in
262                              // dwFileFlags. A bit is valid only if it was
263                              // defined when the file was created.
264    DWORD dwFileFlags;        // VS_FF_DEBUG, VS_FF_PATCHED etc.
265    DWORD dwFileOS;           // VOS_NT, VOS_WINDOWS32 etc.
266    DWORD dwFileType;         // VFT_APP etc.
267    DWORD dwFileSubtype;      // 0 unless VFT_DRV or VFT_FONT or VFT_VXD
268    DWORD dwFileDateMS;
269    DWORD dwFileDateLS;
270    """
271    def __init__(self, filevers=(0, 0, 0, 0), prodvers=(0, 0, 0, 0),
272                 mask=0x3f, flags=0x0, OS=0x40004, fileType=0x1,
273                 subtype=0x0, date=(0, 0)):
274        self.sig = 0xfeef04bd
275        self.strucVersion = 0x10000
276        self.fileVersionMS = (filevers[0] << 16) | (filevers[1] & 0xffff)
277        self.fileVersionLS = (filevers[2] << 16) | (filevers[3] & 0xffff)
278        self.productVersionMS = (prodvers[0] << 16) | (prodvers[1] & 0xffff)
279        self.productVersionLS = (prodvers[2] << 16) | (prodvers[3] & 0xffff)
280        self.fileFlagsMask = mask
281        self.fileFlags = flags
282        self.fileOS = OS
283        self.fileType = fileType
284        self.fileSubtype = subtype
285        self.fileDateMS = date[0]
286        self.fileDateLS = date[1]
287
288    def fromRaw(self, data, i):
289        (self.sig,
290         self.strucVersion,
291         self.fileVersionMS,
292         self.fileVersionLS,
293         self.productVersionMS,
294         self.productVersionLS,
295         self.fileFlagsMask,
296         self.fileFlags,
297         self.fileOS,
298         self.fileType,
299         self.fileSubtype,
300         self.fileDateMS,
301         self.fileDateLS) = struct.unpack('13l', data[i:i+52])
302        return i+52
303
304    def toRaw(self):
305        return struct.pack('L12l', self.sig,
306                             self.strucVersion,
307                             self.fileVersionMS,
308                             self.fileVersionLS,
309                             self.productVersionMS,
310                             self.productVersionLS,
311                             self.fileFlagsMask,
312                             self.fileFlags,
313                             self.fileOS,
314                             self.fileType,
315                             self.fileSubtype,
316                             self.fileDateMS,
317                             self.fileDateLS)
318
319    def __unicode__(self, indent=u''):
320        fv = (self.fileVersionMS >> 16, self.fileVersionMS & 0xffff,
321              self.fileVersionLS >> 16, self.fileVersionLS & 0xFFFF)
322        pv = (self.productVersionMS >> 16, self.productVersionMS & 0xffff,
323              self.productVersionLS >> 16, self.productVersionLS & 0xFFFF)
324        fd = (self.fileDateMS, self.fileDateLS)
325        tmp = [u'FixedFileInfo(',
326            u'# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)',
327            u'# Set not needed items to zero 0.',
328            u'filevers=%s,' % (fv,),
329            u'prodvers=%s,' % (pv,),
330            u"# Contains a bitmask that specifies the valid bits 'flags'r",
331            u'mask=%s,' % hex(self.fileFlagsMask),
332            u'# Contains a bitmask that specifies the Boolean attributes of the file.',
333            u'flags=%s,' % hex(self.fileFlags),
334            u'# The operating system for which this file was designed.',
335            u'# 0x4 - NT and there is no need to change it.',
336            u'OS=%s,' % hex(self.fileOS),
337            u'# The general type of file.',
338            u'# 0x1 - the file is an application.',
339            u'fileType=%s,' % hex(self.fileType),
340            u'# The function of the file.',
341            u'# 0x0 - the function is not defined for this fileType',
342            u'subtype=%s,' % hex(self.fileSubtype),
343            u'# Creation date and time stamp.',
344            u'date=%s' % (fd,),
345            u')'
346        ]
347        return (u'\n'+indent+u'  ').join(tmp)
348
349
350@_py3_str_compat
351class StringFileInfo(object):
352    """
353    WORD        wLength;      // length of the version resource
354    WORD        wValueLength; // length of the Value member in the current
355                              // VS_VERSION_INFO structure
356    WORD        wType;        // 1 means text, 0 means binary
357    WCHAR       szKey[];      // Contains the Unicode string 'StringFileInfo'.
358    WORD        Padding[];
359    StringTable Children[];   // list of zero or more String structures
360    """
361    def __init__(self, kids=None):
362        self.name = u'StringFileInfo'
363        self.kids = kids or []
364
365    def fromRaw(self, sublen, vallen, name, data, i, limit):
366        self.name = name
367        while i < limit:
368            st = StringTable()
369            j = st.fromRaw(data, i, limit)
370            self.kids.append(st)
371            i = j
372        return i
373
374    def toRaw(self):
375        raw_name = getRaw(self.name)
376        vallen = 0
377        typ = 1
378        sublen = 6 + len(raw_name) + 2
379        pad = b''
380        if sublen % 4:
381            pad = b'\000\000'
382        tmp = b''.join([kid.toRaw() for kid in self.kids])
383        sublen = sublen + len(pad) + len(tmp)
384        return (struct.pack('hhh', sublen, vallen, typ)
385                + raw_name + b'\000\000' + pad + tmp)
386
387    def __unicode__(self, indent=u''):
388        newindent = indent + u'  '
389        tmp = [kid.__unicode__(newindent)
390               for kid in self.kids]
391        tmp = u', \n'.join(tmp)
392        return (u'%sStringFileInfo(\n%s[\n%s\n%s])'
393                % (indent, newindent, tmp, newindent))
394
395
396@_py3_str_compat
397class StringTable:
398    """
399    WORD   wLength;
400    WORD   wValueLength;
401    WORD   wType;
402    WCHAR  szKey[];
403    String Children[];    // list of zero or more String structures.
404    """
405    def __init__(self, name=None, kids=None):
406        self.name = name or u''
407        self.kids = kids or []
408
409    def fromRaw(self, data, i, limit):
410        i, (cpsublen, cpwValueLength, cpwType, self.name) = parseCodePage(data, i, limit) # should be code page junk
411        i = nextDWord(i)
412        while i < limit:
413            ss = StringStruct()
414            j = ss.fromRaw(data, i, limit)
415            i = j
416            self.kids.append(ss)
417            i = nextDWord(i)
418        return i
419
420    def toRaw(self):
421        raw_name = getRaw(self.name)
422        vallen = 0
423        typ = 1
424        sublen = 6 + len(raw_name) + 2
425        tmp = []
426        for kid in self.kids:
427            raw = kid.toRaw()
428            if len(raw) % 4:
429                raw = raw + b'\000\000'
430            tmp.append(raw)
431        tmp = b''.join(tmp)
432        sublen += len(tmp)
433        return (struct.pack('hhh', sublen, vallen, typ)
434                + raw_name + b'\000\000' + tmp)
435
436    def __unicode__(self, indent=u''):
437        newindent = indent + u'  '
438        tmp = (u',\n%s' % newindent).join(u'%s' % (kid,) for kid in self.kids)
439        return (u"%sStringTable(\n%su'%s',\n%s[%s])"
440                % (indent, newindent, self.name, newindent, tmp))
441
442
443@_py3_str_compat
444class StringStruct:
445    """
446    WORD   wLength;
447    WORD   wValueLength;
448    WORD   wType;
449    WCHAR  szKey[];
450    WORD   Padding[];
451    String Value[];
452    """
453    def __init__(self, name=None, val=None):
454        self.name = name or u''
455        self.val = val or u''
456
457    def fromRaw(self, data, i, limit):
458        i, (sublen, vallen, typ, self.name) = parseCommon(data, i)
459        limit = i + sublen
460        i = nextDWord(i)
461        i, self.val = parseUString(data, i, limit)
462        return i
463
464    def toRaw(self):
465        raw_name = getRaw(self.name)
466        raw_val = getRaw(self.val)
467        # TODO document the size of vallen and sublen.
468        vallen = len(raw_val) + 2
469        typ = 1
470        sublen = 6 + len(raw_name) + 2
471        pad = b''
472        if sublen % 4:
473            pad = b'\000\000'
474        sublen = sublen + len(pad) + vallen
475        abcd = (struct.pack('hhh', sublen, vallen, typ)
476                + raw_name + b'\000\000' + pad
477                + raw_val + b'\000\000')
478        return abcd
479
480    def __unicode__(self, indent=''):
481        return u"StringStruct(u'%s', u'%s')" % (self.name, self.val)
482
483
484def parseCodePage(data, i, limit):
485    i, (sublen, wValueLength, wType, nm) = parseCommon(data, i)
486    return i, (sublen, wValueLength, wType, nm)
487
488
489@_py3_str_compat
490class VarFileInfo:
491    """
492    WORD  wLength;        // length of the version resource
493    WORD  wValueLength;   // length of the Value member in the current
494                          // VS_VERSION_INFO structure
495    WORD  wType;          // 1 means text, 0 means binary
496    WCHAR szKey[];        // Contains the Unicode string 'VarFileInfo'.
497    WORD  Padding[];
498    Var   Children[];     // list of zero or more Var structures
499    """
500    def __init__(self, kids=None):
501        self.kids = kids or []
502
503    def fromRaw(self, sublen, vallen, name, data, i, limit):
504        self.sublen = sublen
505        self.vallen = vallen
506        self.name = name
507        i = nextDWord(i)
508        while i < limit:
509            vs = VarStruct()
510            j = vs.fromRaw(data, i, limit)
511            self.kids.append(vs)
512            i = j
513        return i
514
515    def toRaw(self):
516        self.vallen = 0
517        self.wType = 1
518        self.name = u'VarFileInfo'
519        raw_name = getRaw(self.name)
520        sublen = 6 + len(raw_name) + 2
521        pad = b''
522        if sublen % 4:
523            pad = b'\000\000'
524        tmp = b''.join([kid.toRaw() for kid in self.kids])
525        self.sublen = sublen + len(pad) + len(tmp)
526        return (struct.pack('hhh', self.sublen, self.vallen, self.wType)
527                + raw_name + b'\000\000' + pad + tmp)
528
529    def __unicode__(self, indent=''):
530        return "%sVarFileInfo([%s])" % (indent, ', '.join(u'%s' % (kid,) for kid in self.kids))
531
532
533@_py3_str_compat
534class VarStruct:
535    """
536    WORD  wLength;        // length of the version resource
537    WORD  wValueLength;   // length of the Value member in the current
538                          // VS_VERSION_INFO structure
539    WORD  wType;          // 1 means text, 0 means binary
540    WCHAR szKey[];        // Contains the Unicode string 'Translation'
541                          // or a user-defined key string value
542    WORD  Padding[];      //
543    WORD  Value[];        // list of one or more values that are language
544                          // and code-page identifiers
545    """
546    def __init__(self, name=None, kids=None):
547        self.name = name or u''
548        self.kids = kids or []
549
550    def fromRaw(self, data, i, limit):
551        i, (self.sublen, self.wValueLength, self.wType, self.name) = parseCommon(data, i)
552        i = nextDWord(i)
553        for j in range(0, self.wValueLength, 2):
554            kid = struct.unpack('h', data[i:i+2])[0]
555            self.kids.append(kid)
556            i += 2
557        return i
558
559    def toRaw(self):
560        self.wValueLength = len(self.kids) * 2
561        self.wType = 0
562        raw_name = getRaw(self.name)
563        sublen = 6 + len(raw_name) + 2
564        pad = b''
565        if sublen % 4:
566            pad = b'\000\000'
567        self.sublen = sublen + len(pad) + self.wValueLength
568        tmp = b''.join([struct.pack('h', kid) for kid in self.kids])
569        return (struct.pack('hhh', self.sublen, self.wValueLength, self.wType)
570                + raw_name + b'\000\000' + pad + tmp)
571
572    def __unicode__(self, indent=u''):
573        return u"VarStruct(u'%s', %r)" % (self.name, self.kids)
574
575
576def SetVersion(exenm, versionfile):
577    if isinstance(versionfile, VSVersionInfo):
578        vs = versionfile
579    else:
580        with codecs.open(versionfile, text_read_mode, 'utf-8') as fp:
581            txt = fp.read()
582        vs = eval(txt)
583
584    # Remember overlay
585    pe = pefile.PE(exenm, fast_load=True)
586    overlay_before = pe.get_overlay()
587    pe.close()
588
589    hdst = win32api.BeginUpdateResource(exenm, 0)
590    win32api.UpdateResource(hdst, pefile.RESOURCE_TYPE['RT_VERSION'], 1, vs.toRaw())
591    win32api.EndUpdateResource (hdst, 0)
592
593    if overlay_before:
594        # Check if the overlay is still present
595        pe = pefile.PE(exenm, fast_load=True)
596        overlay_after = pe.get_overlay()
597        pe.close()
598
599        # If the update removed the overlay data, re-append it
600        if not overlay_after:
601            with open(exenm, 'ab') as exef:
602                exef.write(overlay_before)
603