tl; dr
调用is_path_exists_or_creatable()
下面定义的函数。
严格地使用Python3。这就是我们的发展方向。
两个问题的故事
问题“如何测试路径名的有效性,以及对于有效路径名,这些路径的存在或可写性?” 显然是两个独立的问题。两者都很有趣,而且在这里还是我能找到的任何地方都没有收到真正令人满意的答案。
vikki的答案可能是最接近的,但有以下明显的缺点:
- 不必要地打开(然后无法可靠地关闭)文件句柄。
- 不必要的写作( ...然后无法可靠地关闭或删除)0字节文件。
- 忽略操作系统特定的错误,以区分不可忽略的无效路径名和可忽略的文件系统问题。毫不奇怪,这在Windows下至关重要。(见下文。)
- 忽略由外部进程同时(重新)移动要测试的路径名的父目录导致的竞争条件。(见下文。)
- 忽略此路径名导致的连接超时,该路径名位于陈旧,缓慢或暂时不可访问的文件系统上。这可能会使面向公众的服务遭受潜在的DoS驱动的攻击。(见下文。)
我们将解决所有问题。
问题#0:路径名有效性又是什么?
在将我们脆弱的肉类衣服扔进蟒蛇般的痛苦中之前,我们可能应该定义“路径名有效性”的含义。究竟是什么定义了有效性?
“路径名有效性”是指路径名相对于当前系统的根文件系统的语法正确性,无论该路径或其父目录是否物理存在。如果路径名符合根文件系统的所有语法要求,则在此定义下语法上正确。
所谓“根文件系统”,是指:
- 在与POSIX兼容的系统上,文件系统已安装到根目录(
/
)。
- 在Windows中,文件系统安装到
%HOMEDRIVE%
,包含当前的Windows安装(通常但结肠-后缀盘符不必然C:
)。
反过来,“语法正确性”的含义取决于根文件系统的类型。对于ext4
(且不是大多数但与所有POSIX兼容的)文件系统,路径名称在且仅当该路径名称在语法上正确:
- 不包含空字节(即,
\x00
在Python中)。这是所有POSIX兼容文件系统的硬性要求。
- 包含不超过255个字节的路径组件(例如,
'a'*256
在Python中)。路径成分是含有不路径名的最长子串/
字符(例如,bergtatt
,ind
,i
,和fjeldkamrene
在路径名/bergtatt/ind/i/fjeldkamrene
)。
句法正确性。根文件系统。而已。
问题1:我们现在应如何进行路径名有效性?
令人惊讶的是,在Python中验证路径名是不直观的。我在这里与Fake Name达成坚定协议:官方os.path
软件包应为此提供现成的解决方案。出于未知(可能不令人信服)的原因,事实并非如此。幸运的是,展开您自己的临时解决方案并不是那么费劲……
好的,实际上是。毛茸茸的 讨厌 它在发光时发出嘶哑和咯咯笑声时可能会发痒。但是你会怎么做?Nuthin'。
我们将很快进入低级代码的放射性深渊。但首先,让我们谈谈高级商店。当传递无效的路径名时,标准os.stat()
和os.lstat()
函数会引发以下异常:
- 对于驻留在不存在的目录中的路径名,
FileNotFoundError
。
- 对于现有目录中的路径名:
- 在Windows下,
WindowsError
其winerror
属性为123
(即ERROR_INVALID_NAME
)的实例。
- 在所有其他操作系统下:
- 对于包含空字节(即
'\x00'
)的路径名,请使用的实例TypeError
。
- 对于包含长度超过255个字节的路径成分的路径名,
OSError
其errcode
属性的实例为:
- 在SunOS和* BSD系列操作系统下,
errno.ERANGE
。(这似乎是操作系统级别的错误,否则称为POSIX标准的“选择性解释”。)
- 在所有其他操作系统下,
errno.ENAMETOOLONG
。
至关重要的是,这意味着仅存在于现有目录中的路径名是有效的。当传递的路径名驻留在不存在的目录中时,不管这些路径名是否无效,os.stat()
andos.lstat()
函数都会引发通用FileNotFoundError
异常。目录存在优先于路径名无效。
这是否意味着不存在的目录中的路径名无效?是的-除非我们修改这些路径名以驻留在现有目录中。但是,这甚至安全可行吗?修改路径名是否应该阻止我们验证原始路径名?
要回答这个问题,请从上面回忆一下,ext4
文件系统上语法正确的路径名不包含路径组件(A)包含空字节,或(B)长度超过255个字节。因此,ext4
仅当该路径名中的所有路径组件均有效时,该路径名才有效。大多数 现实世界中感兴趣的文件系统都是如此。
那根学究的见解真的对我们有帮助吗?是。它将一次验证完整路径名的较大问题减少到仅验证该路径名中的所有路径组成部分的较小问题。通过遵循以下算法,可以以跨平台方式对任意路径名进行有效验证(无论该路径名是否位于现有目录中):
- 将该路径名拆分为路径组成部分(例如,将路径名
/troldskog/faren/vild
拆分为list ['', 'troldskog', 'faren', 'vild']
)。
- 对于每个这样的组件:
- 将保证与该组件一起存在的目录的路径名加入新的临时路径名(例如
/troldskog
)。
- 将该路径名传递给
os.stat()
或os.lstat()
。如果该路径名及其组件无效,则可以确保此调用引发一个暴露无效类型的异常,而不是通用FileNotFoundError
异常。为什么?因为该路径名位于现有目录中。(循环逻辑是循环的。)
是否有目录保证存在?是的,但通常只有一个:根文件系统的最顶层目录(如上定义)。
将驻留在任何其他目录(因此不保证存在)中的路径名传递给竞争条件os.stat()
或os.lstat()
引发竞争条件,即使该目录先前已被测试存在。为什么?因为在执行该测试之后但在将该路径名传递给os.stat()
or之前,无法阻止外部进程同时删除该目录os.lstat()
。释放令人发疯的狗!
上述方法也有一个很大的附带好处:安全性。(是不是说好的?)具体为:
前端应用程序通过简单地将这样的路径名传递给拒绝服务(DoS)攻击os.stat()
或os.lstat()
容易受到拒绝的攻击,从而验证来自不受信任来源的任意路径名。恶意用户可能试图反复验证驻留在已知陈旧或缓慢的文件系统上的路径名(例如,NFS Samba共享);在这种情况下,盲目声明传入的路径名可能最终会因连接超时而失败,或者消耗的时间和资源要比您承受失业的能力弱。
上面的方法通过仅针对根文件系统的根目录验证路径名的路径组成部分来避免这种情况。(即使这是陈旧,缓慢或无法访问的,也比路径名验证要麻烦得多。)
丢失?大。让我们开始。(假定使用Python3。请参阅“ leycec对300的脆弱希望是什么?”)
import errno, os
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.
See Also
----------
https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
Official listing of all such codes.
'''
def is_pathname_valid(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS;
`False` otherwise.
'''
try:
if not isinstance(pathname, str) or not pathname:
return False
_, pathname = os.path.splitdrive(pathname)
root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
if sys.platform == 'win32' else os.path.sep
assert os.path.isdir(root_dirname)
root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep
for pathname_part in pathname.split(os.path.sep):
try:
os.lstat(root_dirname + pathname_part)
except OSError as exc:
if hasattr(exc, 'winerror'):
if exc.winerror == ERROR_INVALID_NAME:
return False
elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
return False
except TypeError as exc:
return False
else:
return True
做完了 不要斜视那个代码。(它咬。)
问题2:路径名的存在或可创建性可能无效,是吗?
在上述解决方案的基础上,测试可能无效的路径名的存在或可创建性通常很简单。这里的关键是在测试传递的路径之前调用先前定义的函数:
def is_path_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create the passed
pathname; `False` otherwise.
'''
dirname = os.path.dirname(pathname) or os.getcwd()
return os.access(dirname, os.W_OK)
def is_path_exists_or_creatable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS _and_
either currently exists or is hypothetically creatable; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_creatable(pathname))
except OSError:
return False
完成和完成的。除了不太一样。
问题3:Windows上可能存在无效的路径名或可写性
有一个警告。当然有。
官方os.access()
文件承认:
注意:即使os.access()
表明I / O操作将成功,它也可能会失败,尤其是对于网络文件系统上的操作,其权限语义可能超出通常的POSIX权限位模型。
毫不奇怪,Windows通常是这里的嫌疑人。由于在NTFS文件系统上广泛使用了访问控制列表(ACL),因此简单的POSIX权限位模型无法很好地映射到底层Windows现实。尽管这(不是问题)不是Python的错,但对于与Windows兼容的应用程序,它可能仍然值得关注。
如果是您,那么需要一个更强大的替代方案。如果传递的路径也不会存在,我们不是试图建立保证该路径的父目录被立即删除临时文件- creatability的更便携的(如昂贵的)测试:
import os, tempfile
def is_path_sibling_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create **siblings**
(i.e., arbitrary files in the parent directory) of the passed pathname;
`False` otherwise.
'''
dirname = os.path.dirname(pathname) or os.getcwd()
try:
with tempfile.TemporaryFile(dir=dirname): pass
return True
except EnvironmentError:
return False
def is_path_exists_or_creatable_portable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname on the current OS _and_
either currently exists or is hypothetically creatable in a cross-platform
manner optimized for POSIX-unfriendly filesystems; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_sibling_creatable(pathname))
except OSError:
return False
但是请注意,即使这可能还不够。
多亏了用户访问控制(UAC),永远无法模仿的Windows Vista及其所有后续迭代都明显地涉及与系统目录有关的权限。当非管理员用户尝试在规范目录C:\Windows
或C:\Windows\system32
目录中创建文件时,UAC会从表面上允许用户这样做,同时实际上将所有创建的文件隔离到该用户配置文件中的“虚拟存储”中。(谁能想到欺骗用户会产生有害的长期后果?)
这太疯狂了。这是Windows。
证明给我看
敢吗 现在该进行上述测试了。
由于NULL是面向UNIX的文件系统上路径名中唯一禁止使用的字符,因此让我们利用它来展示冷酷的事实–忽略不可忽略的Windows恶作剧,坦白地说,这同样使我感到厌烦并激怒了我:
>>> print('"foo.bar" valid? ' + str(is_pathname_valid('foo.bar')))
"foo.bar" valid? True
>>> print('Null byte valid? ' + str(is_pathname_valid('\x00')))
Null byte valid? False
>>> print('Long path valid? ' + str(is_pathname_valid('a' * 256)))
Long path valid? False
>>> print('"/dev" exists or creatable? ' + str(is_path_exists_or_creatable('/dev')))
"/dev" exists or creatable? True
>>> print('"/dev/foo.bar" exists or creatable? ' + str(is_path_exists_or_creatable('/dev/foo.bar')))
"/dev/foo.bar" exists or creatable? False
>>> print('Null byte exists or creatable? ' + str(is_path_exists_or_creatable('\x00')))
Null byte exists or creatable? False
超越理智。超越痛苦。您会发现Python可移植性问题。