Newer
Older
Sunfish / src / Sunfish.Core / Shares / PathRules.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 4 KB 3.0
using System.Text;

namespace Sunfish.Shares;

/// <summary>
/// Validacion de los nombres que llegan por la red (web, FTP, WebDAV) antes de tocar el disco.
/// Es la misma para todos los protocolos.
/// </summary>
public static class PathRules
{
    private static readonly HashSet<string> WindowsReserved = new(StringComparer.OrdinalIgnoreCase)
    {
        "CON", "PRN", "AUX", "NUL",
        "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
        "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
    };

    /// <summary>Un componente de ruta valido en cualquier sistema (y ademas en Windows si se pide).</summary>
    public static bool IsValidSegment(string segment, bool windows)
    {
        if (segment.Length == 0 || segment == "." || segment == "..")
            return false;
        foreach (char c in segment)
        {
            if (c < 0x20 || c == 0x7F || c == '/' || c == '\\' || c == ':')
                return false;
        }
        if (windows)
        {
            if (segment.EndsWith('.') || segment.EndsWith(' '))
                return false;
            if (segment.IndexOfAny(['<', '>', '"', '|', '?', '*']) >= 0)
                return false;
            int dot = segment.IndexOf('.');
            string stem = dot < 0 ? segment : segment[..dot];
            if (WindowsReserved.Contains(stem.TrimEnd(' ')))
                return false;
        }
        return true;
    }

    public static bool IsValidSegment(string segment) => IsValidSegment(segment, OperatingSystem.IsWindows());

    /// <summary>
    /// Parte una ruta de URL ("/a/b%20c/") en componentes decodificados. Los componentes vacios
    /// ("//") se ignoran. Devuelve false si algun componente no es valido.
    /// </summary>
    public static bool TrySplitUrlPath(string path, out List<string> segments, out bool trailingSlash)
    {
        segments = new List<string>();
        trailingSlash = path.EndsWith('/');
        foreach (var raw in path.Split('/'))
        {
            if (raw.Length == 0)
                continue;
            string segment = PercentDecode(raw);
            if (!IsValidSegment(segment))
                return false;
            segments.Add(segment);
        }
        return true;
    }

    /// <summary>
    /// Decodifica %XX como UTF-8. Si los bytes no son UTF-8 valido (navegadores antiguos que envian
    /// el juego de caracteres de la pagina) se interpretan como Latin-1.
    /// </summary>
    public static string PercentDecode(string text)
    {
        if (text.IndexOf('%') < 0)
            return text;
        var bytes = new List<byte>(text.Length);
        for (int i = 0; i < text.Length; i++)
        {
            char c = text[i];
            if (c == '%' && i + 2 < text.Length && IsHex(text[i + 1]) && IsHex(text[i + 2]))
            {
                bytes.Add((byte)(HexValue(text[i + 1]) * 16 + HexValue(text[i + 2])));
                i += 2;
            }
            else if (c < 0x80)
            {
                bytes.Add((byte)c);
            }
            else
            {
                bytes.AddRange(Encoding.UTF8.GetBytes(c.ToString()));
            }
        }
        var array = bytes.ToArray();
        try
        {
            return new UTF8Encoding(false, throwOnInvalidBytes: true).GetString(array);
        }
        catch (DecoderFallbackException)
        {
            return Encoding.Latin1.GetString(array);
        }
    }

    private static bool IsHex(char c) => char.IsAsciiHexDigit(c);

    private static int HexValue(char c) => c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10;
}

/// <summary>Locations de los shares: "/", "/fotos", "/fotos/viaje".</summary>
public static class LocationPath
{
    public const string Reserved = "$sunfish";

    /// <summary>Normaliza a "/a/b" (sin barra final salvo la raiz). Rechaza componentes invalidos y /$sunfish.</summary>
    public static bool TryNormalize(string location, out string normalized)
    {
        normalized = "/";
        var parts = new List<string>();
        foreach (var part in location.Trim().Split('/', '\\'))
        {
            if (part.Length == 0)
                continue;
            if (!PathRules.IsValidSegment(part, windows: false) || part.Contains('%'))
                return false;
            parts.Add(part);
        }
        if (parts.Count > 0 && parts[0].Equals(Reserved, StringComparison.OrdinalIgnoreCase))
            return false;
        normalized = "/" + string.Join('/', parts);
        return true;
    }

    public static string[] Segments(string normalized) =>
        normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
}