Skip to content

Commit ff92d4b

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.15] gh-156713: Use the filesystem encoding in nturl2path (GH-156717) (GH-157423)
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 01599e2) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> * gh-156713: Fix test_nturl2path for non-UTF-8 filesystem encodings Since nturl2path uses the filesystem encoding, the expected results of quoting non-ASCII characters depend on it. Use os_helper.FS_NONASCII and os_helper.TESTFN_UNDECODABLE, and skip the tests when there is no suitable character or byte sequence for the current encoding. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 3e1b5d8) --------- Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 657dfc1 commit ff92d4b

3 files changed

Lines changed: 56 additions & 5 deletions

File tree

‎Lib/nturl2path.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ def url2pathname(url):
2222
# ///C:/foo/bar/spam.foo
2323
# become
2424
# C:\foo\bar\spam.foo
25+
import sys
2526
import urllib.parse
27+
encoding = sys.getfilesystemencoding()
28+
errors = sys.getfilesystemencodeerrors()
2629
if url[:3] == '///':
2730
# URL has an empty authority section, so the path begins on the third
2831
# character.
@@ -40,7 +43,8 @@ def url2pathname(url):
4043
if url[1:2] == '|':
4144
# Older URLs use a pipe after a drive letter
4245
url = url[:1] + ':' + url[2:]
43-
return urllib.parse.unquote(url.replace('/', '\\'))
46+
return urllib.parse.unquote(url.replace('/', '\\'),
47+
encoding=encoding, errors=errors)
4448

4549
def pathname2url(p):
4650
"""OS-specific conversion from a file system path to a relative URL
@@ -50,7 +54,10 @@ def pathname2url(p):
5054
# becomes
5155
# ///C:/foo/bar/spam.foo
5256
import ntpath
57+
import sys
5358
import urllib.parse
59+
encoding = sys.getfilesystemencoding()
60+
errors = sys.getfilesystemencodeerrors()
5461
# First, clean up some special forms. We are going to sacrifice
5562
# the additional information anyway
5663
p = p.replace('\\', '/')
@@ -65,10 +72,11 @@ def pathname2url(p):
6572
# an authority section with a zero-length authority, and a path
6673
# section starting with a single slash.
6774
drive = f'///{drive}'
68-
drive = urllib.parse.quote(drive, safe='/:')
75+
drive = urllib.parse.quote(drive, encoding=encoding, errors=errors,
76+
safe='/:')
6977
elif root:
7078
# Add explicitly empty authority to path beginning with one slash.
7179
root = f'//{root}'
7280

73-
tail = urllib.parse.quote(tail)
81+
tail = urllib.parse.quote(tail, encoding=encoding, errors=errors)
7482
return drive + root + tail

‎Lib/test/test_nturl2path.py‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import os
2+
import sys
13
import unittest
4+
import urllib.parse
25

6+
from test.support import os_helper
37
from test.support import warnings_helper
48

59

@@ -34,7 +38,6 @@ def test_pathname2url(self):
3438
self.assertEqual(fn('C:\\a\\b.c\\'), '///C:/a/b.c/')
3539
self.assertEqual(fn('C:\\a\\\\b.c'), '///C:/a//b.c')
3640
self.assertEqual(fn('C:\\a\\b%#c'), '///C:/a/b%25%23c')
37-
self.assertEqual(fn('C:\\a\\b\xe9'), '///C:/a/b%C3%A9')
3841
self.assertEqual(fn('C:\\foo\\bar\\spam.foo'), "///C:/foo/bar/spam.foo")
3942
# NTFS alternate data streams
4043
self.assertEqual(fn('C:\\foo:bar'), '///C:/foo%3Abar')
@@ -45,7 +48,7 @@ def test_pathname2url(self):
4548
self.assertEqual(fn("\\\\\\folder\\test\\"), '///folder/test/')
4649
self.assertEqual(fn('\\\\some\\share\\'), '//some/share/')
4750
self.assertEqual(fn('\\\\some\\share\\a\\b.c'), '//some/share/a/b.c')
48-
self.assertEqual(fn('\\\\some\\share\\a\\b%#c\xe9'), '//some/share/a/b%25%23c%C3%A9')
51+
self.assertEqual(fn('\\\\some\\share\\a\\b%#c'), '//some/share/a/b%25%23c')
4952
# Alternate path separator
5053
self.assertEqual(fn('C:/a/b.c'), '///C:/a/b.c')
5154
self.assertEqual(fn('//some/share/a/b.c'), '//some/share/a/b.c')
@@ -58,6 +61,29 @@ def test_pathname2url(self):
5861
for url in urls:
5962
self.assertEqual(fn(nturl2path.url2pathname(url)), url)
6063

64+
@unittest.skipUnless(os_helper.FS_NONASCII, 'need os_helper.FS_NONASCII')
65+
def test_pathname2url_nonascii(self):
66+
encoding = sys.getfilesystemencoding()
67+
errors = sys.getfilesystemencodeerrors()
68+
char = os_helper.FS_NONASCII
69+
quoted = urllib.parse.quote(char, encoding=encoding, errors=errors)
70+
self.assertEqual(nturl2path.pathname2url(f'C:\\a\\b{char}'),
71+
'///C:/a/b' + quoted)
72+
self.assertEqual(nturl2path.pathname2url(f'\\\\some\\share\\a\\b{char}'),
73+
'//some/share/a/b' + quoted)
74+
75+
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
76+
'need os_helper.TESTFN_UNDECODABLE')
77+
def test_pathname2url_surrogates(self):
78+
# gh-156713: the filesystem encoding and error handler are used,
79+
# so that paths containing surrogate characters can be converted.
80+
encoding = sys.getfilesystemencoding()
81+
errors = sys.getfilesystemencodeerrors()
82+
path = os.fsdecode(os_helper.TESTFN_UNDECODABLE)
83+
url = urllib.parse.quote(path, encoding=encoding, errors=errors)
84+
self.assertEqual(nturl2path.pathname2url('C:\\' + path),
85+
'///C:/' + url)
86+
6187
def test_url2pathname(self):
6288
fn = nturl2path.url2pathname
6389
self.assertEqual(fn('/'), '\\')
@@ -103,5 +129,18 @@ def test_url2pathname(self):
103129
self.assertEqual(fn(nturl2path.pathname2url(path)), path)
104130

105131

132+
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
133+
'need os_helper.TESTFN_UNDECODABLE')
134+
def test_url2pathname_surrogates(self):
135+
# gh-156713: the filesystem encoding and error handler are used, so
136+
# that URLs containing percent-encoded surrogates can be converted.
137+
encoding = sys.getfilesystemencoding()
138+
errors = sys.getfilesystemencodeerrors()
139+
path = os.fsdecode(os_helper.TESTFN_UNDECODABLE)
140+
url = urllib.parse.quote(path, encoding=encoding, errors=errors)
141+
self.assertEqual(nturl2path.url2pathname('///C:/' + url),
142+
'C:\\' + path)
143+
144+
106145
if __name__ == '__main__':
107146
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`!nturl2path.pathname2url` and :func:`!nturl2path.url2pathname`:
2+
the filesystem encoding and error handler are now used for percent-encoding
3+
and decoding, as in :mod:`urllib.request`. Previously paths containing
4+
surrogate characters raised :exc:`UnicodeEncodeError`.

0 commit comments

Comments
 (0)