Skip to content

Commit debb751

Browse files
miss-islingtonbarneygaleeryksun
authored
bpo-27827: identify a greater range of reserved filename on Windows. (GH-26698) (#27422)
`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 1b82dc1 commit debb751

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
@@ -132,16 +132,25 @@ class _WindowsFlavour(_Flavour):
132132
ext_namespace_prefix = '\\\\?\\'
133133

134134
reserved_names = (
135-
{'CON', 'PRN', 'AUX', 'NUL'} |
136-
{'COM%d' % i for i in range(1, 10)} |
137-
{'LPT%d' % i for i in range(1, 10)}
135+
{'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} |
136+
{'COM%s' % c for c in '123456789\xb9\xb2\xb3'} |
137+
{'LPT%s' % c for c in '123456789\xb9\xb2\xb3'}
138138
)
139139

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

146155
def splitroot(self, part, sep=sep):
147156
first = part[0:1]
@@ -231,15 +240,16 @@ def _ext_to_normal(self, s):
231240

232241
def is_reserved(self, parts):
233242
# NOTE: the rules for reserved names seem somewhat complicated
234-
# (e.g. r"..\NUL" is reserved but not r"foo\NUL").
235-
# We err on the side of caution and return True for paths which are
236-
# not considered reserved by Windows.
243+
# (e.g. r"..\NUL" is reserved but not r"foo\NUL" if "foo" does not
244+
# exist). We err on the side of caution and return True for paths
245+
# which are not considered reserved by Windows.
237246
if not parts:
238247
return False
239248
if parts[0].startswith('\\\\'):
240249
# UNC paths are never reserved
241250
return False
242-
return parts[-1].partition('.')[0].upper() in self.reserved_names
251+
name = parts[-1].partition('.')[0].partition(':')[0].rstrip(' ')
252+
return name.upper() in self.reserved_names
243253

244254
def make_uri(self, path):
245255
# 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
@@ -1248,19 +1248,35 @@ def test_is_reserved(self):
12481248
self.assertIs(False, P('').is_reserved())
12491249
self.assertIs(False, P('/').is_reserved())
12501250
self.assertIs(False, P('/foo/bar').is_reserved())
1251+
# UNC paths are never reserved.
1252+
self.assertIs(False, P('//my/share/nul/con/aux').is_reserved())
1253+
# Case-insenstive DOS-device names are reserved.
1254+
self.assertIs(True, P('nul').is_reserved())
1255+
self.assertIs(True, P('aux').is_reserved())
1256+
self.assertIs(True, P('prn').is_reserved())
12511257
self.assertIs(True, P('con').is_reserved())
1252-
self.assertIs(True, P('NUL').is_reserved())
1258+
self.assertIs(True, P('conin$').is_reserved())
1259+
self.assertIs(True, P('conout$').is_reserved())
1260+
# COM/LPT + 1-9 or + superscript 1-3 are reserved.
1261+
self.assertIs(True, P('COM1').is_reserved())
1262+
self.assertIs(True, P('LPT9').is_reserved())
1263+
self.assertIs(True, P('com\xb9').is_reserved())
1264+
self.assertIs(True, P('com\xb2').is_reserved())
1265+
self.assertIs(True, P('lpt\xb3').is_reserved())
1266+
# DOS-device name mataching ignores characters after a dot or
1267+
# a colon and also ignores trailing spaces.
12531268
self.assertIs(True, P('NUL.txt').is_reserved())
1254-
self.assertIs(True, P('com1').is_reserved())
1255-
self.assertIs(True, P('com9.bar').is_reserved())
1269+
self.assertIs(True, P('PRN ').is_reserved())
1270+
self.assertIs(True, P('AUX .txt').is_reserved())
1271+
self.assertIs(True, P('COM1:bar').is_reserved())
1272+
self.assertIs(True, P('LPT9 :bar').is_reserved())
1273+
# DOS-device names are only matched at the beginning
1274+
# of a path component.
12561275
self.assertIs(False, P('bar.com9').is_reserved())
1257-
self.assertIs(True, P('lpt1').is_reserved())
1258-
self.assertIs(True, P('lpt9.bar').is_reserved())
12591276
self.assertIs(False, P('bar.lpt9').is_reserved())
1260-
# Only the last component matters.
1277+
# Only the last path component matters.
1278+
self.assertIs(True, P('c:/baz/con/NUL').is_reserved())
12611279
self.assertIs(False, P('c:/NUL/con/baz').is_reserved())
1262-
# UNC paths are never reserved.
1263-
self.assertIs(False, P('//my/share/nul/con/aux').is_reserved())
12641280

12651281
class PurePathTest(_BasePurePathTest, unittest.TestCase):
12661282
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)