Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/FileRW/WorkspacePathTests.cs
T
RichardandClaude Opus 5.5 837a634a22 Stufe 4c (#764): Kommentare in C#, WebUI-Skripten, CSS und HTML auf Englisch
Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig
erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner
Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft.
Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch.

WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b
umbenannten Parameter angeglichen.

Dabei behoben:
- 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue"
  gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day.
- Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine
  CSS-Klasse gab - jetzt "success" wie laufende Instanzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:45:03 +02:00

164 lines
6.3 KiB
C#

using ClawdDotNet.Tools.FileRW;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.FileRW;
/// <summary>
/// S6 from the inventory: the path check only compared string prefixes.
/// Without a trailing directory separator a root "…\Workspace" thus also allowed
/// "…\Workspace-Backup\…".
///
/// The file system is deliberately NOT abstracted here — the tests are supposed to check the real
/// path semantics of the respective platform (.., UNC, alternate data streams,
/// trailing dots, under Linux additionally upper/lower case and
/// links). An abstraction would hide exactly the class of errors this is
/// about.
///
/// The directory boundary itself is checked by <c>PathBoundaryTests</c> in Core.Tests; here it
/// is about what <see cref="WorkspacePath.Resolve"/> rejects beyond that.
/// </summary>
public sealed class WorkspacePathTests : IDisposable
{
private readonly string _root;
public WorkspacePathTests()
{
_root = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Workspace");
Directory.CreateDirectory(_root);
}
public void Dispose()
{
try { Directory.Delete(Path.GetDirectoryName(_root)!, recursive: true); }
catch { /* Cleaning up is secondary */ }
}
private string Resolve(string? relative) => WorkspacePath.Resolve(_root, relative, "personal");
// ═══════════════════════════════════════════════════════════
// Breakout attempts
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("../../../Windows/System32/drivers/etc/hosts")]
[InlineData(@"..\..\evil.txt")]
[InlineData("unterordner/../../../ausserhalb.txt")]
[InlineData("./../../evil.txt")]
[InlineData("..")]
public void Relative_Ausbrueche_werden_abgelehnt(string path)
{
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
}
/// <summary>
/// The Windows forms must be rejected under Linux too — but there a different
/// rule applies: <c>Path.IsPathRooted(@"C:\temp\x")</c> is <c>false</c> under Linux,
/// because that is simply a file name with a colon. These cases are caught
/// by the character rule: colon and backslash are not permitted in the
/// path, separation is done with <c>/</c>. The result is the same on both
/// platforms, the way there is not.
///
/// The same rule also catches <c>@"..\..\evil.txt"</c> from the test
/// above under Linux. That was never a breakout there — the backslash separates
/// nothing under Linux, the file would have landed in the workspace —, but the test expected a
/// rejection and got none.
/// </summary>
[Theory]
[InlineData(@"C:\Windows\System32\config\SAM")]
[InlineData(@"\\server\share\evil.txt")]
[InlineData("//server/share/evil.txt")]
[InlineData(@"C:\temp\datei.txt")]
public void Absolute_Pfade_und_UNC_Freigaben_werden_abgelehnt(string path)
{
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
}
/// <summary>
/// The Linux equivalents. Under Windows these are ordinary relative paths
/// that either point nowhere or are caught by the <c>..</c> rule —
/// that's why they run along there instead of being skipped.
/// </summary>
[Theory]
[InlineData("/etc/passwd")]
[InlineData("/root/.ssh/id_rsa")]
[InlineData("../../../../etc/shadow")]
public void Unix_Systempfade_werden_abgelehnt(string path)
{
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
}
[Theory]
[InlineData("datei.txt:versteckt")]
[InlineData("datei.txt:$DATA")]
public void Alternate_Data_Streams_werden_abgelehnt(string path)
{
// An ADS would otherwise bypass the extension check: "x.txt:evil.exe".
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
}
/// <summary>
/// The actual core of S6: a neighboring directory with the same prefix.
/// </summary>
[Fact]
public void Ein_Nachbarverzeichnis_mit_gleichem_Praefix_gilt_als_ausserhalb()
{
var backup = _root + "-Backup";
WorkspacePath.IsInside(Path.Combine(backup, "geheim.txt"), _root).ShouldBeFalse();
}
[Fact]
public void Auch_bei_Rootangabe_mit_Trenner_bleibt_das_Nachbarverzeichnis_aussen()
{
var rootWithSeparator = _root + Path.DirectorySeparatorChar;
WorkspacePath.IsInside(_root + "-Backup" + Path.DirectorySeparatorChar + "x.txt", rootWithSeparator)
.ShouldBeFalse();
}
// ═══════════════════════════════════════════════════════════
// Counter-checks — normal use must work
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("bericht.md")]
[InlineData("unterordner/bericht.md")]
[InlineData("a/b/c/tief.json")]
[InlineData("./bericht.md")]
[InlineData("unterordner/../bericht.md")]
public void Pfade_innerhalb_des_Workspace_werden_aufgeloest(string path)
{
var resolved = Resolve(path);
WorkspacePath.IsInside(resolved, _root).ShouldBeTrue();
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData(".")]
public void Leere_Angaben_ergeben_das_Wurzelverzeichnis(string? path)
{
var resolved = Resolve(path);
resolved.TrimEnd(Path.DirectorySeparatorChar)
.ShouldBe(Path.GetFullPath(_root).TrimEnd(Path.DirectorySeparatorChar));
}
[Fact]
public void Das_Wurzelverzeichnis_selbst_gilt_als_innerhalb()
{
WorkspacePath.IsInside(_root, _root).ShouldBeTrue();
}
[Fact]
public void Ein_Unterverzeichnis_mit_aehnlichem_Namen_bleibt_innerhalb()
{
// Counter-check to the prefix rule: within the root everything is allowed.
var inner = Path.Combine(_root, "Workspace-Backup", "x.txt");
WorkspacePath.IsInside(inner, _root).ShouldBeTrue();
}
}