54 lines
2 KiB
C#
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‮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());
|
|
}
|
|
}
|
|
}
|