From fa6fc8eac9af30542aaf7b15cb65e76f86df3292 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 13 Sep 2026 15:48:57 +0300 Subject: [PATCH] [3.13] gh-156713: Use the filesystem encoding in nturl2path (GH-156717) urllib.request.pathname2url() and url2pathname() use the filesystem encoding and error handler since gh-85168, but nturl2path, which implements them on Windows before 3.14, was left unchanged. Paths containing surrogate characters raised UnicodeEncodeError. (cherry picked from commit 01599e203e25610421bb5df413f32fa0848f3ade) Co-authored-by: Serhiy Storchaka --- Lib/nturl2path.py | 18 ++++++++++++------ Lib/test/test_urllib.py | 19 +++++++++++++++++++ ...-08-31-17-10-00.gh-issue-156713.Nt2URL.rst | 4 ++++ 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst diff --git a/Lib/nturl2path.py b/Lib/nturl2path.py index 757fd01bec82238..0761fbf34749752 100644 --- a/Lib/nturl2path.py +++ b/Lib/nturl2path.py @@ -14,7 +14,9 @@ def url2pathname(url): # ///C:/foo/bar/spam.foo # become # C:\foo\bar\spam.foo - import string, urllib.parse + import string, sys, urllib.parse + encoding = sys.getfilesystemencoding() + errors = sys.getfilesystemencodeerrors() if url[:3] == '///': # URL has an empty authority section, so the path begins on the third # character. @@ -30,13 +32,15 @@ def url2pathname(url): if not '|' in url: # No drive specifier, just convert slashes # make sure not to convert quoted slashes :-) - return urllib.parse.unquote(url.replace('/', '\\')) + return urllib.parse.unquote(url.replace('/', '\\'), + encoding=encoding, errors=errors) comp = url.split('|') if len(comp) != 2 or comp[0][-1] not in string.ascii_letters: error = 'Bad URL: ' + url raise OSError(error) drive = comp[0][-1].upper() - tail = urllib.parse.unquote(comp[1].replace('/', '\\')) + tail = urllib.parse.unquote(comp[1].replace('/', '\\'), + encoding=encoding, errors=errors) return drive + ':' + tail def pathname2url(p): @@ -46,7 +50,9 @@ def pathname2url(p): # C:\foo\bar\spam.foo # becomes # ///C:/foo/bar/spam.foo - import urllib.parse + import sys, urllib.parse + encoding = sys.getfilesystemencoding() + errors = sys.getfilesystemencodeerrors() # First, clean up some special forms. We are going to sacrifice # the additional information anyway p = p.replace('\\', '/') @@ -58,12 +64,12 @@ def pathname2url(p): raise OSError('Bad path: ' + p) if not ':' in p: # No DOS drive specified, just quote the pathname - return urllib.parse.quote(p) + return urllib.parse.quote(p, encoding=encoding, errors=errors) comp = p.split(':', maxsplit=2) if len(comp) != 2 or len(comp[0]) > 1: error = 'Bad path: ' + p raise OSError(error) drive = urllib.parse.quote(comp[0].upper()) - tail = urllib.parse.quote(comp[1]) + tail = urllib.parse.quote(comp[1], encoding=encoding, errors=errors) return '///' + drive + ':' + tail diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index c6a3157698d3ad1..d0f743178f78193 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -4,6 +4,7 @@ import urllib.request import urllib.error import http.client +import nturl2path import email.message import io import unittest @@ -1659,6 +1660,15 @@ def test_pathname2url_nonascii(self): url = urllib.parse.quote(os_helper.FS_NONASCII, encoding=encoding, errors=errors) self.assertEqual(urllib.request.pathname2url(os_helper.FS_NONASCII), url) + def test_pathname2url_surrogates(self): + # gh-156713: the filesystem encoding and error handler are used, + # so that paths containing surrogate characters can be converted. + encoding = sys.getfilesystemencoding() + errors = sys.getfilesystemencodeerrors() + tail = urllib.parse.quote('a\udcff', encoding=encoding, errors=errors) + self.assertEqual(nturl2path.pathname2url('C:\\a\udcff'), + '///C:/' + tail) + @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows pathnames.') def test_url2pathname_win(self): @@ -1720,6 +1730,15 @@ def test_url2pathname_nonascii(self): url = urllib.parse.quote(url, encoding=encoding, errors=errors) self.assertEqual(urllib.request.url2pathname(url), os_helper.FS_NONASCII) + def test_url2pathname_surrogates(self): + # gh-156713: the filesystem encoding and error handler are used, so + # that URLs containing percent-encoded surrogates can be converted. + encoding = sys.getfilesystemencoding() + errors = sys.getfilesystemencodeerrors() + url = urllib.parse.quote('a\udcff', encoding=encoding, errors=errors) + self.assertEqual(nturl2path.url2pathname('///C:/' + url), + 'C:\\a\udcff') + class Utility_Tests(unittest.TestCase): """Testcase to test the various utility functions in the urllib.""" diff --git a/Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst b/Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst new file mode 100644 index 000000000000000..1d21fc70fd9256f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst @@ -0,0 +1,4 @@ +Fix :func:`!nturl2path.pathname2url` and :func:`!nturl2path.url2pathname`: +the filesystem encoding and error handler are now used for percent-encoding +and decoding, as in :mod:`urllib.request`. Previously paths containing +surrogate characters raised :exc:`UnicodeEncodeError`.