From 2720dbda51b0e5d6ec85fd5e6379dea2298a1429 Mon Sep 17 00:00:00 2001 From: yuanx749 Date: Mon, 20 Apr 2026 00:58:34 +0300 Subject: [PATCH 1/8] gh-92455: Respect case-sensitive mimetype suffixes --- Lib/mimetypes.py | 6 +++++- Lib/test/test_mimetypes.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py index a834826114614dd..cf54984f9bbf688 100644 --- a/Lib/mimetypes.py +++ b/Lib/mimetypes.py @@ -187,15 +187,19 @@ def _guess_file_type(self, path, strict, splitext): base, ext = splitext(base) else: encoding = None - ext = ext.lower() + ext_lower = ext.lower() types_map = self.types_map[True] if ext in types_map: return types_map[ext], encoding + if ext_lower in types_map: + return types_map[ext_lower], encoding elif strict: return None, encoding types_map = self.types_map[False] if ext in types_map: return types_map[ext], encoding + if ext_lower in types_map: + return types_map[ext_lower], encoding else: return None, encoding diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py index 2d618081521e10d..7296f9ba0e19591 100644 --- a/Lib/test/test_mimetypes.py +++ b/Lib/test/test_mimetypes.py @@ -49,6 +49,18 @@ def test_case_sensitivity(self): eq(self.db.guess_file_type("foobar.tar.z"), (None, None)) eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None)) + def test_added_types_case_sensitive_preferred(self): + self.db.add_type("text/x-r-script", ".R") + self.db.add_type("text/x-test-lowercase-r", ".r") + self.assertEqual( + self.db.guess_file_type("example.R"), + ("text/x-r-script", None), + ) + self.assertEqual( + self.db.guess_file_type("example.r"), + ("text/x-test-lowercase-r", None), + ) + def test_default_data(self): eq = self.assertEqual eq(self.db.guess_file_type("foo.html"), ("text/html", None)) From 547a64efcef738cb6614fcf8fe699fa61906588c Mon Sep 17 00:00:00 2001 From: yuanx749 Date: Mon, 20 Apr 2026 01:26:27 +0300 Subject: [PATCH 2/8] Add NEWS entry --- .../next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst diff --git a/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst b/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst new file mode 100644 index 000000000000000..7d251cae688ae8c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst @@ -0,0 +1,3 @@ +Fix :mod:`mimetypes` to prefer case-sensitive matches for MIME type suffixes +registered with :func:`mimetypes.add_type` before falling back to +case-insensitive matches. Contributed by Xiao Yuan. From 47b2b6b0adc0bfe3c3a33b9a121093d17f52ba56 Mon Sep 17 00:00:00 2001 From: yuanx749 Date: Wed, 3 Jun 2026 20:02:45 +0300 Subject: [PATCH 3/8] Add non-strict mimetype case test --- Lib/test/test_mimetypes.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py index 1f3abd4a10aca68..ce520e2efd385d3 100644 --- a/Lib/test/test_mimetypes.py +++ b/Lib/test/test_mimetypes.py @@ -288,16 +288,28 @@ def test_case_sensitivity(self): eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None)) def test_added_types_case_sensitive_preferred(self): - self.db.add_type("text/x-r-script", ".R") + self.db.add_type("text/x-test-uppercase-r", ".R") self.db.add_type("text/x-test-lowercase-r", ".r") self.assertEqual( self.db.guess_file_type("example.R"), - ("text/x-r-script", None), + ("text/x-test-uppercase-r", None), ) self.assertEqual( self.db.guess_file_type("example.r"), ("text/x-test-lowercase-r", None), ) + self.db.add_type("text/x-test-uppercase-non-strict", + ".NON-STRICT-EXT", strict=False) + self.db.add_type("text/x-test-lowercase-non-strict", + ".non-strict-ext", strict=False) + self.assertEqual( + self.db.guess_file_type("example.NON-STRICT-EXT", strict=False), + ("text/x-test-uppercase-non-strict", None), + ) + self.assertEqual( + self.db.guess_file_type("example.non-strict-ext", strict=False), + ("text/x-test-lowercase-non-strict", None), + ) def test_default_data(self): eq = self.assertEqual From e55489214e3ee716dc38a6655dbc2e9704b93a62 Mon Sep 17 00:00:00 2001 From: yuanx749 Date: Sat, 6 Jun 2026 21:44:58 +0300 Subject: [PATCH 4/8] Test strict lookup excludes non-strict types --- Lib/test/test_mimetypes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py index ce520e2efd385d3..b493187a7252e87 100644 --- a/Lib/test/test_mimetypes.py +++ b/Lib/test/test_mimetypes.py @@ -302,6 +302,14 @@ def test_added_types_case_sensitive_preferred(self): ".NON-STRICT-EXT", strict=False) self.db.add_type("text/x-test-lowercase-non-strict", ".non-strict-ext", strict=False) + self.assertEqual( + self.db.guess_file_type("example.NON-STRICT-EXT"), + (None, None), + ) + self.assertEqual( + self.db.guess_file_type("example.non-strict-ext"), + (None, None), + ) self.assertEqual( self.db.guess_file_type("example.NON-STRICT-EXT", strict=False), ("text/x-test-uppercase-non-strict", None), From 206be36b503d9175bdaca038cb02bfb7fb49ce76 Mon Sep 17 00:00:00 2001 From: yuanx749 Date: Mon, 8 Jun 2026 11:37:56 +0300 Subject: [PATCH 5/8] Test suffix map case lookup --- Lib/test/test_mimetypes.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py index b493187a7252e87..19983fa3fa7628d 100644 --- a/Lib/test/test_mimetypes.py +++ b/Lib/test/test_mimetypes.py @@ -287,6 +287,18 @@ def test_case_sensitivity(self): eq(self.db.guess_file_type("foobar.tar.z"), (None, None)) eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None)) + def test_suffix_map_case_sensitive_preferred(self): + self.db.suffix_map[".TEST-SUFFIX"] = ".tar.gz" + self.db.suffix_map[".test-suffix"] = ".tar.xz" + self.assertEqual( + self.db.guess_file_type("example.TEST-SUFFIX"), + ("application/x-tar", "gzip"), + ) + self.assertEqual( + self.db.guess_file_type("example.test-suffix"), + ("application/x-tar", "xz"), + ) + def test_added_types_case_sensitive_preferred(self): self.db.add_type("text/x-test-uppercase-r", ".R") self.db.add_type("text/x-test-lowercase-r", ".r") From 6f639f225dc7f3ad207c0e3c4c38fe53ce237d13 Mon Sep 17 00:00:00 2001 From: yuanx749 Date: Mon, 8 Jun 2026 11:47:27 +0300 Subject: [PATCH 6/8] Respect suffix map case lookup --- Lib/mimetypes.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py index 54708e344fcfd41..56236bdae8ffb5d 100644 --- a/Lib/mimetypes.py +++ b/Lib/mimetypes.py @@ -172,8 +172,14 @@ def guess_file_type(self, path, *, strict=True): def _guess_file_type(self, path, strict, splitext): base, ext = splitext(path) - while (ext_lower := ext.lower()) in self.suffix_map: - base, ext = splitext(base + self.suffix_map[ext_lower]) + while True: + if ext in self.suffix_map: + suffix = self.suffix_map[ext] + elif (ext_lower := ext.lower()) in self.suffix_map: + suffix = self.suffix_map[ext_lower] + else: + break + base, ext = splitext(base + suffix) # encodings_map is case sensitive if ext in self.encodings_map: encoding = self.encodings_map[ext] From 4886f1326164680d14fc802b7097e4964e1f8b62 Mon Sep 17 00:00:00 2001 From: yuanx749 Date: Mon, 8 Jun 2026 12:02:06 +0300 Subject: [PATCH 7/8] Document mimetypes case lookup --- Doc/library/mimetypes.rst | 4 ++-- .../Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/mimetypes.rst b/Doc/library/mimetypes.rst index f33098faf7d8a77..9ad9454f50c5049 100644 --- a/Doc/library/mimetypes.rst +++ b/Doc/library/mimetypes.rst @@ -39,8 +39,8 @@ the information :func:`init` sets up. (e.g. :program:`compress` or :program:`gzip`). The encoding is suitable for use as a :mailheader:`Content-Encoding` header, **not** as a :mailheader:`Content-Transfer-Encoding` header. The mappings are table driven. - Encoding suffixes are case sensitive; type suffixes are first tried case - sensitively, then case insensitively. + Encoding suffixes are case sensitive. Suffix mappings and type suffixes are + first tried case sensitively, then case insensitively. The optional *strict* argument is a flag specifying whether the list of known MIME types is limited to only the official types `registered with IANA diff --git a/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst b/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst index 7d251cae688ae8c..8d2a11cb7761377 100644 --- a/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst +++ b/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst @@ -1,3 +1,3 @@ -Fix :mod:`mimetypes` to prefer case-sensitive matches for MIME type suffixes -registered with :func:`mimetypes.add_type` before falling back to -case-insensitive matches. Contributed by Xiao Yuan. +Fix :mod:`mimetypes` to prefer case-sensitive matches for suffix mappings and +MIME type suffixes before falling back to case-insensitive matches. +Contributed by Xiao Yuan. From 0874c077dd15b3b74dfa36991f5f4e341d40802a Mon Sep 17 00:00:00 2001 From: yuanx749 Date: Sun, 14 Jun 2026 19:41:04 +0300 Subject: [PATCH 8/8] Update documentation --- Doc/library/mimetypes.rst | 8 ++++++-- Lib/mimetypes.py | 6 ++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Doc/library/mimetypes.rst b/Doc/library/mimetypes.rst index 9ad9454f50c5049..5c29fff146eef00 100644 --- a/Doc/library/mimetypes.rst +++ b/Doc/library/mimetypes.rst @@ -39,8 +39,8 @@ the information :func:`init` sets up. (e.g. :program:`compress` or :program:`gzip`). The encoding is suitable for use as a :mailheader:`Content-Encoding` header, **not** as a :mailheader:`Content-Transfer-Encoding` header. The mappings are table driven. - Encoding suffixes are case sensitive. Suffix mappings and type suffixes are - first tried case sensitively, then case insensitively. + Encoding suffixes are case-sensitive. Suffix mappings and type suffixes are + first tried case-sensitively, then case-insensitively. The optional *strict* argument is a flag specifying whether the list of known MIME types is limited to only the official types `registered with IANA @@ -131,6 +131,8 @@ behavior of the module. is already known the extension will be added to the list of known extensions. Valid extensions are empty or start with a ``'.'``. + Registered lower-case extensions are matched case-insensitively. + When *strict* is ``True`` (the default), the mapping will be added to the official MIME types, otherwise to the non-standard ones. @@ -312,6 +314,8 @@ than one MIME-type database; it provides an interface similar to the one of the extension is already known, the new type will replace the old one. When the type is already known the extension will be added to the list of known extensions. + Registered lower-case extensions are matched case-insensitively. + When *strict* is ``True`` (the default), the mapping will be added to the official MIME types, otherwise to the non-standard ones. diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py index 56236bdae8ffb5d..4339ef5a61397dd 100644 --- a/Lib/mimetypes.py +++ b/Lib/mimetypes.py @@ -86,6 +86,9 @@ def add_type(self, type, ext, strict=True): is already known the extension will be added to the list of known extensions. + Registered lower-case extensions are matched + case-insensitively. + If strict is true, information will be added to list of standard types, else to the list of non-standard types. @@ -396,6 +399,9 @@ def add_type(type, ext, strict=True): is already known the extension will be added to the list of known extensions. + Registered lower-case extensions are matched + case-insensitively. + If strict is true, information will be added to list of standard types, else to the list of non-standard types.