diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..1f931eb
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,119 @@
+Metadata-Version: 2.1
+Name: pylnk3
+Version: 0.4.2
+Summary: Windows LNK File Parser and Creator
+Home-page: https://github.com/strayge/pylnk
+Author: strayge
+Author-email: strayge@gmail.com
+License: GNU Library or Lesser General Public License (LGPL)
+Description: # PyLnk 3
+        
+        [![PyPI version shields.io](https://img.shields.io/pypi/v/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+        [![PyPI pyversions](https://img.shields.io/pypi/pyversions/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+        [![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+        
+        Python library for reading and writing Windows shortcut files (.lnk).  
+        Converted to support python 3.
+        
+        This library can parse .lnk files and extract all relevant information from
+        them. Parsing a .lnk file yields a LNK object which can be altered and saved
+        again. Moreover, .lnk file can be created from scratch be creating a LNK
+        object, populating it with data and then saving it to a file. As that
+        process requires some knowledge about the internals of .lnk files, some
+        convenience functions are provided.
+        
+        Limitation: Windows knows lots of different types of shortcuts which all have
+        different formats. This library currently only supports shortcuts to files and
+        folders on the local machine. 
+        
+        ## CLI
+        
+        Mainly tool has two basic commands.
+        
+        #### Parse existed lnk file
+        
+        ```sh
+        pylnk3 parse [-h] filename [props [props ...]]
+        
+        positional arguments:
+          filename    lnk filename to read
+          props       props path to read
+        
+        optional arguments:
+          -h, --help  show this help message and exit
+        ```
+        
+        #### Create new lnk file
+        
+        ```sh
+        usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]]
+                             [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]]
+                             target name
+        
+        positional arguments:
+          target                target path
+          name                  lnk filename to create
+        
+        optional arguments:
+          -h, --help            show this help message and exit
+          --arguments [ARGUMENTS], -a [ARGUMENTS]
+                                additional arguments
+          --description [DESCRIPTION], -d [DESCRIPTION]
+                                description
+          --icon [ICON], -i [ICON]
+                                icon filename
+          --icon-index [ICON_INDEX], -ii [ICON_INDEX]
+                                icon index
+          --workdir [WORKDIR], -w [WORKDIR]
+                                working directory
+          --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}]
+                                window mode
+        ```
+        
+        #### Examples
+        ```sh
+        pylnk3 p filename.lnk
+        pylnk3 c c:\prog.exe shortcut.lnk
+        pylnk3 c \\192.168.1.1\share\file.doc doc.lnk
+        pylnk3 create c:\1.txt text.lnk -m Minimized -d "Description"
+        ```
+        
+        ## Changes
+        
+        **0.4.2**  
+        changed logic for Lnk.path choose (in case of different paths presents at different structures)  
+        read links with root as GUID of KNOWN_FOLDER  
+        [FIX] disabled padding for writing LinkInfo.local_base_path  
+        
+        **0.4.0**  
+        added support for network links  
+        reworked CLI (added more options for creating links)  
+        added entry point for call tool just like `pylnk3`  
+        [FIX] allow build links for non-existed (from this machine) paths  
+        [FIX] correct building links on Linux (now expect Windows-like path)  
+        [FIX] fixed path priority at parsing with both local & remote presents  
+        
+        
+        **0.3.0**  
+        added support links to UWP apps  
+        
+        
+        **0.2.1**  
+        released to PyPI
+        
+          
+        **0.2.0**  
+        converted to python 3  
+        
+Keywords: lnk,shortcut,windows
+Platform: UNKNOWN
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires-Python: >=3.6
+Description-Content-Type: text/markdown
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..57ab4c1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,98 @@
+# PyLnk 3
+
+[![PyPI version shields.io](https://img.shields.io/pypi/v/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+[![PyPI pyversions](https://img.shields.io/pypi/pyversions/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+[![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+
+Python library for reading and writing Windows shortcut files (.lnk).  
+Converted to support python 3.
+
+This library can parse .lnk files and extract all relevant information from
+them. Parsing a .lnk file yields a LNK object which can be altered and saved
+again. Moreover, .lnk file can be created from scratch be creating a LNK
+object, populating it with data and then saving it to a file. As that
+process requires some knowledge about the internals of .lnk files, some
+convenience functions are provided.
+
+Limitation: Windows knows lots of different types of shortcuts which all have
+different formats. This library currently only supports shortcuts to files and
+folders on the local machine. 
+
+## CLI
+
+Mainly tool has two basic commands.
+
+#### Parse existed lnk file
+
+```sh
+pylnk3 parse [-h] filename [props [props ...]]
+
+positional arguments:
+  filename    lnk filename to read
+  props       props path to read
+
+optional arguments:
+  -h, --help  show this help message and exit
+```
+
+#### Create new lnk file
+
+```sh
+usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]]
+                     [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]]
+                     target name
+
+positional arguments:
+  target                target path
+  name                  lnk filename to create
+
+optional arguments:
+  -h, --help            show this help message and exit
+  --arguments [ARGUMENTS], -a [ARGUMENTS]
+                        additional arguments
+  --description [DESCRIPTION], -d [DESCRIPTION]
+                        description
+  --icon [ICON], -i [ICON]
+                        icon filename
+  --icon-index [ICON_INDEX], -ii [ICON_INDEX]
+                        icon index
+  --workdir [WORKDIR], -w [WORKDIR]
+                        working directory
+  --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}]
+                        window mode
+```
+
+#### Examples
+```sh
+pylnk3 p filename.lnk
+pylnk3 c c:\prog.exe shortcut.lnk
+pylnk3 c \\192.168.1.1\share\file.doc doc.lnk
+pylnk3 create c:\1.txt text.lnk -m Minimized -d "Description"
+```
+
+## Changes
+
+**0.4.2**  
+changed logic for Lnk.path choose (in case of different paths presents at different structures)  
+read links with root as GUID of KNOWN_FOLDER  
+[FIX] disabled padding for writing LinkInfo.local_base_path  
+
+**0.4.0**  
+added support for network links  
+reworked CLI (added more options for creating links)  
+added entry point for call tool just like `pylnk3`  
+[FIX] allow build links for non-existed (from this machine) paths  
+[FIX] correct building links on Linux (now expect Windows-like path)  
+[FIX] fixed path priority at parsing with both local & remote presents  
+
+
+**0.3.0**  
+added support links to UWP apps  
+
+
+**0.2.1**  
+released to PyPI
+
+  
+**0.2.0**  
+converted to python 3  
diff --git a/pylnk3.egg-info/PKG-INFO b/pylnk3.egg-info/PKG-INFO
new file mode 100644
index 0000000..1f931eb
--- /dev/null
+++ b/pylnk3.egg-info/PKG-INFO
@@ -0,0 +1,119 @@
+Metadata-Version: 2.1
+Name: pylnk3
+Version: 0.4.2
+Summary: Windows LNK File Parser and Creator
+Home-page: https://github.com/strayge/pylnk
+Author: strayge
+Author-email: strayge@gmail.com
+License: GNU Library or Lesser General Public License (LGPL)
+Description: # PyLnk 3
+        
+        [![PyPI version shields.io](https://img.shields.io/pypi/v/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+        [![PyPI pyversions](https://img.shields.io/pypi/pyversions/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+        [![PyPI download month](https://img.shields.io/pypi/dm/pylnk3.svg)](https://pypi.python.org/pypi/pylnk3/)
+        
+        Python library for reading and writing Windows shortcut files (.lnk).  
+        Converted to support python 3.
+        
+        This library can parse .lnk files and extract all relevant information from
+        them. Parsing a .lnk file yields a LNK object which can be altered and saved
+        again. Moreover, .lnk file can be created from scratch be creating a LNK
+        object, populating it with data and then saving it to a file. As that
+        process requires some knowledge about the internals of .lnk files, some
+        convenience functions are provided.
+        
+        Limitation: Windows knows lots of different types of shortcuts which all have
+        different formats. This library currently only supports shortcuts to files and
+        folders on the local machine. 
+        
+        ## CLI
+        
+        Mainly tool has two basic commands.
+        
+        #### Parse existed lnk file
+        
+        ```sh
+        pylnk3 parse [-h] filename [props [props ...]]
+        
+        positional arguments:
+          filename    lnk filename to read
+          props       props path to read
+        
+        optional arguments:
+          -h, --help  show this help message and exit
+        ```
+        
+        #### Create new lnk file
+        
+        ```sh
+        usage: pylnk3 create [-h] [--arguments [ARGUMENTS]] [--description [DESCRIPTION]] [--icon [ICON]]
+                             [--icon-index [ICON_INDEX]] [--workdir [WORKDIR]] [--mode [{Maximized,Normal,Minimized}]]
+                             target name
+        
+        positional arguments:
+          target                target path
+          name                  lnk filename to create
+        
+        optional arguments:
+          -h, --help            show this help message and exit
+          --arguments [ARGUMENTS], -a [ARGUMENTS]
+                                additional arguments
+          --description [DESCRIPTION], -d [DESCRIPTION]
+                                description
+          --icon [ICON], -i [ICON]
+                                icon filename
+          --icon-index [ICON_INDEX], -ii [ICON_INDEX]
+                                icon index
+          --workdir [WORKDIR], -w [WORKDIR]
+                                working directory
+          --mode [{Maximized,Normal,Minimized}], -m [{Maximized,Normal,Minimized}]
+                                window mode
+        ```
+        
+        #### Examples
+        ```sh
+        pylnk3 p filename.lnk
+        pylnk3 c c:\prog.exe shortcut.lnk
+        pylnk3 c \\192.168.1.1\share\file.doc doc.lnk
+        pylnk3 create c:\1.txt text.lnk -m Minimized -d "Description"
+        ```
+        
+        ## Changes
+        
+        **0.4.2**  
+        changed logic for Lnk.path choose (in case of different paths presents at different structures)  
+        read links with root as GUID of KNOWN_FOLDER  
+        [FIX] disabled padding for writing LinkInfo.local_base_path  
+        
+        **0.4.0**  
+        added support for network links  
+        reworked CLI (added more options for creating links)  
+        added entry point for call tool just like `pylnk3`  
+        [FIX] allow build links for non-existed (from this machine) paths  
+        [FIX] correct building links on Linux (now expect Windows-like path)  
+        [FIX] fixed path priority at parsing with both local & remote presents  
+        
+        
+        **0.3.0**  
+        added support links to UWP apps  
+        
+        
+        **0.2.1**  
+        released to PyPI
+        
+          
+        **0.2.0**  
+        converted to python 3  
+        
+Keywords: lnk,shortcut,windows
+Platform: UNKNOWN
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires-Python: >=3.6
+Description-Content-Type: text/markdown
diff --git a/pylnk3.egg-info/SOURCES.txt b/pylnk3.egg-info/SOURCES.txt
new file mode 100644
index 0000000..169231c
--- /dev/null
+++ b/pylnk3.egg-info/SOURCES.txt
@@ -0,0 +1,8 @@
+README.md
+pylnk3.py
+setup.py
+pylnk3.egg-info/PKG-INFO
+pylnk3.egg-info/SOURCES.txt
+pylnk3.egg-info/dependency_links.txt
+pylnk3.egg-info/entry_points.txt
+pylnk3.egg-info/top_level.txt
\ No newline at end of file
diff --git a/pylnk3.egg-info/dependency_links.txt b/pylnk3.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/pylnk3.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/pylnk3.egg-info/entry_points.txt b/pylnk3.egg-info/entry_points.txt
new file mode 100644
index 0000000..302c02c
--- /dev/null
+++ b/pylnk3.egg-info/entry_points.txt
@@ -0,0 +1,3 @@
+[console_scripts]
+pylnk3 = pylnk3:cli
+
diff --git a/pylnk3.egg-info/top_level.txt b/pylnk3.egg-info/top_level.txt
new file mode 100644
index 0000000..77454c7
--- /dev/null
+++ b/pylnk3.egg-info/top_level.txt
@@ -0,0 +1 @@
+pylnk3
diff --git a/pylnk3.py b/pylnk3.py
new file mode 100644
index 0000000..376e37d
--- /dev/null
+++ b/pylnk3.py
@@ -0,0 +1,1991 @@
+#!/usr/bin/env python3
+
+# original version written by Tim-Christian Mundt (2011):
+# https://sourceforge.net/p/pylnk/code/HEAD/tree/trunk/pylnk.py
+
+# converted to python3 by strayge:
+# https://github.com/strayge/pylnk
+import argparse
+import ntpath
+import os
+import re
+import time
+from datetime import datetime
+from io import BytesIO, IOBase
+from pprint import pformat
+from struct import pack, unpack
+from typing import Dict, Optional, Tuple, Union
+
+DEFAULT_CHARSET = 'cp1251'
+
+# ---- constants
+
+_SIGNATURE = b'L\x00\x00\x00'
+_GUID = b'\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00F'
+_LINK_INFO_HEADER_DEFAULT = 0x1C
+_LINK_INFO_HEADER_OPTIONAL = 0x24
+
+_LINK_FLAGS = (
+    'HasLinkTargetIDList',
+    'HasLinkInfo',
+    'HasName',
+    'HasRelativePath',
+    'HasWorkingDir',
+    'HasArguments',
+    'HasIconLocation',
+    'IsUnicode',
+    'ForceNoLinkInfo',
+    # new
+    'HasExpString',
+    'RunInSeparateProcess',
+    'Unused1',
+    'HasDarwinID',
+    'RunAsUser',
+    'HasExpIcon',
+    'NoPidlAlias',
+    'Unused2',
+    'RunWithShimLayer',
+    'ForceNoLinkTrack',
+    'EnableTargetMetadata',
+    'DisableLinkPathTracking',
+    'DisableKnownFolderTracking',
+    'DisableKnownFolderAlias',
+    'AllowLinkToLink',
+    'UnaliasOnSave',
+    'PreferEnvironmentPath',
+    'KeepLocalIDListForUNCTarget',
+)
+
+_FILE_ATTRIBUTES_FLAGS = (
+    'read_only', 'hidden', 'system_file', 'reserved1',
+    'directory', 'archive', 'reserved2', 'normal',
+    'temporary', 'sparse_file', 'reparse_point',
+    'compressed', 'offline', 'not_content_indexed',
+    'encrypted',
+)
+
+_MODIFIER_KEYS = ('SHIFT', 'CONTROL', 'ALT')
+
+WINDOW_NORMAL = "Normal"
+WINDOW_MAXIMIZED = "Maximized"
+WINDOW_MINIMIZED = "Minimized"
+_SHOW_COMMANDS = {1: WINDOW_NORMAL, 3: WINDOW_MAXIMIZED, 7: WINDOW_MINIMIZED}
+_SHOW_COMMAND_IDS = dict((v, k) for k, v in _SHOW_COMMANDS.items())
+
+DRIVE_UNKNOWN = "Unknown"
+DRIVE_NO_ROOT_DIR = "No root directory"
+DRIVE_REMOVABLE = "Removable"
+DRIVE_FIXED = "Fixed (Hard disk)"
+DRIVE_REMOTE = "Remote (Network drive)"
+DRIVE_CDROM = "CD-ROM"
+DRIVE_RAMDISK = "Ram disk"
+_DRIVE_TYPES = {0: DRIVE_UNKNOWN,
+                1: DRIVE_NO_ROOT_DIR,
+                2: DRIVE_REMOVABLE,
+                3: DRIVE_FIXED,
+                4: DRIVE_REMOTE,
+                5: DRIVE_CDROM,
+                6: DRIVE_RAMDISK}
+_DRIVE_TYPE_IDS = dict((v, k) for k, v in _DRIVE_TYPES.items())
+
+_KEYS = {
+    0x30: '0', 0x31: '1', 0x32: '2', 0x33: '3', 0x34: '4', 0x35: '5', 0x36: '6',
+    0x37: '7', 0x38: '8', 0x39: '9', 0x41: 'A', 0x42: 'B', 0x43: 'C', 0x44: 'D',
+    0x45: 'E', 0x46: 'F', 0x47: 'G', 0x48: 'H', 0x49: 'I', 0x4A: 'J', 0x4B: 'K',
+    0x4C: 'L', 0x4D: 'M', 0x4E: 'N', 0x4F: 'O', 0x50: 'P', 0x51: 'Q', 0x52: 'R',
+    0x53: 'S', 0x54: 'T', 0x55: 'U', 0x56: 'V', 0x57: 'W', 0x58: 'X', 0x59: 'Y',
+    0x5A: 'Z', 0x70: 'F1', 0x71: 'F2', 0x72: 'F3', 0x73: 'F4', 0x74: 'F5',
+    0x75: 'F6', 0x76: 'F7', 0x77: 'F8', 0x78: 'F9', 0x79: 'F10', 0x7A: 'F11',
+    0x7B: 'F12', 0x7C: 'F13', 0x7D: 'F14', 0x7E: 'F15', 0x7F: 'F16', 0x80: 'F17',
+    0x81: 'F18', 0x82: 'F19', 0x83: 'F20', 0x84: 'F21', 0x85: 'F22', 0x86: 'F23',
+    0x87: 'F24', 0x90: 'NUM LOCK', 0x91: 'SCROLL LOCK'
+}
+_KEY_CODES = dict((v, k) for k, v in _KEYS.items())
+
+ROOT_MY_COMPUTER = 'MY_COMPUTER'
+ROOT_MY_DOCUMENTS = 'MY_DOCUMENTS'
+ROOT_NETWORK_SHARE = 'NETWORK_SHARE'
+ROOT_NETWORK_SERVER = 'NETWORK_SERVER'
+ROOT_NETWORK_PLACES = 'NETWORK_PLACES'
+ROOT_NETWORK_DOMAIN = 'NETWORK_DOMAIN'
+ROOT_INTERNET = 'INTERNET'
+RECYCLE_BIN = 'RECYCLE_BIN'
+ROOT_CONTROL_PANEL = 'CONTROL_PANEL'
+ROOT_USER = 'USERPROFILE'
+ROOT_UWP_APPS = 'APPS'
+
+_ROOT_LOCATIONS = {
+    '{20D04FE0-3AEA-1069-A2D8-08002B30309D}': ROOT_MY_COMPUTER,
+    '{450D8FBA-AD25-11D0-98A8-0800361B1103}': ROOT_MY_DOCUMENTS,
+    '{54a754c0-4bf1-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SHARE,
+    '{c0542a90-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SERVER,
+    '{208D2C60-3AEA-1069-A2D7-08002B30309D}': ROOT_NETWORK_PLACES,
+    '{46e06680-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_DOMAIN,
+    '{871C5380-42A0-1069-A2EA-08002B30309D}': ROOT_INTERNET,
+    '{645FF040-5081-101B-9F08-00AA002F954E}': RECYCLE_BIN,
+    '{21EC2020-3AEA-1069-A2DD-08002B30309D}': ROOT_CONTROL_PANEL,
+    '{59031A47-3F72-44A7-89C5-5595FE6B30EE}': ROOT_USER,
+    '{4234D49B-0245-4DF3-B780-3893943456E1}': ROOT_UWP_APPS,
+}
+_ROOT_LOCATION_GUIDS = dict((v, k) for k, v in _ROOT_LOCATIONS.items())
+
+TYPE_FOLDER = 'FOLDER'
+TYPE_FILE = 'FILE'
+_ENTRY_TYPES = {
+    0x00: 'KNOWN_FOLDER',
+    0x31: 'FOLDER',
+    0x32: 'FILE',
+    0x35: 'FOLDER (UNICODE)',
+    0x36: 'FILE (UNICODE)',
+    0x802E: 'ROOT_KNOWN_FOLDER',
+    # founded in doc, not tested
+    0x1f: 'ROOT_FOLDER',
+    0x61: 'URI',
+    0x71: 'CONTROL_PANEL',
+}
+_ENTRY_TYPE_IDS = dict((v, k) for k, v in _ENTRY_TYPES.items())
+
+_DRIVE_PATTERN = re.compile(r'(\w)[:/\\]*$')
+
+# ---- read and write binary data
+
+
+def read_byte(buf):
+    return unpack('<B', buf.read(1))[0]
+
+
+def read_short(buf):
+    return unpack('<H', buf.read(2))[0]
+
+
+def read_int(buf):
+    return unpack('<I', buf.read(4))[0]
+
+
+def read_double(buf):
+    return unpack('<Q', buf.read(8))[0]
+
+
+def read_cunicode(buf):
+    s = b""
+    b = buf.read(2)
+    while b != b'\x00\x00':
+        s += b
+        b = buf.read(2)
+    return s.decode('utf-16-le')
+
+
+def read_cstring(buf, padding=False):
+    s = b""
+    b = buf.read(1)
+    while b != b'\x00':
+        s += b
+        b = buf.read(1)
+    if padding and not len(s) % 2:
+        buf.read(1)  # make length + terminator even
+    # TODO: encoding is not clear, unicode-escape has been necessary sometimes
+    return s.decode(DEFAULT_CHARSET)
+
+
+def read_sized_string(buf, string=True):
+    size = read_short(buf)
+    if string:
+        return buf.read(size*2).decode('utf-16-le')
+    else:
+        return buf.read(size)
+
+
+def get_bits(value, start, count, length=16):
+    mask = 0
+    for i in range(count):
+        mask = mask | 1 << i
+    shift = length - start - count
+    return value >> shift & mask
+
+
+def read_dos_datetime(buf):
+    date = read_short(buf)
+    time = read_short(buf)
+    year = get_bits(date, 0, 7) + 1980
+    month = get_bits(date, 7, 4)
+    day = get_bits(date, 11, 5)
+    hour = get_bits(time, 0, 5)
+    minute = get_bits(time, 5, 6)
+    second = get_bits(time, 11, 5)
+    # fix zeroes
+    month = max(month, 1)
+    day = max(day, 1)
+    return datetime(year, month, day, hour, minute, second)
+
+
+def write_byte(val, buf):
+    buf.write(pack('<B', val))
+
+
+def write_short(val, buf):
+    buf.write(pack('<H', val))
+
+
+def write_int(val, buf):
+    buf.write(pack('<I', val))
+
+
+def write_double(val, buf):
+    buf.write(pack('<Q', val))
+
+
+def write_cstring(val, buf, padding=False):
+    # val = val.encode('unicode-escape').replace('\\\\', '\\')
+    val = val.encode(DEFAULT_CHARSET)
+    buf.write(val + b'\x00')
+    if padding and not len(val) % 2:
+        buf.write(b'\x00')
+
+
+def write_cunicode(val, buf):
+    uni = val.encode('utf-16-le')
+    buf.write(uni + b'\x00\x00')
+
+
+def write_sized_string(val, buf, string=True):
+    size = len(val)
+    write_short(size, buf)
+    if string:
+        buf.write(val.encode('utf-16-le'))
+    else:
+        buf.write(val.encode())
+
+
+def put_bits(bits, target, start, count, length=16):
+    return target | bits << (length - start - count)
+
+
+def write_dos_datetime(val, buf):
+    date = time = 0
+    date = put_bits(val.year-1980, date, 0, 7)
+    date = put_bits(val.month, date, 7, 4)
+    date = put_bits(val.day, date, 11, 5)
+    time = put_bits(val.hour, time, 0, 5)
+    time = put_bits(val.minute, time, 5, 6)
+    time = put_bits(val.second, time, 11, 5)
+    write_short(date, buf)
+    write_short(time, buf)
+
+
+# ---- helpers
+
+def convert_time_to_unix(windows_time):
+    # Windows time is specified as the number of 0.1 nanoseconds since January 1, 1601.
+    # UNIX time is specified as the number of seconds since January 1, 1970.
+    # There are 134774 days (or 11644473600 seconds) between these dates.
+    unix_time = windows_time / 10000000.0 - 11644473600
+    try:
+        return datetime.fromtimestamp(unix_time)
+    except OSError:
+        return datetime.now()
+
+
+def convert_time_to_windows(unix_time):
+    if isinstance(unix_time, datetime):
+        unix_time = time.mktime(unix_time.timetuple())
+    return int((unix_time + 11644473600) * 10000000)
+
+
+class FormatException(Exception):
+    pass
+
+
+class MissingInformationException(Exception):
+    pass
+
+
+class InvalidKeyException(Exception):
+    pass
+
+
+def guid_from_bytes(bytes):
+    if len(bytes) != 16:
+        raise FormatException("This is no valid _GUID: %s" % bytes)
+    ordered = [
+        bytes[3], bytes[2], bytes[1], bytes[0],
+        bytes[5], bytes[4], bytes[7], bytes[6],
+        bytes[8], bytes[9], bytes[10], bytes[11],
+        bytes[12], bytes[13], bytes[14], bytes[15]
+    ]
+    return "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered])
+
+
+def bytes_from_guid(guid):
+    nums = [
+        guid[1:3], guid[3:5], guid[5:7], guid[7:9],
+        guid[10:12], guid[12:14], guid[15:17], guid[17:19],
+        guid[20:22], guid[22:24], guid[25:27], guid[27:29],
+        guid[29:31], guid[31:33], guid[33:35], guid[35:37]
+    ]
+    ordered_nums = [
+        nums[3], nums[2], nums[1], nums[0],
+        nums[5], nums[4], nums[7], nums[6],
+        nums[8], nums[9], nums[10], nums[11],
+        nums[12], nums[13], nums[14], nums[15],
+    ]
+    return bytes([int(x, 16) for x in ordered_nums])
+
+
+def assert_lnk_signature(f):
+    f.seek(0)
+    sig = f.read(4)
+    guid = f.read(16)
+    if sig != _SIGNATURE:
+        raise FormatException("This is not a .lnk file.")
+    if guid != _GUID:
+        raise FormatException("Cannot read this kind of .lnk file.")
+
+
+def is_lnk(f):
+    if hasattr(f, 'name'):
+        if f.name.split(os.path.extsep)[-1] == "lnk":
+            assert_lnk_signature(f)
+            return True
+        else:
+            return False
+    else:
+        try:
+            assert_lnk_signature(f)
+            return True
+        except FormatException:
+            return False
+
+
+def path_levels(p):
+    dirname, base = ntpath.split(p)
+    if base != '':
+        for level in path_levels(dirname):
+            yield level
+    yield p
+
+
+def is_drive(data):
+    if type(data) not in (str, str):
+        return False
+    p = re.compile("[a-zA-Z]:\\\\?$")
+    return p.match(data) is not None
+
+
+# ---- data structures
+
+class Flags(object):
+    
+    def __init__(self, flag_names: Tuple[str, ...], flags_bytes=0):
+        self._flag_names = flag_names
+        self._flags: Dict[str, bool] = dict([(name, False) for name in flag_names])
+        self.set_flags(flags_bytes)
+    
+    def set_flags(self, flags_bytes):
+        for pos, flag_name in enumerate(self._flag_names):
+            self._flags[flag_name] = bool(flags_bytes >> pos & 0x1)
+
+    @property
+    def bytes(self):
+        bytes = 0
+        for pos in range(len(self._flag_names)):
+            bytes = (self._flags[self._flag_names[pos]] and 1 or 0) << pos | bytes
+        return bytes
+    
+    def __getitem__(self, key):
+        if key in self._flags:
+            return object.__getattribute__(self, '_flags')[key]
+        return object.__getattribute__(self, key)
+    
+    def __setitem__(self, key, value):
+        if key not in self._flags:
+            raise KeyError("The key '%s' is not defined for those flags." % key)
+        self._flags[key] = value
+    
+    def __getattr__(self, key):
+        if key in self._flags:
+            return object.__getattribute__(self, '_flags')[key]
+        return object.__getattribute__(self, key)
+    
+    def __setattr__(self, key, value):
+        if '_flags' not in self.__dict__:
+            object.__setattr__(self, key, value)
+        elif key in self.__dict__:
+            object.__setattr__(self, key, value)
+        else:
+            self.__setitem__(key, value)
+
+    def __str__(self):
+        return pformat(self._flags, indent=2)
+
+
+class ModifierKeys(Flags):
+    
+    def __init__(self, flags_bytes=0):
+        Flags.__init__(self, _MODIFIER_KEYS, flags_bytes)
+    
+    def __str__(self):
+        s = ""
+        s += self.CONTROL and "CONTROL+" or ""
+        s += self.SHIFT and "SHIFT+" or ""
+        s += self.ALT and "ALT+" or ""
+        return s
+
+
+# _ROOT_INDEX = {
+#     0x00: 'INTERNET_EXPLORER1',
+#     0x42: 'LIBRARIES',
+#     0x44: 'USERS',
+#     0x48: 'MY_DOCUMENTS',
+#     0x50: 'MY_COMPUTER',
+#     0x58: 'MY_NETWORK_PLACES',
+#     0x60: 'RECYCLE_BIN',
+#     0x68: 'INTERNET_EXPLORER2',
+#     0x70: 'UNKNOWN',
+#     0x80: 'MY_GAMES',
+# }
+
+
+class RootEntry(object):
+    
+    def __init__(self, root):
+        if root is not None:
+            # create from text representation
+            if root in list(_ROOT_LOCATION_GUIDS.keys()):
+                self.root = root
+                self.guid = _ROOT_LOCATION_GUIDS[root]
+                return
+
+            # from binary
+            root_type = root[0]
+            index = root[1]
+            guid_bytes = root[2:18]
+            self.guid = guid_from_bytes(guid_bytes)
+            self.root = _ROOT_LOCATIONS.get(self.guid, f"UNKNOWN {self.guid}")
+            # if self.root == "UNKNOWN":
+            #     self.root = _ROOT_INDEX.get(index, "UNKNOWN")
+
+    @property
+    def bytes(self):
+        guid = self.guid[1:-1].replace('-', '')
+        chars = [bytes([int(x, 16)]) for x in [guid[i:i+2] for i in range(0, 32, 2)]]
+        return (
+            b'\x1F\x50'
+            + chars[3] + chars[2] + chars[1] + chars[0]
+            + chars[5] + chars[4] + chars[7] + chars[6]
+            + b''.join(chars[8:])
+        )
+
+    def __str__(self):
+        return "<RootEntry: %s>" % self.root
+
+
+class DriveEntry(object):
+    
+    def __init__(self, drive: str):
+        if len(drive) == 23:
+            # binary data from parsed lnk
+            self.drive = drive[1:3]
+        else:
+            # text representation
+            m = _DRIVE_PATTERN.match(drive.strip())
+            if m:
+                self.drive = m.groups()[0].upper() + ':'
+                self.drive = self.drive.encode()
+            else:
+                raise FormatException("This is not a valid drive: " + str(drive))
+
+    @property
+    def bytes(self):
+        drive = self.drive
+        padded_str = drive + b'\\' + b'\x00' * 19
+        return b'\x2F' + padded_str
+        # drive = self.drive
+        # if isinstance(drive, str):
+        #     drive = drive.encode()
+        # return b'/' + drive + b'\\' + b'\x00' * 19
+
+    def __str__(self):
+        return "<DriveEntry: %s>" % self.drive
+
+
+class PathSegmentEntry(object):
+    
+    def __init__(self, bytes=None):
+        self.type = None
+        self.file_size = None
+        self.modified = None
+        self.short_name = None
+        self.created = None
+        self.accessed = None
+        self.full_name = None
+        if bytes is None:
+            return
+
+        buf = BytesIO(bytes)
+        self.type = _ENTRY_TYPES.get(read_short(buf), 'UNKNOWN')
+        short_name_is_unicode = self.type.endswith('(UNICODE)')
+
+        if self.type == 'ROOT_KNOWN_FOLDER':
+            self.full_name = '::' + guid_from_bytes(buf.read(16))
+            # then followed Beef0026 structure:
+            # short size
+            # short version
+            # int signature == 0xBEEF0026
+            # (16 bytes) created timestamp
+            # (16 bytes) modified timestamp
+            # (16 bytes) accessed timestamp
+            return
+
+        if self.type == 'KNOWN_FOLDER':
+            _ = read_short(buf)  # extra block size
+            extra_signature = read_int(buf)
+            if extra_signature == 0x23FEBBEE:
+                _ = read_short(buf)  # unknown
+                _ = read_short(buf)  # guid len
+                # that format recognized by explorer
+                self.full_name = '::' + guid_from_bytes(buf.read(16))
+            return
+
+        self.file_size = read_int(buf)
+        self.modified = read_dos_datetime(buf)
+        unknown = read_short(buf)  # FileAttributesL
+        if short_name_is_unicode:
+            self.short_name = read_cunicode(buf)
+        else:
+            self.short_name = read_cstring(buf, padding=True)
+        extra_size = read_short(buf)
+        extra_version = read_short(buf)
+        extra_signature = read_int(buf)
+        if extra_signature == 0xBEEF0004:
+            # indicator_1 = read_short(buf)  # see below
+            # only_83 = read_short(buf) < 0x03
+            # unknown = read_short(buf)  # 0x04
+            # self.is_unicode = read_short(buf) == 0xBeef
+            self.created = read_dos_datetime(buf)  # 4 bytes
+            self.accessed = read_dos_datetime(buf)  # 4 bytes
+            offset_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):
+        entry = cls()
+        entry.type = os.path.isdir(path) and TYPE_FOLDER or TYPE_FILE
+        try:
+            st = os.stat(path)
+            entry.file_size = st.st_size
+            entry.modified = datetime.fromtimestamp(st.st_mtime)
+            entry.created = datetime.fromtimestamp(st.st_ctime)
+            entry.accessed = datetime.fromtimestamp(st.st_atime)
+        except FileNotFoundError:
+            now = datetime.now()
+            entry.file_size = 0
+            entry.modified = now
+            entry.created = now
+            entry.accessed = now
+        entry.short_name = ntpath.split(path)[1]
+        entry.full_name = entry.short_name
+        return entry
+
+    def _validate(self):
+        if self.type is None:
+            raise MissingInformationException("Type is missing, choose either TYPE_FOLDER or TYPE_FILE.")
+        if self.file_size is None:
+            if self.type.startswith('FOLDER') or self.type in ['KNOWN_FOLDER', 'ROOT_KNOWN_FOLDER']:
+                self.file_size = 0
+            else:
+                raise MissingInformationException("File size missing")
+        if self.created is None:
+            self.created = datetime.now()
+        if self.modified is None:
+            self.modified = datetime.now()
+        if self.accessed is None:
+            self.accessed = datetime.now()
+        # if self.modified is None or self.accessed is None or self.created is None:
+        #     raise MissingInformationException("Date information missing")
+        if self.full_name is None:
+            raise MissingInformationException("A full name is missing")
+        if self.short_name is None:
+            self.short_name = self.full_name
+
+    @property
+    def bytes(self):
+        if self.full_name is None:
+            return
+        self._validate()
+        out = BytesIO()
+        entry_type = self.type
+
+        if entry_type == 'KNOWN_FOLDER':
+            write_short(_ENTRY_TYPE_IDS[entry_type], out)
+            write_short(0x1A, out)  # size
+            write_int(0x23FEBBEE, out)  # extra signature
+            write_short(0x00, out)  # extra signature
+            write_short(0x10, out)  # guid size
+            out.write(bytes_from_guid(self.full_name.strip(':')))
+            return out.getvalue()
+
+        if entry_type == 'ROOT_KNOWN_FOLDER':
+            write_short(_ENTRY_TYPE_IDS[entry_type], out)
+            out.write(bytes_from_guid(self.full_name.strip(':')))
+            write_short(0x26, out)  # 0xBEEF0026 structure size
+            write_short(0x01, out)  # version
+            write_int(0xBEEF0026, out)  # extra signature
+            write_int(0x11, out)  # some flag for containing datetime
+            write_double(0x00, out)  # created datetime
+            write_double(0x00, out)  # modified datetime
+            write_double(0x00, out)  # accessed datetime
+            write_short(0x14, out)  # unknown
+            return out.getvalue()
+
+        short_name_len = len(self.short_name) + 1
+        try:
+            self.short_name.encode("ascii")
+            short_name_is_unicode = False
+            short_name_len += short_name_len % 2  # padding
+        except (UnicodeEncodeError, UnicodeDecodeError):
+            short_name_is_unicode = True
+            short_name_len = short_name_len * 2
+            self.type += " (UNICODE)"
+        write_short(_ENTRY_TYPE_IDS[entry_type], out)
+        write_int(self.file_size, out)
+        write_dos_datetime(self.modified, out)
+        write_short(0x10, out)
+        if short_name_is_unicode:
+            write_cunicode(self.short_name, out)
+        else:
+            write_cstring(self.short_name, out, padding=True)
+        indicator = 24 + 2 * len(self.short_name)
+        write_short(indicator, out)  # size
+        write_short(0x03, out)  # version
+        write_short(0x04, out)  # signature part1
+        write_short(0xBeef, out)  # signature part2
+        write_dos_datetime(self.created, out)
+        write_dos_datetime(self.accessed, out)
+        offset_unicode = 0x14  # fixed data structure, always the same
+        write_short(offset_unicode, out)
+        offset_ansi = 0  # we always write unicode
+        write_short(offset_ansi, out)  # long_string_size
+        write_cunicode(self.full_name, out)
+        offset_part2 = 0x0E + short_name_len
+        write_short(offset_part2, out)
+        return out.getvalue()
+
+    def __str__(self):
+        return "<PathSegmentEntry: %s>" % self.full_name
+
+
+class UwpSubBlock:
+
+    block_names = {
+        0x11: 'PackageFamilyName',
+        # 0x0e: '',
+        # 0x19: '',
+        0x15: 'PackageFullName',
+        0x05: 'Target',
+        0x0f: 'Location',
+        0x20: 'RandomGuid',
+        0x0c: 'Square150x150Logo',
+        0x02: 'Square44x44Logo',
+        0x0d: 'Wide310x150Logo',
+        # 0x04: '',
+        # 0x05: '',
+        0x13: 'Square310x310Logo',
+        # 0x0e: '',
+        0x0b: 'DisplayName',
+        0x14: 'Square71x71Logo',
+        0x64: 'RandomByte',
+        0x0a: 'DisplayName',
+        # 0x07: '',
+    }
+
+    block_types = {
+        'string': [0x11, 0x15, 0x05, 0x0f, 0x0c, 0x02, 0x0d, 0x13, 0x0b, 0x14, 0x0a],
+    }
+
+    def __init__(self, bytes=None, type=None, value=None):
+        self._data = bytes or b''
+        self.type = type
+        self.value = value
+        self.name = None
+        if self.type is not None:
+            self.name = self.block_names.get(self.type, 'UNKNOWN')
+        if not bytes:
+            return
+        buf = BytesIO(bytes)
+        self.type = read_byte(buf)
+        self.name = self.block_names.get(self.type, 'UNKNOWN')
+
+        self.value = self._data[1:]  # skip type
+        if self.type in self.block_types['string']:
+            unknown = read_int(buf)
+            probably_type = read_int(buf)
+            if probably_type == 0x1f:
+                string_len = read_int(buf)
+                self.value = read_cunicode(buf)
+
+    def __str__(self):
+        string = f'UwpSubBlock {self.name} ({hex(self.type)}): {self.value}'
+        return string.strip()
+
+    @property
+    def bytes(self):
+        out = BytesIO()
+        if self.value:
+            if isinstance(self.value, str):
+                string_len = len(self.value) + 1
+
+                write_byte(self.type, out)
+                write_int(0, out)
+                write_int(0x1f, out)
+
+                write_int(string_len, out)
+                write_cunicode(self.value, out)
+                if string_len % 2 == 1:  # padding
+                    write_short(0, out)
+
+            elif isinstance(self.value, bytes):
+                write_byte(self.type, out)
+                out.write(self.value)
+
+        result = out.getvalue()
+        return result
+
+
+class UwpMainBlock:
+    magic = b'\x31\x53\x50\x53'
+
+    def __init__(self, bytes=None, guid: Optional[str] = None, blocks=None):
+        self._data = bytes or b''
+        self._blocks = blocks or []
+        self.guid: str = guid
+        if not bytes:
+            return
+        buf = BytesIO(bytes)
+        magic = buf.read(4)
+        self.guid = guid_from_bytes(buf.read(16))
+        # read sub blocks
+        while True:
+            sub_block_size = read_int(buf)
+            if not sub_block_size:  # last size is zero
+                break
+            sub_block_data = buf.read(sub_block_size - 4)  # includes block_size
+            self._blocks.append(UwpSubBlock(sub_block_data))
+
+    def __str__(self):
+        string = f'<UwpMainBlock> {self.guid}:\n'
+        for block in self._blocks:
+            string += f'      {block}\n'
+        return string.strip()
+
+    @property
+    def bytes(self):
+        blocks_bytes = [block.bytes for block in self._blocks]
+        out = BytesIO()
+        out.write(self.magic)
+        out.write(bytes_from_guid(self.guid))
+        for block in blocks_bytes:
+            write_int(len(block) + 4, out)
+            out.write(block)
+        write_int(0, out)
+        result = out.getvalue()
+        return result
+
+
+class UwpSegmentEntry:
+    magic = b'APPS'
+    header = b'\x08\x00\x03\x00\x00\x00\x00\x00\x00\x00'
+
+    def __init__(self, bytes=None):
+        self._blocks = []
+        self._data = bytes
+        if bytes is None:
+            return
+        buf = BytesIO(bytes)
+        unknown = read_short(buf)
+        size = read_short(buf)
+        magic = buf.read(4)  # b'APPS'
+        blocks_size = read_short(buf)
+        unknown2 = buf.read(10)
+        # read main blocks
+        while True:
+            block_size = read_int(buf)
+            if not block_size:  # last size is zero
+                break
+            block_data = buf.read(block_size - 4)  # includes block_size
+            self._blocks.append(UwpMainBlock(block_data))
+
+    def __str__(self):
+        string = '<UwpSegmentEntry>:\n'
+        for block in self._blocks:
+            string += f'    {block}\n'
+        return string.strip()
+
+    @property
+    def bytes(self):
+        blocks_bytes = [block.bytes for block in self._blocks]
+        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, target, location=None, logo44x44=None):
+        segment = cls()
+
+        blocks = [
+            UwpSubBlock(type=0x11, value=package_family_name),
+            UwpSubBlock(type=0x0e, value=b'\x00\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00'),
+            UwpSubBlock(type=0x05, value=target),
+        ]
+        if location:
+            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
+
+
+class LinkTargetIDList(object):
+    
+    def __init__(self, bytes=None):
+        self.items = []
+        if bytes is not None:
+            buf = BytesIO(bytes)
+            raw = []
+            entry_len = read_short(buf)
+            while entry_len > 0:
+                raw.append(buf.read(entry_len - 2))  # the length includes the size
+                entry_len = read_short(buf)
+            self._interpret(raw)
+    
+    def _interpret(self, raw):
+        if raw[0][0] == 0x1F:
+            self.items.append(RootEntry(raw[0]))
+            if self.items[0].root == ROOT_MY_COMPUTER:
+                if len(raw[1]) == 0x17:
+                    self.items.append(DriveEntry(raw[1]))
+                elif raw[1][0:2] == b'\x2E\x80':  # ROOT_KNOWN_FOLDER
+                    self.items.append(PathSegmentEntry(raw[1]))
+                else:
+                    raise ValueError("This seems to be an absolute link which requires a drive as second element.")
+                items = raw[2:]
+            elif self.items[0].root == ROOT_NETWORK_PLACES:
+                raise NotImplementedError(
+                    "Parsing network lnks has not yet been implemented. "
+                    "If you need it just contact me and we'll see..."
+                )
+            else:
+                items = raw[1:]
+        else:
+            items = raw
+        for item in items:
+            if item[4:8] == b'APPS':
+                self.items.append(UwpSegmentEntry(item))
+            else:
+                self.items.append(PathSegmentEntry(item))
+    
+    def get_path(self):
+        segments = []
+        for item in self.items:
+            if type(item) == RootEntry:
+                segments.append('%' + item.root + '%')
+            elif type(item) == DriveEntry:
+                segments.append(item.drive.decode())
+            elif type(item) == PathSegmentEntry:
+                if item.full_name is not None:
+                    segments.append(item.full_name)
+            else:
+                segments.append(item)
+        return '\\'.join(segments)
+    
+    def _validate(self):
+        if type(self.items[0]) == RootEntry and self.items[0].root == ROOT_MY_COMPUTER:
+            if type(self.items[1]) == DriveEntry:
+                return
+            if type(self.items[1]) == PathSegmentEntry and self.items[1].full_name.startswith('::'):
+                return
+            raise ValueError("A drive is required for absolute lnks")
+
+    @property
+    def bytes(self):
+        self._validate()
+        out = BytesIO()
+        for item in self.items:
+            bytes = item.bytes
+            # skip invalid
+            if bytes is None:
+                continue
+            write_short(len(bytes) + 2, out)  # len + terminator
+            out.write(bytes)
+        out.write(b'\x00\x00')
+        return out.getvalue()
+
+    def __str__(self):
+        string = '<LinkTargetIDList>:\n'
+        for item in self.items:
+            string += f'  {item}\n'
+        return string.strip()
+
+
+class LinkInfo(object):
+
+    def __init__(self, lnk=None):
+        if lnk is not None:
+            self.start = lnk.tell()
+            self.size = read_int(lnk)
+            self.header_size = read_int(lnk)
+            link_info_flags = read_int(lnk)
+            self.local = link_info_flags & 1
+            self.remote = link_info_flags & 2
+            self.offs_local_volume_table = read_int(lnk)
+            self.offs_local_base_path = read_int(lnk)
+            self.offs_network_volume_table = read_int(lnk)
+            self.offs_base_name = read_int(lnk)
+            if self.header_size >= _LINK_INFO_HEADER_OPTIONAL:
+                print("TODO: read the unicode stuff")  # TODO: read the unicode stuff
+            self._parse_path_elements(lnk)
+        else:
+            self.size = None
+            self.header_size = _LINK_INFO_HEADER_DEFAULT
+            self.local = 0
+            self.remote = 0
+            self.offs_local_volume_table = 0
+            self.offs_local_base_path = 0
+            self.offs_network_volume_table = 0
+            self.offs_base_name = 0
+            self.drive_type = None
+            self.drive_serial = None
+            self.volume_label = None
+            self.local_base_path = None
+            self.network_share_name = None
+            self.base_name = None
+            self._path = None
+
+    def _parse_path_elements(self, lnk):
+        if self.remote:
+            # 20 is the offset of the network share name
+            lnk.seek(self.start + self.offs_network_volume_table + 20)
+            self.network_share_name = read_cstring(lnk)
+            lnk.seek(self.start + self.offs_base_name)
+            self.base_name = read_cstring(lnk)
+        if self.local:
+            lnk.seek(self.start + self.offs_local_volume_table + 4)
+            self.drive_type = _DRIVE_TYPES.get(read_int(lnk))
+            self.drive_serial = read_int(lnk)
+            lnk.read(4)  # volume name offset (10h)
+            self.volume_label = read_cstring(lnk)
+            lnk.seek(self.start + self.offs_local_base_path)
+            self.local_base_path = read_cstring(lnk)
+            # TODO: unicode
+        self.make_path()
+
+    def make_path(self):
+        if self.remote:
+            self._path = self.network_share_name + '\\' + self.base_name
+        if self.local:
+            self._path = self.local_base_path
+    
+    def write(self, lnk):
+        if self.remote is None:
+            raise MissingInformationException("No location information given.")
+        self.start = lnk.tell()
+        self._calculate_sizes_and_offsets()
+        write_int(self.size, lnk)
+        write_int(self.header_size, lnk)
+        write_int((self.local and 1) + (self.remote and 2), lnk)
+        write_int(self.offs_local_volume_table, lnk)
+        write_int(self.offs_local_base_path, lnk)
+        write_int(self.offs_network_volume_table, lnk)
+        write_int(self.offs_base_name, lnk)
+        if self.remote:
+            self._write_network_volume_table(lnk)
+            write_cstring(self.base_name, lnk, padding=False)
+        else:
+            self._write_local_volume_table(lnk)
+            write_cstring(self.local_base_path, lnk, padding=False)
+            write_byte(0, lnk)
+    
+    def _calculate_sizes_and_offsets(self):
+        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):
+        write_int(self.size_network_volume_table, buf)
+        write_int(2, buf)  # ?
+        write_int(20, buf)  # size of Network Volume Table
+        write_int(0, buf)  # ?
+        write_int(131072, buf)  # ?
+        write_cstring(self.network_share_name, buf)
+    
+    def _write_local_volume_table(self, buf):
+        write_int(self.size_local_volume_table, buf)
+        try:
+            drive_type = _DRIVE_TYPE_IDS[self.drive_type]
+        except KeyError:
+            raise ValueError("This is not a valid drive type: %s" % self.drive_type)
+        write_int(drive_type, buf)
+        write_int(self.drive_serial, buf)
+        write_int(16, buf)  # volume name offset
+        write_cstring(self.volume_label, buf)
+
+    @property
+    def path(self):
+        return self._path
+
+    def __str__(self):
+        s = "File Location Info:"
+        if self._path is None:
+            return s + " <not specified>"
+        if self.remote:
+            s += "\n  (remote)"
+            s += "\n  Network Share: %s" % self.network_share_name
+            s += "\n  Base Name: %s" % self.base_name
+        else:
+            s += "\n  (local)"
+            s += "\n  Volume Type: %s" % self.drive_type
+            s += "\n  Volume Serial Number: %s" % self.drive_serial
+            s += "\n  Volume Label: %s" % self.volume_label
+            s += "\n  Path: %s" % self.local_base_path
+        return s
+
+
+EXTRA_DATA_TYPES = {
+    0xA0000002: 'ConsoleDataBlock',  # size 0x000000CC
+    0xA0000004: 'ConsoleFEDataBlock',  # size 0x0000000C
+    0xA0000006: 'DarwinDataBlock',  # size 0x00000314
+    0xA0000001: 'EnvironmentVariableDataBlock',  # size 0x00000314
+    0xA0000007: 'IconEnvironmentDataBlock',  # size 0x00000314
+    0xA000000B: 'KnownFolderDataBlock',  # size 0x0000001C
+    0xA0000009: 'PropertyStoreDataBlock',  # size >= 0x0000000C
+    0xA0000008: 'ShimDataBlock',  # size >= 0x00000088
+    0xA0000005: 'SpecialFolderDataBlock',  # size 0x00000010
+    0xA0000003: 'VistaAndAboveIDListDataBlock',  # size 0x00000060
+    0xA000000C: 'VistaIDListDataBlock',  # size 0x00000173
+}
+
+
+class ExtraData_Unparsed(object):
+    def __init__(self, bytes=None, signature=None, data=None):
+        self._signature = signature
+        self._size = None
+        self.data = data
+        # if data:
+        #     self._size = len(data)
+        if bytes:
+            # self._size = len(bytes)
+            self.data = bytes
+            # self.read(bytes)
+
+    # def read(self, bytes):
+    #     buf = BytesIO(bytes)
+    #     size = len(bytes)
+    #     # self._size = read_int(buf)
+    #     # self._signature = read_int(buf)
+    #     self.data = buf.read(self._size - 8)
+
+    def bytes(self):
+        buf = BytesIO()
+        write_int(len(self.data)+8, buf)
+        write_int(self._signature, buf)
+        buf.write(self.data)
+        return buf.getvalue()
+
+    def __str__(self):
+        s = 'ExtraDataBlock\n signature %s\n data: %s' % (hex(self._signature), self.data)
+        return s
+
+
+def padding(val, size, byte=b'\x00'):
+    return val + (size-len(val)) * byte
+
+
+class ExtraData_IconEnvironmentDataBlock(object):
+    def __init__(self, bytes=None):
+        # self._size = None
+        # self._signature = None
+        self._signature = 0xA0000007
+        self.target_ansi = None
+        self.target_unicode = None
+        if bytes:
+            self.read(bytes)
+
+    def read(self, bytes):
+        buf = BytesIO(bytes)
+        # self._size = read_int(buf)
+        # self._signature = read_int(buf)
+        self.target_ansi = buf.read(260).decode()
+        self.target_unicode = buf.read(520).decode('utf-16-le')
+
+    def bytes(self):
+        target_ansi = padding(self.target_ansi.encode(), 260)
+        target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520)
+        size = 8 + len(target_ansi) + len(target_unicode)
+        assert self._signature == 0xA0000007
+        assert size == 0x00000314
+        buf = BytesIO()
+        write_int(size, buf)
+        write_int(self._signature, buf)
+        buf.write(target_ansi)
+        buf.write(target_unicode)
+        return buf.getvalue()
+
+    def __str__(self):
+        target_ansi = self.target_ansi.replace('\x00', '')
+        target_unicode = self.target_unicode.replace('\x00', '')
+        s = f'IconEnvironmentDataBlock\n TargetAnsi: {target_ansi}\n TargetUnicode: {target_unicode}'
+        return s
+
+
+def guid_to_str(guid):
+    ordered = [guid[3], guid[2], guid[1], guid[0], guid[5], guid[4],
+               guid[7], guid[6], guid[8], guid[9], guid[10], guid[11],
+               guid[12], guid[13], guid[14], guid[15]]
+    res = "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered])
+    # print(guid, res)
+    return res
+
+
+class TypedPropertyValue(object):
+    # types: [MS-OLEPS] section 2.15
+    def __init__(self, bytes=None, type=None, value=None):
+        self.type = type
+        self.value = value
+        if bytes:
+            self.type = read_short(BytesIO(bytes))
+            padding = bytes[2:4]
+            self.value = bytes[4:]
+
+    def set_string(self, value):
+        self.type = 0x1f
+        buf = BytesIO()
+        write_int(len(value)+2, buf)
+        buf.write(value.encode('utf-16-le'))
+        # terminator (included in size)
+        buf.write(b'\x00\x00\x00\x00')
+        # padding (not included in size)
+        if len(value) % 2:
+            buf.write(b'\x00\x00')
+        self.value = buf.getvalue()
+
+    @property
+    def bytes(self):
+        buf = BytesIO()
+        write_short(self.type, buf)
+        write_short(0x0000, buf)
+        buf.write(self.value)
+        return buf.getvalue()
+
+    def __str__(self):
+        value = self.value
+        if self.type == 0x1F:
+            size = value[:4]
+            value = value[4:].decode('utf-16-le')
+        if self.type == 0x15:
+            value = unpack('<Q', value)[0]
+        if self.type == 0x13:
+            value = unpack('<I', value)[0]
+        if self.type == 0x14:
+            value = unpack('<q', value)[0]
+        if self.type == 0x16:
+            value = unpack('<i', value)[0]
+        if self.type == 0x17:
+            value = unpack('<I', value)[0]
+        if self.type == 0x48:
+            value = guid_to_str(value)
+        if self.type == 0x40:
+            # FILETIME (Packet Version)
+            stream = BytesIO(value)
+            low = read_int(stream)
+            high = read_int(stream)
+            num = (high << 32) + low
+            value = convert_time_to_unix(num)
+        return '%s: %s' % (hex(self.type), value)
+
+
+class PropertyStore:
+    def __init__(self, bytes=None, properties=None, format_id=None, is_strings=False):
+        self.is_strings = is_strings
+        self.properties = []
+        self.format_id = format_id
+        self._is_end = False
+        if properties:
+            self.properties = properties
+        if bytes:
+            self.read(bytes)
+
+    def read(self, bytes_io):
+        buf = bytes_io
+        size = read_int(buf)
+        assert size < len(buf.getvalue())
+        if size == 0x00000000:
+            self._is_end = True
+            return
+        version = read_int(buf)
+        assert version == 0x53505331
+        self.format_id = buf.read(16)
+        if self.format_id == b'\xD5\xCD\xD5\x05\x2E\x9C\x10\x1B\x93\x97\x08\x00\x2B\x2C\xF9\xAE':
+            self.is_strings = True
+        else:
+            self.is_strings = False
+        while True:
+            # assert lnk.tell() < (start + size)
+            value_size = read_int(buf)
+            if value_size == 0x00000000:
+                break
+            if self.is_strings:
+                name_size = read_int(buf)
+                reserved = read_byte(buf)
+                name = buf.read(name_size).decode('utf-16-le')
+                value = TypedPropertyValue(buf.read(value_size-9))
+                self.properties.append((name, value))
+            else:
+                value_id = read_int(buf)
+                reserved = read_byte(buf)
+                value = TypedPropertyValue(buf.read(value_size-9))
+                self.properties.append((value_id, value))
+
+    @property
+    def bytes(self):
+        size = 8 + len(self.format_id)
+        properties = BytesIO()
+        for name, value in self.properties:
+            value_bytes = value.bytes
+            if self.is_strings:
+                name_bytes = name.encode('utf-16-le')
+                value_size = 9 + len(name_bytes) + len(value_bytes)
+                write_int(value_size, properties)
+                name_size = len(name_bytes)
+                write_int(name_size, properties)
+                properties.write(b'\x00')
+                properties.write(name_bytes)
+            else:
+                value_size = 9 + len(value_bytes)
+                write_int(value_size, properties)
+                write_int(name, properties)
+                properties.write(b'\x00')
+            properties.write(value_bytes)
+            size += value_size
+
+        write_int(0x00000000, properties)
+        size += 4
+
+        buf = BytesIO()
+        write_int(size, buf)
+        write_int(0x53505331, buf)
+        buf.write(self.format_id)
+        buf.write(properties.getvalue())
+
+        return buf.getvalue()
+
+    def __str__(self):
+        s = ' PropertyStore'
+        s += '\n  FormatID: %s' % guid_to_str(self.format_id)
+        for name, value in self.properties:
+            s += '\n  %3s = %s' % (name, str(value))
+        return s.strip()
+
+
+class ExtraData_PropertyStoreDataBlock(object):
+    def __init__(self, bytes=None, stores=None):
+        self._size = None
+        self._signature = 0xA0000009
+        self.stores = []
+        if stores:
+            self.stores = stores
+        if bytes:
+            self.read(bytes)
+
+    def read(self, bytes):
+        buf = BytesIO(bytes)
+        # self._size = read_int(buf)
+        # self._signature = read_int(buf)
+        # [MS-PROPSTORE] section 2.2
+        while True:
+            prop_store = PropertyStore(buf)
+            if prop_store._is_end:
+                break
+            self.stores.append(prop_store)
+
+    def bytes(self):
+        stores = b''
+        for prop_store in self.stores:
+            stores += prop_store.bytes
+        size = len(stores) + 8 + 4
+
+        assert self._signature == 0xA0000009
+        assert size >= 0x0000000C
+
+        buf = BytesIO()
+        write_int(size, buf)
+        write_int(self._signature, buf)
+        buf.write(stores)
+        write_int(0x00000000, buf)
+        return buf.getvalue()
+
+    def __str__(self):
+        s = 'PropertyStoreDataBlock'
+        for prop_store in self.stores:
+            s += '\n %s' % str(prop_store)
+        return s
+
+
+class ExtraData_EnvironmentVariableDataBlock(object):
+    def __init__(self, bytes=None):
+        self._signature = 0xA0000001
+        self.target_ansi = None
+        self.target_unicode = None
+        if bytes:
+            self.read(bytes)
+
+    def read(self, bytes):
+        buf = BytesIO(bytes)
+        self.target_ansi = buf.read(260).decode()
+        self.target_unicode = buf.read(520).decode('utf-16-le')
+
+    def bytes(self):
+        target_ansi = padding(self.target_ansi.encode(), 260)
+        target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520)
+        size = 8 + len(target_ansi) + len(target_unicode)
+        assert self._signature == 0xA0000001
+        assert size == 0x00000314
+        buf = BytesIO()
+        write_int(size, buf)
+        write_int(self._signature, buf)
+        buf.write(target_ansi)
+        buf.write(target_unicode)
+        return buf.getvalue()
+
+    def __str__(self):
+        target_ansi = self.target_ansi.replace('\x00', '')
+        target_unicode = self.target_unicode.replace('\x00', '')
+        s = f'EnvironmentVariableDataBlock\n TargetAnsi: {target_ansi}\n TargetUnicode: {target_unicode}'
+        return s
+
+
+EXTRA_DATA_TYPES_CLASSES = {
+    'IconEnvironmentDataBlock': ExtraData_IconEnvironmentDataBlock,
+    'PropertyStoreDataBlock': ExtraData_PropertyStoreDataBlock,
+    'EnvironmentVariableDataBlock': ExtraData_EnvironmentVariableDataBlock,
+}
+
+
+class ExtraData(object):
+    # EXTRA_DATA = *EXTRA_DATA_BLOCK TERMINAL_BLOCK
+    def __init__(self, lnk=None, blocks=None):
+        self.blocks = []
+        if blocks:
+            self.blocks = blocks
+        if lnk is None:
+            return
+        while True:
+            size = read_int(lnk)
+            if size < 4:  # TerminalBlock
+                break
+            signature = read_int(lnk)
+            bytes = lnk.read(size-8)
+            # lnk.seek(-8, 1)
+            block_type = EXTRA_DATA_TYPES[signature]
+            if block_type in EXTRA_DATA_TYPES_CLASSES:
+                block_class = EXTRA_DATA_TYPES_CLASSES[block_type]
+                block = block_class(bytes=bytes)
+            else:
+                block_class = ExtraData_Unparsed
+                block = block_class(bytes=bytes, signature=signature)
+            self.blocks.append(block)
+
+    @property
+    def bytes(self):
+        result = b''
+        for block in self.blocks:
+            result += block.bytes()
+        result += b'\x00\x00\x00\x00'  # TerminalBlock
+        return result
+
+    def __str__(self):
+        s = ''
+        for block in self.blocks:
+            s += '\n' + str(block)
+        return s
+
+
+class Lnk(object):
+    
+    def __init__(self, f=None):
+        self.file = None
+        if type(f) == str or type(f) == str:
+            self.file = f
+            try:
+                f = open(self.file, 'rb')
+            except IOError:
+                self.file += ".lnk"
+                f = open(self.file, 'rb')
+        # defaults
+        self.link_flags = Flags(_LINK_FLAGS)
+        self.file_flags = Flags(_FILE_ATTRIBUTES_FLAGS)
+        self.creation_time = datetime.now()
+        self.access_time = datetime.now()
+        self.modification_time = datetime.now()
+        self.file_size = 0
+        self.icon_index = 0
+        self._show_command = WINDOW_NORMAL
+        self.hot_key = None
+        self._link_info = LinkInfo()
+        self.description = None
+        self.relative_path = None
+        self.work_dir = None
+        self.arguments = None
+        self.icon = None
+        self.extra_data = None
+        if f is not None:
+            assert_lnk_signature(f)
+            self._parse_lnk_file(f)
+        if self.file:
+            f.close()
+    
+    def _read_hot_key(self, lnk):
+        low = read_byte(lnk)
+        high = read_byte(lnk)
+        key = _KEYS.get(low, '')
+        modifier = high and str(ModifierKeys(high)) or ''
+        return modifier + key
+    
+    def _write_hot_key(self, hot_key, lnk):
+        if hot_key is None or not hot_key:
+            low = high = 0
+        else:
+            hot_key = hot_key.split('+')
+            try:
+                low = _KEY_CODES[hot_key[-1]]
+            except KeyError:
+                raise InvalidKeyException("Cannot find key code for %s" % hot_key[1])
+            modifiers = ModifierKeys()
+            for modifier in hot_key[:-1]:
+                modifiers[modifier.upper()] = True
+            high = modifiers.bytes
+        write_byte(low, lnk)
+        write_byte(high, lnk)
+
+    def _parse_lnk_file(self, lnk):
+        # SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO] [STRING_DATA] *EXTRA_DATA
+
+        # SHELL_LINK_HEADER
+        lnk.seek(20)  # after signature and guid
+        self.link_flags.set_flags(read_int(lnk))
+        self.file_flags.set_flags(read_int(lnk))
+        self.creation_time = convert_time_to_unix(read_double(lnk))
+        self.access_time = convert_time_to_unix(read_double(lnk))
+        self.modification_time = convert_time_to_unix(read_double(lnk))
+        self.file_size = read_int(lnk)
+        self.icon_index = read_int(lnk)
+        show_command = read_int(lnk)
+        self._show_command = _SHOW_COMMANDS[show_command] if show_command in _SHOW_COMMANDS else _SHOW_COMMANDS[1]
+        self.hot_key = self._read_hot_key(lnk)
+        lnk.read(10)  # reserved (0)
+
+        # LINKTARGET_IDLIST (HasLinkTargetIDList)
+        if self.link_flags.HasLinkTargetIDList:
+            shell_item_id_list_size = read_short(lnk)
+            self.shell_item_id_list = LinkTargetIDList(lnk.read(shell_item_id_list_size))
+
+        # LINKINFO (HasLinkInfo)
+        if self.link_flags.HasLinkInfo and not self.link_flags.ForceNoLinkInfo:
+            self._link_info = LinkInfo(lnk)
+            lnk.seek(self._link_info.start + self._link_info.size)
+
+        # STRING_DATA = [NAME_STRING] [RELATIVE_PATH] [WORKING_DIR] [COMMAND_LINE_ARGUMENTS] [ICON_LOCATION]
+        if self.link_flags.HasName:
+            self.description = read_sized_string(lnk, self.link_flags.IsUnicode)
+        if self.link_flags.HasRelativePath:
+            self.relative_path = read_sized_string(lnk, self.link_flags.IsUnicode)
+        if self.link_flags.HasWorkingDir:
+            self.work_dir = read_sized_string(lnk, self.link_flags.IsUnicode)
+        if self.link_flags.HasArguments:
+            self.arguments = read_sized_string(lnk, self.link_flags.IsUnicode)
+        if self.link_flags.HasIconLocation:
+            self.icon = read_sized_string(lnk, self.link_flags.IsUnicode)
+
+        # *EXTRA_DATA
+        self.extra_data = ExtraData(lnk)
+
+    def save(self, f: Optional[Union[str, IOBase]] = None, force_ext=False):
+        if f is None:
+            f = self.file
+        if f is None:
+            raise ValueError("File (name) missing for saving the lnk")
+        is_file = hasattr(f, 'write')
+        if not is_file:
+            if not type(f) == str and not type(f) == str:
+                raise ValueError("Need a writeable object or a file name to save to, got %s" % f)
+            if force_ext:
+                if not f.lower().endswith('.lnk'):
+                    f += '.lnk'
+            f = open(f, 'wb')
+        self.write(f)
+        # only close the stream if it's our own
+        if not is_file:
+            f.close()
+    
+    def write(self, lnk):
+        lnk.write(_SIGNATURE)
+        lnk.write(_GUID)
+        write_int(self.link_flags.bytes, lnk)
+        write_int(self.file_flags.bytes, lnk)
+        write_double(convert_time_to_windows(self.creation_time), lnk)
+        write_double(convert_time_to_windows(self.access_time), lnk)
+        write_double(convert_time_to_windows(self.modification_time), lnk)
+        write_int(self.file_size, lnk)
+        write_int(self.icon_index, lnk)
+        write_int(_SHOW_COMMAND_IDS[self._show_command], lnk)
+        self._write_hot_key(self.hot_key, lnk)
+        lnk.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')  # reserved
+        if self.link_flags.HasLinkTargetIDList:
+            shell_item_id_list = self.shell_item_id_list.bytes
+            write_short(len(shell_item_id_list), lnk)
+            lnk.write(shell_item_id_list)
+        if self.link_flags.HasLinkInfo:
+            self._link_info.write(lnk)
+        if self.link_flags.HasName:
+            write_sized_string(self.description, lnk, self.link_flags.IsUnicode)
+        if self.link_flags.HasRelativePath:
+            write_sized_string(self.relative_path, lnk, self.link_flags.IsUnicode)
+        if self.link_flags.HasWorkingDir:
+            write_sized_string(self.work_dir, lnk, self.link_flags.IsUnicode)
+        if self.link_flags.HasArguments:
+            write_sized_string(self.arguments, lnk, self.link_flags.IsUnicode)
+        if self.link_flags.HasIconLocation:
+            write_sized_string(self.icon, lnk, self.link_flags.IsUnicode)
+        if self.extra_data:
+            lnk.write(self.extra_data.bytes)
+        else:
+            lnk.write(b'\x00\x00\x00\x00')
+
+    def _get_shell_item_id_list(self):
+        return self._shell_item_id_list
+
+    def _set_shell_item_id_list(self, shell_item_id_list):
+        self._shell_item_id_list = shell_item_id_list
+        self.link_flags.HasLinkTargetIDList = shell_item_id_list is not None
+    shell_item_id_list = property(_get_shell_item_id_list, _set_shell_item_id_list)
+
+    def _get_link_info(self):
+        return self._link_info
+
+    def _set_link_info(self, link_info):
+        self._link_info = link_info
+        self.link_flags.ForceNoLinkInfo = link_info is None
+        self.link_flags.HasLinkInfo = link_info is not None
+    link_info = property(_get_link_info, _set_link_info)
+
+    def _get_description(self):
+        return self._description
+
+    def _set_description(self, description):
+        self._description = description
+        self.link_flags.HasName = description is not None
+    description = property(_get_description, _set_description)
+
+    def _get_relative_path(self):
+        return self._relative_path
+
+    def _set_relative_path(self, relative_path):
+        self._relative_path = relative_path
+        self.link_flags.HasRelativePath = relative_path is not None
+    relative_path = property(_get_relative_path, _set_relative_path)
+
+    def _get_work_dir(self):
+        return self._work_dir
+
+    def _set_work_dir(self, work_dir):
+        self._work_dir = work_dir
+        self.link_flags.HasWorkingDir = work_dir is not None
+    work_dir = working_dir = property(_get_work_dir, _set_work_dir)
+
+    def _get_arguments(self):
+        return self._arguments
+
+    def _set_arguments(self, arguments):
+        self._arguments = arguments
+        self.link_flags.HasArguments = arguments is not None
+    arguments = property(_get_arguments, _set_arguments)
+
+    def _get_icon(self):
+        return self._icon
+
+    def _set_icon(self, icon):
+        self._icon = icon
+        self.link_flags.HasIconLocation = icon is not None
+    icon = property(_get_icon, _set_icon)
+    
+    def _get_window_mode(self):
+        return self._show_command
+
+    def _set_window_mode(self, value):
+        if value not in list(_SHOW_COMMANDS.values()):
+            raise ValueError("Not a valid window mode: %s. Choose any of pylnk.WINDOW_*" % value)
+        self._show_command = value
+    window_mode = show_command = property(_get_window_mode, _set_window_mode)
+
+    @property
+    def path(self):
+        # lnk can contains several different paths at different structures
+        # here is some logic consistent with link properties at explorer (at least on test examples)
+
+        link_info_path = self._link_info.path if self._link_info and self._link_info.path else None
+        id_list_path = self._shell_item_id_list.get_path() if hasattr(self, '_shell_item_id_list') else None
+
+        env_var_path = None
+        if self.extra_data and self.extra_data.blocks:
+            for block in self.extra_data.blocks:
+                if type(block) == ExtraData_EnvironmentVariableDataBlock:
+                    env_var_path = block.target_unicode.strip('\x00') or block.target_ansi.strip('\x00')
+                    break
+
+        if id_list_path and id_list_path.startswith('%MY_COMPUTER%'):
+            # full local path has priority
+            return id_list_path[14:]
+        if id_list_path and id_list_path.startswith('%USERPROFILE%\\::'):
+            # path to KNOWN_FOLDER also has priority over link_info
+            return id_list_path[14:]
+        if link_info_path:
+            # local path at link_info_path has priority over network path at id_list_path
+            # full local path at link_info_path has priority over partial path at id_list_path
+            return link_info_path
+        if env_var_path:
+            # some links in Recent folder contains path only at ExtraData_EnvironmentVariableDataBlock
+            return env_var_path
+        return id_list_path
+
+    def specify_local_location(self, path, drive_type=None, drive_serial=None, volume_label=None):
+        self._link_info.drive_type = drive_type or DRIVE_UNKNOWN
+        self._link_info.drive_serial = drive_serial or ''
+        self._link_info.volume_label = volume_label or ''
+        self._link_info.local_base_path = path
+        self._link_info.local = True
+        self._link_info.make_path()
+    
+    def specify_remote_location(self, network_share_name, base_name):
+        self._link_info.network_share_name = network_share_name
+        self._link_info.base_name = base_name
+        self._link_info.remote = True
+        self._link_info.make_path()
+
+    def __str__(self):
+        s = "Target file:\n"
+        s += str(self.file_flags)
+        s += "\nCreation Time: %s" % self.creation_time
+        s += "\nModification Time: %s" % self.modification_time
+        s += "\nAccess Time: %s" % self.access_time
+        s += "\nFile size: %s" % self.file_size
+        s += "\nWindow mode: %s" % self._show_command
+        s += "\nHotkey: %s\n" % self.hot_key
+        s += str(self._link_info)
+        if self.link_flags.HasLinkTargetIDList:
+            s += "\n%s" % self.shell_item_id_list
+        if self.link_flags.HasName:
+            s += "\nDescription: %s" % self.description
+        if self.link_flags.HasRelativePath:
+            s += "\nRelative Path: %s" % self.relative_path
+        if self.link_flags.HasWorkingDir:
+            s += "\nWorking Directory: %s" % self.work_dir
+        if self.link_flags.HasArguments:
+            s += "\nCommandline Arguments: %s" % self.arguments
+        if self.link_flags.HasIconLocation:
+            s += "\nIcon: %s" % self.icon
+        if self._link_info:
+            s += "\nUsed Path: %s" % self.path
+        if self.extra_data:
+            s += str(self.extra_data)
+        return s
+
+
+# ---- convenience functions
+
+def parse(lnk):
+    return Lnk(lnk)
+
+
+def create(f=None):
+    lnk = Lnk()
+    lnk.file = f
+    return lnk
+
+
+def for_file(
+    target_file, lnk_name=None, arguments=None, description=None, icon_file=None, icon_index=0,
+    work_dir=None, window_mode=None,
+):
+    lnk = create(lnk_name)
+    lnk.link_flags.IsUnicode = True
+    lnk.link_info = None
+    if target_file.startswith('\\\\'):
+        # remote link
+        lnk.link_info = LinkInfo()
+        lnk.link_info.remote = 1
+        # extract server + share name from full path
+        path_parts = target_file.split('\\')
+        share_name, base_name = '\\'.join(path_parts[:4]), '\\'.join(path_parts[4:])
+        lnk.link_info.network_share_name = share_name.upper()
+        lnk.link_info.base_name = base_name
+        # somehow it requires EnvironmentVariableDataBlock & HasExpString flag
+        env_data_block = ExtraData_EnvironmentVariableDataBlock()
+        env_data_block.target_ansi = target_file
+        env_data_block.target_unicode = target_file
+        lnk.extra_data = ExtraData(blocks=[env_data_block])
+        lnk.link_flags.HasExpString = True
+    else:
+        # local link
+        levels = list(path_levels(target_file))
+        elements = [RootEntry(ROOT_MY_COMPUTER),
+                    DriveEntry(levels[0])]
+        for level in levels[1:]:
+            segment = PathSegmentEntry.create_for_path(level)
+            elements.append(segment)
+        lnk.shell_item_id_list = LinkTargetIDList()
+        lnk.shell_item_id_list.items = elements
+    # lnk.link_flags.HasLinkInfo = True
+    if arguments:
+        lnk.link_flags.HasArguments = True
+        lnk.arguments = arguments
+    if description:
+        lnk.link_flags.HasName = True
+        lnk.description = description
+    if icon_file:
+        lnk.link_flags.HasIconLocation = True
+        lnk.icon = icon_file
+    lnk.icon_index = icon_index
+    if work_dir:
+        lnk.link_flags.HasWorkingDir = True
+        lnk.work_dir = work_dir
+    if window_mode:
+        lnk.window_mode = window_mode
+    if lnk_name:
+        lnk.save()
+    return lnk
+
+
+def from_segment_list(data, lnk_name=None):
+    """
+    Creates a lnk file from a list of path segments.
+    If lnk_name is given, the resulting lnk will be saved
+    to a file with that name.
+    The expected list for has the following format ("C:\\dir\\file.txt"):
+    
+    ['c:\\',
+     {'type': TYPE_FOLDER,
+      'size': 0,            # optional for folders
+      'name': "dir",
+      'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
+      'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
+      'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476)
+     },
+     {'type': TYPE_FILE,
+      'size': 823,
+      'name': "file.txt",
+      'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
+      'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
+      'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476)
+     }
+    ]
+    
+    For relative paths just omit the drive entry.
+    Hint: Correct dates really are not crucial for working lnks.
+    """
+    if type(data) not in (list, tuple):
+        raise ValueError("Invalid data format, list or tuple expected")
+    lnk = Lnk()
+    entries = []
+    if is_drive(data[0]):
+        # this is an absolute link
+        entries.append(RootEntry(ROOT_MY_COMPUTER))
+        if not data[0].endswith('\\'):
+            data[0] += "\\"
+        drive = data.pop(0).encode("ascii")
+        entries.append(DriveEntry(drive))
+    for level in data:
+        segment = PathSegmentEntry()
+        segment.type = level['type']
+        if level['type'] == TYPE_FOLDER:
+            segment.file_size = 0
+        else:
+            segment.file_size = level['size']
+        segment.short_name = level['name']
+        segment.full_name = level['name']
+        segment.created = level['created']
+        segment.modified = level['modified']
+        segment.accessed = level['accessed']
+        entries.append(segment)
+    lnk.shell_item_id_list = LinkTargetIDList()
+    lnk.shell_item_id_list.items = entries
+    if data[-1]['type'] == TYPE_FOLDER:
+        lnk.file_flags.directory = True
+    if lnk_name:
+        lnk.save(lnk_name)
+    return lnk
+
+
+def build_uwp(
+    package_family_name, target, location=None,logo44x44=None, lnk_name=None,
+) -> Lnk:
+    """
+    :param lnk_name:            ex.: crafted_uwp.lnk
+    :param package_family_name: ex.: Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe
+    :param target:              ex.: Microsoft.WindowsCalculator_8wekyb3d8bbwe!App
+    :param location:            ex.: C:\\Program Files\\WindowsApps\\Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe
+    :param logo44x44:           ex.: Assets\\CalculatorAppList.png
+    """
+    lnk = Lnk()
+    lnk.link_flags.HasLinkTargetIDList = True
+    lnk.link_flags.IsUnicode = True
+    lnk.link_flags.EnableTargetMetadata = True
+
+    lnk.shell_item_id_list = LinkTargetIDList()
+
+    elements = [
+        RootEntry(ROOT_UWP_APPS),
+        UwpSegmentEntry.create(
+            package_family_name=package_family_name,
+            target=target,
+            location=location,
+            logo44x44=logo44x44,
+        )
+    ]
+    lnk.shell_item_id_list.items = elements
+
+    if lnk_name:
+        lnk.file = lnk_name
+        lnk.save()
+    return lnk
+
+
+def get_prop(obj, prop_queue):
+    attr = getattr(obj, prop_queue[0])
+    if len(prop_queue) > 1:
+        return get_prop(attr, prop_queue[1:])
+    return attr
+
+
+def cli():
+    parser = argparse.ArgumentParser(add_help=False)
+    subparsers = parser.add_subparsers(dest='action', metavar='{p, c, d}')
+    parser.add_argument('--help', '-h', action='store_true')
+
+    parser_parse = subparsers.add_parser('parse', aliases=['p'], help='read lnk file')
+    parser_parse.add_argument('filename', help='lnk filename to read')
+    parser_parse.add_argument('props', nargs='*', help='props path to read')
+
+    parser_create = subparsers.add_parser('create', aliases=['c'], help='create new lnk file')
+    parser_create.add_argument('target', help='target path')
+    parser_create.add_argument('name', help='lnk filename to create')
+    parser_create.add_argument('--arguments', '-a', nargs='?', help='additional arguments')
+    parser_create.add_argument('--description', '-d', nargs='?', help='description')
+    parser_create.add_argument('--icon', '-i', nargs='?', help='icon filename')
+    parser_create.add_argument('--icon-index', '-ii', type=int, default=0, nargs='?', help='icon index')
+    parser_create.add_argument('--workdir', '-w', nargs='?', help='working directory')
+    parser_create.add_argument('--mode', '-m', nargs='?', choices=['Maximized', 'Normal', 'Minimized'], help='window mode')
+
+    parser_dup = subparsers.add_parser('duplicate', aliases=['d'], help='read and write lnk file')
+    parser_dup.add_argument('filename', help='lnk filename to read')
+    parser_dup.add_argument('new_filename', help='new filename to write')
+
+    args = parser.parse_args()
+    if args.help or not args.action:
+        print('''
+Tool for read or create .lnk files
+
+usage: pylnk3.py [p]arse / [c]reate ...
+
+Examples:
+pylnk3 p filename.lnk
+pylnk3 c c:\\prog.exe shortcut.lnk
+pylnk3 c \\\\192.168.1.1\\share\\file.doc doc.lnk
+pylnk3 create c:\\1.txt text.lnk -m Minimized -d "Description"
+
+for more info use help for each action (ex.: "pylnk3 create -h")
+        '''.strip())
+        exit(1)
+
+    if args.action in ['create', 'c']:
+        for_file(
+            args.target, args.name, arguments=args.arguments,
+            description=args.description, icon_file=args.icon,
+            icon_index=args.icon_index, work_dir=args.workdir,
+            window_mode=args.mode,
+        )
+    elif args.action in ['parse', 'p']:
+        lnk = parse(args.filename)
+        props = args.props
+        if len(props) == 0:
+            print(lnk)
+        else:
+            for prop in props:
+                print(get_prop(lnk, prop.split('.')))
+    elif args.action in ['d', 'duplicate']:
+        lnk = parse(args.filename)
+        new_filename = args.new_filename
+        print(lnk)
+        lnk.save(new_filename)
+        print('saved')
+
+
+if __name__ == '__main__':
+    cli()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..4927abe
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,4 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..c09ae1a
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,35 @@
+from setuptools import setup
+
+
+with open("README.md", "r") as fh:
+    long_description = fh.read()
+
+setup(
+    name="pylnk3",
+    version="0.4.2",
+    py_modules=["pylnk3"],
+    entry_points={
+        'console_scripts': [
+            'pylnk3 = pylnk3:cli',
+        ],
+    },
+    description="Windows LNK File Parser and Creator",
+    author="strayge",
+    author_email="strayge@gmail.com",
+    url="https://github.com/strayge/pylnk",
+    keywords=["lnk", "shortcut", "windows"],
+    license="GNU Library or Lesser General Public License (LGPL)",
+    classifiers=[
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
+        "Operating System :: OS Independent",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+    ],
+    python_requires='>=3.6',
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+)