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);
}