diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 8a6a6d33f..b65ad7222 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -103,6 +103,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/Util/FileUtilityTests.cs b/ICSharpCode.Decompiler.Tests/Util/FileUtilityTests.cs new file mode 100644 index 000000000..cef3d91f3 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/Util/FileUtilityTests.cs @@ -0,0 +1,183 @@ +// Copyright (c) 2020 Daniel Grunwald +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using NUnit.Framework; + +namespace ICSharpCode.Decompiler.Util +{ + [TestFixture] + public class FileUtilityTests + { + #region NormalizePath + [Test] + public void NormalizePath() + { + Assert.AreEqual(@"c:\temp\test.txt", FileUtility.NormalizePath(@"c:\temp\project\..\test.txt")); + Assert.AreEqual(@"c:\temp\test.txt", FileUtility.NormalizePath(@"c:\temp\project\.\..\test.txt")); + Assert.AreEqual(@"c:\temp\test.txt", FileUtility.NormalizePath(@"c:\temp\\test.txt")); // normalize double backslash + Assert.AreEqual(@"c:\temp", FileUtility.NormalizePath(@"c:\temp\.")); + Assert.AreEqual(@"c:\temp", FileUtility.NormalizePath(@"c:\temp\subdir\..")); + } + + [Test] + public void NormalizePath_DriveRoot() + { + Assert.AreEqual(@"C:\", FileUtility.NormalizePath(@"C:\")); + Assert.AreEqual(@"C:\", FileUtility.NormalizePath(@"C:/")); + Assert.AreEqual(@"C:\", FileUtility.NormalizePath(@"C:")); + Assert.AreEqual(@"C:\", FileUtility.NormalizePath(@"C:/.")); + Assert.AreEqual(@"C:\", FileUtility.NormalizePath(@"C:/..")); + Assert.AreEqual(@"C:\", FileUtility.NormalizePath(@"C:/./")); + Assert.AreEqual(@"C:\", FileUtility.NormalizePath(@"C:/..\")); + } + + [Test] + public void NormalizePath_UNC() + { + Assert.AreEqual(@"\\server\share", FileUtility.NormalizePath(@"\\server\share")); + Assert.AreEqual(@"\\server\share", FileUtility.NormalizePath(@"\\server\share\")); + Assert.AreEqual(@"\\server\share", FileUtility.NormalizePath(@"//server/share/")); + Assert.AreEqual(@"\\server\share\otherdir", FileUtility.NormalizePath(@"//server/share/dir/..\otherdir")); + } + + [Test] + public void NormalizePath_Web() + { + Assert.AreEqual(@"http://danielgrunwald.de/path/", FileUtility.NormalizePath(@"http://danielgrunwald.de/path/")); + Assert.AreEqual(@"browser://http://danielgrunwald.de/path/", FileUtility.NormalizePath(@"browser://http://danielgrunwald.de/wrongpath/../path/")); + } + + [Test] + public void NormalizePath_Relative() + { + Assert.AreEqual(@"../b", FileUtility.NormalizePath(@"..\a\..\b")); + Assert.AreEqual(@".", FileUtility.NormalizePath(@".")); + Assert.AreEqual(@".", FileUtility.NormalizePath(@"a\..")); + } + + [Test] + public void NormalizePath_UnixStyle() + { + Assert.AreEqual("/", FileUtility.NormalizePath("/")); + Assert.AreEqual("/a/b", FileUtility.NormalizePath("/a/b")); + Assert.AreEqual("/a/b", FileUtility.NormalizePath("/c/../a/./b")); + Assert.AreEqual("/a/b", FileUtility.NormalizePath("/c/../../a/./b")); + } + #endregion + + [Test] + public void TestIsBaseDirectory() + { + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\a", @"C:\A\b\hello")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\a", @"C:\a")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\a\", @"C:\a\")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\a\", @"C:\a")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\a", @"C:\a\")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\A", @"C:\a")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\a", @"C:\A")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\a\x\fWufhweoe", @"C:\a\x\fwuFHweoe\a\b\hello")); + + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\b\..\A", @"C:\a")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\HELLO\..\B\..\a", @"C:\b\..\a")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\.\B\..\.\.\a", @"C:\.\.\.\.\.\.\.\a")); + + Assert.IsFalse(FileUtility.IsBaseDirectory(@"C:\b", @"C:\a\b\hello")); + Assert.IsFalse(FileUtility.IsBaseDirectory(@"C:\a\b\hello", @"C:\b")); + Assert.IsFalse(FileUtility.IsBaseDirectory(@"C:\a\x\fwufhweoe", @"C:\a\x\fwuFHweoex\a\b\hello")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\", @"C:\")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"C:\", @"C:\a\b\hello")); + Assert.IsFalse(FileUtility.IsBaseDirectory(@"C:\", @"D:\a\b\hello")); + } + + + [Test] + public void TestIsBaseDirectoryRelative() + { + Assert.IsTrue(FileUtility.IsBaseDirectory(@".", @"a\b")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@".", @"a")); + Assert.IsFalse(FileUtility.IsBaseDirectory(@".", @"c:\")); + Assert.IsFalse(FileUtility.IsBaseDirectory(@".", @"/")); + } + + [Test] + public void TestIsBaseDirectoryUnixStyle() + { + Assert.IsTrue(FileUtility.IsBaseDirectory(@"/", @"/")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"/", @"/a")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"/", @"/a/subdir")); + } + + [Test] + public void TestIsBaseDirectoryUNC() + { + Assert.IsTrue(FileUtility.IsBaseDirectory(@"\\server\share", @"\\server\share\dir\subdir")); + Assert.IsTrue(FileUtility.IsBaseDirectory(@"\\server\share", @"\\server\share\dir\subdir")); + Assert.IsFalse(FileUtility.IsBaseDirectory(@"\\server2\share", @"\\server\share\dir\subdir")); + } + + [Test] + public void TestGetRelativePath() + { + Assert.AreEqual(@"blub", FileUtility.GetRelativePath(@"C:\hello\.\..\a", @"C:\.\a\blub")); + Assert.AreEqual(@"..\a\blub", FileUtility.GetRelativePath(@"C:\.\.\.\.\hello", @"C:\.\blub\.\..\.\a\.\blub")); + Assert.AreEqual(@"..\a\blub", FileUtility.GetRelativePath(@"C:\.\.\.\.\hello\", @"C:\.\blub\.\..\.\a\.\blub")); + Assert.AreEqual(@".", FileUtility.GetRelativePath(@"C:\hello", @"C:\.\hello")); + Assert.AreEqual(@".", FileUtility.GetRelativePath(@"C:\", @"C:\")); + Assert.AreEqual(@"blub", FileUtility.GetRelativePath(@"C:\", @"C:\blub")); + Assert.AreEqual(@"D:\", FileUtility.GetRelativePath(@"C:\", @"D:\")); + Assert.AreEqual(@"D:\def", FileUtility.GetRelativePath(@"C:\abc", @"D:\def")); + + // casing troubles + Assert.AreEqual(@"blub", FileUtility.GetRelativePath(@"C:\hello\.\..\A", @"C:\.\a\blub")); + Assert.AreEqual(@"..\a\blub", FileUtility.GetRelativePath(@"C:\.\.\.\.\HELlo", @"C:\.\blub\.\..\.\a\.\blub")); + Assert.AreEqual(@"..\a\blub", FileUtility.GetRelativePath(@"C:\.\.\.\.\heLLo\A\..", @"C:\.\blub\.\..\.\a\.\blub")); + } + + [Test] + public void RelativeGetRelativePath() + { + // Relative path + Assert.AreEqual(@"a", FileUtility.GetRelativePath(@".", @"a")); + Assert.AreEqual(@"..", FileUtility.GetRelativePath(@"a", @".")); + Assert.AreEqual(@"..\b", FileUtility.GetRelativePath(@"a", @"b")); + Assert.AreEqual(@"..\..", FileUtility.GetRelativePath(@"a", @"..")); + + // Getting a path from an absolute path to a relative path isn't really possible; + // so we just keep the existing relative path (don't introduce incorrect '..\'). + Assert.AreEqual(@"def", FileUtility.GetRelativePath(@"C:\abc", @"def")); + } + + [Test] + public void GetRelativePath_Unix() + { + Assert.AreEqual(@"a", FileUtility.GetRelativePath("/", "/a")); + Assert.AreEqual(@"a\b", FileUtility.GetRelativePath("/", "/a/b")); + Assert.AreEqual(@"b", FileUtility.GetRelativePath("/a", "/a/b")); + } + + [Test] + public void TestIsEqualFile() + { + Assert.IsTrue(FileUtility.IsEqualFileName(@"C:\.\Hello World.Exe", @"C:\HELLO WOrld.exe")); + Assert.IsTrue(FileUtility.IsEqualFileName(@"C:\bla\..\a\my.file.is.this", @"C:\gg\..\.\.\.\.\a\..\a\MY.FILE.IS.THIS")); + + Assert.IsFalse(FileUtility.IsEqualFileName(@"C:\.\Hello World.Exe", @"C:\HELLO_WOrld.exe")); + Assert.IsFalse(FileUtility.IsEqualFileName(@"C:\a\my.file.is.this", @"C:\gg\..\.\.\.\.\a\..\b\MY.FILE.IS.THIS")); + } + } +} \ No newline at end of file diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index e36a2019b..22977662f 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -413,6 +413,7 @@ + diff --git a/ICSharpCode.Decompiler/Util/FileUtility.cs b/ICSharpCode.Decompiler/Util/FileUtility.cs new file mode 100644 index 000000000..aaf26fa06 --- /dev/null +++ b/ICSharpCode.Decompiler/Util/FileUtility.cs @@ -0,0 +1,304 @@ +// Copyright (c) 2020 Daniel Grunwald +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Text; + +namespace ICSharpCode.Decompiler.Util +{ + static class FileUtility + { + /// + /// Gets the normalized version of fileName. + /// Slashes are replaced with backslashes, backreferences "." and ".." are 'evaluated'. + /// + public static string NormalizePath(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + return fileName; + + int i; + + bool isWeb = false; + for (i = 0; i < fileName.Length; i++) + { + if (fileName[i] == '/' || fileName[i] == '\\') + break; + if (fileName[i] == ':') + { + if (i > 1) + isWeb = true; + break; + } + } + + char outputSeparator = '/'; + bool isRelative; + bool isAbsoluteUnixPath = false; + + StringBuilder result = new StringBuilder(); + if (isWeb == false && IsUNCPath(fileName)) + { + // UNC path + i = 2; + outputSeparator = '\\'; + result.Append(outputSeparator); + isRelative = false; + } + else + { + i = 0; + isAbsoluteUnixPath = fileName[0] == '/'; + isRelative = !isWeb && !isAbsoluteUnixPath && (fileName.Length < 2 || fileName[1] != ':'); + if (fileName.Length >= 2 && fileName[1] == ':') + { + outputSeparator = '\\'; + } + } + int levelsBack = 0; + int segmentStartPos = i; + for (; i <= fileName.Length; i++) + { + if (i == fileName.Length || fileName[i] == '/' || fileName[i] == '\\') + { + int segmentLength = i - segmentStartPos; + switch (segmentLength) + { + case 0: + // ignore empty segment (if not in web mode) + if (isWeb) + { + result.Append(outputSeparator); + } + break; + case 1: + // ignore /./ segment, but append other one-letter segments + if (fileName[segmentStartPos] != '.') + { + if (result.Length > 0) + result.Append(outputSeparator); + result.Append(fileName[segmentStartPos]); + } + break; + case 2: + if (fileName[segmentStartPos] == '.' && fileName[segmentStartPos + 1] == '.') + { + // remove previous segment + int j; + for (j = result.Length - 1; j >= 0 && result[j] != outputSeparator; j--) + { + } + if (j > 0) + { + result.Length = j; + } + else if (isAbsoluteUnixPath) + { + result.Length = 0; + } + else if (isRelative) + { + if (result.Length == 0) + levelsBack++; + else + result.Length = 0; + } + break; + } + else + { + // append normal segment + goto default; + } + default: + if (result.Length > 0) + result.Append(outputSeparator); + result.Append(fileName, segmentStartPos, segmentLength); + break; + } + segmentStartPos = i + 1; // remember start position for next segment + } + } + if (isWeb == false) + { + if (isRelative) + { + for (int j = 0; j < levelsBack; j++) + { + result.Insert(0, ".." + outputSeparator); + } + } + if (result.Length > 0 && result[result.Length - 1] == outputSeparator) + { + result.Length -= 1; + } + if (isAbsoluteUnixPath) + { + result.Insert(0, '/'); + } + if (result.Length == 2 && result[1] == ':') + { + result.Append(outputSeparator); + } + if (result.Length == 0) + return "."; + } + return result.ToString(); + } + + static bool IsUNCPath(string fileName) + { + return fileName.Length > 2 + && (fileName[0] == '\\' || fileName[0] == '/') + && (fileName[1] == '\\' || fileName[1] == '/'); + } + + public static bool IsEqualFileName(string fileName1, string fileName2) + { + return string.Equals(NormalizePath(fileName1), + NormalizePath(fileName2), + StringComparison.OrdinalIgnoreCase); + } + + public static bool IsBaseDirectory(string baseDirectory, string testDirectory) + { + if (baseDirectory == null || testDirectory == null) + return false; + baseDirectory = NormalizePath(baseDirectory); + if (baseDirectory == "." || baseDirectory == "") + return !Path.IsPathRooted(testDirectory); + baseDirectory = AddTrailingSeparator(baseDirectory); + testDirectory = AddTrailingSeparator(NormalizePath(testDirectory)); + + return testDirectory.StartsWith(baseDirectory, StringComparison.OrdinalIgnoreCase); + } + + static string AddTrailingSeparator(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + if (input[input.Length - 1] == Path.DirectorySeparatorChar || input[input.Length - 1] == Path.AltDirectorySeparatorChar) + return input; + else + return input + GetSeparatorForPath(input).ToString(); + } + + static char GetSeparatorForPath(string input) + { + if (input.Length > 2 && input[1] == ':' || IsUNCPath(input)) + return '\\'; + return '/'; + } + + readonly static char[] separators = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + + /// + /// Converts a given absolute path and a given base path to a path that leads + /// from the base path to the absoulte path. (as a relative path) + /// + public static string GetRelativePath(string baseDirectoryPath, string absPath) + { + if (string.IsNullOrEmpty(baseDirectoryPath)) + { + return absPath; + } + + baseDirectoryPath = NormalizePath(baseDirectoryPath); + absPath = NormalizePath(absPath); + + string[] bPath = baseDirectoryPath != "." ? baseDirectoryPath.TrimEnd(separators).Split(separators) : new string[0]; + string[] aPath = absPath != "." ? absPath.TrimEnd(separators).Split(separators) : new string[0]; + int indx = 0; + for (; indx < Math.Min(bPath.Length, aPath.Length); ++indx) + { + if (!bPath[indx].Equals(aPath[indx], StringComparison.OrdinalIgnoreCase)) + break; + } + + if (indx == 0 && (Path.IsPathRooted(baseDirectoryPath) || Path.IsPathRooted(absPath))) + { + return absPath; + } + + if (indx == bPath.Length && indx == aPath.Length) + { + return "."; + } + StringBuilder erg = new StringBuilder(); + for (int i = indx; i < bPath.Length; ++i) + { + erg.Append(".."); + erg.Append(Path.DirectorySeparatorChar); + } + erg.Append(String.Join(Path.DirectorySeparatorChar.ToString(), aPath, indx, aPath.Length - indx)); + if (erg[erg.Length - 1] == Path.DirectorySeparatorChar) + erg.Length -= 1; + return erg.ToString(); + } + + public static string TrimPath(string path, int max_chars) + { + const char ellipsis = '\u2026'; // HORIZONTAL ELLIPSIS + const int ellipsisLength = 2; + + if (path == null || path.Length <= max_chars) + return path; + char sep = Path.DirectorySeparatorChar; + if (path.IndexOf(Path.AltDirectorySeparatorChar) >= 0 && path.IndexOf(Path.DirectorySeparatorChar) < 0) + { + sep = Path.AltDirectorySeparatorChar; + } + string[] parts = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + int len = ellipsisLength; // For initial ellipsis + int index = parts.Length; + // From the end of the path, fit as many parts as possible: + while (index > 1 && len + parts[index - 1].Length < max_chars) + { + len += parts[index - 1].Length + 1; + index--; + } + + StringBuilder result = new StringBuilder(); + result.Append(ellipsis); + // If there's 5 chars left, partially fit another part: + if (index > 1 && len + 5 <= max_chars) + { + if (index == 2 && parts[0].Length <= ellipsisLength) + { + // If the partial part is part #1, + // and part #0 is as short as the ellipsis + // (e.g. just a drive letter), use part #0 + // instead of the ellipsis. + result.Clear(); + result.Append(parts[0]); + } + result.Append(sep); + result.Append(parts[index - 1], 0, max_chars - len - 3); + result.Append(ellipsis); + } + while (index < parts.Length) + { + result.Append(sep); + result.Append(parts[index]); + index++; + } + return result.ToString(); + } + } +}