更新履歴(3件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- MAX_PATHの260という数の内訳を表で示しました。`longPathAware`を置くマニフェストの完全な形とVisual Studioでの追加手順、csprojでの指定を追加し、`Path.GetRelativePath`が.NET Frameworkに無いことと代替コード、ファイル名を安全な形に直す処理の入力と出力の対応表(予約名や末尾ドットを含む)、`dir /x`の読み方を追加しました。Windows 11での予約名の扱いの変更については、公式にビルド番号の記載が無いことを明記しています。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21590034)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「MAX_PATHとWindowsのパス・ファイル名の落とし穴 ── 260文字制限、予約名、末尾ドット、大文字小文字」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21590034 https://staging.comcomponent.com/blog/windows-max-path-filename-pitfalls/
- DOI(最新版)
- 10.5281/zenodo.21590034
- DOI(この版)
- 10.5281/zenodo.21733010
「ユーザーの端末でだけ『ファイルが見つかりません』になる」「コピーはできたのに、そのファイルが開けない」── ファイル入出力を伴う業務アプリの不具合調査で、最終的にパスの長さやファイル名そのものが原因だったというケースは珍しくありません。ユーザーは案件名や日付をフォルダー名に詰め込み、階層を深く掘り、こちらの想定を軽々と超えてきます。
厄介なのは、この領域の制限が「Win32 APIの制限」「ファイルシステムの制限」「シェル(Explorer)の制限」「.NETランタイムの制限」と何層にも分かれていて、どこまで対応できて何が残るのかが分かりにくいことです。この記事では、MAX_PATH=260文字の正体から、長いパスを合法的に扱う条件、CONなどの予約名や末尾ドットといったファイル名の罠、大文字小文字の扱いまで、C#での実務対応とセットで整理します。
1. まず結論
- MAX_PATH=260は「ドライブ文字+コロン+バックスラッシュ+最大256文字のパス文字列+終端NUL」を含んだWin32 APIの制限です。ファイルシステム側(NTFS等)はもっと長いパスを扱え、Unicode版APIに
\\?\プレフィックスを付ければ合計約32,767文字まで指定できます。12 - 260文字制限の解除には、Windows 10 バージョン1607以降で「レジストリのLongPathsEnabled=1」と「アプリマニフェストのlongPathAware」の両方が必要です。片方だけでは有効になりません。3
- .NET(Core)/.NET 5以降のランタイムはMAX_PATHチェックを行わず、長いパスを暗黙に扱います。.NET Frameworkは4.6.2以降をターゲットにするとランタイム側の260文字チェックが外れます。45
- ただし長パス非対応のアプリは現実に残ります。Win32 APIで作れるパスをシェル(Explorer)が正しく解釈できないことがある、と公式ドキュメント自身が明記しています。1
- ファイル名には
< > : " / \ | ? *と制御文字(0〜31)が使えず、CON・PRN・AUX・NUL・COM1〜9・LPT1〜9は拡張子を付けても(CON.txtでも)予約名扱いです。6 - 名前の末尾のスペースとドットは、パス正規化の過程で黙って取り除かれます。「hoge.」を指定したつもりが「hoge」に化ける、他OSが作った末尾スペース付きファイルにWindowsからアクセスできない、という事故の原因です。67
- Windowsのファイル名は「大文字小文字を保持するが、区別しない」が既定です。NTFSはディレクトリ単位の区別(
fsutil.exe file setCaseSensitiveInfo)もサポートしますが、Windowsアプリ側が追随できない副作用があります。89 - 実装面では、パス結合は
Path.Combineの「ルート付き引数で前段が捨てられる」仕様に注意し(.NET Core系ならPath.Joinも選択肢)、ユーザー入力のファイル名はPath.GetInvalidFileNameChars+予約名・末尾文字の自前チェックでサニタイズします。1011
この記事の知識マップ
MAX_PATH=260という制限はドライブ文字・コロン・バックスラッシュ・最大256文字のパス文字列・終端NULを合わせたWin32 API側の制限であり、NTFS自体はより長いパスを扱えるため、LongPathsEnabledとlongPathAwareを両方設定するか\?\プレフィックスを使えば260文字の壁を越えられるが、Explorerを含め長パス非対応アプリは現実に残る。.NET(Core)/.NET 5以降は長いパスを暗黙に扱うのに対し、古い.NET Frameworkは260文字超過でPathTooLongExceptionを投げる。ファイル名にはCONやNULなどの予約デバイス名が拡張子付きでも使えず、末尾のスペースやドットは正規化で黙って取り除かれるため、ユーザー入力からのファイル名生成はこれらを自前でサニタイズする必要がある。Path.Combineはルート付き引数で前段のベースパスを黙って捨てるため、ベースディレクトリ配下かの検証と組み合わせる。
flowchart LR
accTitle: MAX_PATHとファイル名の落とし穴の知識マップ
accDescr: 260文字のMAX_PATH制限がWin32 APIの制限でありNTFS自体の限界ではないこと、長パスの有効化や\?\プレフィックスによる回避条件、予約デバイス名や末尾ドット・スペースの正規化によるファイル名の落とし穴、Path.Combineの仕様に起因するリスクとその対策の関係を示す図
max_path["MAX_PATH(260文字制限)"]
reserved_device_name["予約デバイス名(CON/NUL等)"]
path_normalization["パス正規化(末尾ドット・スペースの除去)"]
long_path_enable["長パスの有効化(LongPathsEnabled+longPathAware)"]
long_path_prefix["\\?\プレフィックス"]
path_too_long_exception["PathTooLongException"]
dotnet[".NET(Core以降)"]
dotnet_framework[".NET Framework"]
path_combine["Path.Combineのルート付き引数仕様"]
directory_traversal_risk["意図した保存フォルダー外への書き込み"]
path_containment_check["ベースディレクトリ配下チェック"]
filename_sanitization["ファイル名のサニタイズ"]
filename_case_handling["大文字小文字の保持と非区別"]
fsutil["fsutil"]
case_sensitivity_side_effect["大文字小文字区別有効化の副作用"]
eight_dot_three_name["8.3短縮名"]
ntfs["NTFS"]
path_component_limit["パスコンポーネントの長さ上限(255文字)"]
long_path_enable -->|"軽減する"| max_path
long_path_prefix -.->|"軽減する"| max_path
max_path -.->|"原因になり得る"| path_too_long_exception
dotnet -.->|"防止する"| path_too_long_exception
dotnet_framework -.->|"原因になり得る"| path_too_long_exception
dotnet_framework -.->|"前提とする"| long_path_enable
path_combine -.->|"原因になり得る"| directory_traversal_risk
path_containment_check -->|"軽減する"| directory_traversal_risk
filename_sanitization -->|"防止する"| reserved_device_name
filename_sanitization -->|"軽減する"| path_normalization
long_path_prefix -->|"軽減する"| path_normalization
filename_case_handling -->|"で構成できる"| fsutil
filename_case_handling -.->|"原因になり得る"| case_sensitivity_side_effect
eight_dot_three_name -.->|"軽減する"| max_path
ntfs -.->|"利用する"| eight_dot_three_name
ntfs -->|"前提とする"| path_component_limit
filename_sanitization -->|"前提とする"| path_component_limit
ntfs -->|"実装を担う"| filename_case_handling
path_containment_check -->|"前提とする"| path_normalization
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全19件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. MAX_PATH=260の正体
Win32 APIにおけるパスの最大長は、一部の例外を除いてMAX_PATH=260文字と定義されています。この260には内訳があります。ローカルパスは「ドライブ文字、コロン、バックスラッシュ、バックスラッシュで区切られた名前部分、終端のNUL文字」で構成され、たとえばDドライブなら「D:\ + 256文字ぶんのパス文字列 + 終端NUL」が最大です。1
内訳を分解すると次のとおりです。
| 位置 | 構成要素 | 例 | 文字数 |
|---|---|---|---|
| 先頭 | ドライブ文字+コロン+バックスラッシュ | D:\ |
3 |
| 中間 | バックスラッシュで区切られた名前部分(フォルダー名・ファイル名と区切り記号) | 2026\案件\…\報告書.xlsx |
最大256 |
| 末尾 | 終端のNUL文字(画面には見えない) | ─ | 1 |
| 合計 | 260 = MAX_PATH |
つまり260文字を「ファイル名に使える長さ」と思っていると、実際にはドライブ表記と終端NULで4文字ぶん目減りしている、ということです。さらに細かい制限として、ディレクトリ作成APIでは8.3形式のファイル名を後ろに足せる余地が要求されるため、ディレクトリのパスはMAX_PATH−12文字を超えられません。1
重要なのは、これがWin32 APIレイヤーの制限であって、ファイルシステムの限界ではないことです。NTFSは長いファイル名と拡張パスをサポートしており、多くのWin32関数のUnicode版は合計約32,767文字の拡張長パスを受け付けます。パスを構成する個々のコンポーネント(1つのフォルダー名・ファイル名)の上限はGetVolumeInformationが返す値で、一般に255文字です。12
この「APIは260、ファイルシステムは約32,767」というギャップこそが、現場の障害の源泉です。あるツールでは作れたパスが、別のツール(あるいは自社アプリ)では開けないという非対称な状況が正当に発生します。git cloneで深いリポジトリを長い名前のフォルダーに展開したらビルドが通らなくなった、というのは公式ドキュメントにも載っている典型例です。1
なお、旧来の.NET Frameworkではフルパスが260文字以上になるとSystem.IO.PathTooLongExceptionが投げられていました。この例外を見たら、まずパス長を疑ってください。12
3. 260文字の壁を超える方法と、その条件
長いパスを扱う手段は大きく2つ、「\\?\プレフィックス」と「OSの長パス有効化」です。
3.1. \\?\ プレフィックス
パス文字列の先頭に \\?\ を付けると、Win32 APIは文字列の解析を止めてそのままファイルシステムへ渡します。これによりMAX_PATHの制限を超えられます(UNCパスは \\?\UNC\server\share 形式)。ただし条件と副作用があります。16
- Unicode版API(〜Wや.NETのようにUTF-16で呼ぶもの)であること。
- 正規化がスキップされるため、
/区切りや.・..による相対指定は使えません。相対パスには\\?\を付けられないので、相対パスは常にMAX_PATHまでです。1 - すべてのAPIが対応しているわけではなく、対応可否は各APIのリファレンスで確認が必要です。6
3.2. Windows 10 1607以降の長パス有効化 ── 条件は「両方」
Windows 10 バージョン1607以降では、多くの一般的なWin32ファイル・ディレクトリ関数(CreateFileW、FindFirstFileW、GetFileAttributesWなど)からMAX_PATH制限を外せます。ただしアプリ側のオプトインが前提で、次の2条件を両方満たす必要があります。3
- レジストリ値
HKLM\SYSTEM\CurrentControlSet\Control\FileSystemのLongPathsEnabled(REG_DWORD) が1であること。グループポリシー「コンピューターの構成 > 管理用テンプレート > システム > ファイルシステム > Win32の長いパスを有効にする」でも設定できます。 - アプリケーションマニフェストに
longPathAware要素があること。
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>
# レジストリ側(管理者権限)
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
-Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
マニフェスト側の手順も補足しておきます。上に挙げたlongPathAwareのXMLは公式ドキュメントに載っている断片で、これ単体ではファイルになりません。実際にはアプリケーションマニフェストファイル(慣例的にapp.manifest)のassembly要素の中に置きます。Visual Studioなら「プロジェクトを右クリック > 追加 > 新しい項目」から「アプリケーション マニフェスト ファイル」を追加すると、UAC設定などが入ったひな形が生成されるので、そのassembly要素の中にapplication要素として書き足します。13
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- ここに VS が生成した trustInfo などが入っている -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>
</assembly>
追加した項目をビルドに結び付けるのはMSBuildのApplicationManifestプロパティです。Visual Studioで項目を追加すると通常は自動で書き込まれますが、csprojを直接編集する場合は次の1行を足します(このプロパティが指すマニフェストは、既定ではexeに埋め込まれます)。14
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
「レジストリを設定したのに効かない」という相談は大抵、マニフェスト側の欠落です。公式ドキュメントも「このレジストリ設定は、新機能を利用するよう修正されたアプリにしか影響しない」と念押ししています。また、レジストリ値は最初のファイル関数呼び出し時にプロセス単位でキャッシュされ、プロセスの生存中は再読み込みされません。設定変更を全アプリに確実に反映させるには再起動が必要な場合があります。3
3.3. .NETではどうなるか
- .NET (Core) / .NET 5以降: ランタイムはMAX_PATHチェックを行わず、長いパスを暗黙に扱います。アプリ側で特別なコードは不要です。4
- .NET Framework: 4.6.2以降をターゲットにすると260文字のランタイムチェックが外れ、
PathTooLongExceptionは「32,767文字超」または「OSがエラーを返した場合」に限られます。それ以前をターゲットにした既存アプリでも、Switch.System.IO.BlockLongPaths=false(および旧パス処理を無効にするSwitch.System.IO.UseLegacyPathHandling=false)のAppContextスイッチでオプトインできます。512 - .NET Frameworkアプリが実務で長パスを通すには、上記ランタイム設定に加えてOS側の長パス有効化とマニフェストの併用が必要になります。NuGet.exeの長パス対応ドキュメントが、この構成(Windows 10 1607以降+longPathAwareマニフェスト+UseLegacyPathHandling無効化)を実例として明記しています。15
3.4. それでも残る「非対応アプリ」という現実
ここまでやっても、世の中のすべてのアプリが長パスを扱えるようになるわけではありません。公式ドキュメントは「シェルとファイルシステムは要件が異なる。Win32 APIで作成できるパスをシェルUIが正しく解釈できないことがある」と明記しており1、実際、長パス対応をうたわないツールも残っています(たとえばNuGetのドキュメントは、Visual Studioやmsbuild -t:restoreのrestoreが長パス未対応であることを記載しています15)。自アプリが長いパスでファイルを作れても、ユーザーがExplorerや他のツールでそれを開けるかは別問題です。この非対称性を踏まえた設計判断は第6章の判断表にまとめます。
4. 使えない文字と予約デバイス名、末尾のドット・スペース
パス長と並ぶもう1つの地雷原が、ファイル名そのものの規則です。公式の命名規則から、業務アプリで踏みやすいものを整理します。6
| 分類 | 内容 | 備考 |
|---|---|---|
| 予約文字 | < > : " / \ \| ? * |
パス区切りの\(と/)、ドライブの:を含む |
| 制御文字 | 整数値0(NUL)と1〜31 | 代替データストリーム内を除き不可 |
| 予約デバイス名 | CON, PRN, AUX, NUL, COM1〜COM9, LPT1〜LPT9(および上付き数字のCOM¹〜³, LPT¹〜³) | 拡張子を付けても不可(NUL.txtやNUL.tar.gzはNULと等価) |
| 末尾の文字 | スペースまたはドットで終わる名前 | ファイルシステムは許容してもシェルとUIが非対応 |
4.1. 予約デバイス名 ── CON.txtでもダメ
CONやNULはMS-DOS時代のデバイス名で、現在もNT名前空間の予約名として残っています。だから「CON」というファイルは通常の方法では作れず、CON.txtのように拡張子を付けても予約名として解釈されます。6 シリアルポート連携のログを「COM1.log」のような名前で保存しようとして事故る、Linux側で作られた「aux」というフォルダーがWindowsで展開できない、というのが現場での現れ方です。
補足として、パス正規化の仕様上、従来は「CON」「COM1.TXT」のように予約名で始まるパスはデバイスパス(\\.\CON)に変換されて解釈されてきました。Windows 11ではこの解釈が変わり、レガシーデバイスを指すには\\.\CONのような完全な形式の指定が必要になっています。7 ただし公式ドキュメントの書き方は「Windows 11より前は」「Windows 11ではこれが当てはまらない」という粒度で、どのビルド・どの更新プログラムから切り替わったのかは示されていません。7 混在環境で影響範囲を見積もるときは、ビルド単位ではなく「Windows 10以前か、Windows 11以降か」までしか切り分けられないと考えてください。とはいえ古いOSと既存アプリの両方が従来解釈のまま残っているので、業務データの名前として予約名を避けるべきという結論は変わりません。
4.2. 末尾のスペース・ドットは「黙って」消える
公式の命名規則は「ファイル名・ディレクトリ名をスペースまたはドットで終わらせないこと」と定めています。6 さらに踏み込むと、Windowsのパス正規化には「パスが区切り文字で終わらない場合、末尾のドットとスペース(U+0020)をすべて取り除く」という明確なルールがあります。7
これが実務で厄介なのは、エラーにならず黙って別の名前になることです。ユーザーが「報告書v2.」という名前を入力すると、作成されるのは「報告書v2」です。逆に、SMB経由でLinux側から作られた「report 」(末尾スペース)のようなファイルは、Windowsの通常のパス指定では正規化で名前が変わってしまうため到達できません。こうした「正規化では到達できないが合法な名前」へアクセスする手段が、正規化をスキップする\\?\プレフィックスです。公式ドキュメントも「hidden.のようなファイルは他の方法ではアクセス不可能」と、この用途を明示しています。7
なお、先頭のドットは合法です(.gitignoreのような名前は問題なく作れます)。6
5. 大文字小文字は「保持するが、区別しない」
Windowsのファイルシステムの既定動作はcase-preserving, case-insensitiveです。Readme.txtという名前で作成すればその大文字小文字が保持されて表示されますが、検索や比較では大文字小文字が無視され、README.TXTでも同じファイルに到達します。ドライブ文字も同様に大文字小文字を区別しません。86
公式の命名規則はアプリ開発者向けに「大文字小文字の区別を仮定するな(OSCAR、Oscar、oscarは同じ名前とみなせ)」と明記する一方、NTFS自体はPOSIX的な大文字小文字の区別をサポートしている(ただし既定では無効)とも述べています。6
これが表に出てくるのがLinux連携です。Windows 10 ビルド17107以降では、ディレクトリ単位で大文字小文字の区別を有効化できます。9
# 管理者権限のPowerShellで
fsutil.exe file setCaseSensitiveInfo C:\work\linux-src enable
fsutil.exe file queryCaseSensitiveInfo C:\work\linux-src
WSLでLinux由来のソースツリー(Makefileとmakefileが共存するなど)を扱う場合には有効な手段ですが、公式ドキュメント自身が警告する副作用があります。ファイルシステムを大文字小文字非区別と仮定しているWindowsアプリは、区別が有効なディレクトリでファイルにアクセスできなくなることがあります。また、フラグの変更は対象ディレクトリが空でないとできず、新規作成されるサブディレクトリは親の設定を継承します。9 歴史的には、名前が大文字小文字だけ異なる2つのファイルがあると、Explorerには両方表示されるのにどちらを選んでも同じ一方しか開けない、という現象も公式に記録されています。9
業務アプリの設計としては、「Windows上では大文字小文字違いは同名」と扱うのが既定、ただしLinuxへ渡るファイル名は大文字小文字違いの衝突をチェックする、が実務的な落とし所です。Linux連携ではファイル名以前に文字コードでも罠があるので、「Windows文字コード入門 - Linux連携で起きる文字化け」も併せて確認してください。
6. 業務アプリでの実務 ── パス結合・サニタイズ・判断表
6.1. パス結合はPath.Combineの仕様を知って使う
パスの文字列連結を+で書くのは論外として、Path.Combineにも知っておくべき仕様があります。2番目以降の引数にルート付きパスが渡されると、それより前の引数はすべて無視されます。10
var baseDir = @"C:\App\Data";
// ユーザー入力がルート付きだと、baseDirは黙って捨てられる
Path.Combine(baseDir, @"C:\Windows\secret.txt"); // → "C:\Windows\secret.txt"
Path.Combine(baseDir, @"\evil.txt"); // → "\evil.txt"(現在ドライブのルート)
ユーザー入力や設定ファイル由来の文字列をそのまま第2引数に渡すと、意図した保存フォルダーの外へ書き込む脆弱性になります。公式ドキュメントもこの挙動が機微なファイルへの意図しないアクセスにつながり得ると注意しており、代替としてPath.Join / Path.TryJoin(.NET Frameworkにはありません)を挙げています。1016 どちらを使うにせよ、最終的にはPath.GetFullPathで正規化した結果がベースディレクトリ配下かを検証するのが定石です。
// ベース側も正規化したうえで、相対パスに変換して判定する。
// 文字列の前方一致より、末尾区切りの有無やベースがドライブルートの場合の揺れに強い
var baseFull = Path.GetFullPath(baseDir);
var fullPath = Path.GetFullPath(Path.Combine(baseFull, userInput));
var relative = Path.GetRelativePath(baseFull, fullPath);
if (relative == ".." ||
relative.StartsWith(".." + Path.DirectorySeparatorChar) ||
Path.IsPathRooted(relative)) // 別ドライブ・UNCへ抜けた場合は絶対パスが返る
{
throw new InvalidOperationException("保存先が想定フォルダーの外を指しています。");
}
なおPath.GetRelativePathは.NET Core 2.0以降・.NET Standard 2.1以降・.NET 5以降のAPIで、.NET Frameworkにはありません。17 Framework側では、「ベースを正規化したうえで末尾に区切り文字を付け、前方一致で判定する」形に置き換えます。末尾の区切り文字が要点で、これがないとC:\App\DataのチェックがC:\App\DataBackupを配下と誤判定します。
// .NET Framework 向けの代替(Path.GetRelativePath が無い環境)
var baseFull = Path.GetFullPath(baseDir);
if (!baseFull.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
{
baseFull += Path.DirectorySeparatorChar; // "C:\App\Data" が "C:\App\DataBackup" に前方一致しないようにする
}
var fullPath = Path.GetFullPath(Path.Combine(baseFull, userInput));
if (!fullPath.StartsWith(baseFull, StringComparison.OrdinalIgnoreCase)) // Windowsの既定に合わせて大文字小文字は無視
{
throw new InvalidOperationException("保存先が想定フォルダーの外を指しています。");
}
Path.GetRelativePath はOS既定の流儀でパスを比較します。つまりWindowsでは大文字小文字を区別しない前提の判定で、これは次章で説明する「Windowsの既定は大文字小文字を区別しない」という動作に合っています。裏を返すと、ディレクトリ単位で大文字小文字の区別を有効化した場所(次章参照)では、Data と data が別ディレクトリになり得るため、大文字小文字を無視した判定では「大文字小文字だけが違う別のフォルダー」を配下と誤判定する余地が生まれます。そうした構成を扱う可能性があるなら、区別が有効な場所をベースディレクトリとして受け付けない方針にするのが安全です。
もう1点、この判定は文字列として正規化したパスに対する判定でしかないことも意識しておいてください。ベースディレクトリ配下にジャンクションやシンボリックリンクが存在すると、文字列上は配下を指していても実体はベースの外、ということが起こり得ます。しかもリンクは末端のファイルだけでなく途中のフォルダーにも入り込めるため(ベース\リンク\ファイル.txt の形)、末端だけを File.ResolveLinkTarget で調べても見抜けません。まず、信頼できない利用者がベース配下にリンクやジャンクションを作成できる構成をそもそも避けるのが第一です。そのうえで厳密に守る必要があるなら、実際にファイルを開いたハンドルから確定パスを取得して(Win32の GetFinalPathNameByHandle)それがベース配下かを検証するか、パスの各フォルダー構成要素を順にリンクでないか検査してください。
6.2. ユーザー入力ファイル名のサニタイズ
「取引先名+日付.csv」のようにユーザー入力からファイル名を組み立てる機能では、サニタイズを1か所に集約します。Path.GetInvalidFileNameCharsが出発点ですが、この配列は不正文字の完全な集合を保証しないと公式に明記されています。11 予約デバイス名と末尾のドット・スペースはこのAPIでは検出できないので、自前のチェックを足します。
private static readonly HashSet<string> ReservedNames =
new(StringComparer.OrdinalIgnoreCase)
{
"CON", "PRN", "AUX", "NUL",
"COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9",
"LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9",
"COM¹","COM²","COM³", // 上付き数字のCOM¹〜COM³も予約名
"LPT¹","LPT²","LPT³", // 同じくLPT¹〜LPT³
};
public static string SanitizeFileName(string input)
{
var invalid = Path.GetInvalidFileNameChars();
var name = new string(input.Select(c => invalid.Contains(c) ? '_' : c).ToArray());
name = name.TrimEnd(' ', '.'); // 末尾スペース・ドットは黙って落とされるため除去
// ファイル名1要素の長さ上限(通常255文字)にも収める。
// フォルダー階層やアプリが後付けするサフィックスの余地を残して控えめに切り詰める
const int MaxNameLength = 120;
if (name.Length > MaxNameLength)
{
var ext = Path.GetExtension(name);
if (ext.Length > 20)
{
ext = ""; // 異常に長い「拡張子」は拡張子として温存しない(負の範囲指定で例外になるのを防ぐ)
}
name = name[..(MaxNameLength - ext.Length)].TrimEnd(' ', '.') + ext;
}
// 空・予約名の判定は必ず「最終形」に対して行う。
// 切り詰めやTrimEndの結果、空文字や予約名(NULなど)に化けるケースを拾うため
var stem = name.Split('.')[0]; // NUL.txt対策: 拡張子前の部分で予約名を判定
if (name.Length == 0 || ReservedNames.Contains(stem))
{
name = "_" + name; // 1文字足しても上限255には十分収まる
}
return name;
}
この関数の意図は入出力の対応表にすると伝わりやすいので、単体テストの最初のケースとしてそのまま使える形にしておきます。
| 入力 | 出力 | 効いている処理 |
|---|---|---|
報告書v2. |
報告書v2 |
末尾ドットをTrimEndで除去(正規化で黙って消えるのを先回りする) |
CON.txt |
_CON.txt |
拡張子を外したCONが予約名。Split('.')[0]で判定して接頭辞を付ける |
nul.tar.gz |
_nul.tar.gz |
予約名判定はOrdinalIgnoreCase。二重拡張子でも先頭要素を見る |
COM1 の末尾にスペース1つ |
_COM1 |
TrimEndの結果が予約名に化ける。だから判定は最終形に対して行う |
... |
_ |
TrimEndで空文字になったケース。空のファイル名を返さない |
A/B:C.csv |
A_B_C.csv |
GetInvalidFileNameCharsに含まれる/と:を_へ置換 |
150文字+.csv |
先頭116文字+.csv(計120文字) |
長さ上限で切り詰め、拡張子は温存する |
4行目と5行目が、コード中で「判定は必ず最終形に対して行う」とコメントしている理由そのものです。置換・切り詰め・TrimEndを先に済ませてから予約名と空文字を見ないと、末尾にスペースが付いたCOM1のような入力がチェックをすり抜けます。
CSV出力のファイル名でこの問題に遭遇することが多いので、CSVそのものの実務は「CSVは「ただのテキスト」ではない ── C#業務アプリのCSV実務」も参考にしてください。
6.3. 相対パスとカレントディレクトリの罠
相対パスには2つの罠があります。第一に、カレントディレクトリはプロセス単位の設定なので、どのスレッドからでもいつでも変更され得ます。公式ドキュメントは「相対パスはマルチスレッドアプリでは危険」とまで書いており、.NET Core 2.1以降なら基準パスを明示できるPath.GetFullPath(string, string)が使えます。7 第二に、C:tmp.txtのようにドライブ文字直後にバックスラッシュがない形式は「Cドライブのカレントディレクトリからの相対パス」であり、絶対パスではありません。この「ドライブ相対パス」はプログラムやスクリプトの定番のバグ源として公式に名指しされています。7
設定ファイルやユーザー入力で受け取ったパスは、受け取った時点でPath.GetFullPathにより絶対パス化してからログに残す・検証する、を習慣にしてください。
6.4. 判断表 ── 長いパスをサポートすべきか、入口で弾くべきか
| 状況 | 推奨 | 理由 |
|---|---|---|
| ユーザーが保存先を自由に選ぶ一般業務アプリ | 入口で検証して弾く(フルパス長・ファイル名を保存前にチェックし明確なエラーを出す) | 自アプリが対応してもExplorerや連携先ツールが開けない事故が残るため1 |
| バックアップ・同期・アーカイブ展開など、他者が作った深い階層を「読む」側 | 長パス対応する(.NET Core系+必要に応じマニフェスト、Frameworkなら4.6.2+設定) | 入力を制御できず、読めないと業務が止まるため54 |
| 自アプリが深い階層を「作る」側 | 原則作らない設計に見直す(階層のフラット化、ハッシュ名の採用など) | 作ったパスの利用者(人間・他アプリ)が非対応である可能性が高いため1 |
| Linux/WSLとファイルをやり取りする | 予約名・大文字小文字衝突・末尾文字を転送前にチェック | Windows側で到達不能なファイルが生まれるため69 |
| ユーザー入力からファイル名を生成する | GetInvalidFileNameChars+予約名・末尾文字のサニタイズを共通関数に集約 |
APIの配列だけでは不完全なため11 |
7. トラブルシューティング ── 「エクスプローラーでは見えるのに開けない」
「Explorerにはファイルが見えているのに、アプリで開くと『ファイルが見つかりません』」という相談の切り分け手順です。
| 確認ポイント | 手段 | 該当したら |
|---|---|---|
| フルパスが260文字近辺か | PowerShellで (Get-ChildItem -Recurse).FullName \| Where-Object { $_.Length -ge 250 } |
上位フォルダー名を短縮するか、長パス対応(第3章)を検討 |
| ファイル名が予約名か(aux、con、com1など) | 名前を目視確認。拡張子付きも対象6 | リネーム(作成元がLinux等なら転送時に変換) |
| 末尾にスペース・ドットがないか | cmd /c dir /x や引用符付き表示で確認 |
\\?\プレフィックス付きパスで削除・リネーム7 |
| 大文字小文字だけ異なる同名ファイルがないか | WSL/Git由来のフォルダーで発生しやすい9 | 片方をリネームするか、対象ディレクトリの用途を見直す |
| 相対パス・ドライブ相対パスを使っていないか | ログに実際に開こうとしたパスを絶対パスで記録 | Path.GetFullPathで絶対化してから使用7 |
表の中で使っているdir /xの読み方だけ補足します。/xは「8.3形式に収まらない名前に対して生成された短縮名を表示する」オプションで、表示形式は/n(名前が右端に来る形)と同じで、そこに短縮名の列が長い名前の手前に差し込まれます。18 つまり「日付 時刻 サイズ ─ 短縮名 ─ 長い名前」の並びで、右端が本来の名前、その左隣が短縮名(T97B4~1.TXTのような形)です。名前が最初から8.3形式に収まっているファイルには短縮名が生成されないため、その列は空欄になります。パス長が上限に触れて開けないとき、途中のフォルダーをこの短縮名で指定してパスを詰める、という応急処置にも使えます。
調査の第一歩としておすすめなのは、アプリのエラーログに「開こうとしたパスそのもの」を絶対パス・引用符付きで記録することです。「ファイルが見つかりません」という例外メッセージだけでは、パスが切り詰められたのか、正規化で名前が変わったのか、そもそも別のディレクトリを見ていたのかを事後に区別できません。引用符で囲んで記録しておけば、末尾スペースのような視認しにくい問題も一目で分かります。
なお、DLLのロード失敗も「ファイルが見つかりません」系エラーの常連ですが、こちらはパス長よりも検索順序の問題であることが多く、「Windows DLL名前解決の仕組み - 検索順序とSxS」で整理しています。
8. まとめ
- MAX_PATH=260は「
D:\+最大256文字+終端NUL」を含むWin32 APIの制限で、NTFS自体は約32,767文字の拡張長パスを扱えます。ディレクトリはさらにMAX_PATH−12までという制限もあります。 - 260文字を超えるには、
\\?\プレフィックス(Unicode版API限定・相対パス不可)か、Windows 10 1607以降の長パス有効化(レジストリLongPathsEnabled+マニフェストlongPathAwareの両方)が必要です。 - .NET (Core)/5+は長いパスを暗黙に扱い、.NET Frameworkは4.6.2以降ターゲットでランタイムチェックが外れます。ただしExplorerを含む非対応アプリが残るため、「作れるか」と「ユーザーが扱えるか」を分けて判断します。
- ファイル名は予約文字(
< > : " / \ | ? *)と制御文字が不可、CON・NUL・COM1などの予約デバイス名は拡張子付きでも不可、末尾のスペース・ドットは正規化で黙って消えます。 - 大文字小文字は「保持するが、区別しない」が既定です。
fsutil file setCaseSensitiveInfoによるディレクトリ単位の区別はWSL連携では有効ですが、Windowsアプリ側の誤動作リスクと引き換えです。 - 実装は、
Path.Combineのルート付き引数の仕様を踏まえたベースディレクトリ検証、GetInvalidFileNameChars+予約名・末尾文字チェックのサニタイズ共通化、相対パス排除(Path.GetFullPathでの絶対化)が定石です。
関連記事
- CSVは「ただのテキスト」ではない ── C#業務アプリのCSV実務(文字コード・Excel互換・インジェクション対策)
- Windows文字コード入門 - Linux連携で起きる文字化け
- Windowsの文字コードと改行コード - 文字化けとCRLF/LFの基本
- Windows DLL名前解決の仕組み - 検索順序とSxS
関連する相談領域
合同会社小村ソフトでは、「特定の環境・特定のファイルだけ開けない」といったファイルI/O起因の不具合調査、既存業務アプリの長パス対応やファイル名バリデーション設計の見直し、Windows・Linux混在環境でのファイル連携の設計相談を扱っています。
参考リンク
-
Microsoft Learn, Maximum Path Length Limitation. MAX_PATH=260の定義と「ドライブ文字+コロン+バックスラッシュ+256文字+終端NUL」という内訳、Unicode版APIと
\\?\プレフィックスによる約32,767文字の拡張長パス、コンポーネント長(一般に255文字)、相対パスが常にMAX_PATHに制限されること、ディレクトリ作成がMAX_PATH−12までであること、シェルとファイルシステムの要件が異なりWin32で作れるパスをシェルUIが解釈できない場合があることについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 -
Microsoft Learn, NTFS overview. NTFSが長いファイル名と約32,767文字の拡張長パスをサポートすること、8.3エイリアスによる後方互換について。 ↩ ↩2
-
Microsoft Learn, Maximum Path Length Limitation ── Enable long paths in Windows 10, version 1607, and later. Windows 10 1607以降でレジストリ値LongPathsEnabled=1とアプリマニフェストのlongPathAware要素の両方が必要なこと、グループポリシーでの設定、レジストリ値がプロセス単位でキャッシュされること、制限が外れるWin32関数の一覧について。 ↩ ↩2 ↩3
-
Microsoft Learn, File path formats on Windows systems ── Skip normalization. .NET Coreおよび.NET 5以降が長いパスを暗黙に処理しMAX_PATHチェックを行わないこと(MAX_PATHチェックは.NET Frameworkのみ)、
\\?\が正規化をスキップする仕組みであることについて。 ↩ ↩2 ↩3 -
Microsoft Learn, Retargeting changes for migration to .NET Framework 4.6.x. .NET Framework 4.6.2ターゲットで長いパス(最大32K文字)がサポートされ260文字制限が除去されたこと、旧ターゲットのアプリがSwitch.System.IO.BlockLongPaths=falseでオプトインできることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, Naming Files, Paths, and Namespaces. 予約文字(
< > : " / \ | ? *)と制御文字(0〜31)、予約デバイス名(CON/PRN/AUX/NUL/COM1〜9/LPT1〜9および上付き数字)、NUL.txtのような拡張子付きも予約名と等価であること、名前を末尾スペース・ドットで終わらせないこと、先頭ドットが合法なこと、大文字小文字の区別を仮定すべきでないこととNTFSのPOSIXセマンティクス、\\?\プレフィックスの動作とUnicode API要件について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 -
Microsoft Learn, File path formats on Windows systems ── Path normalization. パス正規化で末尾のドットとスペースが取り除かれること、
hidden.のような名前は\\?\でしかアクセスできないこと、CONなどのレガシーデバイス名の解釈とWindows 11での変更、ドライブ相対パス(C:tmp.txt)がバグの一般的な原因であること、カレントディレクトリがプロセス単位で相対パスがマルチスレッドで危険なこと、Path.GetFullPath(String, String)について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn, File path formats on Windows systems ── Case and the Windows file system. ディレクトリ名・ファイル名が作成時の大文字小文字を保持する一方、名前の比較は大文字小文字を区別しないことについて。 ↩ ↩2
-
Microsoft Learn, Adjust case sensitivity. Windows 10 ビルド17107以降のディレクトリ単位の大文字小文字区別(fsutil.exe file setCaseSensitiveInfo)、変更には管理者権限と空のディレクトリが必要なこと、新規サブディレクトリが設定を継承すること、大文字小文字非区別を仮定するWindowsアプリが誤動作し得るという警告、大文字小文字違いの2ファイルはExplorerに両方表示されても一方しか開けなかったことについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Path.Combine Method. 最初以外の引数にルート付きパスが含まれると、それより前のパス要素が無視されルート付き要素から始まる文字列が返ること、機微なファイルへの意図しないアクセスにつながり得ること、代替としてJoin/TryJoin(.NET Frameworkでは利用不可)が挙げられていることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, Path.GetInvalidFileNameChars Method. ファイル名に使えない文字の配列を返すこと、返される配列が不正文字の完全な集合を保証せず、ファイルシステムによって異なり得ることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, PathTooLongException Class. パスがシステム定義の最大長を超えたときに投げられる例外であること、.NET Framework 4.6.2以降では「32,767文字超」または「OSがエラーを返した場合」に限って投げられることについて。 ↩ ↩2
-
Microsoft Learn, Application Manifests. アプリケーションマニフェストがルート要素
assemblyを持つXMLであること、application要素とwindowsSettingsによる実行時設定の宣言について。 ↩ -
Microsoft Learn, Common MSBuild Project Properties.
ApplicationManifestプロパティがマニフェストファイルのパスを指定するものであり、多くの場合マニフェストは実行可能ファイルに埋め込まれることについて。 ↩ -
Microsoft Learn, Long Path Support (NuGet CLI). .NET Frameworkベースのツールが長パスを使うための実際の構成(Windows 10 1607以降または1511+.NET Framework 4.6.2、Win32 long pathsポリシー、longPathAwareマニフェスト+UseLegacyPathHandling無効化)と、Visual Studioやmsbuildのrestoreが長パス未対応であることについて。 ↩ ↩2
-
Microsoft Learn, Path.Join Method. Joinがルート付きの後続パスを破棄せず連結すること、Combineとの挙動差の実例について。 ↩
-
Microsoft Learn, Path.GetRelativePath Method. 対応バージョン(.NET Core 2.0以降、.NET Standard 2.1、.NET 5以降。.NET Frameworkは含まれない)と、比較にプラットフォーム既定の流儀(WindowsとmacOSはOrdinalIgnoreCase、LinuxはOrdinal)を使うことについて。 ↩
-
Microsoft Learn, dir.
/xオプションが8.3形式でない名前に対して生成された短縮名を表示すること、表示形式は/nと同じで短縮名が長い名前の手前に挿入されることについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
スリープ・休止・Modern Standbyと長時間稼働アプリ ── 「夜中に止まっていた」を設計で防ぐ
長時間動き続けるWindowsアプリが「朝見たら止まっていた」となる原因を、S3スリープ/休止/Modern Standbyの違いから整理します。スリープ中のタイマーやTCP接続の挙動、SetThreadExecutionStateによる抑止まで解説します。
ネットワークドライブとUNCパスの落とし穴 ── 業務アプリでファイルサーバー(共有フォルダ)を扱う実務
業務アプリから共有フォルダへ出力・監視するときの定番トラブルを整理します。ドライブ文字(Z:)がサービスから見えない理由、実行アカウントごとに必要な権限、エラー1219、FileSystemWatcherの注意点まで解説します。
C#からWin32 APIを安全に呼ぶ ── P/Invoke実務ガイド(DllImport / LibraryImport / CsWin32)
C#からWin32 APIをP/Invokeで呼ぶ実務ポイントを整理します。DllImportとLibraryImportの違い、CsWin32による自動生成、文字列マーシャリングの罠、SafeHandle、32/64bit差異まで実装例つきで解説します。
自社開発のWindowsアプリがウイルス扱いされたら ── Microsoft Defenderの誤検知対応と、性能影響との付き合い方
自社開発のWindowsアプリがMicrosoft Defenderに誤検知されたときの正規の対処を整理します。現代のウイルス対策の仕組み、Microsoftへの誤検知報告、隔離からの復元、除外設定の正しい入れ方とリスクまで解説します。
Arm版Windowsで業務アプリは動くのか ── x64エミュレーション(Prism)とネイティブDLL・COMの現実
「Arm版Windowsで業務アプリは動くのか」に開発者・情シス向けに答えます。x64エミュレーション(Prism)の仕組み、ドライバーなど動かない層、.NETのAnyCPUとP/Invokeの問題、Arm対応チェックリストまで整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
不具合調査 / 長期稼働テーマ
再現しにくい不具合、通信停止、長期稼働障害、失敗パス検証を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
不具合調査・原因解析
再現しにくい障害、長期稼働後の不具合、通信停止などの調査を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- Windowsのパスは何文字まで使えますか?
- Win32 APIの既定ではMAX_PATH=260文字で、これは「ドライブ文字+コロン+バックスラッシュ+256文字ぶんのパス文字列+終端NUL」を含んだ長さです。NTFSなどのファイルシステム自体はもっと長いパスを扱え、Unicode版APIに\\?\プレフィックス付きのパスを渡せば合計約32,767文字まで指定できます。ただし1つのフォルダー名・ファイル名(コンポーネント)は一般に255文字までで、相対パスは常にMAX_PATHまでに制限されます。
- MAX_PATHの260文字制限はどうすれば解除できますか?
- Windows 10 バージョン1607以降で、レジストリ値LongPathsEnabled=1(またはグループポリシーの「Win32の長いパスを有効にする」)と、アプリケーションマニフェストのlongPathAware要素の両方を設定すると、多くのWin32ファイル関数で260文字制限が外れます。片方だけでは有効になりません。.NET(Core)/.NET 5以降のランタイムはMAX_PATHチェックを行わず長いパスを暗黙に扱い、.NET Frameworkは4.6.2以降をターゲットにするとランタイム側の260文字チェックが外れます。ただしExplorerを含め長パス非対応のアプリは残るため、作った長いパスを誰が扱うかまで含めて判断が必要です。
- CONやNULという名前のファイルが作れないのはなぜですか?
- CON、PRN、AUX、NUL、COM1〜COM9、LPT1〜LPT9などはMS-DOS時代から続くデバイスの予約名で、Windowsはこれらの名前をファイルではなくデバイスとして解釈するためです。NUL.txtのように拡張子を付けてもNULと同じ扱いになるので回避できません。Windows 11ではパス解釈の挙動が一部変わりましたが、古いOSや多数のアプリが従来の解釈のままなので、業務データのファイル名としては引き続き避けるのが安全です。
- ファイル名の大文字小文字はWindowsでは区別されますか?
- 既定では「保持するが、区別しない」(case-preserving, case-insensitive)です。Readme.txtという名前で作れば表示上はその大文字小文字が保たれますが、README.TXTを開こうとしても同じファイルに到達します。NTFSはPOSIX的な大文字小文字の区別もサポートしており、Windows 10 ビルド17107以降ではfsutil.exe file setCaseSensitiveInfoでディレクトリ単位の区別を有効化できますが、大文字小文字を区別しない前提のWindowsアプリが誤動作する副作用があるため、WSL連携など必要な場面に限定すべきです。