#!/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()