Skip to content

Commit eb648b4

Browse files
committed
feat: add preflight checks for path length, disk space, and write permission
Before loading configuration, ValidateRunDirectories() now runs three pre-flight checks that all map to exit code 2: 1. Path-length check: the constructed Reports/<label> path is validated against the OS limit via PathValidator.ValidateAbsolutePathLengthOrThrow() (260 chars on Windows, 1024 on macOS, 4096 on Linux). 2. Disk-space check (CheckDiskSpaceOrThrow): requires at least 100 MB free on the target drive via DriveInfo; best-effort skip when drive info is unavailable (network shares, virtual drives). 3. Write-permission check (CheckReportsParentWritableOrThrow): creates and deletes a temporary probe file in the Reports/ parent directory to confirm write access before any real output is produced. Added IOException and UnauthorizedAccessException catches to TryValidateAndBuildRunArguments so all three failures produce exit code 2. Both new helpers are internal static for direct unit testing. Tests added to ProgramRunnerTests: - RunAsync_WhenReportsFolderPathExceedsOsLimit_ReturnsInvalidArgumentsExitCode - CheckDiskSpaceOrThrow_WithSufficientFreeSpace_DoesNotThrow - CheckReportsParentWritableOrThrow_WhenDirectoryIsReadOnly_ThrowsUnauthorizedAccessException Updated README.md (EN+JA) and CHANGELOG.md (EN+JA). https://claude.ai/code/session_0174R4FzjudFBdynScKbhX6m
1 parent 5fe9fec commit eb648b4

4 files changed

Lines changed: 202 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1111

1212
#### Added
1313

14+
- Added three preflight checks to [`ProgramRunner.ValidateRunDirectories()`](ProgramRunner.cs) that run before configuration is loaded and all fail with exit code `2`: (1) **path-length check** — the constructed `Reports/<label>` path is validated against the OS limit (260 chars on Windows without long-path opt-in, 1024 on macOS, 4096 on Linux) via [`PathValidator.ValidateAbsolutePathLengthOrThrow()`](FolderDiffIL4DotNet.Core/IO/PathValidator.cs); (2) **disk-space check** — at least 100 MB of free space is verified on the target drive using `DriveInfo`, skipping best-effort when drive information is unavailable; (3) **write-permission check** — a temporary probe file is created and deleted in the `Reports/` parent directory to confirm write access before any output is produced. Added `IOException` and `UnauthorizedAccessException` catches to `TryValidateAndBuildRunArguments` so all three failures map cleanly to exit code `2`. Added 3 unit/integration tests to [`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs) and updated [README.md](README.md).
1415
- Added `ShouldIncludeILCacheStatsInReport` (default `false`) to [`ConfigSettings`](Models/ConfigSettings.cs). When `true` and the IL cache is active, [`ReportGenerateService`](Services/ReportGenerateService.cs) appends an `IL Cache Stats` section between `Summary` and `Warnings` in `diff_report.md`, showing hits, misses, hit-rate, stores, evicted, and expired counts. Also added `_internalMisses` tracking to [`ILCache`](Services/Caching/ILCache.cs) (miss counter now incremented on full cache miss), a `GetReportStats()` method, and the `ILCacheReportStats` sealed record. Added 3 unit tests to [`ReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs) and updated [`ConfigSettingsTests`](FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs), [README.md](README.md), and [CHANGELOG.md](CHANGELOG.md).
1516
- Expanded CLI options: `--help`/`-h` prints usage and exits with code `0` before any logger initialization; `--version` prints the application version and exits with code `0`; `--config <path>` loads a config file from an arbitrary path instead of the default `<exe>/config.json`; `--threads <N>` overrides `MaxParallelism` in [`ConfigSettings`](Models/ConfigSettings.cs) for the current run; `--no-il-cache` forces `EnableILCache = false` for the current run; `--skip-il` skips IL decompilation and IL diff entirely for .NET assemblies (new `SkipIL` property in [`ConfigSettings`](Models/ConfigSettings.cs), also respected by [`FileDiffService`](Services/FileDiffService.cs)); `--no-timestamp-warnings` suppresses timestamp-regression warnings. Unknown flags now produce exit code `2` with a descriptive message instead of silently being ignored. [`ConfigService.LoadConfigAsync()`](Services/ConfigService.cs) now accepts an optional `configFilePath` parameter. Added [`CliOptionsTests`](FolderDiffIL4DotNet.Tests/CliOptionsTests.cs) with 21 parser unit-test cases, and new integration tests in [`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs) and [`ConfigServiceTests`](FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs).
1617
- Added [`ConfigSettings.Validate()`](Models/ConfigSettings.cs) and the companion `ConfigValidationResult` class; [`ConfigService.LoadConfigAsync()`](Services/ConfigService.cs) now calls `Validate()` immediately after deserialization and throws [`InvalidDataException`](https://learn.microsoft.com/en-us/dotnet/api/system.io.invaliddataexception?view=net-8.0) listing all invalid settings when validation fails, so misconfigured runs are caught at startup with a clear error message instead of failing silently or causing undefined behavior later. Validated constraints: `MaxLogGenerations >= 1`; `TextDiffParallelThresholdKilobytes >= 1`; `TextDiffChunkSizeKilobytes >= 1`; and `TextDiffChunkSizeKilobytes < TextDiffParallelThresholdKilobytes`. Added validation unit tests to [`ConfigSettingsTests`](FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs) (7 cases) and validation integration tests to [`ConfigServiceTests`](FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs) (5 cases).
@@ -251,6 +252,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
251252

252253
#### 追加
253254

255+
- [`ProgramRunner.ValidateRunDirectories()`](ProgramRunner.cs) に 3 つのプリフライトチェックを追加しました。いずれも設定読み込み前に実行され、失敗時は終了コード `2` を返します: (1) **パス長チェック** — 構築した `Reports/<label>` パスが OS の上限(Windows 標準 260 文字、macOS 1024 文字、Linux 4096 文字)を超えていないことを [`PathValidator.ValidateAbsolutePathLengthOrThrow()`](FolderDiffIL4DotNet.Core/IO/PathValidator.cs) で検証します; (2) **ディスク空き容量チェック** — `DriveInfo` を使ってレポートドライブに 100 MB 以上の空き容量があることを確認します(ドライブ情報を取得できない場合は best-effort でスキップ); (3) **書き込み権限チェック** — `Reports/` 親ディレクトリに一時プローブファイルを作成・削除し、出力前に書き込み権限を確認します。あわせて `TryValidateAndBuildRunArguments` に `IOException` と `UnauthorizedAccessException` の catch を追加し、3 つの失敗すべてが終了コード `2` に対応するようにしました。[`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs) にユニット/統合テスト 3 件を追加し、[README.md](README.md) を更新しました。
254256
- [`ConfigSettings`](Models/ConfigSettings.cs) に `ShouldIncludeILCacheStatsInReport`(既定値 `false`)を追加しました。`true` に設定し IL キャッシュが有効な場合、[`ReportGenerateService`](Services/ReportGenerateService.cs) は `diff_report.md` の `Summary` と `Warnings` の間に `IL Cache Stats` セクションを追記します(ヒット数・ミス数・ヒット率・保存数・退避数・期限切れ数)。あわせて [`ILCache`](Services/Caching/ILCache.cs) にミス数追跡フィールド `_internalMisses`(完全なキャッシュミスの際にインクリメント)と `GetReportStats()` メソッド、`ILCacheReportStats` レコードを追加しました。[`ReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs) に 3 件のユニットテストを追加し、[`ConfigSettingsTests`](FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs)、[README.md](README.md)、[CHANGELOG.md](CHANGELOG.md) を更新しました。
255257
- CLI オプションを拡充しました。`--help`/`-h` は使い方を表示してロガー初期化前にコード `0` で終了します。`--version` はアプリバージョンを表示してコード `0` で終了します。`--config <path>` はデフォルトの `<exe>/config.json` に代わり任意のパスから設定ファイルを読み込みます。`--threads <N>` は今回の実行に限り [`ConfigSettings`](Models/ConfigSettings.cs) の `MaxParallelism` を上書きします。`--no-il-cache` は今回の実行に限り `EnableILCache = false` に設定します。`--skip-il` は .NET アセンブリの IL 逆アセンブルと IL 差分比較をまるごとスキップします([`ConfigSettings`](Models/ConfigSettings.cs) に新設した `SkipIL` プロパティとして保持され、[`FileDiffService`](Services/FileDiffService.cs) でも参照します)。`--no-timestamp-warnings` はタイムスタンプ逆転の警告を抑制します。未知のフラグを指定した場合は、これまで黙ってスルーされていた挙動を改め、説明付きで終了コード `2` を返します。[`ConfigService.LoadConfigAsync()`](Services/ConfigService.cs) にオプショナルな `configFilePath` パラメータを追加しました。[`CliOptionsTests`](FolderDiffIL4DotNet.Tests/CliOptionsTests.cs) にパーサー単体テスト 21 件を追加し、[`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs) と [`ConfigServiceTests`](FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs) にも統合テストを追加しました。
256258
- [`ConfigSettings.Validate()`](Models/ConfigSettings.cs) と `ConfigValidationResult` クラスを追加しました。[`ConfigService.LoadConfigAsync()`](Services/ConfigService.cs) はデシリアライズ直後に `Validate()` を呼び出し、バリデーションが失敗した場合は全エラーを列挙した [`InvalidDataException`](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.invaliddataexception?view=net-8.0) をスローします。これにより、設定不正な実行は後から無言で失敗したり未定義の振る舞いを引き起こしたりする代わりに、起動時に分かりやすいエラーメッセージとして検出されます。検証対象の制約: `MaxLogGenerations >= 1`、`TextDiffParallelThresholdKilobytes >= 1`、`TextDiffChunkSizeKilobytes >= 1`、`TextDiffChunkSizeKilobytes < TextDiffParallelThresholdKilobytes`。あわせて [`ConfigSettingsTests`](FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs) にバリデーション単体テスト(7 件)、[`ConfigServiceTests`](FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs) にバリデーション統合テスト(5 件)を追加しました。

FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Reflection;
5+
using System.Runtime.InteropServices;
56
using System.Threading.Tasks;
67
using FolderDiffIL4DotNet.Common;
78
using FolderDiffIL4DotNet.Models;
@@ -492,6 +493,87 @@ await WithConfigFileAsync("{}", async () =>
492493
}
493494
}
494495

496+
// -----------------------------------------------------------------------
497+
// Preflight checks
498+
// -----------------------------------------------------------------------
499+
500+
[Fact]
501+
public async Task RunAsync_WhenReportsFolderPathExceedsOsLimit_ReturnsInvalidArgumentsExitCode()
502+
{
503+
var tempRoot = Path.Combine(Path.GetTempPath(), "fd-preflight-pathlen-" + Guid.NewGuid().ToString("N"));
504+
var oldDir = Path.Combine(tempRoot, "old");
505+
var newDir = Path.Combine(tempRoot, "new");
506+
Directory.CreateDirectory(oldDir);
507+
Directory.CreateDirectory(newDir);
508+
var logger = new TestLogger();
509+
var runner = new ProgramRunner(logger, new ConfigService());
510+
511+
// A label long enough so that BaseDirectory + "/Reports/" + label exceeds any OS path limit
512+
var longLabel = new string('a', 4096);
513+
514+
try
515+
{
516+
await WithMissingConfigFileAsync(async () =>
517+
{
518+
var exitCode = await runner.RunAsync(new[] { oldDir, newDir, longLabel, "--no-pause" });
519+
520+
Assert.Equal(2, exitCode);
521+
Assert.Contains(logger.Messages, m => m.Contains("too long", StringComparison.OrdinalIgnoreCase));
522+
});
523+
}
524+
finally
525+
{
526+
TryDeleteDirectory(tempRoot);
527+
}
528+
}
529+
530+
[Fact]
531+
public void CheckDiskSpaceOrThrow_WithSufficientFreeSpace_DoesNotThrow()
532+
{
533+
// Verifies that the disk-space check passes silently on a normal system.
534+
var ex = Record.Exception(() => ProgramRunner.CheckDiskSpaceOrThrow(Path.GetTempPath()));
535+
Assert.Null(ex);
536+
}
537+
538+
[Fact]
539+
public void CheckReportsParentWritableOrThrow_WhenDirectoryIsReadOnly_ThrowsUnauthorizedAccessException()
540+
{
541+
// This test requires Unix file-mode semantics and a non-root user.
542+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
543+
!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
544+
{
545+
return;
546+
}
547+
548+
if (string.Equals(Environment.UserName, "root", StringComparison.OrdinalIgnoreCase))
549+
{
550+
return; // root bypasses Unix permission checks
551+
}
552+
553+
var dir = Path.Combine(Path.GetTempPath(), "fd-perm-test-" + Guid.NewGuid().ToString("N"));
554+
Directory.CreateDirectory(dir);
555+
try
556+
{
557+
// Remove write permission from the directory (read + execute only)
558+
File.SetUnixFileMode(dir, UnixFileMode.UserRead | UnixFileMode.UserExecute);
559+
560+
// Pass a path whose parent is `dir` — the check probes a file inside `dir`
561+
Assert.Throws<UnauthorizedAccessException>(() =>
562+
ProgramRunner.CheckReportsParentWritableOrThrow(Path.Combine(dir, "label")));
563+
}
564+
finally
565+
{
566+
try
567+
{
568+
File.SetUnixFileMode(dir, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
569+
}
570+
catch (IOException) { }
571+
catch (UnauthorizedAccessException) { }
572+
573+
TryDeleteDirectory(dir);
574+
}
575+
}
576+
495577
private static async Task WithMissingConfigFileAsync(Func<Task> assertion)
496578
{
497579
var backupExists = File.Exists(ConfigFilePath);

0 commit comments

Comments
 (0)