using ClawdDotNet.Core.Storage;
namespace ClawdDotNet.Tools.FileRW;
///
/// Löst relative Pfade innerhalb eines Workspace auf und stellt sicher, dass sie
/// diesen nicht verlassen.
///
/// Hintergrund (S6): Die Prüfung verglich vorher nur die Zeichenkette:
///
/// fullPath.StartsWith(rootPath, OrdinalIgnoreCase)
///
/// Ohne abschließenden Verzeichnistrenner erlaubte ein Root wie
/// "…\Agent-X\Workspace" damit auch Zugriffe auf "…\Agent-X\Workspace-Backup\…" —
/// ein fremdes Verzeichnis, das zufällig mit demselben Präfix beginnt.
///
/// Der Vergleich selbst liegt in : Unter Linux
/// unterscheidet das Dateisystem Groß- und Kleinschreibung, und symbolische
/// Verknüpfungen können aus dem Workspace hinauszeigen, ohne dass die reine
/// Pfadrechnung das sieht.
///
public static class WorkspacePath
{
///
/// Setzt auf auf und prüft,
/// dass das Ergebnis innerhalb des Workspace liegt.
///
/// Wenn der Pfad ausbricht.
public static string Resolve(string root, string? relativePath, string workspaceName)
{
var value = string.IsNullOrWhiteSpace(relativePath) ? "." : relativePath.Trim();
// Absolute Pfade und UNC-Freigaben würden Path.Combine den Root verwerfen lassen.
if (Path.IsPathRooted(value) || value.StartsWith("//"))
{
throw new UnauthorizedAccessException(
$"Access denied: Absolute paths are not allowed in the {workspaceName} workspace.");
}
// Doppelpunkt und Backslash werden rundheraus abgewiesen.
//
// Beide sind unter Linux gewöhnliche Namenszeichen, und genau daran scheiterte
// die Abwehr der Windows-Formen: Path.IsPathRooted(@"C:\temp\x") meldet dort
// false, und @"..\..\evil.txt" ist ein einzelner Dateiname statt eines
// Ausbruchs. Ein Ausbruch war das nie — die Datei blieb im Workspace —, aber
// eine Sonderregel je Windows-Form (Doppelpunkt für Laufwerke, "\\" für UNC)
// lässt immer eine Form übrig, die durchfällt. Hier gilt darum eine
// Schreibweise: "/" trennt Abschnitte, alles andere ist ein Name. Beide Zeichen
// sind unter Linux in Dateinamen zwar erlaubt, aber nichts, was ein Agent
// braucht; die Einschränkung kostet uns nichts.
//
// Der Doppelpunkt fängt nebenbei Alternate Data Streams (datei.txt:versteckt),
// die unter Windows die Endungsprüfung umgehen.
if (value.Contains(':') || value.Contains('\\'))
{
throw new UnauthorizedAccessException(
@"Access denied: Path contains an invalid character (':' or '\'). " +
"Sections are separated with '/'.");
}
var normalizedRoot = NormalizeDirectory(root);
var fullPath = Path.GetFullPath(Path.Combine(normalizedRoot, value));
if (!IsInside(fullPath, normalizedRoot))
{
throw new UnauthorizedAccessException(
$"Access denied: Path is outside the {workspaceName} workspace.");
}
return fullPath;
}
///
/// Prüft, ob im Verzeichnis liegt.
/// Der Vergleich erfolgt auf Verzeichnisgrenzen, nicht auf Zeichenketten-Präfixen,
/// und löst symbolische Verknüpfungen auf.
///
public static bool IsInside(string candidate, string root)
=> PathBoundary.IsInside(candidate, root);
/// Absoluter Pfad mit genau einem abschließenden Trenner.
private static string NormalizeDirectory(string path)
=> PathBoundary.NormalizeDirectory(path);
}