From 0d0575f3a9eb753fe5a9a5e3f46eb7e17dc2ef84 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 27 Apr 2024 18:04:50 -0700 Subject: [PATCH] New: Validate that folders in paths don't start or end with a space (cherry picked from commit 316b5cbf75b45ef9a25f96ce1f2fbed25ad94296) Closes #9958 --- .../PathExtensionFixture.cs | 28 +++++++++++++++- .../Extensions/PathExtensions.cs | 32 +++++++++++++++++-- src/Radarr.Api.V3/Movies/MovieController.cs | 2 -- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 163f3f566..d01a5999d 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -3,6 +3,7 @@ using System.IO; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Test.Common; @@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test [TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")] [TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")] [TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")] - [TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")] + [TestCase(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")] [TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")] public void Clean_Path_Windows(string dirty, string clean) { @@ -334,5 +335,30 @@ namespace NzbDrone.Common.Test result[2].Should().Be(@"TV"); result[3].Should().Be(@"Series Title"); } + + [TestCase(@"C:\Test\")] + [TestCase(@"C:\Test")] + [TestCase(@"C:\Test\TV\")] + [TestCase(@"C:\Test\TV")] + public void IsPathValid_should_be_true(string path) + { + path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue(); + } + + [TestCase(@"C:\Test \")] + [TestCase(@"C:\Test ")] + [TestCase(@"C:\ Test\")] + [TestCase(@"C:\ Test")] + [TestCase(@"C:\Test \TV")] + [TestCase(@"C:\ Test\TV")] + [TestCase(@"C:\Test \TV\")] + [TestCase(@"C:\ Test\TV\")] + [TestCase(@" C:\Test\TV\")] + [TestCase(@" C:\Test\TV")] + + public void IsPathValid_should_be_false(string path) + { + path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 579770b7f..1bdab0115 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -30,6 +30,12 @@ namespace NzbDrone.Common.Extensions public static string CleanFilePath(this string path) { + if (path.IsNotNullOrWhiteSpace()) + { + // Trim trailing spaces before checking if the path is valid so validation doesn't fail for something we can fix. + path = path.TrimEnd(' '); + } + Ensure.That(path, () => path).IsNotNullOrWhiteSpace(); Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs); @@ -38,10 +44,10 @@ namespace NzbDrone.Common.Extensions // UNC if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\")) { - return info.FullName.TrimEnd('/', '\\', ' '); + return info.FullName.TrimEnd('/', '\\'); } - return info.FullName.TrimEnd('/').Trim('\\', ' '); + return info.FullName.TrimEnd('/').Trim('\\'); } public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null) @@ -155,6 +161,23 @@ namespace NzbDrone.Common.Extensions return false; } + if (path.Trim() != path) + { + return false; + } + + var directoryInfo = new DirectoryInfo(path); + + while (directoryInfo != null) + { + if (directoryInfo.Name.Trim() != directoryInfo.Name) + { + return false; + } + + directoryInfo = directoryInfo.Parent; + } + if (validationType == PathValidationType.AnyOs) { return IsPathValidForWindows(path) || IsPathValidForNonWindows(path); @@ -292,6 +315,11 @@ namespace NzbDrone.Common.Extensions return processName; } + public static string CleanPath(this string path) + { + return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray()); + } + public static string GetAppDataPath(this IAppFolderInfo appFolderInfo) { return appFolderInfo.AppDataFolder; diff --git a/src/Radarr.Api.V3/Movies/MovieController.cs b/src/Radarr.Api.V3/Movies/MovieController.cs index 165182794..f8aff7822 100644 --- a/src/Radarr.Api.V3/Movies/MovieController.cs +++ b/src/Radarr.Api.V3/Movies/MovieController.cs @@ -107,8 +107,6 @@ namespace Radarr.Api.V3.Movies .When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Title).NotEmpty().When(s => s.TmdbId <= 0); PostValidator.RuleFor(s => s.TmdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator); - - PutValidator.RuleFor(s => s.Path).IsValidPath(); } [HttpGet]