mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-14 15:53:30 -07:00
Fix NTFS filename character sanitization on non-Windows systems
Replace OS-specific Path.GetInvalidFileNameChars() with a comprehensive list of characters invalid on common filesystems (NTFS, FAT32, ext4, etc.) to ensure files can be created on any filesystem regardless of host OS. - Add InvalidFileNameChars array with all NTFS/Windows forbidden chars - Add comprehensive unit tests for filename sanitization - Fixes issue where '?', ':', '*', '<', '>', '|', '"', '\' were not sanitized on Linux Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
This commit is contained in:
parent
8eeefeb2a1
commit
f423e6d262
129
DiscordChatExporter.Cli.Tests/Specs/PathSanitizationSpecs.cs
Normal file
129
DiscordChatExporter.Cli.Tests/Specs/PathSanitizationSpecs.cs
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
using System.IO;
|
||||||
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|
||||||
|
public class PathSanitizationSpecs
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_question_mark_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("How to do this?");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("How to do this_");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_colon_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("Title: Subtitle");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("Title_ Subtitle");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_asterisk_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("File*Name");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("File_Name");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_backslash_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("Path\\File");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("Path_File");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_forward_slash_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("Path/File");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("Path_File");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_double_quote_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("Say \"Hello\"");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("Say _Hello_");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_less_than_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("Value<10");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("Value_10");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_greater_than_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("Value>10");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("Value_10");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_pipe_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("Option A | Option B");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("Option A _ Option B");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_multiple_invalid_characters_is_sanitized()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("How? Why: This/That <works> |right|");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("How_ Why_ This_That _works_ _right_");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_valid_characters_is_not_changed()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("Valid File Name 123");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("Valid File Name 123");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_with_special_but_valid_characters_is_not_changed()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = Path.EscapeFileName("File (with) [brackets] & symbols! @#$%^");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be("File (with) [brackets] & symbols! @#$%^");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,31 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Utils.Extensions;
|
namespace DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
public static class PathExtensions
|
public static class PathExtensions
|
||||||
{
|
{
|
||||||
|
// Characters that are invalid on common filesystems.
|
||||||
|
// This is a union of invalid characters from Windows (NTFS/FAT32), Linux (ext4/XFS), and macOS (HFS+/APFS).
|
||||||
|
// We use this instead of Path.GetInvalidFileNameChars() because that only returns OS-specific characters,
|
||||||
|
// not filesystem-specific characters. For example, on Linux, '?' is valid in filenames, but not on NTFS.
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1417
|
||||||
|
private static readonly char[] InvalidFileNameChars =
|
||||||
|
[
|
||||||
|
'\0', // Null character - invalid on all filesystems
|
||||||
|
'/', // Path separator on Unix
|
||||||
|
'\\', // Path separator on Windows
|
||||||
|
':', // Reserved on Windows (drive letters, NTFS streams)
|
||||||
|
'*', // Wildcard on Windows
|
||||||
|
'?', // Wildcard on Windows
|
||||||
|
'"', // Reserved on Windows
|
||||||
|
'<', // Redirection on Windows
|
||||||
|
'>', // Redirection on Windows
|
||||||
|
'|', // Pipe on Windows
|
||||||
|
];
|
||||||
|
|
||||||
extension(Path)
|
extension(Path)
|
||||||
{
|
{
|
||||||
public static string EscapeFileName(string path)
|
public static string EscapeFileName(string path)
|
||||||
|
|
@ -13,7 +33,7 @@ public static class PathExtensions
|
||||||
var buffer = new StringBuilder(path.Length);
|
var buffer = new StringBuilder(path.Length);
|
||||||
|
|
||||||
foreach (var c in path)
|
foreach (var c in path)
|
||||||
buffer.Append(!Path.GetInvalidFileNameChars().Contains(c) ? c : '_');
|
buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
|
||||||
|
|
||||||
// File names cannot end with a dot on Windows
|
// File names cannot end with a dot on Windows
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/977
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/977
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue