tl; dr
is_path_exists_or_creatable()
以下に定義されている関数を呼び出します。
厳密にはPython3です。それが私たちの役割です。
2つの質問の物語
「パス名の有効性をテストするにはどうすればよいですか。有効なパス名については、それらのパスの存在または書き込み可能性をテストするにはどうすればよいですか?」明らかに2つの別々の質問です。どちらも興味深いものであり、どちらもここで本当に満足のいく答えを受け取っていません...または、まあ、私がgrepできるところならどこでも。
VIKKIさんの答えは、おそらく最も近いhewsが、の顕著な欠点があります。
- 不必要にファイルハンドルを開く(...そして確実に閉じることができない)。
- 0バイトのファイルを不必要に書き込みます(...そして信頼できるクローズまたは削除に失敗します)。
- 無視できない無効なパス名と無視できるファイルシステムの問題を区別するOS固有のエラーを無視します。当然のことながら、これはWindowsでは重要です。(以下を参照してください。)
- テストするパス名の親ディレクトリを同時に(再)移動する外部プロセスに起因する競合状態を無視します。(以下を参照してください。)
- このパス名が古い、遅い、または一時的にアクセスできないファイルシステムに存在することに起因する接続タイムアウトを無視します。これは可能性が潜在的に一般向けサービスを公開DoS攻撃駆動型攻撃。(以下を参照してください。)
それをすべて修正します。
質問#0:パス名の有効性は何ですか?
壊れやすい肉のスーツをパイソンがちりばめられた痛みのモシュピットに投げ込む前に、おそらく「パス名の妥当性」が何を意味するのかを定義する必要があります。正確には、何が妥当性を定義しますか?
「パス名の有効性」とは、パスまたはその親ディレクトリが物理的に存在するかどうかに関係なく、現在のシステムのルートファイルシステムに関するパス名の構文上の正確さを意味します。パス名は、ルートファイルシステムのすべての構文要件に準拠している場合、この定義では構文的に正しいです。
「ルートファイルシステム」とは、次のことを意味します。
- POSIX互換システムでは、ファイルシステムはルートディレクトリ(
/
)にマウントされます。
- Windowsでは、マウントされているファイルシステムで
%HOMEDRIVE%
、現在のWindowsインストールを含むコロン付きのドライブ文字(通常は必須ではありませんC:
)。
「構文の正確さ」の意味は、ルートファイルシステムのタイプによって異なります。以下のためのext4
(そして最もしかしないすべてのPOSIX互換)ファイルシステム、パス名は構文的に正しい場合にのみそのパス名の場合:
- nullバイトは含まれていません(つまり、
\x00
Pythonの場合)。これは、すべてのPOSIX互換ファイルシステムにとって厳しい要件です。
- 255バイトより長いパスコンポーネントは含まれていません(
'a'*256
Pythonなど)。パス成分は全く含まないパス名の最も長いストリングである/
文字(例えば、bergtatt
、ind
、i
、およびfjeldkamrene
パス名で/bergtatt/ind/i/fjeldkamrene
)。
構文の正確さ。ルートファイルシステム。それでおしまい。
質問1:パス名の有効性をどのように実行しますか?
Pythonでパス名を検証することは、驚くほど直感的ではありません。私はここで偽の名前にしっかりと同意しています。公式os.path
パッケージは、このためのすぐに使えるソリューションを提供するはずです。未知の(そしておそらく説得力のない)理由のために、そうではありません。幸いなことに、あなた自身のアドホックソリューションをアンロールすることはないこと、腸痛みます...
はい、実際はそうです。毛深いです。それは厄介です。それはおそらく、それが光るにつれて、それがゴロゴロと笑うように鳴きます。しかし、あなたは何をするつもりですか?Nuthin '。
私たちはすぐに低レベルコードの放射性の深淵に降ります。でもまずは高級店の話をしましょう。標準os.stat()
およびos.lstat()
関数は、無効なパス名が渡されると、次の例外を発生させます。
- 存在しないディレクトリにあるパス名の場合、のインスタンス
FileNotFoundError
。
- 既存のディレクトリにあるパス名の場合:
- Windowsでは、属性が(つまり)で
WindowsError
あるインスタンス。winerror
123
ERROR_INVALID_NAME
- 他のすべてのOSでは:
- nullバイト(つまり、
'\x00'
)を含むパス名の場合、TypeError
。のインスタンス。
- 255バイトより長いパスコンポーネントを含むパス名の場合、
OSError
そのerrcode
属性のインスタンスは次のとおりです。
- SunOSおよび* BSDファミリーのOSでは、
errno.ERANGE
。(これはOSレベルのバグのようで、POSIX標準の「選択的解釈」とも呼ばれます。)
- 他のすべてのOSでは、
errno.ENAMETOOLONG
。
重要なことに、これは、既存のディレクトリにあるパス名のみが有効であることを意味します。os.stat()
およびos.lstat()
機能は、一般的な調達FileNotFoundError
渡されたパス名が存在しないディレクトリに存在するとき、それらのパス名が無効であるかどうかに関係なく、例外を。ディレクトリの存在は、パス名の無効よりも優先されます。
これは、存在しないディレクトリにあるパス名が検証できないことを意味しますか?はい–既存のディレクトリに存在するようにこれらのパス名を変更しない限り。しかし、それでも安全に実行可能ですか?パス名を変更すると、元のパス名の検証が妨げられるべきではありませんか?
この質問に答えるには、ext4
ファイルシステム上の構文的に正しいパス名に、(A) nullバイトを含むパスコンポーネントまたは(B)長さが255バイトを超えるパスコンポーネントが含まれていないことを上から思い出してください。したがって、ext4
パス名は、そのパス名のすべてのパスコンポーネントが有効である場合にのみ有効です。これは、関心のあるほとんどの 実際のファイルシステムに当てはまります。
その衒学的洞察は実際に私たちを助けますか?はい。これにより、一挙にフルパス名を検証するという大きな問題が、そのパス名のすべてのパスコンポーネントのみを検証するという小さな問題に軽減されます。任意のパス名は、次のアルゴリズムに従って、クロスプラットフォームの方法で(そのパス名が既存のディレクトリに存在するかどうかに関係なく)検証可能です。
- そのパス名をパスコンポーネントに分割します(たとえば、パス名
/troldskog/faren/vild
をリストに分割します['', 'troldskog', 'faren', 'vild']
)。
- そのようなコンポーネントごとに:
- そのコンポーネントとともに存在することが保証されているディレクトリのパス名を新しい一時パス名(例:)に結合し
/troldskog
ます。
- そのパス名を
os.stat()
またはに渡しos.lstat()
ます。そのパス名、したがってそのコンポーネントが無効である場合、この呼び出しは、一般FileNotFoundError
的な例外ではなく、無効のタイプを公開する例外を発生させることが保証されています。どうして?そのパス名は既存のディレクトリにあるためです。(循環論理は循環です。)
存在が保証されているディレクトリはありますか?はい。ただし、通常は1つだけです。ルートファイルシステムの最上位ディレクトリ(上記で定義)。
そのディレクトリが以前に存在することがテストされていたとしても、他のディレクトリにある(したがって存在することが保証されていない)パス名を競合状態に渡すos.stat()
かos.lstat()
、競合状態を招きます。どうして?外部プロセスを同時に、そのディレクトリを削除することを防ぐことができないので、後に、そのテストが行われたが、前にそのパス名に渡されるos.stat()
、またはos.lstat()
。心を揺さぶる狂気の犬を解き放ちます!
上記のアプローチには、セキュリティという大きな副次的な利点もあります。(それはいいことではありませんか?)具体的には:
信頼できないソースからの任意のパス名を、サービス拒否(DoS)攻撃やその他のブラックハットシェナニガンに渡すos.stat()
かos.lstat()
、その影響を受けやすいものに渡すだけで検証する前面アプリケーション。悪意のあるユーザーは、古くなっているか遅いことがわかっているファイルシステム(NFS Samba共有など)にあるパス名を繰り返し検証しようとする可能性があります。その場合、着信パス名を盲目的に述べることは、最終的に接続タイムアウトで失敗するか、失業に耐えるあなたの弱い能力よりも多くの時間とリソースを消費する傾向があります。
上記のアプローチは、ルートファイルシステムのルートディレクトリに対してパス名のパスコンポーネントを検証するだけで、これを回避します。(それが古くなっている、遅い、またはアクセスできない場合でも、パス名の検証よりも大きな問題が発生します。)
失くした?すごい。さぁ、始めよう。(Python 3を想定。「300の脆弱な希望とは何か、leycec?」を参照)
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()
文書が認めているように:
注: I / O操作はos.access()
、成功することを示している場合でも失敗する可能性があります。特に、通常のPOSIXパーミッションビットモデルを超えるパーミッションセマンティクスを持つ可能性のあるネットワークファイルシステムでの操作の場合はそうです。
当然のことながら、ここではWindowsが通常の容疑者です。NTFSファイルシステムでのアクセス制御リスト(ACL)の広範な使用のおかげで、単純なPOSIXパーミッションビットモデルは、基盤となるWindowsの現実にうまく対応していません。これは(おそらく)Pythonのせいではありませんが、それでもWindows互換アプリケーションにとっては問題になる可能性があります。
これがあなたの場合、より堅牢な代替手段が必要です。渡されたパスが存在しない場合は、代わりに、そのパスの親ディレクトリにすぐに削除されることが保証されている一時ファイルを作成しようとします。これは、作成性のより移植性の高い(高価な場合)テストです。
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です。
証明する
あえて?上記のテストを試運転する時が来ました。
UNIX指向のファイルシステムのパス名で禁止されている文字はNULLだけなので、それを利用して、冷たくて難しい真実を示しましょう。無視できない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の移植性に関する懸念があります。