FSOの深淵:NTFSジャンクションによる無限ループを断ち切る、堅牢な再帰探索アルゴリズムの設計思想
業務自動化の現場において、ファイルシステムの走査は最も基本的かつ、最も「罠」が多いタスクの一つだ。
多くの開発者が、`FileSystemObject` (FSO) の `GetFolder` メソッドと再帰関数を組み合わせた、一見シンプルで美しいコードを書いて現場に投入する。しかし、それがエンタープライズ環境、特に複雑なNTFS権限やジャンクション(Junction)、シンボリックリンクが張り巡らされたサーバー上で実行された瞬間、その「美しいコード」は牙を剥く。
プログラムは止まらない。CPU使用率は跳ね上がり、メモリは食いつぶされ、最終的にはスタックオーバーフローか、あるいは終わりのないプロセスとしてリソースを浪費し続ける。
今日は、そんな初歩的なミスを犯さないための、「階層レベル制限(Depth Limit)」と「訪問済みパス・トラッキング(Visited Path Tracking)」を用いた、プロフェッショナル仕様の探索アルゴリズムを伝授する。
1. なぜ、あなたの再帰処理は「死」に至るのか
問題の本質は、ファイルシステムの「論理的な構造」と「物理的な構造」の乖離にある。
NTFSジャンクションは、あるディレクトリが別のディレクトリを指し示す「ショートカットの高度な版」だ。例えば、`C:\Users\All Users` は実際には `C:\ProgramData` を指している。もし、あなたの探索ロジックがこれらのリンクを「新しいディレクトリ」として無批判に追跡し、かつリンク先が親ディレクトリを指すような循環参照が発生していた場合、再帰関数は無限の迷宮へと突入する。
また、単に「深い階層」を探索する場合でも、異常に深いディレクトリ構造(悪意のある、あるいは壊れたファイルシステム)に遭遇すれば、スタックメモリを使い果たしてクラッシュする。
プロフェッショナルは、コードを書く前に「最悪のケース(Worst-case scenario)」を設計に組み込む。
2. 堅牢な探索アルゴリズムの2つの防壁
無限ループとリソース枯渇を防ぐために、我々は以下の2つのガードレールを実装する必要がある。
防壁①:階層深度制限 (Depth Limiting)
「どれだけ深く潜っても、これ以上は行かない」という絶対的な境界線を設ける。これはスタックオーバーフローを防ぐ物理的な防御策だ。
防壁②:訪問済みパス管理 (Memoization of Visited Paths)
「一度通った道(パス)は二度と通らない」という論理的な防御策だ。`Scripting.Dictionary` オブジェクトを利用し、正規化されたパスを記録することで、ジャンクションによる循環参照を $O(1)$ の計算量で検出する。
3. 実装:プロダクション・レディな再帰探索エンジン
以下に、実務でそのまま利用可能な、堅牢性を極めたVBScriptのコード例を示す。単なるサンプルではない。エラーハンドリング、パスの正規化、メモリ効率を考慮した設計だ。
'***
' Module: RobustFileScanner
' Description: NTFSジャンクションによる無限ループを回避する堅牢なファイル探索エンジン
' Author: Chief Architect
'***
Option Explicit
' --- 設定定数 ---
Const MAX_DEPTH = 20 ' 最大探索深度(スタックオーバーフロー防止)
' メイン実行処理
Main
Sub Main()
Dim fso, targetPath, visitedPaths, scanner
Set fso = CreateObject("Scripting.FileSystemObject")
' 探索対象のパス(実環境に合わせて変更)
targetPath = "C:\TestFolder"
' 循環参照検知用のDictionaryオブジェクト
' Key: 正規化された絶対パス, Value: Boolean
Set visitedPaths = CreateObject("Scripting.Dictionary")
visitedPaths.CompareMode = 1 ' テキスト比較(大文字小文字を区別しない)
If Not fso.FolderExists(targetPath) Then
WScript.Echo "Error: Target folder does not exist."
Exit Sub
End If
WScript.Echo "Starting robust scan on: " & targetPath
' 探索開始
RecursiveScan fso.GetFolder(targetPath), 0, visitedPaths
WScript.Echo "Scan completed successfully."
End Sub
''' <summary>
''' 再帰的にフォルダを走査するコアエンジン
''' </summary>
''' <param name="currentFolder">現在のFSO Folderオブジェクト</param>
''' <param name="currentDepth">現在の階層レベル</param>
''' <param name="visitedPaths">訪問済みパスを保持するDictionary</param>
Sub RecursiveScan(currentFolder, currentDepth, visitedPaths)
Dim folderPath, subFolder, file, fso
Set fso = CreateObject("Scripting.FileSystemObject")
' 1. 階層深度チェック (Depth Limit Guard)
If currentDepth > MAX_DEPTH Then
WScript.Echo "[WARNING] Max depth reached at: " & currentFolder.Path
Exit Sub
End If
' 2. パスの正規化 (Normalization)
' ジャンクション対策として、絶対パスを取得し、末尾のスラッシュ等を考慮
folderPath = currentFolder.Path
' 3. 循環参照チェック (Circular Reference Guard)
' すでに訪問したパスであれば、その枝は切り捨てる
If visitedPaths.Exists(folderPath) Then
WScript.Echo "[SKIP] Circular reference or Junction detected: " & folderPath
Exit Sub
End If
' 訪問済みリストに登録
visitedPaths.Add folderPath, True
' --- 業務ロジックエリア ---
' ここにファイルに対する処理を記述する
' 例: ファイルのログ出力、DBへの登録など
On Error Resume Next ' ファイルアクセス権限エラー対策
For Each file In currentFolder.Files
If Err.Number = 0 Then
' ここで実際の処理を行う(例:ファイル名出力)
' WScript.Echo "File: " & file.Path
Else
WScript.Echo "[ERROR] Access denied to file: " & file.Path
Err.Clear
End If
Next
On Error GoTo 0
' -----------------------
' 4. 子フォルダへの再帰探索
For Each subFolder In currentFolder.SubFolders
On Error Resume Next
If Err.Number = 0 Then
RecursiveScan subFolder, currentDepth + 1, visitedPaths
Else
WScript.Echo "[ERROR] Access denied to folder: " & subFolder.Path
Err.Clear
End If
On Error GoTo 0
Next
' ※ 注意: Dictionaryからの削除は行わない。
' 親ディレクトリを抜ける際に削除してしまうと、
' 別の枝から同じジャンクションに到達した際に検知できなくなるため。
End Sub
4. アーキテクトの視点:実務における注意点
このコードをプロダクション環境に組み込む際、リーダーとして君たちに伝えておきたいことが3点ある。
① パスの正規化(Normalization)の重要性
`folder.Path` は一見完璧に見えるが、環境によっては末尾のバックスラッシュの有無や、大文字小文字の揺れが生じる可能性がある。`Scripting.Dictionary` の `CompareMode = 1`(テキスト比較)は必須だ。これがないと、`C:\Data` と `c:\data` を別物と判断し、防御壁を突破される。
② Dictionaryのメモリ消費量
数百万のディレクトリを走査する場合、`visitedPaths` に格納されるパス文字列がメモリを圧迫する。もしメモリ制限が厳しい環境であれば、パス全体を保持するのではなく、パスの 「ハッシュ値」 を格納することを検討せよ。ただし、VBScriptには標準のハッシュ関数がないため、実装コストとのトレードオフになる。
③ エラーハンドリングの「質」
`On Error Resume Next` は諸刃の剣だ。単にエラーを無視するのではなく、「アクセス権限がない(Permission Denied)」のか「パスが存在しない」のかを区別し、適切にログへ記録しなければならない。業務ツールにおいて、「見落としたファイル」は「存在しないファイル」と同じくらい致命的なのだ。
結論
FSOによるファイル操作は、一見すると低レイヤーの単純な作業に見える。しかし、その背後にあるファイルシステムの複雑性を理解せず、防壁のないコードを書くことは、設計上の怠慢である。
「深度を制限し、訪問を記録する」
この極めてシンプルな原則を守るだけで、君たちのツールは、ジュニアレベルのスクリプトから、エンタープライズに耐えうるプロフェッショナルなシステムへと昇華する。
常に、最悪のケースを想定して設計せよ。
