Import upstream version 0.4.2
Kali Janitor
2 years ago
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 | 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/) | |
1 | 5 | |
2 | 6 | Python library for reading and writing Windows shortcut files (.lnk). |
3 | 7 | Converted to support python 3. |
12 | 16 | Limitation: Windows knows lots of different types of shortcuts which all have |
13 | 17 | different formats. This library currently only supports shortcuts to files and |
14 | 18 | 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 | #!/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 | pylnk3 |
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 | import setuptools | |
1 | from distutils.core import setup | |
0 | from setuptools import setup | |
2 | 1 | |
3 | 2 | |
4 | 3 | with open("README.md", "r") as fh: |
6 | 5 | |
7 | 6 | setup( |
8 | 7 | 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 | }, | |
11 | 15 | description="Windows LNK File Parser and Creator", |
12 | 16 | author="strayge", |
13 | 17 | author_email="[email protected]", |
15 | 19 | keywords=["lnk", "shortcut", "windows"], |
16 | 20 | license="GNU Library or Lesser General Public License (LGPL)", |
17 | 21 | 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", | |
19 | 26 | "Intended Audience :: Developers", |
20 | 27 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", |
21 | 28 | "Operating System :: OS Independent", |
22 | 29 | "Topic :: Software Development :: Libraries :: Python Modules", |
23 | 30 | ], |
24 | python_requires='>=3', | |
31 | python_requires='>=3.6', | |
25 | 32 | long_description=long_description, |
26 | 33 | long_description_content_type="text/markdown", |
27 | 34 | ) |