"""
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[]")
