cpython/Lib/test/test_mimetypes.py

import io
import mimetypes
import os
import sys
import unittest.mock

from test import support
from test.support import os_helper
from platform import win32_edition

try:
    import _winapi
except ImportError:
    _winapi = None


def setUpModule():
    global knownfiles
    knownfiles = mimetypes.knownfiles

    # Tell it we don't know about external files:
    mimetypes.knownfiles = []
    mimetypes.inited = False
    mimetypes._default_mime_types()


def tearDownModule():
    # Restore knownfiles to its initial state
    mimetypes.knownfiles = knownfiles


class MimeTypesTestCase(unittest.TestCase):
    def setUp(self):
        self.db = mimetypes.MimeTypes()

    def test_case_sensitivity(self):
        eq = self.assertEqual
        eq(self.db.guess_file_type("foobar.html"), ("text/html", None))
        eq(self.db.guess_type("scheme:foobar.html"), ("text/html", None))
        eq(self.db.guess_file_type("foobar.HTML"), ("text/html", None))
        eq(self.db.guess_type("scheme:foobar.HTML"), ("text/html", None))
        eq(self.db.guess_file_type("foobar.tgz"), ("application/x-tar", "gzip"))
        eq(self.db.guess_type("scheme:foobar.tgz"), ("application/x-tar", "gzip"))
        eq(self.db.guess_file_type("foobar.TGZ"), ("application/x-tar", "gzip"))
        eq(self.db.guess_type("scheme:foobar.TGZ"), ("application/x-tar", "gzip"))
        eq(self.db.guess_file_type("foobar.tar.Z"), ("application/x-tar", "compress"))
        eq(self.db.guess_type("scheme:foobar.tar.Z"), ("application/x-tar", "compress"))
        eq(self.db.guess_file_type("foobar.tar.z"), (None, None))
        eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None))

    def test_default_data(self):
        eq = self.assertEqual
        eq(self.db.guess_file_type("foo.html"), ("text/html", None))
        eq(self.db.guess_file_type("foo.HTML"), ("text/html", None))
        eq(self.db.guess_file_type("foo.tgz"), ("application/x-tar", "gzip"))
        eq(self.db.guess_file_type("foo.tar.gz"), ("application/x-tar", "gzip"))
        eq(self.db.guess_file_type("foo.tar.Z"), ("application/x-tar", "compress"))
        eq(self.db.guess_file_type("foo.tar.bz2"), ("application/x-tar", "bzip2"))
        eq(self.db.guess_file_type("foo.tar.xz"), ("application/x-tar", "xz"))

    def test_data_urls(self):
        eq = self.assertEqual
        guess_type = self.db.guess_type
        eq(guess_type("data:invalidDataWithoutComma"), (None, None))
        eq(guess_type("data:,thisIsTextPlain"), ("text/plain", None))
        eq(guess_type("data:;base64,thisIsTextPlain"), ("text/plain", None))
        eq(guess_type("data:text/x-foo,thisIsTextXFoo"), ("text/x-foo", None))

    def test_file_parsing(self):
        eq = self.assertEqual
        sio = io.StringIO("x-application/x-unittest pyunit\n")
        self.db.readfp(sio)
        eq(self.db.guess_file_type("foo.pyunit"),
           ("x-application/x-unittest", None))
        eq(self.db.guess_extension("x-application/x-unittest"), ".pyunit")

    def test_read_mime_types(self):
        eq = self.assertEqual

        # Unreadable file returns None
        self.assertIsNone(mimetypes.read_mime_types("non-existent"))

        with os_helper.temp_dir() as directory:
            data = "x-application/x-unittest pyunit\n"
            file = os.path.join(directory, "sample.mimetype")
            with open(file, 'w', encoding="utf-8") as f:
                f.write(data)
            mime_dict = mimetypes.read_mime_types(file)
            eq(mime_dict[".pyunit"], "x-application/x-unittest")

            data = "x-application/x-unittest2 pyunit2\n"
            file = os.path.join(directory, "sample2.mimetype")
            with open(file, 'w', encoding="utf-8") as f:
                f.write(data)
            mime_dict = mimetypes.read_mime_types(os_helper.FakePath(file))
            eq(mime_dict[".pyunit2"], "x-application/x-unittest2")

        # bpo-41048: read_mime_types should read the rule file with 'utf-8' encoding.
        # Not with locale encoding. _bootlocale has been imported because io.open(...)
        # uses it.
        data = "application/no-mans-land  Fran\u00E7ais"
        filename = "filename"
        fp = io.StringIO(data)
        with unittest.mock.patch.object(mimetypes, 'open',
                                        return_value=fp) as mock_open:
            mime_dict = mimetypes.read_mime_types(filename)
            mock_open.assert_called_with(filename, encoding='utf-8')
        eq(mime_dict[".Français"], "application/no-mans-land")

    def test_non_standard_types(self):
        eq = self.assertEqual
        # First try strict
        eq(self.db.guess_file_type('foo.xul', strict=True), (None, None))
        eq(self.db.guess_extension('image/jpg', strict=True), None)
        # And then non-strict
        eq(self.db.guess_file_type('foo.xul', strict=False), ('text/xul', None))
        eq(self.db.guess_file_type('foo.XUL', strict=False), ('text/xul', None))
        eq(self.db.guess_file_type('foo.invalid', strict=False), (None, None))
        eq(self.db.guess_extension('image/jpg', strict=False), '.jpg')
        eq(self.db.guess_extension('image/JPG', strict=False), '.jpg')

    def test_filename_with_url_delimiters(self):
        # bpo-38449: URL delimiters cases should be handled also.
        # They would have different mime types if interpreted as URL as
        # compared to when interpreted as filename because of the semicolon.
        eq = self.assertEqual
        gzip_expected = ('application/x-tar', 'gzip')
        for name in (
                ';1.tar.gz',
                '?1.tar.gz',
                '#1.tar.gz',
                '#1#.tar.gz',
                ';1#.tar.gz',
                ';&1=123;?.tar.gz',
                '?k1=v1&k2=v2.tar.gz',
            ):
            for prefix in ('', '/', '\\',
                           'c:', 'c:/', 'c:\\', 'c:/d/', 'c:\\d\\',
                           '//share/server/', '\\\\share\\server\\'):
                path = prefix + name
                with self.subTest(path=path):
                    eq(self.db.guess_file_type(path), gzip_expected)
                    eq(self.db.guess_type(path), gzip_expected)
            expected = (None, None) if os.name == 'nt' else gzip_expected
            for prefix in ('//', '\\\\', '//share/', '\\\\share\\'):
                path = prefix + name
                with self.subTest(path=path):
                    eq(self.db.guess_file_type(path), expected)
                    eq(self.db.guess_type(path), expected)
        eq(self.db.guess_file_type(r" \"\`;b&b&c |.tar.gz"), gzip_expected)
        eq(self.db.guess_type(r" \"\`;b&b&c |.tar.gz"), gzip_expected)

        eq(self.db.guess_file_type(r'foo/.tar.gz'), (None, 'gzip'))
        eq(self.db.guess_type(r'foo/.tar.gz'), (None, 'gzip'))
        expected = (None, 'gzip') if os.name == 'nt' else gzip_expected
        eq(self.db.guess_file_type(r'foo\.tar.gz'), expected)
        eq(self.db.guess_type(r'foo\.tar.gz'), expected)
        eq(self.db.guess_type(r'scheme:foo\.tar.gz'), gzip_expected)

    def test_url(self):
        result = self.db.guess_type('http://example.com/host.html')
        result = self.db.guess_type('http://host.html')
        msg = 'URL only has a host name, not a file'
        self.assertSequenceEqual(result, (None, None), msg)
        result = self.db.guess_type('http://example.com/host.html')
        msg = 'Should be text/html'
        self.assertSequenceEqual(result, ('text/html', None), msg)
        result = self.db.guess_type('http://example.com/host.html#x.tar')
        self.assertSequenceEqual(result, ('text/html', None))
        result = self.db.guess_type('http://example.com/host.html?q=x.tar')
        self.assertSequenceEqual(result, ('text/html', None))

    def test_guess_all_types(self):
        # First try strict.  Use a set here for testing the results because if
        # test_urllib2 is run before test_mimetypes, global state is modified
        # such that the 'all' set will have more items in it.
        all = self.db.guess_all_extensions('text/plain', strict=True)
        self.assertTrue(set(all) >= {'.bat', '.c', '.h', '.ksh', '.pl', '.txt'})
        self.assertEqual(len(set(all)), len(all))  # no duplicates
        # And now non-strict
        all = self.db.guess_all_extensions('image/jpg', strict=False)
        self.assertEqual(all, ['.jpg'])
        # And now for no hits
        all = self.db.guess_all_extensions('image/jpg', strict=True)
        self.assertEqual(all, [])
        # And now for type existing in both strict and non-strict mappings.
        self.db.add_type('test-type', '.strict-ext')
        self.db.add_type('test-type', '.non-strict-ext', strict=False)
        all = self.db.guess_all_extensions('test-type', strict=False)
        self.assertEqual(all, ['.strict-ext', '.non-strict-ext'])
        all = self.db.guess_all_extensions('test-type')
        self.assertEqual(all, ['.strict-ext'])
        # Test that changing the result list does not affect the global state
        all.append('.no-such-ext')
        all = self.db.guess_all_extensions('test-type')
        self.assertNotIn('.no-such-ext', all)

    def test_encoding(self):
        filename = support.findfile("mime.types")
        mimes = mimetypes.MimeTypes([filename])
        exts = mimes.guess_all_extensions('application/vnd.geocube+xml',
                                          strict=True)
        self.assertEqual(exts, ['.g3', '.g\xb3'])

    def test_init_reinitializes(self):
        # Issue 4936: make sure an init starts clean
        # First, put some poison into the types table
        mimetypes.add_type('foo/bar', '.foobar')
        self.assertEqual(mimetypes.guess_extension('foo/bar'), '.foobar')
        # Reinitialize
        mimetypes.init()
        # Poison should be gone.
        self.assertEqual(mimetypes.guess_extension('foo/bar'), None)

    @unittest.skipIf(sys.platform.startswith("win"), "Non-Windows only")
    def test_guess_known_extensions(self):
        # Issue 37529
        # The test fails on Windows because Windows adds mime types from the Registry
        # and that creates some duplicates.
        from mimetypes import types_map
        for v in types_map.values():
            self.assertIsNotNone(mimetypes.guess_extension(v))

    def test_preferred_extension(self):
        def check_extensions():
            for mime_type, ext in (
                ("application/octet-stream", ".bin"),
                ("application/postscript", ".ps"),
                ("application/vnd.apple.mpegurl", ".m3u"),
                ("application/vnd.ms-excel", ".xls"),
                ("application/vnd.ms-fontobject", ".eot"),
                ("application/vnd.ms-powerpoint", ".ppt"),
                ("application/x-texinfo", ".texi"),
                ("application/x-troff", ".roff"),
                ("application/xml", ".xsl"),
                ("audio/matroska", ".mka"),
                ("audio/mpeg", ".mp3"),
                ("font/otf", ".otf"),
                ("font/ttf", ".ttf"),
                ("font/woff", ".woff"),
                ("font/woff2", ".woff2"),
                ("image/avif", ".avif"),
                ("image/emf", ".emf"),
                ("image/fits", ".fits"),
                ("image/g3fax", ".g3"),
                ("image/jp2", ".jp2"),
                ("image/jpm", ".jpm"),
                ("image/t38", ".t38"),
                ("image/webp", ".webp"),
                ("image/wmf", ".wmf"),
                ("image/jpeg", ".jpg"),
                ("image/tiff", ".tiff"),
                ("image/tiff-fx", ".tfx"),
                ("message/rfc822", ".eml"),
                ("text/html", ".html"),
                ("text/plain", ".txt"),
                ("text/rtf", ".rtf"),
                ("text/x-rst", ".rst"),
                ("video/matroska", ".mkv"),
                ("video/matroska-3d", ".mk3d"),
                ("video/mpeg", ".mpeg"),
                ("video/quicktime", ".mov"),
            ):
                with self.subTest(mime_type=mime_type, ext=ext):
                    self.assertEqual(mimetypes.guess_extension(mime_type), ext)

        check_extensions()
        mimetypes.init()
        check_extensions()

    def test_init_stability(self):
        mimetypes.init()

        suffix_map = mimetypes.suffix_map
        encodings_map = mimetypes.encodings_map
        types_map = mimetypes.types_map
        common_types = mimetypes.common_types

        mimetypes.init()
        self.assertIsNot(suffix_map, mimetypes.suffix_map)
        self.assertIsNot(encodings_map, mimetypes.encodings_map)
        self.assertIsNot(types_map, mimetypes.types_map)
        self.assertIsNot(common_types, mimetypes.common_types)
        self.assertEqual(suffix_map, mimetypes.suffix_map)
        self.assertEqual(encodings_map, mimetypes.encodings_map)
        self.assertEqual(types_map, mimetypes.types_map)
        self.assertEqual(common_types, mimetypes.common_types)

    def test_path_like_ob(self):
        filename = "LICENSE.txt"
        filepath = os_helper.FakePath(filename)
        filepath_with_abs_dir = os_helper.FakePath('/dir/'+filename)
        filepath_relative = os_helper.FakePath('../dir/'+filename)
        path_dir = os_helper.FakePath('./')

        expected = self.db.guess_file_type(filename)

        self.assertEqual(self.db.guess_file_type(filepath), expected)
        self.assertEqual(self.db.guess_type(filepath), expected)
        self.assertEqual(self.db.guess_file_type(
            filepath_with_abs_dir), expected)
        self.assertEqual(self.db.guess_type(
            filepath_with_abs_dir), expected)
        self.assertEqual(self.db.guess_file_type(filepath_relative), expected)
        self.assertEqual(self.db.guess_type(filepath_relative), expected)

        self.assertEqual(self.db.guess_file_type(path_dir), (None, None))
        self.assertEqual(self.db.guess_type(path_dir), (None, None))

    def test_bytes_path(self):
        self.assertEqual(self.db.guess_file_type(b'foo.html'),
                         self.db.guess_file_type('foo.html'))
        self.assertEqual(self.db.guess_file_type(b'foo.tar.gz'),
                         self.db.guess_file_type('foo.tar.gz'))
        self.assertEqual(self.db.guess_file_type(b'foo.tgz'),
                         self.db.guess_file_type('foo.tgz'))

    def test_keywords_args_api(self):
        self.assertEqual(self.db.guess_file_type(
            path="foo.html", strict=True), ("text/html", None))
        self.assertEqual(self.db.guess_type(
            url="scheme:foo.html", strict=True), ("text/html", None))
        self.assertEqual(self.db.guess_all_extensions(
            type='image/jpg', strict=True), [])
        self.assertEqual(self.db.guess_extension(
            type='image/jpg', strict=False), '.jpg')


@unittest.skipUnless(sys.platform.startswith("win"), "Windows only")
class Win32MimeTypesTestCase(unittest.TestCase):
    def setUp(self):
        # ensure all entries actually come from the Windows registry
        self.original_types_map = mimetypes.types_map.copy()
        mimetypes.types_map.clear()
        mimetypes.init()
        self.db = mimetypes.MimeTypes()

    def tearDown(self):
        # restore default settings
        mimetypes.types_map.clear()
        mimetypes.types_map.update(self.original_types_map)

    @unittest.skipIf(win32_edition() in ('NanoServer', 'WindowsCoreHeadless', 'IoTEdgeOS'),
                                         "MIME types registry keys unavailable")
    def test_registry_parsing(self):
        # the original, minimum contents of the MIME database in the
        # Windows registry is undocumented AFAIK.
        # Use file types that should *always* exist:
        eq = self.assertEqual
        eq(self.db.guess_type("foo.txt"), ("text/plain", None))
        eq(self.db.guess_type("image.jpg"), ("image/jpeg", None))
        eq(self.db.guess_type("image.png"), ("image/png", None))

    @unittest.skipIf(not hasattr(_winapi, "_mimetypes_read_windows_registry"),
                     "read_windows_registry accelerator unavailable")
    def test_registry_accelerator(self):
        from_accel = {}
        from_reg = {}
        _winapi._mimetypes_read_windows_registry(
            lambda v, k: from_accel.setdefault(k, set()).add(v)
        )
        mimetypes.MimeTypes._read_windows_registry(
            lambda v, k: from_reg.setdefault(k, set()).add(v)
        )
        self.assertEqual(list(from_reg), list(from_accel))
        for k in from_reg:
            self.assertEqual(from_reg[k], from_accel[k])


class MiscTestCase(unittest.TestCase):
    def test__all__(self):
        support.check__all__(self, mimetypes)


class MimetypesCliTestCase(unittest.TestCase):

    def mimetypes_cmd(self, *args, **kwargs):
        support.patch(self, sys, "argv", [sys.executable, *args])
        with support.captured_stdout() as output:
            mimetypes._main()
            return output.getvalue().strip()

    def test_help_option(self):
        support.patch(self, sys, "argv", [sys.executable, "-h"])
        with support.captured_stdout() as output:
            with self.assertRaises(SystemExit) as cm:
                mimetypes._main()

        self.assertIn("Usage: mimetypes.py", output.getvalue())
        self.assertEqual(cm.exception.code, 0)

    def test_invalid_option(self):
        support.patch(self, sys, "argv", [sys.executable, "--invalid"])
        with support.captured_stdout() as output:
            with self.assertRaises(SystemExit) as cm:
                mimetypes._main()

        self.assertIn("Usage: mimetypes.py", output.getvalue())
        self.assertEqual(cm.exception.code, 1)

    def test_guess_extension(self):
        eq = self.assertEqual

        extension = self.mimetypes_cmd("-l", "-e", "image/jpg")
        eq(extension, ".jpg")

        extension = self.mimetypes_cmd("-e", "image/jpg")
        eq(extension, "I don't know anything about type image/jpg")

        extension = self.mimetypes_cmd("-e", "image/jpeg")
        eq(extension, ".jpg")

    def test_guess_type(self):
        eq = self.assertEqual

        type_info = self.mimetypes_cmd("-l", "foo.pic")
        eq(type_info, "type: image/pict encoding: None")

        type_info = self.mimetypes_cmd("foo.pic")
        eq(type_info, "I don't know anything about type foo.pic")

if __name__ == "__main__":
    unittest.main()