SickGear/lib/hachoir/parser/misc/lnk.py

637 lines
24 KiB
Python

"""
Windows Shortcut (.lnk) parser.
Documents:
- The Windows Shortcut File Format (document version 1.0)
Reverse-engineered by Jesse Hager
http://www.i2s-lab.com/Papers/The_Windows_Shortcut_File_Format.pdf
- Wine source code:
http://source.winehq.org/source/include/shlobj.h (SHELL_LINK_DATA_FLAGS enum)
http://source.winehq.org/source/dlls/shell32/pidl.h
- Microsoft:
http://msdn2.microsoft.com/en-us/library/ms538128.aspx
Author: Robert Xiao, Victor Stinner
Changes:
2007-06-27 - Robert Xiao
* Fixes to FileLocationInfo to correctly handle Unicode paths
2007-06-13 - Robert Xiao
* ItemID, FileLocationInfo and ExtraInfo structs, correct Unicode string handling
2007-03-15 - Victor Stinner
* Creation of the parser
"""
from hachoir.parser import Parser
from hachoir.field import (FieldSet,
CString, String,
UInt32, UInt16, UInt8,
Bit, Bits, PaddingBits,
TimestampWin64, DateTimeMSDOS32,
NullBytes, PaddingBytes, RawBytes, Enum)
from hachoir.core.endian import LITTLE_ENDIAN
from hachoir.core.text_handler import textHandler, hexadecimal
from hachoir.parser.common.win32 import GUID
from hachoir.parser.common.msdos import MSDOSFileAttr16, MSDOSFileAttr32
from hachoir.core.text_handler import filesizeHandler
from hachoir.core.tools import paddingSize
class ItemIdList(FieldSet):
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._size = (self["size"].value + 2) * 8
def createFields(self):
yield UInt16(self, "size", "Size of item ID list")
while True:
item = ItemId(self, "itemid[]")
yield item
if not item["length"].value:
break
class ItemId(FieldSet):
ITEM_TYPE = {
0x1F: "GUID",
0x23: "Drive",
0x25: "Drive",
0x29: "Drive",
0x2E: "Shell Extension",
0x2F: "Drive",
0x30: "Dir/File",
0x31: "Directory",
0x32: "File",
0x34: "File [Unicode Name]",
0x41: "Workgroup",
0x42: "Computer",
0x46: "Net Provider",
0x47: "Whole Network",
0x4C: "Web Folder",
0x61: "MSITStore",
0x70: "Printer/RAS Connection",
0xB1: "History/Favorite",
0xC3: "Network Share",
}
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
if self["length"].value:
self._size = self["length"].value * 8
else:
self._size = 16
def createFields(self):
yield UInt16(self, "length", "Length of Item ID Entry")
if not self["length"].value:
return
yield Enum(UInt8(self, "type"), self.ITEM_TYPE)
entrytype = self["type"].value
if entrytype in (0x1F, 0x70):
# GUID
yield RawBytes(self, "dummy", 1, "should be 0x50")
yield GUID(self, "guid")
elif entrytype == 0x2E:
# Shell extension
yield RawBytes(self, "dummy", 1, "should be 0x50")
if self["dummy"].value == '\0':
yield UInt16(self, "length_data", "Length of shell extension-specific data")
if self["length_data"].value:
yield RawBytes(self, "data", self["length_data"].value, "Shell extension-specific data")
yield GUID(self, "handler_guid")
yield GUID(self, "guid")
elif entrytype in (0x23, 0x25, 0x29, 0x2F):
# Drive
yield String(self, "drive", self["length"].value - 3, strip="\0")
elif entrytype in (0x30, 0x31, 0x32, 0x61, 0xb1):
yield RawBytes(self, "dummy", 1, "should be 0x00")
yield UInt32(self, "size", "size of file; 0 for folders")
yield DateTimeMSDOS32(self, "date_time", "File/folder date and time")
yield MSDOSFileAttr16(self, "attribs", "File/folder attributes")
yield CString(self, "name", "File/folder name")
if self.root.hasUnicodeNames():
# Align to 2-bytes
n = paddingSize(self.current_size // 8, 2)
if n:
yield PaddingBytes(self, "pad", n)
yield UInt16(self, "length_w", "Length of wide struct member")
yield RawBytes(self, "unknown[]", 6)
yield DateTimeMSDOS32(self, "creation_date_time", "File/folder creation date and time")
yield DateTimeMSDOS32(self, "access_date_time", "File/folder last access date and time")
yield RawBytes(self, "unknown[]", 2)
yield UInt16(self, "length_next", "Length of next two strings (if zero, ignore this field)")
yield CString(self, "unicode_name", "File/folder name", charset="UTF-16-LE")
if self["length_next"].value:
yield CString(self, "localized_name", "Localized name")
yield RawBytes(self, "unknown[]", 2)
else:
yield CString(self, "name_short", "File/folder short name")
elif entrytype in (0x41, 0x42, 0x46):
yield RawBytes(self, "unknown[]", 2)
yield CString(self, "name")
yield CString(self, "protocol")
yield RawBytes(self, "unknown[]", 2)
elif entrytype == 0x47:
# Whole Network
yield RawBytes(self, "unknown[]", 2)
yield CString(self, "name")
elif entrytype == 0xC3:
# Network Share
yield RawBytes(self, "unknown[]", 2)
yield CString(self, "name")
yield CString(self, "protocol")
yield CString(self, "description")
yield RawBytes(self, "unknown[]", 2)
elif entrytype == 0x4C:
# Web Folder
yield RawBytes(self, "unknown[]", 5)
yield TimestampWin64(self, "modification_time")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield LnkString(self, "name")
yield RawBytes(self, "padding[]", 2)
yield LnkString(self, "address")
if self["address/length"].value:
yield RawBytes(self, "padding[]", 2)
else:
yield RawBytes(self, "raw", self["length"].value - 3)
def createDescription(self):
if self["length"].value:
return "Item ID Entry: " + self.ITEM_TYPE.get(self["type"].value, "Unknown")
else:
return "End of Item ID List"
def formatVolumeSerial(field):
val = field.value
return '%04X-%04X' % (val >> 16, val & 0xFFFF)
class LocalVolumeTable(FieldSet):
VOLUME_TYPE = {
1: "No root directory",
2: "Removable (Floppy, Zip, etc.)",
3: "Fixed (Hard disk)",
4: "Remote (Network drive)",
5: "CD-ROM",
6: "Ram drive",
}
def createFields(self):
yield UInt32(self, "length", "Length of this structure")
yield Enum(UInt32(self, "volume_type", "Volume Type"), self.VOLUME_TYPE)
yield textHandler(UInt32(self, "volume_serial", "Volume Serial Number"), formatVolumeSerial)
yield UInt32(self, "label_offset", "Offset to volume label")
padding = self.seekByte(self["label_offset"].value)
if padding:
yield padding
yield CString(self, "drive")
def hasValue(self):
return bool(self["drive"].value)
def createValue(self):
return self["drive"].value
class NetworkVolumeTable(FieldSet):
def createFields(self):
yield UInt32(self, "length", "Length of this structure")
yield UInt32(self, "unknown[]")
yield UInt32(self, "share_name_offset", "Offset to share name")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
padding = self.seekByte(self["share_name_offset"].value)
if padding:
yield padding
yield CString(self, "share_name")
def createValue(self):
return self["share_name"].value
class FileLocationInfo(FieldSet):
def createFields(self):
yield UInt32(self, "length", "Length of this structure")
if not self["length"].value:
return
yield UInt32(self, "first_offset_pos", "Position of first offset")
has_unicode_paths = (self["first_offset_pos"].value == 0x24)
yield Bit(self, "on_local_volume")
yield Bit(self, "on_network_volume")
yield PaddingBits(self, "reserved[]", 30)
yield UInt32(self, "local_info_offset", "Offset to local volume table; only meaningful if on_local_volume = 1")
yield UInt32(self, "local_pathname_offset", "Offset to local base pathname; only meaningful if on_local_volume = 1")
yield UInt32(self, "remote_info_offset", "Offset to network volume table; only meaningful if on_network_volume = 1")
yield UInt32(self, "pathname_offset", "Offset of remaining pathname")
if has_unicode_paths:
yield UInt32(self, "local_pathname_unicode_offset", "Offset to Unicode version of local base pathname; only meaningful if on_local_volume = 1")
yield UInt32(self, "pathname_unicode_offset", "Offset to Unicode version of remaining pathname")
if self["on_local_volume"].value:
padding = self.seekByte(self["local_info_offset"].value)
if padding:
yield padding
yield LocalVolumeTable(self, "local_volume_table", "Local Volume Table")
padding = self.seekByte(self["local_pathname_offset"].value)
if padding:
yield padding
yield CString(self, "local_base_pathname", "Local Base Pathname")
if has_unicode_paths:
padding = self.seekByte(
self["local_pathname_unicode_offset"].value)
if padding:
yield padding
yield CString(self, "local_base_pathname_unicode", "Local Base Pathname in Unicode", charset="UTF-16-LE")
if self["on_network_volume"].value:
padding = self.seekByte(self["remote_info_offset"].value)
if padding:
yield padding
yield NetworkVolumeTable(self, "network_volume_table")
padding = self.seekByte(self["pathname_offset"].value)
if padding:
yield padding
yield CString(self, "final_pathname", "Final component of the pathname")
if has_unicode_paths:
padding = self.seekByte(self["pathname_unicode_offset"].value)
if padding:
yield padding
yield CString(self, "final_pathname_unicode", "Final component of the pathname in Unicode", charset="UTF-16-LE")
padding = self.seekByte(self["length"].value)
if padding:
yield padding
class LnkString(FieldSet):
def createFields(self):
yield UInt16(self, "length", "Length of this string")
if self["length"].value:
if self.root.hasUnicodeNames():
yield String(self, "data", self["length"].value * 2, charset="UTF-16-LE")
else:
yield String(self, "data", self["length"].value, charset="ASCII")
def createValue(self):
if self["length"].value:
return self["data"].value
else:
return ""
class ColorRef(FieldSet):
''' COLORREF struct, 0x00bbggrr '''
static_size = 32
def createFields(self):
yield UInt8(self, "red", "Red")
yield UInt8(self, "green", "Green")
yield UInt8(self, "blue", "Blue")
yield PaddingBytes(self, "pad", 1, "Padding (must be 0)")
def createDescription(self):
rgb = self["red"].value, self["green"].value, self["blue"].value
return "RGB Color: #%02X%02X%02X" % rgb
class ColorTableIndex(Bits):
def __init__(self, parent, name, size, description=None):
Bits.__init__(self, parent, name, size, None)
self.desc = description
def createDescription(self):
assert hasattr(self, 'parent') and hasattr(self, 'value')
return "%s: %s" % (self.desc,
self.parent["color[%i]" % self.value].description)
class ExtraInfo(FieldSet):
INFO_TYPE = {
0xA0000001: "Link Target Information", # EXP_SZ_LINK_SIG
0xA0000002: "Console Window Properties", # NT_CONSOLE_PROPS_SIG
0xA0000003: "Hostname and Other Stuff",
0xA0000004: "Console Codepage Information", # NT_FE_CONSOLE_PROPS_SIG
0xA0000005: "Special Folder Info", # EXP_SPECIAL_FOLDER_SIG
# EXP_DARWIN_ID_SIG
0xA0000006: "DarwinID (Windows Installer ID) Information",
0xA0000007: "Custom Icon Details", # EXP_LOGO3_ID_SIG or EXP_SZ_ICON_SIG
}
SPECIAL_FOLDER = {
0: "DESKTOP",
1: "INTERNET",
2: "PROGRAMS",
3: "CONTROLS",
4: "PRINTERS",
5: "PERSONAL",
6: "FAVORITES",
7: "STARTUP",
8: "RECENT",
9: "SENDTO",
10: "BITBUCKET",
11: "STARTMENU",
16: "DESKTOPDIRECTORY",
17: "DRIVES",
18: "NETWORK",
19: "NETHOOD",
20: "FONTS",
21: "TEMPLATES",
22: "COMMON_STARTMENU",
23: "COMMON_PROGRAMS",
24: "COMMON_STARTUP",
25: "COMMON_DESKTOPDIRECTORY",
26: "APPDATA",
27: "PRINTHOOD",
28: "LOCAL_APPDATA",
29: "ALTSTARTUP",
30: "COMMON_ALTSTARTUP",
31: "COMMON_FAVORITES",
32: "INTERNET_CACHE",
33: "COOKIES",
34: "HISTORY",
35: "COMMON_APPDATA",
36: "WINDOWS",
37: "SYSTEM",
38: "PROGRAM_FILES",
39: "MYPICTURES",
40: "PROFILE",
41: "SYSTEMX86",
42: "PROGRAM_FILESX86",
43: "PROGRAM_FILES_COMMON",
44: "PROGRAM_FILES_COMMONX86",
45: "COMMON_TEMPLATES",
46: "COMMON_DOCUMENTS",
47: "COMMON_ADMINTOOLS",
48: "ADMINTOOLS",
49: "CONNECTIONS",
53: "COMMON_MUSIC",
54: "COMMON_PICTURES",
55: "COMMON_VIDEO",
56: "RESOURCES",
57: "RESOURCES_LOCALIZED",
58: "COMMON_OEM_LINKS",
59: "CDBURN_AREA",
61: "COMPUTERSNEARME",
}
BOOL_ENUM = {
0: "False",
1: "True",
}
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
if self["length"].value:
self._size = self["length"].value * 8
else:
self._size = 32
def createFields(self):
yield UInt32(self, "length", "Length of this structure")
if not self["length"].value:
return
yield Enum(textHandler(UInt32(self, "signature", "Signature determining the function of this structure"), hexadecimal), self.INFO_TYPE)
if self["signature"].value == 0xA0000003:
# Hostname and Other Stuff
yield UInt32(self, "remaining_length")
yield UInt32(self, "unknown[]")
yield String(self, "hostname", 16, "Computer hostname on which shortcut was last modified", strip="\0")
yield RawBytes(self, "unknown[]", 32)
yield RawBytes(self, "unknown[]", 32)
elif self["signature"].value == 0xA0000005:
# Special Folder Info
yield Enum(UInt32(self, "special_folder_id", "ID of the special folder"), self.SPECIAL_FOLDER)
yield UInt32(self, "offset", "Offset to Item ID entry")
elif self["signature"].value in (0xA0000001, 0xA0000006, 0xA0000007):
if self["signature"].value == 0xA0000001: # Link Target Information
object_name = "target"
# DarwinID (Windows Installer ID) Information
elif self["signature"].value == 0xA0000006:
object_name = "darwinID"
else: # Custom Icon Details
object_name = "icon_path"
yield CString(self, object_name, "Data (ASCII format)", charset="ASCII")
# 260*2 = size of next part
remaining = self["length"].value - self.current_size // 8 - 260 * 2
if remaining:
yield RawBytes(self, "slack_space[]", remaining, "Data beyond end of string")
yield CString(self, object_name + '_unicode', "Data (Unicode format)", charset="UTF-16-LE", truncate="\0")
remaining = self["length"].value - self.current_size // 8
if remaining:
yield RawBytes(self, "slack_space[]", remaining, "Data beyond end of string")
elif self["signature"].value == 0xA0000002:
# Console Window Properties
yield ColorTableIndex(self, "color_text", 4, "Screen text color index")
yield ColorTableIndex(self, "color_bg", 4, "Screen background color index")
yield NullBytes(self, "reserved[]", 1)
yield ColorTableIndex(self, "color_popup_text", 4, "Pop-up text color index")
yield ColorTableIndex(self, "color_popup_bg", 4, "Pop-up background color index")
yield NullBytes(self, "reserved[]", 1)
yield UInt16(self, "buffer_width", "Screen buffer width (character cells)")
yield UInt16(self, "buffer_height", "Screen buffer height (character cells)")
yield UInt16(self, "window_width", "Window width (character cells)")
yield UInt16(self, "window_height", "Window height (character cells)")
yield UInt16(self, "position_left", "Window distance from left edge (screen coords)")
yield UInt16(self, "position_top", "Window distance from top edge (screen coords)")
yield UInt32(self, "font_number")
yield UInt32(self, "input_buffer_size")
yield UInt16(self, "font_width", "Font width in pixels; 0 for a non-raster font")
yield UInt16(self, "font_height", "Font height in pixels; equal to the font size for non-raster fonts")
yield UInt32(self, "font_family")
yield UInt32(self, "font_weight")
yield String(self, "font_name_unicode", 64, "Font Name (Unicode format)", charset="UTF-16-LE", truncate="\0")
yield UInt32(self, "cursor_size", "Relative size of cursor (% of character size)")
yield Enum(UInt32(self, "full_screen", "Run console in full screen?"), self.BOOL_ENUM)
yield Enum(UInt32(self, "quick_edit", "Console uses quick-edit feature (using mouse to cut & paste)?"), self.BOOL_ENUM)
yield Enum(UInt32(self, "insert_mode", "Console uses insertion mode?"), self.BOOL_ENUM)
yield Enum(UInt32(self, "auto_position", "System automatically positions window?"), self.BOOL_ENUM)
yield UInt32(self, "history_size", "Size of the history buffer (in lines)")
yield UInt32(self, "history_count", "Number of history buffers (each process gets one up to this limit)")
yield Enum(UInt32(self, "history_no_dup", "Automatically eliminate duplicate lines in the history buffer?"), self.BOOL_ENUM)
for index in range(16):
yield ColorRef(self, "color[]")
elif self["signature"].value == 0xA0000004:
# Console Codepage Information
yield UInt32(self, "codepage", "Console's code page")
else:
yield RawBytes(self, "raw", self["length"].value - self.current_size // 8)
def createDescription(self):
if self["length"].value:
return "Extra Info Entry: " + self["signature"].display
else:
return "End of Extra Info"
HOT_KEYS = {
0x00: 'None',
0x13: 'Pause',
0x14: 'Caps Lock',
0x21: 'Page Up',
0x22: 'Page Down',
0x23: 'End',
0x24: 'Home',
0x25: 'Left',
0x26: 'Up',
0x27: 'Right',
0x28: 'Down',
0x2d: 'Insert',
0x2e: 'Delete',
0x6a: 'Num *',
0x6b: 'Num +',
0x6d: 'Num -',
0x6e: 'Num .',
0x6f: 'Num /',
0x90: 'Num Lock',
0x91: 'Scroll Lock',
0xba: ';',
0xbb: '=',
0xbc: ',',
0xbd: '-',
0xbe: '.',
0xbf: '/',
0xc0: '`',
0xdb: '[',
0xdc: '\\',
0xdd: ']',
0xde: "'",
}
def text_hot_key(field):
assert hasattr(field, "value")
val = field.value
if 0x30 <= val <= 0x39:
return chr(val)
elif 0x41 <= val <= 0x5A:
return chr(val)
elif 0x60 <= val <= 0x69:
return 'Numpad %c' % chr(val - 0x30)
elif 0x70 <= val <= 0x87:
return 'F%i' % (val - 0x6F)
elif val in HOT_KEYS:
return HOT_KEYS[val]
return str(val)
class LnkFile(Parser):
MAGIC = b"\x4C\0\0\0\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46"
PARSER_TAGS = {
"id": "lnk",
"category": "misc",
"file_ext": ("lnk",),
"mime": ("application/x-ms-shortcut",),
"magic": ((MAGIC, 0),),
"min_size": len(MAGIC) * 8, # signature + guid = 20 bytes
"description": "Windows Shortcut (.lnk)",
}
endian = LITTLE_ENDIAN
SHOW_WINDOW_STATE = {
0: "Hide",
1: "Show Normal",
2: "Show Minimized",
3: "Show Maximized",
4: "Show Normal, not activated",
5: "Show",
6: "Minimize",
7: "Show Minimized, not activated",
8: "Show, not activated",
9: "Restore",
10: "Show Default",
}
def validate(self):
if self["signature"].value != 0x0000004C:
return "Invalid signature"
if self["guid"].value != "00021401-0000-0000-C000-000000000046":
return "Invalid GUID"
return True
def hasUnicodeNames(self):
return self["has_unicode_names"].value
def createFields(self):
yield UInt32(self, "signature", "Shortcut signature (0x0000004C)")
yield GUID(self, "guid", "Shortcut GUID (00021401-0000-0000-C000-000000000046)")
yield Bit(self, "has_shell_id", "Is the Item ID List present?")
yield Bit(self, "target_is_file", "Is a file or a directory?")
yield Bit(self, "has_description", "Is the Description field present?")
yield Bit(self, "has_rel_path", "Is the relative path to the target available?")
yield Bit(self, "has_working_dir", "Is there a working directory?")
yield Bit(self, "has_cmd_line_args", "Are there any command line arguments?")
yield Bit(self, "has_custom_icon", "Is there a custom icon?")
yield Bit(self, "has_unicode_names", "Are Unicode names used?")
yield Bit(self, "force_no_linkinfo")
yield Bit(self, "has_exp_sz")
yield Bit(self, "run_in_separate")
yield Bit(self, "has_logo3id", "Is LOGO3 ID info present?")
yield Bit(self, "has_darwinid", "Is the DarwinID info present?")
yield Bit(self, "runas_user", "Is the target run as another user?")
yield Bit(self, "has_exp_icon_sz", "Is custom icon information available?")
yield Bit(self, "no_pidl_alias")
yield Bit(self, "force_unc_name")
yield Bit(self, "run_with_shim_layer")
yield PaddingBits(self, "reserved[]", 14, "Flag bits reserved for future use")
yield MSDOSFileAttr32(self, "target_attr")
yield TimestampWin64(self, "creation_time")
yield TimestampWin64(self, "modification_time")
yield TimestampWin64(self, "last_access_time")
yield filesizeHandler(UInt32(self, "target_filesize"))
yield UInt32(self, "icon_number")
yield Enum(UInt32(self, "show_window"), self.SHOW_WINDOW_STATE)
yield textHandler(UInt8(self, "hot_key", "Hot key used for quick access"), text_hot_key)
yield Bit(self, "hot_key_shift", "Hot key: is Shift used?")
yield Bit(self, "hot_key_ctrl", "Hot key: is Ctrl used?")
yield Bit(self, "hot_key_alt", "Hot key: is Alt used?")
yield PaddingBits(self, "hot_key_reserved", 21, "Hot key: (reserved)")
yield NullBytes(self, "reserved[]", 8)
if self["has_shell_id"].value:
yield ItemIdList(self, "item_idlist", "Item ID List")
if self["target_is_file"].value:
yield FileLocationInfo(self, "file_location_info", "File Location Info")
if self["has_description"].value:
yield LnkString(self, "description")
if self["has_rel_path"].value:
yield LnkString(self, "relative_path", "Relative path to target")
if self["has_working_dir"].value:
yield LnkString(self, "working_dir", "Working directory (dir to start target in)")
if self["has_cmd_line_args"].value:
yield LnkString(self, "cmd_line_args", "Command Line Arguments")
if self["has_custom_icon"].value:
yield LnkString(self, "custom_icon", "Custom Icon Path")
while not self.eof:
yield ExtraInfo(self, "extra_info[]")