diff --git a/plugin/libs/LnkParse3/__init__.py b/plugin/libs/LnkParse3/__init__.py new file mode 100644 index 0000000..5429005 --- /dev/null +++ b/plugin/libs/LnkParse3/__init__.py @@ -0,0 +1 @@ +from .lnk_file import LnkFile as lnk_file diff --git a/plugin/libs/LnkParse3/decorators.py b/plugin/libs/LnkParse3/decorators.py new file mode 100644 index 0000000..267c532 --- /dev/null +++ b/plugin/libs/LnkParse3/decorators.py @@ -0,0 +1,65 @@ +from datetime import datetime +from datetime import timezone +from struct import unpack +import functools +import sys +import warnings + +from .utils import parse_uuid, parse_packed_uuid, parse_filetime, parse_dostime + + +def must_be(expected): + def outer(func): + @functools.wraps(func) + def inner(self, *args, **kwargs): + result = func(self, *args, **kwargs) + + if result != expected: + msg = "%s must be %s: %s" % (func.__name__, expected, result) + warnings.warn(msg) + + return result + + return inner + + return outer + + +def uuid(func): + @functools.wraps(func) + def inner(self, *args, **kwargs): + binary = func(self, *args, **kwargs) + + return parse_uuid(binary) + + return inner + + +def packed_uuid(func): + @functools.wraps(func) + def inner(self, *args, **kwargs): + text = func(self, *args, **kwargs) + + return parse_packed_uuid(text) + + return inner + + +def filetime(func): + @functools.wraps(func) + def inner(self, *args, **kwargs): + binary = func(self, *args, **kwargs) + + return parse_filetime(binary) + + return inner + + +def dostime(func): + @functools.wraps(func) + def inner(self, *args, **kwargs): + binary = func(self, *args, **kwargs) + + return parse_dostime(binary) + + return inner diff --git a/plugin/libs/LnkParse3/exceptions.py b/plugin/libs/LnkParse3/exceptions.py new file mode 100644 index 0000000..0deb09b --- /dev/null +++ b/plugin/libs/LnkParse3/exceptions.py @@ -0,0 +1,2 @@ +class LnkParserError(Exception): + ... diff --git a/plugin/libs/pylnk3/structures/id_list/__init__.py b/plugin/libs/LnkParse3/extra/__init__.py similarity index 100% rename from plugin/libs/pylnk3/structures/id_list/__init__.py rename to plugin/libs/LnkParse3/extra/__init__.py diff --git a/plugin/libs/LnkParse3/extra/code_page.py b/plugin/libs/LnkParse3/extra/code_page.py new file mode 100644 index 0000000..8633295 --- /dev/null +++ b/plugin/libs/LnkParse3/extra/code_page.py @@ -0,0 +1,28 @@ +from struct import unpack +from ..extra.lnk_extra_base import LnkExtraBase + +""" +------------------------------------------------------------------ +| 0-7b | 8-15b | 16-23b | 24-31b | +------------------------------------------------------------------ +| BlockSize == 0x0000000C | +------------------------------------------------------------------ +| BlockSignature == 0xA0000004 | +------------------------------------------------------------------ +| CodePage | +------------------------------------------------------------------ +""" + + +class CodePage(LnkExtraBase): + def name(self): + return "CONSOLE_CODEPAGE_BLOCK" + + def code_page(self): + start, end = 8, 12 + return unpack(" BlockSize == 0x000000CC | +------------------------------------------------------------------ +| BlockSignature == 0xA0000002 | +-----------------------------------------------------------------| +| FillAttributes | PopupFillAttributes | +------------------------------------------------------------------ +| ScreenBufferSizeX | ScreenBufferSizeY | +------------------------------------------------------------------ +| WindowSizeX | WindowSizeY | +------------------------------------------------------------------ +| WindowOriginX | WindowOriginY | +------------------------------------------------------------------ +| Unused1 | +------------------------------------------------------------------ +| Unused2 | +------------------------------------------------------------------ +| FontSize | +------------------------------------------------------------------ +| FontFamily | +------------------------------------------------------------------ +| FontWeight | +------------------------------------------------------------------ +| Face Name | +| 64 B | +------------------------------------------------------------------ +| CursorSize | +------------------------------------------------------------------ +| FullScreen | +------------------------------------------------------------------ +| QuickEdit | +------------------------------------------------------------------ +| InsertMode | +------------------------------------------------------------------ +| AutoPosition | +------------------------------------------------------------------ +| HistoryBufferSize | +------------------------------------------------------------------ +| NumberOfHistoryBuffers | +------------------------------------------------------------------ +| HistoryNoDup | +------------------------------------------------------------------ +| > ColorTable | +| 64 B | +------------------------------------------------------------------ +""" + + +class Console(LnkExtraBase): + def name(self): + return "CONSOLE_PROPERTIES_BLOCK" + + def fill_attributes(self): + start, end = 8, 10 + return unpack(" BlockSize == 0x00000314 | +------------------------------------------------------------------ +| BlockSignature == 0xA0000006 | +------------------------------------------------------------------ +| DarwinDataAnsi | +| 260 B | +------------------------------------------------------------------ +| DarwinDataUnicode | +| 520 B | +------------------------------------------------------------------ + +DarwinData consists of {Product-Code, Feature Key, Component Code}. It is stored +in a compressed format, e.g. "w_1^VX!!!!!!!!!MKKSkEXCELFiles>tW{~$4Q]c@II=l2xaTO5Z", +which can results into +{91120000-0030-0000-0000-0000000ff1ce}EXCELFiles{0638c49d-bb8b-4cd1-b191-052e8f325736}. + +There are four variants according to +https://community.broadcom.com/symantecenterprise/viewdocument/working-with-darwin-descriptors: +1. {compressed product code}{feature name}>{compressed component ID} +2. {compressed product code}>{compressed component ID} +3. {compressed product code}{feature name}< +4. {compressed product code}< + +See http://www.laurierhodes.info/?q=node/34 or +https://metadataconsulting.blogspot.com/2019/12/CSharp-Convert-a-GUID-to-a-Darwin-Descriptor-and-back.html +or https://web.archive.org/web/20080323160816/http://support.microsoft.com/kb/243630. +""" + + +class Darwin(LnkExtraBase): + def name(self): + return "DARWIN_BLOCK" + + def darwin_data_ansi(self): + start = 8 + end = start + 260 + binary = self._raw[start:end] + text = self.text_processor.read_string(binary) + return text + + def darwin_data_unicode(self): + start = 268 + end = start + 520 + binary = self._raw[start:end] + text = self.text_processor.read_unicode_string(binary) + return text + + @packed_uuid + def product_code_id(self): + data = self.darwin_data_unicode() + start, end = 0, 20 + text = data[start:end] + return text + + def feature_name(self): + data = self.darwin_data_unicode() + start = 20 + # Search for a termiantor sign which can be either `<` or `>`. + # None of these characters should be located in a packed GUID. + # If the character is not found, `find` returns `-1`, i.e. by using max + # we want to get the one which is present. + # An absence of both characters leads to an exception. + terminator = max(data.find(">"), data.find("<")) + if terminator == start: + # If the terminator is found but is the same as the start position, + # there is no feature name. + return None + end = terminator + text = data[start:end] + return text + + @packed_uuid + def component_id(self): + data = self.darwin_data_unicode() + terminator = data.find(">") + if terminator == -1: + # Set the ID to `None` if `terminator` is not found. + return None + start = terminator + 1 + text = data[start:] + return text + + def as_dict(self): + tmp = super().as_dict() + tmp["darwin_data_ansi"] = self.darwin_data_ansi() + tmp["darwin_data_unicode"] = self.darwin_data_unicode() + tmp["product_code_id"] = self.product_code_id() + tmp["feature_name"] = self.feature_name() + tmp["component_id"] = self.component_id() + return tmp diff --git a/plugin/libs/LnkParse3/extra/distributed_tracker.py b/plugin/libs/LnkParse3/extra/distributed_tracker.py new file mode 100644 index 0000000..4f8fe71 --- /dev/null +++ b/plugin/libs/LnkParse3/extra/distributed_tracker.py @@ -0,0 +1,104 @@ +from struct import unpack +from ..extra.lnk_extra_base import LnkExtraBase +from ..decorators import must_be +from ..decorators import uuid + +""" +------------------------------------------------------------------ +| 0-7b | 8-15b | 16-23b | 24-31b | +------------------------------------------------------------------ +| BlockSize == 0x00000060 | +------------------------------------------------------------------ +| BlockSignature == 0xA0000003 | +------------------------------------------------------------------ +| Length | +------------------------------------------------------------------ +| Version | +------------------------------------------------------------------ +| MachineID | +| 16 B | +------------------------------------------------------------------ +| DroidVolumeId | +| 16 B | +------------------------------------------------------------------ +| DroidFileId | +| 16 B | +------------------------------------------------------------------ +| DroidBirthVolumeId | +| 16 B | +------------------------------------------------------------------ +| DroidBirthFileId | +| 16 B | +------------------------------------------------------------------ +""" + + +class DistributedTracker(LnkExtraBase): + def name(self): + return "DISTRIBUTED_LINK_TRACKER_BLOCK" + + @must_be(0x00000058) + def length(self): + """Length (4 bytes): + A 32-bit, unsigned integer that specifies the size of the rest of the + TrackerDataBlock structure, including this Length field. This value + MUST be 0x00000058. + """ + start, end = 8, 12 + length = unpack(" BlockSize == 0x00000314 | +------------------------------------------------------------------ +| BlockSignature == 0xA0000001 | +------------------------------------------------------------------ +| TargetAnsi | +| 260 B | +------------------------------------------------------------------ +| TargetUnicode | +| 520 B | +------------------------------------------------------------------ +""" + + +class Environment(LnkExtraBase): + def name(self): + return "ENVIRONMENTAL_VARIABLES_LOCATION_BLOCK" + + def target_ansi(self): + start = 8 + end = start + 260 + binary = self._raw[start:end] + text = self.text_processor.read_string(binary) + return text + + def target_unicode(self): + start = 268 + end = start + 520 + binary = self._raw[start:end] + text = self.text_processor.read_unicode_string(binary) + return text + + def as_dict(self): + tmp = super().as_dict() + tmp["target_ansi"] = self.target_ansi() + tmp["target_unicode"] = self.target_unicode() + return tmp diff --git a/plugin/libs/LnkParse3/extra/icon.py b/plugin/libs/LnkParse3/extra/icon.py new file mode 100644 index 0000000..bec728e --- /dev/null +++ b/plugin/libs/LnkParse3/extra/icon.py @@ -0,0 +1,42 @@ +from ..extra.lnk_extra_base import LnkExtraBase + +""" +------------------------------------------------------------------ +| 0-7b | 8-15b | 16-23b | 24-31b | +------------------------------------------------------------------ +| BlockSize == 0x00000314 | +------------------------------------------------------------------ +| BlockSignature == 0xA0000007 | +------------------------------------------------------------------ +| TargetAnsi | +| 260 B | +------------------------------------------------------------------ +| TargetUnicode | +| 520 B | +------------------------------------------------------------------ +""" + + +class Icon(LnkExtraBase): + def name(self): + return "ICON_LOCATION_BLOCK" + + def target_ansi(self): + start = 8 + end = start + 260 + binary = self._raw[start:end] + text = self.text_processor.read_string(binary) + return text + + def target_unicode(self): + start = 268 + end = start + 520 + binary = self._raw[start:end] + text = self.text_processor.read_unicode_string(binary) + return text + + def as_dict(self): + tmp = super().as_dict() + tmp["target_ansi"] = self.target_ansi() + tmp["target_unicode"] = self.target_unicode() + return tmp diff --git a/plugin/libs/LnkParse3/extra/known_folder.py b/plugin/libs/LnkParse3/extra/known_folder.py new file mode 100644 index 0000000..3b6cf2a --- /dev/null +++ b/plugin/libs/LnkParse3/extra/known_folder.py @@ -0,0 +1,39 @@ +from struct import unpack +from ..extra.lnk_extra_base import LnkExtraBase +from ..decorators import uuid + +""" +------------------------------------------------------------------ +| 0-7b | 8-15b | 16-23b | 24-31b | +------------------------------------------------------------------ +| BlockSize == 0x0000001C | +------------------------------------------------------------------ +| BlockSignature == 0xA000000B | +------------------------------------------------------------------ +| KnownFolderID | +| 16 B | +------------------------------------------------------------------ +| Offset | +------------------------------------------------------------------ +""" + + +class KnownFolder(LnkExtraBase): + def name(self): + return "KNOWN_FOLDER_LOCATION_BLOCK" + + @uuid + def known_folder_id(self): + start, end = 8, 24 + binary = self._raw[start:end] + return binary + + def offset(self): + start, end = 24, 28 + return unpack(" BlockSize == 0x00000314 | +------------------------------------------------------------------ +""" + + +class LnkExtraBase: + def __init__(self, indata=None, cp=None): + self._raw = indata + self.cp = cp + self.text_processor = TextProcessor(cp=cp) + + def size(self): + start, end = 0, 4 + size = unpack(" BlockSize >= 0x0000000C | +------------------------------------------------------------------ +| BlockSignature == 0xA0000009 | +------------------------------------------------------------------ +| StoreSize | +------------------------------------------------------------------ +| > PropertyStore | +| ? B | +------------------------------------------------------------------ +""" + + +class PropertyType(IntEnum): + # fmt: off + VT_EMPTY = 0x0000 #Type is undefined, and the minimum property set version is 0. + VT_NULL = 0x0001 #Type is null, and the minimum property set version is 0. + VT_I2 = 0x0002 #Type is 16-bit signed integer, and the minimum property set version is 0. + VT_I4 = 0x0003 #Type is 32-bit signed integer, and the minimum property set version is 0. + VT_R4 = 0x0004 #Type is 4-byte (single-precision) IEEE floating-point number, and the + #minimum property set version is 0. + VT_R8 = 0x0005 #Type is 8-byte (double-precision) IEEE floating-point number, and the + #minimum property set version is 0. + VT_CY = 0x0006 #Type is CURRENCY, and the minimum property set version is 0. + VT_DATE = 0x0007 #Type is DATE, and the minimum property set version is 0. + VT_BSTR = 0x0008 #Type is CodePageString, and the minimum property set version is 0. + VT_ERROR = 0x000A #Type is HRESULT, and the minimum property set version is 0. + VT_BOOL = 0x000B #Type is VARIANT_BOOL, and the minimum property set version is 0. + VT_DECIMAL = 0x000E #Type is DECIMAL, and the minimum property set version is 0. + VT_I1 = 0x0010 #Type is 1-byte signed integer, and the minimum property set version is 1. + VT_UI1 = 0x0011 #Type is 1-byte unsigned integer, and the minimum property set version is 0. + VT_UI2 = 0x0012 #Type is 2-byte unsigned integer, and the minimum property set version is 0. + VT_UI4 = 0x0013 #Type is 4-byte unsigned integer, and the minimum property set version is 0. + VT_I8 = 0x0014 #Type is 8-byte signed integer, and the minimum property set version is 0. + VT_UI8 = 0x0015 #Type is 8-byte unsigned integer, and the minimum property set version is 0. + VT_INT = 0x0016 #Type is 4-byte signed integer, and the minimum property set version is 1. + VT_UINT = 0x0017 #Type is 4-byte unsigned integer, and the minimum property set version is 1. + VT_LPSTR = 0x001E #Type is CodePageString, and the minimum property set version is 0. + VT_LPWSTR = 0x001F #Type is UnicodeString, and the minimum property set version is 0. + VT_FILETIME = 0x0040 #Type is FILETIME, and the minimum property set version is 0. + VT_BLOB = 0x0041 #Type is binary large object (BLOB), and the minimum property set version is 0. + VT_STREAM = 0x0042 #Type is Stream, and the minimum property set version is 0. VT_STREAM is not + #allowed in a simple property set. + VT_STORAGE = 0x0043 #Type is Storage, and the minimum property set version is 0. VT_STORAGE is not + #allowed in a simple property set. + VT_STREAMED_Object = 0x0044 #Type is Stream representing an Object in an application-specific manner, and the + #minimum property set version is 0. VT_STREAMED_Object is not allowed in a simple + #property set. + VT_STORED_Object = 0x0045 #Type is Storage representing an Object in an application-specific manner, and the + #minimum property set version is 0. VT_STORED_Object is not allowed in a simple + #property set. + VT_BLOB_Object = 0x0046 #Type is BLOB representing an object in an application-specific manner. The minimum + #property set version is 0. + VT_CF = 0x0047 #Type is PropertyIdentifier, and the minimum property set version is 0. + VT_CLSID = 0x0048 #Type is CLSID, and the minimum property set version is 0. + VT_VERSIONED_STREAM = 0x0049 #Type is Stream with application-specific version GUID (VersionedStream). The + #minimum property set version is 0. VT_VERSIONED_STREAM is not allowed in a + #simple property set. + + VT_VECTOR_I2 = 0x1002 #Type is Vector of 16-bit signed integers, and the minimum property set version is 0. + VT_VECTOR_I4 = 0x1003 #Type is Vector of 32-bit signed integers, and the minimum property set version is 0. + VT_VECTOR_R4 = 0x1004 #Type is Vector of 4-byte (single-precision) IEEE floating-point numbers, and + #the minimum property set version is 0 + VT_VECTOR_R8 = 0x1005 #Type is Vector of 8-byte (double-precision) IEEE floating-point numbers, and + #the minimum property set version is 0. + VT_VECTOR_CY = 0x1006 #Type is Vector of CURRENCY, and the minimum property set version is 0. + VT_VECTOR_DATE = 0x1007 #Type is Vector of DATE, and the minimum property set version is 0. + VT_VECTOR_BSTR = 0x1008 #Type is Vector of CodePageString, and the minimum property set version is 0. + VT_VECTOR_ERROR = 0x100A #Type is Vector of HRESULT, and the minimum property set version is 0. + VT_VECTOR_BOOL = 0x100B #Type is Vector of VARIANT_BOOL, and the minimum property set version is 0. + VT_VECTOR_VARIANT = 0x100C #Type is Vector of variable-typed properties, and the minimum property set version is 0. + VT_VECTOR_I1 = 0x1010 #Type is Vector of 1-byte signed integers and the minimum property set version is 1. + VT_VECTOR_UI1 = 0x1011 #Type is Vector of 1-byte unsigned integers, and the minimum property set version is 0. + VT_VECTOR_UI2 = 0x1012 #Type is Vector of 2-byte unsigned integers, and the minimum property set version is 0. + VT_VECTOR_UI4 = 0x1013 #Type is Vector of 4-byte unsigned integers, and the minimum property set version is 0. + VT_VECTOR_I8 = 0x1014 #Type is Vector of 8-byte signed integers, and the minimum property set version is 0. + VT_VECTOR_UI8 = 0x1015 #Type is Vector of 8-byte unsigned integers and the minimum property set version is 0. + VT_VECTOR_LPSTR = 0x101E #Type is Vector of CodePageString, and the minimum property set version is 0. + VT_VECTOR_LPWSTR = 0x101F #Type is Vector of UnicodeString, and the minimum property set version is 0. + VT_VECTOR_FILETIME = 0x1040 #Type is Vector of FILETIME, and the minimum property set version is 0. + VT_VECTOR_CF = 0x1047 #Type is Vector of PropertyIdentifier, and the minimum property set version is 0. + VT_VECTOR_CLSID = 0x1048 #Type is Vector of CLSID, and the minimum property set version is 0 + + VT_ARRAY_I2 = 0x2002 #Type is Array of 16-bit signed integers, and the minimum property set version is 1. + VT_ARRAY_I4 = 0x2003 #Type is Array of 32-bit signed integers, and the minimum property set version is 1. + VT_ARRAY_R4 = 0x2004 #Type is Array of 4-byte (single-precision) IEEE floating-point numbers, and + #the minimum property set version is 1. + VT_ARRAY_R8 = 0x2005 #Type is IEEE floating-point numbers, and the minimum property set version is 1. + VT_ARRAY_CY = 0x2006 #Type is Array of CURRENCY, and the minimum property set version is 1. + VT_ARRAY_DATE = 0x2007 #Type is Array of DATE, and the minimum property set version is 1. + VT_ARRAY_BSTR = 0x2008 #Type is Array of CodePageString, and the minimum property set version is 1. + VT_ARRAY_ERROR = 0x200A #Type is Array of HRESULT, and the minimum property set version is 1. + VT_ARRAY_BOOL = 0x200B #Type is Array of VARIANT_BOOL, and the minimum property set version is 1. + VT_ARRAY_VARIANT = 0x200C #Type is Array of variable-typed properties, and the minimum property set version is 1. + VT_ARRAY_DECIMAL = 0x200E #Type is Array of DECIMAL, and the minimum property set version is 1. + VT_ARRAY_I1 = 0x2010 #Type is Array of 1-byte signed integers, and the minimum property set version is 1. + VT_ARRAY_UI1 = 0x2011 #Type is Array of 1-byte unsigned integers, and the minimum property set version is 1. + VT_ARRAY_UI2 = 0x2012 #Type is Array of 2-byte unsigned integers, and the minimum property set version is 1. + VT_ARRAY_UI4 = 0x2013 #Type is Array of 4-byte unsigned integers, and the minimum property set version is 1. + VT_ARRAY_INT = 0x2016 #Type is Array of 4-byte signed integers, and the minimum property set version is 1. + VT_ARRAY_UINT = 0x2017 #Type is Array of 4-byte unsigned integers, and the minimum property set version is 1 + # fmt: on + + +class TypedPropertyValue: + """ + ------------------------------------------------------------------ + | 0-7b | 8-15b | 16-23b | 24-31b | + ------------------------------------------------------------------ + | Type | Padding == 0x0000 | + ------------------------------------------------------------------ + | Value | + | ? B | + ------------------------------------------------------------------ + """ + + def __init__(self, raw, text_processor): + self._raw = raw + self._text_processor = text_processor + + def value_type(self): + start, end = 0, 2 + return unpack(" Value Size | + ------------------------------------------------------------------ + | ID | + ------------------------------------------------------------------ + | Reserved | > Value | + | ? B | + ------------------------------------------------------------------ + """ + + def __init__(self, raw, text_processor): + self._raw = raw + self._text_processor = text_processor + + def value_size(self): + start, end = 0, 4 + return unpack(" Value Size | + ------------------------------------------------------------------ + | Name Size | + ------------------------------------------------------------------ + | Reserved | Name | + | ? B | + ------------------------------------------------------------------ + | > Value (see MS-OLEPS) | + | ? B | + ------------------------------------------------------------------ + """ + + def __init__(self, raw, text_processor): + self._raw = raw + self._text_processor = text_processor + + def value_size(self): + start, end = 0, 4 + return unpack(" StorageSize | + ------------------------------------------------------------------ + | Version == 0x53505331 | + ------------------------------------------------------------------ + | FormatID | + | 16 B | + ------------------------------------------------------------------ + | > SerializedPropertyValues | + | ? B | + ------------------------------------------------------------------ + """ + + def __init__(self, raw, text_processor): + self._raw = raw + self._text_processor = text_processor + + def storage_size(self): + start, end = 0, 4 + return unpack(" BlockSize >= 0x0000000A | +------------------------------------------------------------------ +| BlockSignature == 0xA000000C | +------------------------------------------------------------------ +| IDList | +------------------------------------------------------------------ +""" + + +class ShellItem(LnkExtraBase): + def name(self): + return "SHELL_ITEM_IDENTIFIER_BLOCK" + + def _id_list(self): + """ItemIDList (variable): + An array of zero or more ItemID structures (section 2.2.2), which + contains the item ID list. An IDList structure conforms to the + following ABNF [RFC5234]: + + IDLIST = *ITEMID TERMINALID + + ------------------------------------------------------------------ + | 0-7b | 8-15b | 16-23b | 24-31b | + ------------------------------------------------------------------ + | ItemIDList (variable) | + ------------------------------------------------------------------ + | ... | + |----------------------------------------------------------------- + | TerminalID | + -------------------------------- + """ + rest = self._raw[8 : self.size()] + while rest: + factory = TargetFactory(indata=rest) + target_class = factory.target_class() + + if not target_class: + # Empty or unknown target object. + break + + target = target_class(indata=rest, cp=self.cp) + + size = factory.item_size() + rest = rest[size:] + yield target + + def id_list(self): + res = [] + for target in self._id_list(): + try: + res.append(target.as_item()) + except KeyError as e: + msg = ( + f"Error while parsing extra TargetID `{target.name}` (KeyError {e})" + ) + warnings.warn(msg) + continue + return res + + def as_dict(self): + tmp = super().as_dict() + tmp["id_list"] = self.id_list() + return tmp diff --git a/plugin/libs/LnkParse3/extra/shim_layer.py b/plugin/libs/LnkParse3/extra/shim_layer.py new file mode 100644 index 0000000..55e3b42 --- /dev/null +++ b/plugin/libs/LnkParse3/extra/shim_layer.py @@ -0,0 +1,30 @@ +from ..extra.lnk_extra_base import LnkExtraBase + +""" +------------------------------------------------------------------ +| 0-7b | 8-15b | 16-23b | 24-31b | +------------------------------------------------------------------ +| BlockSize >= 0x00000088 | +------------------------------------------------------------------ +| BlockSignature == 0xA0000008 | +------------------------------------------------------------------ +| LayerName | +| ? B | +------------------------------------------------------------------ +""" + + +class ShimLayer(LnkExtraBase): + def name(self): + return "SHIM_LAYER_BLOCK" + + def layer_name(self): + start = 8 + binary = self._raw[start:] + text = self.text_processor.read_string(binary) + return text + + def as_dict(self): + tmp = super().as_dict() + tmp["layer_name"] = self.layer_name() + return tmp diff --git a/plugin/libs/LnkParse3/extra/special_folder.py b/plugin/libs/LnkParse3/extra/special_folder.py new file mode 100644 index 0000000..e0e366d --- /dev/null +++ b/plugin/libs/LnkParse3/extra/special_folder.py @@ -0,0 +1,35 @@ +from struct import unpack +from ..extra.lnk_extra_base import LnkExtraBase + +""" +------------------------------------------------------------------ +| 0-7b | 8-15b | 16-23b | 24-31b | +------------------------------------------------------------------ +| BlockSize == 0x00000010 | +------------------------------------------------------------------ +| BlockSignature == 0xA0000005 | +------------------------------------------------------------------ +| SpecialFolderID | +------------------------------------------------------------------ +| Offset | +------------------------------------------------------------------ +""" + + +class SpecialFolder(LnkExtraBase): + def name(self): + return "SPECIAL_FOLDER_LOCATION_BLOCK" + + def special_folder_id(self): + start, end = 8, 12 + return unpack(" BlockSize == 0x00000314 | +------------------------------------------------------------------ +| BlockSignature == 0xA0000001 | +------------------------------------------------------------------ +""" + + +class ExtraFactory: + EXTRA_SIGS = { + "a0000001": Environment, + "a0000002": Console, + "a0000003": DistributedTracker, + "a0000004": CodePage, + "a0000005": SpecialFolder, + "a0000006": Darwin, + "a0000007": Icon, + "a0000008": ShimLayer, + "a0000009": Metadata, + "a000000b": KnownFolder, + "a000000c": ShellItem, + } + + def __init__(self, indata): + self._raw = indata + + def item_size(self): + start, end = 0, 4 + size = unpack(" VolumeIDSize | +------------------------------------------------------------------ +| DriveType | +------------------------------------------------------------------ +| DriveSerialNumber | +------------------------------------------------------------------ +| VolumeLabelOffset | +------------------------------------------------------------------ +| VolumeLabelOffsetUnicode (optional) | +------------------------------------------------------------------ +| Data | +| ? B | +------------------------------------------------------------------ +""" + + +class Local(LnkInfo): + DRIVE_TYPES = [ + "DRIVE_UNKNOWN", + "DRIVE_NO_ROOT_DIR", + "DRIVE_REMOVABLE", + "DRIVE_FIXED", + "DRIVE_REMOTE", + "DRIVE_CDROM", + "DRIVE_RAMDISK", + ] + + def location(self): + return "Local" + + def volume_id_size(self): + start = self.volume_id_offset() + end = start + 4 + return unpack(" CommonNetworkRelativeLinkSize | +------------------------------------------------------------------ +| CommonNetworkRelativeLinkFlags | +------------------------------------------------------------------ +| NetNameOffset | +------------------------------------------------------------------ +| DeviceNameOffset | +------------------------------------------------------------------ +| NetworkProviderType | +------------------------------------------------------------------ +| NetNameOffsetUnicode (optional) | +------------------------------------------------------------------ +| DeviceNameOffsetUnicode (optional) | +------------------------------------------------------------------ +| NetName | +| ? B | +------------------------------------------------------------------ +| DeviceName | +| ? B | +------------------------------------------------------------------ +| NetNameUnicode (optional) | +| ? B | +------------------------------------------------------------------ +| DeviceNameUnicode (optional) | +| ? B | +------------------------------------------------------------------ +""" + + +class Network(LnkInfo): + NETWORK_PROVIDER_TYPES = { + "0x1A000": "WNNC_NET_AVID", + "0x1B000": "WNNC_NET_DOCUSPACE", + "0x1C000": "WNNC_NET_MANGOSOFT", + "0x1D000": "WNNC_NET_SERNET", + "0X1E000": "WNNC_NET_RIVERFRONT1", + "0x1F000": "WNNC_NET_RIVERFRONT2", + "0x20000": "WNNC_NET_DECORB", + "0x21000": "WNNC_NET_PROTSTOR", + "0x22000": "WNNC_NET_FJ_REDIR", + "0x23000": "WNNC_NET_DISTINCT", + "0x24000": "WNNC_NET_TWINS", + "0x25000": "WNNC_NET_RDR2SAMPLE", + "0x26000": "WNNC_NET_CSC", + "0x27000": "WNNC_NET_3IN1", + "0x29000": "WNNC_NET_EXTENDNET", + "0x2A000": "WNNC_NET_STAC", + "0x2B000": "WNNC_NET_FOXBAT", + "0x2C000": "WNNC_NET_YAHOO", + "0x2D000": "WNNC_NET_EXIFS", + "0x2E000": "WNNC_NET_DAV", + "0x2F000": "WNNC_NET_KNOWARE", + "0x30000": "WNNC_NET_OBJECT_DIRE", + "0x31000": "WNNC_NET_MASFAX", + "0x32000": "WNNC_NET_HOB_NFS", + "0x33000": "WNNC_NET_SHIVA", + "0x34000": "WNNC_NET_IBMAL", + "0x35000": "WNNC_NET_LOCK", + "0x36000": "WNNC_NET_TERMSRV", + "0x37000": "WNNC_NET_SRT", + "0x38000": "WNNC_NET_QUINCY", + "0x39000": "WNNC_NET_OPENAFS", + "0X3A000": "WNNC_NET_AVID1", + "0x3B000": "WNNC_NET_DFS", + "0x3C000": "WNNC_NET_KWNP", + "0x3D000": "WNNC_NET_ZENWORKS", + "0x3E000": "WNNC_NET_DRIVEONWEB", + "0x3F000": "WNNC_NET_VMWARE", + "0x40000": "WNNC_NET_RSFX", + "0x41000": "WNNC_NET_MFILES", + "0x42000": "WNNC_NET_MS_NFS", + "0x43000": "WNNC_NET_GOOGLE", + } + + def location(self): + return "Network" + + def common_network_relative_link_size(self): + start = self.common_network_relative_link_offset() + end = start + 4 + return unpack(" 20: + return None + + start = self.common_network_relative_link_offset() + start += self.net_name_offset() + + binary = self._raw[start:] + text = self.text_processor.read_string(binary) + return text + + def device_name(self): + if self.net_name_offset() > 20: + return None + + start = self.common_network_relative_link_offset() + start += self.device_name_offset() + + binary = self._raw[start:] + text = self.text_processor.read_string(binary) + return text diff --git a/plugin/libs/LnkParse3/info_factory.py b/plugin/libs/LnkParse3/info_factory.py new file mode 100644 index 0000000..34a62b3 --- /dev/null +++ b/plugin/libs/LnkParse3/info_factory.py @@ -0,0 +1,48 @@ +import struct +import warnings + +from .info.local import Local +from .info.network import Network + + +class InfoFactory: + def __init__(self, lnk_info): + self._lnk_info = lnk_info + + def _volume_id_and_local_base_path(self): + """ + If set, the VolumeID and LocalBasePath fields are present, and their + locations are specified by the values of the VolumeIDOffset and + LocalBasePathOffset fields, respectively. If the value of the + LinkInfoHeaderSize field is greater than or equal to 0x00000024, the + LocalBasePathUnicode field is present, and its location is specified by + the value of the LocalBasePathOffsetUnicode field. + If not set, the VolumeID, LocalBasePath, and LocalBasePathUnicode + fields are not present, and the values of the VolumeIDOffset and + LocalBasePathOffset fields are zero. If the value of the + LinkInfoHeaderSize field is greater than or equal to 0x00000024, the + value of the LocalBasePathOffsetUnicode field is zero. + """ + return bool(self._lnk_info.flags() & 0x0001) + + def _common_network_relative_link_and_path_suffix(self): + """ + If set, the CommonNetworkRelativeLink field is present, and its + location is specified by the value of the + CommonNetworkRelativeLinkOffset field. + If not set, the CommonNetworkRelativeLink field is not present, and the + value of the CommonNetworkRelativeLinkOffset field is zero. + """ + return bool(self._lnk_info.flags() & 0x0002) + + def info_class(self): + try: + if self._volume_id_and_local_base_path(): + return Local + elif self._common_network_relative_link_and_path_suffix(): + return Network + else: + return None + except struct.error as e: + warnings.warn(f"Error while selecting proper Info class: {e!r}") + return None diff --git a/plugin/libs/LnkParse3/lnk_file.py b/plugin/libs/LnkParse3/lnk_file.py new file mode 100644 index 0000000..aad3409 --- /dev/null +++ b/plugin/libs/LnkParse3/lnk_file.py @@ -0,0 +1,521 @@ +#!/usr/bin/env python3 + +__description__ = "Windows Shortcut file (LNK) parser" +__author__ = "Matmaus" +__version__ = "1.3.3" + +import json +import datetime +import argparse +from subprocess import list2cmdline + +from .lnk_header import LnkHeader +from .lnk_targets import LnkTargets +from .lnk_info import LnkInfo +from .info_factory import InfoFactory +from .string_data import StringData +from .extra_data import ExtraData + + +class LnkFile(object): + def __init__(self, fhandle=None, indata=None, cp=None): + if fhandle: + self.indata = fhandle.read() + elif indata: + self.indata = indata + + self.cp = cp + + self.process() + + def has_relative_path(self): + return bool("HasRelativePath" in self.header.link_flags()) + + def has_arguments(self): + return bool("HasArguments" in self.header.link_flags()) + + def is_unicode(self): + return bool("IsUnicode" in self.header.link_flags()) + + def has_name(self): + return bool("HasName" in self.header.link_flags()) + + def has_working_dir(self): + return bool("HasWorkingDir" in self.header.link_flags()) + + def has_icon_location(self): + return bool("HasIconLocation" in self.header.link_flags()) + + def has_target_id_list(self): + return bool("HasTargetIDList" in self.header.link_flags()) + + def has_link_info(self): + return bool("HasLinkInfo" in self.header.link_flags()) + + def force_no_link_info(self): + return bool("ForceNoLinkInfo" in self.header.link_flags()) + + def process(self): + index = 0 + + # Parse header + self.header = LnkHeader(indata=self.indata) + index += self.header.size() + + # XXX: json + self._target_index = index + 2 + + # Parse ID List + self.targets = None + if self.has_target_id_list(): + self.targets = LnkTargets(indata=self.indata[index:], cp=self.cp) + index += self.targets.size() + + # Parse Link Info + self.info = None + if self.has_link_info() and not self.force_no_link_info(): + info = LnkInfo(indata=self.indata[index:], cp=self.cp) + info_class = InfoFactory(info).info_class() + if info_class: + self.info = info_class(indata=self.indata[index:], cp=self.cp) + index += self.info.size() + + # Parse String Data + self.string_data = StringData(self, indata=self.indata[index:], cp=self.cp) + index += self.string_data.size() + + # Parse Extra Data + self.extras = ExtraData(indata=self.indata[index:], cp=self.cp) + + def print_lnk_file(self, print_all=False): + def cprint(text, level=0): + SPACING = 3 + UNWANTED_TRAITS = ["offset", "reserved", "size"] + text = str(text) + if print_all or all(x not in text.lower() for x in UNWANTED_TRAITS): + print(" " * (level * SPACING) + text) # add leading spaces + + def nice_id(identifier): + return identifier.capitalize().replace("_", " ") + + # TODO recursive nice print + cprint("Windows Shortcut Information:") + cprint("Header Size: %s" % self.header.size(), 1) + cprint("Link CLSID: %s" % self.header.link_cls_id(), 1) + cprint( + "Link Flags: %s - (%s)" + % (self.format_linkFlags(), self.header.r_link_flags()), + 1, + ) + cprint( + "File Flags: %s - (%s)" + % (self.format_fileFlags(), self.header.r_file_flags()), + 1, + ) + cprint("") + cprint("Creation Timestamp: %s" % (self.header.creation_time()), 1) + cprint("Modified Timestamp: %s" % (self.header.write_time()), 1) + cprint("Accessed Timestamp: %s" % (self.header.access_time()), 1) + cprint("") + cprint( + "File Size: %s (r: %s)" + % (str(self.header.file_size()), str(len(self.indata))), + 1, + ) + cprint("Icon Index: %s " % (str(self.header.icon_index())), 1) + cprint("Window Style: %s " % (str(self.header.window_style())), 1) + cprint("HotKey: %s " % (str(self.header.hot_key())), 1) + cprint("Reserved0: %s" % self.header.reserved0(), 1) + cprint("Reserved1: %s" % self.header.reserved1(), 1) + cprint("Reserved2: %s" % self.header.reserved2(), 1) + cprint("") + + if self.targets: + cprint("TARGETS:", 1) + cprint("Size: %s" % self.targets.id_list_size(), 2) + cprint("Index: %s" % self._target_index, 2) + cprint("ITEMS:", 2) + for target in self.targets.as_list(): + cprint(target["class"], 3) + for key, value in target.items(): + if key != "class": + cprint(f"{nice_id(key)}: {value}", 4) + cprint("") + + if self.info: + cprint("LINK INFO:", 1) + cprint("Link info size: %s" % self.info.size(), 2) + cprint("Link info header size: %s" % self.info.header_size(), 2) + cprint("Link info flags: %s" % self.info.flags(), 2) + cprint("Volume ID offset: %s" % self.info.volume_id_offset(), 2) + cprint("Local base path offset: %s" % self.info.local_base_path_offset(), 2) + cprint( + "Common network relative link offset: %s" + % self.info.common_network_relative_link_offset(), + 2, + ) + cprint( + "Common path suffix offset: %s" % self.info.common_path_suffix_offset(), + 2, + ) + if self.info.local_base_path_offset(): + cprint("Local base path: %s" % self.info.local_base_path(), 2) + if self.info.common_path_suffix_offset(): + cprint("Common path suffix: %s" % self.info.common_path_suffix(), 2) + if self.info.local_base_path_offset_unicode(): + cprint( + "Local base path offset unicode: %s" + % self.info.local_base_path_offset_unicode(), + 2, + ) + cprint( + "Local base unicode: %s" % self.info.local_base_path_unicode(), 2 + ) + if self.info.common_path_suffix_offset_unicode(): + cprint( + "Common path suffix offset unicode: %s" + % self.info.common_path_suffix_offset_unicode(), + 2, + ) + cprint( + "Common path suffix unicode: %s" + % self.info.common_path_suffix_unicode(), + 2, + ) + if type(self.info).__name__ == "Local": + cprint("LOCAL:", 2) + cprint("Volume ID size: %s" % self.info.volume_id_size(), 3) + cprint("Drive type: %s" % self.info.r_drive_type(), 3) + cprint("Volume label offset: %s" % self.info.volume_label_offset(), 3) + cprint("Drive serial number: %s" % self.info.drive_serial_number(), 3) + cprint("Drive type: %s" % self.info.drive_type(), 3) + cprint("Volume label: %s" % self.info.volume_label(), 3) + if self.info.common_network_relative_link(): + cprint( + "Common network relative link: %s" + % self.info.common_network_relative_link(), + 3, + ) + if self.info.volume_label_unicode_offset(): + cprint( + "Volume label unicode offset: %s" + % self.info.volume_label_unicode_offset(), + 3, + ) + cprint( + "Volume label unicode: %s" % self.info.volume_label_unicode(), 3 + ) + elif type(self.info).__name__ == "Network": + cprint( + "Common network relative link size: %s" + % self.info.common_network_relative_link_size(), + 3, + ) + cprint( + "Common network relative link flags: %s" + % self.info.common_network_relative_link_flags(), + 3, + ) + cprint("Net name offset: %s" % self.info.net_name_offset(), 3) + cprint("Device name offset: %s" % self.info.device_name_offset(), 3) + cprint( + "Network provider type: %s" % self.info.r_network_provider_type(), 3 + ) + if self.info.network_provider_type(): + cprint( + "Network provider type: %s" % self.info.network_provider_type(), + 3, + ) + if self.info.net_name_offset_unicode(): + cprint( + "Net name offset unicode: %s" + % self.info.net_name_offset_unicode(), + 3, + ) + cprint("Net name unicode: %s" % self.info.net_name_unicode(), 3) + if self.info.device_name_offset_unicode(): + cprint( + "Device name offset unicode: %s" + % self.info.device_name_offset_unicode(), + 3, + ) + cprint( + "Device name unicode: %s" % self.info.device_name_unicode(), 3 + ) + if self.info.net_name(): + cprint("Net name: %s" % self.info.net_name(), 3) + if self.info.device_name(): + cprint("Device name: %s" % self.info.device_name(), 3) + cprint("") + + cprint("DATA", 1) + for key, value in self.string_data.as_dict().items(): + cprint("%s: %s" % (nice_id(key), value), 2) + cprint("") + + cprint("EXTRA BLOCKS:", 1) + for extra_key, extra_value in self.extras.as_dict().items(): + cprint(f"{extra_key}", 2) + for key, value in extra_value.items(): + if extra_key == "METADATA_PROPERTIES_BLOCK" and isinstance(value, list): + cprint(f"{nice_id(key)}:", 3) + for storage in value: + cprint("Storage:", 4) + for storage_key, storage_value in storage.items(): + if isinstance(storage_value, list): + cprint(f"{nice_id(storage_key)}:", 5) + for item in storage_value: + cprint("Property:", 6) + for item_key, item_value in item.items(): + cprint(f"{nice_id(item_key)}: {item_value}", 7) + else: + cprint(f"{nice_id(storage_key)}: {storage_value}", 5) + else: + cprint(f"{nice_id(key)}: {value}", 3) + + def format_linkFlags(self): + return " | ".join(self.header.link_flags()) + + def format_fileFlags(self): + return " | ".join(self.header.file_flags()) + + # FIXME: Simple concat of path and arguments + @property + def lnk_command(self): + out = [] + + if self.has_relative_path(): + relative_path = self.string_data.relative_path() + out.append(list2cmdline([relative_path])) + + if self.has_arguments(): + out.append(self.string_data.command_line_arguments()) + + return " ".join(out) + + def print_shortcut_target(self, pjson=False): + out = self.lnk_command + + if pjson: + print(json.dumps({"shortcut_target": out})) + else: + print(out) + + def print_json(self, print_all=False): + res = self.get_json(print_all) + + def _datetime_to_str(obj): + if isinstance(obj, datetime.datetime): + return obj.replace(microsecond=0).isoformat() + return obj + + print( + json.dumps( + res, + indent=4, + separators=(",", ": "), + default=_datetime_to_str, + sort_keys=True, + ) + ) + + def get_json(self, get_all=False): + res = { + "header": { + "guid": self.header.link_cls_id(), + "r_link_flags": self.header.r_link_flags(), + "r_file_flags": self.header.r_file_flags(), + "creation_time": self.header.creation_time(), + "accessed_time": self.header.access_time(), + "modified_time": self.header.write_time(), + "file_size": self.header.file_size(), + "icon_index": self.header.icon_index(), + "windowstyle": self.header.window_style(), + "hotkey": self.header.hot_key(), + "r_hotkey": self.header.raw_hot_key(), + "link_flags": self.header.link_flags(), + "file_flags": self.header.file_flags(), + "header_size": self.header.size(), + "reserved0": self.header.reserved0(), + "reserved1": self.header.reserved1(), + "reserved2": self.header.reserved2(), + }, + "data": self.string_data.as_dict(), + "extra": self.extras.as_dict(), + } + + if self.targets: + res["target"] = { + "size": self.targets.id_list_size(), + "items": self.targets.as_list(), + "index": self._target_index, + } + + res["link_info"] = {} + if self.info: + res["link_info"] = { + "link_info_size": self.info.size(), + "link_info_header_size": self.info.header_size(), + "link_info_flags": self.info.flags(), + "volume_id_offset": self.info.volume_id_offset(), + "local_base_path_offset": self.info.local_base_path_offset(), + "common_network_relative_link_offset": self.info.common_network_relative_link_offset(), + "common_path_suffix_offset": self.info.common_path_suffix_offset(), + } + + if self.info.local_base_path_offset(): + res["link_info"]["local_base_path"] = self.info.local_base_path() + if self.info.common_path_suffix_offset(): + res["link_info"]["common_path_suffix"] = self.info.common_path_suffix() + if self.info.local_base_path_offset_unicode(): + res["link_info"][ + "local_base_path_offset_unicode" + ] = self.info.local_base_path_offset_unicode() + res["link_info"][ + "local_base_path_unicode" + ] = self.info.local_base_path_unicode() + if self.info.common_path_suffix_offset_unicode(): + res["link_info"][ + "common_path_suffix_offset_unicode" + ] = self.info.common_path_suffix_offset_unicode() + res["link_info"][ + "common_path_suffix_unicode" + ] = self.info.common_path_suffix_unicode() + + res["link_info"]["location_info"] = {} + if type(self.info).__name__ == "Local": + res["link_info"]["location"] = self.info.location() + + res["link_info"]["location_info"] = { + "volume_id_size": self.info.volume_id_size(), + "r_drive_type": self.info.r_drive_type(), + "volume_label_offset": self.info.volume_label_offset(), + "drive_serial_number": self.info.drive_serial_number(), + "drive_type": self.info.drive_type(), + "volume_label": self.info.volume_label(), + } + + if self.info.common_network_relative_link(): + res["link_info"]["location_info"][ + "common_network_relative_link" + ] = self.info.common_network_relative_link() + if self.info.volume_label_unicode_offset(): + res["link_info"]["location_info"][ + "volume_label_unicode_offset" + ] = self.info.volume_label_unicode_offset() + res["link_info"]["location_info"][ + "volume_label_unicode" + ] = self.info.volume_label_unicode() + elif type(self.info).__name__ == "Network": + res["link_info"]["location"] = self.info.location() + res["link_info"]["location_info"] = { + "common_network_relative_link_size": self.info.common_network_relative_link_size(), + "common_network_relative_link_flags": self.info.common_network_relative_link_flags(), + "net_name_offset": self.info.net_name_offset(), + "device_name_offset": self.info.device_name_offset(), + "r_network_provider_type": self.info.r_network_provider_type(), + } + if self.info.network_provider_type(): + res["link_info"]["location_info"][ + "network_provider_type" + ] = self.info.network_provider_type() + if self.info.net_name_offset_unicode(): + res["link_info"]["location_info"][ + "net_name_offset_unicode" + ] = self.info.net_name_offset_unicode() + res["link_info"]["location_info"][ + "net_name_unicode" + ] = self.info.net_name_unicode() + if self.info.device_name_offset_unicode(): + res["link_info"]["location_info"][ + "device_name_offset_unicode" + ] = self.info.device_name_offset_unicode() + res["link_info"]["location_info"][ + "device_name_unicode" + ] = self.info.device_name_unicode() + if self.info.net_name(): + res["link_info"]["location_info"]["net_name"] = self.info.net_name() + if self.info.device_name(): + res["link_info"]["location_info"][ + "device_name" + ] = self.info.device_name() + + if not get_all: + res["header"].pop("header_size", None) + res["header"].pop("reserved0", None) + res["header"].pop("reserved1", None) + res["header"].pop("reserved2", None) + res["link_info"].pop("link_info_size", None) + res["link_info"].pop("link_info_header_size", None) + res["link_info"].pop("volume_id_offset", None) + res["link_info"].pop("local_base_path_offset", None) + res["link_info"].pop("common_network_relative_link_offset", None) + res["link_info"].pop("common_path_suffix_offset", None) + if ( + "location" in res["link_info"] + and "Local" in res["link_info"]["location"] + ): + res["link_info"]["location_info"].pop("volume_id_size", None) + res["link_info"]["location_info"].pop("volume_label_offset", None) + if ( + "location" in res["link_info"] + and "Network" in res["link_info"]["location"] + ): + res["link_info"]["location_info"].pop( + "common_network_relative_link_size", None + ) + res["link_info"]["location_info"].pop("net_name_offset", None) + res["link_info"]["location_info"].pop("device_name_offset", None) + + if "target" in res: + res["target"].pop("index", None) + res["target"].pop("size", None) + if "items" in res["target"]: + for item in res["target"]["items"]: + if item: + item.pop("modification_time", None) + + return res + + +def main(): + arg_parser = argparse.ArgumentParser(description=__description__) + arg_parser.add_argument( + dest="file", + metavar="FILE", + help="absolute or relative path to the file", + ) + arg_parser.add_argument( + "-t", "--target", action="store_true", help="print shortcut target only" + ) + arg_parser.add_argument( + "-j", "--json", action="store_true", help="print output in JSON" + ) + arg_parser.add_argument( + "-c", + "--codepage", + dest="cp", + default="cp1252", + help="set codepage of ASCII strings", + ) + arg_parser.add_argument( + "-a", + "--all", + dest="print_all", + action="store_true", + help="print all extracted data (i.e. offsets and sizes)", + ) + args = arg_parser.parse_args() + + with open(args.file, "rb") as file: + lnk = LnkFile(fhandle=file, cp=args.cp) + if args.target: + lnk.print_shortcut_target(pjson=args.json) + elif args.json: + lnk.print_json(args.print_all) + else: + lnk.print_lnk_file(args.print_all) + + +if __name__ == "__main__": + main() diff --git a/plugin/libs/LnkParse3/lnk_header.py b/plugin/libs/LnkParse3/lnk_header.py new file mode 100644 index 0000000..c153124 --- /dev/null +++ b/plugin/libs/LnkParse3/lnk_header.py @@ -0,0 +1,494 @@ +from struct import unpack +from .decorators import must_be +from .decorators import uuid +from .decorators import filetime +from .exceptions import LnkParserError + +""" +SHELL_LINK_HEADER: +A ShellLinkHeader structure (section 2.1), which contains identification +information, timestamps, and flags that specify the presence of optional +structures. + +------------------------------------------------------------------ +| 0-7b | 8-15b | 16-23b | 24-31b | +------------------------------------------------------------------ +| HeaderSize == 0x0000004C | +------------------------------------------------------------------ +| LinkCLSID == 00021401-0000-0000-C000-000000000046 | +| 16 B | +------------------------------------------------------------------ +| LinkFlags | +------------------------------------------------------------------ +| FileAttributes | +------------------------------------------------------------------ +| CreationTime | +| 16 B | +------------------------------------------------------------------ +| AccessTime | +| 16 B | +------------------------------------------------------------------ +| WriteTime | +| 16 B | +------------------------------------------------------------------ +| FileSize | +------------------------------------------------------------------ +| IconIndex | +------------------------------------------------------------------ +| ShowCommand | +------------------------------------------------------------------ +| HotKey | Reserved1 | +------------------------------------------------------------------ +| Reserved2 | +------------------------------------------------------------------ +| Reserved3 | +------------------------------------------------------------------ +""" + + +class LnkHeader: + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow + WINDOW_STYLES = { + 1: "SW_SHOWNORMAL", + 3: "SW_SHOWMAXIMIZED", + 7: "SW_SHOWMINNOACTIVE", + } + + HOTKEY_VALUES_HIGH = { + b"\x00": "UNSET", + b"\x01": "SHIFT", + b"\x02": "CONTROL", + b"\x04": "ALT", + } + + HOTKEY_VALUES_LOW = { + b"\x00": "UNSET", + b"\x30": "0", + b"\x31": "1", + b"\x32": "2", + b"\x33": "3", + b"\x34": "4", + b"\x35": "5", + b"\x36": "6", + b"\x37": "7", + b"\x38": "8", + b"\x39": "9", + b"\x41": "A", + b"\x42": "B", + b"\x43": "C", + b"\x44": "D", + b"\x45": "E", + b"\x46": "F", + b"\x47": "G", + b"\x48": "H", + b"\x49": "I", + b"\x4A": "J", + b"\x4B": "K", + b"\x4C": "L", + b"\x4D": "M", + b"\x4E": "N", + b"\x4F": "O", + b"\x50": "P", + b"\x51": "Q", + b"\x52": "R", + b"\x53": "S", + b"\x54": "T", + b"\x55": "U", + b"\x56": "V", + b"\x57": "W", + b"\x58": "X", + b"\x59": "Y", + b"\x5A": "Z", + b"\x70": "F1", + b"\x71": "F2", + b"\x72": "F3", + b"\x73": "F4", + b"\x74": "F5", + b"\x75": "F6", + b"\x76": "F7", + b"\x77": "F8", + b"\x78": "F9", + b"\x79": "F10", + b"\x7A": "F11", + b"\x7B": "F12", + b"\x7C": "F13", + b"\x7D": "F14", + b"\x7E": "F15", + b"\x7F": "F16", + b"\x80": "F17", + b"\x81": "F18", + b"\x82": "F19", + b"\x83": "F20", + b"\x84": "F21", + b"\x85": "F22", + b"\x86": "F23", + b"\x87": "F24", + b"\x90": "NUM_LOCK", + b"\x91": "SCROLL_LOCK", + } + + LINK_FLAG_MASK = { # {{{ + # LinkTargetIDList structure (section 2.2) MUST follow the + # ShellLinkHeader. If this bit is not set, this structure MUST NOT + # be present. + 0x00000001: "HasTargetIDList", + # The shell link is saved with link information. If this bit is set, + # a LinkInfo structure (section 2.3) MUST be present. If this bit + # is not set, this structure MUST NOT be present. + 0x00000002: "HasLinkInfo", + # The shell link is saved with a name string. If this bit is set, + # a NAME_STRING StringData structure (section 2.4) MUST be present. + # If this bit is not set, this structure MUST NOT be present. + 0x00000004: "HasName", + # The shell link is saved with a relative path string. If this bit + # is set, a RELATIVE_PATH StringData structure (section 2.4) MUST + # be present. If this bit is not set, this structure MUST NOT be + # present. + 0x00000008: "HasRelativePath", + # The shell link is saved with a working directory string. If this + # bit is set, a WORKING_DIR StringData structure (section 2.4) MUST + # be present. If this bit is not set, this structure MUST NOT be + # present. + 0x00000010: "HasWorkingDir", + # The shell link is saved with command line arguments. If this bit + # is set, a COMMAND_LINE_ARGUMENTS StringData structure (section + # 2.4) MUST be present. If this bit is not set, this structure MUST + # NOT be present. + 0x00000020: "HasArguments", + # The shell link is saved with an icon location string. If this bit + # is set, an ICON_LOCATION StringData structure (section 2.4) MUST + # be present. If this bit is not set, this structure MUST NOT be + # present. + 0x00000040: "HasIconLocation", + # The shell link contains Unicode encoded strings. This bit SHOULD + # be set. If this bit is set, the StringData section contains + # Unicode-encoded strings; otherwise, it contains strings that are + # encoded using the system default code page. + 0x00000080: "IsUnicode", + # The LinkInfo structure (section 2.3) is ignored. + 0x00000100: "ForceNoLinkInfo", + # The shell link is saved with an EnvironmentVariableDataBlock + # (section 2.5.4). + 0x00000200: "HasExpString", + # The target is run in a separate virtual machine when launching + # a link target that is a 16-bit application. + 0x00000400: "RunInSeparateProcess", + # TODO: Unused1 + # A bit that is undefined and MUST be ignored. + 0x00000800: "Reserved0", + # The shell link is saved with a DarwinDataBlock (section 2.5.3). + 0x00001000: "HasDarwinID", + # The application is run as a different user when the target of the + # shell link is activated. + 0x00002000: "RunAsUser", + # The shell link is saved with an IconEnvironmentDataBlock (section + # 2.5.5). + 0x00004000: "HasExpIcon", + # The file system location is represented in the shell namespace + # when the path to an item is parsed into an IDList. + 0x00008000: "NoPidlAlias", + # TODO: Unused2 + # A bit that is undefined and MUST be ignored. + 0x00010000: "Reserved1", + # The shell link is saved with a ShimDataBlock (section 2.5.8). + 0x00020000: "RunWithShimLayer", + # The TrackerDataBlock (section 2.5.10) is ignored. + 0x00040000: "ForceNoLinkTrack", + # The shell link attempts to collect target properties and store + # them in the PropertyStoreDataBlock (section 2.5.7) when the link + # target is set. + 0x00080000: "EnableTargetMetadata", + # The EnvironmentVariableDataBlock is ignored. + 0x00100000: "DisableLinkPathTracking", + # The SpecialFolderDataBlock (section 2.5.9) and the + # KnownFolderDataBlock (section 2.5.6) are ignored when loading the + # shell link. If this bit is set, these extra data blocks SHOULD + # NOT be saved when saving the shell link. + 0x00200000: "DisableKnownFolderTracking", + # If the link has a KnownFolderDataBlock (section 2.5.6), the + # unaliased form of the known folder IDList SHOULD be used when + # translating the target IDList at the time that the link is + # loaded. + 0x00400000: "DisableKnownFolderAlias", + # Creating a link that references another link is enabled. + # Otherwise, specifying a link as the target IDList SHOULD NOT be + # allowed. + 0x00800000: "AllowLinkToLink", + # When saving a link for which the target IDList is under a known + # folder, either the unaliased form of that known folder or the + # target IDList SHOULD be used. + 0x01000000: "UnaliasOnSave", + # The target IDList SHOULD NOT be stored; instead, the path + # specified in the 2.1.2 FileAttributesFlags + # EnvironmentVariableDataBlock (section 2.5.4) SHOULD be used to + # refer to the target. + 0x02000000: "PreferEnvironmentPath", + # When the target is a UNC name that refers to a location on + # a local machine, the local path IDList in the + # PropertyStoreDataBlock (section 2.5.7) SHOULD be stored, so it + # can be used when the link is loaded on the local machine. + 0x04000000: "KeepLocalIDListForUNCTarget", + } # }}} + + FILE_FLAG_MASK = { # {{{ + # The file or directory is read-only. For a file, if this bit is set, + # applications can read the file but cannot write to it or delete it. + # For a directory, if this bit is set, applications cannot delete the + # directory. + 0x00000001: "FILE_ATTRIBUTE_READONLY", + # The file or directory is hidden. If this bit is set, the file or + # folder is not included in an ordinary directory listing. + 0x00000002: "FILE_ATTRIBUTE_HIDDEN", + # The file or directory is part of the operating system or is used + # exclusively by the operating system. + 0x00000004: "FILE_ATTRIBUTE_SYSTEM", + # TODO: Reserved1 + # A bit that MUST be zero. + 0x00000008: "Reserved, not used by the LNK format", + # The link target is a directory instead of a file. + 0x00000010: "FILE_ATTRIBUTE_DIRECTORY", + # The file or directory is an archive file. Applications use this flag + # to mark files for backup or removal. + 0x00000020: "FILE_ATTRIBUTE_ARCHIVE", + # A bit that MUST be zero. + 0x00000040: "FILE_ATTRIBUTE_DEVICE", + # The file or directory has no other flags set. If this bit is 1, all + # other bits in this structure MUST be clear. + 0x00000080: "FILE_ATTRIBUTE_NORMAL", + # The file is being used for temporary storage. + 0x00000100: "FILE_ATTRIBUTE_TEMPORARY", + # The file is a sparse file. + 0x00000200: "FILE_ATTRIBUTE_SPARSE_FILE", + # The file or directory has an associated reparse point. + 0x00000400: "FILE_ATTRIBUTE_REPARSE_POINT", + # The file or directory is compressed. For a file, this means that all + # data in the file is compressed. For a directory, this means that + # compression is the default for newly created files and + # subdirectories. + 0x00000800: "FILE_ATTRIBUTE_COMPRESSED", + # The data of the file is not immediately available. + 0x00001000: "FILE_ATTRIBUTE_OFFLINE", + # The contents of the file need to be indexed. + 0x00002000: "FILE_ATTRIBUTE_NOT_CONTENT_INDEXED", + # The file or directory is encrypted. For a file, this means that all + # data in the file is encrypted. For a directory, this means that + # encryption is the default for newly created files and subdirectories. + 0x00004000: "FILE_ATTRIBUTE_ENCRYPTED", + # The directory or user data stream is configured with integrity + # (only supported on ReFS volumes). + 0x00008000: "FILE_ATTRIBUTE_INTEGRITY_STREAM", + # Is virtual + 0x00010000: "FILE_ATTRIBUTE_VIRTUAL", + # The user data stream not to be read by the background data + # integrity scanner (AKA scrubber). + 0x00020000: "FILE_ATTRIBUTE_NO_SCRUB_DATA", + } # }}} + + def __init__(self, fhandle=None, indata=None): + if fhandle: + self._raw = fhandle.read() + elif indata: + self._raw = indata + else: + raise LnkParserError( + "Both `LnkHeader` arguments `fhandle` and `indata` are evalued as `None`" + ) + + self._lnk_header = {} + self._stash = {} + + self._raw = self._raw[: self.size()] + + @must_be(int("0x0000004C", 16)) + def size(self): + """HeaderSize (4 bytes): + The size, in bytes, of this structure. + This value MUST be 0x0000004C. + """ + start, end = 0, 4 + size = unpack(" LinkInfoSize | +------------------------------------------------------------------ +| LinkInfoHeaderSize | +------------------------------------------------------------------ +| LinkInfoFlags | +------------------------------------------------------------------ +| VolumeIDOffset | +------------------------------------------------------------------ +| LocalBasePathOffset | +------------------------------------------------------------------ +| CommonNetworkRelativeLinkOffset | +------------------------------------------------------------------ +| CommonPathSuffixOffset | +------------------------------------------------------------------ +| LocalBasePathOffsetUnicode (optional) | +------------------------------------------------------------------ +| CommonPathSuffixOffsetUnicode (optional) | +------------------------------------------------------------------ +| VolumeID (optional) | +| ? B | +------------------------------------------------------------------ +| LocalBasePath (optional) | +| ? B | +------------------------------------------------------------------ +| CommonNetworkRelativeLink (optional)| +| ? B | +------------------------------------------------------------------ +| CommonPathSuffix (optional) | +| ? B | +------------------------------------------------------------------ +| LocalBasePathUnicode (optional) | +| ? B | +------------------------------------------------------------------ +| CommonPathSuffixUnicode (optional) | +| ? B | +------------------------------------------------------------------ +""" + + +class LnkInfo: + def __init__(self, indata=None, cp=None): + self._raw = indata + self.text_processor = TextProcessor(cp=cp) + + def size(self): + """LinkInfoSize (4 bytes): + A 32-bit, unsigned integer that specifies the size, in bytes, of the + LinkInfo structure. All offsets specified in this structure MUST be + less than this value, and all strings contained in this structure MUST + fit within the extent defined by this size. + """ + start, end = 0, 4 + return unpack("= 0x00000024) + + def common_path_suffix(self): + """CommonPathSuffix (variable): + A NULL-terminated string, defined by the system default code page, + which is used to construct the full path to the link item or link + target by being appended to the string in the LocalBasePath field. + """ + if not self.common_path_suffix_offset(): + return None + + start = self.common_path_suffix_offset() + + binary = self._raw[start:] + text = self.text_processor.read_string(binary) + return text + + def local_base_path_offset_unicode(self): + """LocalBasePathOffsetUnicode (4 bytes): + An optional, 32-bit, unsigned integer that specifies the location of + the LocalBasePathUnicode field. If the VolumeIDAndLocalBasePath flag + is set, this value is an offset, in bytes, from the start of the + LinkInfo structure; otherwise, this value MUST be zero. This field + can be present only if the value of the LinkInfoHeaderSize field is + greater than or equal to 0x00000024. + """ + if not self._has_opt_fields(): + return None + + start, end = 28, 32 + return unpack(" Location | +| ? B | +---------------------------------------------------------------------- +| Description | +| ? B | +---------------------------------------------------------------------- +| Comments | +| ? B | +---------------------------------------------------------------------- +| Unknown | +| ? B | +---------------------------------------------------------------------- +""" + + +# https://github.com/libyal/libfwsi/blob/master/documentation/Windows%20Shell%20Item%20format.asciidoc#35-network-location-shell-item +class NetworkLocation(LnkTargetBase): + def __init__(self, *args, **kwargs): + self.name = "Network location" + super().__init__(*args, **kwargs) + + it = self._string_data() + self._location = next(it) + + self._description = None + if self._has_description(): + self._description = next(it) + + self._comments = None + if self._has_comments(): + self._comments = next(it) + + def as_item(self): + item = super().as_item() + item["flags"] = self.flags() + item["content_flags"] = self.content_flags() + item["location"] = self.location() + return item + + # TODO: rename to class_type_indicator + def flags(self): + start, end = 0, 1 + flags = unpack(" ShellFolderID | +| 16 B | +---------------------------------------------------------------------- +| ExtensionBlock | +| ? B | +---------------------------------------------------------------------- +""" + + +class RootFolder(LnkTargetBase): + # https://github.com/libyal/libfwsi/blob/master/documentation/Windows%20Shell%20Item%20format.asciidoc#321-sort-index + SORT_INDEX = { + 0x00: "Internet Explorer", + 0x42: "Libraries", + 0x44: "Users", + 0x48: "My Documents", + 0x50: "My Computer", + 0x58: "My Networs Places/Network", + 0x60: "Recycle Bin", + 0x68: "Internet Explorer", + 0x70: "Unknown", + 0x80: "My Games", + } + + def __init__(self, *args, **kwargs): + self.name = "Root Folder" + super().__init__(*args, **kwargs) + + def as_item(self): + item = super().as_item() + item["sort_index"] = self.sort_index() + item["guid"] = self.guid() + return item + + def sort_index(self): + start, end = 1, 2 + index = unpack(" 20: + # TODO: Extension block + return None + else: + return None diff --git a/plugin/libs/LnkParse3/target/shell_fs_folder.py b/plugin/libs/LnkParse3/target/shell_fs_folder.py new file mode 100644 index 0000000..9408e85 --- /dev/null +++ b/plugin/libs/LnkParse3/target/shell_fs_folder.py @@ -0,0 +1,90 @@ +from struct import unpack +from ..target.lnk_target_base import LnkTargetBase +from ..decorators import dostime + +""" +---------------------------------------------------------------------- +| 0-7b | 8-15b | +---------------------------------------------------------------------- +| ClassTypeIndicator == 0x30-0x3F| UnknownValue | +---------------------------------------------------------------------- +| FileSize | +| 4 B | +---------------------------------------------------------------------- +| LastModificationDateAndTime | +| 4 B | +---------------------------------------------------------------------- +| FileAttributeFlags | +---------------------------------------------------------------------- +| PrimaryName | +| ? B | +---------------------------------------------------------------------- +| UnknownData | +| ? B | +---------------------------------------------------------------------- +""" + + +# TODO: rename to file_entry +# https://github.com/libyal/libfwsi/blob/master/documentation/Windows%20Shell%20Item%20format.asciidoc#34-file-entry-shell-item +class ShellFSFolder(LnkTargetBase): + def __init__(self, *args, **kwargs): + self.name = "File entry" + super().__init__(*args, **kwargs) + + def as_item(self): + item = super().as_item() + try: + item["flags"] = self.flags() + item["file_size"] = self.file_size() + item["modification_time"] = self.modification_time() + item["file_attribute_flags"] = self.file_attribute_flags() + item["primary_name"] = self.primary_name() + except KeyError: + # FIXME This try-catch is just a hot-fix. + # We should probably solve failing attributes in a better way. + pass + return item + + # dup: ./my_computer.py flags() + # dup: ../target_factory.py item_type() + def flags(self): + flags = self.class_type_indicator() + + # FIXME: delete masking + return self.SHELL_ITEM_SHEL_FS_FOLDER[flags & 0x0F] + + def file_size(self): + start, end = 2, 6 + size = unpack("HHI", binary[8:16]) + + uuid = "%08X-%04X-%04X-%04X-%04X%08X" % (d1, d2, d3, d4, d51, d52) + + return uuid + + +def _quad_to_hex(quad): + # An implemetation is based on + # https://metadataconsulting.blogspot.com/2019/12/CSharp-Convert-a-GUID-to-a-Darwin-Descriptor-and-back.html + base_85 = "!$%&'()*+,-.0123456789=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~" + i = 5 + ddec = 0 + while i >= 1: + char = quad[i - 1] + b85 = base_85.find(char) + ddec = ddec + b85 + if i > 1: + ddec = ddec * 85 + i -= 1 + + return f"{ddec:08X}" + + +def parse_packed_uuid(text): + if text is None: + return None + + # An implemetation is based on + # https://metadataconsulting.blogspot.com/2019/12/CSharp-Convert-a-GUID-to-a-Darwin-Descriptor-and-back.html + quad1 = _quad_to_hex(text[0:5]) + quad2 = _quad_to_hex(text[5:10]) + quad3 = _quad_to_hex(text[10:15]) + quad4 = _quad_to_hex(text[15:20]) + quads = quad1 + quad2 + quad3 + quad4 + + d1 = quads[:8] + d2 = quads[12:16] + d3 = quads[8:12] + d41 = quads[22:24] + d42 = quads[20:22] + d51 = quads[18:20] + d52 = quads[16:18] + d53 = quads[30:32] + d54 = quads[28:30] + d55 = quads[26:28] + d56 = quads[24:26] + + uuid = "%s-%s-%s-%s%s-%s%s%s%s%s%s" % ( + d1, + d2, + d3, + d41, + d42, + d51, + d52, + d53, + d54, + d55, + d56, + ) + + return uuid + + +def parse_filetime(binary): + # + # Source: + # https://gist.github.com/Mostafa-Hamdy-Elgiar/9714475f1b3bc224ea063af81566d873 + # https://stackoverflow.com/questions/38878647/python-convert-filetime-to-datetime-for-dates-before-1970 + # https://computerforensics.parsonage.co.uk/downloads/TheMeaningofLIFE.pdf + try: + nanosec = unpack("> 9) + 1980, + ((dos & 0x000001E0) >> 5), + ((dos & 0x0000001F) >> 0), + ((dos & 0xF8000000) >> 27), + ((dos & 0x07E00000) >> 21), + ((dos & 0x001F0000) >> 16) * 2, + ) + + return datetime(*ymdhms, tzinfo=timezone.utc) + except ValueError: + if sys.version_info < (3, 8, 0): + # HACK for older versions for bytes.hex() + # https://docs.python.org/3.9/library/stdtypes.html?highlight=hex#bytes.hex + iterator = iter(binary.hex()) + invalid_date = " ".join(a + b for a, b in zip(iterator, iterator)) + else: + invalid_date = binary.hex(" ") + msg = "Invalid dostime: %s" % invalid_date + warnings.warn(msg) + return None diff --git a/plugin/libs/pylnk3/__init__.py b/plugin/libs/pylnk3/__init__.py deleted file mode 100644 index f288aec..0000000 --- a/plugin/libs/pylnk3/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .structures import Lnk diff --git a/plugin/libs/pylnk3/exceptions.py b/plugin/libs/pylnk3/exceptions.py deleted file mode 100644 index ace2407..0000000 --- a/plugin/libs/pylnk3/exceptions.py +++ /dev/null @@ -1,10 +0,0 @@ -class FormatException(Exception): - pass - - -class MissingInformationException(Exception): - pass - - -class InvalidKeyException(Exception): - pass diff --git a/plugin/libs/pylnk3/flags.py b/plugin/libs/pylnk3/flags.py deleted file mode 100644 index c0a2c73..0000000 --- a/plugin/libs/pylnk3/flags.py +++ /dev/null @@ -1,60 +0,0 @@ -from pprint import pformat -from typing import Any, Dict, Tuple - -_MODIFIER_KEYS = ('SHIFT', 'CONTROL', 'ALT') - - -class Flags: - - def __init__(self, flag_names: Tuple[str, ...], flags_bytes: int = 0) -> None: - 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: int) -> None: - for pos, flag_name in enumerate(self._flag_names): - self._flags[flag_name] = bool(flags_bytes >> pos & 0x1) - - @property - def bytes(self) -> int: - result = 0 - for pos in range(len(self._flag_names)): - result = (self._flags[self._flag_names[pos]] and 1 or 0) << pos | result - return result - - def __getitem__(self, key: str) -> Any: - if key in self._flags: - return object.__getattribute__(self, '_flags')[key] - return object.__getattribute__(self, key) - - def __setitem__(self, key: str, value: bool) -> None: - 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: str) -> Any: - if key in self._flags: - return object.__getattribute__(self, '_flags')[key] - return object.__getattribute__(self, key) - - def __setattr__(self, key: str, value: Any) -> None: - if ('_flags' not in self.__dict__) or (key in self.__dict__): - object.__setattr__(self, key, value) - else: - self.__setitem__(key, value) - - def __str__(self) -> str: - return pformat(self._flags, indent=2) - - -class ModifierKeys(Flags): - - def __init__(self, flags_bytes: int = 0) -> None: - Flags.__init__(self, _MODIFIER_KEYS, flags_bytes) - - def __str__(self) -> str: - s = "" - s += self.CONTROL and "CONTROL+" or "" - s += self.SHIFT and "SHIFT+" or "" - s += self.ALT and "ALT+" or "" - return s diff --git a/plugin/libs/pylnk3/helpers.py b/plugin/libs/pylnk3/helpers.py deleted file mode 100644 index f71d927..0000000 --- a/plugin/libs/pylnk3/helpers.py +++ /dev/null @@ -1,216 +0,0 @@ -import ntpath -import re -from typing import Any, Dict, Iterable, List, Optional, Union - -from .structures import ( - DriveEntry, ExtraData, ExtraData_EnvironmentVariableDataBlock, IDListEntry, LinkInfo, - LinkTargetIDList, Lnk, PathSegmentEntry, RootEntry, UwpSegmentEntry, -) -from .structures.id_list.path import TYPE_FOLDER -from .structures.id_list.root import ROOT_MY_COMPUTER, ROOT_UWP_APPS - -# def is_lnk(f: BytesIO) -> bool: -# 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: str) -> Iterable[str]: - dirname, base = ntpath.split(p) - if base != '': - yield from path_levels(dirname) - yield p - - -def is_drive(data: Union[str, Any]) -> bool: - if not isinstance(data, str): - return False - p = re.compile("[a-zA-Z]:\\\\?$") - return p.match(data) is not None - - -def parse(lnk: str) -> Lnk: - return Lnk(lnk) - - -def create(f: Optional[str] = None) -> Lnk: - lnk = Lnk() - lnk.file = f - return lnk - - -def for_file( - target_file: str, - lnk_name: Optional[str] = None, - arguments: Optional[str] = None, - description: Optional[str] = None, - icon_file: Optional[str] = None, - icon_index: int = 0, - work_dir: Optional[str] = None, - window_mode: Optional[str] = None, - is_file: Optional[bool] = None, -) -> Lnk: - 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:]: - is_last_level = level == levels[-1] - # consider all segments before last as directory - segment = PathSegmentEntry.create_for_path(level, is_file=is_file if is_last_level else False) - 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: List[Union[str, Dict[str, Any]]], - lnk_name: Optional[str] = None, -) -> Lnk: - """ - 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 not isinstance(data, (list, tuple)): - raise ValueError("Invalid data format, list or tuple expected") - lnk = Lnk() - entries: List[IDListEntry] = [] - if is_drive(data[0]): - assert isinstance(data[0], str) - # this is an absolute link - entries.append(RootEntry(ROOT_MY_COMPUTER)) - if not data[0].endswith('\\'): - data[0] += "\\" - drive = data[0].encode("ascii") - data.pop(0) - entries.append(DriveEntry(drive)) - data_without_root: List[Dict[str, Any]] = data # type: ignore - for level in data_without_root: - 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_without_root[-1]['type'] == TYPE_FOLDER: - lnk.file_flags.directory = True - if lnk_name: - lnk.save(lnk_name) - return lnk - - -def build_uwp( - package_family_name: str, - target: str, - location: Optional[str] = None, - logo44x44: Optional[str] = None, - lnk_name: Optional[str] = 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 diff --git a/plugin/libs/pylnk3/structures/__init__.py b/plugin/libs/pylnk3/structures/__init__.py deleted file mode 100644 index 6f6acc5..0000000 --- a/plugin/libs/pylnk3/structures/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from ..structures.extra_data import ( - ExtraData, ExtraData_DataBlock, ExtraData_EnvironmentVariableDataBlock, - ExtraData_IconEnvironmentDataBlock, ExtraData_PropertyStoreDataBlock, ExtraData_Unparsed, - PropertyStore, TypedPropertyValue, -) -from ..structures.id_list.base import IDListEntry -from ..structures.id_list.drive import DriveEntry -from ..structures.id_list.id_list import LinkTargetIDList -from ..structures.id_list.path import PathSegmentEntry -from ..structures.id_list.root import RootEntry -from ..structures.id_list.uwp import UwpMainBlock, UwpSegmentEntry -from ..structures.link_info import LinkInfo -from ..structures.lnk import Lnk diff --git a/plugin/libs/pylnk3/structures/extra_data.py b/plugin/libs/pylnk3/structures/extra_data.py deleted file mode 100644 index 6d2faee..0000000 --- a/plugin/libs/pylnk3/structures/extra_data.py +++ /dev/null @@ -1,386 +0,0 @@ -from io import BufferedIOBase, BytesIO -from struct import unpack -from typing import Any, Dict, List, Optional, Tuple, Type, Union - -from ..utils.data import convert_time_to_unix -from ..utils.guid import guid_to_str -from ..utils.padding import padding -from ..utils.read_write import read_byte, read_int, read_short, write_int, write_short - - -class TypedPropertyValue: - # types: [MS-OLEPS] section 2.15 - def __init__( - self, - bytes_: Optional[bytes] = None, - type_: Optional[int] = None, - value: Optional[bytes] = None, - ) -> None: - if bytes_ is not None: - self.type: int = read_short(BytesIO(bytes_)) - padding = bytes_[2:4] - self.value: bytes = bytes_[4:] - elif type_ is not None and value is not None: - self.type = type_ - self.value = value or b'' - else: - raise ValueError("Either bytes or type and value must be given.") - - def set_string(self, value: str) -> None: - 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) -> bytes: - buf = BytesIO() - write_short(self.type, buf) - write_short(0x0000, buf) - buf.write(self.value) - return buf.getvalue() - - def __str__(self) -> str: - value = self.value - if self.type == 0x1F: - size = value[:4] - value_str = value[4:].decode('utf-16-le') - elif self.type == 0x15: - value_str = unpack(' None: - self.is_strings = is_strings - self.format_id = format_id or b'' - self._is_end = False - self.properties: List[Tuple[Union[str, int], TypedPropertyValue]] = properties or [] - if bytes: - self.read(bytes) - - def read(self, bytes_io: BytesIO) -> None: - 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) - _ = read_byte(buf) # reserved - 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) - _ = read_byte(buf) # reserved - value = TypedPropertyValue(buf.read(value_size - 9)) - self.properties.append((value_id, value)) - - @property - def bytes(self) -> bytes: - size = 8 + len(self.format_id) - properties = BytesIO() - for name, value in self.properties: - value_bytes = value.bytes - if self.is_strings: - assert isinstance(name, str) - 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: - assert isinstance(name, int) - 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) -> str: - 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_DataBlock: - def __init__(self, bytes: Optional[bytes] = None, **kwargs: Any) -> None: - raise NotImplementedError - - def bytes(self) -> bytes: - raise NotImplementedError - - -class ExtraData_IconEnvironmentDataBlock(ExtraData_DataBlock): - def __init__(self, bytes: Optional[bytes] = None) -> None: - # self._size = None - # self._signature = None - self._signature = 0xA0000007 - self.target_ansi: str = None # type: ignore[assignment] - self.target_unicode: str = None # type: ignore[assignment] - if bytes: - self.read(bytes) - - def read(self, bytes: bytes) -> None: - 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) -> bytes: - 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) -> str: - 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 - - -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(ExtraData_DataBlock): - def __init__( - self, - signature: int, - bytes: Optional[bytes] = None, - data: Optional[bytes] = None, - ) -> None: - self._signature = signature - self._size = None - if bytes is not None: - self.data = bytes - elif data is not None: - self.data = data - else: - raise ValueError("Either bytes or data must be given.") - - # 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) -> bytes: - buf = BytesIO() - write_int(len(self.data) + 8, buf) - write_int(self._signature, buf) - buf.write(self.data) - return buf.getvalue() - - def __str__(self) -> str: - s = f'ExtraDataBlock\n signature {hex(self._signature)}\n data: {self.data!r}' - return s - - -class ExtraData_PropertyStoreDataBlock(ExtraData_DataBlock): - def __init__( - self, - bytes: Optional[bytes] = None, - stores: Optional[List[PropertyStore]] = None, - ) -> None: - self._size = None - self._signature = 0xA0000009 - self.stores = [] - if stores: - self.stores = stores - if bytes: - self.read(bytes) - - def read(self, bytes: bytes) -> None: - 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) -> bytes: - 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) -> str: - s = 'PropertyStoreDataBlock' - for prop_store in self.stores: - s += '\n %s' % str(prop_store) - return s - - -class ExtraData_EnvironmentVariableDataBlock(ExtraData_DataBlock): - def __init__(self, bytes: Optional[bytes] = None) -> None: - self._signature = 0xA0000001 - self.target_ansi = '' - self.target_unicode = '' - if bytes: - self.read(bytes) - - def read(self, bytes: bytes) -> None: - buf = BytesIO(bytes) - self.target_ansi = buf.read(260).decode() - self.target_unicode = buf.read(520).decode('utf-16-le') - - def bytes(self) -> bytes: - 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) -> str: - 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: Dict[str, Type[ExtraData_DataBlock]] = { - 'IconEnvironmentDataBlock': ExtraData_IconEnvironmentDataBlock, - 'PropertyStoreDataBlock': ExtraData_PropertyStoreDataBlock, - 'EnvironmentVariableDataBlock': ExtraData_EnvironmentVariableDataBlock, -} - - -class ExtraData: - # EXTRA_DATA = *EXTRA_DATA_BLOCK TERMINAL_BLOCK - def __init__(self, lnk: Optional[BufferedIOBase] = None, blocks: Optional[List[ExtraData_DataBlock]] = None) -> 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) -> bytes: - result = b'' - for block in self.blocks: - result += block.bytes() - result += b'\x00\x00\x00\x00' # TerminalBlock - return result - - def __str__(self) -> str: - s = '' - for block in self.blocks: - s += '\n' + str(block) - return s diff --git a/plugin/libs/pylnk3/structures/id_list/base.py b/plugin/libs/pylnk3/structures/id_list/base.py deleted file mode 100644 index 8f52bfd..0000000 --- a/plugin/libs/pylnk3/structures/id_list/base.py +++ /dev/null @@ -1,8 +0,0 @@ -from abc import abstractmethod - - -class IDListEntry: - @property - @abstractmethod - def bytes(self) -> bytes: - ... diff --git a/plugin/libs/pylnk3/structures/id_list/drive.py b/plugin/libs/pylnk3/structures/id_list/drive.py deleted file mode 100644 index 7a18501..0000000 --- a/plugin/libs/pylnk3/structures/id_list/drive.py +++ /dev/null @@ -1,38 +0,0 @@ -import re -from typing import Union - -from ...exceptions import FormatException -from ...structures import IDListEntry - -_DRIVE_PATTERN = re.compile(r'(\w)[:/\\]*$') - - -class DriveEntry(IDListEntry): - - def __init__(self, drive: Union[bytes, str]) -> None: - if len(drive) == 23: - assert isinstance(drive, bytes) - # binary data from parsed lnk - self.drive = drive[1:3] - else: - # text representation - assert isinstance(drive, str) - m = _DRIVE_PATTERN.match(drive.strip()) - if m: - drive = m.groups()[0].upper() + ':' - self.drive = drive.encode() - else: - raise FormatException("This is not a valid drive: " + str(drive)) - - @property - def bytes(self) -> bytes: - 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) -> str: - return f"" diff --git a/plugin/libs/pylnk3/structures/id_list/id_list.py b/plugin/libs/pylnk3/structures/id_list/id_list.py deleted file mode 100644 index 0b42bdf..0000000 --- a/plugin/libs/pylnk3/structures/id_list/id_list.py +++ /dev/null @@ -1,99 +0,0 @@ -from io import BytesIO -from typing import List, Optional - -from ...structures.id_list.base import IDListEntry -from ...structures.id_list.drive import DriveEntry -from ...structures.id_list.path import PathSegmentEntry -from ...structures.id_list.root import ROOT_MY_COMPUTER, ROOT_NETWORK_PLACES, RootEntry -from ...structures.id_list.uwp import UwpSegmentEntry -from ...utils.read_write import read_short, write_short - - -class LinkTargetIDList: - - def __init__(self, bytes: Optional[bytes] = None) -> None: - self.items: List[IDListEntry] = [] - if bytes is not None: - buf = BytesIO(bytes) - raw = [] - entry_len = read_short(buf) - while entry_len > 0: - raw.append(buf.read(entry_len - 2)) # the length includes the size - entry_len = read_short(buf) - self._interpret(raw) - - def _interpret(self, raw: Optional[List[bytes]]) -> None: - if not raw: - return - elif raw[0][0] == 0x1F: - root_entry = RootEntry(raw[0]) - self.items.append(root_entry) - if root_entry.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 root_entry.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) -> str: - segments: List[str] = [] - 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(str(item)) - return '\\'.join(segments) - - def _validate(self) -> None: - if not len(self.items): - return - root_entry = self.items[0] - if isinstance(root_entry, RootEntry) and root_entry.root == ROOT_MY_COMPUTER: - second_entry = self.items[1] - if isinstance(second_entry, DriveEntry): - return - if ( - isinstance(second_entry, PathSegmentEntry) - and second_entry.full_name is not None - and second_entry.full_name.startswith('::') - ): - return - raise ValueError("A drive is required for absolute lnks") - - @property - def bytes(self) -> bytes: - self._validate() - out = BytesIO() - for item in self.items: - bytes = item.bytes - write_short(len(bytes) + 2, out) # len + terminator - out.write(bytes) - out.write(b'\x00\x00') - return out.getvalue() - - def __str__(self) -> str: - string = ':\n' - for item in self.items: - string += f' {item}\n' - return string.strip() diff --git a/plugin/libs/pylnk3/structures/id_list/path.py b/plugin/libs/pylnk3/structures/id_list/path.py deleted file mode 100644 index d4b4e7d..0000000 --- a/plugin/libs/pylnk3/structures/id_list/path.py +++ /dev/null @@ -1,228 +0,0 @@ -import ntpath -import os -from datetime import datetime -from io import BytesIO -from typing import Optional - -from ...exceptions import MissingInformationException -from ...structures.id_list.base import IDListEntry -from ...utils.guid import bytes_from_guid, guid_from_bytes -from ...utils.read_write import ( - read_cstring, read_cunicode, read_dos_datetime, read_double, read_int, read_short, - write_cstring, write_cunicode, write_dos_datetime, write_double, write_int, write_short, -) - -_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()) - -TYPE_FOLDER = 'FOLDER' -TYPE_FILE = 'FILE' - - -class PathSegmentEntry(IDListEntry): - - def __init__(self, bytes: Optional[bytes] = None) -> 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_unicode = read_short(buf) # offset from start of extra_size - # 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: str, is_file: Optional[bool] = None) -> 'PathSegmentEntry': - entry = cls() - 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) - if is_file is None: - is_file = not os.path.isdir(path) - except FileNotFoundError: - now = datetime.now() - entry.file_size = 0 - entry.modified = now - entry.created = now - entry.accessed = now - if is_file is None: - is_file = '.' in ntpath.split(path)[-1][1:] - entry.short_name = ntpath.split(path)[1] - entry.full_name = entry.short_name - entry.type = TYPE_FILE if is_file else TYPE_FOLDER - return entry - - def _validate(self) -> None: - 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) -> bytes: - if self.full_name is None: - return b'' - self._validate() - - # explicit check to have strict types without optionals - assert self.short_name is not None - assert self.type is not None - assert self.file_size is not None - assert self.modified is not None - assert self.created is not None - assert self.accessed is not None - - 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) -> str: - return "" % self.full_name diff --git a/plugin/libs/pylnk3/structures/id_list/root.py b/plugin/libs/pylnk3/structures/id_list/root.py deleted file mode 100644 index 702378a..0000000 --- a/plugin/libs/pylnk3/structures/id_list/root.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import Union - -from ...structures.id_list.base import IDListEntry -from ...utils.guid import guid_from_bytes - -ROOT_MY_COMPUTER = 'MY_COMPUTER' -ROOT_NETWORK_PLACES = 'NETWORK_PLACES' -ROOT_MY_DOCUMENTS = 'MY_DOCUMENTS' -ROOT_NETWORK_SHARE = 'NETWORK_SHARE' -ROOT_NETWORK_SERVER = 'NETWORK_SERVER' -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()) - - -class RootEntry(IDListEntry): - - def __init__(self, root: Union[str, bytes]) -> None: - if root is not None: - # create from text representation - if isinstance(root, str): - self.root = root - self.guid: str = _ROOT_LOCATION_GUIDS[root] - return - else: - # 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) -> bytes: - 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) -> str: - return "" % self.root diff --git a/plugin/libs/pylnk3/structures/id_list/uwp.py b/plugin/libs/pylnk3/structures/id_list/uwp.py deleted file mode 100644 index 9c4b94b..0000000 --- a/plugin/libs/pylnk3/structures/id_list/uwp.py +++ /dev/null @@ -1,224 +0,0 @@ -from io import BytesIO -from typing import List, Optional, Union - -from ...structures.id_list.base import IDListEntry -from ...utils.guid import bytes_from_guid, guid_from_bytes -from ...utils.read_write import ( - read_byte, read_cunicode, read_int, read_short, write_byte, write_cunicode, write_int, - write_short, -) - - -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: Optional[bytes] = None, - type: Optional[int] = None, - value: Optional[Union[str, bytes]] = None, - ) -> None: - if type is None and bytes is None: - raise ValueError("Either bytes or type must be set") - self._data = bytes or b'' - self.value = value - if type is not None: - self.type = type - 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) -> str: - string = f'UwpSubBlock {self.name} ({hex(self.type)}): {self.value!r}' - return string.strip() - - @property - def bytes(self) -> bytes: - 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: Optional[bytes] = None, - guid: Optional[str] = None, - blocks: Optional[List[UwpSubBlock]] = None, - ) -> None: - self._data = bytes or b'' - self._blocks = blocks or [] - if guid is not None: - 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) -> str: - string = f' {self.guid}:\n' - for block in self._blocks: - string += f' {block}\n' - return string.strip() - - @property - def bytes(self) -> bytes: - 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(IDListEntry): - magic = b'APPS' - header = b'\x08\x00\x03\x00\x00\x00\x00\x00\x00\x00' - - def __init__(self, bytes: Optional[bytes] = None) -> 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) -> str: - string = ':\n' - for block in self._blocks: - string += f' {block}\n' - return string.strip() - - @property - def bytes(self) -> bytes: - blocks_bytes = [block.bytes for block in self._blocks] - blocks_size = sum([len(block) + 4 for block in blocks_bytes]) + 4 # with terminator - 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: str, - target: str, - location: Optional[str] = None, - logo44x44: Optional[str] = None, - ) -> 'UwpSegmentEntry': - 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: - blocks.append(UwpSubBlock(type=0x0f, value=location)) # need for relative icon path - 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 diff --git a/plugin/libs/pylnk3/structures/link_info.py b/plugin/libs/pylnk3/structures/link_info.py deleted file mode 100644 index 32a172a..0000000 --- a/plugin/libs/pylnk3/structures/link_info.py +++ /dev/null @@ -1,163 +0,0 @@ -from io import BufferedIOBase -from typing import Optional - -from ..exceptions import MissingInformationException -from ..utils.read_write import read_cstring, read_int, write_byte, write_cstring, write_int - -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_UNKNOWN = "Unknown" - -_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()) - -_LINK_INFO_HEADER_DEFAULT = 0x1C -_LINK_INFO_HEADER_OPTIONAL = 0x24 - - -class LinkInfo: - - def __init__(self, lnk: Optional[BufferedIOBase] = None) -> 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: - print("TODO: read the unicode stuff") # TODO: read the unicode stuff - self._parse_path_elements(lnk) - else: - self.size = 0 - 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: Optional[str] = None - self.drive_serial: int = None # type: ignore[assignment] - self.volume_label: str = None # type: ignore[assignment] - self.local_base_path: str = None # type: ignore[assignment] - self.network_share_name: str = '' - self.base_name: str = '' - self._path: str = '' - - def _parse_path_elements(self, lnk: BufferedIOBase) -> None: - 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) -> None: - 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: BufferedIOBase) -> None: - 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) -> None: - self.size_base_name = 1 # len(self.base_name) + 1 # zero terminated strings - 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: BufferedIOBase) -> None: - 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: BufferedIOBase) -> None: - write_int(self.size_local_volume_table, buf) - if self.drive_type is None or self.drive_type not in _DRIVE_TYPE_IDS: - raise ValueError("This is not a valid drive type: %s" % self.drive_type) - drive_type = _DRIVE_TYPE_IDS[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) -> str: - return self._path - - def __str__(self) -> str: - s = "File Location Info:" - if not self._path: - 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 diff --git a/plugin/libs/pylnk3/structures/lnk.py b/plugin/libs/pylnk3/structures/lnk.py deleted file mode 100644 index 2bd1dd0..0000000 --- a/plugin/libs/pylnk3/structures/lnk.py +++ /dev/null @@ -1,381 +0,0 @@ -from datetime import datetime -from io import BufferedIOBase -from typing import Optional, Union - -from ..exceptions import FormatException, InvalidKeyException -from ..flags import Flags, ModifierKeys -from ..structures.extra_data import ExtraData, ExtraData_EnvironmentVariableDataBlock -from ..structures.id_list.id_list import LinkTargetIDList -from ..structures.link_info import DRIVE_UNKNOWN, LinkInfo -from ..utils.data import convert_time_to_unix, convert_time_to_windows -from ..utils.read_write import ( - read_byte, read_double, read_int, read_short, read_sized_string, write_byte, write_double, - write_int, write_short, write_sized_string, -) - -_SIGNATURE = b'L\x00\x00\x00' -_GUID = b'\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00F' - -_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', -) - -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()) - -_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()) - - -def assert_lnk_signature(f: BufferedIOBase) -> None: - 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.") - - -class Lnk: - - def __init__(self, f: Optional[Union[str, BufferedIOBase]] = None) -> None: - self.file: Optional[str] = None - if isinstance(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: Optional[str] = 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: Optional[ExtraData] = None - if f is not None: - assert_lnk_signature(f) - self._parse_lnk_file(f) - f.close() - - def _read_hot_key(self, lnk: BufferedIOBase) -> str: - 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: Optional[str], lnk: BufferedIOBase) -> None: - if hot_key is None or not hot_key: - low = high = 0 - else: - hot_key_parts = hot_key.split('+') - try: - low = _KEY_CODES[hot_key_parts[-1]] - except KeyError: - raise InvalidKeyException("Cannot find key code for %s" % hot_key_parts[1]) - modifiers = ModifierKeys() - for modifier in hot_key_parts[:-1]: - modifiers[modifier.upper()] = True - high = modifiers.bytes - write_byte(low, lnk) - write_byte(high, lnk) - - def _parse_lnk_file(self, lnk: BufferedIOBase) -> None: - # 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, BufferedIOBase]] = None, force_ext: bool = False) -> None: - f = f or self.file - is_opened_here = False - if isinstance(f, str): - filename: str = f - if force_ext and not filename.endswith('.lnk'): - filename += '.lnk' - f = open(filename, 'wb') - is_opened_here = True - if f is None: - raise ValueError("No file specified for saving LNK file") - self.write(f) - # only close the stream if it's our own - if is_opened_here: - f.close() - - def write(self, lnk: BufferedIOBase) -> None: - 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) -> LinkTargetIDList: - return self._shell_item_id_list - - def _set_shell_item_id_list(self, shell_item_id_list: LinkTargetIDList) -> None: - 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) -> LinkInfo: - return self._link_info - - def _set_link_info(self, link_info: LinkInfo) -> None: - 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) -> str: - return self._description - - def _set_description(self, description: str) -> None: - self._description = description - self.link_flags.HasName = description is not None - description = property(_get_description, _set_description) - - def _get_relative_path(self) -> str: - return self._relative_path - - def _set_relative_path(self, relative_path: str) -> None: - 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) -> str: - return self._work_dir - - def _set_work_dir(self, work_dir: str) -> None: - 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) -> str: - return self._arguments - - def _set_arguments(self, arguments: str) -> None: - self._arguments = arguments - self.link_flags.HasArguments = arguments is not None - arguments = property(_get_arguments, _set_arguments) - - def _get_icon(self) -> str: - return self._icon - - def _set_icon(self, icon: str) -> None: - self._icon = icon - self.link_flags.HasIconLocation = icon is not None - icon = property(_get_icon, _set_icon) - - def _get_window_mode(self) -> str: - return self._show_command - - def _set_window_mode(self, value: str) -> None: - 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) -> str: - # 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 str(id_list_path) - - def specify_local_location( - self, - path: str, - drive_type: Optional[str] = None, - drive_serial: Optional[int] = None, - volume_label: Optional[str] = None, - ) -> None: - self._link_info.drive_type = drive_type or DRIVE_UNKNOWN - self._link_info.drive_serial = drive_serial or 0 - 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: str, base_name: str) -> None: - 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) -> str: - 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 diff --git a/plugin/libs/pylnk3/utils/data.py b/plugin/libs/pylnk3/utils/data.py deleted file mode 100644 index 50ee4ca..0000000 --- a/plugin/libs/pylnk3/utils/data.py +++ /dev/null @@ -1,25 +0,0 @@ -import time -from datetime import datetime -from typing import Union - - -def convert_time_to_unix(windows_time: int) -> datetime: - # 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: Union[int, datetime]) -> int: - if isinstance(unix_time, datetime): - try: - unix_time_int = time.mktime(unix_time.timetuple()) - except OverflowError: - unix_time_int = time.mktime(datetime.now().timetuple()) - else: - unix_time_int = unix_time - return int((unix_time_int + 11644473600) * 10000000) diff --git a/plugin/libs/pylnk3/utils/guid.py b/plugin/libs/pylnk3/utils/guid.py deleted file mode 100644 index e7233a4..0000000 --- a/plugin/libs/pylnk3/utils/guid.py +++ /dev/null @@ -1,41 +0,0 @@ -from ..exceptions import FormatException - - -def guid_to_str(guid: bytes) -> str: - 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 - - -def guid_from_bytes(bytes: bytes) -> str: - if len(bytes) != 16: - raise FormatException(f"This is no valid _GUID: {bytes!s}") - 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: str) -> bytes: - 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]) diff --git a/plugin/libs/pylnk3/utils/padding.py b/plugin/libs/pylnk3/utils/padding.py deleted file mode 100644 index 1e54988..0000000 --- a/plugin/libs/pylnk3/utils/padding.py +++ /dev/null @@ -1,2 +0,0 @@ -def padding(val: bytes, size: int, byte: bytes = b'\x00') -> bytes: - return val + (size - len(val)) * byte diff --git a/plugin/libs/pylnk3/utils/read_write.py b/plugin/libs/pylnk3/utils/read_write.py deleted file mode 100644 index 356d24c..0000000 --- a/plugin/libs/pylnk3/utils/read_write.py +++ /dev/null @@ -1,128 +0,0 @@ -from datetime import datetime -from io import BufferedIOBase -from struct import pack, unpack -from typing import Union - -DEFAULT_CHARSET = 'cp1251' - - -def read_byte(buf: BufferedIOBase) -> int: - return unpack(' int: - return unpack(' int: - return unpack(' int: - return unpack(' str: - 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: BufferedIOBase, padding: bool = False) -> str: - 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: BufferedIOBase, string: bool = True) -> Union[str, bytes]: - size = read_short(buf) - if string: - return buf.read(size * 2).decode('utf-16-le') - else: - return buf.read(size) - - -def get_bits(value: int, start: int, count: int, length: int = 16) -> int: - mask = 0 - for i in range(count): - mask = mask | 1 << i - shift = length - start - count - return value >> shift & mask - - -def read_dos_datetime(buf: BufferedIOBase) -> datetime: - 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: int, buf: BufferedIOBase) -> None: - buf.write(pack(' None: - buf.write(pack(' None: - buf.write(pack(' None: - buf.write(pack(' None: - # val = val.encode('unicode-escape').replace('\\\\', '\\') - val_bytes = val.encode(DEFAULT_CHARSET) - buf.write(val_bytes + b'\x00') - if padding and not len(val_bytes) % 2: - buf.write(b'\x00') - - -def write_cunicode(val: str, buf: BufferedIOBase) -> None: - uni = val.encode('utf-16-le') - buf.write(uni + b'\x00\x00') - - -def write_sized_string(val: str, buf: BufferedIOBase, string: bool = True) -> None: - 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: int, target: int, start: int, count: int, length: int = 16) -> int: - return target | bits << (length - start - count) - - -def write_dos_datetime(val: datetime, buf: BufferedIOBase) -> None: - 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) diff --git a/plugin/listener.py b/plugin/listener.py index e278ef0..3e92c67 100644 --- a/plugin/listener.py +++ b/plugin/listener.py @@ -2,13 +2,14 @@ import os from collections.abc import Generator +from pathlib import Path import sublime import sublime_plugin -from .libs.pylnk3 import Lnk -from .libs.pylnk3.exceptions import FormatException -from .libs.pylnk3.structures.extra_data import ExtraData_PropertyStoreDataBlock, PropertyStore +from .libs.LnkParse3.extra.metadata import Metadata, SerializedPropertyStorage, SerializedPropertyValueIntegerName +from .libs.LnkParse3.lnk_file import LnkFile +from .utils import first_true PACKAGE_NAME = __package__.partition(".")[0] @@ -45,35 +46,51 @@ def on_load(self) -> None: @classmethod def _resolve_lnk(cls, path: str) -> str | None: - # note that ".lnk" can't be a shortcut of ".lnk" - if lnk := cls._make_lnk(path): - return cls._extract_lnk_target(lnk) - return None + if not (lnk_file := cls._make_lnk_file(path)): + return None - @classmethod - def _extract_extradata_propertystore(cls, lnk: Lnk) -> Generator[PropertyStore, None, None]: - if not lnk.extra_data: - return + # print(f"[DEBUG] {lnk_file.get_json() = }") - for block in lnk.extra_data.blocks: - if isinstance(block, ExtraData_PropertyStoreDataBlock): - yield from block.stores + try: + return first_true(cls._list_lnk_targets(lnk_file, lnk_path=path)) + except Exception: + return None @classmethod - def _extract_lnk_target(cls, lnk: Lnk) -> str: - for store in cls._extract_extradata_propertystore(lnk): - # print(f"[DEBUG] {str(store) = }") - for name, value in store.properties: - if name == 30: # System.Link.TargetDOSName (?) - return str(value).partition(": ")[2].replace("\x00", "") # this path is UTF-8 - - return lnk.path # this path may in a wrong encoding + def _list_lnk_targets(cls, lnk_file: LnkFile, *, lnk_path: str | None = None) -> Generator[str, None, None]: + string_data_dict = lnk_file.string_data.as_dict() + + # from property: relative path + if lnk_path and lnk_file.has_relative_path(): + path = Path(lnk_path).parent / string_data_dict["relative_path"] + yield str(path.resolve()) + + # from metadata + for extra in lnk_file.extras: + if isinstance(extra, Metadata): + for store in extra.property_store(): + assert isinstance(store, SerializedPropertyStorage) + if store.format_id() == "28636AA6-953D-11D2-B5D6-00C04FD918D0": + for prop in store.serialized_property_values(): + if isinstance(prop, SerializedPropertyValueIntegerName) and prop.id() == 30: + yield prop.value().value() # type: ignore + + # from info + if lnk_file.info: + try: + if target := lnk_file.info.local_base_path_unicode(): + yield target + except Exception: + pass + try: + if target := lnk_file.info.local_base_path(): + yield target + except Exception: + pass @staticmethod - def _make_lnk(path: str) -> Lnk | None: + def _make_lnk_file(path: str) -> LnkFile | None: if path.lower().endswith(".lnk"): - try: - return Lnk(path) - except FormatException: - return None + with open(path, "rb") as indata: + return LnkFile(indata) return None diff --git a/plugin/utils.py b/plugin/utils.py new file mode 100644 index 0000000..b14205c --- /dev/null +++ b/plugin/utils.py @@ -0,0 +1,36 @@ +# This file is more self-sustained and shouldn't use things from other higher-level modules. +from __future__ import annotations + +from collections.abc import Iterable +from typing import Callable, TypeVar, overload + +_T = TypeVar("_T") +_U = TypeVar("_U") + + +@overload +def first_true( + items: Iterable[_T], + default: _U, + pred: Callable[[_T], bool] | None = None, +) -> _T | _U: ... + + +@overload +def first_true( + items: Iterable[_T], + *, + pred: Callable[[_T], bool] | None = None, +) -> _T | None: ... + + +def first_true( + items: Iterable[_T], + default: _U | None = None, + pred: Callable[[_T], bool] | None = None, +) -> _T | _U | None: + """ + Gets the first item which satisfies the `pred`. Otherwise, `default`. + If `pred` is not given or `None`, the first truthy item will be returned. + """ + return next(filter(pred, items), default)