vim line
[drlaunch] / src / xdg / Mime.py
1 """
2 This module is based on a rox module (LGPL):
3
4 http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log
5
6 This module provides access to the shared MIME database.
7
8 types is a dictionary of all known MIME types, indexed by the type name, e.g.
9 types['application/x-python']
10
11 Applications can install information about MIME types by storing an
12 XML file as <MIME>/packages/<application>.xml and running the
13 update-mime-database command, which is provided by the freedesktop.org
14 shared mime database package.
15
16 See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
17 information about the format of these files.
18
19 (based on version 0.13)
20 """
21
22 import os
23 import stat
24 import fnmatch
25
26 import xdg.BaseDirectory
27 import xdg.Locale
28
29 from xml.dom import Node, minidom, XML_NAMESPACE
30
31 FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
32
33 types = {}      # Maps MIME names to type objects
34
35 exts = None     # Maps extensions to types
36 globs = None    # List of (glob, type) pairs
37 literals = None # Maps liternal names to types
38 magic = None
39
40 def _get_node_data(node):
41     """Get text of XML node"""
42     return ''.join([n.nodeValue for n in node.childNodes]).strip()
43
44 def lookup(media, subtype = None):
45     "Get the MIMEtype object for this type, creating a new one if needed."
46     if subtype is None and '/' in media:
47         media, subtype = media.split('/', 1)
48     if (media, subtype) not in types:
49         types[(media, subtype)] = MIMEtype(media, subtype)
50     return types[(media, subtype)]
51
52 class MIMEtype:
53     """Type holding data about a MIME type"""
54     def __init__(self, media, subtype):
55         "Don't use this constructor directly; use mime.lookup() instead."
56         assert media and '/' not in media
57         assert subtype and '/' not in subtype
58         assert (media, subtype) not in types
59
60         self.media = media
61         self.subtype = subtype
62         self._comment = None
63
64     def _load(self):
65         "Loads comment for current language. Use get_comment() instead."
66         resource = os.path.join('mime', self.media, self.subtype + '.xml')
67         for path in xdg.BaseDirectory.load_data_paths(resource):
68             doc = minidom.parse(path)
69             if doc is None:
70                 continue
71             for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
72                 lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
73                 goodness = 1 + (lang in xdg.Locale.langs)
74                 if goodness > self._comment[0]:
75                     self._comment = (goodness, _get_node_data(comment))
76                 if goodness == 2: return
77
78     # FIXME: add get_icon method
79     def get_comment(self):
80         """Returns comment for current language, loading it if needed."""
81         # Should we ever reload?
82         if self._comment is None:
83             self._comment = (0, str(self))
84             self._load()
85         return self._comment[1]
86
87     def __str__(self):
88         return self.media + '/' + self.subtype
89
90     def __repr__(self):
91         return '[%s: %s]' % (self, self._comment or '(comment not loaded)')
92
93 class MagicRule:
94     def __init__(self, f):
95         self.next=None
96         self.prev=None
97
98         #print line
99         ind=''
100         while True:
101             c=f.read(1)
102             if c=='>':
103                 break
104             ind+=c
105         if not ind:
106             self.nest=0
107         else:
108             self.nest=int(ind)
109
110         start=''
111         while True:
112             c=f.read(1)
113             if c=='=':
114                 break
115             start+=c
116         self.start=int(start)
117         
118         hb=f.read(1)
119         lb=f.read(1)
120         self.lenvalue=ord(lb)+(ord(hb)<<8)
121
122         self.value=f.read(self.lenvalue)
123
124         c=f.read(1)
125         if c=='&':
126             self.mask=f.read(self.lenvalue)
127             c=f.read(1)
128         else:
129             self.mask=None
130
131         if c=='~':
132             w=''
133             while c!='+' and c!='\n':
134                 c=f.read(1)
135                 if c=='+' or c=='\n':
136                     break
137                 w+=c
138             
139             self.word=int(w)
140         else:
141             self.word=1
142
143         if c=='+':
144             r=''
145             while c!='\n':
146                 c=f.read(1)
147                 if c=='\n':
148                     break
149                 r+=c
150             #print r
151             self.range=int(r)
152         else:
153             self.range=1
154
155         if c!='\n':
156             raise 'Malformed MIME magic line'
157
158     def getLength(self):
159         return self.start+self.lenvalue+self.range
160
161     def appendRule(self, rule):
162         if self.nest<rule.nest:
163             self.next=rule
164             rule.prev=self
165
166         elif self.prev:
167             self.prev.appendRule(rule)
168         
169     def match(self, buffer):
170         if self.match0(buffer):
171             if self.next:
172                 return self.next.match(buffer)
173             return True
174
175     def match0(self, buffer):
176         l=len(buffer)
177         for o in range(self.range):
178             s=self.start+o
179             e=s+self.lenvalue
180             if l<e:
181                 return False
182             if self.mask:
183                 test=''
184                 for i in range(self.lenvalue):
185                     c=ord(buffer[s+i]) & ord(self.mask[i])
186                     test+=chr(c)
187             else:
188                 test=buffer[s:e]
189
190             if test==self.value:
191                 return True
192
193     def __repr__(self):
194         return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self.nest,
195                                   self.start,
196                                   self.lenvalue,
197                                   `self.value`,
198                                   `self.mask`,
199                                   self.word,
200                                   self.range)
201
202 class MagicType:
203     def __init__(self, mtype):
204         self.mtype=mtype
205         self.top_rules=[]
206         self.last_rule=None
207
208     def getLine(self, f):
209         nrule=MagicRule(f)
210
211         if nrule.nest and self.last_rule:
212             self.last_rule.appendRule(nrule)
213         else:
214             self.top_rules.append(nrule)
215
216         self.last_rule=nrule
217
218         return nrule
219
220     def match(self, buffer):
221         for rule in self.top_rules:
222             if rule.match(buffer):
223                 return self.mtype
224
225     def __repr__(self):
226         return '<MagicType %s>' % self.mtype
227     
228 class MagicDB:
229     def __init__(self):
230         self.types={}   # Indexed by priority, each entry is a list of type rules
231         self.maxlen=0
232
233     def mergeFile(self, fname):
234         f=file(fname, 'r')
235         line=f.readline()
236         if line!='MIME-Magic\0\n':
237             raise 'Not a MIME magic file'
238
239         while True:
240             shead=f.readline()
241             #print shead
242             if not shead:
243                 break
244             if shead[0]!='[' or shead[-2:]!=']\n':
245                 raise 'Malformed section heading'
246             pri, tname=shead[1:-2].split(':')
247             #print shead[1:-2]
248             pri=int(pri)
249             mtype=lookup(tname)
250
251             try:
252                 ents=self.types[pri]
253             except:
254                 ents=[]
255                 self.types[pri]=ents
256
257             magictype=MagicType(mtype)
258             #print tname
259
260             #rline=f.readline()
261             c=f.read(1)
262             f.seek(-1, 1)
263             while c and c!='[':
264                 rule=magictype.getLine(f)
265                 #print rule
266                 if rule and rule.getLength()>self.maxlen:
267                     self.maxlen=rule.getLength()
268
269                 c=f.read(1)
270                 f.seek(-1, 1)
271
272             ents.append(magictype)
273             #self.types[pri]=ents
274             if not c:
275                 break
276
277     def match_data(self, data, max_pri=100, min_pri=0):
278         pris=self.types.keys()
279         pris.sort(lambda a, b: -cmp(a, b))
280         for pri in pris:
281             #print pri, max_pri, min_pri
282             if pri>max_pri:
283                 continue
284             if pri<min_pri:
285                 break
286             for type in self.types[pri]:
287                 m=type.match(data)
288                 if m:
289                     return m
290         
291
292     def match(self, path, max_pri=100, min_pri=0):
293         try:
294             buf=file(path, 'r').read(self.maxlen)
295             return self.match_data(buf, max_pri, min_pri)
296         except:
297             pass
298
299         return None
300     
301     def __repr__(self):
302         return '<MagicDB %s>' % self.types
303             
304
305 # Some well-known types
306 text = lookup('text', 'plain')
307 inode_block = lookup('inode', 'blockdevice')
308 inode_char = lookup('inode', 'chardevice')
309 inode_dir = lookup('inode', 'directory')
310 inode_fifo = lookup('inode', 'fifo')
311 inode_socket = lookup('inode', 'socket')
312 inode_symlink = lookup('inode', 'symlink')
313 inode_door = lookup('inode', 'door')
314 app_exe = lookup('application', 'executable')
315
316 _cache_uptodate = False
317
318 def _cache_database():
319     global exts, globs, literals, magic, _cache_uptodate
320
321     _cache_uptodate = True
322
323     exts = {}       # Maps extensions to types
324     globs = []      # List of (glob, type) pairs
325     literals = {}   # Maps liternal names to types
326     magic = MagicDB()
327
328     def _import_glob_file(path):
329         """Loads name matching information from a MIME directory."""
330         for line in file(path):
331             if line.startswith('#'): continue
332             line = line[:-1]
333
334             type_name, pattern = line.split(':', 1)
335             mtype = lookup(type_name)
336
337             if pattern.startswith('*.'):
338                 rest = pattern[2:]
339                 if not ('*' in rest or '[' in rest or '?' in rest):
340                     exts[rest] = mtype
341                     continue
342             if '*' in pattern or '[' in pattern or '?' in pattern:
343                 globs.append((pattern, mtype))
344             else:
345                 literals[pattern] = mtype
346
347     for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'globs')):
348         _import_glob_file(path)
349     for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
350         magic.mergeFile(path)
351
352     # Sort globs by length
353     globs.sort(lambda a, b: cmp(len(b[0]), len(a[0])))
354
355 def get_type_by_name(path):
356     """Returns type of file by its name, or None if not known"""
357     if not _cache_uptodate:
358         _cache_database()
359
360     leaf = os.path.basename(path)
361     if leaf in literals:
362         return literals[leaf]
363
364     lleaf = leaf.lower()
365     if lleaf in literals:
366         return literals[lleaf]
367
368     ext = leaf
369     while 1:
370         p = ext.find('.')
371         if p < 0: break
372         ext = ext[p + 1:]
373         if ext in exts:
374             return exts[ext]
375     ext = lleaf
376     while 1:
377         p = ext.find('.')
378         if p < 0: break
379         ext = ext[p+1:]
380         if ext in exts:
381             return exts[ext]
382     for (glob, mime_type) in globs:
383         if fnmatch.fnmatch(leaf, glob):
384             return mime_type
385         if fnmatch.fnmatch(lleaf, glob):
386             return mime_type
387     return None
388
389 def get_type_by_contents(path, max_pri=100, min_pri=0):
390     """Returns type of file by its contents, or None if not known"""
391     if not _cache_uptodate:
392         _cache_database()
393
394     return magic.match(path, max_pri, min_pri)
395
396 def get_type_by_data(data, max_pri=100, min_pri=0):
397     """Returns type of the data"""
398     if not _cache_uptodate:
399         _cache_database()
400
401     return magic.match_data(data, max_pri, min_pri)
402
403 def get_type(path, follow=1, name_pri=100):
404     """Returns type of file indicated by path.
405     path     - pathname to check (need not exist)
406     follow   - when reading file, follow symbolic links
407     name_pri - Priority to do name matches.  100=override magic"""
408     if not _cache_uptodate:
409         _cache_database()
410     
411     try:
412         if follow:
413             st = os.stat(path)
414         else:
415             st = os.lstat(path)
416     except:
417         t = get_type_by_name(path)
418         return t or text
419
420     if stat.S_ISREG(st.st_mode):
421         t = get_type_by_contents(path, min_pri=name_pri)
422         if not t: t = get_type_by_name(path)
423         if not t: t = get_type_by_contents(path, max_pri=name_pri)
424         if t is None:
425             if stat.S_IMODE(st.st_mode) & 0111:
426                 return app_exe
427             else:
428                 return text
429         return t
430     elif stat.S_ISDIR(st.st_mode): return inode_dir
431     elif stat.S_ISCHR(st.st_mode): return inode_char
432     elif stat.S_ISBLK(st.st_mode): return inode_block
433     elif stat.S_ISFIFO(st.st_mode): return inode_fifo
434     elif stat.S_ISLNK(st.st_mode): return inode_symlink
435     elif stat.S_ISSOCK(st.st_mode): return inode_socket
436     return inode_door
437
438 def install_mime_info(application, package_file):
439     """Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
440     If package_file is None, install <app_dir>/<application>.xml.
441     If already installed, does nothing. May overwrite an existing
442     file with the same name (if the contents are different)"""
443     application += '.xml'
444
445     new_data = file(package_file).read()
446
447     # See if the file is already installed
448     package_dir = os.path.join('mime', 'packages')
449     resource = os.path.join(package_dir, application)
450     for x in xdg.BaseDirectory.load_data_paths(resource):
451         try:
452             old_data = file(x).read()
453         except:
454             continue
455         if old_data == new_data:
456             return  # Already installed
457
458     global _cache_uptodate
459     _cache_uptodate = False
460
461     # Not already installed; add a new copy
462     # Create the directory structure...
463     new_file = os.path.join(xdg.BaseDirectory.save_data_path(package_dir), application)
464
465     # Write the file...
466     file(new_file, 'w').write(new_data)
467
468     # Update the database...
469     command = 'update-mime-database'
470     if os.spawnlp(os.P_WAIT, command, command, xdg.BaseDirectory.save_data_path('mime')):
471         os.unlink(new_file)
472         raise Exception("The '%s' command returned an error code!\n" \
473                   "Make sure you have the freedesktop.org shared MIME package:\n" \
474                   "http://standards.freedesktop.org/shared-mime-info/") % command