diff --git a/src/action.py b/src/action.py index 2349891..60862e4 100644 --- a/src/action.py +++ b/src/action.py @@ -17,6 +17,10 @@ # | 4 | from, to | link | # +----+--------------+--------------------------+ # | 5 | command | start | +# +----+--------------+--------------------------+ +# | 6 |path,icon,name| make shortcut on Desktop | +# +----+--------------+--------------------------+ +# | 7 | path | add to %PATH% | # '----+--------------+--------------------------' @@ -28,6 +32,7 @@ import log import uuid import shutil import subprocess +import makelnk def force_copy(pkg_path, pkg_name, data_list, froms, tos): @@ -101,7 +106,7 @@ def start(pkg_name, data_list, cmd): def save_action(actions_list): - tmp_filename = str(uuid.uuid1())+".tmp" # hind pickle file + tmp_filename = "PEinjector.tmp" log.info(f"save action list to {config.TEMP_DIR.replace( '{TEMP}', os.environ['TEMP'].replace("\\", "/"))+'/'+tmp_filename}") with open(config.TEMP_DIR.replace("{TEMP}", os.environ["TEMP"])+"/"+tmp_filename, "wb") as file: @@ -149,4 +154,11 @@ def do_action(actions_list): except Exception as exp: actionlogs = actionlogs + \ f" start \"{i[1]}\" failed: {repr(exp)}\r\n" + elif i[0] == 6: + try: + makelnk.makelnk(i[3], + i[1].replace("\\", "/"), i[2].replace("\\", "/")) + except: + actionlogs = actionlogs + \ + f" make shortcut \"{i[1]}\" failed: {repr(exp)}\r\n" return actionlogs diff --git a/src/loader.py b/src/loader.py index 307d4e6..59ed718 100644 --- a/src/loader.py +++ b/src/loader.py @@ -174,6 +174,8 @@ def load_package(pkg_name): shutil.copytree( pkg_path+"/"+i["from"], data_dir+"/"+i["to"]) + add_action_head = False + if "load" in file_json: open_symlink = config.USE_SYMLINK if "symlink" in file_json["load"] and file_json["load"] == False: @@ -183,6 +185,8 @@ def load_package(pkg_name): if event not in file_json["load"]["mode"]: continue actions[event].append((0, pkg_name)) + if event == "onload": + add_action_head = True if type(file_json["load"]["mode"][event]) != list: log.warn(f"load moudle [{pkg_name}] failed: " + f"Load commands syntax error on {event} (must be a list)") @@ -241,6 +245,22 @@ def load_package(pkg_name): else: log.warn(f"load moudle [{pkg_name}] warning: " + f"Load commands syntax error on {event}[{j+1}] (unknown type), igrone") + if "start" in file_json: + if not add_action_head: + actions[event].append((0, pkg_name)) + if "icon" in file_json["start"]: + for i in ("command", "name", "icon"): + if i not in file_json["start"]["icon"]: + log.warn(f"load moudle [{pkg_name}] warning: " + + f"Load icon syntax error (lost \"{i}\"), igrone") + if len(file_json["start"]["icon"]["icon"]) < 2 or file_json["start"]["icon"]["icon"][1] != ':': + file_json["start"]["icon"]["icon"] = pkg_path + \ + "/"+file_json["start"]["icon"]["icon"] + if len(file_json["start"]["icon"]["command"]) < 2 or file_json["start"]["icon"]["command"][1] != ':': + file_json["start"]["icon"]["command"] = pkg_path + \ + "/"+file_json["start"]["icon"]["command"] + actions["onload"].append( + (6, file_json["start"]["icon"]["command"], file_json["start"]["icon"]["icon"], file_json["start"]["icon"]["name"])) def load(): @@ -257,11 +277,10 @@ def load(): retvar = load_package(packs) if retvar == 0: loaderr_pkgs.append(packs) - filename = action.save_action(actions["onload"]) + action.save_action(actions["onload"]) alog = action.do_action(actions["onboot"]) with open(config.ACTIONLOGPATH.replace("{DISK}", utils.find_disk()), "w") as file: file.write(alog) - os.system(f'..\\env\\pythonw.exe hook.py "{filename}"') except Exception as exp: log.break_err("Exception \n"+str(traceback.format_exc(exp))) raise exp diff --git a/src/makelnk.py b/src/makelnk.py new file mode 100644 index 0000000..986a89f --- /dev/null +++ b/src/makelnk.py @@ -0,0 +1,24 @@ +import os +import log +import pylnk3 + +# Thank the project "pylnk3" + +sysdrive = "X:" +if not os.path.exists(sysdrive+"\\"): + sysdrive = "C:" +username = "Default" +for i in os.listdir(f"{sysdrive}\\Users"): + if i not in ("Default", "Default User", "Public") and os.path.exists(f"{sysdrive}\\Users\\{i}\\Desktop"): + username = i + + +def makelnk(name, exepath, iconpath): + print(f"{sysdrive}\\Users\\{username}" + + f"\\Desktop\\{name}.lnk") + log.info(f"make shortcut \"{name}\"") + pylnk3.for_file(exepath, + lnk_name=f"{sysdrive}\\Users\\{username}" + + f"\\Desktop\\{name}.lnk", + icon_file=iconpath + ) diff --git a/tool/pylnk3.py b/tool/pylnk3.py new file mode 100644 index 0000000..3b298c8 --- /dev/null +++ b/tool/pylnk3.py @@ -0,0 +1,2039 @@ +#!/usr/bin/env python3 + +# original version written by Tim-Christian Mundt (2011): +# https://sourceforge.net/p/pylnk/code/HEAD/tree/trunk/pylnk.py + +# converted to python3 by strayge: +# https://github.com/strayge/pylnk +import argparse +import ntpath +import os +import re +import time +from datetime import datetime +from io import BytesIO, IOBase +from pprint import pformat +from struct import pack, unpack +from typing import Dict, Optional, Tuple, Union + +DEFAULT_CHARSET = 'cp936' # default is "cp1251", we changed it + +# ---- constants + +_SIGNATURE = b'L\x00\x00\x00' +_GUID = b'\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00F' +_LINK_INFO_HEADER_DEFAULT = 0x1C +_LINK_INFO_HEADER_OPTIONAL = 0x24 + +_LINK_FLAGS = ( + 'HasLinkTargetIDList', + 'HasLinkInfo', + 'HasName', + 'HasRelativePath', + 'HasWorkingDir', + 'HasArguments', + 'HasIconLocation', + 'IsUnicode', + 'ForceNoLinkInfo', + # new + 'HasExpString', + 'RunInSeparateProcess', + 'Unused1', + 'HasDarwinID', + 'RunAsUser', + 'HasExpIcon', + 'NoPidlAlias', + 'Unused2', + 'RunWithShimLayer', + 'ForceNoLinkTrack', + 'EnableTargetMetadata', + 'DisableLinkPathTracking', + 'DisableKnownFolderTracking', + 'DisableKnownFolderAlias', + 'AllowLinkToLink', + 'UnaliasOnSave', + 'PreferEnvironmentPath', + 'KeepLocalIDListForUNCTarget', +) + +_FILE_ATTRIBUTES_FLAGS = ( + 'read_only', 'hidden', 'system_file', 'reserved1', + 'directory', 'archive', 'reserved2', 'normal', + 'temporary', 'sparse_file', 'reparse_point', + 'compressed', 'offline', 'not_content_indexed', + 'encrypted', +) + +_MODIFIER_KEYS = ('SHIFT', 'CONTROL', 'ALT') + +WINDOW_NORMAL = "Normal" +WINDOW_MAXIMIZED = "Maximized" +WINDOW_MINIMIZED = "Minimized" +_SHOW_COMMANDS = {1: WINDOW_NORMAL, 3: WINDOW_MAXIMIZED, 7: WINDOW_MINIMIZED} +_SHOW_COMMAND_IDS = dict((v, k) for k, v in _SHOW_COMMANDS.items()) + +DRIVE_UNKNOWN = "Unknown" +DRIVE_NO_ROOT_DIR = "No root directory" +DRIVE_REMOVABLE = "Removable" +DRIVE_FIXED = "Fixed (Hard disk)" +DRIVE_REMOTE = "Remote (Network drive)" +DRIVE_CDROM = "CD-ROM" +DRIVE_RAMDISK = "Ram disk" +_DRIVE_TYPES = {0: DRIVE_UNKNOWN, + 1: DRIVE_NO_ROOT_DIR, + 2: DRIVE_REMOVABLE, + 3: DRIVE_FIXED, + 4: DRIVE_REMOTE, + 5: DRIVE_CDROM, + 6: DRIVE_RAMDISK} +_DRIVE_TYPE_IDS = dict((v, k) for k, v in _DRIVE_TYPES.items()) + +_KEYS = { + 0x30: '0', 0x31: '1', 0x32: '2', 0x33: '3', 0x34: '4', 0x35: '5', 0x36: '6', + 0x37: '7', 0x38: '8', 0x39: '9', 0x41: 'A', 0x42: 'B', 0x43: 'C', 0x44: 'D', + 0x45: 'E', 0x46: 'F', 0x47: 'G', 0x48: 'H', 0x49: 'I', 0x4A: 'J', 0x4B: 'K', + 0x4C: 'L', 0x4D: 'M', 0x4E: 'N', 0x4F: 'O', 0x50: 'P', 0x51: 'Q', 0x52: 'R', + 0x53: 'S', 0x54: 'T', 0x55: 'U', 0x56: 'V', 0x57: 'W', 0x58: 'X', 0x59: 'Y', + 0x5A: 'Z', 0x70: 'F1', 0x71: 'F2', 0x72: 'F3', 0x73: 'F4', 0x74: 'F5', + 0x75: 'F6', 0x76: 'F7', 0x77: 'F8', 0x78: 'F9', 0x79: 'F10', 0x7A: 'F11', + 0x7B: 'F12', 0x7C: 'F13', 0x7D: 'F14', 0x7E: 'F15', 0x7F: 'F16', 0x80: 'F17', + 0x81: 'F18', 0x82: 'F19', 0x83: 'F20', 0x84: 'F21', 0x85: 'F22', 0x86: 'F23', + 0x87: 'F24', 0x90: 'NUM LOCK', 0x91: 'SCROLL LOCK' +} +_KEY_CODES = dict((v, k) for k, v in _KEYS.items()) + +ROOT_MY_COMPUTER = 'MY_COMPUTER' +ROOT_MY_DOCUMENTS = 'MY_DOCUMENTS' +ROOT_NETWORK_SHARE = 'NETWORK_SHARE' +ROOT_NETWORK_SERVER = 'NETWORK_SERVER' +ROOT_NETWORK_PLACES = 'NETWORK_PLACES' +ROOT_NETWORK_DOMAIN = 'NETWORK_DOMAIN' +ROOT_INTERNET = 'INTERNET' +RECYCLE_BIN = 'RECYCLE_BIN' +ROOT_CONTROL_PANEL = 'CONTROL_PANEL' +ROOT_USER = 'USERPROFILE' +ROOT_UWP_APPS = 'APPS' + +_ROOT_LOCATIONS = { + '{20D04FE0-3AEA-1069-A2D8-08002B30309D}': ROOT_MY_COMPUTER, + '{450D8FBA-AD25-11D0-98A8-0800361B1103}': ROOT_MY_DOCUMENTS, + '{54a754c0-4bf1-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SHARE, + '{c0542a90-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SERVER, + '{208D2C60-3AEA-1069-A2D7-08002B30309D}': ROOT_NETWORK_PLACES, + '{46e06680-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_DOMAIN, + '{871C5380-42A0-1069-A2EA-08002B30309D}': ROOT_INTERNET, + '{645FF040-5081-101B-9F08-00AA002F954E}': RECYCLE_BIN, + '{21EC2020-3AEA-1069-A2DD-08002B30309D}': ROOT_CONTROL_PANEL, + '{59031A47-3F72-44A7-89C5-5595FE6B30EE}': ROOT_USER, + '{4234D49B-0245-4DF3-B780-3893943456E1}': ROOT_UWP_APPS, +} +_ROOT_LOCATION_GUIDS = dict((v, k) for k, v in _ROOT_LOCATIONS.items()) + +TYPE_FOLDER = 'FOLDER' +TYPE_FILE = 'FILE' +_ENTRY_TYPES = { + 0x00: 'KNOWN_FOLDER', + 0x31: 'FOLDER', + 0x32: 'FILE', + 0x35: 'FOLDER (UNICODE)', + 0x36: 'FILE (UNICODE)', + 0x802E: 'ROOT_KNOWN_FOLDER', + # founded in doc, not tested + 0x1f: 'ROOT_FOLDER', + 0x61: 'URI', + 0x71: 'CONTROL_PANEL', +} +_ENTRY_TYPE_IDS = dict((v, k) for k, v in _ENTRY_TYPES.items()) + +_DRIVE_PATTERN = re.compile(r'(\w)[:/\\]*$') + +# ---- read and write binary data + + +def read_byte(buf): + return unpack('> shift & mask + + +def read_dos_datetime(buf): + date = read_short(buf) + time = read_short(buf) + year = get_bits(date, 0, 7) + 1980 + month = get_bits(date, 7, 4) + day = get_bits(date, 11, 5) + hour = get_bits(time, 0, 5) + minute = get_bits(time, 5, 6) + second = get_bits(time, 11, 5) + # fix zeroes + month = max(month, 1) + day = max(day, 1) + return datetime(year, month, day, hour, minute, second) + + +def write_byte(val, buf): + buf.write(pack('> pos & 0x1) + + @property + def bytes(self): + bytes = 0 + for pos in range(len(self._flag_names)): + bytes = (self._flags[self._flag_names[pos]] + and 1 or 0) << pos | bytes + return bytes + + def __getitem__(self, key): + if key in self._flags: + return object.__getattribute__(self, '_flags')[key] + return object.__getattribute__(self, key) + + def __setitem__(self, key, value): + if key not in self._flags: + raise KeyError( + "The key '%s' is not defined for those flags." % key) + self._flags[key] = value + + def __getattr__(self, key): + if key in self._flags: + return object.__getattribute__(self, '_flags')[key] + return object.__getattribute__(self, key) + + def __setattr__(self, key, value): + if '_flags' not in self.__dict__: + object.__setattr__(self, key, value) + elif key in self.__dict__: + object.__setattr__(self, key, value) + else: + self.__setitem__(key, value) + + def __str__(self): + return pformat(self._flags, indent=2) + + +class ModifierKeys(Flags): + + def __init__(self, flags_bytes=0): + Flags.__init__(self, _MODIFIER_KEYS, flags_bytes) + + def __str__(self): + s = "" + s += self.CONTROL and "CONTROL+" or "" + s += self.SHIFT and "SHIFT+" or "" + s += self.ALT and "ALT+" or "" + return s + + +# _ROOT_INDEX = { +# 0x00: 'INTERNET_EXPLORER1', +# 0x42: 'LIBRARIES', +# 0x44: 'USERS', +# 0x48: 'MY_DOCUMENTS', +# 0x50: 'MY_COMPUTER', +# 0x58: 'MY_NETWORK_PLACES', +# 0x60: 'RECYCLE_BIN', +# 0x68: 'INTERNET_EXPLORER2', +# 0x70: 'UNKNOWN', +# 0x80: 'MY_GAMES', +# } + + +class RootEntry(object): + + def __init__(self, root): + if root is not None: + # create from text representation + if root in list(_ROOT_LOCATION_GUIDS.keys()): + self.root = root + self.guid = _ROOT_LOCATION_GUIDS[root] + return + + # from binary + root_type = root[0] + index = root[1] + guid_bytes = root[2:18] + self.guid = guid_from_bytes(guid_bytes) + self.root = _ROOT_LOCATIONS.get(self.guid, f"UNKNOWN {self.guid}") + # if self.root == "UNKNOWN": + # self.root = _ROOT_INDEX.get(index, "UNKNOWN") + + @property + def bytes(self): + guid = self.guid[1:-1].replace('-', '') + chars = [bytes([int(x, 16)]) for x in [guid[i:i+2] + for i in range(0, 32, 2)]] + return ( + b'\x1F\x50' + + chars[3] + chars[2] + chars[1] + chars[0] + + chars[5] + chars[4] + chars[7] + chars[6] + + b''.join(chars[8:]) + ) + + def __str__(self): + return "" % self.root + + +class DriveEntry(object): + + def __init__(self, drive: str): + if len(drive) == 23: + # binary data from parsed lnk + self.drive = drive[1:3] + else: + # text representation + m = _DRIVE_PATTERN.match(drive.strip()) + if m: + self.drive = m.groups()[0].upper() + ':' + self.drive = self.drive.encode() + else: + raise FormatException( + "This is not a valid drive: " + str(drive)) + + @property + def bytes(self): + drive = self.drive + padded_str = drive + b'\\' + b'\x00' * 19 + return b'\x2F' + padded_str + # drive = self.drive + # if isinstance(drive, str): + # drive = drive.encode() + # return b'/' + drive + b'\\' + b'\x00' * 19 + + def __str__(self): + return "" % self.drive + + +class PathSegmentEntry(object): + + def __init__(self, bytes=None): + self.type = None + self.file_size = None + self.modified = None + self.short_name = None + self.created = None + self.accessed = None + self.full_name = None + if bytes is None: + return + + buf = BytesIO(bytes) + self.type = _ENTRY_TYPES.get(read_short(buf), 'UNKNOWN') + short_name_is_unicode = self.type.endswith('(UNICODE)') + + if self.type == 'ROOT_KNOWN_FOLDER': + self.full_name = '::' + guid_from_bytes(buf.read(16)) + # then followed Beef0026 structure: + # short size + # short version + # int signature == 0xBEEF0026 + # (16 bytes) created timestamp + # (16 bytes) modified timestamp + # (16 bytes) accessed timestamp + return + + if self.type == 'KNOWN_FOLDER': + _ = read_short(buf) # extra block size + extra_signature = read_int(buf) + if extra_signature == 0x23FEBBEE: + _ = read_short(buf) # unknown + _ = read_short(buf) # guid len + # that format recognized by explorer + self.full_name = '::' + guid_from_bytes(buf.read(16)) + return + + self.file_size = read_int(buf) + self.modified = read_dos_datetime(buf) + unknown = read_short(buf) # FileAttributesL + if short_name_is_unicode: + self.short_name = read_cunicode(buf) + else: + self.short_name = read_cstring(buf, padding=True) + extra_size = read_short(buf) + extra_version = read_short(buf) + extra_signature = read_int(buf) + if extra_signature == 0xBEEF0004: + # indicator_1 = read_short(buf) # see below + # only_83 = read_short(buf) < 0x03 + # unknown = read_short(buf) # 0x04 + # self.is_unicode = read_short(buf) == 0xBeef + self.created = read_dos_datetime(buf) # 4 bytes + self.accessed = read_dos_datetime(buf) # 4 bytes + # offset from start of extra_size + offset_unicode = read_short(buf) + # only_83_2 = offset_unicode >= indicator_1 or offset_unicode < 0x14 + if extra_version >= 7: + offset_ansi = read_short(buf) + file_reference = read_double(buf) + unknown2 = read_double(buf) + long_string_size = 0 + if extra_version >= 3: + long_string_size = read_short(buf) + if extra_version >= 9: + unknown4 = read_int(buf) + if extra_version >= 8: + unknown5 = read_int(buf) + if extra_version >= 3: + self.full_name = read_cunicode(buf) + if long_string_size > 0: + if extra_version >= 7: + self.localized_name = read_cunicode(buf) + else: + self.localized_name = read_cstring(buf) + version_offset = read_short(buf) + + @classmethod + def create_for_path(cls, path): + entry = cls() + entry.type = os.path.isdir(path) and TYPE_FOLDER or TYPE_FILE + try: + st = os.stat(path) + entry.file_size = st.st_size + entry.modified = datetime.fromtimestamp(st.st_mtime) + entry.created = datetime.fromtimestamp(st.st_ctime) + entry.accessed = datetime.fromtimestamp(st.st_atime) + except FileNotFoundError: + now = datetime.now() + entry.file_size = 0 + entry.modified = now + entry.created = now + entry.accessed = now + entry.short_name = ntpath.split(path)[1] + entry.full_name = entry.short_name + return entry + + def _validate(self): + if self.type is None: + raise MissingInformationException( + "Type is missing, choose either TYPE_FOLDER or TYPE_FILE.") + if self.file_size is None: + if self.type.startswith('FOLDER') or self.type in ['KNOWN_FOLDER', 'ROOT_KNOWN_FOLDER']: + self.file_size = 0 + else: + raise MissingInformationException("File size missing") + if self.created is None: + self.created = datetime.now() + if self.modified is None: + self.modified = datetime.now() + if self.accessed is None: + self.accessed = datetime.now() + # if self.modified is None or self.accessed is None or self.created is None: + # raise MissingInformationException("Date information missing") + if self.full_name is None: + raise MissingInformationException("A full name is missing") + if self.short_name is None: + self.short_name = self.full_name + + @property + def bytes(self): + if self.full_name is None: + return + self._validate() + out = BytesIO() + entry_type = self.type + + if entry_type == 'KNOWN_FOLDER': + write_short(_ENTRY_TYPE_IDS[entry_type], out) + write_short(0x1A, out) # size + write_int(0x23FEBBEE, out) # extra signature + write_short(0x00, out) # extra signature + write_short(0x10, out) # guid size + out.write(bytes_from_guid(self.full_name.strip(':'))) + return out.getvalue() + + if entry_type == 'ROOT_KNOWN_FOLDER': + write_short(_ENTRY_TYPE_IDS[entry_type], out) + out.write(bytes_from_guid(self.full_name.strip(':'))) + write_short(0x26, out) # 0xBEEF0026 structure size + write_short(0x01, out) # version + write_int(0xBEEF0026, out) # extra signature + write_int(0x11, out) # some flag for containing datetime + write_double(0x00, out) # created datetime + write_double(0x00, out) # modified datetime + write_double(0x00, out) # accessed datetime + write_short(0x14, out) # unknown + return out.getvalue() + + short_name_len = len(self.short_name) + 1 + try: + self.short_name.encode("ascii") + short_name_is_unicode = False + short_name_len += short_name_len % 2 # padding + except (UnicodeEncodeError, UnicodeDecodeError): + short_name_is_unicode = True + short_name_len = short_name_len * 2 + self.type += " (UNICODE)" + write_short(_ENTRY_TYPE_IDS[entry_type], out) + write_int(self.file_size, out) + write_dos_datetime(self.modified, out) + write_short(0x10, out) + if short_name_is_unicode: + write_cunicode(self.short_name, out) + else: + write_cstring(self.short_name, out, padding=True) + indicator = 24 + 2 * len(self.short_name) + write_short(indicator, out) # size + write_short(0x03, out) # version + write_short(0x04, out) # signature part1 + write_short(0xBeef, out) # signature part2 + write_dos_datetime(self.created, out) + write_dos_datetime(self.accessed, out) + offset_unicode = 0x14 # fixed data structure, always the same + write_short(offset_unicode, out) + offset_ansi = 0 # we always write unicode + write_short(offset_ansi, out) # long_string_size + write_cunicode(self.full_name, out) + offset_part2 = 0x0E + short_name_len + write_short(offset_part2, out) + return out.getvalue() + + def __str__(self): + return "" % self.full_name + + +class UwpSubBlock: + + block_names = { + 0x11: 'PackageFamilyName', + # 0x0e: '', + # 0x19: '', + 0x15: 'PackageFullName', + 0x05: 'Target', + 0x0f: 'Location', + 0x20: 'RandomGuid', + 0x0c: 'Square150x150Logo', + 0x02: 'Square44x44Logo', + 0x0d: 'Wide310x150Logo', + # 0x04: '', + # 0x05: '', + 0x13: 'Square310x310Logo', + # 0x0e: '', + 0x0b: 'DisplayName', + 0x14: 'Square71x71Logo', + 0x64: 'RandomByte', + 0x0a: 'DisplayName', + # 0x07: '', + } + + block_types = { + 'string': [0x11, 0x15, 0x05, 0x0f, 0x0c, 0x02, 0x0d, 0x13, 0x0b, 0x14, 0x0a], + } + + def __init__(self, bytes=None, type=None, value=None): + self._data = bytes or b'' + self.type = type + self.value = value + self.name = None + if self.type is not None: + self.name = self.block_names.get(self.type, 'UNKNOWN') + if not bytes: + return + buf = BytesIO(bytes) + self.type = read_byte(buf) + self.name = self.block_names.get(self.type, 'UNKNOWN') + + self.value = self._data[1:] # skip type + if self.type in self.block_types['string']: + unknown = read_int(buf) + probably_type = read_int(buf) + if probably_type == 0x1f: + string_len = read_int(buf) + self.value = read_cunicode(buf) + + def __str__(self): + string = f'UwpSubBlock {self.name} ({hex(self.type)}): {self.value}' + return string.strip() + + @property + def bytes(self): + out = BytesIO() + if self.value: + if isinstance(self.value, str): + string_len = len(self.value) + 1 + + write_byte(self.type, out) + write_int(0, out) + write_int(0x1f, out) + + write_int(string_len, out) + write_cunicode(self.value, out) + if string_len % 2 == 1: # padding + write_short(0, out) + + elif isinstance(self.value, bytes): + write_byte(self.type, out) + out.write(self.value) + + result = out.getvalue() + return result + + +class UwpMainBlock: + magic = b'\x31\x53\x50\x53' + + def __init__(self, bytes=None, guid: Optional[str] = None, blocks=None): + self._data = bytes or b'' + self._blocks = blocks or [] + self.guid: str = guid + if not bytes: + return + buf = BytesIO(bytes) + magic = buf.read(4) + self.guid = guid_from_bytes(buf.read(16)) + # read sub blocks + while True: + sub_block_size = read_int(buf) + if not sub_block_size: # last size is zero + break + sub_block_data = buf.read( + sub_block_size - 4) # includes block_size + self._blocks.append(UwpSubBlock(sub_block_data)) + + def __str__(self): + string = f' {self.guid}:\n' + for block in self._blocks: + string += f' {block}\n' + return string.strip() + + @property + def bytes(self): + blocks_bytes = [block.bytes for block in self._blocks] + out = BytesIO() + out.write(self.magic) + out.write(bytes_from_guid(self.guid)) + for block in blocks_bytes: + write_int(len(block) + 4, out) + out.write(block) + write_int(0, out) + result = out.getvalue() + return result + + +class UwpSegmentEntry: + magic = b'APPS' + header = b'\x08\x00\x03\x00\x00\x00\x00\x00\x00\x00' + + def __init__(self, bytes=None): + self._blocks = [] + self._data = bytes + if bytes is None: + return + buf = BytesIO(bytes) + unknown = read_short(buf) + size = read_short(buf) + magic = buf.read(4) # b'APPS' + blocks_size = read_short(buf) + unknown2 = buf.read(10) + # read main blocks + while True: + block_size = read_int(buf) + if not block_size: # last size is zero + break + block_data = buf.read(block_size - 4) # includes block_size + self._blocks.append(UwpMainBlock(block_data)) + + def __str__(self): + string = ':\n' + for block in self._blocks: + string += f' {block}\n' + return string.strip() + + @property + def bytes(self): + blocks_bytes = [block.bytes for block in self._blocks] + # with terminator + blocks_size = sum([len(block) + 4 for block in blocks_bytes]) + 4 + size = ( + 2 # size + + len(self.magic) + + 2 # second size + + len(self.header) + + blocks_size # blocks with terminator + ) + + out = BytesIO() + write_short(0, out) + write_short(size, out) + out.write(self.magic) + write_short(blocks_size, out) + out.write(self.header) + for block in blocks_bytes: + write_int(len(block) + 4, out) + out.write(block) + write_int(0, out) # empty block + write_short(0, out) # ?? + + result = out.getvalue() + return result + + @classmethod + def create(cls, package_family_name, target, location=None, logo44x44=None): + segment = cls() + + blocks = [ + UwpSubBlock(type=0x11, value=package_family_name), + UwpSubBlock( + type=0x0e, value=b'\x00\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00'), + UwpSubBlock(type=0x05, value=target), + ] + if location: + # need for relative icon path + blocks.append(UwpSubBlock(type=0x0f, value=location)) + main1 = UwpMainBlock( + guid='{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', blocks=blocks) + segment._blocks.append(main1) + + if logo44x44: + main2 = UwpMainBlock( + guid='{86D40B4D-9069-443C-819A-2A54090DCCEC}', + blocks=[UwpSubBlock(type=0x02, value=logo44x44)] + ) + segment._blocks.append(main2) + + return segment + + +class LinkTargetIDList(object): + + def __init__(self, bytes=None): + self.items = [] + if bytes is not None: + buf = BytesIO(bytes) + raw = [] + entry_len = read_short(buf) + while entry_len > 0: + # the length includes the size + raw.append(buf.read(entry_len - 2)) + entry_len = read_short(buf) + self._interpret(raw) + + def _interpret(self, raw): + if not raw: + return + elif raw[0][0] == 0x1F: + self.items.append(RootEntry(raw[0])) + if self.items[0].root == ROOT_MY_COMPUTER: + if len(raw[1]) == 0x17: + self.items.append(DriveEntry(raw[1])) + elif raw[1][0:2] == b'\x2E\x80': # ROOT_KNOWN_FOLDER + self.items.append(PathSegmentEntry(raw[1])) + else: + raise ValueError( + "This seems to be an absolute link which requires a drive as second element.") + items = raw[2:] + elif self.items[0].root == ROOT_NETWORK_PLACES: + raise NotImplementedError( + "Parsing network lnks has not yet been implemented. " + "If you need it just contact me and we'll see..." + ) + else: + items = raw[1:] + else: + items = raw + for item in items: + if item[4:8] == b'APPS': + self.items.append(UwpSegmentEntry(item)) + else: + self.items.append(PathSegmentEntry(item)) + + def get_path(self): + segments = [] + for item in self.items: + if type(item) == RootEntry: + segments.append('%' + item.root + '%') + elif type(item) == DriveEntry: + segments.append(item.drive.decode()) + elif type(item) == PathSegmentEntry: + if item.full_name is not None: + segments.append(item.full_name) + else: + segments.append(item) + return '\\'.join(segments) + + def _validate(self): + if not len(self.items): + return + if type(self.items[0]) == RootEntry and self.items[0].root == ROOT_MY_COMPUTER: + if type(self.items[1]) == DriveEntry: + return + if type(self.items[1]) == PathSegmentEntry and self.items[1].full_name.startswith('::'): + return + raise ValueError("A drive is required for absolute lnks") + + @property + def bytes(self): + self._validate() + out = BytesIO() + for item in self.items: + bytes = item.bytes + # skip invalid + if bytes is None: + continue + write_short(len(bytes) + 2, out) # len + terminator + out.write(bytes) + out.write(b'\x00\x00') + return out.getvalue() + + def __str__(self): + string = ':\n' + for item in self.items: + string += f' {item}\n' + return string.strip() + + +class LinkInfo(object): + + def __init__(self, lnk=None): + if lnk is not None: + self.start = lnk.tell() + self.size = read_int(lnk) + self.header_size = read_int(lnk) + link_info_flags = read_int(lnk) + self.local = link_info_flags & 1 + self.remote = link_info_flags & 2 + self.offs_local_volume_table = read_int(lnk) + self.offs_local_base_path = read_int(lnk) + self.offs_network_volume_table = read_int(lnk) + self.offs_base_name = read_int(lnk) + if self.header_size >= _LINK_INFO_HEADER_OPTIONAL: + # TODO: read the unicode stuff + print("TODO: read the unicode stuff") + self._parse_path_elements(lnk) + else: + self.size = None + self.header_size = _LINK_INFO_HEADER_DEFAULT + self.local = 0 + self.remote = 0 + self.offs_local_volume_table = 0 + self.offs_local_base_path = 0 + self.offs_network_volume_table = 0 + self.offs_base_name = 0 + self.drive_type = None + self.drive_serial = None + self.volume_label = None + self.local_base_path = None + self.network_share_name = None + self.base_name = None + self._path = None + + def _parse_path_elements(self, lnk): + if self.remote: + # 20 is the offset of the network share name + lnk.seek(self.start + self.offs_network_volume_table + 20) + self.network_share_name = read_cstring(lnk) + lnk.seek(self.start + self.offs_base_name) + self.base_name = read_cstring(lnk) + if self.local: + lnk.seek(self.start + self.offs_local_volume_table + 4) + self.drive_type = _DRIVE_TYPES.get(read_int(lnk)) + self.drive_serial = read_int(lnk) + lnk.read(4) # volume name offset (10h) + self.volume_label = read_cstring(lnk) + lnk.seek(self.start + self.offs_local_base_path) + self.local_base_path = read_cstring(lnk) + # TODO: unicode + self.make_path() + + def make_path(self): + if self.remote: + self._path = self.network_share_name + '\\' + self.base_name + if self.local: + self._path = self.local_base_path + + def write(self, lnk): + if self.remote is None: + raise MissingInformationException("No location information given.") + self.start = lnk.tell() + self._calculate_sizes_and_offsets() + write_int(self.size, lnk) + write_int(self.header_size, lnk) + write_int((self.local and 1) + (self.remote and 2), lnk) + write_int(self.offs_local_volume_table, lnk) + write_int(self.offs_local_base_path, lnk) + write_int(self.offs_network_volume_table, lnk) + write_int(self.offs_base_name, lnk) + if self.remote: + self._write_network_volume_table(lnk) + write_cstring(self.base_name, lnk, padding=False) + else: + self._write_local_volume_table(lnk) + write_cstring(self.local_base_path, lnk, padding=False) + write_byte(0, lnk) + + def _calculate_sizes_and_offsets(self): + # len(self.base_name) + 1 # zero terminated strings + self.size_base_name = 1 + self.size = 28 + self.size_base_name + if self.remote: + self.size_network_volume_table = 20 + \ + len(self.network_share_name) + len(self.base_name) + 1 + self.size += self.size_network_volume_table + self.offs_local_volume_table = 0 + self.offs_local_base_path = 0 + self.offs_network_volume_table = 28 + self.offs_base_name = self.offs_network_volume_table + \ + self.size_network_volume_table + else: + self.size_local_volume_table = 16 + len(self.volume_label) + 1 + self.size_local_base_path = len(self.local_base_path) + 1 + self.size += self.size_local_volume_table + self.size_local_base_path + self.offs_local_volume_table = 28 + self.offs_local_base_path = self.offs_local_volume_table + \ + self.size_local_volume_table + self.offs_network_volume_table = 0 + self.offs_base_name = self.offs_local_base_path + self.size_local_base_path + + def _write_network_volume_table(self, buf): + write_int(self.size_network_volume_table, buf) + write_int(2, buf) # ? + write_int(20, buf) # size of Network Volume Table + write_int(0, buf) # ? + write_int(131072, buf) # ? + write_cstring(self.network_share_name, buf) + + def _write_local_volume_table(self, buf): + write_int(self.size_local_volume_table, buf) + try: + drive_type = _DRIVE_TYPE_IDS[self.drive_type] + except KeyError: + raise ValueError( + "This is not a valid drive type: %s" % self.drive_type) + write_int(drive_type, buf) + write_int(self.drive_serial, buf) + write_int(16, buf) # volume name offset + write_cstring(self.volume_label, buf) + + @property + def path(self): + return self._path + + def __str__(self): + s = "File Location Info:" + if self._path is None: + return s + " " + if self.remote: + s += "\n (remote)" + s += "\n Network Share: %s" % self.network_share_name + s += "\n Base Name: %s" % self.base_name + else: + s += "\n (local)" + s += "\n Volume Type: %s" % self.drive_type + s += "\n Volume Serial Number: %s" % self.drive_serial + s += "\n Volume Label: %s" % self.volume_label + s += "\n Path: %s" % self.local_base_path + return s + + +EXTRA_DATA_TYPES = { + 0xA0000002: 'ConsoleDataBlock', # size 0x000000CC + 0xA0000004: 'ConsoleFEDataBlock', # size 0x0000000C + 0xA0000006: 'DarwinDataBlock', # size 0x00000314 + 0xA0000001: 'EnvironmentVariableDataBlock', # size 0x00000314 + 0xA0000007: 'IconEnvironmentDataBlock', # size 0x00000314 + 0xA000000B: 'KnownFolderDataBlock', # size 0x0000001C + 0xA0000009: 'PropertyStoreDataBlock', # size >= 0x0000000C + 0xA0000008: 'ShimDataBlock', # size >= 0x00000088 + 0xA0000005: 'SpecialFolderDataBlock', # size 0x00000010 + 0xA0000003: 'VistaAndAboveIDListDataBlock', # size 0x00000060 + 0xA000000C: 'VistaIDListDataBlock', # size 0x00000173 +} + + +class ExtraData_Unparsed(object): + def __init__(self, bytes=None, signature=None, data=None): + self._signature = signature + self._size = None + self.data = data + # if data: + # self._size = len(data) + if bytes: + # self._size = len(bytes) + self.data = bytes + # self.read(bytes) + + # def read(self, bytes): + # buf = BytesIO(bytes) + # size = len(bytes) + # # self._size = read_int(buf) + # # self._signature = read_int(buf) + # self.data = buf.read(self._size - 8) + + def bytes(self): + buf = BytesIO() + write_int(len(self.data)+8, buf) + write_int(self._signature, buf) + buf.write(self.data) + return buf.getvalue() + + def __str__(self): + s = 'ExtraDataBlock\n signature %s\n data: %s' % ( + hex(self._signature), self.data) + return s + + +def padding(val, size, byte=b'\x00'): + return val + (size-len(val)) * byte + + +class ExtraData_IconEnvironmentDataBlock(object): + def __init__(self, bytes=None): + # self._size = None + # self._signature = None + self._signature = 0xA0000007 + self.target_ansi = None + self.target_unicode = None + if bytes: + self.read(bytes) + + def read(self, bytes): + buf = BytesIO(bytes) + # self._size = read_int(buf) + # self._signature = read_int(buf) + self.target_ansi = buf.read(260).decode('ansi') + self.target_unicode = buf.read(520).decode('utf-16-le') + + def bytes(self): + target_ansi = padding(self.target_ansi.encode(), 260) + target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520) + size = 8 + len(target_ansi) + len(target_unicode) + assert self._signature == 0xA0000007 + assert size == 0x00000314 + buf = BytesIO() + write_int(size, buf) + write_int(self._signature, buf) + buf.write(target_ansi) + buf.write(target_unicode) + return buf.getvalue() + + def __str__(self): + target_ansi = self.target_ansi.replace('\x00', '') + target_unicode = self.target_unicode.replace('\x00', '') + s = f'IconEnvironmentDataBlock\n TargetAnsi: { + target_ansi}\n TargetUnicode: {target_unicode}' + return s + + +def guid_to_str(guid): + ordered = [guid[3], guid[2], guid[1], guid[0], guid[5], guid[4], + guid[7], guid[6], guid[8], guid[9], guid[10], guid[11], + guid[12], guid[13], guid[14], guid[15]] + res = "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([ + x for x in ordered]) + # print(guid, res) + return res + + +class TypedPropertyValue(object): + # types: [MS-OLEPS] section 2.15 + def __init__(self, bytes=None, type=None, value=None): + self.type = type + self.value = value + if bytes: + self.type = read_short(BytesIO(bytes)) + padding = bytes[2:4] + self.value = bytes[4:] + + def set_string(self, value): + self.type = 0x1f + buf = BytesIO() + write_int(len(value)+2, buf) + buf.write(value.encode('utf-16-le')) + # terminator (included in size) + buf.write(b'\x00\x00\x00\x00') + # padding (not included in size) + if len(value) % 2: + buf.write(b'\x00\x00') + self.value = buf.getvalue() + + @property + def bytes(self): + buf = BytesIO() + write_short(self.type, buf) + write_short(0x0000, buf) + buf.write(self.value) + return buf.getvalue() + + def __str__(self): + value = self.value + if self.type == 0x1F: + size = value[:4] + value = value[4:].decode('utf-16-le') + if self.type == 0x15: + value = unpack('= 0x0000000C + + buf = BytesIO() + write_int(size, buf) + write_int(self._signature, buf) + buf.write(stores) + write_int(0x00000000, buf) + return buf.getvalue() + + def __str__(self): + s = 'PropertyStoreDataBlock' + for prop_store in self.stores: + s += '\n %s' % str(prop_store) + return s + + +class ExtraData_EnvironmentVariableDataBlock(object): + def __init__(self, bytes=None): + self._signature = 0xA0000001 + self.target_ansi = None + self.target_unicode = None + if bytes: + self.read(bytes) + + def read(self, bytes): + buf = BytesIO(bytes) + self.target_ansi = buf.read(260).decode() + self.target_unicode = buf.read(520).decode('utf-16-le') + + def bytes(self): + target_ansi = padding(self.target_ansi.encode(), 260) + target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520) + size = 8 + len(target_ansi) + len(target_unicode) + assert self._signature == 0xA0000001 + assert size == 0x00000314 + buf = BytesIO() + write_int(size, buf) + write_int(self._signature, buf) + buf.write(target_ansi) + buf.write(target_unicode) + return buf.getvalue() + + def __str__(self): + target_ansi = self.target_ansi.replace('\x00', '') + target_unicode = self.target_unicode.replace('\x00', '') + s = f'EnvironmentVariableDataBlock\n TargetAnsi: { + target_ansi}\n TargetUnicode: {target_unicode}' + return s + + +EXTRA_DATA_TYPES_CLASSES = { + 'IconEnvironmentDataBlock': ExtraData_IconEnvironmentDataBlock, + 'PropertyStoreDataBlock': ExtraData_PropertyStoreDataBlock, + 'EnvironmentVariableDataBlock': ExtraData_EnvironmentVariableDataBlock, +} + + +class ExtraData(object): + # EXTRA_DATA = *EXTRA_DATA_BLOCK TERMINAL_BLOCK + def __init__(self, lnk=None, blocks=None): + self.blocks = [] + if blocks: + self.blocks = blocks + if lnk is None: + return + while True: + size = read_int(lnk) + if size < 4: # TerminalBlock + break + signature = read_int(lnk) + bytes = lnk.read(size-8) + # lnk.seek(-8, 1) + block_type = EXTRA_DATA_TYPES[signature] + if block_type in EXTRA_DATA_TYPES_CLASSES: + block_class = EXTRA_DATA_TYPES_CLASSES[block_type] + block = block_class(bytes=bytes) + else: + block_class = ExtraData_Unparsed + block = block_class(bytes=bytes, signature=signature) + self.blocks.append(block) + + @property + def bytes(self): + result = b'' + for block in self.blocks: + result += block.bytes() + result += b'\x00\x00\x00\x00' # TerminalBlock + return result + + def __str__(self): + s = '' + for block in self.blocks: + s += '\n' + str(block) + return s + + +class Lnk(object): + + def __init__(self, f=None): + self.file = None + if type(f) == str or type(f) == str: + self.file = f + try: + f = open(self.file, 'rb') + except IOError: + self.file += ".lnk" + f = open(self.file, 'rb') + # defaults + self.link_flags = Flags(_LINK_FLAGS) + self.file_flags = Flags(_FILE_ATTRIBUTES_FLAGS) + self.creation_time = datetime.now() + self.access_time = datetime.now() + self.modification_time = datetime.now() + self.file_size = 0 + self.icon_index = 0 + self._show_command = WINDOW_NORMAL + self.hot_key = None + self._link_info = LinkInfo() + self.description = None + self.relative_path = None + self.work_dir = None + self.arguments = None + self.icon = None + self.extra_data = None + if f is not None: + assert_lnk_signature(f) + self._parse_lnk_file(f) + if self.file: + f.close() + + def _read_hot_key(self, lnk): + low = read_byte(lnk) + high = read_byte(lnk) + key = _KEYS.get(low, '') + modifier = high and str(ModifierKeys(high)) or '' + return modifier + key + + def _write_hot_key(self, hot_key, lnk): + if hot_key is None or not hot_key: + low = high = 0 + else: + hot_key = hot_key.split('+') + try: + low = _KEY_CODES[hot_key[-1]] + except KeyError: + raise InvalidKeyException( + "Cannot find key code for %s" % hot_key[1]) + modifiers = ModifierKeys() + for modifier in hot_key[:-1]: + modifiers[modifier.upper()] = True + high = modifiers.bytes + write_byte(low, lnk) + write_byte(high, lnk) + + def _parse_lnk_file(self, lnk): + # SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO] [STRING_DATA] *EXTRA_DATA + + # SHELL_LINK_HEADER + lnk.seek(20) # after signature and guid + self.link_flags.set_flags(read_int(lnk)) + self.file_flags.set_flags(read_int(lnk)) + self.creation_time = convert_time_to_unix(read_double(lnk)) + self.access_time = convert_time_to_unix(read_double(lnk)) + self.modification_time = convert_time_to_unix(read_double(lnk)) + self.file_size = read_int(lnk) + self.icon_index = read_int(lnk) + show_command = read_int(lnk) + self._show_command = _SHOW_COMMANDS[show_command] if show_command in _SHOW_COMMANDS else _SHOW_COMMANDS[1] + self.hot_key = self._read_hot_key(lnk) + lnk.read(10) # reserved (0) + + # LINKTARGET_IDLIST (HasLinkTargetIDList) + if self.link_flags.HasLinkTargetIDList: + shell_item_id_list_size = read_short(lnk) + self.shell_item_id_list = LinkTargetIDList( + lnk.read(shell_item_id_list_size)) + + # LINKINFO (HasLinkInfo) + if self.link_flags.HasLinkInfo and not self.link_flags.ForceNoLinkInfo: + self._link_info = LinkInfo(lnk) + lnk.seek(self._link_info.start + self._link_info.size) + + # STRING_DATA = [NAME_STRING] [RELATIVE_PATH] [WORKING_DIR] [COMMAND_LINE_ARGUMENTS] [ICON_LOCATION] + if self.link_flags.HasName: + self.description = read_sized_string( + lnk, self.link_flags.IsUnicode) + if self.link_flags.HasRelativePath: + self.relative_path = read_sized_string( + lnk, self.link_flags.IsUnicode) + if self.link_flags.HasWorkingDir: + self.work_dir = read_sized_string(lnk, self.link_flags.IsUnicode) + if self.link_flags.HasArguments: + self.arguments = read_sized_string(lnk, self.link_flags.IsUnicode) + if self.link_flags.HasIconLocation: + self.icon = read_sized_string(lnk, self.link_flags.IsUnicode) + + # *EXTRA_DATA + self.extra_data = ExtraData(lnk) + + def save(self, f: Optional[Union[str, IOBase]] = None, force_ext=False): + if f is None: + f = self.file + if f is None: + raise ValueError("File (name) missing for saving the lnk") + is_file = hasattr(f, 'write') + if not is_file: + if not type(f) == str and not type(f) == str: + raise ValueError( + "Need a writeable object or a file name to save to, got %s" % f) + if force_ext: + if not f.lower().endswith('.lnk'): + f += '.lnk' + f = open(f, 'wb') + self.write(f) + # only close the stream if it's our own + if not is_file: + f.close() + + def write(self, lnk): + lnk.write(_SIGNATURE) + lnk.write(_GUID) + write_int(self.link_flags.bytes, lnk) + write_int(self.file_flags.bytes, lnk) + write_double(convert_time_to_windows(self.creation_time), lnk) + write_double(convert_time_to_windows(self.access_time), lnk) + write_double(convert_time_to_windows(self.modification_time), lnk) + write_int(self.file_size, lnk) + write_int(self.icon_index, lnk) + write_int(_SHOW_COMMAND_IDS[self._show_command], lnk) + self._write_hot_key(self.hot_key, lnk) + lnk.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') # reserved + if self.link_flags.HasLinkTargetIDList: + shell_item_id_list = self.shell_item_id_list.bytes + write_short(len(shell_item_id_list), lnk) + lnk.write(shell_item_id_list) + if self.link_flags.HasLinkInfo: + self._link_info.write(lnk) + if self.link_flags.HasName: + write_sized_string(self.description, lnk, + self.link_flags.IsUnicode) + if self.link_flags.HasRelativePath: + write_sized_string(self.relative_path, lnk, + self.link_flags.IsUnicode) + if self.link_flags.HasWorkingDir: + write_sized_string(self.work_dir, lnk, self.link_flags.IsUnicode) + if self.link_flags.HasArguments: + write_sized_string(self.arguments, lnk, self.link_flags.IsUnicode) + if self.link_flags.HasIconLocation: + write_sized_string(self.icon, lnk, self.link_flags.IsUnicode) + if self.extra_data: + lnk.write(self.extra_data.bytes) + else: + lnk.write(b'\x00\x00\x00\x00') + + def _get_shell_item_id_list(self): + return self._shell_item_id_list + + def _set_shell_item_id_list(self, shell_item_id_list): + self._shell_item_id_list = shell_item_id_list + self.link_flags.HasLinkTargetIDList = shell_item_id_list is not None + shell_item_id_list = property( + _get_shell_item_id_list, _set_shell_item_id_list) + + def _get_link_info(self): + return self._link_info + + def _set_link_info(self, link_info): + self._link_info = link_info + self.link_flags.ForceNoLinkInfo = link_info is None + self.link_flags.HasLinkInfo = link_info is not None + link_info = property(_get_link_info, _set_link_info) + + def _get_description(self): + return self._description + + def _set_description(self, description): + self._description = description + self.link_flags.HasName = description is not None + description = property(_get_description, _set_description) + + def _get_relative_path(self): + return self._relative_path + + def _set_relative_path(self, relative_path): + self._relative_path = relative_path + self.link_flags.HasRelativePath = relative_path is not None + relative_path = property(_get_relative_path, _set_relative_path) + + def _get_work_dir(self): + return self._work_dir + + def _set_work_dir(self, work_dir): + self._work_dir = work_dir + self.link_flags.HasWorkingDir = work_dir is not None + work_dir = working_dir = property(_get_work_dir, _set_work_dir) + + def _get_arguments(self): + return self._arguments + + def _set_arguments(self, arguments): + self._arguments = arguments + self.link_flags.HasArguments = arguments is not None + arguments = property(_get_arguments, _set_arguments) + + def _get_icon(self): + return self._icon + + def _set_icon(self, icon): + self._icon = icon + self.link_flags.HasIconLocation = icon is not None + icon = property(_get_icon, _set_icon) + + def _get_window_mode(self): + return self._show_command + + def _set_window_mode(self, value): + if value not in list(_SHOW_COMMANDS.values()): + raise ValueError( + "Not a valid window mode: %s. Choose any of pylnk.WINDOW_*" % value) + self._show_command = value + window_mode = show_command = property(_get_window_mode, _set_window_mode) + + @property + def path(self): + # lnk can contains several different paths at different structures + # here is some logic consistent with link properties at explorer (at least on test examples) + + link_info_path = self._link_info.path if self._link_info and self._link_info.path else None + id_list_path = self._shell_item_id_list.get_path() if hasattr( + self, '_shell_item_id_list') else None + + env_var_path = None + if self.extra_data and self.extra_data.blocks: + for block in self.extra_data.blocks: + if type(block) == ExtraData_EnvironmentVariableDataBlock: + env_var_path = block.target_unicode.strip( + '\x00') or block.target_ansi.strip('\x00') + break + + if id_list_path and id_list_path.startswith('%MY_COMPUTER%'): + # full local path has priority + return id_list_path[14:] + if id_list_path and id_list_path.startswith('%USERPROFILE%\\::'): + # path to KNOWN_FOLDER also has priority over link_info + return id_list_path[14:] + if link_info_path: + # local path at link_info_path has priority over network path at id_list_path + # full local path at link_info_path has priority over partial path at id_list_path + return link_info_path + if env_var_path: + # some links in Recent folder contains path only at ExtraData_EnvironmentVariableDataBlock + return env_var_path + return id_list_path + + def specify_local_location(self, path, drive_type=None, drive_serial=None, volume_label=None): + self._link_info.drive_type = drive_type or DRIVE_UNKNOWN + self._link_info.drive_serial = drive_serial or '' + self._link_info.volume_label = volume_label or '' + self._link_info.local_base_path = path + self._link_info.local = True + self._link_info.make_path() + + def specify_remote_location(self, network_share_name, base_name): + self._link_info.network_share_name = network_share_name + self._link_info.base_name = base_name + self._link_info.remote = True + self._link_info.make_path() + + def __str__(self): + s = "Target file:\n" + s += str(self.file_flags) + s += "\nCreation Time: %s" % self.creation_time + s += "\nModification Time: %s" % self.modification_time + s += "\nAccess Time: %s" % self.access_time + s += "\nFile size: %s" % self.file_size + s += "\nWindow mode: %s" % self._show_command + s += "\nHotkey: %s\n" % self.hot_key + s += str(self._link_info) + if self.link_flags.HasLinkTargetIDList: + s += "\n%s" % self.shell_item_id_list + if self.link_flags.HasName: + s += "\nDescription: %s" % self.description + if self.link_flags.HasRelativePath: + s += "\nRelative Path: %s" % self.relative_path + if self.link_flags.HasWorkingDir: + s += "\nWorking Directory: %s" % self.work_dir + if self.link_flags.HasArguments: + s += "\nCommandline Arguments: %s" % self.arguments + if self.link_flags.HasIconLocation: + s += "\nIcon: %s" % self.icon + if self._link_info: + s += "\nUsed Path: %s" % self.path + if self.extra_data: + s += str(self.extra_data) + return s + + +# ---- convenience functions + +def parse(lnk): + return Lnk(lnk) + + +def create(f=None): + lnk = Lnk() + lnk.file = f + return lnk + + +def for_file( + target_file, lnk_name=None, arguments=None, description=None, icon_file=None, icon_index=0, + work_dir=None, window_mode=None, +): + lnk = create(lnk_name) + lnk.link_flags.IsUnicode = True + lnk.link_info = None + if target_file.startswith('\\\\'): + # remote link + lnk.link_info = LinkInfo() + lnk.link_info.remote = 1 + # extract server + share name from full path + path_parts = target_file.split('\\') + share_name, base_name = '\\'.join( + path_parts[:4]), '\\'.join(path_parts[4:]) + lnk.link_info.network_share_name = share_name.upper() + lnk.link_info.base_name = base_name + # somehow it requires EnvironmentVariableDataBlock & HasExpString flag + env_data_block = ExtraData_EnvironmentVariableDataBlock() + env_data_block.target_ansi = target_file + env_data_block.target_unicode = target_file + lnk.extra_data = ExtraData(blocks=[env_data_block]) + lnk.link_flags.HasExpString = True + else: + # local link + levels = list(path_levels(target_file)) + elements = [RootEntry(ROOT_MY_COMPUTER), + DriveEntry(levels[0])] + for level in levels[1:]: + segment = PathSegmentEntry.create_for_path(level) + elements.append(segment) + lnk.shell_item_id_list = LinkTargetIDList() + lnk.shell_item_id_list.items = elements + # lnk.link_flags.HasLinkInfo = True + if arguments: + lnk.link_flags.HasArguments = True + lnk.arguments = arguments + if description: + lnk.link_flags.HasName = True + lnk.description = description + if icon_file: + lnk.link_flags.HasIconLocation = True + lnk.icon = icon_file + lnk.icon_index = icon_index + if work_dir: + lnk.link_flags.HasWorkingDir = True + lnk.work_dir = work_dir + if window_mode: + lnk.window_mode = window_mode + if lnk_name: + lnk.save() + return lnk + + +def from_segment_list(data, lnk_name=None): + """ + Creates a lnk file from a list of path segments. + If lnk_name is given, the resulting lnk will be saved + to a file with that name. + The expected list for has the following format ("C:\\dir\\file.txt"): + + ['c:\\', + {'type': TYPE_FOLDER, + 'size': 0, # optional for folders + 'name': "dir", + 'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476), + 'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476), + 'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476) + }, + {'type': TYPE_FILE, + 'size': 823, + 'name': "file.txt", + 'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476), + 'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476), + 'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476) + } + ] + + For relative paths just omit the drive entry. + Hint: Correct dates really are not crucial for working lnks. + """ + if type(data) not in (list, tuple): + raise ValueError("Invalid data format, list or tuple expected") + lnk = Lnk() + entries = [] + if is_drive(data[0]): + # this is an absolute link + entries.append(RootEntry(ROOT_MY_COMPUTER)) + if not data[0].endswith('\\'): + data[0] += "\\" + drive = data.pop(0).encode("ascii") + entries.append(DriveEntry(drive)) + for level in data: + segment = PathSegmentEntry() + segment.type = level['type'] + if level['type'] == TYPE_FOLDER: + segment.file_size = 0 + else: + segment.file_size = level['size'] + segment.short_name = level['name'] + segment.full_name = level['name'] + segment.created = level['created'] + segment.modified = level['modified'] + segment.accessed = level['accessed'] + entries.append(segment) + lnk.shell_item_id_list = LinkTargetIDList() + lnk.shell_item_id_list.items = entries + if data[-1]['type'] == TYPE_FOLDER: + lnk.file_flags.directory = True + if lnk_name: + lnk.save(lnk_name) + return lnk + + +def build_uwp( + package_family_name, target, location=None, logo44x44=None, lnk_name=None, +) -> Lnk: + """ + :param lnk_name: ex.: crafted_uwp.lnk + :param package_family_name: ex.: Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe + :param target: ex.: Microsoft.WindowsCalculator_8wekyb3d8bbwe!App + :param location: ex.: C:\\Program Files\\WindowsApps\\Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe + :param logo44x44: ex.: Assets\\CalculatorAppList.png + """ + lnk = Lnk() + lnk.link_flags.HasLinkTargetIDList = True + lnk.link_flags.IsUnicode = True + lnk.link_flags.EnableTargetMetadata = True + + lnk.shell_item_id_list = LinkTargetIDList() + + elements = [ + RootEntry(ROOT_UWP_APPS), + UwpSegmentEntry.create( + package_family_name=package_family_name, + target=target, + location=location, + logo44x44=logo44x44, + ) + ] + lnk.shell_item_id_list.items = elements + + if lnk_name: + lnk.file = lnk_name + lnk.save() + return lnk + + +def get_prop(obj, prop_queue): + attr = getattr(obj, prop_queue[0]) + if len(prop_queue) > 1: + return get_prop(attr, prop_queue[1:]) + return attr + + +def cli(): + parser = argparse.ArgumentParser(add_help=False) + subparsers = parser.add_subparsers(dest='action', metavar='{p, c, d}') + parser.add_argument('--help', '-h', action='store_true') + + parser_parse = subparsers.add_parser( + 'parse', aliases=['p'], help='read lnk file') + parser_parse.add_argument('filename', help='lnk filename to read') + parser_parse.add_argument('props', nargs='*', help='props path to read') + + parser_create = subparsers.add_parser( + 'create', aliases=['c'], help='create new lnk file') + parser_create.add_argument('target', help='target path') + parser_create.add_argument('name', help='lnk filename to create') + parser_create.add_argument( + '--arguments', '-a', nargs='?', help='additional arguments') + parser_create.add_argument( + '--description', '-d', nargs='?', help='description') + parser_create.add_argument('--icon', '-i', nargs='?', help='icon filename') + parser_create.add_argument( + '--icon-index', '-ii', type=int, default=0, nargs='?', help='icon index') + parser_create.add_argument( + '--workdir', '-w', nargs='?', help='working directory') + parser_create.add_argument( + '--mode', '-m', nargs='?', choices=['Maximized', 'Normal', 'Minimized'], help='window mode') + + parser_dup = subparsers.add_parser( + 'duplicate', aliases=['d'], help='read and write lnk file') + parser_dup.add_argument('filename', help='lnk filename to read') + parser_dup.add_argument('new_filename', help='new filename to write') + + args = parser.parse_args() + if args.help or not args.action: + print(''' +Tool for read or create .lnk files + +usage: pylnk3.py [p]arse / [c]reate ... + +Examples: +pylnk3 p filename.lnk +pylnk3 c c:\\prog.exe shortcut.lnk +pylnk3 c \\\\192.168.1.1\\share\\file.doc doc.lnk +pylnk3 create c:\\1.txt text.lnk -m Minimized -d "Description" + +for more info use help for each action (ex.: "pylnk3 create -h") + '''.strip()) + exit(1) + + if args.action in ['create', 'c']: + for_file( + args.target, args.name, arguments=args.arguments, + description=args.description, icon_file=args.icon, + icon_index=args.icon_index, work_dir=args.workdir, + window_mode=args.mode, + ) + elif args.action in ['parse', 'p']: + lnk = parse(args.filename) + props = args.props + if len(props) == 0: + print(lnk) + else: + for prop in props: + print(get_prop(lnk, prop.split('.'))) + elif args.action in ['d', 'duplicate']: + lnk = parse(args.filename) + new_filename = args.new_filename + print(lnk) + lnk.save(new_filename) + print('saved') + + +if __name__ == '__main__': + cli()