using ClawdDotNet.Core.Storage; using ClawdDotNet.Core.Tests.Infrastructure; using Shouldly; namespace ClawdDotNet.Core.Tests.Storage; /// /// Die Einschließungsprüfung verglich bis zur Linux-Portierung mit /// OrdinalIgnoreCase — die Windows-Annahme, dass Groß- und Kleinschreibung /// zusammenfallen. Unter Linux sind Workspace und workspace zwei /// Verzeichnisse, und eine symbolische Verknüpfung kann aus dem Wurzelverzeichnis /// hinauszeigen, ohne dass die reine Pfadrechnung das bemerkt. /// /// Das Dateisystem wird hier bewusst nicht abstrahiert: Genau die Semantik, um die es /// geht, würde eine Abstraktion verstecken. /// public sealed class PathBoundaryTests : IDisposable { private readonly string _base; private readonly string _root; public PathBoundaryTests() { _base = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N")); _root = Path.Combine(_base, "Workspace"); Directory.CreateDirectory(_root); } public void Dispose() { try { Directory.Delete(_base, recursive: true); } catch { /* Aufräumen ist Nebensache */ } } // ═══════════════════════════════════════════════════════════ // Verzeichnisgrenzen (auf beiden Plattformen gleich) // ═══════════════════════════════════════════════════════════ [Fact] public void Ein_Nachbarverzeichnis_mit_gleichem_Praefix_liegt_ausserhalb() { PathBoundary.IsInside(Path.Combine(_root + "-Backup", "geheim.txt"), _root) .ShouldBeFalse(); } [Fact] public void Das_Wurzelverzeichnis_selbst_liegt_innerhalb() { PathBoundary.IsInside(_root, _root).ShouldBeTrue(); PathBoundary.IsInside(_root + Path.DirectorySeparatorChar, _root).ShouldBeTrue(); } [Fact] public void Ein_Unterverzeichnis_liegt_innerhalb() { PathBoundary.IsInside(Path.Combine(_root, "a", "b", "c.txt"), _root).ShouldBeTrue(); } [Fact] public void Punkt_Punkt_fuehrt_hinaus() { PathBoundary.IsInside(Path.Combine(_root, "..", "evil.txt"), _root).ShouldBeFalse(); } // ═══════════════════════════════════════════════════════════ // Groß-/Kleinschreibung — hier gehen die Plattformen auseinander // ═══════════════════════════════════════════════════════════ [LinuxFact] public void Unter_Linux_ist_ein_Verzeichnis_mit_anderer_Schreibweise_ein_anderes() { // "workspace" neben "Workspace": zwei echte, getrennte Verzeichnisse. var lower = Path.Combine(_base, "workspace"); Directory.CreateDirectory(lower); PathBoundary.IsInside(Path.Combine(lower, "geheim.txt"), _root).ShouldBeFalse(); } [WindowsFact] public void Unter_Windows_meint_eine_andere_Schreibweise_dasselbe_Verzeichnis() { var lower = Path.Combine(_base, "workspace"); PathBoundary.IsInside(Path.Combine(lower, "datei.txt"), _root).ShouldBeTrue(); } [Fact] public void Der_Schluessel_fuer_Sperren_folgt_der_Plattform() { var upper = Path.Combine(_base, "Config.json"); var lower = Path.Combine(_base, "config.json"); var gleich = PathBoundary.CanonicalKey(upper) == PathBoundary.CanonicalKey(lower); // Unter Windows dieselbe Datei — dieselbe Sperre. Unter Linux zwei Dateien, // die sich keine teilen dürfen. gleich.ShouldBe(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); } // ═══════════════════════════════════════════════════════════ // Symbolische Verknüpfungen // ═══════════════════════════════════════════════════════════ [LinuxFact] public void Eine_Verknuepfung_aus_dem_Wurzelverzeichnis_heraus_wird_erkannt() { // Der Fall, den die reine Pfadrechnung nicht sieht: "Workspace/raus" zeigt // nach draußen, "Workspace/raus/beute.txt" liegt lexikalisch aber innerhalb. var draussen = Path.Combine(_base, "Draussen"); Directory.CreateDirectory(draussen); Directory.CreateSymbolicLink(Path.Combine(_root, "raus"), draussen); PathBoundary.IsInside(Path.Combine(_root, "raus", "beute.txt"), _root) .ShouldBeFalse(); } [LinuxFact] public void Eine_Verknuepfung_innerhalb_des_Wurzelverzeichnisses_bleibt_erlaubt() { // Gegenprobe: Wer innerhalb bleibt, wird nicht ausgesperrt. var ziel = Path.Combine(_root, "echte-daten"); Directory.CreateDirectory(ziel); Directory.CreateSymbolicLink(Path.Combine(_root, "abkuerzung"), ziel); PathBoundary.IsInside(Path.Combine(_root, "abkuerzung", "bericht.md"), _root) .ShouldBeTrue(); } [LinuxFact] public void Ein_Wurzelverzeichnis_hinter_einer_Verknuepfung_sperrt_sich_nicht_selbst_aus() { // Liegt der Workspace selbst hinter einer Verknüpfung (üblich bei /var → /private/var // oder gemounteten Datenverzeichnissen), muss die Auflösung auf beiden Seiten // greifen — sonst passt nach dem Auflösen nichts mehr zusammen. var echt = Path.Combine(_base, "EchterOrt"); Directory.CreateDirectory(echt); var ueber = Path.Combine(_base, "Verknuepft"); Directory.CreateSymbolicLink(ueber, echt); PathBoundary.IsInside(Path.Combine(ueber, "datei.txt"), ueber).ShouldBeTrue(); PathBoundary.IsInside(Path.Combine(echt, "datei.txt"), ueber).ShouldBeTrue(); } [Fact] public void Nicht_existierende_Pfade_bleiben_pruefbar() { // Beim Schreiben gibt es die Datei noch nicht. Was nicht existiert, kann keine // Verknüpfung sein — die Auflösung darf daran nicht scheitern. PathBoundary.IsInside(Path.Combine(_root, "gibt", "es", "noch", "nicht.txt"), _root) .ShouldBeTrue(); } }