New upstream release.
Kali Janitor
1 year, 5 months ago
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 | Metadata-Version: 2.1 | |
1 | Name: pylnk3 | |
2 | Version: 1.0.0a1 | |
3 | Summary: Windows LNK File Parser and Creator | |
4 | License: GNU Library or Lesser General Public License (LGPL) | |
5 | Keywords: lnk,shortcut,windows | |
6 | Author-email: strayge <[email protected]> | |
7 | Requires-Python: >=3.7 | |
8 | Classifier: Intended Audience :: Developers | |
9 | Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) | |
10 | Classifier: Operating System :: OS Independent | |
11 | Classifier: Programming Language :: Python :: 3.10 | |
12 | Classifier: Programming Language :: Python :: 3.11 | |
13 | Classifier: Programming Language :: Python :: 3.7 | |
14 | Classifier: Programming Language :: Python :: 3.8 | |
15 | Classifier: Programming Language :: Python :: 3.9 | |
16 | Classifier: Topic :: Software Development :: Libraries :: Python Modules | |
17 | Project-URL: homepage, https://github.com/strayge/pylnk | |
18 | Description-Content-Type: text/markdown | |
19 | ||
20 | # PyLnk 3 | |
21 | ||
22 | [![PyPI version shields.io](https://img.shields.io/pypi/v/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/) | |
23 | [![PyPI pyversions](https://img.shields.io/pypi/pyversions/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/) | |
24 | [![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/) | |
25 | ||
26 | Python library for reading and writing Windows shortcut files (.lnk). | |
27 | Converted to support python 3. | |
28 | [Original](https://sourceforge.net/p/pylnk/code/HEAD/tree/trunk/pylnk.py) version written by Tim-Christian Mundt. | |
29 | ||
30 | This library can parse .lnk files and extract all relevant information from | |
31 | them. Parsing a .lnk file yields a LNK object which can be altered and saved | |
32 | again. Moreover, .lnk file can be created from scratch be creating a LNK | |
33 | object, populating it with data and then saving it to a file. As that | |
34 | process requires some knowledge about the internals of .lnk files, some | |
35 | convenience functions are provided. | |
36 | ||
37 | Limitation: Windows knows lots of different types of shortcuts which all have | |
38 | different formats. This library currently only supports shortcuts to files and | |
39 | folders on the local machine. | |
40 | ||
41 | ## CLI | |
42 | ||
43 | Mainly tool has two basic commands. | |
44 | ||
45 | #### Parse existed lnk file | |
46 | ||
47 | ```help | |
48 | pylnk3 parse [-h] filename [props [props ...]] | |
49 | ||
50 | positional arguments: | |
51 | filename lnk filename to read | |
52 | props props path to read | |
53 | ||
54 | optional arguments: | |
55 | -h, --help show this help message and exit | |
56 | ``` | |
57 | ||
58 | #### Create new lnk file | |
59 | ||
60 | ```help | |
61 | usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]] | |
62 | [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]] | |
63 | target name | |
64 | ||
65 | positional arguments: | |
66 | target target path | |
67 | name lnk filename to create | |
68 | ||
69 | optional arguments: | |
70 | -h, --help show this help message and exit | |
71 | --arguments [ARGUMENTS], -a [ARGUMENTS] | |
72 | additional arguments | |
73 | --description [DESCRIPTION], -d [DESCRIPTION] | |
74 | description | |
75 | --icon [ICON], -i [ICON] | |
76 | icon filename | |
77 | --icon-index [ICON_INDEX], -ii [ICON_INDEX] | |
78 | icon index | |
79 | --workdir [WORKDIR], -w [WORKDIR] | |
80 | working directory | |
81 | --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}] | |
82 | window mode | |
83 | --file threat target as file (by default guessed by dot in target) | |
84 | --directory threat target as directory (by default guessed by dot in target) | |
85 | ``` | |
86 | ||
87 | #### Examples | |
88 | ```sh | |
89 | # windows | |
90 | pylnk3 p filename.lnk | |
91 | pylnk3 c c:\prog.exe shortcut.lnk | |
92 | pylnk3 c \\192.168.1.1\share\file.doc doc.lnk | |
93 | pylnk3 create c:\1.txt text.lnk -m Minimized -d "Description" | |
94 | # linux (escaped backslashes) | |
95 | pylnk3 create 'c:\\dir\\file.txt' text.lnk -m Minimized -d "Description" | |
96 | ``` | |
97 | ||
98 | ## Changes | |
99 | ||
100 | **1.0.0a1** | |
101 | split single `pylnk3.py` to separated modules | |
102 | [FIX] fixed building links for non-existing locally paths (guessing target type by dot in name) | |
103 | added `--file` / `--directory` create command options to override target type guessing | |
104 | ||
105 | **0.4.2** | |
106 | changed logic for Lnk.path choose (in case of different paths presents at different structures) | |
107 | read links with root as GUID of KNOWN_FOLDER | |
108 | [FIX] disabled padding for writing LinkInfo.local_base_path | |
109 | ||
110 | **0.4.0** | |
111 | added support for network links | |
112 | reworked CLI (added more options for creating links) | |
113 | added entry point for call tool just like `pylnk3` | |
114 | [FIX] allow build links for non-existed (from this machine) paths | |
115 | [FIX] correct building links on Linux (now expect Windows-like path) | |
116 | [FIX] fixed path priority at parsing with both local & remote presents | |
117 | ||
118 | **0.3.0** | |
119 | added support links to UWP apps | |
120 | ||
121 | **0.2.1** | |
122 | released to PyPI | |
123 | ||
124 | **0.2.0** | |
125 | converted to python 3 | |
126 |
4 | 4 | [![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/) |
5 | 5 | |
6 | 6 | Python library for reading and writing Windows shortcut files (.lnk). |
7 | Converted to support python 3. | |
7 | Converted to support python 3. | |
8 | [Original](https://sourceforge.net/p/pylnk/code/HEAD/tree/trunk/pylnk.py) version written by Tim-Christian Mundt. | |
8 | 9 | |
9 | 10 | This library can parse .lnk files and extract all relevant information from |
10 | 11 | them. Parsing a .lnk file yields a LNK object which can be altered and saved |
15 | 16 | |
16 | 17 | Limitation: Windows knows lots of different types of shortcuts which all have |
17 | 18 | different formats. This library currently only supports shortcuts to files and |
18 | folders on the local machine. | |
19 | folders on the local machine. | |
19 | 20 | |
20 | 21 | ## CLI |
21 | 22 | |
23 | 24 | |
24 | 25 | #### Parse existed lnk file |
25 | 26 | |
26 | ```sh | |
27 | ```help | |
27 | 28 | pylnk3 parse [-h] filename [props [props ...]] |
28 | 29 | |
29 | 30 | positional arguments: |
36 | 37 | |
37 | 38 | #### Create new lnk file |
38 | 39 | |
39 | ```sh | |
40 | ```help | |
40 | 41 | usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]] |
41 | 42 | [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]] |
42 | 43 | target name |
59 | 60 | working directory |
60 | 61 | --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}] |
61 | 62 | window mode |
63 | --file threat target as file (by default guessed by dot in target) | |
64 | --directory threat target as directory (by default guessed by dot in target) | |
62 | 65 | ``` |
63 | 66 | |
64 | 67 | #### Examples |
65 | 68 | ```sh |
69 | # windows | |
66 | 70 | pylnk3 p filename.lnk |
67 | 71 | pylnk3 c c:\prog.exe shortcut.lnk |
68 | 72 | pylnk3 c \\192.168.1.1\share\file.doc doc.lnk |
69 | 73 | pylnk3 create c:\1.txt text.lnk -m Minimized -d "Description" |
74 | # linux (escaped backslashes) | |
75 | pylnk3 create 'c:\\dir\\file.txt' text.lnk -m Minimized -d "Description" | |
70 | 76 | ``` |
71 | 77 | |
72 | 78 | ## Changes |
79 | ||
80 | **1.0.0a1** | |
81 | split single `pylnk3.py` to separated modules | |
82 | [FIX] fixed building links for non-existing locally paths (guessing target type by dot in name) | |
83 | added `--file` / `--directory` create command options to override target type guessing | |
73 | 84 | |
74 | 85 | **0.4.2** |
75 | 86 | changed logic for Lnk.path choose (in case of different paths presents at different structures) |
84 | 95 | [FIX] correct building links on Linux (now expect Windows-like path) |
85 | 96 | [FIX] fixed path priority at parsing with both local & remote presents |
86 | 97 | |
87 | ||
88 | 98 | **0.3.0** |
89 | 99 | added support links to UWP apps |
90 | 100 | |
101 | **0.2.1** | |
102 | released to PyPI | |
91 | 103 | |
92 | **0.2.1** | |
93 | released to PyPI | |
94 | ||
95 | ||
96 | 104 | **0.2.0** |
97 | 105 | converted to python 3 |
0 | pylnk (1.0.0a1-0kali1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream release. | |
3 | ||
4 | -- Kali Janitor <[email protected]> Thu, 15 Dec 2022 11:09:12 -0000 | |
5 | ||
0 | 6 | pylnk (0.4.2-0kali2) kali-dev; urgency=medium |
1 | 7 | |
2 | 8 | * Rename the binary package python3-pylnk3: now upstream installs pylnk3.py |
0 | from pylnk3.structures import Lnk |
0 | import argparse | |
1 | from typing import Any, List | |
2 | ||
3 | from pylnk3.helpers import for_file, parse | |
4 | ||
5 | ||
6 | def get_prop(obj: Any, prop_queue: List[str]) -> Any: | |
7 | attr = getattr(obj, prop_queue[0]) | |
8 | if len(prop_queue) > 1: | |
9 | return get_prop(attr, prop_queue[1:]) | |
10 | return attr | |
11 | ||
12 | ||
13 | HELP = ''' | |
14 | Tool for read or create .lnk files | |
15 | ||
16 | usage: pylnk3.py [p]arse / [c]reate ... | |
17 | ||
18 | Examples: | |
19 | pylnk3 p filename.lnk | |
20 | pylnk3 c c:\\prog.exe shortcut.lnk | |
21 | pylnk3 c \\\\192.168.1.1\\share\\file.doc doc.lnk | |
22 | pylnk3 create c:\\1.txt text.lnk -m Minimized -d "Description" | |
23 | ||
24 | for more info use help for each action (ex.: "pylnk3 create -h") | |
25 | ''' | |
26 | ||
27 | ||
28 | def main() -> None: | |
29 | parser = argparse.ArgumentParser(add_help=False, prog='pylnk3') | |
30 | subparsers = parser.add_subparsers(dest='action', metavar='{p, c, d}') | |
31 | parser.add_argument('--help', '-h', action='store_true') | |
32 | ||
33 | parser_parse = subparsers.add_parser('parse', aliases=['p'], help='read lnk file') | |
34 | parser_parse.add_argument('filename', help='lnk filename to read') | |
35 | parser_parse.add_argument('props', nargs='*', help='props path to read') | |
36 | ||
37 | parser_create = subparsers.add_parser('create', aliases=['c'], help='create new lnk file') | |
38 | parser_create.add_argument('target', help='target path') | |
39 | parser_create.add_argument('name', help='lnk filename to create') | |
40 | parser_create.add_argument('--arguments', '-a', nargs='?', help='additional arguments') | |
41 | parser_create.add_argument('--description', '-d', nargs='?', help='description') | |
42 | parser_create.add_argument('--icon', '-i', nargs='?', help='icon filename') | |
43 | parser_create.add_argument('--icon-index', '-ii', type=int, default=0, nargs='?', help='icon index') | |
44 | parser_create.add_argument('--workdir', '-w', nargs='?', help='working directory') | |
45 | parser_create.add_argument('--mode', '-m', nargs='?', choices=['Maximized', 'Normal', 'Minimized'], help='window mode') | |
46 | parser_create.add_argument('--file', action='store_true', help='threat target as file (by default guessed by dot in target)') | |
47 | parser_create.add_argument( | |
48 | '--directory', action='store_true', help='threat target as directory (by default guessed by dot in target)', | |
49 | ) | |
50 | ||
51 | parser_dup = subparsers.add_parser('duplicate', aliases=['d'], help='read and write lnk file') | |
52 | parser_dup.add_argument('filename', help='lnk filename to read') | |
53 | parser_dup.add_argument('new_filename', help='new filename to write') | |
54 | ||
55 | args = parser.parse_args() | |
56 | if args.help or not args.action: | |
57 | print(HELP.strip()) | |
58 | exit(1) | |
59 | ||
60 | if args.action in ('create', 'c'): | |
61 | is_file = None | |
62 | if args.file: | |
63 | is_file = True | |
64 | elif args.directory: | |
65 | is_file = False | |
66 | for_file( | |
67 | args.target, args.name, arguments=args.arguments, | |
68 | description=args.description, icon_file=args.icon, | |
69 | icon_index=args.icon_index, work_dir=args.workdir, | |
70 | window_mode=args.mode, | |
71 | is_file=is_file, | |
72 | ) | |
73 | elif args.action in ('parse', 'p'): | |
74 | lnk = parse(args.filename) | |
75 | props = args.props | |
76 | if len(props) == 0: | |
77 | print(lnk) | |
78 | else: | |
79 | for prop in props: | |
80 | print(get_prop(lnk, prop.split('.'))) | |
81 | elif args.action in ('d', 'duplicate'): | |
82 | lnk = parse(args.filename) | |
83 | new_filename = args.new_filename | |
84 | print(lnk) | |
85 | lnk.save(new_filename) | |
86 | print('saved') |
0 | class FormatException(Exception): | |
1 | pass | |
2 | ||
3 | ||
4 | class MissingInformationException(Exception): | |
5 | pass | |
6 | ||
7 | ||
8 | class InvalidKeyException(Exception): | |
9 | pass |
0 | from pprint import pformat | |
1 | from typing import Any, Dict, Tuple | |
2 | ||
3 | _MODIFIER_KEYS = ('SHIFT', 'CONTROL', 'ALT') | |
4 | ||
5 | ||
6 | class Flags: | |
7 | ||
8 | def __init__(self, flag_names: Tuple[str, ...], flags_bytes: int = 0) -> None: | |
9 | self._flag_names = flag_names | |
10 | self._flags: Dict[str, bool] = dict([(name, False) for name in flag_names]) | |
11 | self.set_flags(flags_bytes) | |
12 | ||
13 | def set_flags(self, flags_bytes: int) -> None: | |
14 | for pos, flag_name in enumerate(self._flag_names): | |
15 | self._flags[flag_name] = bool(flags_bytes >> pos & 0x1) | |
16 | ||
17 | @property | |
18 | def bytes(self) -> int: | |
19 | result = 0 | |
20 | for pos in range(len(self._flag_names)): | |
21 | result = (self._flags[self._flag_names[pos]] and 1 or 0) << pos | result | |
22 | return result | |
23 | ||
24 | def __getitem__(self, key: str) -> Any: | |
25 | if key in self._flags: | |
26 | return object.__getattribute__(self, '_flags')[key] | |
27 | return object.__getattribute__(self, key) | |
28 | ||
29 | def __setitem__(self, key: str, value: bool) -> None: | |
30 | if key not in self._flags: | |
31 | raise KeyError("The key '%s' is not defined for those flags." % key) | |
32 | self._flags[key] = value | |
33 | ||
34 | def __getattr__(self, key: str) -> Any: | |
35 | if key in self._flags: | |
36 | return object.__getattribute__(self, '_flags')[key] | |
37 | return object.__getattribute__(self, key) | |
38 | ||
39 | def __setattr__(self, key: str, value: Any) -> None: | |
40 | if ('_flags' not in self.__dict__) or (key in self.__dict__): | |
41 | object.__setattr__(self, key, value) | |
42 | else: | |
43 | self.__setitem__(key, value) | |
44 | ||
45 | def __str__(self) -> str: | |
46 | return pformat(self._flags, indent=2) | |
47 | ||
48 | ||
49 | class ModifierKeys(Flags): | |
50 | ||
51 | def __init__(self, flags_bytes: int = 0) -> None: | |
52 | Flags.__init__(self, _MODIFIER_KEYS, flags_bytes) | |
53 | ||
54 | def __str__(self) -> str: | |
55 | s = "" | |
56 | s += self.CONTROL and "CONTROL+" or "" | |
57 | s += self.SHIFT and "SHIFT+" or "" | |
58 | s += self.ALT and "ALT+" or "" | |
59 | return s |
0 | import ntpath | |
1 | import re | |
2 | from typing import Any, Dict, Iterable, List, Optional, Union | |
3 | ||
4 | from pylnk3.structures import ( | |
5 | DriveEntry, ExtraData, ExtraData_EnvironmentVariableDataBlock, IDListEntry, LinkInfo, | |
6 | LinkTargetIDList, Lnk, PathSegmentEntry, RootEntry, UwpSegmentEntry, | |
7 | ) | |
8 | from pylnk3.structures.id_list.path import TYPE_FOLDER | |
9 | from pylnk3.structures.id_list.root import ROOT_MY_COMPUTER, ROOT_UWP_APPS | |
10 | ||
11 | # def is_lnk(f: BytesIO) -> bool: | |
12 | # if hasattr(f, 'name'): | |
13 | # if f.name.split(os.path.extsep)[-1] == "lnk": | |
14 | # assert_lnk_signature(f) | |
15 | # return True | |
16 | # else: | |
17 | # return False | |
18 | # else: | |
19 | # try: | |
20 | # assert_lnk_signature(f) | |
21 | # return True | |
22 | # except FormatException: | |
23 | # return False | |
24 | ||
25 | ||
26 | def path_levels(p: str) -> Iterable[str]: | |
27 | dirname, base = ntpath.split(p) | |
28 | if base != '': | |
29 | yield from path_levels(dirname) | |
30 | yield p | |
31 | ||
32 | ||
33 | def is_drive(data: Union[str, Any]) -> bool: | |
34 | if not isinstance(data, str): | |
35 | return False | |
36 | p = re.compile("[a-zA-Z]:\\\\?$") | |
37 | return p.match(data) is not None | |
38 | ||
39 | ||
40 | def parse(lnk: str) -> Lnk: | |
41 | return Lnk(lnk) | |
42 | ||
43 | ||
44 | def create(f: Optional[str] = None) -> Lnk: | |
45 | lnk = Lnk() | |
46 | lnk.file = f | |
47 | return lnk | |
48 | ||
49 | ||
50 | def for_file( | |
51 | target_file: str, | |
52 | lnk_name: Optional[str] = None, | |
53 | arguments: Optional[str] = None, | |
54 | description: Optional[str] = None, | |
55 | icon_file: Optional[str] = None, | |
56 | icon_index: int = 0, | |
57 | work_dir: Optional[str] = None, | |
58 | window_mode: Optional[str] = None, | |
59 | is_file: Optional[bool] = None, | |
60 | ) -> Lnk: | |
61 | lnk = create(lnk_name) | |
62 | lnk.link_flags.IsUnicode = True | |
63 | lnk.link_info = None | |
64 | if target_file.startswith('\\\\'): | |
65 | # remote link | |
66 | lnk.link_info = LinkInfo() | |
67 | lnk.link_info.remote = 1 | |
68 | # extract server + share name from full path | |
69 | path_parts = target_file.split('\\') | |
70 | share_name, base_name = '\\'.join(path_parts[:4]), '\\'.join(path_parts[4:]) | |
71 | lnk.link_info.network_share_name = share_name.upper() | |
72 | lnk.link_info.base_name = base_name | |
73 | # somehow it requires EnvironmentVariableDataBlock & HasExpString flag | |
74 | env_data_block = ExtraData_EnvironmentVariableDataBlock() | |
75 | env_data_block.target_ansi = target_file | |
76 | env_data_block.target_unicode = target_file | |
77 | lnk.extra_data = ExtraData(blocks=[env_data_block]) | |
78 | lnk.link_flags.HasExpString = True | |
79 | else: | |
80 | # local link | |
81 | levels = list(path_levels(target_file)) | |
82 | elements = [ | |
83 | RootEntry(ROOT_MY_COMPUTER), | |
84 | DriveEntry(levels[0]), | |
85 | ] | |
86 | for level in levels[1:]: | |
87 | is_last_level = level == levels[-1] | |
88 | # consider all segments before last as directory | |
89 | segment = PathSegmentEntry.create_for_path(level, is_file=is_file if is_last_level else False) | |
90 | elements.append(segment) | |
91 | lnk.shell_item_id_list = LinkTargetIDList() | |
92 | lnk.shell_item_id_list.items = elements | |
93 | # lnk.link_flags.HasLinkInfo = True | |
94 | if arguments: | |
95 | lnk.link_flags.HasArguments = True | |
96 | lnk.arguments = arguments | |
97 | if description: | |
98 | lnk.link_flags.HasName = True | |
99 | lnk.description = description | |
100 | if icon_file: | |
101 | lnk.link_flags.HasIconLocation = True | |
102 | lnk.icon = icon_file | |
103 | lnk.icon_index = icon_index | |
104 | if work_dir: | |
105 | lnk.link_flags.HasWorkingDir = True | |
106 | lnk.work_dir = work_dir | |
107 | if window_mode: | |
108 | lnk.window_mode = window_mode | |
109 | if lnk_name: | |
110 | lnk.save() | |
111 | return lnk | |
112 | ||
113 | ||
114 | def from_segment_list( | |
115 | data: List[Union[str, Dict[str, Any]]], | |
116 | lnk_name: Optional[str] = None, | |
117 | ) -> Lnk: | |
118 | """ | |
119 | Creates a lnk file from a list of path segments. | |
120 | If lnk_name is given, the resulting lnk will be saved | |
121 | to a file with that name. | |
122 | The expected list for has the following format ("C:\\dir\\file.txt"): | |
123 | ||
124 | ['c:\\', | |
125 | {'type': TYPE_FOLDER, | |
126 | 'size': 0, # optional for folders | |
127 | 'name': "dir", | |
128 | 'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476), | |
129 | 'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476), | |
130 | 'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476) | |
131 | }, | |
132 | {'type': TYPE_FILE, | |
133 | 'size': 823, | |
134 | 'name': "file.txt", | |
135 | 'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476), | |
136 | 'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476), | |
137 | 'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476) | |
138 | } | |
139 | ] | |
140 | ||
141 | For relative paths just omit the drive entry. | |
142 | Hint: Correct dates really are not crucial for working lnks. | |
143 | """ | |
144 | if not isinstance(data, (list, tuple)): | |
145 | raise ValueError("Invalid data format, list or tuple expected") | |
146 | lnk = Lnk() | |
147 | entries: List[IDListEntry] = [] | |
148 | if is_drive(data[0]): | |
149 | assert isinstance(data[0], str) | |
150 | # this is an absolute link | |
151 | entries.append(RootEntry(ROOT_MY_COMPUTER)) | |
152 | if not data[0].endswith('\\'): | |
153 | data[0] += "\\" | |
154 | drive = data[0].encode("ascii") | |
155 | data.pop(0) | |
156 | entries.append(DriveEntry(drive)) | |
157 | data_without_root: List[Dict[str, Any]] = data # type: ignore | |
158 | for level in data_without_root: | |
159 | segment = PathSegmentEntry() | |
160 | segment.type = level['type'] | |
161 | if level['type'] == TYPE_FOLDER: | |
162 | segment.file_size = 0 | |
163 | else: | |
164 | segment.file_size = level['size'] | |
165 | segment.short_name = level['name'] | |
166 | segment.full_name = level['name'] | |
167 | segment.created = level['created'] | |
168 | segment.modified = level['modified'] | |
169 | segment.accessed = level['accessed'] | |
170 | entries.append(segment) | |
171 | lnk.shell_item_id_list = LinkTargetIDList() | |
172 | lnk.shell_item_id_list.items = entries | |
173 | if data_without_root[-1]['type'] == TYPE_FOLDER: | |
174 | lnk.file_flags.directory = True | |
175 | if lnk_name: | |
176 | lnk.save(lnk_name) | |
177 | return lnk | |
178 | ||
179 | ||
180 | def build_uwp( | |
181 | package_family_name: str, | |
182 | target: str, | |
183 | location: Optional[str] = None, | |
184 | logo44x44: Optional[str] = None, | |
185 | lnk_name: Optional[str] = None, | |
186 | ) -> Lnk: | |
187 | """ | |
188 | :param lnk_name: ex.: crafted_uwp.lnk | |
189 | :param package_family_name: ex.: Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe | |
190 | :param target: ex.: Microsoft.WindowsCalculator_8wekyb3d8bbwe!App | |
191 | :param location: ex.: C:\\Program Files\\WindowsApps\\Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe | |
192 | :param logo44x44: ex.: Assets\\CalculatorAppList.png | |
193 | """ | |
194 | lnk = Lnk() | |
195 | lnk.link_flags.HasLinkTargetIDList = True | |
196 | lnk.link_flags.IsUnicode = True | |
197 | lnk.link_flags.EnableTargetMetadata = True | |
198 | ||
199 | lnk.shell_item_id_list = LinkTargetIDList() | |
200 | ||
201 | elements = [ | |
202 | RootEntry(ROOT_UWP_APPS), | |
203 | UwpSegmentEntry.create( | |
204 | package_family_name=package_family_name, | |
205 | target=target, | |
206 | location=location, | |
207 | logo44x44=logo44x44, | |
208 | ), | |
209 | ] | |
210 | lnk.shell_item_id_list.items = elements | |
211 | ||
212 | if lnk_name: | |
213 | lnk.file = lnk_name | |
214 | lnk.save() | |
215 | return lnk |
0 | from pylnk3.structures.extra_data import ( | |
1 | ExtraData, ExtraData_DataBlock, ExtraData_EnvironmentVariableDataBlock, | |
2 | ExtraData_IconEnvironmentDataBlock, ExtraData_PropertyStoreDataBlock, ExtraData_Unparsed, | |
3 | PropertyStore, TypedPropertyValue, | |
4 | ) | |
5 | from pylnk3.structures.id_list.base import IDListEntry | |
6 | from pylnk3.structures.id_list.drive import DriveEntry | |
7 | from pylnk3.structures.id_list.id_list import LinkTargetIDList | |
8 | from pylnk3.structures.id_list.path import PathSegmentEntry | |
9 | from pylnk3.structures.id_list.root import RootEntry | |
10 | from pylnk3.structures.id_list.uwp import UwpMainBlock, UwpSegmentEntry | |
11 | from pylnk3.structures.link_info import LinkInfo | |
12 | from pylnk3.structures.lnk import Lnk |
0 | from io import BufferedIOBase, BytesIO | |
1 | from struct import unpack | |
2 | from typing import Any, Dict, List, Optional, Tuple, Type, Union | |
3 | ||
4 | from pylnk3.utils.data import convert_time_to_unix | |
5 | from pylnk3.utils.guid import guid_to_str | |
6 | from pylnk3.utils.padding import padding | |
7 | from pylnk3.utils.read_write import read_byte, read_int, read_short, write_int, write_short | |
8 | ||
9 | ||
10 | class TypedPropertyValue: | |
11 | # types: [MS-OLEPS] section 2.15 | |
12 | def __init__( | |
13 | self, | |
14 | bytes_: Optional[bytes] = None, | |
15 | type_: Optional[int] = None, | |
16 | value: Optional[bytes] = None, | |
17 | ) -> None: | |
18 | if bytes_ is not None: | |
19 | self.type: int = read_short(BytesIO(bytes_)) | |
20 | padding = bytes_[2:4] | |
21 | self.value: bytes = bytes_[4:] | |
22 | elif type_ is not None and value is not None: | |
23 | self.type = type_ | |
24 | self.value = value or b'' | |
25 | else: | |
26 | raise ValueError("Either bytes or type and value must be given.") | |
27 | ||
28 | def set_string(self, value: str) -> None: | |
29 | self.type = 0x1f | |
30 | buf = BytesIO() | |
31 | write_int(len(value) + 2, buf) | |
32 | buf.write(value.encode('utf-16-le')) | |
33 | # terminator (included in size) | |
34 | buf.write(b'\x00\x00\x00\x00') | |
35 | # padding (not included in size) | |
36 | if len(value) % 2: | |
37 | buf.write(b'\x00\x00') | |
38 | self.value = buf.getvalue() | |
39 | ||
40 | @property | |
41 | def bytes(self) -> bytes: | |
42 | buf = BytesIO() | |
43 | write_short(self.type, buf) | |
44 | write_short(0x0000, buf) | |
45 | buf.write(self.value) | |
46 | return buf.getvalue() | |
47 | ||
48 | def __str__(self) -> str: | |
49 | value = self.value | |
50 | if self.type == 0x1F: | |
51 | size = value[:4] | |
52 | value_str = value[4:].decode('utf-16-le') | |
53 | elif self.type == 0x15: | |
54 | value_str = unpack('<Q', value)[0] | |
55 | elif self.type == 0x13: | |
56 | value_str = unpack('<I', value)[0] | |
57 | elif self.type == 0x14: | |
58 | value_str = unpack('<q', value)[0] | |
59 | elif self.type == 0x16: | |
60 | value_str = unpack('<i', value)[0] | |
61 | elif self.type == 0x17: | |
62 | value_str = unpack('<I', value)[0] | |
63 | elif self.type == 0x48: | |
64 | value_str = guid_to_str(value) | |
65 | elif self.type == 0x40: | |
66 | # FILETIME (Packet Version) | |
67 | stream = BytesIO(value) | |
68 | low = read_int(stream) | |
69 | high = read_int(stream) | |
70 | num = (high << 32) + low | |
71 | value_str = str(convert_time_to_unix(num)) | |
72 | else: | |
73 | value_str = str(value) | |
74 | return f'{hex(self.type)}: {value_str!s}' | |
75 | ||
76 | ||
77 | class PropertyStore: | |
78 | def __init__( | |
79 | self, | |
80 | bytes: Optional[BytesIO] = None, | |
81 | properties: Optional[List[Tuple[Union[str, int], TypedPropertyValue]]] = None, | |
82 | format_id: Optional[bytes] = None, | |
83 | is_strings: bool = False, | |
84 | ) -> None: | |
85 | self.is_strings = is_strings | |
86 | self.format_id = format_id or b'' | |
87 | self._is_end = False | |
88 | self.properties: List[Tuple[Union[str, int], TypedPropertyValue]] = properties or [] | |
89 | if bytes: | |
90 | self.read(bytes) | |
91 | ||
92 | def read(self, bytes_io: BytesIO) -> None: | |
93 | buf = bytes_io | |
94 | size = read_int(buf) | |
95 | assert size < len(buf.getvalue()) | |
96 | if size == 0x00000000: | |
97 | self._is_end = True | |
98 | return | |
99 | version = read_int(buf) | |
100 | assert version == 0x53505331 | |
101 | self.format_id = buf.read(16) | |
102 | if self.format_id == b'\xD5\xCD\xD5\x05\x2E\x9C\x10\x1B\x93\x97\x08\x00\x2B\x2C\xF9\xAE': | |
103 | self.is_strings = True | |
104 | else: | |
105 | self.is_strings = False | |
106 | while True: | |
107 | # assert lnk.tell() < (start + size) | |
108 | value_size = read_int(buf) | |
109 | if value_size == 0x00000000: | |
110 | break | |
111 | if self.is_strings: | |
112 | name_size = read_int(buf) | |
113 | _ = read_byte(buf) # reserved | |
114 | name = buf.read(name_size).decode('utf-16-le') | |
115 | value = TypedPropertyValue(buf.read(value_size - 9)) | |
116 | self.properties.append((name, value)) | |
117 | else: | |
118 | value_id = read_int(buf) | |
119 | _ = read_byte(buf) # reserved | |
120 | value = TypedPropertyValue(buf.read(value_size - 9)) | |
121 | self.properties.append((value_id, value)) | |
122 | ||
123 | @property | |
124 | def bytes(self) -> bytes: | |
125 | size = 8 + len(self.format_id) | |
126 | properties = BytesIO() | |
127 | for name, value in self.properties: | |
128 | value_bytes = value.bytes | |
129 | if self.is_strings: | |
130 | assert isinstance(name, str) | |
131 | name_bytes = name.encode('utf-16-le') | |
132 | value_size = 9 + len(name_bytes) + len(value_bytes) | |
133 | write_int(value_size, properties) | |
134 | name_size = len(name_bytes) | |
135 | write_int(name_size, properties) | |
136 | properties.write(b'\x00') | |
137 | properties.write(name_bytes) | |
138 | else: | |
139 | assert isinstance(name, int) | |
140 | value_size = 9 + len(value_bytes) | |
141 | write_int(value_size, properties) | |
142 | write_int(name, properties) | |
143 | properties.write(b'\x00') | |
144 | properties.write(value_bytes) | |
145 | size += value_size | |
146 | ||
147 | write_int(0x00000000, properties) | |
148 | size += 4 | |
149 | ||
150 | buf = BytesIO() | |
151 | write_int(size, buf) | |
152 | write_int(0x53505331, buf) | |
153 | buf.write(self.format_id) | |
154 | buf.write(properties.getvalue()) | |
155 | ||
156 | return buf.getvalue() | |
157 | ||
158 | def __str__(self) -> str: | |
159 | s = ' PropertyStore' | |
160 | s += '\n FormatID: %s' % guid_to_str(self.format_id) | |
161 | for name, value in self.properties: | |
162 | s += '\n %3s = %s' % (name, str(value)) | |
163 | return s.strip() | |
164 | ||
165 | ||
166 | class ExtraData_DataBlock: | |
167 | def __init__(self, bytes: Optional[bytes] = None, **kwargs: Any) -> None: | |
168 | raise NotImplementedError | |
169 | ||
170 | def bytes(self) -> bytes: | |
171 | raise NotImplementedError | |
172 | ||
173 | ||
174 | class ExtraData_IconEnvironmentDataBlock(ExtraData_DataBlock): | |
175 | def __init__(self, bytes: Optional[bytes] = None) -> None: | |
176 | # self._size = None | |
177 | # self._signature = None | |
178 | self._signature = 0xA0000007 | |
179 | self.target_ansi: str = None # type: ignore[assignment] | |
180 | self.target_unicode: str = None # type: ignore[assignment] | |
181 | if bytes: | |
182 | self.read(bytes) | |
183 | ||
184 | def read(self, bytes: bytes) -> None: | |
185 | buf = BytesIO(bytes) | |
186 | # self._size = read_int(buf) | |
187 | # self._signature = read_int(buf) | |
188 | self.target_ansi = buf.read(260).decode('ansi') | |
189 | self.target_unicode = buf.read(520).decode('utf-16-le') | |
190 | ||
191 | def bytes(self) -> bytes: | |
192 | target_ansi = padding(self.target_ansi.encode(), 260) | |
193 | target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520) | |
194 | size = 8 + len(target_ansi) + len(target_unicode) | |
195 | assert self._signature == 0xA0000007 | |
196 | assert size == 0x00000314 | |
197 | buf = BytesIO() | |
198 | write_int(size, buf) | |
199 | write_int(self._signature, buf) | |
200 | buf.write(target_ansi) | |
201 | buf.write(target_unicode) | |
202 | return buf.getvalue() | |
203 | ||
204 | def __str__(self) -> str: | |
205 | target_ansi = self.target_ansi.replace('\x00', '') | |
206 | target_unicode = self.target_unicode.replace('\x00', '') | |
207 | s = f'IconEnvironmentDataBlock\n TargetAnsi: {target_ansi}\n TargetUnicode: {target_unicode}' | |
208 | return s | |
209 | ||
210 | ||
211 | EXTRA_DATA_TYPES = { | |
212 | 0xA0000002: 'ConsoleDataBlock', # size 0x000000CC | |
213 | 0xA0000004: 'ConsoleFEDataBlock', # size 0x0000000C | |
214 | 0xA0000006: 'DarwinDataBlock', # size 0x00000314 | |
215 | 0xA0000001: 'EnvironmentVariableDataBlock', # size 0x00000314 | |
216 | 0xA0000007: 'IconEnvironmentDataBlock', # size 0x00000314 | |
217 | 0xA000000B: 'KnownFolderDataBlock', # size 0x0000001C | |
218 | 0xA0000009: 'PropertyStoreDataBlock', # size >= 0x0000000C | |
219 | 0xA0000008: 'ShimDataBlock', # size >= 0x00000088 | |
220 | 0xA0000005: 'SpecialFolderDataBlock', # size 0x00000010 | |
221 | 0xA0000003: 'VistaAndAboveIDListDataBlock', # size 0x00000060 | |
222 | 0xA000000C: 'VistaIDListDataBlock', # size 0x00000173 | |
223 | } | |
224 | ||
225 | ||
226 | class ExtraData_Unparsed(ExtraData_DataBlock): | |
227 | def __init__( | |
228 | self, | |
229 | signature: int, | |
230 | bytes: Optional[bytes] = None, | |
231 | data: Optional[bytes] = None, | |
232 | ) -> None: | |
233 | self._signature = signature | |
234 | self._size = None | |
235 | if bytes is not None: | |
236 | self.data = bytes | |
237 | elif data is not None: | |
238 | self.data = data | |
239 | else: | |
240 | raise ValueError("Either bytes or data must be given.") | |
241 | ||
242 | # def read(self, bytes): | |
243 | # buf = BytesIO(bytes) | |
244 | # size = len(bytes) | |
245 | # # self._size = read_int(buf) | |
246 | # # self._signature = read_int(buf) | |
247 | # self.data = buf.read(self._size - 8) | |
248 | ||
249 | def bytes(self) -> bytes: | |
250 | buf = BytesIO() | |
251 | write_int(len(self.data) + 8, buf) | |
252 | write_int(self._signature, buf) | |
253 | buf.write(self.data) | |
254 | return buf.getvalue() | |
255 | ||
256 | def __str__(self) -> str: | |
257 | s = f'ExtraDataBlock\n signature {hex(self._signature)}\n data: {self.data!r}' | |
258 | return s | |
259 | ||
260 | ||
261 | class ExtraData_PropertyStoreDataBlock(ExtraData_DataBlock): | |
262 | def __init__( | |
263 | self, | |
264 | bytes: Optional[bytes] = None, | |
265 | stores: Optional[List[PropertyStore]] = None, | |
266 | ) -> None: | |
267 | self._size = None | |
268 | self._signature = 0xA0000009 | |
269 | self.stores = [] | |
270 | if stores: | |
271 | self.stores = stores | |
272 | if bytes: | |
273 | self.read(bytes) | |
274 | ||
275 | def read(self, bytes: bytes) -> None: | |
276 | buf = BytesIO(bytes) | |
277 | # self._size = read_int(buf) | |
278 | # self._signature = read_int(buf) | |
279 | # [MS-PROPSTORE] section 2.2 | |
280 | while True: | |
281 | prop_store = PropertyStore(buf) | |
282 | if prop_store._is_end: | |
283 | break | |
284 | self.stores.append(prop_store) | |
285 | ||
286 | def bytes(self) -> bytes: | |
287 | stores = b'' | |
288 | for prop_store in self.stores: | |
289 | stores += prop_store.bytes | |
290 | size = len(stores) + 8 + 4 | |
291 | ||
292 | assert self._signature == 0xA0000009 | |
293 | assert size >= 0x0000000C | |
294 | ||
295 | buf = BytesIO() | |
296 | write_int(size, buf) | |
297 | write_int(self._signature, buf) | |
298 | buf.write(stores) | |
299 | write_int(0x00000000, buf) | |
300 | return buf.getvalue() | |
301 | ||
302 | def __str__(self) -> str: | |
303 | s = 'PropertyStoreDataBlock' | |
304 | for prop_store in self.stores: | |
305 | s += '\n %s' % str(prop_store) | |
306 | return s | |
307 | ||
308 | ||
309 | class ExtraData_EnvironmentVariableDataBlock(ExtraData_DataBlock): | |
310 | def __init__(self, bytes: Optional[bytes] = None) -> None: | |
311 | self._signature = 0xA0000001 | |
312 | self.target_ansi = '' | |
313 | self.target_unicode = '' | |
314 | if bytes: | |
315 | self.read(bytes) | |
316 | ||
317 | def read(self, bytes: bytes) -> None: | |
318 | buf = BytesIO(bytes) | |
319 | self.target_ansi = buf.read(260).decode() | |
320 | self.target_unicode = buf.read(520).decode('utf-16-le') | |
321 | ||
322 | def bytes(self) -> bytes: | |
323 | target_ansi = padding(self.target_ansi.encode(), 260) | |
324 | target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520) | |
325 | size = 8 + len(target_ansi) + len(target_unicode) | |
326 | assert self._signature == 0xA0000001 | |
327 | assert size == 0x00000314 | |
328 | buf = BytesIO() | |
329 | write_int(size, buf) | |
330 | write_int(self._signature, buf) | |
331 | buf.write(target_ansi) | |
332 | buf.write(target_unicode) | |
333 | return buf.getvalue() | |
334 | ||
335 | def __str__(self) -> str: | |
336 | target_ansi = self.target_ansi.replace('\x00', '') | |
337 | target_unicode = self.target_unicode.replace('\x00', '') | |
338 | s = f'EnvironmentVariableDataBlock\n TargetAnsi: {target_ansi}\n TargetUnicode: {target_unicode}' | |
339 | return s | |
340 | ||
341 | ||
342 | EXTRA_DATA_TYPES_CLASSES: Dict[str, Type[ExtraData_DataBlock]] = { | |
343 | 'IconEnvironmentDataBlock': ExtraData_IconEnvironmentDataBlock, | |
344 | 'PropertyStoreDataBlock': ExtraData_PropertyStoreDataBlock, | |
345 | 'EnvironmentVariableDataBlock': ExtraData_EnvironmentVariableDataBlock, | |
346 | } | |
347 | ||
348 | ||
349 | class ExtraData: | |
350 | # EXTRA_DATA = *EXTRA_DATA_BLOCK TERMINAL_BLOCK | |
351 | def __init__(self, lnk: Optional[BufferedIOBase] = None, blocks: Optional[List[ExtraData_DataBlock]] = None) -> None: | |
352 | self.blocks = [] | |
353 | if blocks: | |
354 | self.blocks = blocks | |
355 | if lnk is None: | |
356 | return | |
357 | while True: | |
358 | size = read_int(lnk) | |
359 | if size < 4: # TerminalBlock | |
360 | break | |
361 | signature = read_int(lnk) | |
362 | bytes = lnk.read(size - 8) | |
363 | # lnk.seek(-8, 1) | |
364 | block_type = EXTRA_DATA_TYPES[signature] | |
365 | if block_type in EXTRA_DATA_TYPES_CLASSES: | |
366 | block_class = EXTRA_DATA_TYPES_CLASSES[block_type] | |
367 | block = block_class(bytes=bytes) | |
368 | else: | |
369 | block_class = ExtraData_Unparsed | |
370 | block = block_class(bytes=bytes, signature=signature) | |
371 | self.blocks.append(block) | |
372 | ||
373 | @property | |
374 | def bytes(self) -> bytes: | |
375 | result = b'' | |
376 | for block in self.blocks: | |
377 | result += block.bytes() | |
378 | result += b'\x00\x00\x00\x00' # TerminalBlock | |
379 | return result | |
380 | ||
381 | def __str__(self) -> str: | |
382 | s = '' | |
383 | for block in self.blocks: | |
384 | s += '\n' + str(block) | |
385 | return s |
0 | from abc import abstractmethod | |
1 | ||
2 | ||
3 | class IDListEntry: | |
4 | @property | |
5 | @abstractmethod | |
6 | def bytes(self) -> bytes: | |
7 | ... |
0 | import re | |
1 | from typing import Union | |
2 | ||
3 | from pylnk3.exceptions import FormatException | |
4 | from pylnk3.structures import IDListEntry | |
5 | ||
6 | _DRIVE_PATTERN = re.compile(r'(\w)[:/\\]*$') | |
7 | ||
8 | ||
9 | class DriveEntry(IDListEntry): | |
10 | ||
11 | def __init__(self, drive: Union[bytes, str]) -> None: | |
12 | if len(drive) == 23: | |
13 | assert isinstance(drive, bytes) | |
14 | # binary data from parsed lnk | |
15 | self.drive = drive[1:3] | |
16 | else: | |
17 | # text representation | |
18 | assert isinstance(drive, str) | |
19 | m = _DRIVE_PATTERN.match(drive.strip()) | |
20 | if m: | |
21 | drive = m.groups()[0].upper() + ':' | |
22 | self.drive = drive.encode() | |
23 | else: | |
24 | raise FormatException("This is not a valid drive: " + str(drive)) | |
25 | ||
26 | @property | |
27 | def bytes(self) -> bytes: | |
28 | drive = self.drive | |
29 | padded_str = drive + b'\\' + b'\x00' * 19 | |
30 | return b'\x2F' + padded_str | |
31 | # drive = self.drive | |
32 | # if isinstance(drive, str): | |
33 | # drive = drive.encode() | |
34 | # return b'/' + drive + b'\\' + b'\x00' * 19 | |
35 | ||
36 | def __str__(self) -> str: | |
37 | return f"<DriveEntry: {self.drive!r}>" |
0 | from io import BytesIO | |
1 | from typing import List, Optional | |
2 | ||
3 | from pylnk3.structures.id_list.base import IDListEntry | |
4 | from pylnk3.structures.id_list.drive import DriveEntry | |
5 | from pylnk3.structures.id_list.path import PathSegmentEntry | |
6 | from pylnk3.structures.id_list.root import ROOT_MY_COMPUTER, ROOT_NETWORK_PLACES, RootEntry | |
7 | from pylnk3.structures.id_list.uwp import UwpSegmentEntry | |
8 | from pylnk3.utils.read_write import read_short, write_short | |
9 | ||
10 | ||
11 | class LinkTargetIDList: | |
12 | ||
13 | def __init__(self, bytes: Optional[bytes] = None) -> None: | |
14 | self.items: List[IDListEntry] = [] | |
15 | if bytes is not None: | |
16 | buf = BytesIO(bytes) | |
17 | raw = [] | |
18 | entry_len = read_short(buf) | |
19 | while entry_len > 0: | |
20 | raw.append(buf.read(entry_len - 2)) # the length includes the size | |
21 | entry_len = read_short(buf) | |
22 | self._interpret(raw) | |
23 | ||
24 | def _interpret(self, raw: Optional[List[bytes]]) -> None: | |
25 | if not raw: | |
26 | return | |
27 | elif raw[0][0] == 0x1F: | |
28 | root_entry = RootEntry(raw[0]) | |
29 | self.items.append(root_entry) | |
30 | if root_entry.root == ROOT_MY_COMPUTER: | |
31 | if len(raw[1]) == 0x17: | |
32 | self.items.append(DriveEntry(raw[1])) | |
33 | elif raw[1][0:2] == b'\x2E\x80': # ROOT_KNOWN_FOLDER | |
34 | self.items.append(PathSegmentEntry(raw[1])) | |
35 | else: | |
36 | raise ValueError("This seems to be an absolute link which requires a drive as second element.") | |
37 | items = raw[2:] | |
38 | elif root_entry.root == ROOT_NETWORK_PLACES: | |
39 | raise NotImplementedError( | |
40 | "Parsing network lnks has not yet been implemented. " | |
41 | "If you need it just contact me and we'll see...", | |
42 | ) | |
43 | else: | |
44 | items = raw[1:] | |
45 | else: | |
46 | items = raw | |
47 | for item in items: | |
48 | if item[4:8] == b'APPS': | |
49 | self.items.append(UwpSegmentEntry(item)) | |
50 | else: | |
51 | self.items.append(PathSegmentEntry(item)) | |
52 | ||
53 | def get_path(self) -> str: | |
54 | segments: List[str] = [] | |
55 | for item in self.items: | |
56 | if type(item) == RootEntry: | |
57 | segments.append('%' + item.root + '%') | |
58 | elif type(item) == DriveEntry: | |
59 | segments.append(item.drive.decode()) | |
60 | elif type(item) == PathSegmentEntry: | |
61 | if item.full_name is not None: | |
62 | segments.append(item.full_name) | |
63 | else: | |
64 | segments.append(str(item)) | |
65 | return '\\'.join(segments) | |
66 | ||
67 | def _validate(self) -> None: | |
68 | if not len(self.items): | |
69 | return | |
70 | root_entry = self.items[0] | |
71 | if isinstance(root_entry, RootEntry) and root_entry.root == ROOT_MY_COMPUTER: | |
72 | second_entry = self.items[1] | |
73 | if isinstance(second_entry, DriveEntry): | |
74 | return | |
75 | if ( | |
76 | isinstance(second_entry, PathSegmentEntry) | |
77 | and second_entry.full_name is not None | |
78 | and second_entry.full_name.startswith('::') | |
79 | ): | |
80 | return | |
81 | raise ValueError("A drive is required for absolute lnks") | |
82 | ||
83 | @property | |
84 | def bytes(self) -> bytes: | |
85 | self._validate() | |
86 | out = BytesIO() | |
87 | for item in self.items: | |
88 | bytes = item.bytes | |
89 | write_short(len(bytes) + 2, out) # len + terminator | |
90 | out.write(bytes) | |
91 | out.write(b'\x00\x00') | |
92 | return out.getvalue() | |
93 | ||
94 | def __str__(self) -> str: | |
95 | string = '<LinkTargetIDList>:\n' | |
96 | for item in self.items: | |
97 | string += f' {item}\n' | |
98 | return string.strip() |
0 | import ntpath | |
1 | import os | |
2 | from datetime import datetime | |
3 | from io import BytesIO | |
4 | from typing import Optional | |
5 | ||
6 | from pylnk3.exceptions import MissingInformationException | |
7 | from pylnk3.structures.id_list.base import IDListEntry | |
8 | from pylnk3.utils.guid import bytes_from_guid, guid_from_bytes | |
9 | from pylnk3.utils.read_write import ( | |
10 | read_cstring, read_cunicode, read_dos_datetime, read_double, read_int, read_short, | |
11 | write_cstring, write_cunicode, write_dos_datetime, write_double, write_int, write_short, | |
12 | ) | |
13 | ||
14 | _ENTRY_TYPES = { | |
15 | 0x00: 'KNOWN_FOLDER', | |
16 | 0x31: 'FOLDER', | |
17 | 0x32: 'FILE', | |
18 | 0x35: 'FOLDER (UNICODE)', | |
19 | 0x36: 'FILE (UNICODE)', | |
20 | 0x802E: 'ROOT_KNOWN_FOLDER', | |
21 | # founded in doc, not tested | |
22 | 0x1f: 'ROOT_FOLDER', | |
23 | 0x61: 'URI', | |
24 | 0x71: 'CONTROL_PANEL', | |
25 | } | |
26 | _ENTRY_TYPE_IDS = dict((v, k) for k, v in _ENTRY_TYPES.items()) | |
27 | ||
28 | TYPE_FOLDER = 'FOLDER' | |
29 | TYPE_FILE = 'FILE' | |
30 | ||
31 | ||
32 | class PathSegmentEntry(IDListEntry): | |
33 | ||
34 | def __init__(self, bytes: Optional[bytes] = None) -> None: | |
35 | self.type = None | |
36 | self.file_size = None | |
37 | self.modified = None | |
38 | self.short_name = None | |
39 | self.created = None | |
40 | self.accessed = None | |
41 | self.full_name = None | |
42 | if bytes is None: | |
43 | return | |
44 | ||
45 | buf = BytesIO(bytes) | |
46 | self.type = _ENTRY_TYPES.get(read_short(buf), 'UNKNOWN') | |
47 | short_name_is_unicode = self.type.endswith('(UNICODE)') | |
48 | ||
49 | if self.type == 'ROOT_KNOWN_FOLDER': | |
50 | self.full_name = '::' + guid_from_bytes(buf.read(16)) | |
51 | # then followed Beef0026 structure: | |
52 | # short size | |
53 | # short version | |
54 | # int signature == 0xBEEF0026 | |
55 | # (16 bytes) created timestamp | |
56 | # (16 bytes) modified timestamp | |
57 | # (16 bytes) accessed timestamp | |
58 | return | |
59 | ||
60 | if self.type == 'KNOWN_FOLDER': | |
61 | _ = read_short(buf) # extra block size | |
62 | extra_signature = read_int(buf) | |
63 | if extra_signature == 0x23FEBBEE: | |
64 | _ = read_short(buf) # unknown | |
65 | _ = read_short(buf) # guid len | |
66 | # that format recognized by explorer | |
67 | self.full_name = '::' + guid_from_bytes(buf.read(16)) | |
68 | return | |
69 | ||
70 | self.file_size = read_int(buf) | |
71 | self.modified = read_dos_datetime(buf) | |
72 | unknown = read_short(buf) # FileAttributesL | |
73 | if short_name_is_unicode: | |
74 | self.short_name = read_cunicode(buf) | |
75 | else: | |
76 | self.short_name = read_cstring(buf, padding=True) | |
77 | extra_size = read_short(buf) | |
78 | extra_version = read_short(buf) | |
79 | extra_signature = read_int(buf) | |
80 | if extra_signature == 0xBEEF0004: | |
81 | # indicator_1 = read_short(buf) # see below | |
82 | # only_83 = read_short(buf) < 0x03 | |
83 | # unknown = read_short(buf) # 0x04 | |
84 | # self.is_unicode = read_short(buf) == 0xBeef | |
85 | self.created = read_dos_datetime(buf) # 4 bytes | |
86 | self.accessed = read_dos_datetime(buf) # 4 bytes | |
87 | offset_unicode = read_short(buf) # offset from start of extra_size | |
88 | # only_83_2 = offset_unicode >= indicator_1 or offset_unicode < 0x14 | |
89 | if extra_version >= 7: | |
90 | offset_ansi = read_short(buf) | |
91 | file_reference = read_double(buf) | |
92 | unknown2 = read_double(buf) | |
93 | long_string_size = 0 | |
94 | if extra_version >= 3: | |
95 | long_string_size = read_short(buf) | |
96 | if extra_version >= 9: | |
97 | unknown4 = read_int(buf) | |
98 | if extra_version >= 8: | |
99 | unknown5 = read_int(buf) | |
100 | if extra_version >= 3: | |
101 | self.full_name = read_cunicode(buf) | |
102 | if long_string_size > 0: | |
103 | if extra_version >= 7: | |
104 | self.localized_name = read_cunicode(buf) | |
105 | else: | |
106 | self.localized_name = read_cstring(buf) | |
107 | version_offset = read_short(buf) | |
108 | ||
109 | @classmethod | |
110 | def create_for_path(cls, path: str, is_file: Optional[bool] = None) -> 'PathSegmentEntry': | |
111 | entry = cls() | |
112 | try: | |
113 | st = os.stat(path) | |
114 | entry.file_size = st.st_size | |
115 | entry.modified = datetime.fromtimestamp(st.st_mtime) | |
116 | entry.created = datetime.fromtimestamp(st.st_ctime) | |
117 | entry.accessed = datetime.fromtimestamp(st.st_atime) | |
118 | if is_file is None: | |
119 | is_file = not os.path.isdir(path) | |
120 | except FileNotFoundError: | |
121 | now = datetime.now() | |
122 | entry.file_size = 0 | |
123 | entry.modified = now | |
124 | entry.created = now | |
125 | entry.accessed = now | |
126 | if is_file is None: | |
127 | is_file = '.' in ntpath.split(path)[-1][1:] | |
128 | entry.short_name = ntpath.split(path)[1] | |
129 | entry.full_name = entry.short_name | |
130 | entry.type = TYPE_FILE if is_file else TYPE_FOLDER | |
131 | return entry | |
132 | ||
133 | def _validate(self) -> None: | |
134 | if self.type is None: | |
135 | raise MissingInformationException("Type is missing, choose either TYPE_FOLDER or TYPE_FILE.") | |
136 | if self.file_size is None: | |
137 | if self.type.startswith('FOLDER') or self.type in ('KNOWN_FOLDER', 'ROOT_KNOWN_FOLDER'): | |
138 | self.file_size = 0 | |
139 | else: | |
140 | raise MissingInformationException("File size missing") | |
141 | if self.created is None: | |
142 | self.created = datetime.now() | |
143 | if self.modified is None: | |
144 | self.modified = datetime.now() | |
145 | if self.accessed is None: | |
146 | self.accessed = datetime.now() | |
147 | # if self.modified is None or self.accessed is None or self.created is None: | |
148 | # raise MissingInformationException("Date information missing") | |
149 | if self.full_name is None: | |
150 | raise MissingInformationException("A full name is missing") | |
151 | if self.short_name is None: | |
152 | self.short_name = self.full_name | |
153 | ||
154 | @property | |
155 | def bytes(self) -> bytes: | |
156 | if self.full_name is None: | |
157 | return b'' | |
158 | self._validate() | |
159 | ||
160 | # explicit check to have strict types without optionals | |
161 | assert self.short_name is not None | |
162 | assert self.type is not None | |
163 | assert self.file_size is not None | |
164 | assert self.modified is not None | |
165 | assert self.created is not None | |
166 | assert self.accessed is not None | |
167 | ||
168 | out = BytesIO() | |
169 | entry_type = self.type | |
170 | ||
171 | if entry_type == 'KNOWN_FOLDER': | |
172 | write_short(_ENTRY_TYPE_IDS[entry_type], out) | |
173 | write_short(0x1A, out) # size | |
174 | write_int(0x23FEBBEE, out) # extra signature | |
175 | write_short(0x00, out) # extra signature | |
176 | write_short(0x10, out) # guid size | |
177 | out.write(bytes_from_guid(self.full_name.strip(':'))) | |
178 | return out.getvalue() | |
179 | ||
180 | if entry_type == 'ROOT_KNOWN_FOLDER': | |
181 | write_short(_ENTRY_TYPE_IDS[entry_type], out) | |
182 | out.write(bytes_from_guid(self.full_name.strip(':'))) | |
183 | write_short(0x26, out) # 0xBEEF0026 structure size | |
184 | write_short(0x01, out) # version | |
185 | write_int(0xBEEF0026, out) # extra signature | |
186 | write_int(0x11, out) # some flag for containing datetime | |
187 | write_double(0x00, out) # created datetime | |
188 | write_double(0x00, out) # modified datetime | |
189 | write_double(0x00, out) # accessed datetime | |
190 | write_short(0x14, out) # unknown | |
191 | return out.getvalue() | |
192 | ||
193 | short_name_len = len(self.short_name) + 1 | |
194 | try: | |
195 | self.short_name.encode("ascii") | |
196 | short_name_is_unicode = False | |
197 | short_name_len += short_name_len % 2 # padding | |
198 | except (UnicodeEncodeError, UnicodeDecodeError): | |
199 | short_name_is_unicode = True | |
200 | short_name_len = short_name_len * 2 | |
201 | self.type += " (UNICODE)" | |
202 | write_short(_ENTRY_TYPE_IDS[entry_type], out) | |
203 | write_int(self.file_size, out) | |
204 | write_dos_datetime(self.modified, out) | |
205 | write_short(0x10, out) | |
206 | if short_name_is_unicode: | |
207 | write_cunicode(self.short_name, out) | |
208 | else: | |
209 | write_cstring(self.short_name, out, padding=True) | |
210 | indicator = 24 + 2 * len(self.short_name) | |
211 | write_short(indicator, out) # size | |
212 | write_short(0x03, out) # version | |
213 | write_short(0x04, out) # signature part1 | |
214 | write_short(0xBeef, out) # signature part2 | |
215 | write_dos_datetime(self.created, out) | |
216 | write_dos_datetime(self.accessed, out) | |
217 | offset_unicode = 0x14 # fixed data structure, always the same | |
218 | write_short(offset_unicode, out) | |
219 | offset_ansi = 0 # we always write unicode | |
220 | write_short(offset_ansi, out) # long_string_size | |
221 | write_cunicode(self.full_name, out) | |
222 | offset_part2 = 0x0E + short_name_len | |
223 | write_short(offset_part2, out) | |
224 | return out.getvalue() | |
225 | ||
226 | def __str__(self) -> str: | |
227 | return "<PathSegmentEntry: %s>" % self.full_name |
0 | from typing import Union | |
1 | ||
2 | from pylnk3.structures.id_list.base import IDListEntry | |
3 | from pylnk3.utils.guid import guid_from_bytes | |
4 | ||
5 | ROOT_MY_COMPUTER = 'MY_COMPUTER' | |
6 | ROOT_NETWORK_PLACES = 'NETWORK_PLACES' | |
7 | ROOT_MY_DOCUMENTS = 'MY_DOCUMENTS' | |
8 | ROOT_NETWORK_SHARE = 'NETWORK_SHARE' | |
9 | ROOT_NETWORK_SERVER = 'NETWORK_SERVER' | |
10 | ROOT_NETWORK_DOMAIN = 'NETWORK_DOMAIN' | |
11 | ROOT_INTERNET = 'INTERNET' | |
12 | RECYCLE_BIN = 'RECYCLE_BIN' | |
13 | ROOT_CONTROL_PANEL = 'CONTROL_PANEL' | |
14 | ROOT_USER = 'USERPROFILE' | |
15 | ROOT_UWP_APPS = 'APPS' | |
16 | ||
17 | _ROOT_LOCATIONS = { | |
18 | '{20D04FE0-3AEA-1069-A2D8-08002B30309D}': ROOT_MY_COMPUTER, | |
19 | '{450D8FBA-AD25-11D0-98A8-0800361B1103}': ROOT_MY_DOCUMENTS, | |
20 | '{54a754c0-4bf1-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SHARE, | |
21 | '{c0542a90-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SERVER, | |
22 | '{208D2C60-3AEA-1069-A2D7-08002B30309D}': ROOT_NETWORK_PLACES, | |
23 | '{46e06680-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_DOMAIN, | |
24 | '{871C5380-42A0-1069-A2EA-08002B30309D}': ROOT_INTERNET, | |
25 | '{645FF040-5081-101B-9F08-00AA002F954E}': RECYCLE_BIN, | |
26 | '{21EC2020-3AEA-1069-A2DD-08002B30309D}': ROOT_CONTROL_PANEL, | |
27 | '{59031A47-3F72-44A7-89C5-5595FE6B30EE}': ROOT_USER, | |
28 | '{4234D49B-0245-4DF3-B780-3893943456E1}': ROOT_UWP_APPS, | |
29 | } | |
30 | _ROOT_LOCATION_GUIDS = dict((v, k) for k, v in _ROOT_LOCATIONS.items()) | |
31 | ||
32 | ||
33 | class RootEntry(IDListEntry): | |
34 | ||
35 | def __init__(self, root: Union[str, bytes]) -> None: | |
36 | if root is not None: | |
37 | # create from text representation | |
38 | if isinstance(root, str): | |
39 | self.root = root | |
40 | self.guid: str = _ROOT_LOCATION_GUIDS[root] | |
41 | return | |
42 | else: | |
43 | # from binary | |
44 | root_type = root[0] | |
45 | index = root[1] | |
46 | guid_bytes = root[2:18] | |
47 | self.guid = guid_from_bytes(guid_bytes) | |
48 | self.root = _ROOT_LOCATIONS.get(self.guid, f"UNKNOWN {self.guid}") | |
49 | # if self.root == "UNKNOWN": | |
50 | # self.root = _ROOT_INDEX.get(index, "UNKNOWN") | |
51 | ||
52 | @property | |
53 | def bytes(self) -> bytes: | |
54 | guid = self.guid[1:-1].replace('-', '') | |
55 | chars = [bytes([int(x, 16)]) for x in [guid[i:i + 2] for i in range(0, 32, 2)]] | |
56 | return ( | |
57 | b'\x1F\x50' | |
58 | + chars[3] + chars[2] + chars[1] + chars[0] | |
59 | + chars[5] + chars[4] + chars[7] + chars[6] | |
60 | + b''.join(chars[8:]) | |
61 | ) | |
62 | ||
63 | def __str__(self) -> str: | |
64 | return "<RootEntry: %s>" % self.root |
0 | from io import BytesIO | |
1 | from typing import List, Optional, Union | |
2 | ||
3 | from pylnk3.structures.id_list.base import IDListEntry | |
4 | from pylnk3.utils.guid import bytes_from_guid, guid_from_bytes | |
5 | from pylnk3.utils.read_write import ( | |
6 | read_byte, read_cunicode, read_int, read_short, write_byte, write_cunicode, write_int, | |
7 | write_short, | |
8 | ) | |
9 | ||
10 | ||
11 | class UwpSubBlock: | |
12 | ||
13 | block_names = { | |
14 | 0x11: 'PackageFamilyName', | |
15 | # 0x0e: '', | |
16 | # 0x19: '', | |
17 | 0x15: 'PackageFullName', | |
18 | 0x05: 'Target', | |
19 | 0x0f: 'Location', | |
20 | 0x20: 'RandomGuid', | |
21 | 0x0c: 'Square150x150Logo', | |
22 | 0x02: 'Square44x44Logo', | |
23 | 0x0d: 'Wide310x150Logo', | |
24 | # 0x04: '', | |
25 | # 0x05: '', | |
26 | 0x13: 'Square310x310Logo', | |
27 | # 0x0e: '', | |
28 | 0x0b: 'DisplayName', | |
29 | 0x14: 'Square71x71Logo', | |
30 | 0x64: 'RandomByte', | |
31 | 0x0a: 'DisplayName', | |
32 | # 0x07: '', | |
33 | } | |
34 | ||
35 | block_types = { | |
36 | 'string': [0x11, 0x15, 0x05, 0x0f, 0x0c, 0x02, 0x0d, 0x13, 0x0b, 0x14, 0x0a], | |
37 | } | |
38 | ||
39 | def __init__( | |
40 | self, | |
41 | bytes: Optional[bytes] = None, | |
42 | type: Optional[int] = None, | |
43 | value: Optional[Union[str, bytes]] = None, | |
44 | ) -> None: | |
45 | if type is None and bytes is None: | |
46 | raise ValueError("Either bytes or type must be set") | |
47 | self._data = bytes or b'' | |
48 | self.value = value | |
49 | if type is not None: | |
50 | self.type = type | |
51 | self.name = self.block_names.get(self.type, 'UNKNOWN') | |
52 | if not bytes: | |
53 | return | |
54 | buf = BytesIO(bytes) | |
55 | self.type = read_byte(buf) | |
56 | self.name = self.block_names.get(self.type, 'UNKNOWN') | |
57 | ||
58 | self.value = self._data[1:] # skip type | |
59 | if self.type in self.block_types['string']: | |
60 | unknown = read_int(buf) | |
61 | probably_type = read_int(buf) | |
62 | if probably_type == 0x1f: | |
63 | string_len = read_int(buf) | |
64 | self.value = read_cunicode(buf) | |
65 | ||
66 | def __str__(self) -> str: | |
67 | string = f'UwpSubBlock {self.name} ({hex(self.type)}): {self.value!r}' | |
68 | return string.strip() | |
69 | ||
70 | @property | |
71 | def bytes(self) -> bytes: | |
72 | out = BytesIO() | |
73 | if self.value: | |
74 | if isinstance(self.value, str): | |
75 | string_len = len(self.value) + 1 | |
76 | ||
77 | write_byte(self.type, out) | |
78 | write_int(0, out) | |
79 | write_int(0x1f, out) | |
80 | ||
81 | write_int(string_len, out) | |
82 | write_cunicode(self.value, out) | |
83 | if string_len % 2 == 1: # padding | |
84 | write_short(0, out) | |
85 | ||
86 | elif isinstance(self.value, bytes): | |
87 | write_byte(self.type, out) | |
88 | out.write(self.value) | |
89 | ||
90 | result = out.getvalue() | |
91 | return result | |
92 | ||
93 | ||
94 | class UwpMainBlock: | |
95 | magic = b'\x31\x53\x50\x53' | |
96 | ||
97 | def __init__( | |
98 | self, | |
99 | bytes: Optional[bytes] = None, | |
100 | guid: Optional[str] = None, | |
101 | blocks: Optional[List[UwpSubBlock]] = None, | |
102 | ) -> None: | |
103 | self._data = bytes or b'' | |
104 | self._blocks = blocks or [] | |
105 | if guid is not None: | |
106 | self.guid: str = guid | |
107 | if not bytes: | |
108 | return | |
109 | buf = BytesIO(bytes) | |
110 | magic = buf.read(4) | |
111 | self.guid = guid_from_bytes(buf.read(16)) | |
112 | # read sub blocks | |
113 | while True: | |
114 | sub_block_size = read_int(buf) | |
115 | if not sub_block_size: # last size is zero | |
116 | break | |
117 | sub_block_data = buf.read(sub_block_size - 4) # includes block_size | |
118 | self._blocks.append(UwpSubBlock(sub_block_data)) | |
119 | ||
120 | def __str__(self) -> str: | |
121 | string = f'<UwpMainBlock> {self.guid}:\n' | |
122 | for block in self._blocks: | |
123 | string += f' {block}\n' | |
124 | return string.strip() | |
125 | ||
126 | @property | |
127 | def bytes(self) -> bytes: | |
128 | blocks_bytes = [block.bytes for block in self._blocks] | |
129 | out = BytesIO() | |
130 | out.write(self.magic) | |
131 | out.write(bytes_from_guid(self.guid)) | |
132 | for block in blocks_bytes: | |
133 | write_int(len(block) + 4, out) | |
134 | out.write(block) | |
135 | write_int(0, out) | |
136 | result = out.getvalue() | |
137 | return result | |
138 | ||
139 | ||
140 | class UwpSegmentEntry(IDListEntry): | |
141 | magic = b'APPS' | |
142 | header = b'\x08\x00\x03\x00\x00\x00\x00\x00\x00\x00' | |
143 | ||
144 | def __init__(self, bytes: Optional[bytes] = None) -> None: | |
145 | self._blocks = [] | |
146 | self._data = bytes | |
147 | if bytes is None: | |
148 | return | |
149 | buf = BytesIO(bytes) | |
150 | unknown = read_short(buf) | |
151 | size = read_short(buf) | |
152 | magic = buf.read(4) # b'APPS' | |
153 | blocks_size = read_short(buf) | |
154 | unknown2 = buf.read(10) | |
155 | # read main blocks | |
156 | while True: | |
157 | block_size = read_int(buf) | |
158 | if not block_size: # last size is zero | |
159 | break | |
160 | block_data = buf.read(block_size - 4) # includes block_size | |
161 | self._blocks.append(UwpMainBlock(block_data)) | |
162 | ||
163 | def __str__(self) -> str: | |
164 | string = '<UwpSegmentEntry>:\n' | |
165 | for block in self._blocks: | |
166 | string += f' {block}\n' | |
167 | return string.strip() | |
168 | ||
169 | @property | |
170 | def bytes(self) -> bytes: | |
171 | blocks_bytes = [block.bytes for block in self._blocks] | |
172 | blocks_size = sum([len(block) + 4 for block in blocks_bytes]) + 4 # with terminator | |
173 | size = ( | |
174 | 2 # size | |
175 | + len(self.magic) | |
176 | + 2 # second size | |
177 | + len(self.header) | |
178 | + blocks_size # blocks with terminator | |
179 | ) | |
180 | ||
181 | out = BytesIO() | |
182 | write_short(0, out) | |
183 | write_short(size, out) | |
184 | out.write(self.magic) | |
185 | write_short(blocks_size, out) | |
186 | out.write(self.header) | |
187 | for block in blocks_bytes: | |
188 | write_int(len(block) + 4, out) | |
189 | out.write(block) | |
190 | write_int(0, out) # empty block | |
191 | write_short(0, out) # ?? | |
192 | ||
193 | result = out.getvalue() | |
194 | return result | |
195 | ||
196 | @classmethod | |
197 | def create( | |
198 | cls, | |
199 | package_family_name: str, | |
200 | target: str, | |
201 | location: Optional[str] = None, | |
202 | logo44x44: Optional[str] = None, | |
203 | ) -> 'UwpSegmentEntry': | |
204 | segment = cls() | |
205 | ||
206 | blocks = [ | |
207 | UwpSubBlock(type=0x11, value=package_family_name), | |
208 | UwpSubBlock(type=0x0e, value=b'\x00\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00'), | |
209 | UwpSubBlock(type=0x05, value=target), | |
210 | ] | |
211 | if location: | |
212 | blocks.append(UwpSubBlock(type=0x0f, value=location)) # need for relative icon path | |
213 | main1 = UwpMainBlock(guid='{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', blocks=blocks) | |
214 | segment._blocks.append(main1) | |
215 | ||
216 | if logo44x44: | |
217 | main2 = UwpMainBlock( | |
218 | guid='{86D40B4D-9069-443C-819A-2A54090DCCEC}', | |
219 | blocks=[UwpSubBlock(type=0x02, value=logo44x44)], | |
220 | ) | |
221 | segment._blocks.append(main2) | |
222 | ||
223 | return segment |
0 | from io import BufferedIOBase | |
1 | from typing import Optional | |
2 | ||
3 | from pylnk3.exceptions import MissingInformationException | |
4 | from pylnk3.utils.read_write import read_cstring, read_int, write_byte, write_cstring, write_int | |
5 | ||
6 | DRIVE_NO_ROOT_DIR = "No root directory" | |
7 | DRIVE_REMOVABLE = "Removable" | |
8 | DRIVE_FIXED = "Fixed (Hard disk)" | |
9 | DRIVE_REMOTE = "Remote (Network drive)" | |
10 | DRIVE_CDROM = "CD-ROM" | |
11 | DRIVE_RAMDISK = "Ram disk" | |
12 | DRIVE_UNKNOWN = "Unknown" | |
13 | ||
14 | _DRIVE_TYPES = { | |
15 | 0: DRIVE_UNKNOWN, | |
16 | 1: DRIVE_NO_ROOT_DIR, | |
17 | 2: DRIVE_REMOVABLE, | |
18 | 3: DRIVE_FIXED, | |
19 | 4: DRIVE_REMOTE, | |
20 | 5: DRIVE_CDROM, | |
21 | 6: DRIVE_RAMDISK, | |
22 | } | |
23 | _DRIVE_TYPE_IDS = dict((v, k) for k, v in _DRIVE_TYPES.items()) | |
24 | ||
25 | _LINK_INFO_HEADER_DEFAULT = 0x1C | |
26 | _LINK_INFO_HEADER_OPTIONAL = 0x24 | |
27 | ||
28 | ||
29 | class LinkInfo: | |
30 | ||
31 | def __init__(self, lnk: Optional[BufferedIOBase] = None) -> None: | |
32 | if lnk is not None: | |
33 | self.start = lnk.tell() | |
34 | self.size = read_int(lnk) | |
35 | self.header_size = read_int(lnk) | |
36 | link_info_flags = read_int(lnk) | |
37 | self.local = link_info_flags & 1 | |
38 | self.remote = link_info_flags & 2 | |
39 | self.offs_local_volume_table = read_int(lnk) | |
40 | self.offs_local_base_path = read_int(lnk) | |
41 | self.offs_network_volume_table = read_int(lnk) | |
42 | self.offs_base_name = read_int(lnk) | |
43 | if self.header_size >= _LINK_INFO_HEADER_OPTIONAL: | |
44 | print("TODO: read the unicode stuff") # TODO: read the unicode stuff | |
45 | self._parse_path_elements(lnk) | |
46 | else: | |
47 | self.size = 0 | |
48 | self.header_size = _LINK_INFO_HEADER_DEFAULT | |
49 | self.local = 0 | |
50 | self.remote = 0 | |
51 | self.offs_local_volume_table = 0 | |
52 | self.offs_local_base_path = 0 | |
53 | self.offs_network_volume_table = 0 | |
54 | self.offs_base_name = 0 | |
55 | self.drive_type: Optional[str] = None | |
56 | self.drive_serial: int = None # type: ignore[assignment] | |
57 | self.volume_label: str = None # type: ignore[assignment] | |
58 | self.local_base_path: str = None # type: ignore[assignment] | |
59 | self.network_share_name: str = '' | |
60 | self.base_name: str = '' | |
61 | self._path: str = '' | |
62 | ||
63 | def _parse_path_elements(self, lnk: BufferedIOBase) -> None: | |
64 | if self.remote: | |
65 | # 20 is the offset of the network share name | |
66 | lnk.seek(self.start + self.offs_network_volume_table + 20) | |
67 | self.network_share_name = read_cstring(lnk) | |
68 | lnk.seek(self.start + self.offs_base_name) | |
69 | self.base_name = read_cstring(lnk) | |
70 | if self.local: | |
71 | lnk.seek(self.start + self.offs_local_volume_table + 4) | |
72 | self.drive_type = _DRIVE_TYPES.get(read_int(lnk)) | |
73 | self.drive_serial = read_int(lnk) | |
74 | lnk.read(4) # volume name offset (10h) | |
75 | self.volume_label = read_cstring(lnk) | |
76 | lnk.seek(self.start + self.offs_local_base_path) | |
77 | self.local_base_path = read_cstring(lnk) | |
78 | # TODO: unicode | |
79 | self.make_path() | |
80 | ||
81 | def make_path(self) -> None: | |
82 | if self.remote: | |
83 | self._path = self.network_share_name + '\\' + self.base_name | |
84 | if self.local: | |
85 | self._path = self.local_base_path | |
86 | ||
87 | def write(self, lnk: BufferedIOBase) -> None: | |
88 | if self.remote is None: | |
89 | raise MissingInformationException("No location information given.") | |
90 | self.start = lnk.tell() | |
91 | self._calculate_sizes_and_offsets() | |
92 | write_int(self.size, lnk) | |
93 | write_int(self.header_size, lnk) | |
94 | write_int((self.local and 1) + (self.remote and 2), lnk) | |
95 | write_int(self.offs_local_volume_table, lnk) | |
96 | write_int(self.offs_local_base_path, lnk) | |
97 | write_int(self.offs_network_volume_table, lnk) | |
98 | write_int(self.offs_base_name, lnk) | |
99 | if self.remote: | |
100 | self._write_network_volume_table(lnk) | |
101 | write_cstring(self.base_name, lnk, padding=False) | |
102 | else: | |
103 | self._write_local_volume_table(lnk) | |
104 | write_cstring(self.local_base_path, lnk, padding=False) | |
105 | write_byte(0, lnk) | |
106 | ||
107 | def _calculate_sizes_and_offsets(self) -> None: | |
108 | self.size_base_name = 1 # len(self.base_name) + 1 # zero terminated strings | |
109 | self.size = 28 + self.size_base_name | |
110 | if self.remote: | |
111 | self.size_network_volume_table = 20 + len(self.network_share_name) + len(self.base_name) + 1 | |
112 | self.size += self.size_network_volume_table | |
113 | self.offs_local_volume_table = 0 | |
114 | self.offs_local_base_path = 0 | |
115 | self.offs_network_volume_table = 28 | |
116 | self.offs_base_name = self.offs_network_volume_table + self.size_network_volume_table | |
117 | else: | |
118 | self.size_local_volume_table = 16 + len(self.volume_label) + 1 | |
119 | self.size_local_base_path = len(self.local_base_path) + 1 | |
120 | self.size += self.size_local_volume_table + self.size_local_base_path | |
121 | self.offs_local_volume_table = 28 | |
122 | self.offs_local_base_path = self.offs_local_volume_table + self.size_local_volume_table | |
123 | self.offs_network_volume_table = 0 | |
124 | self.offs_base_name = self.offs_local_base_path + self.size_local_base_path | |
125 | ||
126 | def _write_network_volume_table(self, buf: BufferedIOBase) -> None: | |
127 | write_int(self.size_network_volume_table, buf) | |
128 | write_int(2, buf) # ? | |
129 | write_int(20, buf) # size of Network Volume Table | |
130 | write_int(0, buf) # ? | |
131 | write_int(131072, buf) # ? | |
132 | write_cstring(self.network_share_name, buf) | |
133 | ||
134 | def _write_local_volume_table(self, buf: BufferedIOBase) -> None: | |
135 | write_int(self.size_local_volume_table, buf) | |
136 | if self.drive_type is None or self.drive_type not in _DRIVE_TYPE_IDS: | |
137 | raise ValueError("This is not a valid drive type: %s" % self.drive_type) | |
138 | drive_type = _DRIVE_TYPE_IDS[self.drive_type] | |
139 | write_int(drive_type, buf) | |
140 | write_int(self.drive_serial, buf) | |
141 | write_int(16, buf) # volume name offset | |
142 | write_cstring(self.volume_label, buf) | |
143 | ||
144 | @property | |
145 | def path(self) -> str: | |
146 | return self._path | |
147 | ||
148 | def __str__(self) -> str: | |
149 | s = "File Location Info:" | |
150 | if not self._path: | |
151 | return s + " <not specified>" | |
152 | if self.remote: | |
153 | s += "\n (remote)" | |
154 | s += "\n Network Share: %s" % self.network_share_name | |
155 | s += "\n Base Name: %s" % self.base_name | |
156 | else: | |
157 | s += "\n (local)" | |
158 | s += "\n Volume Type: %s" % self.drive_type | |
159 | s += "\n Volume Serial Number: %s" % self.drive_serial | |
160 | s += "\n Volume Label: %s" % self.volume_label | |
161 | s += "\n Path: %s" % self.local_base_path | |
162 | return s |
0 | from datetime import datetime | |
1 | from io import BufferedIOBase | |
2 | from typing import Optional, Union | |
3 | ||
4 | from pylnk3.exceptions import FormatException, InvalidKeyException | |
5 | from pylnk3.flags import Flags, ModifierKeys | |
6 | from pylnk3.structures.extra_data import ExtraData, ExtraData_EnvironmentVariableDataBlock | |
7 | from pylnk3.structures.id_list.id_list import LinkTargetIDList | |
8 | from pylnk3.structures.link_info import DRIVE_UNKNOWN, LinkInfo | |
9 | from pylnk3.utils.data import convert_time_to_unix, convert_time_to_windows | |
10 | from pylnk3.utils.read_write import ( | |
11 | read_byte, read_double, read_int, read_short, read_sized_string, write_byte, write_double, | |
12 | write_int, write_short, write_sized_string, | |
13 | ) | |
14 | ||
15 | _SIGNATURE = b'L\x00\x00\x00' | |
16 | _GUID = b'\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00F' | |
17 | ||
18 | _LINK_FLAGS = ( | |
19 | 'HasLinkTargetIDList', | |
20 | 'HasLinkInfo', | |
21 | 'HasName', | |
22 | 'HasRelativePath', | |
23 | 'HasWorkingDir', | |
24 | 'HasArguments', | |
25 | 'HasIconLocation', | |
26 | 'IsUnicode', | |
27 | 'ForceNoLinkInfo', | |
28 | # new | |
29 | 'HasExpString', | |
30 | 'RunInSeparateProcess', | |
31 | 'Unused1', | |
32 | 'HasDarwinID', | |
33 | 'RunAsUser', | |
34 | 'HasExpIcon', | |
35 | 'NoPidlAlias', | |
36 | 'Unused2', | |
37 | 'RunWithShimLayer', | |
38 | 'ForceNoLinkTrack', | |
39 | 'EnableTargetMetadata', | |
40 | 'DisableLinkPathTracking', | |
41 | 'DisableKnownFolderTracking', | |
42 | 'DisableKnownFolderAlias', | |
43 | 'AllowLinkToLink', | |
44 | 'UnaliasOnSave', | |
45 | 'PreferEnvironmentPath', | |
46 | 'KeepLocalIDListForUNCTarget', | |
47 | ) | |
48 | ||
49 | _FILE_ATTRIBUTES_FLAGS = ( | |
50 | 'read_only', 'hidden', 'system_file', 'reserved1', | |
51 | 'directory', 'archive', 'reserved2', 'normal', | |
52 | 'temporary', 'sparse_file', 'reparse_point', | |
53 | 'compressed', 'offline', 'not_content_indexed', | |
54 | 'encrypted', | |
55 | ) | |
56 | ||
57 | WINDOW_NORMAL = "Normal" | |
58 | WINDOW_MAXIMIZED = "Maximized" | |
59 | WINDOW_MINIMIZED = "Minimized" | |
60 | ||
61 | ||
62 | _SHOW_COMMANDS = {1: WINDOW_NORMAL, 3: WINDOW_MAXIMIZED, 7: WINDOW_MINIMIZED} | |
63 | _SHOW_COMMAND_IDS = dict((v, k) for k, v in _SHOW_COMMANDS.items()) | |
64 | ||
65 | _KEYS = { | |
66 | 0x30: '0', 0x31: '1', 0x32: '2', 0x33: '3', 0x34: '4', 0x35: '5', 0x36: '6', | |
67 | 0x37: '7', 0x38: '8', 0x39: '9', 0x41: 'A', 0x42: 'B', 0x43: 'C', 0x44: 'D', | |
68 | 0x45: 'E', 0x46: 'F', 0x47: 'G', 0x48: 'H', 0x49: 'I', 0x4A: 'J', 0x4B: 'K', | |
69 | 0x4C: 'L', 0x4D: 'M', 0x4E: 'N', 0x4F: 'O', 0x50: 'P', 0x51: 'Q', 0x52: 'R', | |
70 | 0x53: 'S', 0x54: 'T', 0x55: 'U', 0x56: 'V', 0x57: 'W', 0x58: 'X', 0x59: 'Y', | |
71 | 0x5A: 'Z', 0x70: 'F1', 0x71: 'F2', 0x72: 'F3', 0x73: 'F4', 0x74: 'F5', | |
72 | 0x75: 'F6', 0x76: 'F7', 0x77: 'F8', 0x78: 'F9', 0x79: 'F10', 0x7A: 'F11', | |
73 | 0x7B: 'F12', 0x7C: 'F13', 0x7D: 'F14', 0x7E: 'F15', 0x7F: 'F16', 0x80: 'F17', | |
74 | 0x81: 'F18', 0x82: 'F19', 0x83: 'F20', 0x84: 'F21', 0x85: 'F22', 0x86: 'F23', | |
75 | 0x87: 'F24', 0x90: 'NUM LOCK', 0x91: 'SCROLL LOCK', | |
76 | } | |
77 | _KEY_CODES = dict((v, k) for k, v in _KEYS.items()) | |
78 | ||
79 | ||
80 | def assert_lnk_signature(f: BufferedIOBase) -> None: | |
81 | f.seek(0) | |
82 | sig = f.read(4) | |
83 | guid = f.read(16) | |
84 | if sig != _SIGNATURE: | |
85 | raise FormatException("This is not a .lnk file.") | |
86 | if guid != _GUID: | |
87 | raise FormatException("Cannot read this kind of .lnk file.") | |
88 | ||
89 | ||
90 | class Lnk: | |
91 | ||
92 | def __init__(self, f: Optional[Union[str, BufferedIOBase]] = None) -> None: | |
93 | self.file: Optional[str] = None | |
94 | if isinstance(f, str): | |
95 | self.file = f | |
96 | try: | |
97 | f = open(self.file, 'rb') | |
98 | except IOError: | |
99 | self.file += ".lnk" | |
100 | f = open(self.file, 'rb') | |
101 | # defaults | |
102 | self.link_flags = Flags(_LINK_FLAGS) | |
103 | self.file_flags = Flags(_FILE_ATTRIBUTES_FLAGS) | |
104 | self.creation_time = datetime.now() | |
105 | self.access_time = datetime.now() | |
106 | self.modification_time = datetime.now() | |
107 | self.file_size = 0 | |
108 | self.icon_index = 0 | |
109 | self._show_command = WINDOW_NORMAL | |
110 | self.hot_key: Optional[str] = None | |
111 | self._link_info = LinkInfo() | |
112 | self.description = None | |
113 | self.relative_path = None | |
114 | self.work_dir = None | |
115 | self.arguments = None | |
116 | self.icon = None | |
117 | self.extra_data: Optional[ExtraData] = None | |
118 | if f is not None: | |
119 | assert_lnk_signature(f) | |
120 | self._parse_lnk_file(f) | |
121 | f.close() | |
122 | ||
123 | def _read_hot_key(self, lnk: BufferedIOBase) -> str: | |
124 | low = read_byte(lnk) | |
125 | high = read_byte(lnk) | |
126 | key = _KEYS.get(low, '') | |
127 | modifier = high and str(ModifierKeys(high)) or '' | |
128 | return modifier + key | |
129 | ||
130 | def _write_hot_key(self, hot_key: Optional[str], lnk: BufferedIOBase) -> None: | |
131 | if hot_key is None or not hot_key: | |
132 | low = high = 0 | |
133 | else: | |
134 | hot_key_parts = hot_key.split('+') | |
135 | try: | |
136 | low = _KEY_CODES[hot_key_parts[-1]] | |
137 | except KeyError: | |
138 | raise InvalidKeyException("Cannot find key code for %s" % hot_key_parts[1]) | |
139 | modifiers = ModifierKeys() | |
140 | for modifier in hot_key_parts[:-1]: | |
141 | modifiers[modifier.upper()] = True | |
142 | high = modifiers.bytes | |
143 | write_byte(low, lnk) | |
144 | write_byte(high, lnk) | |
145 | ||
146 | def _parse_lnk_file(self, lnk: BufferedIOBase) -> None: | |
147 | # SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO] [STRING_DATA] *EXTRA_DATA | |
148 | ||
149 | # SHELL_LINK_HEADER | |
150 | lnk.seek(20) # after signature and guid | |
151 | self.link_flags.set_flags(read_int(lnk)) | |
152 | self.file_flags.set_flags(read_int(lnk)) | |
153 | self.creation_time = convert_time_to_unix(read_double(lnk)) | |
154 | self.access_time = convert_time_to_unix(read_double(lnk)) | |
155 | self.modification_time = convert_time_to_unix(read_double(lnk)) | |
156 | self.file_size = read_int(lnk) | |
157 | self.icon_index = read_int(lnk) | |
158 | show_command = read_int(lnk) | |
159 | self._show_command = _SHOW_COMMANDS[show_command] if show_command in _SHOW_COMMANDS else _SHOW_COMMANDS[1] | |
160 | self.hot_key = self._read_hot_key(lnk) | |
161 | lnk.read(10) # reserved (0) | |
162 | ||
163 | # LINKTARGET_IDLIST (HasLinkTargetIDList) | |
164 | if self.link_flags.HasLinkTargetIDList: | |
165 | shell_item_id_list_size = read_short(lnk) | |
166 | self.shell_item_id_list = LinkTargetIDList(lnk.read(shell_item_id_list_size)) | |
167 | ||
168 | # LINKINFO (HasLinkInfo) | |
169 | if self.link_flags.HasLinkInfo and not self.link_flags.ForceNoLinkInfo: | |
170 | self._link_info = LinkInfo(lnk) | |
171 | lnk.seek(self._link_info.start + self._link_info.size) | |
172 | ||
173 | # STRING_DATA = [NAME_STRING] [RELATIVE_PATH] [WORKING_DIR] [COMMAND_LINE_ARGUMENTS] [ICON_LOCATION] | |
174 | if self.link_flags.HasName: | |
175 | self.description = read_sized_string(lnk, self.link_flags.IsUnicode) | |
176 | if self.link_flags.HasRelativePath: | |
177 | self.relative_path = read_sized_string(lnk, self.link_flags.IsUnicode) | |
178 | if self.link_flags.HasWorkingDir: | |
179 | self.work_dir = read_sized_string(lnk, self.link_flags.IsUnicode) | |
180 | if self.link_flags.HasArguments: | |
181 | self.arguments = read_sized_string(lnk, self.link_flags.IsUnicode) | |
182 | if self.link_flags.HasIconLocation: | |
183 | self.icon = read_sized_string(lnk, self.link_flags.IsUnicode) | |
184 | ||
185 | # *EXTRA_DATA | |
186 | self.extra_data = ExtraData(lnk) | |
187 | ||
188 | def save(self, f: Optional[Union[str, BufferedIOBase]] = None, force_ext: bool = False) -> None: | |
189 | f = f or self.file | |
190 | is_opened_here = False | |
191 | if isinstance(f, str): | |
192 | filename: str = f | |
193 | if force_ext and not filename.endswith('.lnk'): | |
194 | filename += '.lnk' | |
195 | f = open(filename, 'wb') | |
196 | is_opened_here = True | |
197 | if f is None: | |
198 | raise ValueError("No file specified for saving LNK file") | |
199 | self.write(f) | |
200 | # only close the stream if it's our own | |
201 | if is_opened_here: | |
202 | f.close() | |
203 | ||
204 | def write(self, lnk: BufferedIOBase) -> None: | |
205 | lnk.write(_SIGNATURE) | |
206 | lnk.write(_GUID) | |
207 | write_int(self.link_flags.bytes, lnk) | |
208 | write_int(self.file_flags.bytes, lnk) | |
209 | write_double(convert_time_to_windows(self.creation_time), lnk) | |
210 | write_double(convert_time_to_windows(self.access_time), lnk) | |
211 | write_double(convert_time_to_windows(self.modification_time), lnk) | |
212 | write_int(self.file_size, lnk) | |
213 | write_int(self.icon_index, lnk) | |
214 | write_int(_SHOW_COMMAND_IDS[self._show_command], lnk) | |
215 | self._write_hot_key(self.hot_key, lnk) | |
216 | lnk.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') # reserved | |
217 | if self.link_flags.HasLinkTargetIDList: | |
218 | shell_item_id_list = self.shell_item_id_list.bytes | |
219 | write_short(len(shell_item_id_list), lnk) | |
220 | lnk.write(shell_item_id_list) | |
221 | if self.link_flags.HasLinkInfo: | |
222 | self._link_info.write(lnk) | |
223 | if self.link_flags.HasName: | |
224 | write_sized_string(self.description, lnk, self.link_flags.IsUnicode) | |
225 | if self.link_flags.HasRelativePath: | |
226 | write_sized_string(self.relative_path, lnk, self.link_flags.IsUnicode) | |
227 | if self.link_flags.HasWorkingDir: | |
228 | write_sized_string(self.work_dir, lnk, self.link_flags.IsUnicode) | |
229 | if self.link_flags.HasArguments: | |
230 | write_sized_string(self.arguments, lnk, self.link_flags.IsUnicode) | |
231 | if self.link_flags.HasIconLocation: | |
232 | write_sized_string(self.icon, lnk, self.link_flags.IsUnicode) | |
233 | if self.extra_data: | |
234 | lnk.write(self.extra_data.bytes) | |
235 | else: | |
236 | lnk.write(b'\x00\x00\x00\x00') | |
237 | ||
238 | def _get_shell_item_id_list(self) -> LinkTargetIDList: | |
239 | return self._shell_item_id_list | |
240 | ||
241 | def _set_shell_item_id_list(self, shell_item_id_list: LinkTargetIDList) -> None: | |
242 | self._shell_item_id_list = shell_item_id_list | |
243 | self.link_flags.HasLinkTargetIDList = shell_item_id_list is not None | |
244 | shell_item_id_list = property(_get_shell_item_id_list, _set_shell_item_id_list) | |
245 | ||
246 | def _get_link_info(self) -> LinkInfo: | |
247 | return self._link_info | |
248 | ||
249 | def _set_link_info(self, link_info: LinkInfo) -> None: | |
250 | self._link_info = link_info | |
251 | self.link_flags.ForceNoLinkInfo = link_info is None | |
252 | self.link_flags.HasLinkInfo = link_info is not None | |
253 | link_info = property(_get_link_info, _set_link_info) | |
254 | ||
255 | def _get_description(self) -> str: | |
256 | return self._description | |
257 | ||
258 | def _set_description(self, description: str) -> None: | |
259 | self._description = description | |
260 | self.link_flags.HasName = description is not None | |
261 | description = property(_get_description, _set_description) | |
262 | ||
263 | def _get_relative_path(self) -> str: | |
264 | return self._relative_path | |
265 | ||
266 | def _set_relative_path(self, relative_path: str) -> None: | |
267 | self._relative_path = relative_path | |
268 | self.link_flags.HasRelativePath = relative_path is not None | |
269 | relative_path = property(_get_relative_path, _set_relative_path) | |
270 | ||
271 | def _get_work_dir(self) -> str: | |
272 | return self._work_dir | |
273 | ||
274 | def _set_work_dir(self, work_dir: str) -> None: | |
275 | self._work_dir = work_dir | |
276 | self.link_flags.HasWorkingDir = work_dir is not None | |
277 | work_dir = working_dir = property(_get_work_dir, _set_work_dir) | |
278 | ||
279 | def _get_arguments(self) -> str: | |
280 | return self._arguments | |
281 | ||
282 | def _set_arguments(self, arguments: str) -> None: | |
283 | self._arguments = arguments | |
284 | self.link_flags.HasArguments = arguments is not None | |
285 | arguments = property(_get_arguments, _set_arguments) | |
286 | ||
287 | def _get_icon(self) -> str: | |
288 | return self._icon | |
289 | ||
290 | def _set_icon(self, icon: str) -> None: | |
291 | self._icon = icon | |
292 | self.link_flags.HasIconLocation = icon is not None | |
293 | icon = property(_get_icon, _set_icon) | |
294 | ||
295 | def _get_window_mode(self) -> str: | |
296 | return self._show_command | |
297 | ||
298 | def _set_window_mode(self, value: str) -> None: | |
299 | if value not in list(_SHOW_COMMANDS.values()): | |
300 | raise ValueError("Not a valid window mode: %s. Choose any of pylnk.WINDOW_*" % value) | |
301 | self._show_command = value | |
302 | window_mode = show_command = property(_get_window_mode, _set_window_mode) | |
303 | ||
304 | @property | |
305 | def path(self) -> str: | |
306 | # lnk can contains several different paths at different structures | |
307 | # here is some logic consistent with link properties at explorer (at least on test examples) | |
308 | ||
309 | link_info_path = self._link_info.path if self._link_info and self._link_info.path else None | |
310 | id_list_path = self._shell_item_id_list.get_path() if hasattr(self, '_shell_item_id_list') else None | |
311 | ||
312 | env_var_path = None | |
313 | if self.extra_data and self.extra_data.blocks: | |
314 | for block in self.extra_data.blocks: | |
315 | if type(block) == ExtraData_EnvironmentVariableDataBlock: | |
316 | env_var_path = block.target_unicode.strip('\x00') or block.target_ansi.strip('\x00') | |
317 | break | |
318 | ||
319 | if id_list_path and id_list_path.startswith('%MY_COMPUTER%'): | |
320 | # full local path has priority | |
321 | return id_list_path[14:] | |
322 | if id_list_path and id_list_path.startswith('%USERPROFILE%\\::'): | |
323 | # path to KNOWN_FOLDER also has priority over link_info | |
324 | return id_list_path[14:] | |
325 | if link_info_path: | |
326 | # local path at link_info_path has priority over network path at id_list_path | |
327 | # full local path at link_info_path has priority over partial path at id_list_path | |
328 | return link_info_path | |
329 | if env_var_path: | |
330 | # some links in Recent folder contains path only at ExtraData_EnvironmentVariableDataBlock | |
331 | return env_var_path | |
332 | return str(id_list_path) | |
333 | ||
334 | def specify_local_location( | |
335 | self, | |
336 | path: str, | |
337 | drive_type: Optional[str] = None, | |
338 | drive_serial: Optional[int] = None, | |
339 | volume_label: Optional[str] = None, | |
340 | ) -> None: | |
341 | self._link_info.drive_type = drive_type or DRIVE_UNKNOWN | |
342 | self._link_info.drive_serial = drive_serial or 0 | |
343 | self._link_info.volume_label = volume_label or '' | |
344 | self._link_info.local_base_path = path | |
345 | self._link_info.local = True | |
346 | self._link_info.make_path() | |
347 | ||
348 | def specify_remote_location(self, network_share_name: str, base_name: str) -> None: | |
349 | self._link_info.network_share_name = network_share_name | |
350 | self._link_info.base_name = base_name | |
351 | self._link_info.remote = True | |
352 | self._link_info.make_path() | |
353 | ||
354 | def __str__(self) -> str: | |
355 | s = "Target file:\n" | |
356 | s += str(self.file_flags) | |
357 | s += "\nCreation Time: %s" % self.creation_time | |
358 | s += "\nModification Time: %s" % self.modification_time | |
359 | s += "\nAccess Time: %s" % self.access_time | |
360 | s += "\nFile size: %s" % self.file_size | |
361 | s += "\nWindow mode: %s" % self._show_command | |
362 | s += "\nHotkey: %s\n" % self.hot_key | |
363 | s += str(self._link_info) | |
364 | if self.link_flags.HasLinkTargetIDList: | |
365 | s += "\n%s" % self.shell_item_id_list | |
366 | if self.link_flags.HasName: | |
367 | s += "\nDescription: %s" % self.description | |
368 | if self.link_flags.HasRelativePath: | |
369 | s += "\nRelative Path: %s" % self.relative_path | |
370 | if self.link_flags.HasWorkingDir: | |
371 | s += "\nWorking Directory: %s" % self.work_dir | |
372 | if self.link_flags.HasArguments: | |
373 | s += "\nCommandline Arguments: %s" % self.arguments | |
374 | if self.link_flags.HasIconLocation: | |
375 | s += "\nIcon: %s" % self.icon | |
376 | if self._link_info: | |
377 | s += "\nUsed Path: %s" % self.path | |
378 | if self.extra_data: | |
379 | s += str(self.extra_data) | |
380 | return s |
0 | import time | |
1 | from datetime import datetime | |
2 | from typing import Union | |
3 | ||
4 | ||
5 | def convert_time_to_unix(windows_time: int) -> datetime: | |
6 | # Windows time is specified as the number of 0.1 nanoseconds since January 1, 1601. | |
7 | # UNIX time is specified as the number of seconds since January 1, 1970. | |
8 | # There are 134774 days (or 11644473600 seconds) between these dates. | |
9 | unix_time = windows_time / 10000000.0 - 11644473600 | |
10 | try: | |
11 | return datetime.fromtimestamp(unix_time) | |
12 | except OSError: | |
13 | return datetime.now() | |
14 | ||
15 | ||
16 | def convert_time_to_windows(unix_time: Union[int, datetime]) -> int: | |
17 | if isinstance(unix_time, datetime): | |
18 | try: | |
19 | unix_time_int = time.mktime(unix_time.timetuple()) | |
20 | except OverflowError: | |
21 | unix_time_int = time.mktime(datetime.now().timetuple()) | |
22 | else: | |
23 | unix_time_int = unix_time | |
24 | return int((unix_time_int + 11644473600) * 10000000) |
0 | from pylnk3.exceptions import FormatException | |
1 | ||
2 | ||
3 | def guid_to_str(guid: bytes) -> str: | |
4 | ordered = [ | |
5 | guid[3], guid[2], guid[1], guid[0], | |
6 | guid[5], guid[4], guid[7], guid[6], | |
7 | guid[8], guid[9], guid[10], guid[11], | |
8 | guid[12], guid[13], guid[14], guid[15], | |
9 | ] | |
10 | res = "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered]) | |
11 | # print(guid, res) | |
12 | return res | |
13 | ||
14 | ||
15 | def guid_from_bytes(bytes: bytes) -> str: | |
16 | if len(bytes) != 16: | |
17 | raise FormatException(f"This is no valid _GUID: {bytes!s}") | |
18 | ordered = [ | |
19 | bytes[3], bytes[2], bytes[1], bytes[0], | |
20 | bytes[5], bytes[4], bytes[7], bytes[6], | |
21 | bytes[8], bytes[9], bytes[10], bytes[11], | |
22 | bytes[12], bytes[13], bytes[14], bytes[15], | |
23 | ] | |
24 | return "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered]) | |
25 | ||
26 | ||
27 | def bytes_from_guid(guid: str) -> bytes: | |
28 | nums = [ | |
29 | guid[1:3], guid[3:5], guid[5:7], guid[7:9], | |
30 | guid[10:12], guid[12:14], guid[15:17], guid[17:19], | |
31 | guid[20:22], guid[22:24], guid[25:27], guid[27:29], | |
32 | guid[29:31], guid[31:33], guid[33:35], guid[35:37], | |
33 | ] | |
34 | ordered_nums = [ | |
35 | nums[3], nums[2], nums[1], nums[0], | |
36 | nums[5], nums[4], nums[7], nums[6], | |
37 | nums[8], nums[9], nums[10], nums[11], | |
38 | nums[12], nums[13], nums[14], nums[15], | |
39 | ] | |
40 | return bytes([int(x, 16) for x in ordered_nums]) |
0 | def padding(val: bytes, size: int, byte: bytes = b'\x00') -> bytes: | |
1 | return val + (size - len(val)) * byte |
0 | from datetime import datetime | |
1 | from io import BufferedIOBase | |
2 | from struct import pack, unpack | |
3 | from typing import Union | |
4 | ||
5 | DEFAULT_CHARSET = 'cp1251' | |
6 | ||
7 | ||
8 | def read_byte(buf: BufferedIOBase) -> int: | |
9 | return unpack('<B', buf.read(1))[0] # type: ignore[no-any-return] | |
10 | ||
11 | ||
12 | def read_short(buf: BufferedIOBase) -> int: | |
13 | return unpack('<H', buf.read(2))[0] # type: ignore[no-any-return] | |
14 | ||
15 | ||
16 | def read_int(buf: BufferedIOBase) -> int: | |
17 | return unpack('<I', buf.read(4))[0] # type: ignore[no-any-return] | |
18 | ||
19 | ||
20 | def read_double(buf: BufferedIOBase) -> int: | |
21 | return unpack('<Q', buf.read(8))[0] # type: ignore[no-any-return] | |
22 | ||
23 | ||
24 | def read_cunicode(buf: BufferedIOBase) -> str: | |
25 | s = b"" | |
26 | b = buf.read(2) | |
27 | while b != b'\x00\x00': | |
28 | s += b | |
29 | b = buf.read(2) | |
30 | return s.decode('utf-16-le') | |
31 | ||
32 | ||
33 | def read_cstring(buf: BufferedIOBase, padding: bool = False) -> str: | |
34 | s = b"" | |
35 | b = buf.read(1) | |
36 | while b != b'\x00': | |
37 | s += b | |
38 | b = buf.read(1) | |
39 | if padding and not len(s) % 2: | |
40 | buf.read(1) # make length + terminator even | |
41 | # TODO: encoding is not clear, unicode-escape has been necessary sometimes | |
42 | return s.decode(DEFAULT_CHARSET) | |
43 | ||
44 | ||
45 | def read_sized_string(buf: BufferedIOBase, string: bool = True) -> Union[str, bytes]: | |
46 | size = read_short(buf) | |
47 | if string: | |
48 | return buf.read(size * 2).decode('utf-16-le') | |
49 | else: | |
50 | return buf.read(size) | |
51 | ||
52 | ||
53 | def get_bits(value: int, start: int, count: int, length: int = 16) -> int: | |
54 | mask = 0 | |
55 | for i in range(count): | |
56 | mask = mask | 1 << i | |
57 | shift = length - start - count | |
58 | return value >> shift & mask | |
59 | ||
60 | ||
61 | def read_dos_datetime(buf: BufferedIOBase) -> datetime: | |
62 | date = read_short(buf) | |
63 | time = read_short(buf) | |
64 | year = get_bits(date, 0, 7) + 1980 | |
65 | month = get_bits(date, 7, 4) | |
66 | day = get_bits(date, 11, 5) | |
67 | hour = get_bits(time, 0, 5) | |
68 | minute = get_bits(time, 5, 6) | |
69 | second = get_bits(time, 11, 5) | |
70 | # fix zeroes | |
71 | month = max(month, 1) | |
72 | day = max(day, 1) | |
73 | return datetime(year, month, day, hour, minute, second) | |
74 | ||
75 | ||
76 | def write_byte(val: int, buf: BufferedIOBase) -> None: | |
77 | buf.write(pack('<B', val)) | |
78 | ||
79 | ||
80 | def write_short(val: int, buf: BufferedIOBase) -> None: | |
81 | buf.write(pack('<H', val)) | |
82 | ||
83 | ||
84 | def write_int(val: int, buf: BufferedIOBase) -> None: | |
85 | buf.write(pack('<I', val)) | |
86 | ||
87 | ||
88 | def write_double(val: int, buf: BufferedIOBase) -> None: | |
89 | buf.write(pack('<Q', val)) | |
90 | ||
91 | ||
92 | def write_cstring(val: str, buf: BufferedIOBase, padding: bool = False) -> None: | |
93 | # val = val.encode('unicode-escape').replace('\\\\', '\\') | |
94 | val_bytes = val.encode(DEFAULT_CHARSET) | |
95 | buf.write(val_bytes + b'\x00') | |
96 | if padding and not len(val_bytes) % 2: | |
97 | buf.write(b'\x00') | |
98 | ||
99 | ||
100 | def write_cunicode(val: str, buf: BufferedIOBase) -> None: | |
101 | uni = val.encode('utf-16-le') | |
102 | buf.write(uni + b'\x00\x00') | |
103 | ||
104 | ||
105 | def write_sized_string(val: str, buf: BufferedIOBase, string: bool = True) -> None: | |
106 | size = len(val) | |
107 | write_short(size, buf) | |
108 | if string: | |
109 | buf.write(val.encode('utf-16-le')) | |
110 | else: | |
111 | buf.write(val.encode()) | |
112 | ||
113 | ||
114 | def put_bits(bits: int, target: int, start: int, count: int, length: int = 16) -> int: | |
115 | return target | bits << (length - start - count) | |
116 | ||
117 | ||
118 | def write_dos_datetime(val: datetime, buf: BufferedIOBase) -> None: | |
119 | date = time = 0 | |
120 | date = put_bits(val.year - 1980, date, 0, 7) | |
121 | date = put_bits(val.month, date, 7, 4) | |
122 | date = put_bits(val.day, date, 11, 5) | |
123 | time = put_bits(val.hour, time, 0, 5) | |
124 | time = put_bits(val.minute, time, 5, 6) | |
125 | time = put_bits(val.second, time, 11, 5) | |
126 | write_short(date, buf) | |
127 | write_short(time, buf) |
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 | #!/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 | [project] | |
1 | name = "pylnk3" | |
2 | description = "Windows LNK File Parser and Creator" | |
3 | keywords = [ | |
4 | "lnk", | |
5 | "shortcut", | |
6 | "windows", | |
7 | ] | |
8 | readme = "README.md" | |
9 | authors = [ | |
10 | { name = "strayge", email = "[email protected]" }, | |
11 | ] | |
12 | classifiers = [ | |
13 | "Intended Audience :: Developers", | |
14 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", | |
15 | "Operating System :: OS Independent", | |
16 | "Topic :: Software Development :: Libraries :: Python Modules", | |
17 | "Programming Language :: Python :: 3.7", | |
18 | "Programming Language :: Python :: 3.8", | |
19 | "Programming Language :: Python :: 3.9", | |
20 | "Programming Language :: Python :: 3.10", | |
21 | "Programming Language :: Python :: 3.11", | |
22 | ] | |
23 | requires-python = ">=3.7" | |
24 | dynamic = [] | |
25 | version = "1.0.0a1" | |
26 | ||
27 | [project.license] | |
28 | text = "GNU Library or Lesser General Public License (LGPL)" | |
29 | ||
30 | [project.urls] | |
31 | homepage = "https://github.com/strayge/pylnk" | |
32 | ||
33 | [project.scripts] | |
34 | pylnk3 = "pylnk3.__main__:main" | |
35 | ||
36 | [tool.pdm.version] | |
37 | source = "scm" | |
38 | ||
39 | [tool.flake8] | |
40 | exclude = [ | |
41 | ".git", | |
42 | ".idea", | |
43 | "__pycache__", | |
44 | "venv*", | |
45 | ] | |
46 | max-line-length = 130 | |
47 | max-cognitive-complexity = 60 | |
48 | ignore = [ | |
49 | "C819", | |
50 | "W503", | |
51 | "F841", | |
52 | ] | |
53 | per-file-ignores = [ | |
54 | "*/__init__.py:F401", | |
55 | "lnk.py:SIM115", | |
56 | ] | |
57 | ||
58 | [tool.mypy] | |
59 | exclude = [ | |
60 | "venv", | |
61 | ] | |
62 | namespace_packages = true | |
63 | ignore_missing_imports = true | |
64 | disallow_untyped_calls = true | |
65 | disallow_untyped_defs = true | |
66 | disallow_incomplete_defs = true | |
67 | check_untyped_defs = true | |
68 | disallow_untyped_decorators = true | |
69 | warn_return_any = true | |
70 | warn_unreachable = true | |
71 | ||
72 | [tool.isort] | |
73 | line_length = 100 | |
74 | multi_line_output = 6 | |
75 | include_trailing_comma = true | |
76 | ||
77 | [tool.pytest.ini_options] | |
78 | testpaths = "tests" | |
79 | addopts = [ | |
80 | "-s", | |
81 | "-l", | |
82 | "--cov", | |
83 | "--cov-branch", | |
84 | "--cov-report=xml", | |
85 | ] | |
86 | ||
87 | [tool.coverage.run] | |
88 | relative_files = true | |
89 | branch = true | |
90 | omit = [ | |
91 | "tests/*", | |
92 | ] | |
93 | ||
94 | [tool.coverage.report] | |
95 | fail_under = 70 | |
96 | precision = 2 | |
97 | ||
98 | [build-system] | |
99 | requires = [ | |
100 | "pdm-pep517>=1.0.0", | |
101 | ] | |
102 | build-backend = "pdm.pep517.api" |
0 | from setuptools import setup | |
1 | ||
2 | ||
3 | with open("README.md", "r") as fh: | |
4 | long_description = fh.read() | |
5 | ||
6 | setup( | |
7 | name="pylnk3", | |
8 | version="0.4.2", | |
9 | py_modules=["pylnk3"], | |
10 | entry_points={ | |
11 | 'console_scripts': [ | |
12 | 'pylnk3 = pylnk3:cli', | |
13 | ], | |
14 | }, | |
15 | description="Windows LNK File Parser and Creator", | |
16 | author="strayge", | |
17 | author_email="[email protected]", | |
18 | url="https://github.com/strayge/pylnk", | |
19 | keywords=["lnk", "shortcut", "windows"], | |
20 | license="GNU Library or Lesser General Public License (LGPL)", | |
21 | classifiers=[ | |
22 | "Programming Language :: Python :: 3.6", | |
23 | "Programming Language :: Python :: 3.7", | |
24 | "Programming Language :: Python :: 3.8", | |
25 | "Programming Language :: Python :: 3.9", | |
26 | "Intended Audience :: Developers", | |
27 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", | |
28 | "Operating System :: OS Independent", | |
29 | "Topic :: Software Development :: Libraries :: Python Modules", | |
30 | ], | |
31 | python_requires='>=3.6', | |
32 | long_description=long_description, | |
33 | long_description_content_type="text/markdown", | |
34 | ) |
0 | import os | |
1 | ||
2 | import pytest | |
3 | ||
4 | ||
5 | @pytest.fixture() | |
6 | def examples_path() -> str: | |
7 | return os.path.join('tests', 'examples') | |
8 | ||
9 | ||
10 | @pytest.fixture() | |
11 | def temp_filename() -> str: | |
12 | return 'temp.lnk' |
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
0 | import os | |
1 | import subprocess | |
2 | import sys | |
3 | from typing import Optional | |
4 | ||
5 | import pytest | |
6 | ||
7 | from pylnk3 import Lnk | |
8 | from pylnk3.structures import PathSegmentEntry | |
9 | from pylnk3.structures.id_list.base import IDListEntry | |
10 | from pylnk3.structures.id_list.path import TYPE_FILE, TYPE_FOLDER | |
11 | ||
12 | ||
13 | def quote_cmd(line: str) -> str: | |
14 | if sys.platform == 'win32': | |
15 | return f'"{line}"' | |
16 | return f"'{line}'" # type: ignore[unreachable] | |
17 | ||
18 | ||
19 | def call_cli(params: str) -> Optional[str]: | |
20 | # copy full environ, otherwise required SYSTEMROOT will be missing on Windows | |
21 | env = os.environ.copy() | |
22 | env['PYTHONPATH'] = os.path.abspath('.') | |
23 | exec_path = 'pylnk3' | |
24 | result = subprocess.run( | |
25 | f'{sys.executable} {exec_path} {params}', check=True, shell=True, | |
26 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
27 | env=env, | |
28 | ) | |
29 | return result.stdout.decode() | |
30 | ||
31 | ||
32 | def check_segment_type(segment: IDListEntry, expected_type: str) -> None: | |
33 | assert isinstance(segment, PathSegmentEntry) | |
34 | assert segment.type == expected_type | |
35 | ||
36 | ||
37 | def test_cli_create_local_file(temp_filename: str) -> None: | |
38 | path = 'C:\\folder\\file.txt' | |
39 | call_cli(f'c {quote_cmd(path)} {temp_filename}') | |
40 | lnk = Lnk(temp_filename) | |
41 | assert lnk.path == path | |
42 | ||
43 | ||
44 | @pytest.mark.parametrize( | |
45 | ('path', 'params', 'last_entry_type'), | |
46 | ( | |
47 | # detect by dot in name | |
48 | ('C:\\folder\\file.txt', '', TYPE_FILE), | |
49 | ('C:\\folder\\folder', '', TYPE_FOLDER), | |
50 | # overrive with cli argument | |
51 | ('C:\\folder\\folder.with.txt', '--directory', TYPE_FOLDER), | |
52 | ('C:\\folder\\file_without_txt', '--file', TYPE_FILE), | |
53 | ), | |
54 | ) | |
55 | def test_cli_local_link_type(temp_filename: str, path: str, params: str, last_entry_type: str) -> None: | |
56 | call_cli(f'c {quote_cmd(path)} {temp_filename} {params}') | |
57 | lnk = Lnk(temp_filename) | |
58 | check_segment_type(lnk.shell_item_id_list.items[-2], TYPE_FOLDER) | |
59 | check_segment_type(lnk.shell_item_id_list.items[-1], last_entry_type) | |
60 | ||
61 | ||
62 | def test_cli_create_net(temp_filename: str) -> None: | |
63 | path = '\\\\192.168.1.1\\SHARE\\path\\file.txt' | |
64 | share = '\\\\192.168.1.1\\SHARE\\' | |
65 | call_cli(f'c {quote_cmd(path)} {temp_filename}') | |
66 | lnk = Lnk(temp_filename) | |
67 | assert lnk.path == share | |
68 | ||
69 | ||
70 | def test_cli_parse(examples_path: str) -> None: | |
71 | path = os.path.join(examples_path, 'local_file.lnk') | |
72 | output = call_cli(f'p {path}') | |
73 | assert output | |
74 | assert 'Path: C:\\Windows\\explorer.exe' in output |
0 | import os | |
1 | ||
2 | import pytest | |
3 | ||
4 | from pylnk3 import Lnk | |
5 | ||
6 | ||
7 | @pytest.mark.parametrize( | |
8 | 'filename,path', | |
9 | ( | |
10 | ('mounted_folder1_file1.lnk', 'Z:\\Downloads\\folder1\\file1.txt'), | |
11 | ('mounted_folder1_file2.lnk', 'Z:\\Downloads\\folder1\\file12.txt'), | |
12 | ('mounted_folder2_file1.lnk', 'Z:\\Downloads\\folder12\\file1.txt'), | |
13 | ('mounted_folder2_file2.lnk', 'Z:\\Downloads\\folder12\\file12.txt'), | |
14 | ), | |
15 | ) | |
16 | def test_local_mounted_share(examples_path: str, temp_filename: str, filename: str, path: str) -> None: | |
17 | """This links contains both local and network path.""" | |
18 | full_filename = os.path.join(examples_path, filename) | |
19 | lnk = Lnk(full_filename) | |
20 | assert lnk.path == path | |
21 | lnk.save(temp_filename) | |
22 | lnk2 = Lnk(temp_filename) | |
23 | assert lnk2.path == path | |
24 | ||
25 | ||
26 | def test_local_disk_link(examples_path: str, temp_filename: str) -> None: | |
27 | filename = os.path.join(examples_path, 'local_disk.lnk') | |
28 | path = 'C:' | |
29 | lnk = Lnk(filename) | |
30 | assert lnk.path == path | |
31 | lnk.save(temp_filename) | |
32 | lnk2 = Lnk(temp_filename) | |
33 | assert lnk2.path == path | |
34 | ||
35 | ||
36 | def test_local_file_link(examples_path: str, temp_filename: str) -> None: | |
37 | filename = os.path.join(examples_path, 'local_file.lnk') | |
38 | path = 'C:\\Windows\\explorer.exe' | |
39 | lnk = Lnk(filename) | |
40 | assert lnk.path == path | |
41 | lnk.save(temp_filename) | |
42 | lnk2 = Lnk(temp_filename) | |
43 | assert lnk2.path == path | |
44 | ||
45 | ||
46 | def test_local_folder_link(examples_path: str, temp_filename: str) -> None: | |
47 | filename = os.path.join(examples_path, 'local_folder.lnk') | |
48 | path = 'C:\\Users\\stray\\Desktop\\New folder' | |
49 | lnk = Lnk(filename) | |
50 | assert lnk.path == path | |
51 | lnk.save(temp_filename) | |
52 | lnk2 = Lnk(temp_filename) | |
53 | assert lnk2.path == path | |
54 | ||
55 | ||
56 | def test_local_send_to_fax(examples_path: str, temp_filename: str) -> None: | |
57 | filename = os.path.join(examples_path, 'send_to_fax.lnk') | |
58 | path = '%windir%\\system32\\WFS.exe' | |
59 | lnk = Lnk(filename) | |
60 | assert lnk.path == path | |
61 | lnk.save(temp_filename) | |
62 | lnk2 = Lnk(temp_filename) | |
63 | assert lnk2.path == path | |
64 | ||
65 | ||
66 | def test_local_recent1(examples_path: str, temp_filename: str) -> None: | |
67 | filename = os.path.join(examples_path, 'recent1.lnk') | |
68 | path = '::{374DE290-123F-4565-9164-39C4925E467B}\\2020M09_01_contract.pdf' | |
69 | lnk = Lnk(filename) | |
70 | assert lnk.path == path | |
71 | lnk.save(temp_filename) | |
72 | lnk2 = Lnk(temp_filename) | |
73 | assert lnk2.path == path | |
74 | ||
75 | ||
76 | def test_local_recent2(examples_path: str, temp_filename: str) -> None: | |
77 | filename = os.path.join(examples_path, 'recent2.lnk') | |
78 | path = '::{088E3905-0323-4B02-9826-5D99428E115F}\\catastrophe_89f317b5c3.7z' | |
79 | lnk = Lnk(filename) | |
80 | assert lnk.path == path | |
81 | lnk.save(temp_filename) | |
82 | lnk2 = Lnk(temp_filename) | |
83 | assert lnk2.path == path | |
84 | ||
85 | ||
86 | def test_empty_idlist(examples_path: str, temp_filename: str) -> None: | |
87 | filename = os.path.join(examples_path, 'desktop.lnk') | |
88 | path = 'C:\\Users\\heznik\\Desktop' | |
89 | lnk = Lnk(filename) | |
90 | assert lnk.path == path | |
91 | lnk.save(temp_filename) | |
92 | lnk2 = Lnk(temp_filename) | |
93 | assert lnk2.path == path |
0 | import os | |
1 | ||
2 | import pytest | |
3 | ||
4 | from pylnk3 import Lnk | |
5 | from pylnk3.structures import ExtraData_EnvironmentVariableDataBlock | |
6 | ||
7 | ||
8 | def check_path(lnk: Lnk, path: str) -> None: | |
9 | assert lnk.path == path | |
10 | ||
11 | ||
12 | def check_extra_env_path(lnk: Lnk, path: str) -> None: | |
13 | assert lnk.extra_data | |
14 | extra_env = [ | |
15 | block for block in lnk.extra_data.blocks | |
16 | if isinstance(block, ExtraData_EnvironmentVariableDataBlock) | |
17 | ][0] | |
18 | # target_unicode fulfilled with \x00 and share name stored in upper case | |
19 | assert extra_env.target_unicode.rstrip('\x00').lower() == path.lower() | |
20 | ||
21 | ||
22 | @pytest.mark.parametrize( | |
23 | 'filename,path', | |
24 | ( | |
25 | ('net_folder1_file1.lnk', '\\\\192.168.138.2\\STORAGE\\Downloads\\folder1\\file1.txt'), | |
26 | ('net_folder1_file2.lnk', '\\\\192.168.138.2\\STORAGE\\Downloads\\folder1\\file12.txt'), | |
27 | ('net_folder2_file1.lnk', '\\\\192.168.138.2\\STORAGE\\Downloads\\folder12\\file1.txt'), | |
28 | ('net_folder2_file2.lnk', '\\\\192.168.138.2\\STORAGE\\Downloads\\folder12\\file12.txt'), | |
29 | ), | |
30 | ) | |
31 | def test_network_lnk(examples_path: str, temp_filename: str, filename: str, path: str) -> None: | |
32 | full_filename = os.path.join(examples_path, filename) | |
33 | # read | |
34 | lnk = Lnk(full_filename) | |
35 | check_path(lnk, path) | |
36 | check_extra_env_path(lnk, path) | |
37 | # write | |
38 | lnk.save(temp_filename) | |
39 | # check | |
40 | lnk2 = Lnk(temp_filename) | |
41 | # check_path(lnk2, path) # FIXME: something wrong with lnk.link_info.base_name | |
42 | check_extra_env_path(lnk2, path) |
0 | import os | |
1 | ||
2 | from pylnk3 import Lnk | |
3 | from pylnk3.structures.id_list.root import ROOT_UWP_APPS | |
4 | ||
5 | ||
6 | def get_sub_blocks(lnk: Lnk) -> dict: | |
7 | uwp_segment = lnk.shell_item_id_list.items[1] | |
8 | sub_blocks = {} | |
9 | for main_block in uwp_segment._blocks: | |
10 | for sub_block in main_block._blocks: | |
11 | if sub_block.name in sub_blocks: | |
12 | continue | |
13 | sub_blocks[sub_block.name] = sub_block.value | |
14 | return sub_blocks | |
15 | ||
16 | ||
17 | def test_uwp_read(examples_path: str) -> None: | |
18 | full_filename = os.path.join(examples_path, 'uwp_calc.lnk') | |
19 | ||
20 | lnk = Lnk(full_filename) | |
21 | uwp_root = lnk.shell_item_id_list.items[0] | |
22 | assert uwp_root.root == ROOT_UWP_APPS | |
23 | ||
24 | sub_blocks = get_sub_blocks(lnk) | |
25 | assert sub_blocks['PackageFamilyName'] == 'Microsoft.WindowsCalculator_8wekyb3d8bbwe' | |
26 | assert sub_blocks['PackageFullName'] == 'Microsoft.WindowsCalculator_10.2008.2.0_x64__8wekyb3d8bbwe' | |
27 | assert sub_blocks['Target'] == 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' | |
28 | assert sub_blocks['Location'] == 'C:\\Program Files\\WindowsApps\\Microsoft.WindowsCalculator_10.2008.2.0_x64__8wekyb3d8bbwe' | |
29 | assert sub_blocks['DisplayName'] == 'Calculator' | |
30 | ||
31 | ||
32 | def test_uwp_write(examples_path: str, temp_filename: str) -> None: | |
33 | full_filename = os.path.join(examples_path, 'uwp_calc.lnk') | |
34 | ||
35 | lnk = Lnk(full_filename) | |
36 | lnk.save(temp_filename) | |
37 | lnk2 = Lnk(temp_filename) | |
38 | ||
39 | uwp_root = lnk2.shell_item_id_list.items[0] | |
40 | assert uwp_root.root == ROOT_UWP_APPS | |
41 | ||
42 | sub_blocks = get_sub_blocks(lnk2) | |
43 | assert sub_blocks['PackageFamilyName'] == 'Microsoft.WindowsCalculator_8wekyb3d8bbwe' | |
44 | assert sub_blocks['PackageFullName'] == 'Microsoft.WindowsCalculator_10.2008.2.0_x64__8wekyb3d8bbwe' | |
45 | assert sub_blocks['Target'] == 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' | |
46 | assert sub_blocks['Location'] == 'C:\\Program Files\\WindowsApps\\Microsoft.WindowsCalculator_10.2008.2.0_x64__8wekyb3d8bbwe' | |
47 | assert sub_blocks['DisplayName'] == 'Calculator' |