namespace Metin2Launcher.Apply;

/// <summary>
/// Guards against manifest-supplied paths escaping the client root.
///
/// Every path in a manifest is a relative path under the client directory. A malicious
/// or buggy manifest that contains <c>..</c> segments, absolute paths, or drive letters
/// could otherwise trick the applier into writing outside the client root
/// (e.g. <c>../../../etc/passwd</c> or <c>C:\Windows\System32\evil.dll</c>). The
/// signature verifier is our primary defence — an attacker cannot produce a valid
/// manifest without the private key — but defence-in-depth demands this check too.
/// </summary>
public static class PathSafety
{
    /// <summary>
    /// Resolves <paramref name="relativePath"/> under <paramref name="clientRoot"/> and
    /// confirms the result stays inside the root. Returns the absolute final path.
    /// Throws <see cref="UnsafePathException"/> if the path escapes.
    /// </summary>
    public static string ResolveInside(string clientRoot, string relativePath)
    {
        if (string.IsNullOrEmpty(relativePath))
            throw new UnsafePathException("empty manifest path");

        if (Path.IsPathRooted(relativePath))
            throw new UnsafePathException($"rooted manifest path not allowed: {relativePath}");

        // Normalise to the native separator so Path.GetFullPath handles segments uniformly.
        var localized = relativePath.Replace('/', Path.DirectorySeparatorChar);

        var rootFull  = Path.GetFullPath(clientRoot);
        var candidate = Path.GetFullPath(Path.Combine(rootFull, localized));

        var rootWithSep = rootFull.EndsWith(Path.DirectorySeparatorChar)
            ? rootFull
            : rootFull + Path.DirectorySeparatorChar;

        if (candidate.Length < rootWithSep.Length ||
            !candidate.StartsWith(rootWithSep, StringComparison.Ordinal))
        {
            throw new UnsafePathException(
                $"manifest path escapes client root: {relativePath} -> {candidate}");
        }

        return candidate;
    }
}

public sealed class UnsafePathException : Exception
{
    public UnsafePathException(string message) : base(message) { }
}
