diff --git a/server/RdtClient.Service.Test/Helpers/FilenameSanitizerTest.cs b/server/RdtClient.Service.Test/Helpers/FilenameSanitizerTest.cs index c48e46b..b558c64 100644 --- a/server/RdtClient.Service.Test/Helpers/FilenameSanitizerTest.cs +++ b/server/RdtClient.Service.Test/Helpers/FilenameSanitizerTest.cs @@ -1,4 +1,5 @@ using RdtClient.Service.Helpers; +using RdtClient.Service.Services; namespace RdtClient.Service.Test.Helpers; @@ -43,28 +44,18 @@ public class FilenameSanitizerTest Assert.Equal(expected, result); } - [Fact] - public void SanitizeFilename_StripsControlCharacters() + [Theory] + // Uses explicit \u escapes so the control characters survive editors, + // diffs, and CI log copy-paste. Each case verifies a different range + // of Unicode control characters. + [InlineData("Movie\u0001Name\u001F.mkv", "MovieName.mkv")] // C0 range (0x00-0x1F) + [InlineData("Movie\tName.mkv", "MovieName.mkv")] // tab + [InlineData("Movie\u007FName.mkv", "MovieName.mkv")] // DEL (0x7F) + [InlineData("Movie\u0080Name\u009F.mkv", "MovieName.mkv")] // C1 range (0x80-0x9F) + public void SanitizeFilename_StripsControlCharacters(String input, String expected) { - var input = "MovieName.mkv"; var result = FilenameSanitizer.SanitizeFilename(input); - Assert.Equal("MovieName.mkv", result); - } - - [Fact] - public void SanitizeFilename_StripsDeleteCharacter() - { - var input = "MovieName.mkv"; - var result = FilenameSanitizer.SanitizeFilename(input); - Assert.Equal("MovieName.mkv", result); - } - - [Fact] - public void SanitizeFilename_StripsExtendedControlCharacters() - { - var input = "Movie€NameŸ.mkv"; - var result = FilenameSanitizer.SanitizeFilename(input); - Assert.Equal("MovieName.mkv", result); + Assert.Equal(expected, result); } [Fact] @@ -83,12 +74,18 @@ public class FilenameSanitizerTest Assert.Equal(input, result); } - [Theory] - [InlineData("", "")] - public void SanitizeFilename_HandlesEmpty(String input, String expected) + [Fact] + public void SanitizeFilename_ReturnsEmptyForEmpty() { - var result = FilenameSanitizer.SanitizeFilename(input); - Assert.Equal(expected, result); + var result = FilenameSanitizer.SanitizeFilename(""); + Assert.Equal("", result); + } + + [Fact] + public void SanitizeFilename_ReturnsEmptyForNull() + { + var result = FilenameSanitizer.SanitizeFilename(null); + Assert.Equal("", result); } [Fact] @@ -137,11 +134,74 @@ public class FilenameSanitizerTest Assert.Equal(path, result); } - [Theory] - [InlineData("")] - public void SanitizePath_HandlesEmpty(String input) + [Fact] + public void SanitizePath_HandlesMixedSeparators() { - var result = FilenameSanitizer.SanitizePath(input); - Assert.Equal(input, result); + var result = FilenameSanitizer.SanitizePath("data/downloads\\Some Show [2024]/Episode[eztv].mkv"); + var expected = String.Join(Path.DirectorySeparatorChar, "data", "downloads", "Some Show 2024", "Episodeeztv.mkv"); + + Assert.Equal(expected, result); + } + + [Fact] + public void SanitizePath_ReturnsEmptyForEmpty() + { + Assert.Equal("", FilenameSanitizer.SanitizePath("")); + } + + [Fact] + public void SanitizePath_ReturnsEmptyForNull() + { + Assert.Equal("", FilenameSanitizer.SanitizePath(null)); + } + + [Fact] + public void SanitizeFilenameIfEnabled_ReturnsUnchangedWhenDisabled() + { + var original = Settings.Get.DownloadClient.SanitizeFilenames; + try + { + Settings.Get.DownloadClient.SanitizeFilenames = false; + + const String input = "Movie [eztv].mkv"; + Assert.Equal(input, FilenameSanitizer.SanitizeFilenameIfEnabled(input)); + } + finally + { + Settings.Get.DownloadClient.SanitizeFilenames = original; + } + } + + [Fact] + public void SanitizeFilenameIfEnabled_SanitizesWhenEnabled() + { + var original = Settings.Get.DownloadClient.SanitizeFilenames; + try + { + Settings.Get.DownloadClient.SanitizeFilenames = true; + + Assert.Equal("Movie eztv.mkv", FilenameSanitizer.SanitizeFilenameIfEnabled("Movie [eztv].mkv")); + } + finally + { + Settings.Get.DownloadClient.SanitizeFilenames = original; + } + } + + [Fact] + public void SanitizePathIfEnabled_ReturnsUnchangedWhenDisabled() + { + var original = Settings.Get.DownloadClient.SanitizeFilenames; + try + { + Settings.Get.DownloadClient.SanitizeFilenames = false; + + var input = Path.Combine(Path.DirectorySeparatorChar + "data", "downloads", "Show [2024]", "Episode[eztv].mkv"); + Assert.Equal(input, FilenameSanitizer.SanitizePathIfEnabled(input)); + } + finally + { + Settings.Get.DownloadClient.SanitizeFilenames = original; + } } } diff --git a/server/RdtClient.Service/Helpers/DownloadHelper.cs b/server/RdtClient.Service/Helpers/DownloadHelper.cs index c88b1b2..982ad87 100644 --- a/server/RdtClient.Service/Helpers/DownloadHelper.cs +++ b/server/RdtClient.Service/Helpers/DownloadHelper.cs @@ -20,7 +20,7 @@ public static class DownloadHelper var torrentPath = Path.Combine(downloadPath, directory); - var originalFileName = GetOriginalFileName(download); + var originalFileName = GetFileName(download); if (originalFileName == null) { @@ -112,13 +112,6 @@ public static class DownloadHelper } public static String? GetFileName(Download download) - { - var cleaned = GetOriginalFileName(download); - - return cleaned == null ? null : FilenameSanitizer.SanitizeFilenameIfEnabled(cleaned); - } - - private static String? GetOriginalFileName(Download download) { if (String.IsNullOrWhiteSpace(download.Link)) { diff --git a/server/RdtClient.Service/Helpers/FilenameSanitizer.cs b/server/RdtClient.Service/Helpers/FilenameSanitizer.cs index d3a28ad..cde15d3 100644 --- a/server/RdtClient.Service/Helpers/FilenameSanitizer.cs +++ b/server/RdtClient.Service/Helpers/FilenameSanitizer.cs @@ -25,28 +25,28 @@ public static partial class FilenameSanitizer /// /// Sanitizes a filename if enabled in settings; otherwise returns it unchanged. /// - public static String SanitizeFilenameIfEnabled(String filename) + public static String SanitizeFilenameIfEnabled(String? filename) { - return IsEnabled ? SanitizeFilename(filename) : filename; + return IsEnabled ? SanitizeFilename(filename) : filename ?? String.Empty; } /// /// Sanitizes a full path if enabled in settings; otherwise returns it unchanged. /// - public static String SanitizePathIfEnabled(String filePath) + public static String SanitizePathIfEnabled(String? filePath) { - return IsEnabled ? SanitizePath(filePath) : filePath; + return IsEnabled ? SanitizePath(filePath) : filePath ?? String.Empty; } /// /// Sanitizes a filename by stripping problematic characters and normalizing whitespace. /// Does NOT touch directory separators — only a single filename segment. /// - public static String SanitizeFilename(String filename) + public static String SanitizeFilename(String? filename) { if (String.IsNullOrEmpty(filename)) { - return filename; + return String.Empty; } var sb = new StringBuilder(filename.Length); @@ -75,16 +75,18 @@ public static partial class FilenameSanitizer /// /// Sanitizes each segment of a full file path, preserving directory separators. + /// Splits on both '/' and '\' regardless of host OS so paths that arrive + /// from the debrid provider with foreign separators are still handled; + /// the result is re-joined with the host's native separator. /// - public static String SanitizePath(String filePath) + public static String SanitizePath(String? filePath) { if (String.IsNullOrEmpty(filePath)) { - return filePath; + return String.Empty; } - var separator = Path.DirectorySeparatorChar; - var segments = filePath.Split(separator); + var segments = filePath.Split('/', '\\'); for (var i = 0; i < segments.Length; i++) { @@ -94,6 +96,6 @@ public static partial class FilenameSanitizer } } - return String.Join(separator, segments); + return String.Join(Path.DirectorySeparatorChar, segments); } }