diff --git a/Sharpmake.UnitTests/UtilTest.cs b/Sharpmake.UnitTests/UtilTest.cs
index 80c058f59..a9fabbeb3 100644
--- a/Sharpmake.UnitTests/UtilTest.cs
+++ b/Sharpmake.UnitTests/UtilTest.cs
@@ -110,30 +110,21 @@ public void ProcessesWithAListAsArgument()
public class SimplifyPath
{
- ///
- /// Verify that an error is thrown when a path begin with three dots
- ///
[Test]
- public void ThrowsErrorDot()
+ public void ThrowsOnInvalidNameWithOnlyDots()
{
Assert.Throws(() => Util.SimplifyPath(".../sharpmake/README.md"));
- }
- ///
- /// Verify that an error is thrown when a path start with dots but no slash
- ///
- [Test]
- public void ThrowsErrorSeparator()
- {
- Assert.Throws(() => Util.SimplifyPath("..sharpmake/README.md"));
+ Assert.Throws(() => Util.SimplifyPath("sharpmake/..../README.md"));
+
+ Assert.Throws(() => Util.SimplifyPath("sharpmake/..."));
}
[Test]
public void LeavesEmptyStringsUntouched()
{
- Assert.That(Util.SimplifyPath(string.Empty), Is.EqualTo(string.Empty));
- Assert.That(Util.SimplifyPath(""), Is.EqualTo(string.Empty));
- Assert.That(Util.SimplifyPath(""), Is.EqualTo(""));
+ Assert.That(Util.SimplifyPath(""),
+ Is.EqualTo(""));
}
[Test]
@@ -153,6 +144,40 @@ public void HandlesReturningToParentFolder()
Is.EqualTo("test.cpp"));
}
+ [Test]
+ public void HandlesDotsInFileNameOrFolder()
+ {
+ Assert.That(Util.SimplifyPath(@".test.cpp"),
+ Is.EqualTo(".test.cpp"));
+
+ Assert.That(Util.SimplifyPath(@"test.cpp."),
+ Is.EqualTo("test.cpp."));
+
+ Assert.That(Util.SimplifyPath(@"test.cpp.."),
+ Is.EqualTo("test.cpp.."));
+
+ Assert.That(Util.SimplifyPath(@"test.cpp..."),
+ Is.EqualTo("test.cpp..."));
+
+ Assert.That(Util.SimplifyPath(@".test\.test.cpp"),
+ Is.EqualTo(Path.Combine(".test", ".test.cpp")));
+
+ Assert.That(Util.SimplifyPath(@"test.\test.cpp."),
+ Is.EqualTo(Path.Combine("test.", "test.cpp.")));
+
+ Assert.That(Util.SimplifyPath(@"..test\..test.cpp"),
+ Is.EqualTo(Path.Combine("..test", "..test.cpp")));
+
+ Assert.That(Util.SimplifyPath(@"test..\test.cpp.."),
+ Is.EqualTo(Path.Combine("test..", "test.cpp..")));
+
+ Assert.That(Util.SimplifyPath(@"...test\...test.cpp"),
+ Is.EqualTo(Path.Combine("...test", "...test.cpp")));
+
+ Assert.That(Util.SimplifyPath(@"test...\test.cpp..."),
+ Is.EqualTo(Path.Combine("test...", "test.cpp...")));
+ }
+
[Test]
public void HandlesReturningToParentFolderRelativeToCurrentFolder()
{
@@ -176,30 +201,204 @@ public void CollapsesMultipleFolderSeparators()
[Test]
public void HandlesSlashesInFullPath()
{
- var currentDirectory = Directory.GetCurrentDirectory();
- Assert.That(Util.SimplifyPath(currentDirectory + "\\main/test//t.cpp"),
- Is.EqualTo(Path.Combine(currentDirectory, "main", "test", "t.cpp")));
+ Assert.That(Util.SimplifyPath("c:\\main/test//t.cpp"),
+ Is.EqualTo(Path.Combine("c:", "main", "test", "t.cpp")));
+
+ Assert.That(Util.SimplifyPath("c:\\"),
+ Is.EqualTo("c:\\"));
+
+ Assert.That(Util.SimplifyPath("/main/test/t.cpp"),
+ Is.EqualTo(Path.Combine("/", "main", "test", "t.cpp")));
+
+ Assert.That(Util.SimplifyPath("/"),
+ Is.EqualTo("/"));
+ }
+
+ [Test]
+ public void LeaveTrailingPathSeparator()
+ {
+ Assert.That(Util.SimplifyPath(@"alpha\beta\"),
+ Is.EqualTo(Path.Combine("alpha", $"beta{Path.DirectorySeparatorChar}")));
+
+ Assert.That(Util.SimplifyPath(@"alpha\beta\\"),
+ Is.EqualTo(Path.Combine("alpha", $"beta{Path.DirectorySeparatorChar}")));
}
[Test]
public void HandlesFolderParentsAtTheEnd()
{
- Assert.That(Util.SimplifyPath(@"alpha\beta\gamma\sigma\omega\zeta\..\.."),
- Is.EqualTo(Path.Combine("alpha", "beta", "gamma", "sigma")));
+ Assert.That(Util.SimplifyPath(@"a\b\c\..\.."),
+ Is.EqualTo("a"));
+
+ Assert.That(Util.SimplifyPath(@"a\b\c\..\..\"),
+ Is.EqualTo($"a{Path.DirectorySeparatorChar}"));
}
[Test]
public void LeavesCleanPathUntouched()
{
// Check that we do not change dot and dot dot
- Assert.That(".", Is.EqualTo(Util.SimplifyPath(".")));
- Assert.That("..", Is.EqualTo(Util.SimplifyPath("..")));
+ Assert.That(Util.SimplifyPath("."), Is.EqualTo("."));
+ Assert.That(Util.SimplifyPath(".."), Is.EqualTo(".."));
- Assert.That(Util.SimplifyPath(Util.PathMakeStandard(@"alpha\beta\gamma\sigma\omega\zeta\lambda\phi\")),
+ Assert.That(Util.SimplifyPath(@"alpha\beta\gamma\sigma\omega\zeta\lambda\phi"),
Is.EqualTo(Path.Combine("alpha", "beta", "gamma", "sigma", "omega", "zeta", "lambda", "phi")));
}
}
+ public class PathGetRelative
+ {
+ [Test]
+ public void PathGetRelative_Highter()
+ {
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2", "c:/folder1"), Is.EqualTo(".."));
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2/", "c:/folder1"), Is.EqualTo(".."));
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2", "c:/folder1/"), Is.EqualTo(".."));
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2/", "c:/folder1/"), Is.EqualTo(".."));
+
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2/folder3", "c:/folder1"), Is.EqualTo(Path.Combine("..", "..")));
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2/folder3/", "c:/folder1"), Is.EqualTo(Path.Combine("..", "..")));
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2/folder3", "c:/folder1/"), Is.EqualTo(Path.Combine("..", "..")));
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2/folder3/", "c:/folder1/"), Is.EqualTo(Path.Combine("..", "..")));
+ }
+
+ [Test]
+ public void PathGetRelative_Deeper()
+ {
+ Assert.That(Util.PathGetRelative("c:/folder1", "c:/folder1/folder2"), Is.EqualTo("folder2"));
+ Assert.That(Util.PathGetRelative("c:/folder1/", "c:/folder1/folder2"), Is.EqualTo("folder2"));
+ Assert.That(Util.PathGetRelative("c:/folder1", "c:/folder1/folder2/"), Is.EqualTo("folder2")); // standard keep the last dirSep != sharpmake that remove it
+ Assert.That(Util.PathGetRelative("c:/folder1/", "c:/folder1/folder2/"), Is.EqualTo("folder2")); // standard keep the last dirSep != sharpmake that remove it
+
+ Assert.That(Util.PathGetRelative("c:/folder1", "c:/folder1/folder2/folder3"), Is.EqualTo(Path.Combine("folder2", "folder3")));
+ Assert.That(Util.PathGetRelative("c:/folder1/", "c:/folder1/folder2/folder3"), Is.EqualTo(Path.Combine("folder2", "folder3")));
+ Assert.That(Util.PathGetRelative("c:/folder1", "c:/folder1/folder2/folder3/"), Is.EqualTo(Path.Combine("folder2", "folder3"))); // standard keep the last dirSep != sharpmake that remove it
+ Assert.That(Util.PathGetRelative("c:/folder1/", "c:/folder1/folder2/folder3/"), Is.EqualTo(Path.Combine("folder2", "folder3"))); // standard keep the last dirSep != sharpmake that remove it
+ }
+
+ [Test]
+ public void PathGetRelativeSimple_Parallel()
+ {
+ Assert.That(Util.PathGetRelative("c:/folder1", "c:/folder2"), Is.EqualTo(Path.Combine("..", "folder2")));
+ Assert.That(Util.PathGetRelative("c:/folder1/", "c:/folder2"), Is.EqualTo(Path.Combine("..", "folder2")));
+ Assert.That(Util.PathGetRelative("c:/folder1", "c:/folder2/"), Is.EqualTo(Path.Combine("..", "folder2"))); // standard keep the last dirSep != sharpmake that remove it
+ Assert.That(Util.PathGetRelative("c:/folder1/", "c:/folder2/"), Is.EqualTo(Path.Combine("..", "folder2"))); // standard keep the last dirSep != sharpmake that remove it
+
+ Assert.That(Util.PathGetRelative("c:/folder1/folderA", "c:/folder2/folderB"), Is.EqualTo(Path.Combine("..", "..", "folder2", "folderB")));
+ Assert.That(Util.PathGetRelative("c:/folder1/folderA/", "c:/folder2/folderB"), Is.EqualTo(Path.Combine("..", "..", "folder2", "folderB")));
+ Assert.That(Util.PathGetRelative("c:/folder1/folderA", "c:/folder2/folderB/"), Is.EqualTo(Path.Combine("..", "..", "folder2", "folderB"))); // standard keep the last dirSep != sharpmake that remove it
+ Assert.That(Util.PathGetRelative("c:/folder1/folderA/", "c:/folder2/folderB/"), Is.EqualTo(Path.Combine("..", "..", "folder2", "folderB"))); // standard keep the last dirSep != sharpmake that remove it
+ }
+
+ [Test]
+ public void PathGetRelative_Same()
+ {
+ Assert.That(Util.PathGetRelative("c:/folder1", "c:/folder1"), Is.EqualTo("."));
+ Assert.That(Util.PathGetRelative("c:/folder1/", "c:/folder1"), Is.EqualTo("."));
+ Assert.That(Util.PathGetRelative("c:/folder1", "c:/folder1/"), Is.EqualTo("."));
+ Assert.That(Util.PathGetRelative("c:/folder1/", "c:/folder1/"), Is.EqualTo("."));
+
+ Assert.That(Util.PathGetRelative("c:/", "c:/"), Is.EqualTo("."));
+ Assert.That(Util.PathGetRelative("/", "/"), Is.EqualTo("."));
+ }
+
+ [Test]
+ public void PathGetRelative_DifferentRoot()
+ {
+ Assert.That(Util.PathGetRelative("c:/folder1", "d:/folder2"), Is.EqualTo(Path.Combine("d:", "folder2")));
+ Assert.That(Util.PathGetRelative("c:/folder1/", "d:/folder2"), Is.EqualTo(Path.Combine("d:", "folder2")));
+ Assert.That(Util.PathGetRelative("c:/folder1", "d:/folder2/"), Is.EqualTo(Path.Combine("d:", $"folder2{Path.DirectorySeparatorChar}")));
+ Assert.That(Util.PathGetRelative("c:/folder1/", "d:/folder2/"), Is.EqualTo(Path.Combine("d:", $"folder2{Path.DirectorySeparatorChar}")));
+ }
+
+ [Test]
+ public void PathGetRelative_Tricky()
+ {
+ // Names are halfway the same (common part must be properly computed)
+ Assert.That(Util.PathGetRelative("c:/abc", "c:/abx/folder"), Is.EqualTo(Path.Combine("..", "abx", "folder")));
+ Assert.That(Util.PathGetRelative("c:/abc", "c:/abc_"), Is.EqualTo(Path.Combine("..", "abc_")));
+ Assert.That(Util.PathGetRelative("c:/abc_", "c:/abc"), Is.EqualTo(Path.Combine("..", "abc")));
+ Assert.That(Util.PathGetRelative("c:/abc_def", "c:/abc_xyz"), Is.EqualTo(Path.Combine("..", "abc_xyz")));
+
+ // One character names
+ Assert.That(Util.PathGetRelative("c:/1/2/3", "c:/1"), Is.EqualTo(Path.Combine("..", "..")));
+ Assert.That(Util.PathGetRelative("c:/1/", "c:/1/2/3"), Is.EqualTo(Path.Combine("2", "3")));
+ }
+
+ [Test]
+ public void PathGetRelative_Invalid()
+ {
+ // Not rooted
+ Assert.That(Util.PathGetRelative("c:/folder1", "folder2"), Is.EqualTo("folder2")); // Should probably throw as not rooted
+ Assert.That(Util.PathGetRelative("folder1", "c:/folder2"), Is.EqualTo(Path.Combine("c:", "folder2"))); // Should probably throw as not rooted
+
+ // Empty
+ Assert.That(Util.PathGetRelative("", "c:/folder2"), Is.EqualTo(Path.Combine("c:", "folder2"))); // Should probably throw as empty
+ Assert.That(Util.PathGetRelative("c:/folder2", ""), Is.EqualTo("")); // Should probably throw as empty
+
+ // Null
+ Assert.Throws(() => Util.PathGetRelative(null, "c:/folder2"));
+ Assert.Throws(() => Util.PathGetRelative("c:/folder2", (string)null));
+ }
+
+
+ [Test]
+ public void PathGetRelative_IgnoreCase()
+ {
+ Assert.Inconclusive("The implementation expose a 'ignoreCase' argument, but never use it (it always ignore the case even with ignoreCase == false)");
+
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2", "c:/Folder1", ignoreCase: true), Is.EqualTo(".."));
+ Assert.That(Util.PathGetRelative("c:/folder1/folder2", "c:/Folder1", ignoreCase: false), Is.EqualTo(Path.Combine("..", "..", "Folder1"))); // ori always ignore case, whatever the user ask
+
+ Assert.That(Util.PathGetRelative("c:/folder1", "C:/folder1", ignoreCase: true), Is.EqualTo("."));
+ Assert.That(Util.PathGetRelative("c:/folder1", "C:/folder1", ignoreCase: false), Is.EqualTo(Path.Combine("C:", "folder1"))); // ori always ignore case, whatever the user ask
+ }
+
+ [Test]
+ public void PathGetRelativeStrings()
+ {
+ Strings stringsDest = new Strings(Util.PathMakeStandard(@"C:\Windows\local\cmd.exe"));
+ string stringsSource = Util.PathMakeStandard(@"C:\Windows\System32\cmd.exe");
+ string expectedString = Util.PathMakeStandard(@"..\..\local\cmd.exe");
+
+ Assert.AreEqual(expectedString, Util.PathGetRelative(stringsSource, stringsDest, false)[0]);
+ }
+
+ [Test]
+ public void PathGetRelativeOrderableStrings()
+ {
+ string stringsSource = Util.PathMakeStandard(@"F:\SharpMake\sharpmake\Sharpmake.Platforms");
+ OrderableStrings stringsDest = new OrderableStrings
+ {
+ @"F:\SharpMake\sharpmake\Sharpmake.Generators\Generic",
+ @"F:\SharpMake\sharpmake\Sharpmake.Platforms\subdir\test.txt",
+ @"F:\SharpMake\sharpmake\Sharpmake.Platforms\test2.txt"
+ };
+ Util.PathMakeStandard(stringsDest);
+ OrderableStrings listResult = Util.PathGetRelative(stringsSource, stringsDest, false);
+
+ Assert.AreEqual(Util.PathMakeStandard(@"..\Sharpmake.Generators\Generic", !Util.IsRunningInMono()), listResult[0]);
+ Assert.AreEqual(Util.PathMakeStandard(@"subdir\test.txt", !Util.IsRunningInMono()), listResult[1]);
+ Assert.AreEqual("test2.txt", listResult[2]);
+ }
+
+ [Test]
+ public void PathGetRelativeOrderableIEnumerable()
+ {
+ string stringsSource = Util.PathMakeStandard(@"F:\SharpMake\sharpmake\Sharpmake.Generators\Apple");
+ List stringsDest = new List()
+ {
+ @"F:\SharpMake\sharpmake\Sharpmake.Generators\Generic",
+ @"F:\SharpMake\sharpmake\Sharpmake.Generators\Properties"
+ };
+ Util.PathMakeStandard(stringsDest);
+
+ var result = Util.PathGetRelative(stringsSource, stringsDest, false);
+ Assert.AreEqual(Util.PathMakeStandard(@"..\Generic"), result[0]);
+ Assert.AreEqual(Util.PathMakeStandard(@"..\Properties"), result[1]);
+ }
+ }
+
public class FindCommonRootPath
{
[Test]
@@ -1339,59 +1538,6 @@ public void GetPathIntersection()
Path.Combine(pathB)));
}
- ///
- /// Verify if the relative path to the destination directory is correct from the source path
- ///
- [Test]
- public void PathGetRelativeStrings()
- {
- Strings stringsDest = new Strings(Util.PathMakeStandard(@"C:\Windows\local\cmd.exe"));
- string stringsSource = Util.PathMakeStandard(@"C:\Windows\System32\cmd.exe");
- string expectedString = Util.PathMakeStandard(@"..\..\local\cmd.exe");
-
- Assert.AreEqual(expectedString, Util.PathGetRelative(stringsSource, stringsDest, false)[0]);
- }
-
- ///
- /// Verify if the relative path to the destination directory is correct from the source path
- ///
- [Test]
- public void PathGetRelativeOrderableStrings()
- {
- string stringsSource = Util.PathMakeStandard(@"F:\SharpMake\sharpmake\Sharpmake.Platforms");
- OrderableStrings stringsDest = new OrderableStrings
- {
- @"F:\SharpMake\sharpmake\Sharpmake.Generators\Generic",
- @"F:\SharpMake\sharpmake\Sharpmake.Platforms\subdir\test.txt",
- @"F:\SharpMake\sharpmake\Sharpmake.Platforms\test2.txt"
- };
- Util.PathMakeStandard(stringsDest);
- OrderableStrings listResult = Util.PathGetRelative(stringsSource, stringsDest, false);
-
- Assert.AreEqual(Util.PathMakeStandard(@"..\Sharpmake.Generators\Generic", !Util.IsRunningInMono()), listResult[0]);
- Assert.AreEqual(Util.PathMakeStandard(@"subdir\test.txt", !Util.IsRunningInMono()), listResult[1]);
- Assert.AreEqual("test2.txt", listResult[2]);
- }
-
- ///
- /// Verify if the relative path to the destination directory is correct from the source path
- ///
- [Test]
- public void PathGetRelativeOrderableIEnumerable()
- {
- string stringsSource = Util.PathMakeStandard(@"F:\SharpMake\sharpmake\Sharpmake.Generators\Apple");
- List stringsDest = new List()
- {
- @"F:\SharpMake\sharpmake\Sharpmake.Generators\Generic",
- @"F:\SharpMake\sharpmake\Sharpmake.Generators\Properties"
- };
- Util.PathMakeStandard(stringsDest);
-
- var result = Util.PathGetRelative(stringsSource, stringsDest, false);
- Assert.AreEqual(Util.PathMakeStandard(@"..\Generic"), result[0]);
- Assert.AreEqual(Util.PathMakeStandard(@"..\Properties"), result[1]);
- }
-
///
/// Verify that the path and the file's name were separated
///
@@ -1445,12 +1591,20 @@ public void GetConvertedRelativePathRoot()
var root = @"C:\SharpMake\sharpmake\Sharpmake.Application\Properties";
- var newRelativeToFullPath = "";
- Assert.AreEqual(Path.Combine(absolutePath.ToLower(), fileName), Util.GetConvertedRelativePath(absolutePath, fileName, newRelativeToFullPath, false, null));
- Assert.AreEqual(mockPath, Util.GetConvertedRelativePath(absolutePath, mockPath, newRelativeToFullPath, false, root));
- Assert.AreEqual(Path.Combine(absolutePath.ToLower(), fileName), Util.GetConvertedRelativePath(absolutePath, fileName, newRelativeToFullPath, false, ""));
- Assert.AreEqual(absolutePath, Util.GetConvertedRelativePath(absolutePath, null, newRelativeToFullPath, false, null));
- Assert.AreEqual(Path.Combine(root.ToLower(), Path.GetTempPath()), Util.GetConvertedRelativePath(root, Path.GetTempPath(), newRelativeToFullPath, false, null));
+ Assert.AreEqual(Path.Combine(absolutePath.ToLower(), fileName),
+ Util.GetConvertedRelativePath(absolutePath, fileName, newRelativeToFullPath: "", false, null));
+
+ Assert.AreEqual(mockPath,
+ Util.GetConvertedRelativePath(absolutePath, mockPath, newRelativeToFullPath: "", false, root));
+
+ Assert.AreEqual(Path.Combine(absolutePath.ToLower(), fileName),
+ Util.GetConvertedRelativePath(absolutePath, fileName, newRelativeToFullPath: "", false, ""));
+
+ Assert.AreEqual(absolutePath,
+ Util.GetConvertedRelativePath(absolutePath, null, newRelativeToFullPath: "", false, null));
+
+ Assert.AreEqual(Path.GetTempPath(),
+ Util.GetConvertedRelativePath(root, Path.GetTempPath(), newRelativeToFullPath: "", false, null));
File.Delete(mockPath);
}
diff --git a/Sharpmake/PathUtil.cs b/Sharpmake/PathUtil.cs
index 31d14a6e6..5f1b2f0af 100644
--- a/Sharpmake/PathUtil.cs
+++ b/Sharpmake/PathUtil.cs
@@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using System.Text;
+using Microsoft.CodeAnalysis;
namespace Sharpmake
{
@@ -134,7 +135,7 @@ public static string GetConvertedRelativePath(
if (rootPath != null)
{
- string cleanPath = Util.SimplifyPath(rootPath);
+ string cleanPath = SimplifyPath(rootPath);
if (!tmpAbsolute.StartsWith(cleanPath, StringComparison.OrdinalIgnoreCase))
return tmpAbsolute;
}
@@ -142,133 +143,118 @@ public static string GetConvertedRelativePath(
return newRelativePath;
}
- private sealed unsafe class PathHelper
- {
- public static readonly int MaxPath = 280;
- private int _capacity;
-
- // Array of stack members.
- private char* _buffer;
- private int _bufferLength;
-
- public PathHelper(char* buffer, int length)
- {
- _buffer = buffer;
- _capacity = length;
- _bufferLength = 0;
- }
+ private static ConcurrentDictionary s_cachedSimplifiedPaths = new ConcurrentDictionary();
- // This method is called when we find a .. in the path.
- public bool RemoveLastDirectory(int lowestRemovableIndex)
+ ///
+ /// Take a path and compute a canonical version of it. It removes any extra: "..", ".", directory separators...
+ /// Note that symbolic links are not expanded, path does not need to exist on the file system.
+ ///
+ /// Basic implementation details:
+ /// - We take for granted that the simplified path will always be smaller that the input path.
+ /// - This allow to allocate a working buffer only once, and work inside it.
+ /// - The logic starts from the end of the input path and walks it backward.
+ /// - This allow to simply count ".." occurrences and skip folder accordingly as we move toward the root of the path.
+ /// - Characters are written in the working buffer starting by its end, and an index is always written once (no back tracking).
+ /// - At the end, if there is remaining "..", they are added back to the path.
+ /// - At the end, the working buffer may still have some room at its beginning (when the simplified path is smaller than the input path).
+ /// - A string is created occordingly to skip this unused/uninitialized space
+ public static unsafe string SimplifyPathImpl(string path)
+ {
+ // First construct a path helper to help with the conversion
+ char* arrayPtr = stackalloc char[path.Length + 1];
+
+ int consecutiveDotsCounter = 0;
+ bool pathSeparatorWritePending = IsPathSeparator(path[^1]); // Explicitly handle path with a trailing path separator (we want to keep it)
+ int writePosition = path.Length;
+ int dotDotCounter = 0;
+
+ // Start by the end of the path to easily handle '..' case
+ for (int index = path.Length - 1; index >= 0; --index)
{
- if (Length == 0)
- return false;
-
- Trace.Assert(_buffer[_bufferLength - 1] == Path.DirectorySeparatorChar);
-
- int lastSlash = -1;
-
- for (int i = _bufferLength - 2; i >= lowestRemovableIndex; i--)
+ char currentChar = path[index];
+ if (IsPathSeparator(currentChar))
{
- if (_buffer[i] == Path.DirectorySeparatorChar)
- {
- lastSlash = i;
- break;
- }
- }
+ pathSeparatorWritePending = pathSeparatorWritePending
+ || (consecutiveDotsCounter == -1 && dotDotCounter == 0); // We want to consider this path separator only if we are not on a '.' or '..'
- if (lastSlash == -1)
+ HandleDotDotCounter(ref consecutiveDotsCounter, ref dotDotCounter, path);
+ }
+ else
{
- if (lowestRemovableIndex == 0)
- {
- _bufferLength = 0;
- return true;
- }
- else
+ // Count consecutive dots (if there is only dots in the name)
+ if (currentChar == '.' && consecutiveDotsCounter != -1)
{
- return false;
+ ++consecutiveDotsCounter;
+ continue;
}
- }
- // Truncate the path.
- _bufferLength = lastSlash;
+ if (dotDotCounter == 0)
+ {
+ // Write pending path separator
+ if (pathSeparatorWritePending)
+ {
+ arrayPtr[writePosition--] = Path.DirectorySeparatorChar;
+ pathSeparatorWritePending = false;
+ }
- return true;
- }
+ // Write held back '.'
+ for (int i = 0; i < consecutiveDotsCounter; ++i)
+ {
+ arrayPtr[writePosition--] = '.';
+ }
- public void Append(char value)
- {
- if (Length + 1 >= _capacity)
- throw new PathTooLongException("Path too long:");
+ arrayPtr[writePosition--] = currentChar;
+ }
- if (value == Path.DirectorySeparatorChar)
- {
- // Skipping consecutive backslashes.
- if (_bufferLength > 0 && _buffer[_bufferLength - 1] == Path.DirectorySeparatorChar)
- return;
+ // We encountered something else than '.', now we don't care about them until next path separator
+ consecutiveDotsCounter = -1;
}
-
- // Important note: Must imcrement _bufferLength at the same time as writing into it as otherwise if
- // you are stepping in the debugger and ToString() is implicitly called by the debugger this could truncate the string
- // before the increment takes place
- _buffer[_bufferLength++] = value;
}
- // Append a substring path component to the
- public void Append(string str, int substStringIndex, int subStringLength)
+ // Handle additional '..' that was not taken into account nor consummed
+ HandleDotDotCounter(ref consecutiveDotsCounter, ref dotDotCounter, path);
+ for (int i = 0; i < dotDotCounter; ++i)
{
- if (Length + subStringLength >= _capacity)
- throw new PathTooLongException("Path too long:");
-
- Trace.Assert(substStringIndex < str.Length);
- Trace.Assert(substStringIndex + subStringLength <= str.Length);
-
-
- int endLoop = substStringIndex + subStringLength;
- for (int i = substStringIndex; i < endLoop; ++i)
- {
- // Important note: Must imcrement _bufferLength at the same time as writing into it as otherwise if
- // you are stepping in the debugger and ToString() is implicitly called by the debugger this could truncate the string
- // before the increment takes place
- char value = str[i];
- _buffer[_bufferLength++] = value;
- }
+ if (pathSeparatorWritePending)
+ arrayPtr[writePosition--] = Path.DirectorySeparatorChar;
+ arrayPtr[writePosition--] = '.';
+ arrayPtr[writePosition--] = '.';
+ pathSeparatorWritePending = true;
}
- public void RemoveChar(int index)
- {
- Debug.Assert(index < _bufferLength);
- for (int i = index; i < _bufferLength - 1; ++i)
- {
- _buffer[i] = _buffer[i + 1];
- }
- --_bufferLength;
- }
+ // Handle rooted path on Unix platforms
+ if (path[0] == '/')
+ arrayPtr[writePosition--] = '/';
- public override string ToString()
- {
- return new string(_buffer, 0, _bufferLength);
- }
+ return new string(arrayPtr, writePosition + 1, path.Length - writePosition);
- public int Length
- {
- get
- {
- return _bufferLength;
- }
- }
+ static bool IsPathSeparator(char c) => c == Path.DirectorySeparatorChar || c == OtherSeparator;
- internal char this[int index]
+ static void HandleDotDotCounter(ref int consecutiveDotsCounter, ref int dotDotCounter, string path)
{
- get
+ switch (consecutiveDotsCounter)
{
- Debug.Assert(index < _bufferLength);
- return _buffer[index];
+ case -1:
+ // We encountered a real folder name (not made exclusively of dots), and it have been skipped if dotDotCounter was not 0, so we decrement it
+ if (dotDotCounter > 0)
+ --dotDotCounter;
+ break;
+ case 0:
+ break;
+ case 1:
+ // skip: "./"
+ break;
+ case 2:
+ ++dotDotCounter;
+ break;
+ default:
+ throw new ArgumentException($"Invalid path format: '{path}' (folder made of three or more consecutive dots detected)");
}
- }
- };
- private static ConcurrentDictionary s_cachedSimplifiedPaths = new ConcurrentDictionary();
+ // We are on a path separator, consecutive dots counter must be reset
+ consecutiveDotsCounter = 0;
+ }
+ }
public static unsafe string SimplifyPath(string path)
{
@@ -278,93 +264,13 @@ public static unsafe string SimplifyPath(string path)
if (path == ".")
return path;
- string simplifiedPath = s_cachedSimplifiedPaths.GetOrAdd(path, s =>
- {
- // First construct a path helper to help with the conversion
- char* arrayPtr = stackalloc char[PathHelper.MaxPath];
- PathHelper pathHelper = new PathHelper(arrayPtr, PathHelper.MaxPath);
-
- int index = 0;
- int pathLength = path.Length;
- int numDot = 0;
- int lowestRemovableIndex = 0;
- for (; index < pathLength; ++index)
- {
- char currentChar = path[index];
- if (currentChar == OtherSeparator)
- currentChar = Path.DirectorySeparatorChar;
-
- if (currentChar == '.')
- {
- ++numDot;
- if (numDot > 2)
- {
- throw new ArgumentException($"Invalid path format: {path}");
- }
- }
- else
- {
- if (numDot == 1)
- {
- if (currentChar == Path.DirectorySeparatorChar)
- {
- // Path starts a path of the format .\
- numDot = 0;
- continue;
- }
- else
- {
- pathHelper.Append('.');
- }
- numDot = 0;
- pathHelper.Append(currentChar);
- }
- else if (numDot == 2)
- {
- if (currentChar != Path.DirectorySeparatorChar)
- throw new ArgumentException($"Invalid path format: {path}");
-
- // Path contains a path of the format ..\
- bool success = pathHelper.RemoveLastDirectory(lowestRemovableIndex);
- if (!success)
- {
- pathHelper.Append('.');
- pathHelper.Append('.');
- lowestRemovableIndex = pathHelper.Length;
- }
- numDot = 0;
- if (pathHelper.Length > 0)
- pathHelper.Append(currentChar);
- }
- else
- {
- if (Util.IsRunningOnUnix() &&
- index == 0 && currentChar == Path.DirectorySeparatorChar && Path.IsPathRooted(path))
- pathHelper.Append(currentChar);
-
- if (currentChar != Path.DirectorySeparatorChar || pathHelper.Length > 0)
- pathHelper.Append(currentChar);
- }
- }
- }
- if (numDot == 2)
- {
- // Path contains a path of the format \..\
- if (!pathHelper.RemoveLastDirectory(lowestRemovableIndex))
- {
- pathHelper.Append('.');
- pathHelper.Append('.');
- }
- }
-
- return pathHelper.ToString();
- });
+ string simplifiedPath = s_cachedSimplifiedPaths.GetOrAdd(path, s => SimplifyPathImpl(s));
return simplifiedPath;
}
// Note: This method assumes that SimplifyPath has been called for the argument.
- internal static unsafe void SplitStringUsingStack(string path, char separator, int* splitIndexes, int* splitLengths, ref int splitElementsUsedCount, int splitArraySize)
+ internal static unsafe void SplitStringUsingStack(string path, int* splitIndexes, int* splitLengths, ref int splitElementsUsedCount, int splitArraySize)
{
int lastSeparatorIndex = -1;
int pathLength = path.Length;
@@ -372,10 +278,10 @@ internal static unsafe void SplitStringUsingStack(string path, char separator, i
{
char currentChar = path[index];
- if (currentChar == separator)
+ if (currentChar == Path.DirectorySeparatorChar)
{
if (splitElementsUsedCount == splitArraySize)
- throw new Exception("Too much path separators");
+ throw new Exception($"Too much path separators in path '{path}'");
int startIndex = lastSeparatorIndex + 1;
int length = index - startIndex;
@@ -398,66 +304,126 @@ internal static unsafe void SplitStringUsingStack(string path, char separator, i
}
}
- public static unsafe string PathGetRelative(string sourceFullPath, string destFullPath, bool ignoreCase = false)
+ ///
+ /// Return a relative version of a path from another.
+ ///
+ /// The path from which to compute the relative path.
+ /// The path to make relative.
+ /// WARNING: this argument is never used. Whatever the value provided, case will always be ignored.
+ ///
+ public static unsafe string PathGetRelative(string relativeTo, string path, bool ignoreCase = false)
{
- sourceFullPath = SimplifyPath(sourceFullPath);
- destFullPath = SimplifyPath(destFullPath);
+ // ------------------ THIS IS NOT CORRECT ----------------
+ // Force to always ignore case, whatever the user ask
+ // This keep the legacy Sharpmake behavior. It may be fixed at a later date to reduce modification scope.
+ ignoreCase = true;
+ // ------------------ THIS IS NOT CORRECT ----------------
- int* sourcePathIndexes = stackalloc int[128];
- int* sourcePathLengths = stackalloc int[128];
- int sourcePathNbrElements = 0;
- SplitStringUsingStack(sourceFullPath, Path.DirectorySeparatorChar, sourcePathIndexes, sourcePathLengths, ref sourcePathNbrElements, 128);
+ relativeTo = SimplifyPath(relativeTo);
+ path = SimplifyPath(path);
- int* destPathIndexes = stackalloc int[128];
- int* destPathLengths = stackalloc int[128];
- int destPathNbrElements = 0;
- SplitStringUsingStack(destFullPath, Path.DirectorySeparatorChar, destPathIndexes, destPathLengths, ref destPathNbrElements, 128);
+ // Check different root
+ var relativeToLength = relativeTo.Length;
+ var pathLength = path.Length;
+ if (relativeToLength == 0 || pathLength == 0 || !IsCharEqual(relativeTo[0], path[0], ignoreCase))
+ return path;
+
+ // Compute common part length
+ var lastCommonDirSepPosition = 0;
+ var commonPartLength = 0;
+ while (commonPartLength < relativeToLength && commonPartLength < pathLength && IsCharEqual(relativeTo[commonPartLength], path[commonPartLength], ignoreCase))
+ {
+ if (relativeTo[commonPartLength] == Path.DirectorySeparatorChar)
+ lastCommonDirSepPosition = commonPartLength;
+ ++commonPartLength;
+ }
- int samePathCounter = 0;
+#if NET7_0_OR_GREATER
+ [Obsolete("Directly use 'char.IsAsciiLetter()' in 'IsCharEqual()' bellow (char.IsAsciiLetter() is available starting net7)")]
+#endif
+ static bool IsAsciiLetter(char c) => (uint)((c | 0x20) - 'a') <= 'z' - 'a';
+ static bool IsCharEqual(char a, char b, bool ignoreCase) => a == b || (ignoreCase && (a | 0x20) == (b | 0x20) && IsAsciiLetter(a));
- // Find out common path length.
- //StringComparison comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
- int maxPathLength = Math.Min(sourcePathNbrElements, destPathNbrElements);
- for (int i = 0; i < maxPathLength; i++)
+ // Check if both paths are the same (ignoring the last directory separator if any)
+ if ((relativeToLength == commonPartLength && pathLength == commonPartLength)
+ || (relativeToLength == commonPartLength && pathLength == commonPartLength + 1 && path[commonPartLength] == Path.DirectorySeparatorChar)
+ || (pathLength == commonPartLength && relativeToLength == commonPartLength + 1 && relativeTo[commonPartLength] == Path.DirectorySeparatorChar))
{
- int sourceLength = sourcePathLengths[i];
- if (sourceLength != destPathLengths[i])
- break;
+ return ".";
+ }
- if (string.Compare(sourceFullPath, sourcePathIndexes[i], destFullPath, destPathIndexes[i], sourceLength, StringComparison.OrdinalIgnoreCase) != 0)
- break;
+ // Adjust 'commonPartLength' in case we stopped the comparison in the middle of an entry name:
+ // -> we went too far, we must move back 'commonPartLength' to the 'lastCommonDirSepPosition' position '+ 1'
+ // - /abc_def and /abc_xyz
+ // - /abc_ and /abc
+ if (commonPartLength < relativeToLength && commonPartLength < pathLength
+ || (relativeToLength == commonPartLength && path[commonPartLength] != Path.DirectorySeparatorChar)
+ || (pathLength == commonPartLength && relativeTo[commonPartLength] != Path.DirectorySeparatorChar))
+ {
+ commonPartLength = lastCommonDirSepPosition + 1;
+ }
- samePathCounter++;
+ // Compute the number of ".." to add (to get out of the 'relativeTo' path)
+ var dotDotCount = 0;
+ var relativeToLengthWithoutTrailingDirSep = relativeTo[^1] == Path.DirectorySeparatorChar ? relativeToLength - 1 : relativeToLength;
+ if (relativeToLengthWithoutTrailingDirSep > commonPartLength)
+ {
+ dotDotCount = 1;
+ for (int i = commonPartLength + 1; i < relativeToLengthWithoutTrailingDirSep; i++)
+ {
+ if (relativeTo[i] == Path.DirectorySeparatorChar)
+ ++dotDotCount;
+ }
}
- if (samePathCounter == 0)
- return destFullPath;
+ // Compute the length of the two parts to write
+ // - The sequences that looks like this: ".." or "../.." or ...
+ var dotDotCountSequenceLength = dotDotCount == 0 ? 0 : dotDotCount * 2 + dotDotCount - 1;
- if (sourcePathNbrElements == destPathNbrElements && sourcePathNbrElements == samePathCounter)
- return ".";
+ // - The remaining from the 'path' not yet added (skip the starting directory separator if any)
+ var remainingStartPosition = commonPartLength < pathLength && path[commonPartLength] == Path.DirectorySeparatorChar ? commonPartLength + 1 : commonPartLength;
+ var remainingLength = pathLength - remainingStartPosition;
- char* arrayPtr = stackalloc char[PathHelper.MaxPath];
- PathHelper pathHelper = new PathHelper(arrayPtr, PathHelper.MaxPath);
+ // This 'if' deviate from the standard .net behavior and is here to keep legacy Sharpmake behavior
+ // Trim last directory separator if any
+ if (remainingLength > 0 && path[^1] == Path.DirectorySeparatorChar)
+ --remainingLength;
- for (int i = samePathCounter; i < sourcePathNbrElements; i++)
+ // - The directory separator between the two parts (in case there is both dotDots and remains)
+ var needToInsertDirSepBeforeRemainingPart = dotDotCountSequenceLength > 0 && remainingLength > 0;
+
+ var finalLength =
+ dotDotCountSequenceLength
+ + remainingLength
+ + (needToInsertDirSepBeforeRemainingPart ? 1 : 0);
+
+ // Allocate and write in the buffer
+ Span arrayPtr = stackalloc char[finalLength];
+ int writePosition = 0;
+ if (dotDotCount > 0)
{
- if (pathHelper.Length > 0)
- pathHelper.Append(Path.DirectorySeparatorChar);
- pathHelper.Append('.');
- pathHelper.Append('.');
+ arrayPtr[writePosition++] = '.';
+ arrayPtr[writePosition++] = '.';
+ for (int i = 0; i < dotDotCount - 1; i++)
+ {
+ arrayPtr[writePosition++] = Path.DirectorySeparatorChar;
+ arrayPtr[writePosition++] = '.';
+ arrayPtr[writePosition++] = '.';
+ }
}
- for (int i = samePathCounter; i < destPathNbrElements; i++)
+ if (needToInsertDirSepBeforeRemainingPart)
+ arrayPtr[writePosition++] = Path.DirectorySeparatorChar;
+
+ if (remainingLength > 0)
{
- if (pathHelper.Length > 0)
- pathHelper.Append(Path.DirectorySeparatorChar);
- pathHelper.Append(destFullPath, destPathIndexes[i], destPathLengths[i]);
+ var remainAsSpan = path.AsSpan(remainingStartPosition, remainingLength);
+ remainAsSpan.CopyTo(arrayPtr.Slice(writePosition, remainingLength));
}
- return pathHelper.ToString();
+ return new string(arrayPtr);
}
-
public static List PathGetAbsolute(string sourceFullPath, Strings destFullPaths)
{
List result = new List(destFullPaths.Count);