Codebase list pylnk / 8c465ce
Import upstream version 0.4.2, md5 480f24893704b40d5b15bcd6bf63af22 Kali Janitor 3 years ago
13 changed file(s) with 2343 addition(s) and 2114 deletion(s). Raw diff Collapse all Expand all
+0
-80
.gitignore less more
0 # Byte-compiled / optimized / DLL files
1 __pycache__/
2 *.py[cod]
3 *$py.class
4
5 # Distribution / packaging
6 .Python
7 build/
8 develop-eggs/
9 dist/
10 downloads/
11 eggs/
12 .eggs/
13 lib/
14 lib64/
15 parts/
16 sdist/
17 var/
18 wheels/
19 pip-wheel-metadata/
20 share/python-wheels/
21 *.egg-info/
22 .installed.cfg
23 *.egg
24 MANIFEST
25
26 # Installer logs
27 pip-log.txt
28 pip-delete-this-directory.txt
29
30 # Unit test / coverage reports
31 htmlcov/
32 .tox/
33 .nox/
34 .coverage
35 .coverage.*
36 .cache
37 nosetests.xml
38 coverage.xml
39 *.cover
40 *.py,cover
41 .hypothesis/
42 .pytest_cache/
43
44 # Jupyter Notebook
45 .ipynb_checkpoints
46
47 # IPython
48 profile_default/
49 ipython_config.py
50
51 # pyenv
52 .python-version
53
54 # PEP 582; used by e.g. github.com/David-OConnor/pyflow
55 __pypackages__/
56
57 # Environments
58 .env
59 .venv
60 env/
61 venv/
62 ENV/
63 env.bak/
64 venv.bak/
65
66 # mypy
67 .mypy_cache/
68 .dmypy.json
69 dmypy.json
70
71 # Pyre type checker
72 .pyre/
73
74 # custom
75 .idea/
76
77 # test artifacts
78 /*.lnk
79 /*.ini
+0
-165
LICENSE less more
0 GNU LESSER GENERAL PUBLIC LICENSE
1 Version 3, 29 June 2007
2
3 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
4 Everyone is permitted to copy and distribute verbatim copies
5 of this license document, but changing it is not allowed.
6
7
8 This version of the GNU Lesser General Public License incorporates
9 the terms and conditions of version 3 of the GNU General Public
10 License, supplemented by the additional permissions listed below.
11
12 0. Additional Definitions.
13
14 As used herein, "this License" refers to version 3 of the GNU Lesser
15 General Public License, and the "GNU GPL" refers to version 3 of the GNU
16 General Public License.
17
18 "The Library" refers to a covered work governed by this License,
19 other than an Application or a Combined Work as defined below.
20
21 An "Application" is any work that makes use of an interface provided
22 by the Library, but which is not otherwise based on the Library.
23 Defining a subclass of a class defined by the Library is deemed a mode
24 of using an interface provided by the Library.
25
26 A "Combined Work" is a work produced by combining or linking an
27 Application with the Library. The particular version of the Library
28 with which the Combined Work was made is also called the "Linked
29 Version".
30
31 The "Minimal Corresponding Source" for a Combined Work means the
32 Corresponding Source for the Combined Work, excluding any source code
33 for portions of the Combined Work that, considered in isolation, are
34 based on the Application, and not on the Linked Version.
35
36 The "Corresponding Application Code" for a Combined Work means the
37 object code and/or source code for the Application, including any data
38 and utility programs needed for reproducing the Combined Work from the
39 Application, but excluding the System Libraries of the Combined Work.
40
41 1. Exception to Section 3 of the GNU GPL.
42
43 You may convey a covered work under sections 3 and 4 of this License
44 without being bound by section 3 of the GNU GPL.
45
46 2. Conveying Modified Versions.
47
48 If you modify a copy of the Library, and, in your modifications, a
49 facility refers to a function or data to be supplied by an Application
50 that uses the facility (other than as an argument passed when the
51 facility is invoked), then you may convey a copy of the modified
52 version:
53
54 a) under this License, provided that you make a good faith effort to
55 ensure that, in the event an Application does not supply the
56 function or data, the facility still operates, and performs
57 whatever part of its purpose remains meaningful, or
58
59 b) under the GNU GPL, with none of the additional permissions of
60 this License applicable to that copy.
61
62 3. Object Code Incorporating Material from Library Header Files.
63
64 The object code form of an Application may incorporate material from
65 a header file that is part of the Library. You may convey such object
66 code under terms of your choice, provided that, if the incorporated
67 material is not limited to numerical parameters, data structure
68 layouts and accessors, or small macros, inline functions and templates
69 (ten or fewer lines in length), you do both of the following:
70
71 a) Give prominent notice with each copy of the object code that the
72 Library is used in it and that the Library and its use are
73 covered by this License.
74
75 b) Accompany the object code with a copy of the GNU GPL and this license
76 document.
77
78 4. Combined Works.
79
80 You may convey a Combined Work under terms of your choice that,
81 taken together, effectively do not restrict modification of the
82 portions of the Library contained in the Combined Work and reverse
83 engineering for debugging such modifications, if you also do each of
84 the following:
85
86 a) Give prominent notice with each copy of the Combined Work that
87 the Library is used in it and that the Library and its use are
88 covered by this License.
89
90 b) Accompany the Combined Work with a copy of the GNU GPL and this license
91 document.
92
93 c) For a Combined Work that displays copyright notices during
94 execution, include the copyright notice for the Library among
95 these notices, as well as a reference directing the user to the
96 copies of the GNU GPL and this license document.
97
98 d) Do one of the following:
99
100 0) Convey the Minimal Corresponding Source under the terms of this
101 License, and the Corresponding Application Code in a form
102 suitable for, and under terms that permit, the user to
103 recombine or relink the Application with a modified version of
104 the Linked Version to produce a modified Combined Work, in the
105 manner specified by section 6 of the GNU GPL for conveying
106 Corresponding Source.
107
108 1) Use a suitable shared library mechanism for linking with the
109 Library. A suitable mechanism is one that (a) uses at run time
110 a copy of the Library already present on the user's computer
111 system, and (b) will operate properly with a modified version
112 of the Library that is interface-compatible with the Linked
113 Version.
114
115 e) Provide Installation Information, but only if you would otherwise
116 be required to provide such information under section 6 of the
117 GNU GPL, and only to the extent that such information is
118 necessary to install and execute a modified version of the
119 Combined Work produced by recombining or relinking the
120 Application with a modified version of the Linked Version. (If
121 you use option 4d0, the Installation Information must accompany
122 the Minimal Corresponding Source and Corresponding Application
123 Code. If you use option 4d1, you must provide the Installation
124 Information in the manner specified by section 6 of the GNU GPL
125 for conveying Corresponding Source.)
126
127 5. Combined Libraries.
128
129 You may place library facilities that are a work based on the
130 Library side by side in a single library together with other library
131 facilities that are not Applications and are not covered by this
132 License, and convey such a combined library under terms of your
133 choice, if you do both of the following:
134
135 a) Accompany the combined library with a copy of the same work based
136 on the Library, uncombined with any other library facilities,
137 conveyed under the terms of this License.
138
139 b) Give prominent notice with the combined library that part of it
140 is a work based on the Library, and explaining where to find the
141 accompanying uncombined form of the same work.
142
143 6. Revised Versions of the GNU Lesser General Public License.
144
145 The Free Software Foundation may publish revised and/or new versions
146 of the GNU Lesser General Public License from time to time. Such new
147 versions will be similar in spirit to the present version, but may
148 differ in detail to address new problems or concerns.
149
150 Each version is given a distinguishing version number. If the
151 Library as you received it specifies that a certain numbered version
152 of the GNU Lesser General Public License "or any later version"
153 applies to it, you have the option of following the terms and
154 conditions either of that published version or of any later version
155 published by the Free Software Foundation. If the Library as you
156 received it does not specify a version number of the GNU Lesser
157 General Public License, you may choose any version of the GNU Lesser
158 General Public License ever published by the Free Software Foundation.
159
160 If the Library as you received it specifies that a proxy can decide
161 whether future versions of the GNU Lesser General Public License shall
162 apply, that proxy's public statement of acceptance of any version is
163 permanent authorization for you to choose that version for the
164 Library.
0 Metadata-Version: 2.1
1 Name: pylnk3
2 Version: 0.4.2
3 Summary: Windows LNK File Parser and Creator
4 Home-page: https://github.com/strayge/pylnk
5 Author: strayge
6 Author-email: [email protected]
7 License: GNU Library or Lesser General Public License (LGPL)
8 Description: # PyLnk 3
9
10 [![PyPI version shields.io](https://img.shields.io/pypi/v/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
11 [![PyPI pyversions](https://img.shields.io/pypi/pyversions/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
12 [![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
13
14 Python library for reading and writing Windows shortcut files (.lnk).
15 Converted to support python 3.
16
17 This library can parse .lnk files and extract all relevant information from
18 them. Parsing a .lnk file yields a LNK object which can be altered and saved
19 again. Moreover, .lnk file can be created from scratch be creating a LNK
20 object, populating it with data and then saving it to a file. As that
21 process requires some knowledge about the internals of .lnk files, some
22 convenience functions are provided.
23
24 Limitation: Windows knows lots of different types of shortcuts which all have
25 different formats. This library currently only supports shortcuts to files and
26 folders on the local machine.
27
28 ## CLI
29
30 Mainly tool has two basic commands.
31
32 #### Parse existed lnk file
33
34 ```sh
35 pylnk3 parse [-h] filename [props [props ...]]
36
37 positional arguments:
38 filename lnk filename to read
39 props props path to read
40
41 optional arguments:
42 -h, --help show this help message and exit
43 ```
44
45 #### Create new lnk file
46
47 ```sh
48 usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]]
49 [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]]
50 target name
51
52 positional arguments:
53 target target path
54 name lnk filename to create
55
56 optional arguments:
57 -h, --help show this help message and exit
58 --arguments [ARGUMENTS], -a [ARGUMENTS]
59 additional arguments
60 --description [DESCRIPTION], -d [DESCRIPTION]
61 description
62 --icon [ICON], -i [ICON]
63 icon filename
64 --icon-index [ICON_INDEX], -ii [ICON_INDEX]
65 icon index
66 --workdir [WORKDIR], -w [WORKDIR]
67 working directory
68 --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}]
69 window mode
70 ```
71
72 #### Examples
73 ```sh
74 pylnk3 p filename.lnk
75 pylnk3 c c:\prog.exe shortcut.lnk
76 pylnk3 c \\192.168.1.1\share\file.doc doc.lnk
77 pylnk3 create c:\1.txt text.lnk -m Minimized -d "Description"
78 ```
79
80 ## Changes
81
82 **0.4.2**
83 changed logic for Lnk.path choose (in case of different paths presents at different structures)
84 read links with root as GUID of KNOWN_FOLDER
85 [FIX] disabled padding for writing LinkInfo.local_base_path
86
87 **0.4.0**
88 added support for network links
89 reworked CLI (added more options for creating links)
90 added entry point for call tool just like `pylnk3`
91 [FIX] allow build links for non-existed (from this machine) paths
92 [FIX] correct building links on Linux (now expect Windows-like path)
93 [FIX] fixed path priority at parsing with both local & remote presents
94
95
96 **0.3.0**
97 added support links to UWP apps
98
99
100 **0.2.1**
101 released to PyPI
102
103
104 **0.2.0**
105 converted to python 3
106
107 Keywords: lnk,shortcut,windows
108 Platform: UNKNOWN
109 Classifier: Programming Language :: Python :: 3.6
110 Classifier: Programming Language :: Python :: 3.7
111 Classifier: Programming Language :: Python :: 3.8
112 Classifier: Programming Language :: Python :: 3.9
113 Classifier: Intended Audience :: Developers
114 Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
115 Classifier: Operating System :: OS Independent
116 Classifier: Topic :: Software Development :: Libraries :: Python Modules
117 Requires-Python: >=3.6
118 Description-Content-Type: text/markdown
0 # PyLink3
0 # PyLnk 3
1
2 [![PyPI version shields.io](https://img.shields.io/pypi/v/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
3 [![PyPI pyversions](https://img.shields.io/pypi/pyversions/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
4 [![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
15
26 Python library for reading and writing Windows shortcut files (.lnk).
37 Converted to support python 3.
1216 Limitation: Windows knows lots of different types of shortcuts which all have
1317 different formats. This library currently only supports shortcuts to files and
1418 folders on the local machine.
19
20 ## CLI
21
22 Mainly tool has two basic commands.
23
24 #### Parse existed lnk file
25
26 ```sh
27 pylnk3 parse [-h] filename [props [props ...]]
28
29 positional arguments:
30 filename lnk filename to read
31 props props path to read
32
33 optional arguments:
34 -h, --help show this help message and exit
35 ```
36
37 #### Create new lnk file
38
39 ```sh
40 usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]]
41 [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]]
42 target name
43
44 positional arguments:
45 target target path
46 name lnk filename to create
47
48 optional arguments:
49 -h, --help show this help message and exit
50 --arguments [ARGUMENTS], -a [ARGUMENTS]
51 additional arguments
52 --description [DESCRIPTION], -d [DESCRIPTION]
53 description
54 --icon [ICON], -i [ICON]
55 icon filename
56 --icon-index [ICON_INDEX], -ii [ICON_INDEX]
57 icon index
58 --workdir [WORKDIR], -w [WORKDIR]
59 working directory
60 --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}]
61 window mode
62 ```
63
64 #### Examples
65 ```sh
66 pylnk3 p filename.lnk
67 pylnk3 c c:\prog.exe shortcut.lnk
68 pylnk3 c \\192.168.1.1\share\file.doc doc.lnk
69 pylnk3 create c:\1.txt text.lnk -m Minimized -d "Description"
70 ```
71
72 ## Changes
73
74 **0.4.2**
75 changed logic for Lnk.path choose (in case of different paths presents at different structures)
76 read links with root as GUID of KNOWN_FOLDER
77 [FIX] disabled padding for writing LinkInfo.local_base_path
78
79 **0.4.0**
80 added support for network links
81 reworked CLI (added more options for creating links)
82 added entry point for call tool just like `pylnk3`
83 [FIX] allow build links for non-existed (from this machine) paths
84 [FIX] correct building links on Linux (now expect Windows-like path)
85 [FIX] fixed path priority at parsing with both local & remote presents
86
87
88 **0.3.0**
89 added support links to UWP apps
90
91
92 **0.2.1**
93 released to PyPI
94
95
96 **0.2.0**
97 converted to python 3
+0
-1862
pylnk.py less more
0 #!/usr/bin/env python3
1
2 # original version written by Tim-Christian Mundt (2011):
3 # https://sourceforge.net/p/pylnk/code/HEAD/tree/trunk/pylnk.py
4
5 # converted to python3 by strayge:
6 # https://github.com/strayge/pylnk
7
8 import os
9 import re
10 import sys
11 import time
12 from datetime import datetime
13 from io import BytesIO, IOBase
14 from pprint import pformat
15 from struct import pack, unpack
16 from typing import Optional, Union, Tuple, Dict
17
18 DEFAULT_CHARSET = 'cp1251'
19
20 # ---- constants
21
22 _SIGNATURE = b'L\x00\x00\x00'
23 _GUID = b'\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00F'
24 _LINK_INFO_HEADER_DEFAULT = 0x1C
25 _LINK_INFO_HEADER_OPTIONAL = 0x24
26
27 _LINK_FLAGS = (
28 'HasLinkTargetIDList',
29 'HasLinkInfo',
30 'HasName',
31 'HasRelativePath',
32 'HasWorkingDir',
33 'HasArguments',
34 'HasIconLocation',
35 'IsUnicode',
36 'ForceNoLinkInfo',
37 # new
38 'HasExpString',
39 'RunInSeparateProcess',
40 'Unused1',
41 'HasDarwinID',
42 'RunAsUser',
43 'HasExpIcon',
44 'NoPidlAlias',
45 'Unused2',
46 'RunWithShimLayer',
47 'ForceNoLinkTrack',
48 'EnableTargetMetadata',
49 'DisableLinkPathTracking',
50 'DisableKnownFolderTracking',
51 'DisableKnownFolderAlias',
52 'AllowLinkToLink',
53 'UnaliasOnSave',
54 'PreferEnvironmentPath',
55 'KeepLocalIDListForUNCTarget',
56 )
57
58 _FILE_ATTRIBUTES_FLAGS = (
59 'read_only', 'hidden', 'system_file', 'reserved1',
60 'directory', 'archive', 'reserved2', 'normal',
61 'temporary', 'sparse_file', 'reparse_point',
62 'compressed', 'offline', 'not_content_indexed',
63 'encrypted',
64 )
65
66 _MODIFIER_KEYS = ('SHIFT', 'CONTROL', 'ALT')
67
68 WINDOW_NORMAL = "Normal"
69 WINDOW_MAXIMIZED = "Maximized"
70 WINDOW_MINIMIZED = "Minimized"
71 _SHOW_COMMANDS = {1: WINDOW_NORMAL, 3: WINDOW_MAXIMIZED, 7: WINDOW_MINIMIZED}
72 _SHOW_COMMAND_IDS = dict((v, k) for k, v in _SHOW_COMMANDS.items())
73
74 DRIVE_UNKNOWN = "Unknown"
75 DRIVE_NO_ROOT_DIR = "No root directory"
76 DRIVE_REMOVABLE = "Removable"
77 DRIVE_FIXED = "Fixed (Hard disk)"
78 DRIVE_REMOTE = "Remote (Network drive)"
79 DRIVE_CDROM = "CD-ROM"
80 DRIVE_RAMDISK = "Ram disk"
81 _DRIVE_TYPES = {0: DRIVE_UNKNOWN,
82 1: DRIVE_NO_ROOT_DIR,
83 2: DRIVE_REMOVABLE,
84 3: DRIVE_FIXED,
85 4: DRIVE_REMOTE,
86 5: DRIVE_CDROM,
87 6: DRIVE_RAMDISK}
88 _DRIVE_TYPE_IDS = dict((v, k) for k, v in _DRIVE_TYPES.items())
89
90 _KEYS = {
91 0x30: '0', 0x31: '1', 0x32: '2', 0x33: '3', 0x34: '4', 0x35: '5', 0x36: '6',
92 0x37: '7', 0x38: '8', 0x39: '9', 0x41: 'A', 0x42: 'B', 0x43: 'C', 0x44: 'D',
93 0x45: 'E', 0x46: 'F', 0x47: 'G', 0x48: 'H', 0x49: 'I', 0x4A: 'J', 0x4B: 'K',
94 0x4C: 'L', 0x4D: 'M', 0x4E: 'N', 0x4F: 'O', 0x50: 'P', 0x51: 'Q', 0x52: 'R',
95 0x53: 'S', 0x54: 'T', 0x55: 'U', 0x56: 'V', 0x57: 'W', 0x58: 'X', 0x59: 'Y',
96 0x5A: 'Z', 0x70: 'F1', 0x71: 'F2', 0x72: 'F3', 0x73: 'F4', 0x74: 'F5',
97 0x75: 'F6', 0x76: 'F7', 0x77: 'F8', 0x78: 'F9', 0x79: 'F10', 0x7A: 'F11',
98 0x7B: 'F12', 0x7C: 'F13', 0x7D: 'F14', 0x7E: 'F15', 0x7F: 'F16', 0x80: 'F17',
99 0x81: 'F18', 0x82: 'F19', 0x83: 'F20', 0x84: 'F21', 0x85: 'F22', 0x86: 'F23',
100 0x87: 'F24', 0x90: 'NUM LOCK', 0x91: 'SCROLL LOCK'
101 }
102 _KEY_CODES = dict((v, k) for k, v in _KEYS.items())
103
104 ROOT_MY_COMPUTER = 'MY_COMPUTER'
105 ROOT_MY_DOCUMENTS = 'MY_DOCUMENTS'
106 ROOT_NETWORK_SHARE = 'NETWORK_SHARE'
107 ROOT_NETWORK_SERVER = 'NETWORK_SERVER'
108 ROOT_NETWORK_PLACES = 'NETWORK_PLACES'
109 ROOT_NETWORK_DOMAIN = 'NETWORK_DOMAIN'
110 ROOT_INTERNET = 'INTERNET'
111 RECYCLE_BIN = 'RECYCLE_BIN'
112 ROOT_CONTROL_PANEL = 'CONTROL_PANEL'
113 ROOT_USER = 'USERPROFILE'
114 ROOT_UWP_APPS = 'APPS'
115
116 _ROOT_LOCATIONS = {
117 '{20D04FE0-3AEA-1069-A2D8-08002B30309D}': ROOT_MY_COMPUTER,
118 '{450D8FBA-AD25-11D0-98A8-0800361B1103}': ROOT_MY_DOCUMENTS,
119 '{54a754c0-4bf1-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SHARE,
120 '{c0542a90-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SERVER,
121 '{208D2C60-3AEA-1069-A2D7-08002B30309D}': ROOT_NETWORK_PLACES,
122 '{46e06680-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_DOMAIN,
123 '{871C5380-42A0-1069-A2EA-08002B30309D}': ROOT_INTERNET,
124 '{645FF040-5081-101B-9F08-00AA002F954E}': RECYCLE_BIN,
125 '{21EC2020-3AEA-1069-A2DD-08002B30309D}': ROOT_CONTROL_PANEL,
126 '{59031A47-3F72-44A7-89C5-5595FE6B30EE}': ROOT_USER,
127 '{4234D49B-0245-4DF3-B780-3893943456E1}': ROOT_UWP_APPS,
128 }
129 _ROOT_LOCATION_GUIDS = dict((v, k) for k, v in _ROOT_LOCATIONS.items())
130
131 TYPE_FOLDER = 'FOLDER'
132 TYPE_FILE = 'FILE'
133 _ENTRY_TYPES = {
134 0x31: 'FOLDER',
135 0x32: 'FILE',
136 0x35: 'FOLDER (UNICODE)',
137 0x36: 'FILE (UNICODE)',
138 # founded in doc, not tested
139 0x1f: 'ROOT_FOLDER',
140 0x61: 'URI',
141 0x71: 'CONTROL_PANEL',
142 }
143 _ENTRY_TYPE_IDS = dict((v, k) for k, v in _ENTRY_TYPES.items())
144
145 _DRIVE_PATTERN = re.compile(r'(\w)[:/\\]*$')
146
147 # ---- read and write binary data
148
149
150 def read_byte(buf):
151 return unpack('<B', buf.read(1))[0]
152
153
154 def read_short(buf):
155 return unpack('<H', buf.read(2))[0]
156
157
158 def read_int(buf):
159 return unpack('<I', buf.read(4))[0]
160
161
162 def read_double(buf):
163 return unpack('<Q', buf.read(8))[0]
164
165
166 def read_cunicode(buf):
167 s = b""
168 b = buf.read(2)
169 while b != b'\x00\x00':
170 s += b
171 b = buf.read(2)
172 return s.decode('utf-16-le')
173
174
175 def read_cstring(buf, padding=False):
176 s = b""
177 b = buf.read(1)
178 while b != b'\x00':
179 s += b
180 b = buf.read(1)
181 if padding and not len(s) % 2:
182 buf.read(1) # make length + terminator even
183 # TODO: encoding is not clear, unicode-escape has been necessary sometimes
184 return s.decode(DEFAULT_CHARSET)
185
186
187 def read_sized_string(buf, string=True):
188 size = read_short(buf)
189 if string:
190 return buf.read(size*2).decode('utf-16-le')
191 else:
192 return buf.read(size)
193
194
195 def get_bits(value, start, count, length=16):
196 mask = 0
197 for i in range(count):
198 mask = mask | 1 << i
199 shift = length - start - count
200 return value >> shift & mask
201
202
203 def read_dos_datetime(buf):
204 date = read_short(buf)
205 time = read_short(buf)
206 year = get_bits(date, 0, 7) + 1980
207 month = get_bits(date, 7, 4)
208 day = get_bits(date, 11, 5)
209 hour = get_bits(time, 0, 5)
210 minute = get_bits(time, 5, 6)
211 second = get_bits(time, 11, 5)
212 # fix zeroes
213 month = max(month, 1)
214 day = max(day, 1)
215 return datetime(year, month, day, hour, minute, second)
216
217
218 def write_byte(val, buf):
219 buf.write(pack('<B', val))
220
221
222 def write_short(val, buf):
223 buf.write(pack('<H', val))
224
225
226 def write_int(val, buf):
227 buf.write(pack('<I', val))
228
229
230 def write_double(val, buf):
231 buf.write(pack('<Q', val))
232
233
234 def write_cstring(val, buf, padding=False):
235 # val = val.encode('unicode-escape').replace('\\\\', '\\')
236 val = val.encode(DEFAULT_CHARSET)
237 buf.write(val + b'\x00')
238 if padding and not len(val) % 2:
239 buf.write(b'\x00')
240
241
242 def write_cunicode(val, buf):
243 uni = val.encode('utf-16-le')
244 buf.write(uni + b'\x00\x00')
245
246
247 def write_sized_string(val, buf, string=True):
248 size = len(val)
249 write_short(size, buf)
250 if string:
251 buf.write(val.encode('utf-16-le'))
252 else:
253 buf.write(val.encode())
254
255
256 def put_bits(bits, target, start, count, length=16):
257 return target | bits << (length - start - count)
258
259
260 def write_dos_datetime(val, buf):
261 date = time = 0
262 date = put_bits(val.year-1980, date, 0, 7)
263 date = put_bits(val.month, date, 7, 4)
264 date = put_bits(val.day, date, 11, 5)
265 time = put_bits(val.hour, time, 0, 5)
266 time = put_bits(val.minute, time, 5, 6)
267 time = put_bits(val.second, time, 11, 5)
268 write_short(date, buf)
269 write_short(time, buf)
270
271
272 # ---- helpers
273
274 def convert_time_to_unix(windows_time):
275 # Windows time is specified as the number of 0.1 nanoseconds since January 1, 1601.
276 # UNIX time is specified as the number of seconds since January 1, 1970.
277 # There are 134774 days (or 11644473600 seconds) between these dates.
278 unix_time = windows_time / 10000000.0 - 11644473600
279 try:
280 return datetime.fromtimestamp(unix_time)
281 except OSError:
282 return datetime.now()
283
284
285 def convert_time_to_windows(unix_time):
286 if isinstance(unix_time, datetime):
287 unix_time = time.mktime(unix_time.timetuple())
288 return int((unix_time + 11644473600) * 10000000)
289
290
291 class FormatException(Exception):
292 pass
293
294
295 class MissingInformationException(Exception):
296 pass
297
298
299 class InvalidKeyException(Exception):
300 pass
301
302
303 def guid_from_bytes(bytes):
304 if len(bytes) != 16:
305 raise FormatException("This is no valid _GUID: %s" % bytes)
306 ordered = [
307 bytes[3], bytes[2], bytes[1], bytes[0],
308 bytes[5], bytes[4], bytes[7], bytes[6],
309 bytes[8], bytes[9], bytes[10], bytes[11],
310 bytes[12], bytes[13], bytes[14], bytes[15]
311 ]
312 return "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered])
313
314
315 def bytes_from_guid(guid):
316 nums = [
317 guid[1:3], guid[3:5], guid[5:7], guid[7:9],
318 guid[10:12], guid[12:14], guid[15:17], guid[17:19],
319 guid[20:22], guid[22:24], guid[25:27], guid[27:29],
320 guid[29:31], guid[31:33], guid[33:35], guid[35:37]
321 ]
322 ordered_nums = [
323 nums[3], nums[2], nums[1], nums[0],
324 nums[5], nums[4], nums[7], nums[6],
325 nums[8], nums[9], nums[10], nums[11],
326 nums[12], nums[13], nums[14], nums[15],
327 ]
328 return bytes([int(x, 16) for x in ordered_nums])
329
330
331 def assert_lnk_signature(f):
332 f.seek(0)
333 sig = f.read(4)
334 guid = f.read(16)
335 if sig != _SIGNATURE:
336 raise FormatException("This is not a .lnk file.")
337 if guid != _GUID:
338 raise FormatException("Cannot read this kind of .lnk file.")
339
340
341 def is_lnk(f):
342 if hasattr(f, 'name'):
343 if f.name.split(os.path.extsep)[-1] == "lnk":
344 assert_lnk_signature(f)
345 return True
346 else:
347 return False
348 else:
349 try:
350 assert_lnk_signature(f)
351 return True
352 except FormatException:
353 return False
354
355
356 def path_levels(p):
357 dirname, base = os.path.split(p)
358 if base != '':
359 for level in path_levels(dirname):
360 yield level
361 yield p
362
363
364 def is_drive(data):
365 if type(data) not in (str, str):
366 return False
367 p = re.compile("[a-zA-Z]:\\\\?$")
368 return p.match(data) is not None
369
370
371 # ---- data structures
372
373 class Flags(object):
374
375 def __init__(self, flag_names: Tuple[str, ...], flags_bytes=0):
376 self._flag_names = flag_names
377 self._flags: Dict[str, bool] = dict([(name, False) for name in flag_names])
378 self.set_flags(flags_bytes)
379
380 def set_flags(self, flags_bytes):
381 for pos, flag_name in enumerate(self._flag_names):
382 self._flags[flag_name] = bool(flags_bytes >> pos & 0x1)
383
384 @property
385 def bytes(self):
386 bytes = 0
387 for pos in range(len(self._flag_names)):
388 bytes = (self._flags[self._flag_names[pos]] and 1 or 0) << pos | bytes
389 return bytes
390
391 def __getitem__(self, key):
392 if key in self._flags:
393 return object.__getattribute__(self, '_flags')[key]
394 return object.__getattribute__(self, key)
395
396 def __setitem__(self, key, value):
397 if key not in self._flags:
398 raise KeyError("The key '%s' is not defined for those flags." % key)
399 self._flags[key] = value
400
401 def __getattr__(self, key):
402 if key in self._flags:
403 return object.__getattribute__(self, '_flags')[key]
404 return object.__getattribute__(self, key)
405
406 def __setattr__(self, key, value):
407 if '_flags' not in self.__dict__:
408 object.__setattr__(self, key, value)
409 elif key in self.__dict__:
410 object.__setattr__(self, key, value)
411 else:
412 self.__setitem__(key, value)
413
414 def __str__(self):
415 return pformat(self._flags, indent=2)
416
417
418 class ModifierKeys(Flags):
419
420 def __init__(self, flags_bytes=0):
421 Flags.__init__(self, _MODIFIER_KEYS, flags_bytes)
422
423 def __str__(self):
424 s = ""
425 s += self.CONTROL and "CONTROL+" or ""
426 s += self.SHIFT and "SHIFT+" or ""
427 s += self.ALT and "ALT+" or ""
428 return s
429
430
431 # _ROOT_INDEX = {
432 # 0x00: 'INTERNET_EXPLORER1',
433 # 0x42: 'LIBRARIES',
434 # 0x44: 'USERS',
435 # 0x48: 'MY_DOCUMENTS',
436 # 0x50: 'MY_COMPUTER',
437 # 0x58: 'MY_NETWORK_PLACES',
438 # 0x60: 'RECYCLE_BIN',
439 # 0x68: 'INTERNET_EXPLORER2',
440 # 0x70: 'UNKNOWN',
441 # 0x80: 'MY_GAMES',
442 # }
443
444
445 class RootEntry(object):
446
447 def __init__(self, root):
448 if root is not None:
449 # create from text representation
450 if root in list(_ROOT_LOCATION_GUIDS.keys()):
451 self.root = root
452 self.guid = _ROOT_LOCATION_GUIDS[root]
453 return
454
455 # from binary
456 root_type = root[0]
457 index = root[1]
458 guid_bytes = root[2:18]
459 self.guid = guid_from_bytes(guid_bytes)
460 self.root = _ROOT_LOCATIONS.get(self.guid, f"UNKNOWN {self.guid}")
461 # if self.root == "UNKNOWN":
462 # self.root = _ROOT_INDEX.get(index, "UNKNOWN")
463
464 @property
465 def bytes(self):
466 guid = self.guid[1:-1].replace('-', '')
467 chars = [bytes([int(x, 16)]) for x in [guid[i:i+2] for i in range(0, 32, 2)]]
468 return (
469 b'\x1F\x50'
470 + chars[3] + chars[2] + chars[1] + chars[0]
471 + chars[5] + chars[4] + chars[7] + chars[6]
472 + b''.join(chars[8:])
473 )
474
475 def __str__(self):
476 return "<RootEntry: %s>" % self.root
477
478
479 class DriveEntry(object):
480
481 def __init__(self, drive: str):
482 if len(drive) == 23:
483 # binary data from parsed lnk
484 self.drive = drive[1:3]
485 else:
486 # text representation
487 m = _DRIVE_PATTERN.match(drive.strip())
488 if m:
489 self.drive = m.groups()[0].upper() + ':'
490 self.drive = self.drive.encode()
491 else:
492 raise FormatException("This is not a valid drive: " + str(drive))
493
494 @property
495 def bytes(self):
496 drive = self.drive
497 padded_str = drive + b'\\' + b'\x00' * 19
498 return b'\x2F' + padded_str
499 # drive = self.drive
500 # if isinstance(drive, str):
501 # drive = drive.encode()
502 # return b'/' + drive + b'\\' + b'\x00' * 19
503
504 def __str__(self):
505 return "<DriveEntry: %s>" % self.drive
506
507
508 class PathSegmentEntry(object):
509
510 def __init__(self, bytes=None):
511 self.type = None
512 self.file_size = None
513 self.modified = None
514 self.short_name = None
515 self.created = None
516 self.accessed = None
517 self.full_name = None
518 if bytes is None:
519 return
520
521 buf = BytesIO(bytes)
522 self.type = _ENTRY_TYPES.get(read_short(buf), 'UNKNOWN')
523 short_name_is_unicode = self.type.endswith('(UNICODE)')
524 self.file_size = read_int(buf)
525 self.modified = read_dos_datetime(buf)
526 unknown = read_short(buf) # FileAttributesL
527 if short_name_is_unicode:
528 self.short_name = read_cunicode(buf)
529 else:
530 self.short_name = read_cstring(buf, padding=True)
531 extra_size = read_short(buf)
532 extra_version = read_short(buf)
533 extra_signature = read_int(buf)
534 if extra_signature == 0xBEEF0004:
535 # indicator_1 = read_short(buf) # see below
536 # only_83 = read_short(buf) < 0x03
537 # unknown = read_short(buf) # 0x04
538 # self.is_unicode = read_short(buf) == 0xBeef
539 self.created = read_dos_datetime(buf) # 4 bytes
540 self.accessed = read_dos_datetime(buf) # 4 bytes
541 offset_unicode = read_short(buf) # offset from start of extra_size
542 # only_83_2 = offset_unicode >= indicator_1 or offset_unicode < 0x14
543 if extra_version >= 7:
544 offset_ansi = read_short(buf)
545 file_reference = read_double(buf)
546 unknown2 = read_double(buf)
547 long_string_size = 0
548 if extra_version >= 3:
549 long_string_size = read_short(buf)
550 if extra_version >= 9:
551 unknown4 = read_int(buf)
552 if extra_version >= 8:
553 unknown5 = read_int(buf)
554 if extra_version >= 3:
555 self.full_name = read_cunicode(buf)
556 if long_string_size > 0:
557 if extra_version >= 7:
558 self.localized_name = read_cunicode(buf)
559 else:
560 self.localized_name = read_cstring(buf)
561 version_offset = read_short(buf)
562
563 @classmethod
564 def create_for_path(cls, path):
565 entry = cls()
566 entry.type = os.path.isdir(path) and TYPE_FOLDER or TYPE_FILE
567 st = os.stat(path)
568 entry.file_size = st.st_size
569 entry.modified = datetime.fromtimestamp(st.st_mtime)
570 entry.short_name = os.path.split(path)[1]
571 entry.created = datetime.fromtimestamp(st.st_ctime)
572 entry.accessed = datetime.fromtimestamp(st.st_atime)
573 entry.full_name = entry.short_name
574 return entry
575
576 def _validate(self):
577 if self.type is None:
578 raise MissingInformationException("Type is missing, choose either TYPE_FOLDER or TYPE_FILE.")
579 if self.file_size is None:
580 if self.type.startswith('FOLDER'):
581 self.file_size = 0
582 else:
583 raise MissingInformationException("File size missing")
584 if self.created is None:
585 self.created = datetime.now()
586 if self.modified is None:
587 self.modified = datetime.now()
588 if self.accessed is None:
589 self.accessed = datetime.now()
590 # if self.modified is None or self.accessed is None or self.created is None:
591 # raise MissingInformationException("Date information missing")
592 if self.full_name is None:
593 raise MissingInformationException("A full name is missing")
594 if self.short_name is None:
595 self.short_name = self.full_name
596
597 @property
598 def bytes(self):
599 if self.full_name is None:
600 return
601 self._validate()
602 out = BytesIO()
603 entry_type = self.type
604 short_name_len = len(self.short_name) + 1
605 try:
606 self.short_name.encode("ascii")
607 short_name_is_unicode = False
608 short_name_len += short_name_len % 2 # padding
609 except (UnicodeEncodeError, UnicodeDecodeError):
610 short_name_is_unicode = True
611 short_name_len = short_name_len * 2
612 self.type += " (UNICODE)"
613 write_short(_ENTRY_TYPE_IDS[entry_type], out)
614 write_int(self.file_size, out)
615 write_dos_datetime(self.modified, out)
616 write_short(0x10, out)
617 if short_name_is_unicode:
618 write_cunicode(self.short_name, out)
619 else:
620 write_cstring(self.short_name, out, padding=True)
621 indicator = 24 + 2 * len(self.short_name)
622 write_short(indicator, out) # size
623 write_short(0x03, out) # version
624 write_short(0x04, out) # signature part1
625 write_short(0xBeef, out) # signature part2
626 write_dos_datetime(self.created, out)
627 write_dos_datetime(self.accessed, out)
628 offset_unicode = 0x14 # fixed data structure, always the same
629 write_short(offset_unicode, out)
630 offset_ansi = 0 # we always write unicode
631 write_short(offset_ansi, out) # long_string_size
632 write_cunicode(self.full_name, out)
633 offset_part2 = 0x0E + short_name_len
634 write_short(offset_part2, out)
635 return out.getvalue()
636
637 def __str__(self):
638 return "<PathSegmentEntry: %s>" % self.full_name
639
640
641 class UwpSubBlock:
642
643 block_names = {
644 0x11: 'PackageFamilyName',
645 # 0x0e: '',
646 # 0x19: '',
647 0x15: 'PackageFullName',
648 0x05: 'Target',
649 0x0f: 'Location',
650 0x20: 'RandomGuid',
651 0x0c: 'Square150x150Logo',
652 0x02: 'Square44x44Logo',
653 0x0d: 'Wide310x150Logo',
654 # 0x04: '',
655 # 0x05: '',
656 0x13: 'Square310x310Logo',
657 # 0x0e: '',
658 0x0b: 'DisplayName',
659 0x14: 'Square71x71Logo',
660 0x64: 'RandomByte',
661 0x0a: 'DisplayName',
662 # 0x07: '',
663 }
664
665 block_types = {
666 'string': [0x11, 0x15, 0x05, 0x0f, 0x0c, 0x02, 0x0d, 0x13, 0x0b, 0x14, 0x0a],
667 }
668
669 def __init__(self, bytes=None, type=None, value=None):
670 self._data = bytes or b''
671 self.type = type
672 self.value = value
673 self.name = None
674 if self.type is not None:
675 self.name = self.block_names.get(self.type, 'UNKNOWN')
676 if not bytes:
677 return
678 buf = BytesIO(bytes)
679 self.type = read_byte(buf)
680 self.name = self.block_names.get(self.type, 'UNKNOWN')
681
682 self.value = self._data[1:] # skip type
683 if self.type in self.block_types['string']:
684 unknown = read_int(buf)
685 probably_type = read_int(buf)
686 if probably_type == 0x1f:
687 string_len = read_int(buf)
688 self.value = read_cunicode(buf)
689
690 def __str__(self):
691 string = f'UwpSubBlock {self.name} ({hex(self.type)}): {self.value}'
692 return string.strip()
693
694 @property
695 def bytes(self):
696 out = BytesIO()
697 if self.value:
698 if isinstance(self.value, str):
699 string_len = len(self.value) + 1
700
701 write_byte(self.type, out)
702 write_int(0, out)
703 write_int(0x1f, out)
704
705 write_int(string_len, out)
706 write_cunicode(self.value, out)
707 if string_len % 2 == 1: # padding
708 write_short(0, out)
709
710 elif isinstance(self.value, bytes):
711 write_byte(self.type, out)
712 out.write(self.value)
713
714 result = out.getvalue()
715 return result
716
717
718 class UwpMainBlock:
719 magic = b'\x31\x53\x50\x53'
720
721 def __init__(self, bytes=None, guid: Optional[str] = None, blocks=None):
722 self._data = bytes or b''
723 self._blocks = blocks or []
724 self.guid: str = guid
725 if not bytes:
726 return
727 buf = BytesIO(bytes)
728 magic = buf.read(4)
729 self.guid = guid_from_bytes(buf.read(16))
730 # read sub blocks
731 while True:
732 sub_block_size = read_int(buf)
733 if not sub_block_size: # last size is zero
734 break
735 sub_block_data = buf.read(sub_block_size - 4) # includes block_size
736 self._blocks.append(UwpSubBlock(sub_block_data))
737
738 def __str__(self):
739 string = f'<UwpMainBlock> {self.guid}:\n'
740 for block in self._blocks:
741 string += f' {block}\n'
742 return string.strip()
743
744 @property
745 def bytes(self):
746 blocks_bytes = [block.bytes for block in self._blocks]
747 out = BytesIO()
748 out.write(self.magic)
749 out.write(bytes_from_guid(self.guid))
750 for block in blocks_bytes:
751 write_int(len(block) + 4, out)
752 out.write(block)
753 write_int(0, out)
754 result = out.getvalue()
755 return result
756
757
758 class UwpSegmentEntry:
759 magic = b'APPS'
760 header = b'\x08\x00\x03\x00\x00\x00\x00\x00\x00\x00'
761
762 def __init__(self, bytes=None):
763 self._blocks = []
764 self._data = bytes
765 if bytes is None:
766 return
767 buf = BytesIO(bytes)
768 unknown = read_short(buf)
769 size = read_short(buf)
770 magic = buf.read(4) # b'APPS'
771 blocks_size = read_short(buf)
772 unknown2 = buf.read(10)
773 # read main blocks
774 while True:
775 block_size = read_int(buf)
776 if not block_size: # last size is zero
777 break
778 block_data = buf.read(block_size - 4) # includes block_size
779 self._blocks.append(UwpMainBlock(block_data))
780
781 def __str__(self):
782 string = '<UwpSegmentEntry>:\n'
783 for block in self._blocks:
784 string += f' {block}\n'
785 return string.strip()
786
787 @property
788 def bytes(self):
789 blocks_bytes = [block.bytes for block in self._blocks]
790 blocks_size = sum([len(block) + 4 for block in blocks_bytes]) + 4 # with terminator
791 size = (
792 2 # size
793 + len(self.magic)
794 + 2 # second size
795 + len(self.header)
796 + blocks_size # blocks with terminator
797 )
798
799 out = BytesIO()
800 write_short(0, out)
801 write_short(size, out)
802 out.write(self.magic)
803 write_short(blocks_size, out)
804 out.write(self.header)
805 for block in blocks_bytes:
806 write_int(len(block) + 4, out)
807 out.write(block)
808 write_int(0, out) # empty block
809 write_short(0, out) # ??
810
811 result = out.getvalue()
812 return result
813
814 @classmethod
815 def create(cls, package_family_name, target, location=None, logo44x44=None):
816 segment = cls()
817
818 blocks = [
819 UwpSubBlock(type=0x11, value=package_family_name),
820 UwpSubBlock(type=0x0e, value=b'\x00\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00'),
821 UwpSubBlock(type=0x05, value=target),
822 ]
823 if location:
824 blocks.append(UwpSubBlock(type=0x0f, value=location)) # need for relative icon path
825 main1 = UwpMainBlock(guid='{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', blocks=blocks)
826 segment._blocks.append(main1)
827
828 if logo44x44:
829 main2 = UwpMainBlock(
830 guid='{86D40B4D-9069-443C-819A-2A54090DCCEC}',
831 blocks=[UwpSubBlock(type=0x02, value=logo44x44)]
832 )
833 segment._blocks.append(main2)
834
835 return segment
836
837
838 class LinkTargetIDList(object):
839
840 def __init__(self, bytes=None):
841 self.items = []
842 if bytes is not None:
843 buf = BytesIO(bytes)
844 raw = []
845 entry_len = read_short(buf)
846 while entry_len > 0:
847 raw.append(buf.read(entry_len - 2)) # the length includes the size
848 entry_len = read_short(buf)
849 self._interpret(raw)
850
851 def _interpret(self, raw):
852 # if len(raw[0]) == 0x12:
853 if raw[0][0] == 0x1F:
854 self.items.append(RootEntry(raw[0]))
855 if self.items[0].root == ROOT_MY_COMPUTER:
856 if not len(raw[1]) == 0x17:
857 raise ValueError("This seems to be an absolute link which requires a drive as second element.")
858 self.items.append(DriveEntry(raw[1]))
859 items = raw[2:]
860 elif self.items[0].root == ROOT_NETWORK_PLACES:
861 raise NotImplementedError(
862 "Parsing network lnks has not yet been implemented. "
863 "If you need it just contact me and we'll see..."
864 )
865 else:
866 items = raw[1:]
867 else:
868 items = raw
869 for item in items:
870 if item[4:8] == b'APPS':
871 self.items.append(UwpSegmentEntry(item))
872 else:
873 self.items.append(PathSegmentEntry(item))
874
875 def get_path(self):
876 segments = []
877 for item in self.items:
878 if type(item) == RootEntry:
879 segments.append('%' + item.root + '%')
880 elif type(item) == DriveEntry:
881 segments.append(item.drive.decode())
882 elif type(item) == PathSegmentEntry:
883 if item.full_name is not None:
884 segments.append(item.full_name)
885 else:
886 segments.append(item)
887 return '\\'.join(segments)
888
889 def _validate(self):
890 if type(self.items[0]) == RootEntry:
891 if self.items[0].root == ROOT_MY_COMPUTER and type(self.items[1]) != DriveEntry:
892 raise ValueError("A drive is required for absolute lnks")
893
894 @property
895 def bytes(self):
896 self._validate()
897 out = BytesIO()
898 for item in self.items:
899 bytes = item.bytes
900 # skip invalid
901 if bytes is None:
902 continue
903 write_short(len(bytes) + 2, out) # len + terminator
904 out.write(bytes)
905 out.write(b'\x00\x00')
906 return out.getvalue()
907
908 def __str__(self):
909 string = '<LinkTargetIDList>:\n'
910 for item in self.items:
911 string += f' {item}\n'
912 return string.strip()
913
914
915 class LinkInfo(object):
916
917 def __init__(self, lnk=None):
918 if lnk is not None:
919 self.start = lnk.tell()
920 self.size = read_int(lnk)
921 self.header_size = read_int(lnk)
922 link_info_flags = read_int(lnk)
923 self.local = link_info_flags & 1
924 self.remote = link_info_flags & 2
925 self.offs_local_volume_table = read_int(lnk)
926 self.offs_local_base_path = read_int(lnk)
927 self.offs_network_volume_table = read_int(lnk)
928 self.offs_base_name = read_int(lnk)
929 if self.header_size >= _LINK_INFO_HEADER_OPTIONAL:
930 print("TODO: read the unicode stuff") # TODO: read the unicode stuff
931 self._parse_path_elements(lnk)
932 else:
933 self.size = None
934 self.header_size = _LINK_INFO_HEADER_DEFAULT
935 self.remote = None
936 self.offs_local_volume_table = 0
937 self.offs_local_base_path = 0
938 self.offs_network_volume_table = 0
939 self.offs_base_name = 0
940 self.drive_type = None
941 self.drive_serial = None
942 self.volume_label = None
943 self.local_base_path = None
944 self.network_share_name = None
945 self.base_name = None
946 self._path = None
947
948 def _parse_path_elements(self, lnk):
949 if self.remote:
950 # 20 is the offset of the network share name
951 lnk.seek(self.start + self.offs_network_volume_table + 20)
952 self.network_share_name = read_cstring(lnk)
953 lnk.seek(self.start + self.offs_base_name)
954 self.base_name = read_cstring(lnk)
955 if self.local:
956 lnk.seek(self.start + self.offs_local_volume_table + 4)
957 self.drive_type = _DRIVE_TYPES.get(read_int(lnk))
958 self.drive_serial = read_int(lnk)
959 lnk.read(4) # volume name offset (10h)
960 self.volume_label = read_cstring(lnk)
961 lnk.seek(self.start + self.offs_local_base_path)
962 self.local_base_path = read_cstring(lnk)
963 # TODO: unicode
964 self.make_path()
965
966 def make_path(self):
967 if self.remote:
968 self._path = self.network_share_name + self.base_name
969 if self.local:
970 self._path = self.local_base_path
971
972 def write(self, lnk):
973 if self.remote is None:
974 raise MissingInformationException("No location information given.")
975 self.start = lnk.tell()
976 self._calculate_sizes_and_offsets()
977 write_int(self.size, lnk)
978 write_int(self.header_size, lnk)
979 write_int((self.local and 1) + (self.remote and 2), lnk)
980 write_int(self.offs_local_volume_table, lnk)
981 write_int(self.offs_local_base_path, lnk)
982 write_int(self.offs_network_volume_table, lnk)
983 write_int(self.offs_base_name, lnk)
984 if self.remote:
985 self._write_network_volume_table(lnk)
986 write_cstring(self.base_name, lnk)
987 else:
988 self._write_local_volume_table(lnk)
989 write_cstring(self.local_base_path, lnk, padding=True)
990 write_byte(0, lnk)
991
992 def _calculate_sizes_and_offsets(self):
993 self.size_base_name = 1 # len(self.base_name) + 1 # zero terminated strings
994 self.size = 28 + self.size_base_name
995 if self.remote:
996 self.size_network_volume_table = 20 + len(self.network_share_name) + 1
997 self.size += self.size_network_volume_table
998 self.offs_local_volume_table = 0
999 self.offs_local_base_path = 0
1000 self.offs_network_volume_table = 28
1001 self.offs_base_name = self.offs_network_volume_table + self.size_network_volume_table
1002 else:
1003 self.size_local_volume_table = 16 + len(self.volume_label) + 1
1004 self.size_local_base_path = len(self.local_base_path) + 1
1005 self.size += self.size_local_volume_table + self.size_local_base_path
1006 self.offs_local_volume_table = 28
1007 self.offs_local_base_path = self.offs_local_volume_table + self.size_local_volume_table
1008 self.offs_network_volume_table = 0
1009 self.offs_base_name = self.offs_local_base_path + self.size_local_base_path
1010
1011 def _write_network_volume_table(self, buf):
1012 write_int(self.size_network_volume_table, buf)
1013 write_int(2, buf) # ?
1014 write_int(20, buf) # size of Network Volume Table
1015 write_int(0, buf) # ?
1016 write_int(131072, buf) # ?
1017 write_cstring(self.network_share_name, buf)
1018
1019 def _write_local_volume_table(self, buf):
1020 write_int(self.size_local_volume_table, buf)
1021 try:
1022 drive_type = _DRIVE_TYPE_IDS[self.drive_type]
1023 except KeyError:
1024 raise ValueError("This is not a valid drive type: %s" % self.drive_type)
1025 write_int(drive_type, buf)
1026 write_int(self.drive_serial, buf)
1027 write_int(16, buf) # volume name offset
1028 write_cstring(self.volume_label, buf)
1029
1030 @property
1031 def path(self):
1032 return self._path
1033
1034 def __str__(self):
1035 s = "File Location Info:"
1036 if self._path is None:
1037 return s + " <not specified>"
1038 if self.remote:
1039 s += "\n (remote)"
1040 s += "\n Network Share: %s" % self.network_share_name
1041 s += "\n Base Name: %s" % self.base_name
1042 else:
1043 s += "\n (local)"
1044 s += "\n Volume Type: %s" % self.drive_type
1045 s += "\n Volume Serial Number: %s" % self.drive_serial
1046 s += "\n Volume Label: %s" % self.volume_label
1047 s += "\n Path: %s" % self.local_base_path
1048 return s
1049
1050
1051 EXTRA_DATA_TYPES = {
1052 0xA0000002: 'ConsoleDataBlock', # size 0x000000CC
1053 0xA0000004: 'ConsoleFEDataBlock', # size 0x0000000C
1054 0xA0000006: 'DarwinDataBlock', # size 0x00000314
1055 0xA0000001: 'EnvironmentVariableDataBlock', # size 0x00000314
1056 0xA0000007: 'IconEnvironmentDataBlock', # size 0x00000314
1057 0xA000000B: 'KnownFolderDataBlock', # size 0x0000001C
1058 0xA0000009: 'PropertyStoreDataBlock', # size >= 0x0000000C
1059 0xA0000008: 'ShimDataBlock', # size >= 0x00000088
1060 0xA0000005: 'SpecialFolderDataBlock', # size 0x00000010
1061 0xA0000003: 'VistaAndAboveIDListDataBlock', # size 0x00000060
1062 }
1063
1064
1065 class ExtraData_Unparsed(object):
1066 def __init__(self, bytes=None, signature=None, data=None):
1067 self._signature = signature
1068 self._size = None
1069 self.data = data
1070 # if data:
1071 # self._size = len(data)
1072 if bytes:
1073 # self._size = len(bytes)
1074 self.data = bytes
1075 # self.read(bytes)
1076
1077 # def read(self, bytes):
1078 # buf = BytesIO(bytes)
1079 # size = len(bytes)
1080 # # self._size = read_int(buf)
1081 # # self._signature = read_int(buf)
1082 # self.data = buf.read(self._size - 8)
1083
1084 def bytes(self):
1085 buf = BytesIO()
1086 write_int(len(self.data)+8, buf)
1087 write_int(self._signature, buf)
1088 buf.write(self.data)
1089 return buf.getvalue()
1090
1091 def __str__(self):
1092 s = 'ExtraDataBlock\n signature %s\n data: %s' % (hex(self._signature), self.data)
1093 return s
1094
1095
1096 def padding(val, size, byte=b'\x00'):
1097 return val + (size-len(val)) * byte
1098
1099
1100 class ExtraData_IconEnvironmentDataBlock(object):
1101 def __init__(self, bytes=None):
1102 # self._size = None
1103 # self._signature = None
1104 self._signature = 0xA0000007
1105 self.target_ansi = None
1106 self.target_unicode = None
1107 if bytes:
1108 self.read(bytes)
1109
1110 def read(self, bytes):
1111 buf = BytesIO(bytes)
1112 # self._size = read_int(buf)
1113 # self._signature = read_int(buf)
1114 self.target_ansi = buf.read(260).decode()
1115 self.target_unicode = buf.read(520).decode('utf-16-le')
1116
1117 def bytes(self):
1118 target_ansi = padding(self.target_ansi.encode(), 260)
1119 target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520)
1120 size = 8 + len(target_ansi) + len(target_unicode)
1121 assert self._signature == 0xA0000007
1122 assert size == 0x00000314
1123 buf = BytesIO()
1124 write_int(size, buf)
1125 write_int(self._signature, buf)
1126 buf.write(target_ansi)
1127 buf.write(target_unicode)
1128 return buf.getvalue()
1129
1130 def __str__(self):
1131 target_ansi = self.target_ansi.replace('\x00', '')
1132 target_unicode = self.target_unicode.replace('\x00', '')
1133 s = f'IconEnvironmentDataBlock\n TargetAnsi: {target_ansi}\n TargetUnicode: {target_unicode}'
1134 return s
1135
1136
1137 def guid_to_str(guid):
1138 ordered = [guid[3], guid[2], guid[1], guid[0], guid[5], guid[4],
1139 guid[7], guid[6], guid[8], guid[9], guid[10], guid[11],
1140 guid[12], guid[13], guid[14], guid[15]]
1141 res = "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered])
1142 # print(guid, res)
1143 return res
1144
1145
1146 class TypedPropertyValue(object):
1147 # types: [MS-OLEPS] section 2.15
1148 def __init__(self, bytes=None, type=None, value=None):
1149 self.type = type
1150 self.value = value
1151 if bytes:
1152 self.type = read_short(BytesIO(bytes))
1153 padding = bytes[2:4]
1154 self.value = bytes[4:]
1155
1156 def set_string(self, value):
1157 self.type = 0x1f
1158 buf = BytesIO()
1159 write_int(len(value)+2, buf)
1160 buf.write(value.encode('utf-16-le'))
1161 # terminator (included in size)
1162 buf.write(b'\x00\x00\x00\x00')
1163 # padding (not included in size)
1164 if len(value) % 2:
1165 buf.write(b'\x00\x00')
1166 self.value = buf.getvalue()
1167
1168 @property
1169 def bytes(self):
1170 buf = BytesIO()
1171 write_short(self.type, buf)
1172 write_short(0x0000, buf)
1173 buf.write(self.value)
1174 return buf.getvalue()
1175
1176 def __str__(self):
1177 value = self.value
1178 if self.type == 0x1F:
1179 size = value[:4]
1180 value = value[4:].decode('utf-16-le')
1181 if self.type == 0x15:
1182 value = unpack('<Q', value)[0]
1183 if self.type == 0x13:
1184 value = unpack('<I', value)[0]
1185 if self.type == 0x14:
1186 value = unpack('<q', value)[0]
1187 if self.type == 0x16:
1188 value = unpack('<i', value)[0]
1189 if self.type == 0x17:
1190 value = unpack('<I', value)[0]
1191 if self.type == 0x48:
1192 value = guid_to_str(value)
1193 if self.type == 0x40:
1194 # FILETIME (Packet Version)
1195 stream = BytesIO(value)
1196 low = read_int(stream)
1197 high = read_int(stream)
1198 num = (high << 32) + low
1199 value = convert_time_to_unix(num)
1200 return '%s: %s' % (hex(self.type), value)
1201
1202
1203 class PropertyStore:
1204 def __init__(self, bytes=None, properties=None, format_id=None, is_strings=False):
1205 self.is_strings = is_strings
1206 self.properties = []
1207 self.format_id = format_id
1208 self._is_end = False
1209 if properties:
1210 self.properties = properties
1211 if bytes:
1212 self.read(bytes)
1213
1214 def read(self, bytes_io):
1215 buf = bytes_io
1216 size = read_int(buf)
1217 assert size < len(buf.getvalue())
1218 if size == 0x00000000:
1219 self._is_end = True
1220 return
1221 version = read_int(buf)
1222 assert version == 0x53505331
1223 self.format_id = buf.read(16)
1224 if self.format_id == b'\xD5\xCD\xD5\x05\x2E\x9C\x10\x1B\x93\x97\x08\x00\x2B\x2C\xF9\xAE':
1225 self.is_strings = True
1226 else:
1227 self.is_strings = False
1228 while True:
1229 # assert lnk.tell() < (start + size)
1230 value_size = read_int(buf)
1231 if value_size == 0x00000000:
1232 break
1233 if self.is_strings:
1234 name_size = read_int(buf)
1235 reserved = read_byte(buf)
1236 name = buf.read(name_size).decode('utf-16-le')
1237 value = TypedPropertyValue(buf.read(value_size-9))
1238 self.properties.append((name, value))
1239 else:
1240 value_id = read_int(buf)
1241 reserved = read_byte(buf)
1242 value = TypedPropertyValue(buf.read(value_size-9))
1243 self.properties.append((value_id, value))
1244
1245 @property
1246 def bytes(self):
1247 size = 8 + len(self.format_id)
1248 properties = BytesIO()
1249 for name, value in self.properties:
1250 value_bytes = value.bytes
1251 if self.is_strings:
1252 name_bytes = name.encode('utf-16-le')
1253 value_size = 9 + len(name_bytes) + len(value_bytes)
1254 write_int(value_size, properties)
1255 name_size = len(name_bytes)
1256 write_int(name_size, properties)
1257 properties.write(b'\x00')
1258 properties.write(name_bytes)
1259 else:
1260 value_size = 9 + len(value_bytes)
1261 write_int(value_size, properties)
1262 write_int(name, properties)
1263 properties.write(b'\x00')
1264 properties.write(value_bytes)
1265 size += value_size
1266
1267 write_int(0x00000000, properties)
1268 size += 4
1269
1270 buf = BytesIO()
1271 write_int(size, buf)
1272 write_int(0x53505331, buf)
1273 buf.write(self.format_id)
1274 buf.write(properties.getvalue())
1275
1276 return buf.getvalue()
1277
1278 def __str__(self):
1279 s = ' PropertyStore'
1280 s += '\n FormatID: %s' % guid_to_str(self.format_id)
1281 for name, value in self.properties:
1282 s += '\n %3s = %s' % (name, str(value))
1283 return s.strip()
1284
1285
1286 class ExtraData_PropertyStoreDataBlock(object):
1287 def __init__(self, bytes=None, stores=None):
1288 self._size = None
1289 self._signature = 0xA0000009
1290 self.stores = []
1291 if stores:
1292 self.stores = stores
1293 if bytes:
1294 self.read(bytes)
1295
1296 def read(self, bytes):
1297 buf = BytesIO(bytes)
1298 # self._size = read_int(buf)
1299 # self._signature = read_int(buf)
1300 # [MS-PROPSTORE] section 2.2
1301 while True:
1302 prop_store = PropertyStore(buf)
1303 if prop_store._is_end:
1304 break
1305 self.stores.append(prop_store)
1306
1307 def bytes(self):
1308 stores = b''
1309 for prop_store in self.stores:
1310 stores += prop_store.bytes
1311 size = len(stores) + 8 + 4
1312
1313 assert self._signature == 0xA0000009
1314 assert size >= 0x0000000C
1315
1316 buf = BytesIO()
1317 write_int(size, buf)
1318 write_int(self._signature, buf)
1319 buf.write(stores)
1320 write_int(0x00000000, buf)
1321 return buf.getvalue()
1322
1323 def __str__(self):
1324 s = 'PropertyStoreDataBlock'
1325 for prop_store in self.stores:
1326 s += '\n %s' % str(prop_store)
1327 return s
1328
1329
1330 class ExtraData_EnvironmentVariableDataBlock(object):
1331 def __init__(self, bytes=None):
1332 self._signature = 0xA0000001
1333 self.target_ansi = None
1334 self.target_unicode = None
1335 if bytes:
1336 self.read(bytes)
1337
1338 def read(self, bytes):
1339 buf = BytesIO(bytes)
1340 self.target_ansi = buf.read(260).decode()
1341 self.target_unicode = buf.read(520).decode('utf-16-le')
1342
1343 def bytes(self):
1344 target_ansi = padding(self.target_ansi.encode(), 260)
1345 target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520)
1346 size = 8 + len(target_ansi) + len(target_unicode)
1347 assert self._signature == 0xA0000001
1348 assert size == 0x00000314
1349 buf = BytesIO()
1350 write_int(size, buf)
1351 write_int(self._signature, buf)
1352 buf.write(target_ansi)
1353 buf.write(target_unicode)
1354 return buf.getvalue()
1355
1356 def __str__(self):
1357 target_ansi = self.target_ansi.replace('\x00', '')
1358 target_unicode = self.target_unicode.replace('\x00', '')
1359 s = f'EnvironmentVariableDataBlock\n TargetAnsi: {target_ansi}\n TargetUnicode: {target_unicode}'
1360 return s
1361
1362
1363 EXTRA_DATA_TYPES_CLASSES = {
1364 'IconEnvironmentDataBlock': ExtraData_IconEnvironmentDataBlock,
1365 'PropertyStoreDataBlock': ExtraData_PropertyStoreDataBlock,
1366 'EnvironmentVariableDataBlock': ExtraData_EnvironmentVariableDataBlock,
1367 }
1368
1369
1370 class ExtraData(object):
1371 # EXTRA_DATA = *EXTRA_DATA_BLOCK TERMINAL_BLOCK
1372 def __init__(self, lnk=None, blocks=None):
1373 self.blocks = []
1374 if blocks:
1375 self.blocks = blocks
1376 if lnk is None:
1377 return
1378 while True:
1379 size = read_int(lnk)
1380 if size < 4: # TerminalBlock
1381 break
1382 signature = read_int(lnk)
1383 bytes = lnk.read(size-8)
1384 # lnk.seek(-8, 1)
1385 block_type = EXTRA_DATA_TYPES[signature]
1386 if block_type in EXTRA_DATA_TYPES_CLASSES:
1387 block_class = EXTRA_DATA_TYPES_CLASSES[block_type]
1388 block = block_class(bytes=bytes)
1389 else:
1390 block_class = ExtraData_Unparsed
1391 block = block_class(bytes=bytes, signature=signature)
1392 self.blocks.append(block)
1393
1394 @property
1395 def bytes(self):
1396 result = b''
1397 for block in self.blocks:
1398 result += block.bytes()
1399 result += b'\x00\x00\x00\x00' # TerminalBlock
1400 return result
1401
1402 def __str__(self):
1403 s = ''
1404 for block in self.blocks:
1405 s += '\n' + str(block)
1406 return s
1407
1408
1409 class Lnk(object):
1410
1411 def __init__(self, f=None):
1412 self.file = None
1413 if type(f) == str or type(f) == str:
1414 self.file = f
1415 try:
1416 f = open(self.file, 'rb')
1417 except IOError:
1418 self.file += ".lnk"
1419 f = open(self.file, 'rb')
1420 # defaults
1421 self.link_flags = Flags(_LINK_FLAGS)
1422 self.file_flags = Flags(_FILE_ATTRIBUTES_FLAGS)
1423 self.creation_time = datetime.now()
1424 self.access_time = datetime.now()
1425 self.modification_time = datetime.now()
1426 self.file_size = 0
1427 self.icon_index = 0
1428 self._show_command = WINDOW_NORMAL
1429 self.hot_key = None
1430 self._link_info = LinkInfo()
1431 self.description = None
1432 self.relative_path = None
1433 self.work_dir = None
1434 self.arguments = None
1435 self.icon = None
1436 self.extra_data = None
1437 if f is not None:
1438 assert_lnk_signature(f)
1439 self._parse_lnk_file(f)
1440 if self.file:
1441 f.close()
1442
1443 def _read_hot_key(self, lnk):
1444 low = read_byte(lnk)
1445 high = read_byte(lnk)
1446 key = _KEYS.get(low, '')
1447 modifier = high and str(ModifierKeys(high)) or ''
1448 return modifier + key
1449
1450 def _write_hot_key(self, hot_key, lnk):
1451 if hot_key is None or not hot_key:
1452 low = high = 0
1453 else:
1454 hot_key = hot_key.split('+')
1455 try:
1456 low = _KEY_CODES[hot_key[-1]]
1457 except KeyError:
1458 raise InvalidKeyException("Cannot find key code for %s" % hot_key[1])
1459 modifiers = ModifierKeys()
1460 for modifier in hot_key[:-1]:
1461 modifiers[modifier.upper()] = True
1462 high = modifiers.bytes
1463 write_byte(low, lnk)
1464 write_byte(high, lnk)
1465
1466 def _parse_lnk_file(self, lnk):
1467 # SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO] [STRING_DATA] *EXTRA_DATA
1468
1469 # SHELL_LINK_HEADER
1470 lnk.seek(20) # after signature and guid
1471 self.link_flags.set_flags(read_int(lnk))
1472 self.file_flags.set_flags(read_int(lnk))
1473 self.creation_time = convert_time_to_unix(read_double(lnk))
1474 self.access_time = convert_time_to_unix(read_double(lnk))
1475 self.modification_time = convert_time_to_unix(read_double(lnk))
1476 self.file_size = read_int(lnk)
1477 self.icon_index = read_int(lnk)
1478 show_command = read_int(lnk)
1479 self._show_command = _SHOW_COMMANDS[show_command] if show_command in _SHOW_COMMANDS else _SHOW_COMMANDS[1]
1480 self.hot_key = self._read_hot_key(lnk)
1481 lnk.read(10) # reserved (0)
1482
1483 # LINKTARGET_IDLIST (HasLinkTargetIDList)
1484 if self.link_flags.HasLinkTargetIDList:
1485 shell_item_id_list_size = read_short(lnk)
1486 self.shell_item_id_list = LinkTargetIDList(lnk.read(shell_item_id_list_size))
1487
1488 # LINKINFO (HasLinkInfo)
1489 if self.link_flags.HasLinkInfo and not self.link_flags.ForceNoLinkInfo:
1490 self._link_info = LinkInfo(lnk)
1491 lnk.seek(self._link_info.start + self._link_info.size)
1492
1493 # STRING_DATA = [NAME_STRING] [RELATIVE_PATH] [WORKING_DIR] [COMMAND_LINE_ARGUMENTS] [ICON_LOCATION]
1494 if self.link_flags.HasName:
1495 self.description = read_sized_string(lnk, self.link_flags.IsUnicode)
1496 if self.link_flags.HasRelativePath:
1497 self.relative_path = read_sized_string(lnk, self.link_flags.IsUnicode)
1498 if self.link_flags.HasWorkingDir:
1499 self.work_dir = read_sized_string(lnk, self.link_flags.IsUnicode)
1500 if self.link_flags.HasArguments:
1501 self.arguments = read_sized_string(lnk, self.link_flags.IsUnicode)
1502 if self.link_flags.HasIconLocation:
1503 self.icon = read_sized_string(lnk, self.link_flags.IsUnicode)
1504
1505 # *EXTRA_DATA
1506 self.extra_data = ExtraData(lnk)
1507
1508 def save(self, f: Optional[Union[str, IOBase]] = None, force_ext=False):
1509 if f is None:
1510 f = self.file
1511 if f is None:
1512 raise ValueError("File (name) missing for saving the lnk")
1513 is_file = hasattr(f, 'write')
1514 if not is_file:
1515 if not type(f) == str and not type(f) == str:
1516 raise ValueError("Need a writeable object or a file name to save to, got %s" % f)
1517 if force_ext:
1518 if not f.lower().endswith('.lnk'):
1519 f += '.lnk'
1520 f = open(f, 'wb')
1521 self.write(f)
1522 # only close the stream if it's our own
1523 if not is_file:
1524 f.close()
1525
1526 def write(self, lnk):
1527 lnk.write(_SIGNATURE)
1528 lnk.write(_GUID)
1529 write_int(self.link_flags.bytes, lnk)
1530 write_int(self.file_flags.bytes, lnk)
1531 write_double(convert_time_to_windows(self.creation_time), lnk)
1532 write_double(convert_time_to_windows(self.access_time), lnk)
1533 write_double(convert_time_to_windows(self.modification_time), lnk)
1534 write_int(self.file_size, lnk)
1535 write_int(self.icon_index, lnk)
1536 write_int(_SHOW_COMMAND_IDS[self._show_command], lnk)
1537 self._write_hot_key(self.hot_key, lnk)
1538 lnk.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') # reserved
1539 if self.link_flags.HasLinkTargetIDList:
1540 shell_item_id_list = self.shell_item_id_list.bytes
1541 write_short(len(shell_item_id_list), lnk)
1542 lnk.write(shell_item_id_list)
1543 if self.link_flags.HasLinkInfo:
1544 self._link_info.write(lnk)
1545 if self.link_flags.HasName:
1546 write_sized_string(self.description, lnk, self.link_flags.IsUnicode)
1547 if self.link_flags.HasRelativePath:
1548 write_sized_string(self.relative_path, lnk, self.link_flags.IsUnicode)
1549 if self.link_flags.HasWorkingDir:
1550 write_sized_string(self.work_dir, lnk, self.link_flags.IsUnicode)
1551 if self.link_flags.HasArguments:
1552 write_sized_string(self.arguments, lnk, self.link_flags.IsUnicode)
1553 if self.link_flags.HasIconLocation:
1554 write_sized_string(self.icon, lnk, self.link_flags.IsUnicode)
1555 if self.extra_data:
1556 lnk.write(self.extra_data.bytes)
1557 else:
1558 lnk.write(b'\x00\x00\x00\x00')
1559
1560 def _get_shell_item_id_list(self):
1561 return self._shell_item_id_list
1562
1563 def _set_shell_item_id_list(self, shell_item_id_list):
1564 self._shell_item_id_list = shell_item_id_list
1565 self.link_flags.HasLinkTargetIDList = shell_item_id_list is not None
1566 shell_item_id_list = property(_get_shell_item_id_list, _set_shell_item_id_list)
1567
1568 def _get_link_info(self):
1569 return self._link_info
1570
1571 def _set_link_info(self, link_info):
1572 self._link_info = link_info
1573 self.link_flags.ForceNoLinkInfo = link_info is None
1574 self.link_flags.HasLinkInfo = link_info is not None
1575 link_info = property(_get_link_info, _set_link_info)
1576
1577 def _get_description(self):
1578 return self._description
1579
1580 def _set_description(self, description):
1581 self._description = description
1582 self.link_flags.HasName = description is not None
1583 description = property(_get_description, _set_description)
1584
1585 def _get_relative_path(self):
1586 return self._relative_path
1587
1588 def _set_relative_path(self, relative_path):
1589 self._relative_path = relative_path
1590 self.link_flags.HasRelativePath = relative_path is not None
1591 relative_path = property(_get_relative_path, _set_relative_path)
1592
1593 def _get_work_dir(self):
1594 return self._work_dir
1595
1596 def _set_work_dir(self, work_dir):
1597 self._work_dir = work_dir
1598 self.link_flags.HasWorkingDir = work_dir is not None
1599 work_dir = working_dir = property(_get_work_dir, _set_work_dir)
1600
1601 def _get_arguments(self):
1602 return self._arguments
1603
1604 def _set_arguments(self, arguments):
1605 self._arguments = arguments
1606 self.link_flags.HasArguments = arguments is not None
1607 arguments = property(_get_arguments, _set_arguments)
1608
1609 def _get_icon(self):
1610 return self._icon
1611
1612 def _set_icon(self, icon):
1613 self._icon = icon
1614 self.link_flags.HasIconLocation = icon is not None
1615 icon = property(_get_icon, _set_icon)
1616
1617 def _get_window_mode(self):
1618 return self._show_command
1619
1620 def _set_window_mode(self, value):
1621 if value not in list(_SHOW_COMMANDS.values()):
1622 raise ValueError("Not a valid window mode: %s. Choose any of pylnk.WINDOW_*" % value)
1623 self._show_command = value
1624 window_mode = show_command = property(_get_window_mode, _set_window_mode)
1625
1626 @property
1627 def path(self):
1628 return self._shell_item_id_list.get_path()
1629
1630 def specify_local_location(self, path, drive_type=None, drive_serial=None, volume_label=None):
1631 self._link_info.drive_type = drive_type or DRIVE_UNKNOWN
1632 self._link_info.drive_serial = drive_serial or ''
1633 self._link_info.volume_label = volume_label or ''
1634 self._link_info.local_base_path = path
1635 self._link_info.local = True
1636 self._link_info.make_path()
1637
1638 def specify_remote_location(self, network_share_name, base_name):
1639 self._link_info.network_share_name = network_share_name
1640 self._link_info.base_name = base_name
1641 self._link_info.remote = True
1642 self._link_info.make_path()
1643
1644 def __str__(self):
1645 s = "Target file:\n"
1646 s += str(self.file_flags)
1647 s += "\nCreation Time: %s" % self.creation_time
1648 s += "\nModification Time: %s" % self.modification_time
1649 s += "\nAccess Time: %s" % self.access_time
1650 s += "\nFile size: %s" % self.file_size
1651 s += "\nWindow mode: %s" % self._show_command
1652 s += "\nHotkey: %s\n" % self.hot_key
1653 s += str(self._link_info)
1654 if self.link_flags.HasLinkTargetIDList:
1655 s += "\n%s" % self.shell_item_id_list
1656 if self.link_flags.HasName:
1657 s += "\nDescription: %s" % self.description
1658 if self.link_flags.HasRelativePath:
1659 s += "\nRelative Path: %s" % self.relative_path
1660 if self.link_flags.HasWorkingDir:
1661 s += "\nWorking Directory: %s" % self.work_dir
1662 if self.link_flags.HasArguments:
1663 s += "\nCommandline Arguments: %s" % self.arguments
1664 if self.link_flags.HasIconLocation:
1665 s += "\nIcon: %s" % self.icon
1666 if self.link_flags.HasLinkInfo:
1667 s += "\nUsed Path: %s" % self.shell_item_id_list.get_path()
1668 if self.extra_data:
1669 s += str(self.extra_data)
1670 return s
1671
1672
1673 # ---- convenience functions
1674
1675 def parse(lnk):
1676 return Lnk(lnk)
1677
1678
1679 def create(f=None):
1680 lnk = Lnk()
1681 lnk.file = f
1682 return lnk
1683
1684
1685 def for_file(target_file, lnk_name=None, arguments=None, description=None, icon_file=None, icon_index=0, work_dir=None):
1686 lnk = create(lnk_name)
1687 lnk.link_flags._flags['IsUnicode'] = True
1688 lnk.link_info = None
1689 levels = list(path_levels(target_file))
1690 elements = [RootEntry(ROOT_MY_COMPUTER),
1691 DriveEntry(levels[0])]
1692 for level in levels[1:]:
1693 segment = PathSegmentEntry.create_for_path(level)
1694 elements.append(segment)
1695 lnk.shell_item_id_list = LinkTargetIDList()
1696 lnk.shell_item_id_list.items = elements
1697 # lnk.link_flags._flags['HasLinkInfo'] = True
1698 if arguments:
1699 lnk.link_flags._flags['HasArguments'] = True
1700 lnk.arguments = arguments
1701 if description:
1702 lnk.link_flags._flags['HasName'] = True
1703 lnk.description = description
1704 if icon_file:
1705 lnk.link_flags._flags['HasIconLocation'] = True
1706 lnk.icon = icon_file
1707 lnk.icon_index = icon_index
1708 if work_dir:
1709 lnk.link_flags._flags['HasWorkingDir'] = True
1710 lnk.work_dir = work_dir
1711 if lnk_name:
1712 lnk.save()
1713 return lnk
1714
1715
1716 def from_segment_list(data, lnk_name=None):
1717 """
1718 Creates a lnk file from a list of path segments.
1719 If lnk_name is given, the resulting lnk will be saved
1720 to a file with that name.
1721 The expected list for has the following format ("C:\\dir\\file.txt"):
1722
1723 ['c:\\',
1724 {'type': TYPE_FOLDER,
1725 'size': 0, # optional for folders
1726 'name': "dir",
1727 'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
1728 'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
1729 'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476)
1730 },
1731 {'type': TYPE_FILE,
1732 'size': 823,
1733 'name': "file.txt",
1734 'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
1735 'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
1736 'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476)
1737 }
1738 ]
1739
1740 For relative paths just omit the drive entry.
1741 Hint: Correct dates really are not crucial for working lnks.
1742 """
1743 if type(data) not in (list, tuple):
1744 raise ValueError("Invalid data format, list or tuple expected")
1745 lnk = Lnk()
1746 entries = []
1747 if is_drive(data[0]):
1748 # this is an absolute link
1749 entries.append(RootEntry(ROOT_MY_COMPUTER))
1750 if not data[0].endswith('\\'):
1751 data[0] += "\\"
1752 drive = data.pop(0).encode("ascii")
1753 entries.append(DriveEntry(drive))
1754 for level in data:
1755 segment = PathSegmentEntry()
1756 segment.type = level['type']
1757 if level['type'] == TYPE_FOLDER:
1758 segment.file_size = 0
1759 else:
1760 segment.file_size = level['size']
1761 segment.short_name = level['name']
1762 segment.full_name = level['name']
1763 segment.created = level['created']
1764 segment.modified = level['modified']
1765 segment.accessed = level['accessed']
1766 entries.append(segment)
1767 lnk.shell_item_id_list = LinkTargetIDList()
1768 lnk.shell_item_id_list.items = entries
1769 if data[-1]['type'] == TYPE_FOLDER:
1770 lnk.file_flags.directory = True
1771 if lnk_name:
1772 lnk.save(lnk_name)
1773 return lnk
1774
1775
1776 def build_uwp(
1777 package_family_name, target, location=None,logo44x44=None, lnk_name=None,
1778 ) -> Lnk:
1779 """
1780 :param lnk_name: ex.: crafted_uwp.lnk
1781 :param package_family_name: ex.: Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe
1782 :param target: ex.: Microsoft.WindowsCalculator_8wekyb3d8bbwe!App
1783 :param location: ex.: C:\\Program Files\\WindowsApps\\Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe
1784 :param logo44x44: ex.: Assets\\CalculatorAppList.png
1785 """
1786 lnk = Lnk()
1787 lnk.link_flags._flags['HasLinkTargetIDList'] = True
1788 lnk.link_flags._flags['IsUnicode'] = True
1789 lnk.link_flags._flags['EnableTargetMetadata'] = True
1790
1791 lnk.shell_item_id_list = LinkTargetIDList()
1792
1793 elements = [
1794 RootEntry(ROOT_UWP_APPS),
1795 UwpSegmentEntry.create(
1796 package_family_name=package_family_name,
1797 target=target,
1798 location=location,
1799 logo44x44=logo44x44,
1800 )
1801 ]
1802 lnk.shell_item_id_list.items = elements
1803
1804 if lnk_name:
1805 lnk.file = lnk_name
1806 lnk.save()
1807 return lnk
1808
1809
1810 def get_prop(obj, prop_queue):
1811 attr = getattr(obj, prop_queue[0])
1812 if len(prop_queue) > 1:
1813 return get_prop(attr, prop_queue[1:])
1814 return attr
1815
1816
1817 def usage_and_exit():
1818 usage = """usage: pylnk.py c[reate] TARGETFILE LNKFILE
1819 pylnk.py p[arse] LNKFILE [PROPERTY[, PROPERTY[, ...]]]"""
1820 print(usage)
1821 sys.exit(1)
1822
1823
1824 if __name__ == "__main__":
1825 # lnk = parse('temp_1.lnk.q')
1826 # print(lnk)
1827 # lnk.save('result.lnk.1')
1828 # lnk = parse('result.lnk.1')
1829 # print(lnk)
1830 # exit(0)
1831
1832 if len(sys.argv) == 1:
1833 usage_and_exit()
1834 if sys.argv[1] in ['h', '-h', '--help']:
1835 usage_and_exit()
1836 action = sys.argv[1]
1837 if action not in ['c', 'create', 'p', 'parse', 'd']:
1838 print("unknown action: " + action)
1839 usage_and_exit()
1840 if action.startswith('c'):
1841 if len(sys.argv) < 4:
1842 usage_and_exit()
1843 for_file(sys.argv[2], sys.argv[3])
1844 elif action.startswith('p'):
1845 if len(sys.argv) < 3:
1846 usage_and_exit()
1847 lnk = parse(sys.argv[2])
1848 props = sys.argv[3:]
1849 if len(props) == 0:
1850 print(lnk)
1851 else:
1852 for prop in props:
1853 print(get_prop(lnk, prop.split('.')))
1854 elif action.startswith('d'):
1855 if len(sys.argv) < 3:
1856 usage_and_exit()
1857 lnk = parse(sys.argv[2])
1858 new_filename = sys.argv[3]
1859 print(lnk)
1860 lnk.save(new_filename)
1861 print('saved')
0 Metadata-Version: 2.1
1 Name: pylnk3
2 Version: 0.4.2
3 Summary: Windows LNK File Parser and Creator
4 Home-page: https://github.com/strayge/pylnk
5 Author: strayge
6 Author-email: [email protected]
7 License: GNU Library or Lesser General Public License (LGPL)
8 Description: # PyLnk 3
9
10 [![PyPI version shields.io](https://img.shields.io/pypi/v/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
11 [![PyPI pyversions](https://img.shields.io/pypi/pyversions/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
12 [![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
13
14 Python library for reading and writing Windows shortcut files (.lnk).
15 Converted to support python 3.
16
17 This library can parse .lnk files and extract all relevant information from
18 them. Parsing a .lnk file yields a LNK object which can be altered and saved
19 again. Moreover, .lnk file can be created from scratch be creating a LNK
20 object, populating it with data and then saving it to a file. As that
21 process requires some knowledge about the internals of .lnk files, some
22 convenience functions are provided.
23
24 Limitation: Windows knows lots of different types of shortcuts which all have
25 different formats. This library currently only supports shortcuts to files and
26 folders on the local machine.
27
28 ## CLI
29
30 Mainly tool has two basic commands.
31
32 #### Parse existed lnk file
33
34 ```sh
35 pylnk3 parse [-h] filename [props [props ...]]
36
37 positional arguments:
38 filename lnk filename to read
39 props props path to read
40
41 optional arguments:
42 -h, --help show this help message and exit
43 ```
44
45 #### Create new lnk file
46
47 ```sh
48 usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]]
49 [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]]
50 target name
51
52 positional arguments:
53 target target path
54 name lnk filename to create
55
56 optional arguments:
57 -h, --help show this help message and exit
58 --arguments [ARGUMENTS], -a [ARGUMENTS]
59 additional arguments
60 --description [DESCRIPTION], -d [DESCRIPTION]
61 description
62 --icon [ICON], -i [ICON]
63 icon filename
64 --icon-index [ICON_INDEX], -ii [ICON_INDEX]
65 icon index
66 --workdir [WORKDIR], -w [WORKDIR]
67 working directory
68 --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}]
69 window mode
70 ```
71
72 #### Examples
73 ```sh
74 pylnk3 p filename.lnk
75 pylnk3 c c:\prog.exe shortcut.lnk
76 pylnk3 c \\192.168.1.1\share\file.doc doc.lnk
77 pylnk3 create c:\1.txt text.lnk -m Minimized -d "Description"
78 ```
79
80 ## Changes
81
82 **0.4.2**
83 changed logic for Lnk.path choose (in case of different paths presents at different structures)
84 read links with root as GUID of KNOWN_FOLDER
85 [FIX] disabled padding for writing LinkInfo.local_base_path
86
87 **0.4.0**
88 added support for network links
89 reworked CLI (added more options for creating links)
90 added entry point for call tool just like `pylnk3`
91 [FIX] allow build links for non-existed (from this machine) paths
92 [FIX] correct building links on Linux (now expect Windows-like path)
93 [FIX] fixed path priority at parsing with both local & remote presents
94
95
96 **0.3.0**
97 added support links to UWP apps
98
99
100 **0.2.1**
101 released to PyPI
102
103
104 **0.2.0**
105 converted to python 3
106
107 Keywords: lnk,shortcut,windows
108 Platform: UNKNOWN
109 Classifier: Programming Language :: Python :: 3.6
110 Classifier: Programming Language :: Python :: 3.7
111 Classifier: Programming Language :: Python :: 3.8
112 Classifier: Programming Language :: Python :: 3.9
113 Classifier: Intended Audience :: Developers
114 Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
115 Classifier: Operating System :: OS Independent
116 Classifier: Topic :: Software Development :: Libraries :: Python Modules
117 Requires-Python: >=3.6
118 Description-Content-Type: text/markdown
0 README.md
1 pylnk3.py
2 setup.py
3 pylnk3.egg-info/PKG-INFO
4 pylnk3.egg-info/SOURCES.txt
5 pylnk3.egg-info/dependency_links.txt
6 pylnk3.egg-info/entry_points.txt
7 pylnk3.egg-info/top_level.txt
0 [console_scripts]
1 pylnk3 = pylnk3:cli
2
0 #!/usr/bin/env python3
1
2 # original version written by Tim-Christian Mundt (2011):
3 # https://sourceforge.net/p/pylnk/code/HEAD/tree/trunk/pylnk.py
4
5 # converted to python3 by strayge:
6 # https://github.com/strayge/pylnk
7 import argparse
8 import ntpath
9 import os
10 import re
11 import time
12 from datetime import datetime
13 from io import BytesIO, IOBase
14 from pprint import pformat
15 from struct import pack, unpack
16 from typing import Dict, Optional, Tuple, Union
17
18 DEFAULT_CHARSET = 'cp1251'
19
20 # ---- constants
21
22 _SIGNATURE = b'L\x00\x00\x00'
23 _GUID = b'\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00F'
24 _LINK_INFO_HEADER_DEFAULT = 0x1C
25 _LINK_INFO_HEADER_OPTIONAL = 0x24
26
27 _LINK_FLAGS = (
28 'HasLinkTargetIDList',
29 'HasLinkInfo',
30 'HasName',
31 'HasRelativePath',
32 'HasWorkingDir',
33 'HasArguments',
34 'HasIconLocation',
35 'IsUnicode',
36 'ForceNoLinkInfo',
37 # new
38 'HasExpString',
39 'RunInSeparateProcess',
40 'Unused1',
41 'HasDarwinID',
42 'RunAsUser',
43 'HasExpIcon',
44 'NoPidlAlias',
45 'Unused2',
46 'RunWithShimLayer',
47 'ForceNoLinkTrack',
48 'EnableTargetMetadata',
49 'DisableLinkPathTracking',
50 'DisableKnownFolderTracking',
51 'DisableKnownFolderAlias',
52 'AllowLinkToLink',
53 'UnaliasOnSave',
54 'PreferEnvironmentPath',
55 'KeepLocalIDListForUNCTarget',
56 )
57
58 _FILE_ATTRIBUTES_FLAGS = (
59 'read_only', 'hidden', 'system_file', 'reserved1',
60 'directory', 'archive', 'reserved2', 'normal',
61 'temporary', 'sparse_file', 'reparse_point',
62 'compressed', 'offline', 'not_content_indexed',
63 'encrypted',
64 )
65
66 _MODIFIER_KEYS = ('SHIFT', 'CONTROL', 'ALT')
67
68 WINDOW_NORMAL = "Normal"
69 WINDOW_MAXIMIZED = "Maximized"
70 WINDOW_MINIMIZED = "Minimized"
71 _SHOW_COMMANDS = {1: WINDOW_NORMAL, 3: WINDOW_MAXIMIZED, 7: WINDOW_MINIMIZED}
72 _SHOW_COMMAND_IDS = dict((v, k) for k, v in _SHOW_COMMANDS.items())
73
74 DRIVE_UNKNOWN = "Unknown"
75 DRIVE_NO_ROOT_DIR = "No root directory"
76 DRIVE_REMOVABLE = "Removable"
77 DRIVE_FIXED = "Fixed (Hard disk)"
78 DRIVE_REMOTE = "Remote (Network drive)"
79 DRIVE_CDROM = "CD-ROM"
80 DRIVE_RAMDISK = "Ram disk"
81 _DRIVE_TYPES = {0: DRIVE_UNKNOWN,
82 1: DRIVE_NO_ROOT_DIR,
83 2: DRIVE_REMOVABLE,
84 3: DRIVE_FIXED,
85 4: DRIVE_REMOTE,
86 5: DRIVE_CDROM,
87 6: DRIVE_RAMDISK}
88 _DRIVE_TYPE_IDS = dict((v, k) for k, v in _DRIVE_TYPES.items())
89
90 _KEYS = {
91 0x30: '0', 0x31: '1', 0x32: '2', 0x33: '3', 0x34: '4', 0x35: '5', 0x36: '6',
92 0x37: '7', 0x38: '8', 0x39: '9', 0x41: 'A', 0x42: 'B', 0x43: 'C', 0x44: 'D',
93 0x45: 'E', 0x46: 'F', 0x47: 'G', 0x48: 'H', 0x49: 'I', 0x4A: 'J', 0x4B: 'K',
94 0x4C: 'L', 0x4D: 'M', 0x4E: 'N', 0x4F: 'O', 0x50: 'P', 0x51: 'Q', 0x52: 'R',
95 0x53: 'S', 0x54: 'T', 0x55: 'U', 0x56: 'V', 0x57: 'W', 0x58: 'X', 0x59: 'Y',
96 0x5A: 'Z', 0x70: 'F1', 0x71: 'F2', 0x72: 'F3', 0x73: 'F4', 0x74: 'F5',
97 0x75: 'F6', 0x76: 'F7', 0x77: 'F8', 0x78: 'F9', 0x79: 'F10', 0x7A: 'F11',
98 0x7B: 'F12', 0x7C: 'F13', 0x7D: 'F14', 0x7E: 'F15', 0x7F: 'F16', 0x80: 'F17',
99 0x81: 'F18', 0x82: 'F19', 0x83: 'F20', 0x84: 'F21', 0x85: 'F22', 0x86: 'F23',
100 0x87: 'F24', 0x90: 'NUM LOCK', 0x91: 'SCROLL LOCK'
101 }
102 _KEY_CODES = dict((v, k) for k, v in _KEYS.items())
103
104 ROOT_MY_COMPUTER = 'MY_COMPUTER'
105 ROOT_MY_DOCUMENTS = 'MY_DOCUMENTS'
106 ROOT_NETWORK_SHARE = 'NETWORK_SHARE'
107 ROOT_NETWORK_SERVER = 'NETWORK_SERVER'
108 ROOT_NETWORK_PLACES = 'NETWORK_PLACES'
109 ROOT_NETWORK_DOMAIN = 'NETWORK_DOMAIN'
110 ROOT_INTERNET = 'INTERNET'
111 RECYCLE_BIN = 'RECYCLE_BIN'
112 ROOT_CONTROL_PANEL = 'CONTROL_PANEL'
113 ROOT_USER = 'USERPROFILE'
114 ROOT_UWP_APPS = 'APPS'
115
116 _ROOT_LOCATIONS = {
117 '{20D04FE0-3AEA-1069-A2D8-08002B30309D}': ROOT_MY_COMPUTER,
118 '{450D8FBA-AD25-11D0-98A8-0800361B1103}': ROOT_MY_DOCUMENTS,
119 '{54a754c0-4bf1-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SHARE,
120 '{c0542a90-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SERVER,
121 '{208D2C60-3AEA-1069-A2D7-08002B30309D}': ROOT_NETWORK_PLACES,
122 '{46e06680-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_DOMAIN,
123 '{871C5380-42A0-1069-A2EA-08002B30309D}': ROOT_INTERNET,
124 '{645FF040-5081-101B-9F08-00AA002F954E}': RECYCLE_BIN,
125 '{21EC2020-3AEA-1069-A2DD-08002B30309D}': ROOT_CONTROL_PANEL,
126 '{59031A47-3F72-44A7-89C5-5595FE6B30EE}': ROOT_USER,
127 '{4234D49B-0245-4DF3-B780-3893943456E1}': ROOT_UWP_APPS,
128 }
129 _ROOT_LOCATION_GUIDS = dict((v, k) for k, v in _ROOT_LOCATIONS.items())
130
131 TYPE_FOLDER = 'FOLDER'
132 TYPE_FILE = 'FILE'
133 _ENTRY_TYPES = {
134 0x00: 'KNOWN_FOLDER',
135 0x31: 'FOLDER',
136 0x32: 'FILE',
137 0x35: 'FOLDER (UNICODE)',
138 0x36: 'FILE (UNICODE)',
139 0x802E: 'ROOT_KNOWN_FOLDER',
140 # founded in doc, not tested
141 0x1f: 'ROOT_FOLDER',
142 0x61: 'URI',
143 0x71: 'CONTROL_PANEL',
144 }
145 _ENTRY_TYPE_IDS = dict((v, k) for k, v in _ENTRY_TYPES.items())
146
147 _DRIVE_PATTERN = re.compile(r'(\w)[:/\\]*$')
148
149 # ---- read and write binary data
150
151
152 def read_byte(buf):
153 return unpack('<B', buf.read(1))[0]
154
155
156 def read_short(buf):
157 return unpack('<H', buf.read(2))[0]
158
159
160 def read_int(buf):
161 return unpack('<I', buf.read(4))[0]
162
163
164 def read_double(buf):
165 return unpack('<Q', buf.read(8))[0]
166
167
168 def read_cunicode(buf):
169 s = b""
170 b = buf.read(2)
171 while b != b'\x00\x00':
172 s += b
173 b = buf.read(2)
174 return s.decode('utf-16-le')
175
176
177 def read_cstring(buf, padding=False):
178 s = b""
179 b = buf.read(1)
180 while b != b'\x00':
181 s += b
182 b = buf.read(1)
183 if padding and not len(s) % 2:
184 buf.read(1) # make length + terminator even
185 # TODO: encoding is not clear, unicode-escape has been necessary sometimes
186 return s.decode(DEFAULT_CHARSET)
187
188
189 def read_sized_string(buf, string=True):
190 size = read_short(buf)
191 if string:
192 return buf.read(size*2).decode('utf-16-le')
193 else:
194 return buf.read(size)
195
196
197 def get_bits(value, start, count, length=16):
198 mask = 0
199 for i in range(count):
200 mask = mask | 1 << i
201 shift = length - start - count
202 return value >> shift & mask
203
204
205 def read_dos_datetime(buf):
206 date = read_short(buf)
207 time = read_short(buf)
208 year = get_bits(date, 0, 7) + 1980
209 month = get_bits(date, 7, 4)
210 day = get_bits(date, 11, 5)
211 hour = get_bits(time, 0, 5)
212 minute = get_bits(time, 5, 6)
213 second = get_bits(time, 11, 5)
214 # fix zeroes
215 month = max(month, 1)
216 day = max(day, 1)
217 return datetime(year, month, day, hour, minute, second)
218
219
220 def write_byte(val, buf):
221 buf.write(pack('<B', val))
222
223
224 def write_short(val, buf):
225 buf.write(pack('<H', val))
226
227
228 def write_int(val, buf):
229 buf.write(pack('<I', val))
230
231
232 def write_double(val, buf):
233 buf.write(pack('<Q', val))
234
235
236 def write_cstring(val, buf, padding=False):
237 # val = val.encode('unicode-escape').replace('\\\\', '\\')
238 val = val.encode(DEFAULT_CHARSET)
239 buf.write(val + b'\x00')
240 if padding and not len(val) % 2:
241 buf.write(b'\x00')
242
243
244 def write_cunicode(val, buf):
245 uni = val.encode('utf-16-le')
246 buf.write(uni + b'\x00\x00')
247
248
249 def write_sized_string(val, buf, string=True):
250 size = len(val)
251 write_short(size, buf)
252 if string:
253 buf.write(val.encode('utf-16-le'))
254 else:
255 buf.write(val.encode())
256
257
258 def put_bits(bits, target, start, count, length=16):
259 return target | bits << (length - start - count)
260
261
262 def write_dos_datetime(val, buf):
263 date = time = 0
264 date = put_bits(val.year-1980, date, 0, 7)
265 date = put_bits(val.month, date, 7, 4)
266 date = put_bits(val.day, date, 11, 5)
267 time = put_bits(val.hour, time, 0, 5)
268 time = put_bits(val.minute, time, 5, 6)
269 time = put_bits(val.second, time, 11, 5)
270 write_short(date, buf)
271 write_short(time, buf)
272
273
274 # ---- helpers
275
276 def convert_time_to_unix(windows_time):
277 # Windows time is specified as the number of 0.1 nanoseconds since January 1, 1601.
278 # UNIX time is specified as the number of seconds since January 1, 1970.
279 # There are 134774 days (or 11644473600 seconds) between these dates.
280 unix_time = windows_time / 10000000.0 - 11644473600
281 try:
282 return datetime.fromtimestamp(unix_time)
283 except OSError:
284 return datetime.now()
285
286
287 def convert_time_to_windows(unix_time):
288 if isinstance(unix_time, datetime):
289 unix_time = time.mktime(unix_time.timetuple())
290 return int((unix_time + 11644473600) * 10000000)
291
292
293 class FormatException(Exception):
294 pass
295
296
297 class MissingInformationException(Exception):
298 pass
299
300
301 class InvalidKeyException(Exception):
302 pass
303
304
305 def guid_from_bytes(bytes):
306 if len(bytes) != 16:
307 raise FormatException("This is no valid _GUID: %s" % bytes)
308 ordered = [
309 bytes[3], bytes[2], bytes[1], bytes[0],
310 bytes[5], bytes[4], bytes[7], bytes[6],
311 bytes[8], bytes[9], bytes[10], bytes[11],
312 bytes[12], bytes[13], bytes[14], bytes[15]
313 ]
314 return "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered])
315
316
317 def bytes_from_guid(guid):
318 nums = [
319 guid[1:3], guid[3:5], guid[5:7], guid[7:9],
320 guid[10:12], guid[12:14], guid[15:17], guid[17:19],
321 guid[20:22], guid[22:24], guid[25:27], guid[27:29],
322 guid[29:31], guid[31:33], guid[33:35], guid[35:37]
323 ]
324 ordered_nums = [
325 nums[3], nums[2], nums[1], nums[0],
326 nums[5], nums[4], nums[7], nums[6],
327 nums[8], nums[9], nums[10], nums[11],
328 nums[12], nums[13], nums[14], nums[15],
329 ]
330 return bytes([int(x, 16) for x in ordered_nums])
331
332
333 def assert_lnk_signature(f):
334 f.seek(0)
335 sig = f.read(4)
336 guid = f.read(16)
337 if sig != _SIGNATURE:
338 raise FormatException("This is not a .lnk file.")
339 if guid != _GUID:
340 raise FormatException("Cannot read this kind of .lnk file.")
341
342
343 def is_lnk(f):
344 if hasattr(f, 'name'):
345 if f.name.split(os.path.extsep)[-1] == "lnk":
346 assert_lnk_signature(f)
347 return True
348 else:
349 return False
350 else:
351 try:
352 assert_lnk_signature(f)
353 return True
354 except FormatException:
355 return False
356
357
358 def path_levels(p):
359 dirname, base = ntpath.split(p)
360 if base != '':
361 for level in path_levels(dirname):
362 yield level
363 yield p
364
365
366 def is_drive(data):
367 if type(data) not in (str, str):
368 return False
369 p = re.compile("[a-zA-Z]:\\\\?$")
370 return p.match(data) is not None
371
372
373 # ---- data structures
374
375 class Flags(object):
376
377 def __init__(self, flag_names: Tuple[str, ...], flags_bytes=0):
378 self._flag_names = flag_names
379 self._flags: Dict[str, bool] = dict([(name, False) for name in flag_names])
380 self.set_flags(flags_bytes)
381
382 def set_flags(self, flags_bytes):
383 for pos, flag_name in enumerate(self._flag_names):
384 self._flags[flag_name] = bool(flags_bytes >> pos & 0x1)
385
386 @property
387 def bytes(self):
388 bytes = 0
389 for pos in range(len(self._flag_names)):
390 bytes = (self._flags[self._flag_names[pos]] and 1 or 0) << pos | bytes
391 return bytes
392
393 def __getitem__(self, key):
394 if key in self._flags:
395 return object.__getattribute__(self, '_flags')[key]
396 return object.__getattribute__(self, key)
397
398 def __setitem__(self, key, value):
399 if key not in self._flags:
400 raise KeyError("The key '%s' is not defined for those flags." % key)
401 self._flags[key] = value
402
403 def __getattr__(self, key):
404 if key in self._flags:
405 return object.__getattribute__(self, '_flags')[key]
406 return object.__getattribute__(self, key)
407
408 def __setattr__(self, key, value):
409 if '_flags' not in self.__dict__:
410 object.__setattr__(self, key, value)
411 elif key in self.__dict__:
412 object.__setattr__(self, key, value)
413 else:
414 self.__setitem__(key, value)
415
416 def __str__(self):
417 return pformat(self._flags, indent=2)
418
419
420 class ModifierKeys(Flags):
421
422 def __init__(self, flags_bytes=0):
423 Flags.__init__(self, _MODIFIER_KEYS, flags_bytes)
424
425 def __str__(self):
426 s = ""
427 s += self.CONTROL and "CONTROL+" or ""
428 s += self.SHIFT and "SHIFT+" or ""
429 s += self.ALT and "ALT+" or ""
430 return s
431
432
433 # _ROOT_INDEX = {
434 # 0x00: 'INTERNET_EXPLORER1',
435 # 0x42: 'LIBRARIES',
436 # 0x44: 'USERS',
437 # 0x48: 'MY_DOCUMENTS',
438 # 0x50: 'MY_COMPUTER',
439 # 0x58: 'MY_NETWORK_PLACES',
440 # 0x60: 'RECYCLE_BIN',
441 # 0x68: 'INTERNET_EXPLORER2',
442 # 0x70: 'UNKNOWN',
443 # 0x80: 'MY_GAMES',
444 # }
445
446
447 class RootEntry(object):
448
449 def __init__(self, root):
450 if root is not None:
451 # create from text representation
452 if root in list(_ROOT_LOCATION_GUIDS.keys()):
453 self.root = root
454 self.guid = _ROOT_LOCATION_GUIDS[root]
455 return
456
457 # from binary
458 root_type = root[0]
459 index = root[1]
460 guid_bytes = root[2:18]
461 self.guid = guid_from_bytes(guid_bytes)
462 self.root = _ROOT_LOCATIONS.get(self.guid, f"UNKNOWN {self.guid}")
463 # if self.root == "UNKNOWN":
464 # self.root = _ROOT_INDEX.get(index, "UNKNOWN")
465
466 @property
467 def bytes(self):
468 guid = self.guid[1:-1].replace('-', '')
469 chars = [bytes([int(x, 16)]) for x in [guid[i:i+2] for i in range(0, 32, 2)]]
470 return (
471 b'\x1F\x50'
472 + chars[3] + chars[2] + chars[1] + chars[0]
473 + chars[5] + chars[4] + chars[7] + chars[6]
474 + b''.join(chars[8:])
475 )
476
477 def __str__(self):
478 return "<RootEntry: %s>" % self.root
479
480
481 class DriveEntry(object):
482
483 def __init__(self, drive: str):
484 if len(drive) == 23:
485 # binary data from parsed lnk
486 self.drive = drive[1:3]
487 else:
488 # text representation
489 m = _DRIVE_PATTERN.match(drive.strip())
490 if m:
491 self.drive = m.groups()[0].upper() + ':'
492 self.drive = self.drive.encode()
493 else:
494 raise FormatException("This is not a valid drive: " + str(drive))
495
496 @property
497 def bytes(self):
498 drive = self.drive
499 padded_str = drive + b'\\' + b'\x00' * 19
500 return b'\x2F' + padded_str
501 # drive = self.drive
502 # if isinstance(drive, str):
503 # drive = drive.encode()
504 # return b'/' + drive + b'\\' + b'\x00' * 19
505
506 def __str__(self):
507 return "<DriveEntry: %s>" % self.drive
508
509
510 class PathSegmentEntry(object):
511
512 def __init__(self, bytes=None):
513 self.type = None
514 self.file_size = None
515 self.modified = None
516 self.short_name = None
517 self.created = None
518 self.accessed = None
519 self.full_name = None
520 if bytes is None:
521 return
522
523 buf = BytesIO(bytes)
524 self.type = _ENTRY_TYPES.get(read_short(buf), 'UNKNOWN')
525 short_name_is_unicode = self.type.endswith('(UNICODE)')
526
527 if self.type == 'ROOT_KNOWN_FOLDER':
528 self.full_name = '::' + guid_from_bytes(buf.read(16))
529 # then followed Beef0026 structure:
530 # short size
531 # short version
532 # int signature == 0xBEEF0026
533 # (16 bytes) created timestamp
534 # (16 bytes) modified timestamp
535 # (16 bytes) accessed timestamp
536 return
537
538 if self.type == 'KNOWN_FOLDER':
539 _ = read_short(buf) # extra block size
540 extra_signature = read_int(buf)
541 if extra_signature == 0x23FEBBEE:
542 _ = read_short(buf) # unknown
543 _ = read_short(buf) # guid len
544 # that format recognized by explorer
545 self.full_name = '::' + guid_from_bytes(buf.read(16))
546 return
547
548 self.file_size = read_int(buf)
549 self.modified = read_dos_datetime(buf)
550 unknown = read_short(buf) # FileAttributesL
551 if short_name_is_unicode:
552 self.short_name = read_cunicode(buf)
553 else:
554 self.short_name = read_cstring(buf, padding=True)
555 extra_size = read_short(buf)
556 extra_version = read_short(buf)
557 extra_signature = read_int(buf)
558 if extra_signature == 0xBEEF0004:
559 # indicator_1 = read_short(buf) # see below
560 # only_83 = read_short(buf) < 0x03
561 # unknown = read_short(buf) # 0x04
562 # self.is_unicode = read_short(buf) == 0xBeef
563 self.created = read_dos_datetime(buf) # 4 bytes
564 self.accessed = read_dos_datetime(buf) # 4 bytes
565 offset_unicode = read_short(buf) # offset from start of extra_size
566 # only_83_2 = offset_unicode >= indicator_1 or offset_unicode < 0x14
567 if extra_version >= 7:
568 offset_ansi = read_short(buf)
569 file_reference = read_double(buf)
570 unknown2 = read_double(buf)
571 long_string_size = 0
572 if extra_version >= 3:
573 long_string_size = read_short(buf)
574 if extra_version >= 9:
575 unknown4 = read_int(buf)
576 if extra_version >= 8:
577 unknown5 = read_int(buf)
578 if extra_version >= 3:
579 self.full_name = read_cunicode(buf)
580 if long_string_size > 0:
581 if extra_version >= 7:
582 self.localized_name = read_cunicode(buf)
583 else:
584 self.localized_name = read_cstring(buf)
585 version_offset = read_short(buf)
586
587 @classmethod
588 def create_for_path(cls, path):
589 entry = cls()
590 entry.type = os.path.isdir(path) and TYPE_FOLDER or TYPE_FILE
591 try:
592 st = os.stat(path)
593 entry.file_size = st.st_size
594 entry.modified = datetime.fromtimestamp(st.st_mtime)
595 entry.created = datetime.fromtimestamp(st.st_ctime)
596 entry.accessed = datetime.fromtimestamp(st.st_atime)
597 except FileNotFoundError:
598 now = datetime.now()
599 entry.file_size = 0
600 entry.modified = now
601 entry.created = now
602 entry.accessed = now
603 entry.short_name = ntpath.split(path)[1]
604 entry.full_name = entry.short_name
605 return entry
606
607 def _validate(self):
608 if self.type is None:
609 raise MissingInformationException("Type is missing, choose either TYPE_FOLDER or TYPE_FILE.")
610 if self.file_size is None:
611 if self.type.startswith('FOLDER') or self.type in ['KNOWN_FOLDER', 'ROOT_KNOWN_FOLDER']:
612 self.file_size = 0
613 else:
614 raise MissingInformationException("File size missing")
615 if self.created is None:
616 self.created = datetime.now()
617 if self.modified is None:
618 self.modified = datetime.now()
619 if self.accessed is None:
620 self.accessed = datetime.now()
621 # if self.modified is None or self.accessed is None or self.created is None:
622 # raise MissingInformationException("Date information missing")
623 if self.full_name is None:
624 raise MissingInformationException("A full name is missing")
625 if self.short_name is None:
626 self.short_name = self.full_name
627
628 @property
629 def bytes(self):
630 if self.full_name is None:
631 return
632 self._validate()
633 out = BytesIO()
634 entry_type = self.type
635
636 if entry_type == 'KNOWN_FOLDER':
637 write_short(_ENTRY_TYPE_IDS[entry_type], out)
638 write_short(0x1A, out) # size
639 write_int(0x23FEBBEE, out) # extra signature
640 write_short(0x00, out) # extra signature
641 write_short(0x10, out) # guid size
642 out.write(bytes_from_guid(self.full_name.strip(':')))
643 return out.getvalue()
644
645 if entry_type == 'ROOT_KNOWN_FOLDER':
646 write_short(_ENTRY_TYPE_IDS[entry_type], out)
647 out.write(bytes_from_guid(self.full_name.strip(':')))
648 write_short(0x26, out) # 0xBEEF0026 structure size
649 write_short(0x01, out) # version
650 write_int(0xBEEF0026, out) # extra signature
651 write_int(0x11, out) # some flag for containing datetime
652 write_double(0x00, out) # created datetime
653 write_double(0x00, out) # modified datetime
654 write_double(0x00, out) # accessed datetime
655 write_short(0x14, out) # unknown
656 return out.getvalue()
657
658 short_name_len = len(self.short_name) + 1
659 try:
660 self.short_name.encode("ascii")
661 short_name_is_unicode = False
662 short_name_len += short_name_len % 2 # padding
663 except (UnicodeEncodeError, UnicodeDecodeError):
664 short_name_is_unicode = True
665 short_name_len = short_name_len * 2
666 self.type += " (UNICODE)"
667 write_short(_ENTRY_TYPE_IDS[entry_type], out)
668 write_int(self.file_size, out)
669 write_dos_datetime(self.modified, out)
670 write_short(0x10, out)
671 if short_name_is_unicode:
672 write_cunicode(self.short_name, out)
673 else:
674 write_cstring(self.short_name, out, padding=True)
675 indicator = 24 + 2 * len(self.short_name)
676 write_short(indicator, out) # size
677 write_short(0x03, out) # version
678 write_short(0x04, out) # signature part1
679 write_short(0xBeef, out) # signature part2
680 write_dos_datetime(self.created, out)
681 write_dos_datetime(self.accessed, out)
682 offset_unicode = 0x14 # fixed data structure, always the same
683 write_short(offset_unicode, out)
684 offset_ansi = 0 # we always write unicode
685 write_short(offset_ansi, out) # long_string_size
686 write_cunicode(self.full_name, out)
687 offset_part2 = 0x0E + short_name_len
688 write_short(offset_part2, out)
689 return out.getvalue()
690
691 def __str__(self):
692 return "<PathSegmentEntry: %s>" % self.full_name
693
694
695 class UwpSubBlock:
696
697 block_names = {
698 0x11: 'PackageFamilyName',
699 # 0x0e: '',
700 # 0x19: '',
701 0x15: 'PackageFullName',
702 0x05: 'Target',
703 0x0f: 'Location',
704 0x20: 'RandomGuid',
705 0x0c: 'Square150x150Logo',
706 0x02: 'Square44x44Logo',
707 0x0d: 'Wide310x150Logo',
708 # 0x04: '',
709 # 0x05: '',
710 0x13: 'Square310x310Logo',
711 # 0x0e: '',
712 0x0b: 'DisplayName',
713 0x14: 'Square71x71Logo',
714 0x64: 'RandomByte',
715 0x0a: 'DisplayName',
716 # 0x07: '',
717 }
718
719 block_types = {
720 'string': [0x11, 0x15, 0x05, 0x0f, 0x0c, 0x02, 0x0d, 0x13, 0x0b, 0x14, 0x0a],
721 }
722
723 def __init__(self, bytes=None, type=None, value=None):
724 self._data = bytes or b''
725 self.type = type
726 self.value = value
727 self.name = None
728 if self.type is not None:
729 self.name = self.block_names.get(self.type, 'UNKNOWN')
730 if not bytes:
731 return
732 buf = BytesIO(bytes)
733 self.type = read_byte(buf)
734 self.name = self.block_names.get(self.type, 'UNKNOWN')
735
736 self.value = self._data[1:] # skip type
737 if self.type in self.block_types['string']:
738 unknown = read_int(buf)
739 probably_type = read_int(buf)
740 if probably_type == 0x1f:
741 string_len = read_int(buf)
742 self.value = read_cunicode(buf)
743
744 def __str__(self):
745 string = f'UwpSubBlock {self.name} ({hex(self.type)}): {self.value}'
746 return string.strip()
747
748 @property
749 def bytes(self):
750 out = BytesIO()
751 if self.value:
752 if isinstance(self.value, str):
753 string_len = len(self.value) + 1
754
755 write_byte(self.type, out)
756 write_int(0, out)
757 write_int(0x1f, out)
758
759 write_int(string_len, out)
760 write_cunicode(self.value, out)
761 if string_len % 2 == 1: # padding
762 write_short(0, out)
763
764 elif isinstance(self.value, bytes):
765 write_byte(self.type, out)
766 out.write(self.value)
767
768 result = out.getvalue()
769 return result
770
771
772 class UwpMainBlock:
773 magic = b'\x31\x53\x50\x53'
774
775 def __init__(self, bytes=None, guid: Optional[str] = None, blocks=None):
776 self._data = bytes or b''
777 self._blocks = blocks or []
778 self.guid: str = guid
779 if not bytes:
780 return
781 buf = BytesIO(bytes)
782 magic = buf.read(4)
783 self.guid = guid_from_bytes(buf.read(16))
784 # read sub blocks
785 while True:
786 sub_block_size = read_int(buf)
787 if not sub_block_size: # last size is zero
788 break
789 sub_block_data = buf.read(sub_block_size - 4) # includes block_size
790 self._blocks.append(UwpSubBlock(sub_block_data))
791
792 def __str__(self):
793 string = f'<UwpMainBlock> {self.guid}:\n'
794 for block in self._blocks:
795 string += f' {block}\n'
796 return string.strip()
797
798 @property
799 def bytes(self):
800 blocks_bytes = [block.bytes for block in self._blocks]
801 out = BytesIO()
802 out.write(self.magic)
803 out.write(bytes_from_guid(self.guid))
804 for block in blocks_bytes:
805 write_int(len(block) + 4, out)
806 out.write(block)
807 write_int(0, out)
808 result = out.getvalue()
809 return result
810
811
812 class UwpSegmentEntry:
813 magic = b'APPS'
814 header = b'\x08\x00\x03\x00\x00\x00\x00\x00\x00\x00'
815
816 def __init__(self, bytes=None):
817 self._blocks = []
818 self._data = bytes
819 if bytes is None:
820 return
821 buf = BytesIO(bytes)
822 unknown = read_short(buf)
823 size = read_short(buf)
824 magic = buf.read(4) # b'APPS'
825 blocks_size = read_short(buf)
826 unknown2 = buf.read(10)
827 # read main blocks
828 while True:
829 block_size = read_int(buf)
830 if not block_size: # last size is zero
831 break
832 block_data = buf.read(block_size - 4) # includes block_size
833 self._blocks.append(UwpMainBlock(block_data))
834
835 def __str__(self):
836 string = '<UwpSegmentEntry>:\n'
837 for block in self._blocks:
838 string += f' {block}\n'
839 return string.strip()
840
841 @property
842 def bytes(self):
843 blocks_bytes = [block.bytes for block in self._blocks]
844 blocks_size = sum([len(block) + 4 for block in blocks_bytes]) + 4 # with terminator
845 size = (
846 2 # size
847 + len(self.magic)
848 + 2 # second size
849 + len(self.header)
850 + blocks_size # blocks with terminator
851 )
852
853 out = BytesIO()
854 write_short(0, out)
855 write_short(size, out)
856 out.write(self.magic)
857 write_short(blocks_size, out)
858 out.write(self.header)
859 for block in blocks_bytes:
860 write_int(len(block) + 4, out)
861 out.write(block)
862 write_int(0, out) # empty block
863 write_short(0, out) # ??
864
865 result = out.getvalue()
866 return result
867
868 @classmethod
869 def create(cls, package_family_name, target, location=None, logo44x44=None):
870 segment = cls()
871
872 blocks = [
873 UwpSubBlock(type=0x11, value=package_family_name),
874 UwpSubBlock(type=0x0e, value=b'\x00\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00'),
875 UwpSubBlock(type=0x05, value=target),
876 ]
877 if location:
878 blocks.append(UwpSubBlock(type=0x0f, value=location)) # need for relative icon path
879 main1 = UwpMainBlock(guid='{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', blocks=blocks)
880 segment._blocks.append(main1)
881
882 if logo44x44:
883 main2 = UwpMainBlock(
884 guid='{86D40B4D-9069-443C-819A-2A54090DCCEC}',
885 blocks=[UwpSubBlock(type=0x02, value=logo44x44)]
886 )
887 segment._blocks.append(main2)
888
889 return segment
890
891
892 class LinkTargetIDList(object):
893
894 def __init__(self, bytes=None):
895 self.items = []
896 if bytes is not None:
897 buf = BytesIO(bytes)
898 raw = []
899 entry_len = read_short(buf)
900 while entry_len > 0:
901 raw.append(buf.read(entry_len - 2)) # the length includes the size
902 entry_len = read_short(buf)
903 self._interpret(raw)
904
905 def _interpret(self, raw):
906 if raw[0][0] == 0x1F:
907 self.items.append(RootEntry(raw[0]))
908 if self.items[0].root == ROOT_MY_COMPUTER:
909 if len(raw[1]) == 0x17:
910 self.items.append(DriveEntry(raw[1]))
911 elif raw[1][0:2] == b'\x2E\x80': # ROOT_KNOWN_FOLDER
912 self.items.append(PathSegmentEntry(raw[1]))
913 else:
914 raise ValueError("This seems to be an absolute link which requires a drive as second element.")
915 items = raw[2:]
916 elif self.items[0].root == ROOT_NETWORK_PLACES:
917 raise NotImplementedError(
918 "Parsing network lnks has not yet been implemented. "
919 "If you need it just contact me and we'll see..."
920 )
921 else:
922 items = raw[1:]
923 else:
924 items = raw
925 for item in items:
926 if item[4:8] == b'APPS':
927 self.items.append(UwpSegmentEntry(item))
928 else:
929 self.items.append(PathSegmentEntry(item))
930
931 def get_path(self):
932 segments = []
933 for item in self.items:
934 if type(item) == RootEntry:
935 segments.append('%' + item.root + '%')
936 elif type(item) == DriveEntry:
937 segments.append(item.drive.decode())
938 elif type(item) == PathSegmentEntry:
939 if item.full_name is not None:
940 segments.append(item.full_name)
941 else:
942 segments.append(item)
943 return '\\'.join(segments)
944
945 def _validate(self):
946 if type(self.items[0]) == RootEntry and self.items[0].root == ROOT_MY_COMPUTER:
947 if type(self.items[1]) == DriveEntry:
948 return
949 if type(self.items[1]) == PathSegmentEntry and self.items[1].full_name.startswith('::'):
950 return
951 raise ValueError("A drive is required for absolute lnks")
952
953 @property
954 def bytes(self):
955 self._validate()
956 out = BytesIO()
957 for item in self.items:
958 bytes = item.bytes
959 # skip invalid
960 if bytes is None:
961 continue
962 write_short(len(bytes) + 2, out) # len + terminator
963 out.write(bytes)
964 out.write(b'\x00\x00')
965 return out.getvalue()
966
967 def __str__(self):
968 string = '<LinkTargetIDList>:\n'
969 for item in self.items:
970 string += f' {item}\n'
971 return string.strip()
972
973
974 class LinkInfo(object):
975
976 def __init__(self, lnk=None):
977 if lnk is not None:
978 self.start = lnk.tell()
979 self.size = read_int(lnk)
980 self.header_size = read_int(lnk)
981 link_info_flags = read_int(lnk)
982 self.local = link_info_flags & 1
983 self.remote = link_info_flags & 2
984 self.offs_local_volume_table = read_int(lnk)
985 self.offs_local_base_path = read_int(lnk)
986 self.offs_network_volume_table = read_int(lnk)
987 self.offs_base_name = read_int(lnk)
988 if self.header_size >= _LINK_INFO_HEADER_OPTIONAL:
989 print("TODO: read the unicode stuff") # TODO: read the unicode stuff
990 self._parse_path_elements(lnk)
991 else:
992 self.size = None
993 self.header_size = _LINK_INFO_HEADER_DEFAULT
994 self.local = 0
995 self.remote = 0
996 self.offs_local_volume_table = 0
997 self.offs_local_base_path = 0
998 self.offs_network_volume_table = 0
999 self.offs_base_name = 0
1000 self.drive_type = None
1001 self.drive_serial = None
1002 self.volume_label = None
1003 self.local_base_path = None
1004 self.network_share_name = None
1005 self.base_name = None
1006 self._path = None
1007
1008 def _parse_path_elements(self, lnk):
1009 if self.remote:
1010 # 20 is the offset of the network share name
1011 lnk.seek(self.start + self.offs_network_volume_table + 20)
1012 self.network_share_name = read_cstring(lnk)
1013 lnk.seek(self.start + self.offs_base_name)
1014 self.base_name = read_cstring(lnk)
1015 if self.local:
1016 lnk.seek(self.start + self.offs_local_volume_table + 4)
1017 self.drive_type = _DRIVE_TYPES.get(read_int(lnk))
1018 self.drive_serial = read_int(lnk)
1019 lnk.read(4) # volume name offset (10h)
1020 self.volume_label = read_cstring(lnk)
1021 lnk.seek(self.start + self.offs_local_base_path)
1022 self.local_base_path = read_cstring(lnk)
1023 # TODO: unicode
1024 self.make_path()
1025
1026 def make_path(self):
1027 if self.remote:
1028 self._path = self.network_share_name + '\\' + self.base_name
1029 if self.local:
1030 self._path = self.local_base_path
1031
1032 def write(self, lnk):
1033 if self.remote is None:
1034 raise MissingInformationException("No location information given.")
1035 self.start = lnk.tell()
1036 self._calculate_sizes_and_offsets()
1037 write_int(self.size, lnk)
1038 write_int(self.header_size, lnk)
1039 write_int((self.local and 1) + (self.remote and 2), lnk)
1040 write_int(self.offs_local_volume_table, lnk)
1041 write_int(self.offs_local_base_path, lnk)
1042 write_int(self.offs_network_volume_table, lnk)
1043 write_int(self.offs_base_name, lnk)
1044 if self.remote:
1045 self._write_network_volume_table(lnk)
1046 write_cstring(self.base_name, lnk, padding=False)
1047 else:
1048 self._write_local_volume_table(lnk)
1049 write_cstring(self.local_base_path, lnk, padding=False)
1050 write_byte(0, lnk)
1051
1052 def _calculate_sizes_and_offsets(self):
1053 self.size_base_name = 1 # len(self.base_name) + 1 # zero terminated strings
1054 self.size = 28 + self.size_base_name
1055 if self.remote:
1056 self.size_network_volume_table = 20 + len(self.network_share_name) + len(self.base_name) + 1
1057 self.size += self.size_network_volume_table
1058 self.offs_local_volume_table = 0
1059 self.offs_local_base_path = 0
1060 self.offs_network_volume_table = 28
1061 self.offs_base_name = self.offs_network_volume_table + self.size_network_volume_table
1062 else:
1063 self.size_local_volume_table = 16 + len(self.volume_label) + 1
1064 self.size_local_base_path = len(self.local_base_path) + 1
1065 self.size += self.size_local_volume_table + self.size_local_base_path
1066 self.offs_local_volume_table = 28
1067 self.offs_local_base_path = self.offs_local_volume_table + self.size_local_volume_table
1068 self.offs_network_volume_table = 0
1069 self.offs_base_name = self.offs_local_base_path + self.size_local_base_path
1070
1071 def _write_network_volume_table(self, buf):
1072 write_int(self.size_network_volume_table, buf)
1073 write_int(2, buf) # ?
1074 write_int(20, buf) # size of Network Volume Table
1075 write_int(0, buf) # ?
1076 write_int(131072, buf) # ?
1077 write_cstring(self.network_share_name, buf)
1078
1079 def _write_local_volume_table(self, buf):
1080 write_int(self.size_local_volume_table, buf)
1081 try:
1082 drive_type = _DRIVE_TYPE_IDS[self.drive_type]
1083 except KeyError:
1084 raise ValueError("This is not a valid drive type: %s" % self.drive_type)
1085 write_int(drive_type, buf)
1086 write_int(self.drive_serial, buf)
1087 write_int(16, buf) # volume name offset
1088 write_cstring(self.volume_label, buf)
1089
1090 @property
1091 def path(self):
1092 return self._path
1093
1094 def __str__(self):
1095 s = "File Location Info:"
1096 if self._path is None:
1097 return s + " <not specified>"
1098 if self.remote:
1099 s += "\n (remote)"
1100 s += "\n Network Share: %s" % self.network_share_name
1101 s += "\n Base Name: %s" % self.base_name
1102 else:
1103 s += "\n (local)"
1104 s += "\n Volume Type: %s" % self.drive_type
1105 s += "\n Volume Serial Number: %s" % self.drive_serial
1106 s += "\n Volume Label: %s" % self.volume_label
1107 s += "\n Path: %s" % self.local_base_path
1108 return s
1109
1110
1111 EXTRA_DATA_TYPES = {
1112 0xA0000002: 'ConsoleDataBlock', # size 0x000000CC
1113 0xA0000004: 'ConsoleFEDataBlock', # size 0x0000000C
1114 0xA0000006: 'DarwinDataBlock', # size 0x00000314
1115 0xA0000001: 'EnvironmentVariableDataBlock', # size 0x00000314
1116 0xA0000007: 'IconEnvironmentDataBlock', # size 0x00000314
1117 0xA000000B: 'KnownFolderDataBlock', # size 0x0000001C
1118 0xA0000009: 'PropertyStoreDataBlock', # size >= 0x0000000C
1119 0xA0000008: 'ShimDataBlock', # size >= 0x00000088
1120 0xA0000005: 'SpecialFolderDataBlock', # size 0x00000010
1121 0xA0000003: 'VistaAndAboveIDListDataBlock', # size 0x00000060
1122 0xA000000C: 'VistaIDListDataBlock', # size 0x00000173
1123 }
1124
1125
1126 class ExtraData_Unparsed(object):
1127 def __init__(self, bytes=None, signature=None, data=None):
1128 self._signature = signature
1129 self._size = None
1130 self.data = data
1131 # if data:
1132 # self._size = len(data)
1133 if bytes:
1134 # self._size = len(bytes)
1135 self.data = bytes
1136 # self.read(bytes)
1137
1138 # def read(self, bytes):
1139 # buf = BytesIO(bytes)
1140 # size = len(bytes)
1141 # # self._size = read_int(buf)
1142 # # self._signature = read_int(buf)
1143 # self.data = buf.read(self._size - 8)
1144
1145 def bytes(self):
1146 buf = BytesIO()
1147 write_int(len(self.data)+8, buf)
1148 write_int(self._signature, buf)
1149 buf.write(self.data)
1150 return buf.getvalue()
1151
1152 def __str__(self):
1153 s = 'ExtraDataBlock\n signature %s\n data: %s' % (hex(self._signature), self.data)
1154 return s
1155
1156
1157 def padding(val, size, byte=b'\x00'):
1158 return val + (size-len(val)) * byte
1159
1160
1161 class ExtraData_IconEnvironmentDataBlock(object):
1162 def __init__(self, bytes=None):
1163 # self._size = None
1164 # self._signature = None
1165 self._signature = 0xA0000007
1166 self.target_ansi = None
1167 self.target_unicode = None
1168 if bytes:
1169 self.read(bytes)
1170
1171 def read(self, bytes):
1172 buf = BytesIO(bytes)
1173 # self._size = read_int(buf)
1174 # self._signature = read_int(buf)
1175 self.target_ansi = buf.read(260).decode()
1176 self.target_unicode = buf.read(520).decode('utf-16-le')
1177
1178 def bytes(self):
1179 target_ansi = padding(self.target_ansi.encode(), 260)
1180 target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520)
1181 size = 8 + len(target_ansi) + len(target_unicode)
1182 assert self._signature == 0xA0000007
1183 assert size == 0x00000314
1184 buf = BytesIO()
1185 write_int(size, buf)
1186 write_int(self._signature, buf)
1187 buf.write(target_ansi)
1188 buf.write(target_unicode)
1189 return buf.getvalue()
1190
1191 def __str__(self):
1192 target_ansi = self.target_ansi.replace('\x00', '')
1193 target_unicode = self.target_unicode.replace('\x00', '')
1194 s = f'IconEnvironmentDataBlock\n TargetAnsi: {target_ansi}\n TargetUnicode: {target_unicode}'
1195 return s
1196
1197
1198 def guid_to_str(guid):
1199 ordered = [guid[3], guid[2], guid[1], guid[0], guid[5], guid[4],
1200 guid[7], guid[6], guid[8], guid[9], guid[10], guid[11],
1201 guid[12], guid[13], guid[14], guid[15]]
1202 res = "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered])
1203 # print(guid, res)
1204 return res
1205
1206
1207 class TypedPropertyValue(object):
1208 # types: [MS-OLEPS] section 2.15
1209 def __init__(self, bytes=None, type=None, value=None):
1210 self.type = type
1211 self.value = value
1212 if bytes:
1213 self.type = read_short(BytesIO(bytes))
1214 padding = bytes[2:4]
1215 self.value = bytes[4:]
1216
1217 def set_string(self, value):
1218 self.type = 0x1f
1219 buf = BytesIO()
1220 write_int(len(value)+2, buf)
1221 buf.write(value.encode('utf-16-le'))
1222 # terminator (included in size)
1223 buf.write(b'\x00\x00\x00\x00')
1224 # padding (not included in size)
1225 if len(value) % 2:
1226 buf.write(b'\x00\x00')
1227 self.value = buf.getvalue()
1228
1229 @property
1230 def bytes(self):
1231 buf = BytesIO()
1232 write_short(self.type, buf)
1233 write_short(0x0000, buf)
1234 buf.write(self.value)
1235 return buf.getvalue()
1236
1237 def __str__(self):
1238 value = self.value
1239 if self.type == 0x1F:
1240 size = value[:4]
1241 value = value[4:].decode('utf-16-le')
1242 if self.type == 0x15:
1243 value = unpack('<Q', value)[0]
1244 if self.type == 0x13:
1245 value = unpack('<I', value)[0]
1246 if self.type == 0x14:
1247 value = unpack('<q', value)[0]
1248 if self.type == 0x16:
1249 value = unpack('<i', value)[0]
1250 if self.type == 0x17:
1251 value = unpack('<I', value)[0]
1252 if self.type == 0x48:
1253 value = guid_to_str(value)
1254 if self.type == 0x40:
1255 # FILETIME (Packet Version)
1256 stream = BytesIO(value)
1257 low = read_int(stream)
1258 high = read_int(stream)
1259 num = (high << 32) + low
1260 value = convert_time_to_unix(num)
1261 return '%s: %s' % (hex(self.type), value)
1262
1263
1264 class PropertyStore:
1265 def __init__(self, bytes=None, properties=None, format_id=None, is_strings=False):
1266 self.is_strings = is_strings
1267 self.properties = []
1268 self.format_id = format_id
1269 self._is_end = False
1270 if properties:
1271 self.properties = properties
1272 if bytes:
1273 self.read(bytes)
1274
1275 def read(self, bytes_io):
1276 buf = bytes_io
1277 size = read_int(buf)
1278 assert size < len(buf.getvalue())
1279 if size == 0x00000000:
1280 self._is_end = True
1281 return
1282 version = read_int(buf)
1283 assert version == 0x53505331
1284 self.format_id = buf.read(16)
1285 if self.format_id == b'\xD5\xCD\xD5\x05\x2E\x9C\x10\x1B\x93\x97\x08\x00\x2B\x2C\xF9\xAE':
1286 self.is_strings = True
1287 else:
1288 self.is_strings = False
1289 while True:
1290 # assert lnk.tell() < (start + size)
1291 value_size = read_int(buf)
1292 if value_size == 0x00000000:
1293 break
1294 if self.is_strings:
1295 name_size = read_int(buf)
1296 reserved = read_byte(buf)
1297 name = buf.read(name_size).decode('utf-16-le')
1298 value = TypedPropertyValue(buf.read(value_size-9))
1299 self.properties.append((name, value))
1300 else:
1301 value_id = read_int(buf)
1302 reserved = read_byte(buf)
1303 value = TypedPropertyValue(buf.read(value_size-9))
1304 self.properties.append((value_id, value))
1305
1306 @property
1307 def bytes(self):
1308 size = 8 + len(self.format_id)
1309 properties = BytesIO()
1310 for name, value in self.properties:
1311 value_bytes = value.bytes
1312 if self.is_strings:
1313 name_bytes = name.encode('utf-16-le')
1314 value_size = 9 + len(name_bytes) + len(value_bytes)
1315 write_int(value_size, properties)
1316 name_size = len(name_bytes)
1317 write_int(name_size, properties)
1318 properties.write(b'\x00')
1319 properties.write(name_bytes)
1320 else:
1321 value_size = 9 + len(value_bytes)
1322 write_int(value_size, properties)
1323 write_int(name, properties)
1324 properties.write(b'\x00')
1325 properties.write(value_bytes)
1326 size += value_size
1327
1328 write_int(0x00000000, properties)
1329 size += 4
1330
1331 buf = BytesIO()
1332 write_int(size, buf)
1333 write_int(0x53505331, buf)
1334 buf.write(self.format_id)
1335 buf.write(properties.getvalue())
1336
1337 return buf.getvalue()
1338
1339 def __str__(self):
1340 s = ' PropertyStore'
1341 s += '\n FormatID: %s' % guid_to_str(self.format_id)
1342 for name, value in self.properties:
1343 s += '\n %3s = %s' % (name, str(value))
1344 return s.strip()
1345
1346
1347 class ExtraData_PropertyStoreDataBlock(object):
1348 def __init__(self, bytes=None, stores=None):
1349 self._size = None
1350 self._signature = 0xA0000009
1351 self.stores = []
1352 if stores:
1353 self.stores = stores
1354 if bytes:
1355 self.read(bytes)
1356
1357 def read(self, bytes):
1358 buf = BytesIO(bytes)
1359 # self._size = read_int(buf)
1360 # self._signature = read_int(buf)
1361 # [MS-PROPSTORE] section 2.2
1362 while True:
1363 prop_store = PropertyStore(buf)
1364 if prop_store._is_end:
1365 break
1366 self.stores.append(prop_store)
1367
1368 def bytes(self):
1369 stores = b''
1370 for prop_store in self.stores:
1371 stores += prop_store.bytes
1372 size = len(stores) + 8 + 4
1373
1374 assert self._signature == 0xA0000009
1375 assert size >= 0x0000000C
1376
1377 buf = BytesIO()
1378 write_int(size, buf)
1379 write_int(self._signature, buf)
1380 buf.write(stores)
1381 write_int(0x00000000, buf)
1382 return buf.getvalue()
1383
1384 def __str__(self):
1385 s = 'PropertyStoreDataBlock'
1386 for prop_store in self.stores:
1387 s += '\n %s' % str(prop_store)
1388 return s
1389
1390
1391 class ExtraData_EnvironmentVariableDataBlock(object):
1392 def __init__(self, bytes=None):
1393 self._signature = 0xA0000001
1394 self.target_ansi = None
1395 self.target_unicode = None
1396 if bytes:
1397 self.read(bytes)
1398
1399 def read(self, bytes):
1400 buf = BytesIO(bytes)
1401 self.target_ansi = buf.read(260).decode()
1402 self.target_unicode = buf.read(520).decode('utf-16-le')
1403
1404 def bytes(self):
1405 target_ansi = padding(self.target_ansi.encode(), 260)
1406 target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520)
1407 size = 8 + len(target_ansi) + len(target_unicode)
1408 assert self._signature == 0xA0000001
1409 assert size == 0x00000314
1410 buf = BytesIO()
1411 write_int(size, buf)
1412 write_int(self._signature, buf)
1413 buf.write(target_ansi)
1414 buf.write(target_unicode)
1415 return buf.getvalue()
1416
1417 def __str__(self):
1418 target_ansi = self.target_ansi.replace('\x00', '')
1419 target_unicode = self.target_unicode.replace('\x00', '')
1420 s = f'EnvironmentVariableDataBlock\n TargetAnsi: {target_ansi}\n TargetUnicode: {target_unicode}'
1421 return s
1422
1423
1424 EXTRA_DATA_TYPES_CLASSES = {
1425 'IconEnvironmentDataBlock': ExtraData_IconEnvironmentDataBlock,
1426 'PropertyStoreDataBlock': ExtraData_PropertyStoreDataBlock,
1427 'EnvironmentVariableDataBlock': ExtraData_EnvironmentVariableDataBlock,
1428 }
1429
1430
1431 class ExtraData(object):
1432 # EXTRA_DATA = *EXTRA_DATA_BLOCK TERMINAL_BLOCK
1433 def __init__(self, lnk=None, blocks=None):
1434 self.blocks = []
1435 if blocks:
1436 self.blocks = blocks
1437 if lnk is None:
1438 return
1439 while True:
1440 size = read_int(lnk)
1441 if size < 4: # TerminalBlock
1442 break
1443 signature = read_int(lnk)
1444 bytes = lnk.read(size-8)
1445 # lnk.seek(-8, 1)
1446 block_type = EXTRA_DATA_TYPES[signature]
1447 if block_type in EXTRA_DATA_TYPES_CLASSES:
1448 block_class = EXTRA_DATA_TYPES_CLASSES[block_type]
1449 block = block_class(bytes=bytes)
1450 else:
1451 block_class = ExtraData_Unparsed
1452 block = block_class(bytes=bytes, signature=signature)
1453 self.blocks.append(block)
1454
1455 @property
1456 def bytes(self):
1457 result = b''
1458 for block in self.blocks:
1459 result += block.bytes()
1460 result += b'\x00\x00\x00\x00' # TerminalBlock
1461 return result
1462
1463 def __str__(self):
1464 s = ''
1465 for block in self.blocks:
1466 s += '\n' + str(block)
1467 return s
1468
1469
1470 class Lnk(object):
1471
1472 def __init__(self, f=None):
1473 self.file = None
1474 if type(f) == str or type(f) == str:
1475 self.file = f
1476 try:
1477 f = open(self.file, 'rb')
1478 except IOError:
1479 self.file += ".lnk"
1480 f = open(self.file, 'rb')
1481 # defaults
1482 self.link_flags = Flags(_LINK_FLAGS)
1483 self.file_flags = Flags(_FILE_ATTRIBUTES_FLAGS)
1484 self.creation_time = datetime.now()
1485 self.access_time = datetime.now()
1486 self.modification_time = datetime.now()
1487 self.file_size = 0
1488 self.icon_index = 0
1489 self._show_command = WINDOW_NORMAL
1490 self.hot_key = None
1491 self._link_info = LinkInfo()
1492 self.description = None
1493 self.relative_path = None
1494 self.work_dir = None
1495 self.arguments = None
1496 self.icon = None
1497 self.extra_data = None
1498 if f is not None:
1499 assert_lnk_signature(f)
1500 self._parse_lnk_file(f)
1501 if self.file:
1502 f.close()
1503
1504 def _read_hot_key(self, lnk):
1505 low = read_byte(lnk)
1506 high = read_byte(lnk)
1507 key = _KEYS.get(low, '')
1508 modifier = high and str(ModifierKeys(high)) or ''
1509 return modifier + key
1510
1511 def _write_hot_key(self, hot_key, lnk):
1512 if hot_key is None or not hot_key:
1513 low = high = 0
1514 else:
1515 hot_key = hot_key.split('+')
1516 try:
1517 low = _KEY_CODES[hot_key[-1]]
1518 except KeyError:
1519 raise InvalidKeyException("Cannot find key code for %s" % hot_key[1])
1520 modifiers = ModifierKeys()
1521 for modifier in hot_key[:-1]:
1522 modifiers[modifier.upper()] = True
1523 high = modifiers.bytes
1524 write_byte(low, lnk)
1525 write_byte(high, lnk)
1526
1527 def _parse_lnk_file(self, lnk):
1528 # SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO] [STRING_DATA] *EXTRA_DATA
1529
1530 # SHELL_LINK_HEADER
1531 lnk.seek(20) # after signature and guid
1532 self.link_flags.set_flags(read_int(lnk))
1533 self.file_flags.set_flags(read_int(lnk))
1534 self.creation_time = convert_time_to_unix(read_double(lnk))
1535 self.access_time = convert_time_to_unix(read_double(lnk))
1536 self.modification_time = convert_time_to_unix(read_double(lnk))
1537 self.file_size = read_int(lnk)
1538 self.icon_index = read_int(lnk)
1539 show_command = read_int(lnk)
1540 self._show_command = _SHOW_COMMANDS[show_command] if show_command in _SHOW_COMMANDS else _SHOW_COMMANDS[1]
1541 self.hot_key = self._read_hot_key(lnk)
1542 lnk.read(10) # reserved (0)
1543
1544 # LINKTARGET_IDLIST (HasLinkTargetIDList)
1545 if self.link_flags.HasLinkTargetIDList:
1546 shell_item_id_list_size = read_short(lnk)
1547 self.shell_item_id_list = LinkTargetIDList(lnk.read(shell_item_id_list_size))
1548
1549 # LINKINFO (HasLinkInfo)
1550 if self.link_flags.HasLinkInfo and not self.link_flags.ForceNoLinkInfo:
1551 self._link_info = LinkInfo(lnk)
1552 lnk.seek(self._link_info.start + self._link_info.size)
1553
1554 # STRING_DATA = [NAME_STRING] [RELATIVE_PATH] [WORKING_DIR] [COMMAND_LINE_ARGUMENTS] [ICON_LOCATION]
1555 if self.link_flags.HasName:
1556 self.description = read_sized_string(lnk, self.link_flags.IsUnicode)
1557 if self.link_flags.HasRelativePath:
1558 self.relative_path = read_sized_string(lnk, self.link_flags.IsUnicode)
1559 if self.link_flags.HasWorkingDir:
1560 self.work_dir = read_sized_string(lnk, self.link_flags.IsUnicode)
1561 if self.link_flags.HasArguments:
1562 self.arguments = read_sized_string(lnk, self.link_flags.IsUnicode)
1563 if self.link_flags.HasIconLocation:
1564 self.icon = read_sized_string(lnk, self.link_flags.IsUnicode)
1565
1566 # *EXTRA_DATA
1567 self.extra_data = ExtraData(lnk)
1568
1569 def save(self, f: Optional[Union[str, IOBase]] = None, force_ext=False):
1570 if f is None:
1571 f = self.file
1572 if f is None:
1573 raise ValueError("File (name) missing for saving the lnk")
1574 is_file = hasattr(f, 'write')
1575 if not is_file:
1576 if not type(f) == str and not type(f) == str:
1577 raise ValueError("Need a writeable object or a file name to save to, got %s" % f)
1578 if force_ext:
1579 if not f.lower().endswith('.lnk'):
1580 f += '.lnk'
1581 f = open(f, 'wb')
1582 self.write(f)
1583 # only close the stream if it's our own
1584 if not is_file:
1585 f.close()
1586
1587 def write(self, lnk):
1588 lnk.write(_SIGNATURE)
1589 lnk.write(_GUID)
1590 write_int(self.link_flags.bytes, lnk)
1591 write_int(self.file_flags.bytes, lnk)
1592 write_double(convert_time_to_windows(self.creation_time), lnk)
1593 write_double(convert_time_to_windows(self.access_time), lnk)
1594 write_double(convert_time_to_windows(self.modification_time), lnk)
1595 write_int(self.file_size, lnk)
1596 write_int(self.icon_index, lnk)
1597 write_int(_SHOW_COMMAND_IDS[self._show_command], lnk)
1598 self._write_hot_key(self.hot_key, lnk)
1599 lnk.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') # reserved
1600 if self.link_flags.HasLinkTargetIDList:
1601 shell_item_id_list = self.shell_item_id_list.bytes
1602 write_short(len(shell_item_id_list), lnk)
1603 lnk.write(shell_item_id_list)
1604 if self.link_flags.HasLinkInfo:
1605 self._link_info.write(lnk)
1606 if self.link_flags.HasName:
1607 write_sized_string(self.description, lnk, self.link_flags.IsUnicode)
1608 if self.link_flags.HasRelativePath:
1609 write_sized_string(self.relative_path, lnk, self.link_flags.IsUnicode)
1610 if self.link_flags.HasWorkingDir:
1611 write_sized_string(self.work_dir, lnk, self.link_flags.IsUnicode)
1612 if self.link_flags.HasArguments:
1613 write_sized_string(self.arguments, lnk, self.link_flags.IsUnicode)
1614 if self.link_flags.HasIconLocation:
1615 write_sized_string(self.icon, lnk, self.link_flags.IsUnicode)
1616 if self.extra_data:
1617 lnk.write(self.extra_data.bytes)
1618 else:
1619 lnk.write(b'\x00\x00\x00\x00')
1620
1621 def _get_shell_item_id_list(self):
1622 return self._shell_item_id_list
1623
1624 def _set_shell_item_id_list(self, shell_item_id_list):
1625 self._shell_item_id_list = shell_item_id_list
1626 self.link_flags.HasLinkTargetIDList = shell_item_id_list is not None
1627 shell_item_id_list = property(_get_shell_item_id_list, _set_shell_item_id_list)
1628
1629 def _get_link_info(self):
1630 return self._link_info
1631
1632 def _set_link_info(self, link_info):
1633 self._link_info = link_info
1634 self.link_flags.ForceNoLinkInfo = link_info is None
1635 self.link_flags.HasLinkInfo = link_info is not None
1636 link_info = property(_get_link_info, _set_link_info)
1637
1638 def _get_description(self):
1639 return self._description
1640
1641 def _set_description(self, description):
1642 self._description = description
1643 self.link_flags.HasName = description is not None
1644 description = property(_get_description, _set_description)
1645
1646 def _get_relative_path(self):
1647 return self._relative_path
1648
1649 def _set_relative_path(self, relative_path):
1650 self._relative_path = relative_path
1651 self.link_flags.HasRelativePath = relative_path is not None
1652 relative_path = property(_get_relative_path, _set_relative_path)
1653
1654 def _get_work_dir(self):
1655 return self._work_dir
1656
1657 def _set_work_dir(self, work_dir):
1658 self._work_dir = work_dir
1659 self.link_flags.HasWorkingDir = work_dir is not None
1660 work_dir = working_dir = property(_get_work_dir, _set_work_dir)
1661
1662 def _get_arguments(self):
1663 return self._arguments
1664
1665 def _set_arguments(self, arguments):
1666 self._arguments = arguments
1667 self.link_flags.HasArguments = arguments is not None
1668 arguments = property(_get_arguments, _set_arguments)
1669
1670 def _get_icon(self):
1671 return self._icon
1672
1673 def _set_icon(self, icon):
1674 self._icon = icon
1675 self.link_flags.HasIconLocation = icon is not None
1676 icon = property(_get_icon, _set_icon)
1677
1678 def _get_window_mode(self):
1679 return self._show_command
1680
1681 def _set_window_mode(self, value):
1682 if value not in list(_SHOW_COMMANDS.values()):
1683 raise ValueError("Not a valid window mode: %s. Choose any of pylnk.WINDOW_*" % value)
1684 self._show_command = value
1685 window_mode = show_command = property(_get_window_mode, _set_window_mode)
1686
1687 @property
1688 def path(self):
1689 # lnk can contains several different paths at different structures
1690 # here is some logic consistent with link properties at explorer (at least on test examples)
1691
1692 link_info_path = self._link_info.path if self._link_info and self._link_info.path else None
1693 id_list_path = self._shell_item_id_list.get_path() if hasattr(self, '_shell_item_id_list') else None
1694
1695 env_var_path = None
1696 if self.extra_data and self.extra_data.blocks:
1697 for block in self.extra_data.blocks:
1698 if type(block) == ExtraData_EnvironmentVariableDataBlock:
1699 env_var_path = block.target_unicode.strip('\x00') or block.target_ansi.strip('\x00')
1700 break
1701
1702 if id_list_path and id_list_path.startswith('%MY_COMPUTER%'):
1703 # full local path has priority
1704 return id_list_path[14:]
1705 if id_list_path and id_list_path.startswith('%USERPROFILE%\\::'):
1706 # path to KNOWN_FOLDER also has priority over link_info
1707 return id_list_path[14:]
1708 if link_info_path:
1709 # local path at link_info_path has priority over network path at id_list_path
1710 # full local path at link_info_path has priority over partial path at id_list_path
1711 return link_info_path
1712 if env_var_path:
1713 # some links in Recent folder contains path only at ExtraData_EnvironmentVariableDataBlock
1714 return env_var_path
1715 return id_list_path
1716
1717 def specify_local_location(self, path, drive_type=None, drive_serial=None, volume_label=None):
1718 self._link_info.drive_type = drive_type or DRIVE_UNKNOWN
1719 self._link_info.drive_serial = drive_serial or ''
1720 self._link_info.volume_label = volume_label or ''
1721 self._link_info.local_base_path = path
1722 self._link_info.local = True
1723 self._link_info.make_path()
1724
1725 def specify_remote_location(self, network_share_name, base_name):
1726 self._link_info.network_share_name = network_share_name
1727 self._link_info.base_name = base_name
1728 self._link_info.remote = True
1729 self._link_info.make_path()
1730
1731 def __str__(self):
1732 s = "Target file:\n"
1733 s += str(self.file_flags)
1734 s += "\nCreation Time: %s" % self.creation_time
1735 s += "\nModification Time: %s" % self.modification_time
1736 s += "\nAccess Time: %s" % self.access_time
1737 s += "\nFile size: %s" % self.file_size
1738 s += "\nWindow mode: %s" % self._show_command
1739 s += "\nHotkey: %s\n" % self.hot_key
1740 s += str(self._link_info)
1741 if self.link_flags.HasLinkTargetIDList:
1742 s += "\n%s" % self.shell_item_id_list
1743 if self.link_flags.HasName:
1744 s += "\nDescription: %s" % self.description
1745 if self.link_flags.HasRelativePath:
1746 s += "\nRelative Path: %s" % self.relative_path
1747 if self.link_flags.HasWorkingDir:
1748 s += "\nWorking Directory: %s" % self.work_dir
1749 if self.link_flags.HasArguments:
1750 s += "\nCommandline Arguments: %s" % self.arguments
1751 if self.link_flags.HasIconLocation:
1752 s += "\nIcon: %s" % self.icon
1753 if self._link_info:
1754 s += "\nUsed Path: %s" % self.path
1755 if self.extra_data:
1756 s += str(self.extra_data)
1757 return s
1758
1759
1760 # ---- convenience functions
1761
1762 def parse(lnk):
1763 return Lnk(lnk)
1764
1765
1766 def create(f=None):
1767 lnk = Lnk()
1768 lnk.file = f
1769 return lnk
1770
1771
1772 def for_file(
1773 target_file, lnk_name=None, arguments=None, description=None, icon_file=None, icon_index=0,
1774 work_dir=None, window_mode=None,
1775 ):
1776 lnk = create(lnk_name)
1777 lnk.link_flags.IsUnicode = True
1778 lnk.link_info = None
1779 if target_file.startswith('\\\\'):
1780 # remote link
1781 lnk.link_info = LinkInfo()
1782 lnk.link_info.remote = 1
1783 # extract server + share name from full path
1784 path_parts = target_file.split('\\')
1785 share_name, base_name = '\\'.join(path_parts[:4]), '\\'.join(path_parts[4:])
1786 lnk.link_info.network_share_name = share_name.upper()
1787 lnk.link_info.base_name = base_name
1788 # somehow it requires EnvironmentVariableDataBlock & HasExpString flag
1789 env_data_block = ExtraData_EnvironmentVariableDataBlock()
1790 env_data_block.target_ansi = target_file
1791 env_data_block.target_unicode = target_file
1792 lnk.extra_data = ExtraData(blocks=[env_data_block])
1793 lnk.link_flags.HasExpString = True
1794 else:
1795 # local link
1796 levels = list(path_levels(target_file))
1797 elements = [RootEntry(ROOT_MY_COMPUTER),
1798 DriveEntry(levels[0])]
1799 for level in levels[1:]:
1800 segment = PathSegmentEntry.create_for_path(level)
1801 elements.append(segment)
1802 lnk.shell_item_id_list = LinkTargetIDList()
1803 lnk.shell_item_id_list.items = elements
1804 # lnk.link_flags.HasLinkInfo = True
1805 if arguments:
1806 lnk.link_flags.HasArguments = True
1807 lnk.arguments = arguments
1808 if description:
1809 lnk.link_flags.HasName = True
1810 lnk.description = description
1811 if icon_file:
1812 lnk.link_flags.HasIconLocation = True
1813 lnk.icon = icon_file
1814 lnk.icon_index = icon_index
1815 if work_dir:
1816 lnk.link_flags.HasWorkingDir = True
1817 lnk.work_dir = work_dir
1818 if window_mode:
1819 lnk.window_mode = window_mode
1820 if lnk_name:
1821 lnk.save()
1822 return lnk
1823
1824
1825 def from_segment_list(data, lnk_name=None):
1826 """
1827 Creates a lnk file from a list of path segments.
1828 If lnk_name is given, the resulting lnk will be saved
1829 to a file with that name.
1830 The expected list for has the following format ("C:\\dir\\file.txt"):
1831
1832 ['c:\\',
1833 {'type': TYPE_FOLDER,
1834 'size': 0, # optional for folders
1835 'name': "dir",
1836 'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
1837 'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
1838 'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476)
1839 },
1840 {'type': TYPE_FILE,
1841 'size': 823,
1842 'name': "file.txt",
1843 'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
1844 'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
1845 'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476)
1846 }
1847 ]
1848
1849 For relative paths just omit the drive entry.
1850 Hint: Correct dates really are not crucial for working lnks.
1851 """
1852 if type(data) not in (list, tuple):
1853 raise ValueError("Invalid data format, list or tuple expected")
1854 lnk = Lnk()
1855 entries = []
1856 if is_drive(data[0]):
1857 # this is an absolute link
1858 entries.append(RootEntry(ROOT_MY_COMPUTER))
1859 if not data[0].endswith('\\'):
1860 data[0] += "\\"
1861 drive = data.pop(0).encode("ascii")
1862 entries.append(DriveEntry(drive))
1863 for level in data:
1864 segment = PathSegmentEntry()
1865 segment.type = level['type']
1866 if level['type'] == TYPE_FOLDER:
1867 segment.file_size = 0
1868 else:
1869 segment.file_size = level['size']
1870 segment.short_name = level['name']
1871 segment.full_name = level['name']
1872 segment.created = level['created']
1873 segment.modified = level['modified']
1874 segment.accessed = level['accessed']
1875 entries.append(segment)
1876 lnk.shell_item_id_list = LinkTargetIDList()
1877 lnk.shell_item_id_list.items = entries
1878 if data[-1]['type'] == TYPE_FOLDER:
1879 lnk.file_flags.directory = True
1880 if lnk_name:
1881 lnk.save(lnk_name)
1882 return lnk
1883
1884
1885 def build_uwp(
1886 package_family_name, target, location=None,logo44x44=None, lnk_name=None,
1887 ) -> Lnk:
1888 """
1889 :param lnk_name: ex.: crafted_uwp.lnk
1890 :param package_family_name: ex.: Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe
1891 :param target: ex.: Microsoft.WindowsCalculator_8wekyb3d8bbwe!App
1892 :param location: ex.: C:\\Program Files\\WindowsApps\\Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe
1893 :param logo44x44: ex.: Assets\\CalculatorAppList.png
1894 """
1895 lnk = Lnk()
1896 lnk.link_flags.HasLinkTargetIDList = True
1897 lnk.link_flags.IsUnicode = True
1898 lnk.link_flags.EnableTargetMetadata = True
1899
1900 lnk.shell_item_id_list = LinkTargetIDList()
1901
1902 elements = [
1903 RootEntry(ROOT_UWP_APPS),
1904 UwpSegmentEntry.create(
1905 package_family_name=package_family_name,
1906 target=target,
1907 location=location,
1908 logo44x44=logo44x44,
1909 )
1910 ]
1911 lnk.shell_item_id_list.items = elements
1912
1913 if lnk_name:
1914 lnk.file = lnk_name
1915 lnk.save()
1916 return lnk
1917
1918
1919 def get_prop(obj, prop_queue):
1920 attr = getattr(obj, prop_queue[0])
1921 if len(prop_queue) > 1:
1922 return get_prop(attr, prop_queue[1:])
1923 return attr
1924
1925
1926 def cli():
1927 parser = argparse.ArgumentParser(add_help=False)
1928 subparsers = parser.add_subparsers(dest='action', metavar='{p, c, d}')
1929 parser.add_argument('--help', '-h', action='store_true')
1930
1931 parser_parse = subparsers.add_parser('parse', aliases=['p'], help='read lnk file')
1932 parser_parse.add_argument('filename', help='lnk filename to read')
1933 parser_parse.add_argument('props', nargs='*', help='props path to read')
1934
1935 parser_create = subparsers.add_parser('create', aliases=['c'], help='create new lnk file')
1936 parser_create.add_argument('target', help='target path')
1937 parser_create.add_argument('name', help='lnk filename to create')
1938 parser_create.add_argument('--arguments', '-a', nargs='?', help='additional arguments')
1939 parser_create.add_argument('--description', '-d', nargs='?', help='description')
1940 parser_create.add_argument('--icon', '-i', nargs='?', help='icon filename')
1941 parser_create.add_argument('--icon-index', '-ii', type=int, default=0, nargs='?', help='icon index')
1942 parser_create.add_argument('--workdir', '-w', nargs='?', help='working directory')
1943 parser_create.add_argument('--mode', '-m', nargs='?', choices=['Maximized', 'Normal', 'Minimized'], help='window mode')
1944
1945 parser_dup = subparsers.add_parser('duplicate', aliases=['d'], help='read and write lnk file')
1946 parser_dup.add_argument('filename', help='lnk filename to read')
1947 parser_dup.add_argument('new_filename', help='new filename to write')
1948
1949 args = parser.parse_args()
1950 if args.help or not args.action:
1951 print('''
1952 Tool for read or create .lnk files
1953
1954 usage: pylnk3.py [p]arse / [c]reate ...
1955
1956 Examples:
1957 pylnk3 p filename.lnk
1958 pylnk3 c c:\\prog.exe shortcut.lnk
1959 pylnk3 c \\\\192.168.1.1\\share\\file.doc doc.lnk
1960 pylnk3 create c:\\1.txt text.lnk -m Minimized -d "Description"
1961
1962 for more info use help for each action (ex.: "pylnk3 create -h")
1963 '''.strip())
1964 exit(1)
1965
1966 if args.action in ['create', 'c']:
1967 for_file(
1968 args.target, args.name, arguments=args.arguments,
1969 description=args.description, icon_file=args.icon,
1970 icon_index=args.icon_index, work_dir=args.workdir,
1971 window_mode=args.mode,
1972 )
1973 elif args.action in ['parse', 'p']:
1974 lnk = parse(args.filename)
1975 props = args.props
1976 if len(props) == 0:
1977 print(lnk)
1978 else:
1979 for prop in props:
1980 print(get_prop(lnk, prop.split('.')))
1981 elif args.action in ['d', 'duplicate']:
1982 lnk = parse(args.filename)
1983 new_filename = args.new_filename
1984 print(lnk)
1985 lnk.save(new_filename)
1986 print('saved')
1987
1988
1989 if __name__ == '__main__':
1990 cli()
0 [egg_info]
1 tag_build =
2 tag_date = 0
3
0 import setuptools
1 from distutils.core import setup
0 from setuptools import setup
21
32
43 with open("README.md", "r") as fh:
65
76 setup(
87 name="pylnk3",
9 version="0.2.1",
10 py_modules=["pylnk"],
8 version="0.4.2",
9 py_modules=["pylnk3"],
10 entry_points={
11 'console_scripts': [
12 'pylnk3 = pylnk3:cli',
13 ],
14 },
1115 description="Windows LNK File Parser and Creator",
1216 author="strayge",
1317 author_email="[email protected]",
1519 keywords=["lnk", "shortcut", "windows"],
1620 license="GNU Library or Lesser General Public License (LGPL)",
1721 classifiers=[
18 "Programming Language :: Python :: 3",
22 "Programming Language :: Python :: 3.6",
23 "Programming Language :: Python :: 3.7",
24 "Programming Language :: Python :: 3.8",
25 "Programming Language :: Python :: 3.9",
1926 "Intended Audience :: Developers",
2027 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
2128 "Operating System :: OS Independent",
2229 "Topic :: Software Development :: Libraries :: Python Modules",
2330 ],
24 python_requires='>=3',
31 python_requires='>=3.6',
2532 long_description=long_description,
2633 long_description_content_type="text/markdown",
2734 )