Skip to content

Commit 5fb443d

Browse files
[3.12] GH-126212: Fix removal of slashes in file URIs on Windows (GH-126214) (#126591)
GH-126212: Fix removal of slashes in file URIs on Windows (GH-126214) Adjust `urllib.request.pathname2url()` and `url2pathname()` so that they don't remove slashes from Windows DOS drive paths and URLs. There was no basis for this behaviour, and it conflicts with how UNC and POSIX paths are handled. (cherry picked from commit 54c63a3) Co-authored-by: Barney Gale <[email protected]>
1 parent 69849ad commit 5fb443d

File tree

3 files changed

+18
-21
lines changed

3 files changed

+18
-21
lines changed

Lib/nturl2path.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,15 @@ def url2pathname(url):
2424
# convert this to \\host\path\on\remote\host
2525
# (notice halving of slashes at the start of the path)
2626
url = url[2:]
27-
components = url.split('/')
2827
# make sure not to convert quoted slashes :-)
29-
return urllib.parse.unquote('\\'.join(components))
28+
return urllib.parse.unquote(url.replace('/', '\\'))
3029
comp = url.split('|')
3130
if len(comp) != 2 or comp[0][-1] not in string.ascii_letters:
3231
error = 'Bad URL: ' + url
3332
raise OSError(error)
3433
drive = comp[0][-1].upper()
35-
components = comp[1].split('/')
36-
path = drive + ':'
37-
for comp in components:
38-
if comp:
39-
path = path + '\\' + urllib.parse.unquote(comp)
40-
# Issue #11474 - handing url such as |c/|
41-
if path.endswith(':') and url.endswith('/'):
42-
path += '\\'
43-
return path
34+
tail = urllib.parse.unquote(comp[1].replace('/', '\\'))
35+
return drive + ':' + tail
4436

4537
def pathname2url(p):
4638
"""OS-specific conversion from a file system path to a relative URL
@@ -60,17 +52,12 @@ def pathname2url(p):
6052
raise OSError('Bad path: ' + p)
6153
if not ':' in p:
6254
# No drive specifier, just convert slashes and quote the name
63-
components = p.split('\\')
64-
return urllib.parse.quote('/'.join(components))
55+
return urllib.parse.quote(p.replace('\\', '/'))
6556
comp = p.split(':', maxsplit=2)
6657
if len(comp) != 2 or len(comp[0]) > 1:
6758
error = 'Bad path: ' + p
6859
raise OSError(error)
6960

7061
drive = urllib.parse.quote(comp[0].upper())
71-
components = comp[1].split('\\')
72-
path = '///' + drive + ':'
73-
for comp in components:
74-
if comp:
75-
path = path + '/' + urllib.parse.quote(comp)
76-
return path
62+
tail = urllib.parse.quote(comp[1].replace('\\', '/'))
63+
return '///' + drive + ':' + tail

Lib/test/test_urllib.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,8 +1535,10 @@ def test_pathname2url_win(self):
15351535
self.assertEqual(fn('\\\\?\\C:\\dir'), '///C:/dir')
15361536
self.assertEqual(fn('\\\\?\\unc\\server\\share\\dir'), '//server/share/dir')
15371537
self.assertEqual(fn("C:"), '///C:')
1538-
self.assertEqual(fn("C:\\"), '///C:')
1538+
self.assertEqual(fn("C:\\"), '///C:/')
15391539
self.assertEqual(fn('C:\\a\\b.c'), '///C:/a/b.c')
1540+
self.assertEqual(fn('C:\\a\\b.c\\'), '///C:/a/b.c/')
1541+
self.assertEqual(fn('C:\\a\\\\b.c'), '///C:/a//b.c')
15401542
self.assertEqual(fn('C:\\a\\b%#c'), '///C:/a/b%25%23c')
15411543
self.assertEqual(fn('C:\\a\\b\xe9'), '///C:/a/b%C3%A9')
15421544
self.assertEqual(fn('C:\\foo\\bar\\spam.foo'), "///C:/foo/bar/spam.foo")
@@ -1572,13 +1574,15 @@ def test_url2pathname_win(self):
15721574
self.assertEqual(fn("///C|"), 'C:')
15731575
self.assertEqual(fn("///C:"), 'C:')
15741576
self.assertEqual(fn('///C:/'), 'C:\\')
1575-
self.assertEqual(fn('/C|//'), 'C:\\')
1577+
self.assertEqual(fn('/C|//'), 'C:\\\\')
15761578
self.assertEqual(fn('///C|/path'), 'C:\\path')
15771579
# No DOS drive
15781580
self.assertEqual(fn("///C/test/"), '\\\\\\C\\test\\')
15791581
self.assertEqual(fn("////C/test/"), '\\\\C\\test\\')
15801582
# DOS drive paths
15811583
self.assertEqual(fn('C:/path/to/file'), 'C:\\path\\to\\file')
1584+
self.assertEqual(fn('C:/path/to/file/'), 'C:\\path\\to\\file\\')
1585+
self.assertEqual(fn('C:/path/to//file'), 'C:\\path\\to\\\\file')
15821586
self.assertEqual(fn('C|/path/to/file'), 'C:\\path\\to\\file')
15831587
self.assertEqual(fn('/C|/path/to/file'), 'C:\\path\\to\\file')
15841588
self.assertEqual(fn('///C|/path/to/file'), 'C:\\path\\to\\file')
@@ -1592,6 +1596,9 @@ def test_url2pathname_win(self):
15921596
# Localhost paths
15931597
self.assertEqual(fn('//localhost/C:/path/to/file'), 'C:\\path\\to\\file')
15941598
self.assertEqual(fn('//localhost/C|/path/to/file'), 'C:\\path\\to\\file')
1599+
# Percent-encoded forward slashes are preserved for backwards compatibility
1600+
self.assertEqual(fn('C:/foo%2fbar'), 'C:\\foo/bar')
1601+
self.assertEqual(fn('//server/share/foo%2fbar'), '\\\\server\\share\\foo/bar')
15951602
# Round-tripping
15961603
paths = ['C:',
15971604
r'\\\C\test\\',
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix issue where :func:`urllib.request.pathname2url` and
2+
:func:`~urllib.request.url2pathname` removed slashes from Windows DOS drive
3+
paths and URLs.

0 commit comments

Comments
 (0)