// 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. #nullable enable using System; using System.Diagnostics.CodeAnalysis; 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'. /// [return: NotNullIfNotNull("fileName")] public static string? NormalizePath(string? fileName) { if (fileName == null || fileName.Length == 0) 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); } [return: NotNullIfNotNull("input")] static string? AddTrailingSeparator(string? input) { if (input == null || input.Length == 0) 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 (baseDirectoryPath == null || baseDirectoryPath.Length == 0) { 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(); } [return: NotNullIfNotNull("path")] 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(); } } }