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.