2040 lines
68 KiB
Python
2040 lines
68 KiB
Python
#!/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('<B', buf.read(1))[0]
|
|
|
|
|
|
def read_short(buf):
|
|
return unpack('<H', buf.read(2))[0]
|
|
|
|
|
|
def read_int(buf):
|
|
return unpack('<I', buf.read(4))[0]
|
|
|
|
|
|
def read_double(buf):
|
|
return unpack('<Q', buf.read(8))[0]
|
|
|
|
|
|
def read_cunicode(buf):
|
|
s = b""
|
|
b = buf.read(2)
|
|
while b != b'\x00\x00':
|
|
s += b
|
|
b = buf.read(2)
|
|
return s.decode('utf-16-le')
|
|
|
|
|
|
def read_cstring(buf, padding=False):
|
|
s = b""
|
|
b = buf.read(1)
|
|
while b != b'\x00':
|
|
s += b
|
|
b = buf.read(1)
|
|
if padding and not len(s) % 2:
|
|
buf.read(1) # make length + terminator even
|
|
# TODO: encoding is not clear, unicode-escape has been necessary sometimes
|
|
return s.decode(DEFAULT_CHARSET)
|
|
|
|
|
|
def read_sized_string(buf, string=True):
|
|
size = read_short(buf)
|
|
if string:
|
|
return buf.read(size*2).decode('utf-16-le')
|
|
else:
|
|
return buf.read(size)
|
|
|
|
|
|
def get_bits(value, start, count, length=16):
|
|
mask = 0
|
|
for i in range(count):
|
|
mask = mask | 1 << i
|
|
shift = length - start - count
|
|
return value >> 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('<B', val))
|
|
|
|
|
|
def write_short(val, buf):
|
|
buf.write(pack('<H', val))
|
|
|
|
|
|
def write_int(val, buf):
|
|
buf.write(pack('<I', val))
|
|
|
|
|
|
def write_double(val, buf):
|
|
buf.write(pack('<Q', val))
|
|
|
|
|
|
def write_cstring(val, buf, padding=False):
|
|
# val = val.encode('unicode-escape').replace('\\\\', '\\')
|
|
val = val.encode(DEFAULT_CHARSET)
|
|
buf.write(val + b'\x00')
|
|
if padding and not len(val) % 2:
|
|
buf.write(b'\x00')
|
|
|
|
|
|
def write_cunicode(val, buf):
|
|
uni = val.encode('utf-16-le')
|
|
buf.write(uni + b'\x00\x00')
|
|
|
|
|
|
def write_sized_string(val, buf, string=True):
|
|
size = len(val)
|
|
write_short(size, buf)
|
|
if string:
|
|
buf.write(val.encode('utf-16-le'))
|
|
else:
|
|
buf.write(val.encode())
|
|
|
|
|
|
def put_bits(bits, target, start, count, length=16):
|
|
return target | bits << (length - start - count)
|
|
|
|
|
|
def write_dos_datetime(val, buf):
|
|
date = time = 0
|
|
date = put_bits(val.year-1980, date, 0, 7)
|
|
date = put_bits(val.month, date, 7, 4)
|
|
date = put_bits(val.day, date, 11, 5)
|
|
time = put_bits(val.hour, time, 0, 5)
|
|
time = put_bits(val.minute, time, 5, 6)
|
|
time = put_bits(val.second, time, 11, 5)
|
|
write_short(date, buf)
|
|
write_short(time, buf)
|
|
|
|
|
|
# ---- helpers
|
|
|
|
def convert_time_to_unix(windows_time):
|
|
# Windows time is specified as the number of 0.1 nanoseconds since January 1, 1601.
|
|
# UNIX time is specified as the number of seconds since January 1, 1970.
|
|
# There are 134774 days (or 11644473600 seconds) between these dates.
|
|
unix_time = windows_time / 10000000.0 - 11644473600
|
|
try:
|
|
return datetime.fromtimestamp(unix_time)
|
|
except OSError:
|
|
return datetime.now()
|
|
|
|
|
|
def convert_time_to_windows(unix_time):
|
|
if isinstance(unix_time, datetime):
|
|
unix_time = time.mktime(unix_time.timetuple())
|
|
return int((unix_time + 11644473600) * 10000000)
|
|
|
|
|
|
class FormatException(Exception):
|
|
pass
|
|
|
|
|
|
class MissingInformationException(Exception):
|
|
pass
|
|
|
|
|
|
class InvalidKeyException(Exception):
|
|
pass
|
|
|
|
|
|
def guid_from_bytes(bytes):
|
|
if len(bytes) != 16:
|
|
raise FormatException("This is no valid _GUID: %s" % bytes)
|
|
ordered = [
|
|
bytes[3], bytes[2], bytes[1], bytes[0],
|
|
bytes[5], bytes[4], bytes[7], bytes[6],
|
|
bytes[8], bytes[9], bytes[10], bytes[11],
|
|
bytes[12], bytes[13], bytes[14], bytes[15]
|
|
]
|
|
return "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered])
|
|
|
|
|
|
def bytes_from_guid(guid):
|
|
nums = [
|
|
guid[1:3], guid[3:5], guid[5:7], guid[7:9],
|
|
guid[10:12], guid[12:14], guid[15:17], guid[17:19],
|
|
guid[20:22], guid[22:24], guid[25:27], guid[27:29],
|
|
guid[29:31], guid[31:33], guid[33:35], guid[35:37]
|
|
]
|
|
ordered_nums = [
|
|
nums[3], nums[2], nums[1], nums[0],
|
|
nums[5], nums[4], nums[7], nums[6],
|
|
nums[8], nums[9], nums[10], nums[11],
|
|
nums[12], nums[13], nums[14], nums[15],
|
|
]
|
|
return bytes([int(x, 16) for x in ordered_nums])
|
|
|
|
|
|
def assert_lnk_signature(f):
|
|
f.seek(0)
|
|
sig = f.read(4)
|
|
guid = f.read(16)
|
|
if sig != _SIGNATURE:
|
|
raise FormatException("This is not a .lnk file.")
|
|
if guid != _GUID:
|
|
raise FormatException("Cannot read this kind of .lnk file.")
|
|
|
|
|
|
def is_lnk(f):
|
|
if hasattr(f, 'name'):
|
|
if f.name.split(os.path.extsep)[-1] == "lnk":
|
|
assert_lnk_signature(f)
|
|
return True
|
|
else:
|
|
return False
|
|
else:
|
|
try:
|
|
assert_lnk_signature(f)
|
|
return True
|
|
except FormatException:
|
|
return False
|
|
|
|
|
|
def path_levels(p):
|
|
dirname, base = ntpath.split(p)
|
|
if base != '':
|
|
for level in path_levels(dirname):
|
|
yield level
|
|
yield p
|
|
|
|
|
|
def is_drive(data):
|
|
if type(data) not in (str, str):
|
|
return False
|
|
p = re.compile("[a-zA-Z]:\\\\?$")
|
|
return p.match(data) is not None
|
|
|
|
|
|
# ---- data structures
|
|
|
|
class Flags(object):
|
|
|
|
def __init__(self, flag_names: Tuple[str, ...], flags_bytes=0):
|
|
self._flag_names = flag_names
|
|
self._flags: Dict[str, bool] = dict(
|
|
[(name, False) for name in flag_names])
|
|
self.set_flags(flags_bytes)
|
|
|
|
def set_flags(self, flags_bytes):
|
|
for pos, flag_name in enumerate(self._flag_names):
|
|
self._flags[flag_name] = bool(flags_bytes >> 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 "<RootEntry: %s>" % 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 "<DriveEntry: %s>" % 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 "<PathSegmentEntry: %s>" % 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'<UwpMainBlock> {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 = '<UwpSegmentEntry>:\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 = '<LinkTargetIDList>:\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 + " <not specified>"
|
|
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('<Q', value)[0]
|
|
if self.type == 0x13:
|
|
value = unpack('<I', value)[0]
|
|
if self.type == 0x14:
|
|
value = unpack('<q', value)[0]
|
|
if self.type == 0x16:
|
|
value = unpack('<i', value)[0]
|
|
if self.type == 0x17:
|
|
value = unpack('<I', value)[0]
|
|
if self.type == 0x48:
|
|
value = guid_to_str(value)
|
|
if self.type == 0x40:
|
|
# FILETIME (Packet Version)
|
|
stream = BytesIO(value)
|
|
low = read_int(stream)
|
|
high = read_int(stream)
|
|
num = (high << 32) + low
|
|
value = convert_time_to_unix(num)
|
|
return '%s: %s' % (hex(self.type), value)
|
|
|
|
|
|
class PropertyStore:
|
|
def __init__(self, bytes=None, properties=None, format_id=None, is_strings=False):
|
|
self.is_strings = is_strings
|
|
self.properties = []
|
|
self.format_id = format_id
|
|
self._is_end = False
|
|
if properties:
|
|
self.properties = properties
|
|
if bytes:
|
|
self.read(bytes)
|
|
|
|
def read(self, bytes_io):
|
|
buf = bytes_io
|
|
size = read_int(buf)
|
|
assert size < len(buf.getvalue())
|
|
if size == 0x00000000:
|
|
self._is_end = True
|
|
return
|
|
version = read_int(buf)
|
|
assert version == 0x53505331
|
|
self.format_id = buf.read(16)
|
|
if self.format_id == b'\xD5\xCD\xD5\x05\x2E\x9C\x10\x1B\x93\x97\x08\x00\x2B\x2C\xF9\xAE':
|
|
self.is_strings = True
|
|
else:
|
|
self.is_strings = False
|
|
while True:
|
|
# assert lnk.tell() < (start + size)
|
|
value_size = read_int(buf)
|
|
if value_size == 0x00000000:
|
|
break
|
|
if self.is_strings:
|
|
name_size = read_int(buf)
|
|
reserved = read_byte(buf)
|
|
name = buf.read(name_size).decode('utf-16-le')
|
|
value = TypedPropertyValue(buf.read(value_size-9))
|
|
self.properties.append((name, value))
|
|
else:
|
|
value_id = read_int(buf)
|
|
reserved = read_byte(buf)
|
|
value = TypedPropertyValue(buf.read(value_size-9))
|
|
self.properties.append((value_id, value))
|
|
|
|
@property
|
|
def bytes(self):
|
|
size = 8 + len(self.format_id)
|
|
properties = BytesIO()
|
|
for name, value in self.properties:
|
|
value_bytes = value.bytes
|
|
if self.is_strings:
|
|
name_bytes = name.encode('utf-16-le')
|
|
value_size = 9 + len(name_bytes) + len(value_bytes)
|
|
write_int(value_size, properties)
|
|
name_size = len(name_bytes)
|
|
write_int(name_size, properties)
|
|
properties.write(b'\x00')
|
|
properties.write(name_bytes)
|
|
else:
|
|
value_size = 9 + len(value_bytes)
|
|
write_int(value_size, properties)
|
|
write_int(name, properties)
|
|
properties.write(b'\x00')
|
|
properties.write(value_bytes)
|
|
size += value_size
|
|
|
|
write_int(0x00000000, properties)
|
|
size += 4
|
|
|
|
buf = BytesIO()
|
|
write_int(size, buf)
|
|
write_int(0x53505331, buf)
|
|
buf.write(self.format_id)
|
|
buf.write(properties.getvalue())
|
|
|
|
return buf.getvalue()
|
|
|
|
def __str__(self):
|
|
s = ' PropertyStore'
|
|
s += '\n FormatID: %s' % guid_to_str(self.format_id)
|
|
for name, value in self.properties:
|
|
s += '\n %3s = %s' % (name, str(value))
|
|
return s.strip()
|
|
|
|
|
|
class ExtraData_PropertyStoreDataBlock(object):
|
|
def __init__(self, bytes=None, stores=None):
|
|
self._size = None
|
|
self._signature = 0xA0000009
|
|
self.stores = []
|
|
if stores:
|
|
self.stores = stores
|
|
if bytes:
|
|
self.read(bytes)
|
|
|
|
def read(self, bytes):
|
|
buf = BytesIO(bytes)
|
|
# self._size = read_int(buf)
|
|
# self._signature = read_int(buf)
|
|
# [MS-PROPSTORE] section 2.2
|
|
while True:
|
|
prop_store = PropertyStore(buf)
|
|
if prop_store._is_end:
|
|
break
|
|
self.stores.append(prop_store)
|
|
|
|
def bytes(self):
|
|
stores = b''
|
|
for prop_store in self.stores:
|
|
stores += prop_store.bytes
|
|
size = len(stores) + 8 + 4
|
|
|
|
assert self._signature == 0xA0000009
|
|
assert size >= 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()
|