Skip to content

Commit 8789add

Browse files
miss-islingtonbarneygaleeryksun
authored
bpo-27827: identify a greater range of reserved filename on Windows. (GH-26698) (GH-27421)
`pathlib.PureWindowsPath.is_reserved()` now identifies as reserved filenames with trailing spaces or colons. Co-authored-by: Barney Gale <[email protected]> Co-authored-by: Eryk Sun <[email protected]> (cherry picked from commit 56c1f6d)
1 parent a90a57e commit 8789add

File tree

3 files changed

+47
-19
lines changed

3 files changed

+47
-19
lines changed

Lib/pathlib.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,25 @@ class _WindowsFlavour(_Flavour):
124124
ext_namespace_prefix = '\\\\?\\'
125125

126126
reserved_names = (
127-
{'CON', 'PRN', 'AUX', 'NUL'} |
128-
{'COM%d' % i for i in range(1, 10)} |
129-
{'LPT%d' % i for i in range(1, 10)}
127+
{'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} |
128+
{'COM%s' % c for c in '123456789\xb9\xb2\xb3'} |
129+
{'LPT%s' % c for c in '123456789\xb9\xb2\xb3'}
130130
)
131131

132132
# Interesting findings about extended paths:
133-
# - '\\?\c:\a', '//?/c:\a' and '//?/c:/a' are all supported
134-
# but '\\?\c:/a' is not
135-
# - extended paths are always absolute; "relative" extended paths will
136-
# fail.
133+
# * '\\?\c:\a' is an extended path, which bypasses normal Windows API
134+
# path processing. Thus relative paths are not resolved and slash is not
135+
# translated to backslash. It has the native NT path limit of 32767
136+
# characters, but a bit less after resolving device symbolic links,
137+
# such as '\??\C:' => '\Device\HarddiskVolume2'.
138+
# * '\\?\c:/a' looks for a device named 'C:/a' because slash is a
139+
# regular name character in the object namespace.
140+
# * '\\?\c:\foo/bar' is invalid because '/' is illegal in NT filesystems.
141+
# The only path separator at the filesystem level is backslash.
142+
# * '//?/c:\a' and '//?/c:/a' are effectively equivalent to '\\.\c:\a' and
143+
# thus limited to MAX_PATH.
144+
# * Prior to Windows 8, ANSI API bytes paths are limited to MAX_PATH,
145+
# even with the '\\?\' prefix.
137146

138147
def splitroot(self, part, sep=sep):
139148
first = part[0:1]
@@ -195,15 +204,16 @@ def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix):
195204

196205
def is_reserved(self, parts):
197206
# NOTE: the rules for reserved names seem somewhat complicated
198-
# (e.g. r"..\NUL" is reserved but not r"foo\NUL").
199-
# We err on the side of caution and return True for paths which are
200-
# not considered reserved by Windows.
207+
# (e.g. r"..\NUL" is reserved but not r"foo\NUL" if "foo" does not
208+
# exist). We err on the side of caution and return True for paths
209+
# which are not considered reserved by Windows.
201210
if not parts:
202211
return False
203212
if parts[0].startswith('\\\\'):
204213
# UNC paths are never reserved
205214
return False
206-
return parts[-1].partition('.')[0].upper() in self.reserved_names
215+
name = parts[-1].partition('.')[0].partition(':')[0].rstrip(' ')
216+
return name.upper() in self.reserved_names
207217

208218
def make_uri(self, path):
209219
# Under Windows, file URIs use the UTF-8 encoding.

Lib/test/test_pathlib.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,19 +1282,35 @@ def test_is_reserved(self):
12821282
self.assertIs(False, P('').is_reserved())
12831283
self.assertIs(False, P('/').is_reserved())
12841284
self.assertIs(False, P('/foo/bar').is_reserved())
1285+
# UNC paths are never reserved.
1286+
self.assertIs(False, P('//my/share/nul/con/aux').is_reserved())
1287+
# Case-insenstive DOS-device names are reserved.
1288+
self.assertIs(True, P('nul').is_reserved())
1289+
self.assertIs(True, P('aux').is_reserved())
1290+
self.assertIs(True, P('prn').is_reserved())
12851291
self.assertIs(True, P('con').is_reserved())
1286-
self.assertIs(True, P('NUL').is_reserved())
1292+
self.assertIs(True, P('conin$').is_reserved())
1293+
self.assertIs(True, P('conout$').is_reserved())
1294+
# COM/LPT + 1-9 or + superscript 1-3 are reserved.
1295+
self.assertIs(True, P('COM1').is_reserved())
1296+
self.assertIs(True, P('LPT9').is_reserved())
1297+
self.assertIs(True, P('com\xb9').is_reserved())
1298+
self.assertIs(True, P('com\xb2').is_reserved())
1299+
self.assertIs(True, P('lpt\xb3').is_reserved())
1300+
# DOS-device name mataching ignores characters after a dot or
1301+
# a colon and also ignores trailing spaces.
12871302
self.assertIs(True, P('NUL.txt').is_reserved())
1288-
self.assertIs(True, P('com1').is_reserved())
1289-
self.assertIs(True, P('com9.bar').is_reserved())
1303+
self.assertIs(True, P('PRN ').is_reserved())
1304+
self.assertIs(True, P('AUX .txt').is_reserved())
1305+
self.assertIs(True, P('COM1:bar').is_reserved())
1306+
self.assertIs(True, P('LPT9 :bar').is_reserved())
1307+
# DOS-device names are only matched at the beginning
1308+
# of a path component.
12901309
self.assertIs(False, P('bar.com9').is_reserved())
1291-
self.assertIs(True, P('lpt1').is_reserved())
1292-
self.assertIs(True, P('lpt9.bar').is_reserved())
12931310
self.assertIs(False, P('bar.lpt9').is_reserved())
1294-
# Only the last component matters.
1311+
# Only the last path component matters.
1312+
self.assertIs(True, P('c:/baz/con/NUL').is_reserved())
12951313
self.assertIs(False, P('c:/NUL/con/baz').is_reserved())
1296-
# UNC paths are never reserved.
1297-
self.assertIs(False, P('//my/share/nul/con/aux').is_reserved())
12981314

12991315
class PurePathTest(_BasePurePathTest, unittest.TestCase):
13001316
cls = pathlib.PurePath
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:meth:`pathlib.PureWindowsPath.is_reserved` now identifies a greater range of
2+
reserved filenames, including those with trailing spaces or colons.

0 commit comments

Comments
 (0)