diff --git a/TIASshot.Tests/IoUtilCreateSaveFolderTests.cs b/TIASshot.Tests/IoUtilCreateSaveFolderTests.cs
new file mode 100644
index 0000000..c0c1627
--- /dev/null
+++ b/TIASshot.Tests/IoUtilCreateSaveFolderTests.cs
@@ -0,0 +1,103 @@
+using System;
+using System.IO;
+using NUnit.Framework;
+
+namespace TIASshot.Tests {
+ ///
+ /// IoUtil.CreateSaveFolder のテスト.
+ /// データ名(ユーザー入力)がフォルダ名要素として無害化され,
+ /// 保存先が設定ベースフォルダ(File/SaveFolder)の外へ出ないことを検証する.
+ ///
+ [TestFixture]
+ public class IoUtilCreateSaveFolderTests {
+
+ private string _rootDir; // テスト用ルート(脱出検知用の外側)
+ private string _baseSaveFolder; // config の File/SaveFolder に設定するベース
+ private string _tempConfigPath;
+
+ [SetUp]
+ public void SetUp() {
+ // 脱出をルート内に閉じ込めて観測できるよう,ベースを深い位置に置く
+ _rootDir = Path.Combine(Path.GetTempPath(), "TIASshotTests_" + Guid.NewGuid().ToString("N"));
+ _baseSaveFolder = Path.Combine(_rootDir, "outer", "SaveFolder");
+ Directory.CreateDirectory(_baseSaveFolder);
+
+ _tempConfigPath = Path.Combine(_rootDir, "config.xml");
+ var xml =
+ "" +
+ "" + _baseSaveFolder + "";
+ File.WriteAllText(_tempConfigPath, xml, System.Text.Encoding.UTF8);
+ Config.Load(_tempConfigPath);
+ }
+
+ [TearDown]
+ public void TearDown() {
+ if (Directory.Exists(_rootDir)) Directory.Delete(_rootDir, true);
+ }
+
+ ///
+ /// 生成されたフォルダがベースフォルダ配下かどうかを判定する
+ ///
+ private bool IsUnderBase(string path) {
+ var full = Path.GetFullPath(path);
+ var baseFull = Path.GetFullPath(_baseSaveFolder)
+ .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
+ return full.StartsWith(baseFull, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// データ名に ..\ トラバーサルを含めても保存先がベースフォルダの外へ出ない
+ ///
+ [Test]
+ public void CreateSaveFolder_トラバーサル入力_ベースフォルダ配下に留まる() {
+ // Arrange: ".." 4 個で SaveFolder\日付\時刻- の 3 段を超えて脱出を試みる入力
+ var note = @"..\..\..\..\evil";
+
+ // Act
+ var result = IoUtil.CreateSaveFolder(note);
+
+ // Assert
+ Assert.IsTrue(IsUnderBase(result),
+ $"保存フォルダがベースフォルダの外に生成された: {Path.GetFullPath(result)}");
+ Assert.IsFalse(Directory.Exists(Path.Combine(_rootDir, "evil")),
+ "ベースフォルダ外に 'evil' ディレクトリが作られてはならない");
+ }
+
+ ///
+ /// データ名に不正なファイル名文字(: ? 等)を含めても例外にならず,配下に生成される
+ ///
+ [Test]
+ public void CreateSaveFolder_不正文字入力_例外を出さず配下に生成() {
+ // Arrange
+ var note = "a:b?c";
+
+ // Act
+ string result = null;
+ Assert.DoesNotThrow(() => { result = IoUtil.CreateSaveFolder(note); },
+ "不正文字を含むデータ名で例外が発生してはならない(撮影中断の可用性影響)");
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsTrue(Directory.Exists(result), "保存フォルダが生成されていること");
+ Assert.IsTrue(IsUnderBase(result), "保存フォルダはベースフォルダ配下であること");
+ }
+
+ ///
+ /// 通常のデータ名(安全な文字のみ)はそのままフォルダ名末尾に使われる(現状挙動の固定)
+ ///
+ [Test]
+ public void CreateSaveFolder_通常入力_データ名がフォルダ名に保持される() {
+ // Arrange
+ var note = "subject01";
+
+ // Act
+ var result = IoUtil.CreateSaveFolder(note);
+
+ // Assert
+ Assert.IsTrue(Directory.Exists(result), "保存フォルダが生成されていること");
+ Assert.IsTrue(IsUnderBase(result), "保存フォルダはベースフォルダ配下であること");
+ StringAssert.EndsWith("-subject01", Path.GetFileName(result),
+ "フォルダ名末尾にデータ名がそのまま保持されること");
+ }
+ }
+}
diff --git a/TIASshot.Tests/TIASshot.Tests.csproj b/TIASshot.Tests/TIASshot.Tests.csproj
index 829bc8b..6cf0344 100644
--- a/TIASshot.Tests/TIASshot.Tests.csproj
+++ b/TIASshot.Tests/TIASshot.Tests.csproj
@@ -59,6 +59,7 @@
+