DataTransfer/DataTransfer.Client/Services/FileNameSanitizer.cs
2026-10-15 20:09:11 +02:00

54 lines
2 KiB
C#

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 = "<>:\"/\\|?*";
/// <summary>
/// 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 <c>invoice&#x202E;fdp.exe</c> as <c>invoiceexe.pdf</c>.
/// </summary>
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());
}
}
}