using System.Globalization; using System.Text; namespace DataTransfer.Client.Services; public static class FileNameSanitizer { private const int MaxLength = 255; private const string Fallback = "file"; // Characters invalid in file names on Windows, macOS or Linux. Path.GetInvalidFileNameChars() returns only the // Unix set in WebAssembly, but the file is saved on any operating system. private const string InvalidCharacters = "<>:\"/\\|?*"; /// /// Makes a file name received from the peer safe to display and save. Invisible formatting characters are removed, /// so for example a right-to-left override can't disguise invoice‮fdp.exe as invoiceexe.pdf. /// public static string Sanitize(string? name) { if (string.IsNullOrEmpty(name)) { return Fallback; } var builder = new StringBuilder(Math.Min(name.Length, MaxLength)); foreach (var rune in name.EnumerateRunes()) { switch (Rune.GetUnicodeCategory(rune)) { case UnicodeCategory.Format: continue; case UnicodeCategory.Control: Append(builder, new Rune('_')); continue; } Append(builder, rune.IsAscii && InvalidCharacters.Contains((char)rune.Value) ? new Rune('_') : rune); } // Windows silently drops trailing dots and spaces, which could change the extension. var cleaned = builder.ToString().Trim().TrimEnd('.'); return cleaned.Length == 0 ? Fallback : cleaned; } // Appends whole runes only, so truncation never splits a surrogate pair. private static void Append(StringBuilder builder, Rune rune) { if (builder.Length + rune.Utf16SequenceLength <= MaxLength) { builder.Append(rune.ToString()); } } }