using System.Globalization;
using System.Text;
namespace Sunfish.Shares;
/// <summary>
/// Alias 8.3 para DOS y Windows 3.1 (DOC/CLIENTES.md). Son estables: dependen del nombre (un hash),
/// no del orden de la carpeta, asi que un listado o un enlace antiguo no acaban apuntando a otro
/// fichero cuando se anaden o quitan otros. "Fotos del viaje.jpeg" -> "FOTxxx~1.JPE" (xxx: el hash).
/// </summary>
public static class ShortNames
{
private const string Allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'()-@^_`{}~";
/// <summary>El nombre ya es 8.3 valido (sin distinguir mayusculas).</summary>
public static bool Is83(string name)
{
int dot = name.IndexOf('.');
string stem = dot < 0 ? name : name[..dot];
string ext = dot < 0 ? "" : name[(dot + 1)..];
return stem.Length is >= 1 and <= 8 && ext.Length <= 3 && ext.IndexOf('.') < 0 &&
(dot < 0 || ext.Length > 0) &&
stem.ToUpperInvariant().All(c => Allowed.Contains(c)) && ext.ToUpperInvariant().All(c => Allowed.Contains(c));
}
/// <summary>Alias de un nombre suelto (sin tener en cuenta colisiones).</summary>
public static string Alias(string name, int number = 1)
{
if (number == 1 && Is83(name))
return name;
int dot = name.LastIndexOf('.');
string stem = dot > 0 ? name[..dot] : name;
string ext = dot > 0 ? Clean(name[(dot + 1)..], 3) : "";
string prefix = Clean(stem, 3);
if (prefix.Length == 0)
prefix = "_";
string hash = (Fnv1a(name) % 0x1000).ToString("X3", CultureInfo.InvariantCulture);
string tail = "~" + number.ToString(CultureInfo.InvariantCulture);
string baseName = (prefix + hash)[..Math.Min(prefix.Length + 3, 8 - tail.Length)] + tail;
return ext.Length > 0 ? baseName + "." + ext : baseName;
}
/// <summary>
/// Alias de todos los nombres de una carpeta. Si dos coinciden, el segundo en orden alfabetico
/// pasa a ~2, y asi sucesivamente (muy raro).
/// </summary>
public static Dictionary<string, string> Map(IEnumerable<string> names)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var ordered = names.OrderBy(n => n, StringComparer.Ordinal).ToList();
// Primero los que ya son 8.3: conservan su nombre tal cual (DOS no distingue mayusculas).
foreach (var name in ordered.Where(Is83))
{
result[name] = name;
used.Add(result[name]);
}
foreach (var name in ordered.Where(n => !Is83(n)))
{
for (int n = 1; ; n++)
{
string alias = Alias(name, n);
if (used.Add(alias))
{
result[name] = alias;
break;
}
}
}
return result;
}
/// <summary>Nombre real dentro de una carpeta a partir de su alias (null si no hay ninguno).</summary>
public static string? Resolve(string physicalDirectory, string alias)
{
if (!Directory.Exists(physicalDirectory))
return null;
var names = new DirectoryInfo(physicalDirectory).EnumerateFileSystemInfos().Select(i => i.Name).ToList();
foreach (var (name, a) in Map(names))
if (a.Equals(alias, StringComparison.OrdinalIgnoreCase))
return name;
return null;
}
/// <summary>Solo los caracteres validos en 8.3, transliterados a ASCII y en mayusculas.</summary>
private static string Clean(string text, int max)
{
var sb = new StringBuilder();
foreach (char c in text.Normalize(NormalizationForm.FormD))
{
if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark)
continue;
char u = char.ToUpperInvariant(c);
if (Allowed.Contains(u) && u != '~')
sb.Append(u);
if (sb.Length == max)
break;
}
return sb.ToString();
}
private static uint Fnv1a(string text)
{
uint hash = 2166136261;
foreach (byte b in Encoding.UTF8.GetBytes(text))
{
hash ^= b;
hash *= 16777619;
}
return hash;
}
}