1import re
2import os.path
3import glob
4import fnmatch
5import codecs
6import Prophet
7
8from Paths import ValidPathSet as PS
9from Keywords import Keyword as Kw, Set as KwS
10from Prophet import NotSet
11from Prophet.Categories import *
12from Prophet import msg, verbose
13
14
15class DebianSet(PS):
16
17    def adopt(self, path):
18        path = super(DebianSet, self).adopt(path)
19        if len(glob.glob(os.path.join(path, "*"))):
20            return path  # Directory should not be empty
21        raise ValueError
22
23
24dirs = DebianSet(("/etc/menu", "/usr/lib/menu",
25                  "/usr/share/menu", "/usr/share/menu/default", "~/.menu"))
26
27# If dirs isn't empty then assume we're running Debian and therefore it's reasonable
28# to use local menu entries instead of cached ones
29if not len(dirs):
30    dirs = DebianSet((os.path.join(__path__[0], "menu"),))
31    runDebian = False
32
33else:
34    runDebian = True
35
36
37def scan():
38    """Scan through all containers and return a list of found valid entries"""
39    result = []
40    msg("  debian...", newline=False)
41    for d in dirs:
42        for f in glob.glob(os.path.join(d, "*")):
43            if verbose > 1:
44                msg("\nparsing " + f)
45            try:
46                entries = _parse(f)
47            except NotSet as e:
48                if verbose > 1:
49                    msg("REJECTED : " + str(e))
50            else:
51                for e in entries:
52                    try:
53                        result.append(App(e))
54                    except Prophet.NotSet as e:
55                        if verbose > 1:
56                            msg("REJECTED : " + str(e))
57
58    msg(" %d apps found" % len(result))
59    return result
60
61
62def _parse(debmenu):
63    try:
64        # Latest Py3 versions in 64-bit mode sometimes have problems decoding Debian menus
65        # complaining about malformed utf-8 codes, therefore one has to enforce the 8-bit encoding.
66        # encoding= is not supported by Py2, so use codecs.open() as the least
67        # common denominator.
68        file = codecs.open(debmenu, "r", "latin1")  # Py 2.4+
69    except IOError:
70        raise NotSet("couldn't read the file " + debmenu)
71    entry = ""
72    entries = []
73    next = False
74    try:
75        for x in file:
76            x = x.strip()
77            if next:
78                if not len(x):
79                    next = False
80                    continue
81                if not x.startswith("#"):
82                    if x.endswith("\\"):
83                        entry += " " + x.rstrip("\\")
84                        next = True
85                    else:
86                        entry += " " + x
87                        next = False
88
89            else:
90                if not len(x):
91                    continue
92                if not x.startswith("#") and x.startswith("?"):
93                    if len(entry):
94                        entries += _parseEntry(entry)
95                    if x.endswith("\\"):
96                        entry = x.rstrip("\\")
97                        next = True
98                    else:
99                        entry = x
100                        next = False
101
102    except IOError:
103        raise NotSet("i/o error while reading the file " + debmenu)
104    except StopIteration:
105        pass
106    if len(entry):
107        entries += _parseEntry(entry)
108    return entries
109
110_package = re.compile('^\?\s*?package\s*?\((.*?)\)\s*?\:')
111_key = re.compile('(\w+)\s*=\s*(".*?"|\S+)')
112
113
114def _parseEntry(s):
115    entry = {}
116    x = _package.match(s)
117    if x:
118        entry["package"] = x.group(1)
119        for x in re.findall(_key, s):
120            entry[x[0]] = x[1].strip('"')
121
122    return [entry]
123
124
125class App(Prophet.App):
126
127    pref = 10
128
129    def __new__(cls, entry):
130        self = object.__new__(cls)
131        self.__setup__(entry)
132        return self
133
134    def __setup__(self, entry):
135        self.entry = entry
136        super(App, self).__setup__()
137        del self.entry
138        # TODO : filter out GNOME/KDE apps since they should be picked up by the .desktop scanner anyways
139        # To accomplish this scan title/longtitle/description/whatever for the
140        # clues (gnome-*, "Calculator for GNOME", etc.)
141
142    def setName(self):
143        try:
144            self.name = self.entry["title"]
145        except KeyError:
146            super(App, self).setName()
147
148    def setComment(self):
149        try:
150            self.comment = self.entry["longtitle"]
151        except KeyError:
152            try:
153                self.comment = self.entry["description"]
154            except KeyError:
155                super(App, self).setComment()
156
157    def setKeywords(self):
158        try:
159            sect = self.entry["section"]
160        except KeyError:
161            sect = ""
162        try:
163            hint = self.entry["hints"]
164        except KeyError:
165            hint = ""
166        kws = _deb2kws(sect, hint)
167        if not len(kws):
168            raise Prophet.NotSet("no keywords guessed")
169        self.keywords = kws
170
171    def setTerminal(self):
172        try:
173            needs = Kw(self.entry["needs"])
174            if needs == Kw("X11"):
175                self.terminal = False
176            elif needs == Kw("text"):
177                self.terminal = True
178            else:
179                raise Prophet.NotSet("unsupported needs entry (%s)" % needs)
180
181        except KeyError:
182            super(App, self).setTerminal()
183
184    def setExename(self):
185        try:
186            cmd = self.entry["command"]
187        except KeyError:
188            raise Prophet.NotSet("no command entry found")
189        self.testGoodness(cmd)
190        scmd = cmd.split(" ", 1)
191        if len(scmd) >= 2:
192            cmd = scmd[0].strip()
193            args = scmd[1].strip()
194            for x in args.split():
195                if fnmatch.fnmatch(x, "*/*/*") and not os.path.exists(x):
196                    # An argument specified in the command line looks like a path. If it
197                    # doesn't exist, the entire command is likely to be
198                    # worthless, so throw it off
199                    raise Prophet.NotSet(
200                        "nonexistent path %s as command argument")
201
202            self.exeargs = args
203        if not runDebian:
204            # If we're not running Debian specified command may be located
205            # elsewhere therefore we have to scan PATH for it
206            cmd = os.path.basename(cmd)
207            # List of globs of unwanted commands
208            if cmd in (
209                    "sh",
210                    "bash",
211                    "csh",
212                    "tcsh",
213                    "zsh",
214                    "ls",
215                    "killall",
216                    "echo",
217                    "nice",
218                    "xmessage",
219                    "xlock"):
220                # Debian seems to have lots of shell commands. While it would be nice
221                # to try to analyze them, this isn't a priority since they're often
222                # too Debian-specific.
223                raise Prophet.NotSet("%s blacklisted" % cmd)
224
225        self.exename = cmd
226
227
228_secs = {
229    Kw("Appearance"): (Settings, DesktopSettings),
230    Kw("AI"): (Science,),
231    Kw("Databases"): (Database, Office),
232    Kw("Editors"): (TextEditor, Utility),
233    Kw("Educational"): (Education,),
234    Kw("Emulators"): (Emulator,),
235    Kw("Games"): (Game,),
236    Kw("Adventure"): (AdventureGame, Game),
237    Kw("Arcade"): (ArcadeGame, Game),
238    Kw("Board"): (BoardGame, Game),
239    Kw("Card"): (CardGame, Game),
240    Kw("Puzzles"): (LogicGame, Game),
241    Kw("Simulation"): (Simulation, Game),
242    Kw("Sports"): (SportsGame, Game),
243    Kw("Strategy"): (StrategyGame, Game),
244    Kw("Tetris-like"): (BlocksGame, Game),
245    Kw("Graphics"): (Graphics,),
246    Kw("Hamradio"): (HamRadio, Network),
247    Kw("Math"): (Math, Science),
248    Kw("Misc"): (Utility,),  # ???
249    Kw("Net"): (Network,),
250    Kw("Analog"): (Engineering, Network),
251    Kw("Headlines"): (News, Network),
252    Kw("Programming"): (Development,),
253    Kw("Shells"): (Shell, ConsoleOnly),  # ???
254    Kw("Sound"): (Audio, AudioVideo),
255    Kw("System"): (System,),
256    Kw("Admin"): (Settings, System),
257
258    #    Kw("GNOME") : (GNOME, GTK),
259    Kw("Technical"): (Engineering,),
260    Kw("Text"): (TextEditor, Utility),
261    Kw("Tools"): (Utility,),
262    Kw("Viewers"): (Viewer, Graphics),
263    Kw("XawTV"): (TV, Viewer, Video, AudioVideo),
264    Kw("Toys"): (Amusement,),
265    Kw("Help"): (Utility, Kw("X-Help")),
266    Kw("Screen"): (Utility,),  # ???
267
268    #    Kw("Lock") : (Utility, Kw("X-Lock")),
269    Kw("Saver"): (Screensaver, Utility),
270    Kw("System"): (System,),
271    Kw("XShells"): (Shell,)
272}
273
274_hints = {
275    Kw("3D"): (ActionGame, Game),  # ???
276    Kw("Bitmap"): (RasterGraphics, x2DGraphics, Graphics),
277    Kw("Boot"): (System, Utility),
278    Kw("Bug reporting"): (Development,),
279    Kw("Calculators"): (Calculator, Utility),
280    Kw("Clocks"): (Clock, Utility),
281    Kw("Config"): (Settings,),
282    Kw("Debuggers"): (Debugger, Development),
283    Kw("Documents"): (Office,),
284    Kw("Doom"): (ActionGame, Game),
285    Kw("Drawing"): (RasterGraphics, x2DGraphics, Graphics),
286    Kw("Vector"): (VectorGraphics, Graphics),
287    Kw("Equation"): (Math, Science),
288    Kw("Editor"): (WordProcessor, Office),
289    Kw("Formula"): (Presentation, Office),
290    Kw("Fonts"): (DesktopSettings, Settings),
291    Kw("Mail"): (Email, Office, Network),
292    Kw("Calendar"): (Calendar, Office),
293    Kw("Monitoring"): (Monitor, System),
294    Kw("IRC"): (IRCClient, Network),
295    Kw("ISDN"): (Telephony, Dialup, Network),  # ???
296    Kw("Images"): (x2DGraphics, Graphics),
297    Kw("Internet"): (Network,),
298    Kw("HTML"): (Network,),
299    Kw("Mahjongg"): (LogicGame, Game),
300    Kw("Mines"): (LogicGame, Game),
301    Kw("Mixers"): (Mixer, Audio),
302    Kw("Real-time"): (StrategyGame, Game),
303    Kw("Parallel"): (Math, Science, Network),
304    Kw("Distributed"): (Math, Science, Network),
305    Kw("PostScript"): (Presentation, Office),
306    Kw("Presentation"): (Presentation, Office),
307    Kw("SameGame"): (LogicGame, Game),
308    Kw("Screenshot"): (Graphics, Utility),
309    Kw("Setup"): (System,),
310    Kw("Install"): (System,),
311    Kw("Config"): (Settings,),
312    Kw("Spreadsheets"): (Spreadsheet, Office),
313    Kw("Terminal"): (TerminalEmulator),
314    Kw("Translation"): (Languages, Education),
315    Kw("Dictionary"): (Languages, Education),
316    Kw("Time"): (Clock, Utility),
317    Kw("Users"): (System, Settings),
318    Kw("VNC"): (Network,),
319    Kw("Video"): (Video, AudioVideo),
320    Kw("Web Browsers"): (WebBrowser, Network),
321    Kw("Word Processors"): (WordProcessor, Office)
322}
323
324_rejs = (
325    Kw("Modules"), Kw("WindowManagers"), Kw(
326        "WorkSpace"), Kw("Lock"), Kw("GNOME")
327)
328
329
330def _deb2kws(sec, hint):
331    """Convert Debian style section/hints entries to an equivalent keyword set"""
332    kws = []
333    for x in sec.split("/"):
334        x = Kw(x)
335        if x in _rejs:
336            raise Prophet.NotSet("section %s rejected" % x)
337        try:
338            kws += _secs[x]
339        except KeyError:
340            pass
341
342    for x in hint.split(","):
343        x = Kw(x)
344        try:
345            kws += _hints[x]
346        except KeyError:
347            pass
348
349    return KwS(*kws)
350