using System.Text; using Flawless.Abstraction.Exceptions; namespace Flawless.Abstraction; /// /// A platform-independent path system for version controlling which provides a safe and easy used path calculation. /// Some of those function is a wrapper of with ensure of correction. /// public static class WorkPath { /* What is a valid work path? * * A worm path is something like this: * * root/subfolder1/subfolder2/file.txt * * 1. Should being trim at anytime * 2. Every separated name should being trim at anytime * 3. Should not start or end with '/' * 4. Directory separator is '/' * 5. Can be converted into and from any platform and self to be platform standalone * 6. Without any irregular or invisible character. */ private static readonly char[] InvalidPathChars = [ '\"', '\\', '<', '>', '|', '?', '*', ':', '^', '%', (char)0, (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31]; private static readonly HashSet InvalidPathCharsQuickTest = new(InvalidPathChars); public const char DirectorySeparatorChar = '/'; /// /// Check if path contains any invalid characters. Do not guarantee path is valid. /// /// Tested path. /// If there has no invalid path, return true. public static bool IsPathHasInvalidPathChars(string workPath) { foreach (var c in workPath) { if (!InvalidPathCharsQuickTest.Contains(c)) return true; } return false; } /// /// Get an array of invalid characters. Do not guarantee path is valid. /// /// Array of invalid characters public static char[] GetInvalidPathChars() { var r = new char[InvalidPathChars.Length]; InvalidPathChars.CopyTo(r, 0); return r; } /// /// Convert a work path into current platform file system path. Do not guarantee path is valid. /// /// A work path in this repository. This path will not do any validation. /// The root path of repository in platform path. /// A platform path mapping from work path. public static string ToPlatformPath(string workPath, string platformWorkingDirectory) { var sb = new StringBuilder(workPath.Length + platformWorkingDirectory.Length); sb.Append(platformWorkingDirectory); if (!Path.EndsInDirectorySeparator(platformWorkingDirectory)) sb.Append(Path.DirectorySeparatorChar); foreach (var c in workPath) sb.Append(c == DirectorySeparatorChar ? Path.DirectorySeparatorChar : c); return sb.ToString(); } /// /// Convert a platform-specific file system path into work path. Do not guarantee path is valid. /// /// A platform path. /// The root path of repository in platform path. /// Work path mapping from platform path . /// If platform path is not a sub entity in platform working path, /// this path will not being managed. So make error. public static string FromPlatformPath(string platformPath, string platformWorkingDirectory) { var workPath = Path.GetRelativePath(platformWorkingDirectory, platformPath); if (workPath == ".") return string.Empty; if (workPath == platformPath) // If not share same root, it will return platform path. So compare it directly. throw new PlatformPathNonManagedException(platformPath, platformWorkingDirectory); var sb = new StringBuilder(workPath.Length); foreach (var c in workPath) { var isSplitChar = c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; if (sb.Length == 0 && isSplitChar) continue; sb.Append(isSplitChar ? DirectorySeparatorChar : c); } return sb.ToString(); } /// /// Split work path into path vector. /// /// The path will being split. /// The list to store result (Non-allocate) /// The count of added elements /// Argument is null /// Work path is invalid public static int GetPathVector(string workPath, List result) { ArgumentNullException.ThrowIfNull(result); if (string.IsNullOrWhiteSpace(workPath)) throw new ArgumentNullException(nameof(workPath), "Not a valid work path!"); if (workPath[0] == DirectorySeparatorChar) throw new ArgumentException("Work path cannot start with a DirectorySeparatorChar!", nameof(workPath)); var start = 0; var end = 0; var count = 0; for (var i = 0; i <= workPath.Length; i++) { if (i < workPath.Length) { var c = workPath[i]; if (InvalidPathChars.Contains(c)) throw new ArgumentException("Invalid work path character: " + c); if (c != DirectorySeparatorChar) { end = i; continue; } } if (start >= end) throw new ArgumentException("Work path contains empty vector!"); if (workPath[end] == ' ' || workPath[start] == ' ') throw new ArgumentException("Work path vector can not start or end with a space!"); result.Add(workPath.Substring(start, end - start + 1)); count++; start = i + 1; } return count; } /// /// Split work path into path vector. /// /// The path will being split. /// Enumerable of vector /// Argument is null /// Work path is invalid public static IEnumerable GetPathVector(string workPath) { if (string.IsNullOrWhiteSpace(workPath)) throw new ArgumentNullException(nameof(workPath), "Not a valid work path!"); if (workPath[0] == DirectorySeparatorChar) throw new ArgumentException("Work path cannot start with a DirectorySeparatorChar!", nameof(workPath)); var start = 0; var end = 0; for (var i = 0; i <= workPath.Length; i++) { if (i < workPath.Length) { var c = workPath[i]; if (InvalidPathChars.Contains(c)) throw new ArgumentException("Invalid work path character: " + c); if (c != DirectorySeparatorChar) { end = i; continue; } } if (start >= end) throw new ArgumentException("Work path contains empty vector!"); if (workPath[end] == ' ' || workPath[start] == ' ') throw new ArgumentException("Work path vector can not start or end with a space!"); yield return workPath.Substring(start, end - start + 1); start = i + 1; } } /// /// Check work path is legal but not ensure that existed in platform. /// /// The path will being tested. /// True when path is valid. If you need more details, consider use /// . public static bool IsPathValid(string workPath) { if (string.IsNullOrWhiteSpace(workPath) || workPath[0] == DirectorySeparatorChar) return false; var start = 0; var end = 0; for (var i = 0; i <= workPath.Length; i++) { if (i < workPath.Length) { var c = workPath[i]; if (InvalidPathChars.Contains(c)) return false; if (c != DirectorySeparatorChar) { end = i; continue; } } if (start >= end || end >= workPath.Length || start >= workPath.Length || workPath[end] == ' ' || workPath[start] == ' ') return false; start = i + 1; } return true; } /// /// Check if last vector has a valid extension. /// /// Targeting work path /// Is valid. public static bool HasExtension(string workPath) { for (var i = workPath.Length - 1; i >= 0; i--) { var c = workPath[i]; if (c == DirectorySeparatorChar) break; if (c == '.') return true; } return false; } /// /// Get the last vector extension. /// /// Targeting work path /// The name of last vector extension. If the last vector is empty or no valid extension existed, /// return empty. public static string GetExtension(string workPath) { for (var i = workPath.Length - 1; i >= 0; i--) { var c = workPath[i]; if (c == DirectorySeparatorChar) break; if (c == '.') return i + 1 >= workPath.Length ? String.Empty : workPath.Substring(i + 1); } return string.Empty; } /// /// Change the last vector extension. /// /// Targeting work path /// Targeting extension, null means clean the extension /// Modified path. public static string ChangeExtension(string workPath, string? extension) { var hasExtension = extension != null; for (var i = workPath.Length - 1; i >= 0; i--) { var c = workPath[i]; if (c == DirectorySeparatorChar) break; if (c == '.') return workPath.Substring(0, hasExtension ? i + 1 : i) + extension; } return hasExtension ? workPath + "." + extension : workPath; } /// /// Get the last vector name. /// /// Targeting work path /// The name of last vector name. If the last vector is empty, return empty. public static string GetName(string workPath) { var start = workPath.Length - 1; var length = 0; for (var i = workPath.Length - 1; i >= 0; i--) { var c = workPath[i]; if (c == DirectorySeparatorChar) { start = i + 1; break; } start = i; length += 1; } return start < 0 ? string.Empty : workPath.Substring(start, length); } /// /// Get the last vector name without extension. /// /// Targeting work path /// The name of last vector without extension. If the last vector is empty, return empty. public static string GetNameWithoutExtension(string workPath) { var start = workPath.Length - 1; var end = workPath.Length; for (var i = workPath.Length - 1; i >= 0; i--) { var c = workPath[i]; start = i; if (end == workPath.Length && c == '.') end = i; if (c == DirectorySeparatorChar) { start += 1; break; } } return start < 0 || end <= start ? string.Empty : workPath.Substring(start, end - start); } /// /// Check if path is a root path. Do not guarantee path is valid. /// /// Work path being tested. /// True when is root. public static bool IsRootPath(string workPath) { return workPath.Contains(DirectorySeparatorChar); } /// /// Check if path is ended with directory separator. Do not guarantee path is valid. /// /// Work path being tested. /// True when is ended with directory separator. public static bool EndsInDirectorySeparator(string workPath) { return workPath.Length > 0 && workPath[^1] == DirectorySeparatorChar; } /// /// Get a relative work path from another work path in case first one is a sub path of second one. It will raise /// exception if path is invalid. /// /// The parent one. /// The child one. /// Null if workPath is not a child of relatedTo, or empty if workPath equals to relatedTo, or a new path /// when workPath is child of relatedTo. public static string? GetRelativePath(string relatedTo, string workPath) { if (workPath.Length == 0 || relatedTo.Length == 0) return null; using var parentTester = GetPathVector(relatedTo).GetEnumerator(); using var childTester = GetPathVector(workPath).GetEnumerator(); while (true) { var parentMoveNext = parentTester.MoveNext(); var childMoveNext = childTester.MoveNext(); // Both are not reach end if (parentMoveNext && childMoveNext) { if (parentTester.Current == childTester.Current) continue; // Or going to break if they are not equal. } // Only if child has left but parent has all pass away, this means they share a same parent. else if (childMoveNext && !parentMoveNext) { var sb = new StringBuilder(); sb.Append(childTester.Current); while (childTester.MoveNext()) sb.Append(DirectorySeparatorChar).Append(childTester.Current); return sb.ToString(); } // If they are same path, return empty to indicate that. else if (!childMoveNext && !parentMoveNext) return string.Empty; break; } return null; } private static void CombineInternal(StringBuilder sb, string addon) { if (addon.Length <= 0) return; if (sb.Length <= 0) { sb.Append(addon); return; } var endSep = sb[^1] == DirectorySeparatorChar; var startSep = addon[0] == DirectorySeparatorChar; if (!endSep && !startSep) { sb.Append(DirectorySeparatorChar); sb.Append(addon); } else if (endSep && startSep) { sb.Append(addon, 1, addon.Length - 1); } else { sb.Append(addon); } } /// /// Combine path one by one. Do not guarantee path is valid. /// /// /// It will connect those path in correct order and separator. /// /// Combine("abc", "def") => "abc/def"; /// Combine("abc", "def/") => "abc/def/"; /// Combine("abc", "def/gg") => "abc/def/gg"; /// Combine("abc/", "/def/gg") => "abc/def/gg"; /// /// /// The connected path public static string Combine(string path1, string path2) { var sb = new StringBuilder(); CombineInternal(sb, path1); CombineInternal(sb, path2); return sb.ToString(); } /// /// Combine path one by one. Do not guarantee path is valid. /// /// /// It will connect those path in correct order and separator. /// /// Combine("abc", "def") => "abc/def"; /// Combine("abc", "def/") => "abc/def/"; /// Combine("abc", "def/gg") => "abc/def/gg"; /// Combine("abc/", "/def/gg") => "abc/def/gg"; /// /// /// The connected path public static string Combine(string path1, string path2, string path3) { var sb = new StringBuilder(); CombineInternal(sb, path1); CombineInternal(sb, path2); CombineInternal(sb, path3); return sb.ToString(); } /// /// Combine path one by one. Do not guarantee path is valid. /// /// /// It will connect those path in correct order and separator. /// /// Combine("abc", "def") => "abc/def"; /// Combine("abc", "def/") => "abc/def/"; /// Combine("abc", "def/gg") => "abc/def/gg"; /// Combine("abc/", "/def/gg") => "abc/def/gg"; /// /// /// The connected path public static string Combine(string path1, string path2, string path3, string path4) { var sb = new StringBuilder(); CombineInternal(sb, path1); CombineInternal(sb, path2); CombineInternal(sb, path3); CombineInternal(sb, path4); return sb.ToString(); } /// /// Combine path one by one. Do not guarantee path is valid. /// /// /// It will connect those path in correct order and separator. /// /// Combine("abc", "def") => "abc/def"; /// Combine("abc", "def/") => "abc/def/"; /// Combine("abc", "def/gg") => "abc/def/gg"; /// Combine("abc/", "/def/gg") => "abc/def/gg"; /// /// /// The connected path public static string Combine(params string[] paths) { var sb = new StringBuilder(); foreach (var path in paths) CombineInternal(sb, path); return sb.ToString(); } }