Codebase list pylnk / run/84c97d7e-887e-4151-b13e-a8f4159c6cbb/main
New upstream release. Kali Janitor 1 year, 5 months ago
57 changed file(s) with 2870 addition(s) and 2290 deletion(s). Raw diff Collapse all Expand all
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.
+127
-119
PKG-INFO less more
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
44 [![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
55
66 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.
89
910 This library can parse .lnk files and extract all relevant information from
1011 them. Parsing a .lnk file yields a LNK object which can be altered and saved
1516
1617 Limitation: Windows knows lots of different types of shortcuts which all have
1718 different formats. This library currently only supports shortcuts to files and
18 folders on the local machine.
19 folders on the local machine.
1920
2021 ## CLI
2122
2324
2425 #### Parse existed lnk file
2526
26 ```sh
27 ```help
2728 pylnk3 parse [-h] filename [props [props ...]]
2829
2930 positional arguments:
3637
3738 #### Create new lnk file
3839
39 ```sh
40 ```help
4041 usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]]
4142 [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]]
4243 target name
5960 working directory
6061 --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}]
6162 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)
6265 ```
6366
6467 #### Examples
6568 ```sh
69 # windows
6670 pylnk3 p filename.lnk
6771 pylnk3 c c:\prog.exe shortcut.lnk
6872 pylnk3 c \\192.168.1.1\share\file.doc doc.lnk
6973 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"
7076 ```
7177
7278 ## 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
7384
7485 **0.4.2**
7586 changed logic for Lnk.path choose (in case of different paths presents at different structures)
8495 [FIX] correct building links on Linux (now expect Windows-like path)
8596 [FIX] fixed path priority at parsing with both local & remote presents
8697
87
8898 **0.3.0**
8999 added support links to UWP apps
90100
101 **0.2.1**
102 released to PyPI
91103
92 **0.2.1**
93 released to PyPI
94
95
96104 **0.2.0**
97105 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
06 pylnk (0.4.2-0kali2) kali-dev; urgency=medium
17
28 * Rename the binary package python3-pylnk3: now upstream installs pylnk3.py
0 from pylnk3.structures import Lnk
0 from pylnk3.cli import main
1
2 if __name__ == '__main__':
3 main()
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
(New empty file)
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
-119
pylnk3.egg-info/PKG-INFO less more
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
-8
pylnk3.egg-info/SOURCES.txt less more
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
-1
pylnk3.egg-info/dependency_links.txt less more
0
+0
-3
pylnk3.egg-info/entry_points.txt less more
0 [console_scripts]
1 pylnk3 = pylnk3:cli
2
+0
-1
pylnk3.egg-info/top_level.txt less more
0 pylnk3
+0
-1991
pylnk3.py less more
0 #!/usr/bin/env python3
1
2 # original version written by Tim-Christian Mundt (2011):
3 # https://sourceforge.net/p/pylnk/code/HEAD/tree/trunk/pylnk.py
4
5 # converted to python3 by strayge:
6 # https://github.com/strayge/pylnk
7 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
-4
setup.cfg less more
0 [egg_info]
1 tag_build =
2 tag_date = 0
3
+0
-35
setup.py less more
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 )
(New empty file)
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
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'