diff --git a/src/Cake.Issues.Tests/StringPathExtensionsTests.cs b/src/Cake.Issues.Tests/StringPathExtensionsTests.cs index d6e6aeb43..d84cd11eb 100644 --- a/src/Cake.Issues.Tests/StringPathExtensionsTests.cs +++ b/src/Cake.Issues.Tests/StringPathExtensionsTests.cs @@ -403,5 +403,349 @@ public void Should_Handle_Trailing_Slashes(string path, string expectedResult) result.ShouldBe(expectedResult); } } + + public sealed class TheIsValideRepositoryFilePathExtension + { + [Fact] + public void Should_Throw_If_FilePath_Is_Null() + { + // Given / When + var result = + Record.Exception( + () => + ((string)null).IsValideRepositoryFilePath(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentNullException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_Empty() + { + // Given / When + var result = + Record.Exception( + () => + string.Empty.IsValideRepositoryFilePath(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_WhiteSpace() + { + // Given / When + var result = + Record.Exception( + () => + " ".IsValideRepositoryFilePath(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_RepositorySettings_Are_Null() + { + // Given / When + var result = + Record.Exception( + () => + @"C:\repo".IsValideRepositoryFilePath(null)); + + // Then + result.IsArgumentNullException("repositorySettings"); + } + + [Theory] + [InlineData(@"C:\repo", @"C:\repo\foo")] + [InlineData(@"C:\repo", @"C:\repo\foo\")] + [InlineData(@"C:\repo", @"C:\repo\foo\bar")] + [InlineData("/repo", "/repo/foo")] + [InlineData("/repo", "/repo/foo/")] + [InlineData("/repo", "/repo/foo/bar")] + public void Should_Return_True_If_File_Is_Valid_Repository_FilePath(string repoRoot, string filePath) + { + // Given + var repositorySettings = new RepositorySettings(repoRoot); + + // When + var (valid, _) = filePath.IsValideRepositoryFilePath(repositorySettings); + + // Then + valid.ShouldBeTrue(); + } + + [Theory] + [InlineData(@"C:\repo", @"C:\r\foo")] + [InlineData(@"C:\repo", @"C:\r\foo\")] + [InlineData(@"C:\repo", @"C:\r\foo\bar")] + [InlineData("/repo", "/r/foo")] + [InlineData("/repo", "/r/foo/")] + [InlineData("/repo", "/r/foo/bar")] + public void Should_Return_False_If_File_Is_Outside_Repository(string repoRoot, string filePath) + { + // Given + var repositorySettings = new RepositorySettings(repoRoot); + + // When + var (valid, _) = filePath.IsValideRepositoryFilePath(repositorySettings); + + // Then + valid.ShouldBeFalse(); + } + + [Theory] + [InlineData(@"C:\repo", @"C:\repo\foo", @"foo")] + [InlineData(@"C:\repo", @"C:\repo\foo\", @"foo\")] + [InlineData(@"C:\repo", @"C:\repo\foo\bar", @"foo\bar")] + [InlineData("/repo", "/repo/foo", "foo")] + [InlineData("/repo", "/repo/foo/", "foo/")] + [InlineData("/repo", "/repo/foo/bar", "foo/bar")] + [InlineData(@"C:\repo", @"C:\r\foo", "")] + public void Should_Return_Correct_FilePath(string repoRoot, string filePath, string expectedResult) + { + // Given + var repositorySettings = new RepositorySettings(repoRoot); + + // When + var (_, filePathResult) = filePath.IsValideRepositoryFilePath(repositorySettings); + + // Then + filePathResult.ShouldBe(expectedResult); + } + } + + public sealed class TheIsInRepositoryExtension + { + [Fact] + public void Should_Throw_If_FilePath_Is_Null() + { + // Given / When + var result = + Record.Exception( + () => + ((string)null).IsInRepository(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentNullException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_Empty() + { + // Given / When + var result = + Record.Exception( + () => + string.Empty.IsInRepository(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_WhiteSpace() + { + // Given / When + var result = + Record.Exception( + () => + " ".IsInRepository(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_RepositorySettings_Are_Null() + { + // Given / When + var result = + Record.Exception( + () => + @"C:\repo".IsInRepository(null)); + + // Then + result.IsArgumentNullException("repositorySettings"); + } + + [Theory] + [InlineData(@"C:\repo", @"C:\repo\foo")] + [InlineData(@"C:\repo", @"C:\repo\foo\")] + [InlineData(@"C:\repo", @"C:\repo\foo\bar")] + [InlineData("/repo", "/repo/foo")] + [InlineData("/repo", "/repo/foo/")] + [InlineData("/repo", "/repo/foo/bar")] + public void Should_Return_True_If_File_Is_In_Repository(string repoRoot, string filePath) + { + // Given + var repositorySettings = new RepositorySettings(repoRoot); + + // When + var result = filePath.IsInRepository(repositorySettings); + + // Then + result.ShouldBeTrue(); + } + + [Theory] + [InlineData(@"C:\repo", @"C:\r\foo")] + [InlineData(@"C:\repo", @"C:\r\foo\")] + [InlineData(@"C:\repo", @"C:\r\foo\bar")] + [InlineData("/repo", "/r/foo")] + [InlineData("/repo", "/r/foo/")] + [InlineData("/repo", "/r/foo/bar")] + public void Should_Return_False_If_File_Is_Outside_Repository(string repoRoot, string filePath) + { + // Given + var repositorySettings = new RepositorySettings(repoRoot); + + // When + var result = filePath.IsInRepository(repositorySettings); + + // Then + result.ShouldBeFalse(); + } + } + + public sealed class TheMakeFilePathRelativeToRepositoryRootExtension + { + [Fact] + public void Should_Throw_If_FilePath_Is_Null() + { + // Given / When + var result = + Record.Exception( + () => + ((string)null).MakeFilePathRelativeToRepositoryRoot(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentNullException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_Empty() + { + // Given / When + var result = + Record.Exception( + () => + string.Empty.MakeFilePathRelativeToRepositoryRoot(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_WhiteSpace() + { + // Given / When + var result = + Record.Exception( + () => + " ".MakeFilePathRelativeToRepositoryRoot(new RepositorySettings(@"C:\repo"))); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_RepositorySettings_Are_Null() + { + // Given / When + var result = + Record.Exception( + () => + @"C:\repo".MakeFilePathRelativeToRepositoryRoot(null)); + + // Then + result.IsArgumentNullException("repositorySettings"); + } + + [Theory] + [InlineData(@"C:\repo", @"C:\repo\foo", @"foo")] + [InlineData(@"C:\repo", @"C:\repo\foo\", @"foo\")] + [InlineData(@"C:\repo", @"C:\repo\foo\bar", @"foo\bar")] + [InlineData("/repo", "/repo/foo", "foo")] + [InlineData("/repo", "/repo/foo/", "foo/")] + [InlineData("/repo", "/repo/foo/bar", "foo/bar")] + public void Should_Make_FilePath_Relative_To_Repository_Root(string repoRoot, string filePath, string expectedResult) + { + // Given + var repositorySettings = new RepositorySettings(repoRoot); + + // When + var result = filePath.MakeFilePathRelativeToRepositoryRoot(repositorySettings); + + // Then + result.ShouldBe(expectedResult); + } + } + + public sealed class TheRemoveLeadingDirectorySeparatorExtension + { + [Fact] + public void Should_Throw_If_FilePath_Is_Null() + { + // Given / When + var result = + Record.Exception( + () => + ((string)null).RemoveLeadingDirectorySeparator()); + + // Then + result.IsArgumentNullException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_Empty() + { + // Given / When + var result = + Record.Exception( + () => + string.Empty.RemoveLeadingDirectorySeparator()); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_WhiteSpace() + { + // Given / When + var result = + Record.Exception( + () => + " ".RemoveLeadingDirectorySeparator()); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Theory] + [InlineData("foo", "foo")] + [InlineData(@"foo\", @"foo\")] + [InlineData(@"foo\bar", @"foo\bar")] + [InlineData(@"\foo", @"foo")] + [InlineData(@"\foo\", @"foo\")] + [InlineData(@"\foo\bar", @"foo\bar")] + [InlineData(@"c:\foo", @"c:\foo")] + [InlineData(@"c:\foo\", @"c:\foo\")] + [InlineData(@"c:\foo\bar", @"c:\foo\bar")] + [InlineData("/foo", "foo")] + [InlineData("/foo/", "foo/")] + [InlineData("/foo/bar", "foo/bar")] + public void Should_Remove_Leading_Directory_Separators(string filePath, string expectedResult) + { + // Given / When + var result = filePath.RemoveLeadingDirectorySeparator(); + + // Then + result.ShouldBe(expectedResult); + } + } } } diff --git a/src/Cake.Issues/StringPathExtensions.cs b/src/Cake.Issues/StringPathExtensions.cs index 07ea8ff20..142726f51 100644 --- a/src/Cake.Issues/StringPathExtensions.cs +++ b/src/Cake.Issues/StringPathExtensions.cs @@ -1,6 +1,4 @@ -// Based on http://stackoverflow.com/a/31941159/566901 - -namespace Cake.Issues +namespace Cake.Issues { using System; using System.IO; @@ -54,6 +52,7 @@ public static bool IsFullPath(this string path) /// Returns true if starts with the path . public static bool IsSubpathOf(this string path, string baseDirPath) { + // Based on http://stackoverflow.com/a/31941159/566901 path.NotNullOrWhiteSpace(nameof(path)); baseDirPath.NotNullOrWhiteSpace(nameof(baseDirPath)); @@ -127,6 +126,79 @@ public static string WithEnding(this string value, string ending) return result; } + /// + /// Validates if a file path is a valid path below . + /// + /// Full file path. + /// Repository settings. + /// Tuple containing a value if validation was successful, + /// and file path relative to . + public static (bool Valid, string FilePath) IsValideRepositoryFilePath(this string filePath, IRepositorySettings repositorySettings) + { + filePath.NotNullOrWhiteSpace(nameof(filePath)); + repositorySettings.NotNull(nameof(repositorySettings)); + + // Ignore files from outside the repository. + if (!filePath.IsInRepository(repositorySettings)) + { + return (false, string.Empty); + } + + // Make path relative to repository root. + filePath = filePath.MakeFilePathRelativeToRepositoryRoot(repositorySettings); + + return (true, filePath); + } + + /// + /// Checks if a file is part of the repository. + /// + /// Full file path. + /// Repository settings. + /// True if file is in repository, false otherwise. + public static bool IsInRepository(this string filePath, IRepositorySettings repositorySettings) + { + filePath.NotNullOrWhiteSpace(nameof(filePath)); + repositorySettings.NotNull(nameof(repositorySettings)); + + return filePath.IsSubpathOf(repositorySettings.RepositoryRoot.FullPath); + } + + /// + /// Make path relative to repository root. + /// + /// Full file path. + /// Repository settings. + /// File path relative to the repository root. + public static string MakeFilePathRelativeToRepositoryRoot(this string filePath, IRepositorySettings repositorySettings) + { + filePath.NotNullOrWhiteSpace(nameof(filePath)); + repositorySettings.NotNull(nameof(repositorySettings)); + + filePath = filePath[repositorySettings.RepositoryRoot.FullPath.Length..]; + + // Remove leading directory separator. + return filePath.RemoveLeadingDirectorySeparator(); + } + + /// + /// Remove the leading directory separator from a file path. + /// + /// Full file path. + /// File path without leading directory separator. + public static string RemoveLeadingDirectorySeparator(this string filePath) + { + filePath.NotNullOrWhiteSpace(nameof(filePath)); + + if (filePath.StartsWith("\\", StringComparison.Ordinal) || + filePath.StartsWith("/", StringComparison.Ordinal)) + { + return filePath[1..]; + } + + return filePath; + } + /// Gets the rightmost characters from a string. /// The string to retrieve the substring from. /// The number of characters to retrieve.