1from xdg import Mime
2import unittest
3import os.path
4import tempfile, shutil
5
6import resources
7
8example_dir = os.path.join(os.path.dirname(__file__), 'example')
9def example_file(filename):
10    return os.path.join(example_dir, filename)
11
12class MimeTestBase(unittest.TestCase):
13    def check_mimetype(self, mimetype, media, subtype):
14        self.assertEqual(mimetype.media, media)
15        self.assertEqual(mimetype.subtype, subtype)
16
17class MimeTest(MimeTestBase):
18    def test_create_mimetype(self):
19        mt1 = Mime.MIMEtype('application', 'pdf')
20        mt2 = Mime.MIMEtype('application', 'pdf')
21        self.assertEqual(id(mt1), id(mt2))  # Check caching
22
23        amr = Mime.MIMEtype('audio', 'AMR')
24        self.check_mimetype(amr, 'audio', 'amr')  # Check lowercase
25
26        ogg = Mime.MIMEtype('audio/ogg')
27        self.check_mimetype(ogg, 'audio', 'ogg')  # Check split on /
28
29        self.assertRaises(Exception, Mime.MIMEtype, 'audio/foo/bar')
30
31    def test_get_type_by_name(self):
32        appzip = Mime.get_type_by_name("foo.zip")
33        self.check_mimetype(appzip, 'application', 'zip')
34
35    def test_get_type_by_data(self):
36        imgpng = Mime.get_type_by_data(resources.png_data)
37        self.check_mimetype(imgpng, 'image', 'png')
38
39    def test_mimetype_repr(self):
40        mt = Mime.lookup('application', 'zip')
41        repr(mt)   # Just check that this doesn't throw an error.
42
43    def test_get_type_by_contents(self):
44        tmpdir = tempfile.mkdtemp()
45        try:
46            test_file = os.path.join(tmpdir, "test")
47            with open(test_file, "wb") as f:
48                f.write(resources.png_data)
49
50            imgpng = Mime.get_type_by_contents(test_file)
51            self.check_mimetype(imgpng, 'image', 'png')
52
53        finally:
54            shutil.rmtree(tmpdir)
55
56    def test_get_type(self):
57        # File that doesn't exist - get type by name
58        imgpng = Mime.get_type(example_file("test.gif"))
59        self.check_mimetype(imgpng, 'image', 'gif')
60
61        # File that does exist - get type by contents
62        imgpng = Mime.get_type(example_file("png_file"))
63        self.check_mimetype(imgpng, 'image', 'png')
64
65        # Directory - special case
66        inodedir = Mime.get_type(example_file("subdir"))
67        self.check_mimetype(inodedir, 'inode', 'directory')
68
69        # Mystery files
70        mystery_text = Mime.get_type(example_file('mystery_text'))
71        self.check_mimetype(mystery_text, 'text', 'plain')
72        mystery_exe = Mime.get_type(example_file('mystery_exe'))
73        self.check_mimetype(mystery_exe, 'application', 'executable')
74
75        # Symlink
76        self.check_mimetype(Mime.get_type(example_file("png_symlink")),
77                                    'image', 'png')
78        self.check_mimetype(Mime.get_type(example_file("png_symlink"), follow=False),
79                                    'inode', 'symlink')
80
81    def test_get_type2(self):
82        # File that doesn't exist - use the name
83        self.check_mimetype(Mime.get_type2(example_file('test.gif')), 'image', 'gif')
84
85        # File that does exist - use the contents
86        self.check_mimetype(Mime.get_type2(example_file('png_file')), 'image', 'png')
87
88        # Does exist - use name before contents
89        self.check_mimetype(Mime.get_type2(example_file('file.png')), 'image', 'png')
90        self.check_mimetype(Mime.get_type2(example_file('word.doc')), 'application', 'msword')
91
92        # Ambiguous file extension
93        glade_mime = Mime.get_type2(example_file('glade.ui'))
94        self.assertEqual(glade_mime.media, 'application')
95        # Grumble, this is still ambiguous on some systems
96        self.assertIn(glade_mime.subtype, {'x-gtk-builder', 'x-glade'})
97        self.check_mimetype(Mime.get_type2(example_file('qtdesigner.ui')), 'application', 'x-designer')
98
99        # text/x-python has greater weight than text/x-readme
100        self.check_mimetype(Mime.get_type2(example_file('README.py')), 'text', 'x-python')
101
102        # Directory - special filesystem object
103        self.check_mimetype(Mime.get_type2(example_file('subdir')), 'inode', 'directory')
104
105        # Mystery files:
106        mystery_missing = Mime.get_type2(example_file('mystery_missing'))
107        self.check_mimetype(mystery_missing, 'application', 'octet-stream')
108        mystery_binary = Mime.get_type2(example_file('mystery_binary'))
109        self.check_mimetype(mystery_binary, 'application', 'octet-stream')
110        mystery_text = Mime.get_type2(example_file('mystery_text'))
111        self.check_mimetype(mystery_text, 'text', 'plain')
112        mystery_exe = Mime.get_type2(example_file('mystery_exe'))
113        self.check_mimetype(mystery_exe, 'application', 'executable')
114
115        # Symlink
116        self.check_mimetype(Mime.get_type2(example_file("png_symlink")),
117                                    'image', 'png')
118        self.check_mimetype(Mime.get_type2(example_file("png_symlink"), follow=False),
119                                    'inode', 'symlink')
120
121    def test_lookup(self):
122        pdf1 = Mime.lookup("application/pdf")
123        pdf2 = Mime.lookup("application", "pdf")
124        self.assertEqual(pdf1, pdf2)
125        self.check_mimetype(pdf1, 'application', 'pdf')
126
127    def test_get_comment(self):
128        # Check these don't throw an error. One that is likely to exist:
129        Mime.MIMEtype("application", "pdf").get_comment()
130        # And one that's unlikely to exist:
131        Mime.MIMEtype("application", "ierjg").get_comment()
132
133    def test_by_name(self):
134        dot_c = Mime.get_type_by_name('foo.c')
135        self.check_mimetype(dot_c, 'text', 'x-csrc')
136        dot_C = Mime.get_type_by_name('foo.C')
137        self.check_mimetype(dot_C, 'text', 'x-c++src')
138
139        # But most names should be case insensitive
140        dot_GIF = Mime.get_type_by_name('IMAGE.GIF')
141        self.check_mimetype(dot_GIF, 'image', 'gif')
142
143    def test_canonical(self):
144        text_xml = Mime.lookup('text/xml')
145        self.check_mimetype(text_xml, 'text', 'xml')
146        self.check_mimetype(text_xml.canonical(), 'application', 'xml')
147
148        # Already is canonical
149        python = Mime.lookup('text/x-python')
150        self.check_mimetype(python.canonical(), 'text', 'x-python')
151
152    def test_inheritance(self):
153        text_python = Mime.lookup('text/x-python')
154        self.check_mimetype(text_python, 'text', 'x-python')
155        text_plain = Mime.lookup('text/plain')
156        app_executable = Mime.lookup('application/x-executable')
157        self.assertEqual(text_python.inherits_from(), set([text_plain, app_executable]))
158
159    def test_is_text(self):
160        assert Mime._is_text(b'abcdef \n')
161        assert not Mime._is_text(b'abcdef\x08')
162        assert not Mime._is_text(b'abcdef\x0e')
163        assert not Mime._is_text(b'abcdef\x1f')
164        assert not Mime._is_text(b'abcdef\x7f')
165
166        # Check nonexistant file.
167        assert not Mime.is_text_file('/fwoijorij')
168
169class MagicDBTest(MimeTestBase):
170    def setUp(self):
171        self.tmpdir = tempfile.mkdtemp()
172        self.path = os.path.join(self.tmpdir, 'mimemagic')
173        with open(self.path, 'wb') as f:
174            f.write(resources.mime_magic_db)
175
176        self.path2 = os.path.join(self.tmpdir, 'mimemagic2')
177        with open(self.path2, 'wb') as f:
178            f.write(resources.mime_magic_db2)
179
180        # Read the files
181        self.magic = Mime.MagicDB()
182        self.magic.merge_file(self.path)
183        self.magic.merge_file(self.path2)
184        self.magic.finalise()
185
186    def tearDown(self):
187        shutil.rmtree(self.tmpdir)
188
189    def test_parsing(self):
190        self.assertEqual(len(self.magic.bytype), 9)
191
192        # Check repr() doesn't throw an error
193        repr(self.magic)
194
195        prio, png = self.magic.bytype[Mime.lookup('image', 'png')][0]
196        self.assertEqual(prio, 50)
197        assert isinstance(png, Mime.MagicRule), type(png)
198        repr(png)    # Check this doesn't throw an error.
199        self.assertEqual(png.start, 0)
200        self.assertEqual(png.value, b'\x89PNG')
201        self.assertEqual(png.mask, None)
202        self.assertEqual(png.also, None)
203
204        prio, jpeg = self.magic.bytype[Mime.lookup('image', 'jpeg')][0]
205        assert isinstance(jpeg, Mime.MagicMatchAny), type(jpeg)
206        self.assertEqual(len(jpeg.rules), 2)
207        self.assertEqual(jpeg.rules[0].value, b'\xff\xd8\xff')
208
209        prio, ora = self.magic.bytype[Mime.lookup('image', 'openraster')][0]
210        assert isinstance(ora, Mime.MagicRule), type(ora)
211        self.assertEqual(ora.value, b'PK\x03\x04')
212        ora1 = ora.also
213        assert ora1 is not None
214        self.assertEqual(ora1.start, 30)
215        ora2 = ora1.also
216        assert ora2 is not None
217        self.assertEqual(ora2.start, 38)
218        self.assertEqual(ora2.value, b'image/openraster')
219
220        prio, svg = self.magic.bytype[Mime.lookup('image', 'svg+xml')][0]
221        self.assertEqual(len(svg.rules), 2)
222        self.assertEqual(svg.rules[0].value, b'<!DOCTYPE svg')
223        self.assertEqual(svg.rules[0].range, 257)
224
225        prio, psd = self.magic.bytype[Mime.lookup('image', 'vnd.adobe.photoshop')][0]
226        self.assertEqual(psd.value, b'8BPS  \0\0\0\0')
227        self.assertEqual(psd.mask, b'\xff\xff\xff\xff\0\0\xff\xff\xff\xff')
228
229        prio, elf = self.magic.bytype[Mime.lookup('application', 'x-executable')][0]
230        self.assertEqual(elf.value, b'\x01\x11')
231        self.assertEqual(elf.word, 2)
232
233        # Test that a newline within the value doesn't break parsing.
234        prio, madeup = self.magic.bytype[Mime.lookup('application', 'madeup')][0]
235        self.assertEqual(madeup.rules[0].value, b'ab\ncd')
236        self.assertEqual(madeup.rules[1].mask, b'\xff\xff\n\xff\xff')
237
238        prio, replaced = self.magic.bytype[Mime.lookup('application', 'tobereplaced')][0]
239        self.assertEqual(replaced.value, b'jkl')
240
241        addedrules = self.magic.bytype[Mime.lookup('application', 'tobeaddedto')]
242        self.assertEqual(len(addedrules), 2)
243        self.assertEqual(addedrules[1][1].value, b'pqr')
244
245    def test_match_data(self):
246        res = self.magic.match_data(resources.png_data)
247        self.check_mimetype(res, 'image', 'png')
248
249        # Denied by min or max priority
250        notpng_max40 = self.magic.match_data(resources.png_data, max_pri=40)
251        assert notpng_max40 is None, notpng_max40
252        notpng_min60 = self.magic.match_data(resources.png_data, min_pri=60)
253        assert notpng_min60 is None, notpng_min60
254
255        # With list of options
256        options = [Mime.lookup('image', 'nonexistant'), # Missing MIMEtype should be dropped
257                   Mime.lookup('image','png'), Mime.lookup('image', 'jpeg')]
258        res = self.magic.match_data(resources.png_data, possible=options)
259        self.check_mimetype(res, 'image', 'png')
260
261        # Non matching
262        res = self.magic.match_data(b'oiejgoethetrkjgnwefergoijekngjekg')
263        assert res is None, res
264
265    def test_match_nested(self):
266        data = b'PK\x03\x04' + (b' ' * 26) + b'mimetype' + b'image/openraster'
267        res = self.magic.match_data(data)
268        self.check_mimetype(res, 'image', 'openraster')
269
270    def test_match_file(self):
271        png_file = os.path.join(self.tmpdir, 'image')
272        with open(png_file, 'wb') as f:
273            f.write(resources.png_data)
274
275        res = self.magic.match(png_file)
276        self.check_mimetype(res, 'image', 'png')
277
278        # With list of options
279        options = [Mime.lookup('image','png'), Mime.lookup('image', 'jpeg'),
280                   Mime.lookup('image', 'nonexistant')]  # Missing MIMEtype should be dropped
281        res = self.magic.match(png_file, possible=options)
282        self.check_mimetype(res, 'image', 'png')
283
284        # Nonexistant file
285        path = os.path.join(self.tmpdir, 'nonexistant')
286        self.assertRaises(IOError, self.magic.match, path)
287
288_l = Mime.lookup
289
290class GlobDBTest(MimeTestBase):
291    allglobs = {_l('text/x-makefile'): [(50, 'makefile', [])],
292                _l('application/x-core'): [(50, 'core', ['cs']), (50, 'core', [])],
293                _l('text/x-c++src'): [(50, '*.C', ['cs'])],
294                _l('text/x-csrc'): [(50, '*.c', ['cs'])],
295                _l('text/x-python'): [(50, '*.py', [])],
296                _l('text/x-python'): [(50, '*.py', [])],    # Check not added 2x
297                _l('video/x-anim'): [(50, '*.anim[1-9j]', [])],
298                _l('text/x-readme'): [(10, 'readme*', [])],
299                _l('text/x-readme2'): [(20, 'readme2*', [])],
300                _l('image/jpeg'): [(50, '*.jpg', []), (50, '*.jpeg', [])],
301               }
302
303    def setUp(self):
304        self.globs = Mime.GlobDB()
305        self.globs.allglobs = self.allglobs
306        self.globs.finalise()
307
308    def test_build_globdb(self):
309        globs = self.globs
310
311        self.assertEqual(len(globs.cased_literals), 1)
312        assert 'core' in globs.cased_literals, globs.cased_literals
313
314        literals = globs.literals
315        self.assertEqual(len(literals), 2)
316        assert 'core' in literals, literals
317        assert 'makefile' in literals, literals
318
319        cexts = globs.cased_exts
320        self.assertEqual(len(cexts), 2)
321        assert 'C' in cexts, cexts
322        assert 'c' in cexts, cexts
323
324        exts = globs.exts
325        self.assertEqual(len(exts), 3)
326        assert 'py' in exts, exts
327        self.assertEqual(exts['py'], [(_l('text/x-python'), 50)] )
328        assert 'jpeg' in exts, exts
329        assert 'jpg' in exts, exts
330
331        pats = globs.globs
332        self.assertEqual(len(pats), 3)
333        self.assertEqual(pats[0][1], _l('video', 'x-anim'))
334        self.assertEqual(pats[1][1], _l('text', 'x-readme2'))
335
336    def test_first_match(self):
337        g = self.globs
338
339        self.check_mimetype(g.first_match('Makefile'), 'text', 'x-makefile')
340        self.check_mimetype(g.first_match('core'), 'application', 'x-core')
341        self.check_mimetype(g.first_match('foo.C'), 'text', 'x-c++src')
342        self.check_mimetype(g.first_match('foo.c'), 'text', 'x-csrc')
343        self.check_mimetype(g.first_match('foo.py'), 'text', 'x-python')
344        self.check_mimetype(g.first_match('foo.Anim4'), 'video', 'x-anim')
345        self.check_mimetype(g.first_match('README.txt'), 'text', 'x-readme')
346        self.check_mimetype(g.first_match('README2.txt'), 'text', 'x-readme2')
347        self.check_mimetype(g.first_match('README'), 'text', 'x-readme')
348
349        qrte = g.first_match('qrte')
350        assert qrte is None, qrte
351
352    def test_all_matches(self):
353        g = self.globs
354
355        self.assertEqual(g.all_matches('qrte'), [])
356
357        self.assertEqual(g.all_matches('Makefile'),
358                                    [(_l('text', 'x-makefile'), 50)])
359
360        self.assertEqual(g.all_matches('readme2.rst'),
361                                [(_l('text', 'x-readme2'), 20),
362                                 (_l('text', 'x-readme'), 10)]
363                                 )
364
365    def test_get_extensions(self):
366        Mime.globs = self.globs
367        Mime._cache_uptodate = True
368
369        try:
370            get_ext = Mime.get_extensions
371            self.assertEqual(get_ext(_l('text/x-python')), set(['py']))
372            self.assertEqual(get_ext(_l('image/jpeg')), set(['jpg', 'jpeg']))
373            self.assertEqual(get_ext(_l('image/inary')), set())
374        finally:
375            # Ensure that future tests will re-cache the database.
376            Mime._cache_uptodate = False
377
378
379class GlobsParsingTest(MimeTestBase):
380    def setUp(self):
381        self.tmpdir = tempfile.mkdtemp()
382
383    def tearDown(self):
384        shutil.rmtree(self.tmpdir)
385
386    def test_parsing(self):
387        p1 = os.path.join(self.tmpdir, 'globs2a')
388        with open(p1, 'w') as f:
389            f.write(resources.mime_globs2_a)
390
391        p2 = os.path.join(self.tmpdir, 'globs2b')
392        with open(p2, 'w') as f:
393            f.write(resources.mime_globs2_b)
394
395        globs = Mime.GlobDB()
396        globs.merge_file(p1)
397        globs.merge_file(p2)
398
399        ag = globs.allglobs
400        self.assertEqual(ag[_l('text', 'x-diff')],
401                                set([(55, '*.patch', ()), (50, '*.diff', ())]) )
402        self.assertEqual(ag[_l('text', 'x-c++src')], set([(50, '*.C', ('cs',))]) )
403        self.assertEqual(ag[_l('text', 'x-readme')], set([(20, 'RDME', ('cs',))]) )
404        assert _l('text', 'x-python') not in ag, ag
405