Sfoglia il codice sorgente

fix: 在混合使用 / 和 \ 时路径裁剪有误

龙腾猫跃 5 mesi fa
parent
commit
5ee6f88cde

+ 1 - 1
Shared/Extensions/ExceptionExtensions.cs

@@ -42,11 +42,11 @@ public static class ExceptionExtensions {
         foreach (var currentEx in ex.Flatten()) {
             if (showMultilineStacks) {
                 lines.Add((isInnerException ? "→ " : "") + currentEx.Message.ReplaceLineEndings("\r\n", true));
-                if (currentEx.GetType() != typeof(Exception)) lines.Add("   错误类型:" + currentEx.GetType().FullName);
                 var stackLines = (currentEx.StackTrace?.SplitLines(true) ?? [])
                     .Select(l => l.BeforeLast("(") + l.AfterLast(")"))
                     .Distinct();
                 lines.AddRange(stackLines);
+                if (currentEx.GetType() != typeof(Exception)) lines.Add("   错误类型:" + currentEx.GetType().FullName);
             } else {
                 lines.Add(currentEx.Message.ReplaceLineEndings(" ", true));
             }

+ 82 - 7
Shared/Extensions/StringExtensions.cs

@@ -1,4 +1,5 @@
 using System.Text.RegularExpressions;
+using static System.Net.Mime.MediaTypeNames;
 
 namespace MeloongCore.Extensions;
 public static class StringExtensions {
@@ -79,7 +80,7 @@ public static class StringExtensions {
 
     #endregion
 
-    #region 分割与裁切
+    #region Split
 
     /// <summary>
     /// 分割字符串。
@@ -111,6 +112,16 @@ public static class StringExtensions {
     public static string[] Split(this string fullStr, char[] splitChars, bool removeEmptyEntries = false)
         => fullStr.Split(splitChars, removeEmptyEntries ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None);
 
+    /// <summary>
+    /// 将字符串分割为多行。
+    /// </summary>
+    public static string[] SplitLines(this string input, bool skipEmptyLines = false)
+        => input.ReplaceLineEndings("\n").Split('\n', skipEmptyLines);
+
+    #endregion
+
+    #region 裁切
+
     /// <summary>
     /// 获取在子字符串第一次出现之前的部分,如果未找到子字符串则不裁切。
     /// <code>"2024/11/08".BeforeFirst("/") => "2024"</code>
@@ -120,6 +131,22 @@ public static class StringExtensions {
         if (pos < 0) return str;
         return str.Substring(0, pos);
     }
+    /// <summary>
+    /// 获取在任意子字符串第一次出现之前的部分,如果未找到任意子字符串则不裁切。
+    /// <code>"2024/11-08".BeforeFirstOfAny(["/", "-"]) => "2024"</code>
+    /// </summary>
+    public static string BeforeFirstOfAny(this string str, IEnumerable<string> texts, bool ignoreCase = false) {
+        if (texts == null) return str;
+        int pos = -1;
+        foreach (string text in texts) {
+            if (string.IsNullOrEmpty(text)) continue;
+            int p = str.IndexOfF(text, ignoreCase);
+            if (p >= 0 && (pos < 0 || pos > p)) pos = p;
+        }
+        if (pos < 0) return str;
+        return str.Substring(0, pos);
+    }
+
     /// <summary>
     /// 获取在子字符串最后一次出现之前的部分,如果未找到子字符串则不裁切。
     /// <code>"2024/11/08".BeforeLast("/") => "2024/11"</code>
@@ -129,6 +156,21 @@ public static class StringExtensions {
         if (pos < 0) return str;
         return str.Substring(0, pos);
     }
+    /// <summary>
+    /// 获取在任意子字符串最后一次出现之前的部分,如果未找到任意子字符串则不裁切。
+    /// <code>"2024/11-08".BeforeLastOfAny(["/", "-"]) => "2024/11"</code>
+    /// </summary>
+    public static string BeforeLastOfAny(this string str, IEnumerable<string> texts, bool ignoreCase = false) {
+        if (texts == null) return str;
+        int pos = -1;
+        foreach (string text in texts) {
+            if (string.IsNullOrEmpty(text)) continue;
+            int p = str.LastIndexOfF(text, ignoreCase);
+            if (p >= 0 && (pos < 0 || pos < p)) pos = p;
+        }
+        if (pos < 0) return str;
+        return str.Substring(0, pos);
+    }
 
     /// <summary>
     /// 获取在子字符串第一次出现之后的部分,如果未找到子字符串则不裁切。
@@ -139,6 +181,26 @@ public static class StringExtensions {
         if (pos < 0) return str;
         return str.Substring(pos + text!.Length);
     }
+    /// <summary>
+    /// 获取在任意子字符串第一次出现之后的部分,如果未找到任意子字符串则不裁切。
+    /// <code>"2024/11-08".AfterFirstOfAny(["/", "-"]) => "11-08"</code>
+    /// </summary>
+    public static string AfterFirstOfAny(this string str, IEnumerable<string> texts, bool ignoreCase = false) {
+        if (texts == null) return str;
+        int pos = -1;
+        int len = 0;
+        foreach (string text in texts) {
+            if (string.IsNullOrEmpty(text)) continue;
+            int p = str.IndexOfF(text, ignoreCase);
+            if (p >= 0 && (pos < 0 || pos > p)) {
+                pos = p;
+                len = text.Length;
+            }
+        }
+        if (pos < 0) return str;
+        return str.Substring(pos + len);
+    }
+
     /// <summary>
     /// 获取在子字符串最后一次出现之后的部分,如果未找到子字符串则不裁切。
     /// <code>"2024/11/08".AfterLast("/") => "08"</code>
@@ -148,6 +210,25 @@ public static class StringExtensions {
         if (pos < 0) return str;
         return str.Substring(pos + text!.Length);
     }
+    /// <summary>
+    /// 获取在任意子字符串最后一次出现之后的部分,如果未找到任意子字符串则不裁切。
+    /// <code>"2024/11-08".AfterLastOfAny(["/", "-"]) => "08"</code>
+    /// </summary>
+    public static string AfterLastOfAny(this string str, IEnumerable<string> texts, bool ignoreCase = false) {
+        if (texts == null) return str;
+        int pos = -1;
+        int len = 0;
+        foreach (string text in texts) {
+            if (string.IsNullOrEmpty(text)) continue;
+            int p = str.LastIndexOfF(text, ignoreCase);
+            if (p >= 0 && (pos < 0 || pos < p)) {
+                pos = p;
+                len = text.Length;
+            }
+        }
+        if (pos < 0) return str;
+        return str.Substring(pos + len);
+    }
 
     /// <summary>
     /// 获取处于两个子字符串之间的部分,裁切尽可能多的内容。
@@ -192,12 +273,6 @@ public static class StringExtensions {
     private static readonly Regex regexLineEndingAndMerge = new(@"(?:\r\n|[\n\r\f\u0085\u2028\u2029])+", RegexOptions.Compiled);
     private static readonly Regex regexLineEnding = new(@"\r\n|[\n\r\f\u0085\u2028\u2029]", RegexOptions.Compiled);
 
-    /// <summary>
-    /// 将字符串分割为多行。
-    /// </summary>
-    public static string[] SplitLines(this string input, bool skipEmptyLines = false) 
-        => input.ReplaceLineEndings("\n").Split('\n', skipEmptyLines);
-
     #endregion
 
     #region 正则

+ 3 - 5
Shared/Utils/PathUtils.cs

@@ -94,10 +94,8 @@ public static class PathUtils {
     public static string RemoveLastPart(string path) {
         if (path!.Contains("://")) { // 网络路径
             path = PathUtils.RemoveSlashSuffix(path.BeforeFirst("#").BeforeFirst("?")); // 去除参数
-            return PathUtils.RemoveSlashSuffix(path).BeforeLast("/");
-        } else { // 文件路径
-            return PathUtils.RemoveSlashSuffix(path!).BeforeLast("\\");
         }
+        return PathUtils.RemoveSlashSuffix(path!).BeforeLastOfAny([@"\", "/"]);
     }
 
     /// <summary>
@@ -111,10 +109,10 @@ public static class PathUtils {
     public static string GetLastPart(string path) {
         if (path!.Contains("://")) { // 网络路径
             path = PathUtils.RemoveSlashSuffix(path.BeforeFirst("#").BeforeFirst("?")); // 去除参数
-            return path.AfterLast("/");
         } else { // 文件路径
-            return PathUtils.RemoveSlashSuffix(path).AfterLast("\\");
+            path = PathUtils.RemoveSlashSuffix(path);
         }
+        return path.AfterLastOfAny([@"\", "/"]);
     }
 
     /// <summary>

+ 43 - 18
Tests/Utils/PathUtilsTest.cs

@@ -5,6 +5,9 @@ public class PathUtilsTests : TestBase {
 
     [Test]
     [Arguments(@"\\?\C:\foo\bar.txt", @"\\?\C:\foo")]
+    [Arguments(@"C:\foo\extra/bar.txt", @"C:\foo\extra")]
+    [Arguments(@"C:\foo/extra/bar.txt", @"C:\foo/extra")]
+    [Arguments(@"C:\foo/extra\bar.txt", @"C:\foo/extra")]
     [Arguments(@"C:\foo\bar.txt", @"C:\foo")]
     [Arguments(@"\\?\C:\foo\Bar", @"\\?\C:\foo")]
     [Arguments(@"C:\foo\bar", @"C:\foo")]
@@ -12,27 +15,37 @@ public class PathUtilsTests : TestBase {
     [Arguments(@"\\?\C:\foo\", @"\\?\C:")]
     [Arguments(@"C:\foo", @"C:")]
     [Arguments(@"C:\foo\", @"C:")]
-    [Arguments("https://foo.bar/file/long/pack.zip?arg=1", "https://foo.bar/file/long")]
-    [Arguments("https://foo.bar/file/", "https://foo.bar")]
-    [Arguments("https://foo.bar/file/?arg=1", "https://foo.bar")]
-    [Arguments("https://foo.bar/file?arg=1", "https://foo.bar")]
+    [Arguments(@"https://foo.bar/file\long/pack.zip?arg=1", @"https://foo.bar/file\long")]
+    [Arguments(@"https://foo.bar/file\long\pack.zip?arg=1", @"https://foo.bar/file\long")]
+    [Arguments(@"https://foo.bar/file/long\pack.zip?arg=1", @"https://foo.bar/file/long")]
+    [Arguments(@"https://foo.bar/file/long/pack.zip?arg=1", @"https://foo.bar/file/long")]
+    [Arguments(@"https://foo.bar/file/", "https://foo.bar")]
+    [Arguments(@"https://foo.bar/file/?arg=1", "https://foo.bar")]
+    [Arguments(@"https://foo.bar/file?arg=1", "https://foo.bar")]
     public async Task 路径处理_RemoveLastPart(string input, string expected)
         => await Assert.That(PathUtils.RemoveLastPart(input)).IsEqualTo(expected);
 
     [Test]
     [Arguments(@"\\?\C:\foo\bar.txt", "bar.txt")]
+    [Arguments(@"C:\foo\extra/bar.txt", "bar.txt")]
+    [Arguments(@"C:\foo/extra/bar.txt", "bar.txt")]
+    [Arguments(@"C:\foo/extra\bar.txt", "bar.txt")]
     [Arguments(@"C:\foo\bar.txt", "bar.txt")]
-    [Arguments(@"\\?\C:\foo\bar", "bar")]
+    [Arguments(@"C:\foo\bar.txt", "bar.txt")]
+    [Arguments(@"\\?\C:\foo/bar", "bar")]
     [Arguments(@"C:\foo\bar", "bar")]
     [Arguments(@"C:\foo\bar\", "bar")]
     [Arguments(@"\\?\C:\foo\", "foo")]
     [Arguments(@"C:\foo", "foo")]
     [Arguments(@"C:\foo\", "foo")]
-    [Arguments("https://foo.bar/file/long/pack.zip?arg=1", "pack.zip")]
-    [Arguments("https://foo.bar/file/", "file")]
-    [Arguments("https://foo.bar/file/?arg=1", "file")]
-    [Arguments("https://foo.bar/File?arg=1", "File")]
-    [Arguments("https://cdn.modrinth.com/data/RvfAlf4Z/versions/XauQUBeR/Redstone Tweaks 2.5.3.zip?mr_download_reason=modpack", "Redstone Tweaks 2.5.3.zip")]
+    [Arguments(@"https://foo.bar/file\long/pack.zip?arg=1", "pack.zip")]
+    [Arguments(@"https://foo.bar/file\long\pack.zip?arg=1", "pack.zip")]
+    [Arguments(@"https://foo.bar/file/long\pack.zip?arg=1", "pack.zip")]
+    [Arguments(@"https://foo.bar/file/long/pack.zip?arg=1", "pack.zip")]
+    [Arguments(@"https://foo.bar/file/", "file")]
+    [Arguments(@"https://foo.bar/file/?arg=1", "file")]
+    [Arguments(@"https://foo.bar/File?arg=1", "File")]
+    [Arguments(@"https://cdn.modrinth.com/data/RvfAlf4Z/versions/XauQUBeR\Redstone Tweaks 2.5.3.zip?mr_download_reason=modpack", "Redstone Tweaks 2.5.3.zip")]
     public async Task 路径处理_GetLastPart(string input, string expected)
         => await Assert.That(PathUtils.GetLastPart(input)).IsEqualTo(expected);
 
@@ -41,24 +54,33 @@ public class PathUtilsTests : TestBase {
     [Arguments(@"C:\foo\bar.2.txt", "bar.2")]
     [Arguments(@"\\?\C:\foo\bar", "bar")]
     [Arguments(@"C:\foo\bar", "bar")]
+    [Arguments(@"C:\foo.bar\how/long\path.txt", "path")]
+    [Arguments(@"C:\foo.bar\how/long/path.txt", "path")]
+    [Arguments(@"C:\foo.bar\how\long/path.txt", "path")]
     [Arguments(@"C:\foo.bar\how\", "how")]
     [Arguments(@"\\?\C:\foo\", "foo")]
     [Arguments(@"C:\foo", "foo")]
     [Arguments(@"C:\foo\", "foo")]
-    [Arguments("cREate.jar.disabled", "cREate.jar")]
-    [Arguments("create.jar", "create")]
-    [Arguments("create", "create")]
-    [Arguments("https://foo.bar/file/page.xaml.vb?arg=1", "page.xaml")]
-    [Arguments("https://foo.bar/file/long/pack.zip?arg=1", "pack")]
-    [Arguments("https://foo.bar/some.how/file/", "file")]
-    [Arguments("https://foo.bar/file/?arg=1", "file")]
-    [Arguments("https://foo.bar/file?arg=1", "file")]
+    [Arguments(@"cREate.jar.disabled", "cREate.jar")]
+    [Arguments(@"create.jar", "create")]
+    [Arguments(@"create", "create")]
+    [Arguments(@"https://foo.bar/file\long/pack.zip?arg=1", "pack")]
+    [Arguments(@"https://foo.bar/file\long\pack.zip?arg=1", "pack")]
+    [Arguments(@"https://foo.bar/file/long\pack.zip?arg=1", "pack")]
+    [Arguments(@"https://foo.bar/file/long/pack.zip?arg=1", "pack")]
+    [Arguments(@"https://foo.bar/file/page.xaml.vb?arg=1", "page.xaml")]
+    [Arguments(@"https://foo.bar/some.how/file/", "file")]
+    [Arguments(@"https://foo.bar/file/?arg=1", "file")]
+    [Arguments(@"https://foo.bar/file?arg=1", "file")]
     public async Task 路径处理_GetFileNameWithoutExtension(string input, string expected)
         => await Assert.That(PathUtils.GetFileNameWithoutExtension(input)).IsEqualTo(expected);
 
     [Test]
     [Arguments(@"\\?\C:\FOO\BAR.TXT", "txt")]
     [Arguments(@"C:\foo\bar.txt", "txt")]
+    [Arguments(@"C:\foo.bar\how/long\path.txt", "txt")]
+    [Arguments(@"C:\foo.bar\how/long/path.txt", "txt")]
+    [Arguments(@"C:\foo.bar\how\long/path.txt", "txt")]
     [Arguments(@"\\?\C:\foo\bar", "")]
     [Arguments(@"C:\foo\bar", "")]
     [Arguments(@"C:\foo\bar\", "")]
@@ -66,6 +88,9 @@ public class PathUtilsTests : TestBase {
     [Arguments(@"C:\foo", "")]
     [Arguments(@"C:\foo\", "")]
     [Arguments("create.jar.diSAbled", "disabled")]
+    [Arguments(@"https://foo.bar/file\long/pack.zip?arg=1", "zip")]
+    [Arguments(@"https://foo.bar/file\long\pack.zip?arg=1", "zip")]
+    [Arguments(@"https://foo.bar/file/long\pack.zip?arg=1", "zip")]
     [Arguments("https://foo.bar/file/page.xaml.vb?arg=1", "vb")]
     [Arguments("https://foo.bar/file/long/pack.ZIP?arg=1", "zip")]
     [Arguments("https://foo.bar/file/", "")]