1# This file is part of Xpra.
2# Copyright (C) 2020 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
5
6import os
7import struct
8
9from xpra.platform.win32 import get_common_startmenu_dir, get_startmenu_dir
10from xpra.log import Logger
11from xpra.util import print_nested_dict
12
13log = Logger("exec")
14
15
16def parse_link(content):
17    # skip first 20 bytes (HeaderSize and LinkCLSID)
18    # read the LinkFlags structure (4 bytes)
19    lflags = struct.unpack('I', content[0x14:0x18])[0]
20    position = 0x18
21    # if the HasLinkTargetIDList bit is set then skip the stored IDList
22    # structure and header
23    if (lflags & 0x01) == 1:
24        position = struct.unpack('H', content[0x4C:0x4E])[0] + 0x4E
25    last_pos = position
26    position += 0x04
27    # get how long the file information is (LinkInfoSize)
28    length = struct.unpack('I', content[last_pos:position])[0]
29    # skip 12 bytes (LinkInfoHeaderSize, LinkInfoFlags, and VolumeIDOffset)
30    position += 0x0C
31    # go to the LocalBasePath position
32    lbpos = struct.unpack('I', content[position:position+0x04])[0]
33    position = last_pos + lbpos
34    # read the string at the given position of the determined length
35    size= (length + last_pos) - position - 0x02
36    temp = struct.unpack('c' * size, content[position:position+size])
37    return ''.join([chr(ord(a)) for a in temp])
38
39def read_link(path):
40    try:
41        with open(path, 'rb') as stream:
42            content = stream.read()
43        return parse_link(content)
44    except Exception as e:
45        #log("error parsing '%s'", path, exc_info=True)
46        log("error parsing '%s': %s", path, e)
47        return None
48
49def listdir(d):
50    try:
51        return os.listdir(d)
52    except PermissionError as e:
53        log("listdir(%s)", d, exc_info=True)
54        log.warn("Warning: cannot access directory '%s':", d)
55        log.warn(" %s", e)
56        return ()
57
58def load_subdir(d):
59    #recurse down directories
60    #and return a dictionary of entries
61    menu = {}
62    for x in listdir(d):
63        if x.endswith(".ini"):
64            continue
65        name = os.path.join(d, x)
66        if os.path.isdir(name):
67            menu.update(load_subdir(name))
68        elif name.endswith(".lnk"):
69            exe = read_link(name)
70            if exe:
71                menu[x[:-4]] = {
72                    "command" : exe,
73                    }
74    return menu
75
76def load_dir(d):
77    log("load_dir(%s)", d)
78    menu = {}
79    for x in listdir(d):
80        log(" %s" % (x))
81        if x.endswith(".ini"):
82            continue
83        name = os.path.join(d, x)
84        if os.path.isdir(name):
85            subdirmenu = load_subdir(name)
86            if subdirmenu:
87                menu[x] = {
88                    "Entries" : subdirmenu,
89                    }
90        elif name.endswith(".lnk"):
91            #add them to the "Shortcuts" submenu:
92            exe = read_link(name)
93            if exe:
94                menu.setdefault("Shortcuts", {}).setdefault("Entries", {})[x[:-4]] = {
95                    "command" : exe,
96                    }
97    return menu
98
99def load_menu():
100    menu = {}
101    for d_fn in (get_common_startmenu_dir, get_startmenu_dir):
102        d = d_fn()
103        if not d:
104            continue
105        #ie: "C:\ProgramData\Microsoft\Windows\Start Menu"
106        for x in listdir(d):
107            subdir = os.path.join(d, x)
108            if os.path.isdir(subdir):
109                #ie: "C:\ProgramData\Microsoft\Windows\Start Menu\Programs"
110                m = load_dir(subdir)
111                if not m:
112                    continue
113                #TODO: recursive merge
114                for k,v in m.items():
115                    ev = menu.get(k)
116                    if isinstance(ev, dict):
117                        ev.update(v)
118                    else:
119                        menu[k] = v
120    return menu
121
122
123def main():
124    from xpra.platform import program_context
125    with program_context("menu-helper", "Menu Helper"):
126        menu = load_menu()
127        print_nested_dict(menu)
128
129
130if __name__ == "__main__":
131    main()
132