diff --git a/history.md b/history.md
index bb2df853f6..4e99337e6f 100644
--- a/history.md
+++ b/history.md
@@ -45,6 +45,7 @@ Semantic Versioning 2.0.0 is from version 0.1.6+
- [x] (Changed) _Front-end_ Make prev / next more contrast (PR #1511)
- [x] (Fixed) _Docs_ Demo site is not working (PR #1486)
+- [x] (Fixed) _Back-end_ GetFileNameRegex refactor to avoid timeouts (PR #1515)
## version 0.6.0 - 2024-03-15 {#v0.6.0}
diff --git a/starsky/starsky.foundation.platform/Helpers/PathHelper.cs b/starsky/starsky.foundation.platform/Helpers/PathHelper.cs
index 018b3cfeb7..be2ac7808f 100644
--- a/starsky/starsky.foundation.platform/Helpers/PathHelper.cs
+++ b/starsky/starsky.foundation.platform/Helpers/PathHelper.cs
@@ -1,25 +1,13 @@
+using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
namespace starsky.foundation.platform.Helpers
{
- public static partial class PathHelper
+ public static class PathHelper
{
- ///
- /// Regex to match a filename in a path
- /// unescaped:
- /// [^/]+(?=(?:\.[^.]+)?$)
- /// pre compiled regex Regex.Match
- ///
- /// Regex object
- [GeneratedRegex(
- "[^/]+(?=(?:\\.[^.]+)?$)",
- RegexOptions.CultureInvariant,
- matchTimeoutMilliseconds: 300)]
- private static partial Regex GetFileNameRegex();
-
///
/// Return value (works for POSIX/Windows paths)
///
@@ -32,9 +20,33 @@ public static string GetFileName(string? filePath)
return string.Empty;
}
- return GetFileNameRegex().Match(filePath).Value;
+ if ( filePath.Length >= 4095 )
+ {
+ // why? https://serverfault.com/questions/9546/filename-length-limits-on-linux
+ throw new ArgumentException("[PathHelper] FilePath over Unix limits", nameof(filePath));
+ }
+
+ var fileName = GetFileNameUnix(filePath.AsSpan());
+ return fileName.ToString();
}
+ [SuppressMessage("Style", "IDE0057:Use range operator")]
+ [SuppressMessage("ReSharper", "ReplaceSliceWithRangeIndexer")]
+ internal static ReadOnlySpan GetFileNameUnix(ReadOnlySpan path)
+ {
+ var length = GetPathRootUnix(path).Length;
+ var num = path.LastIndexOf('/');
+ return path.Slice(num < length ? length : num + 1);
+ }
+
+ private static ReadOnlySpan GetPathRootUnix(ReadOnlySpan path)
+ {
+ return !IsPathRootedUnix(path) ? [] : "/".AsSpan();
+ }
+
+ private static bool IsPathRootedUnix(ReadOnlySpan path) =>
+ path.Length > 0 && path[0] == '/';
+
///
/// Removes the latest backslash. Path.DirectorySeparatorChar
///
@@ -52,7 +64,7 @@ public static string GetFileName(string? filePath)
// remove latest backslash
if ( basePath.Substring(basePath.Length - 1, 1) ==
- Path.DirectorySeparatorChar.ToString() )
+ Path.DirectorySeparatorChar.ToString() )
{
basePath = basePath.Substring(0, basePath.Length - 1);
}
@@ -94,7 +106,7 @@ public static string AddBackslash(string thumbnailTempFolder)
if ( string.IsNullOrWhiteSpace(thumbnailTempFolder) ) return thumbnailTempFolder;
if ( thumbnailTempFolder.Substring(thumbnailTempFolder.Length - 1,
- 1) != Path.DirectorySeparatorChar.ToString() )
+ 1) != Path.DirectorySeparatorChar.ToString() )
{
thumbnailTempFolder += Path.DirectorySeparatorChar.ToString();
}
diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelperTest.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelperTest.cs
index 11f62d3c5d..c6a45fd1b0 100644
--- a/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelperTest.cs
+++ b/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelperTest.cs
@@ -1,50 +1,49 @@
+using System;
using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using starsky.foundation.platform.Helpers;
-using starsky.foundation.storage.Helpers;
-using starskytest.FakeCreateAn;
namespace starskytest.starsky.foundation.platform.Helpers;
[TestClass]
public class PathHelperTests
{
- [TestMethod]
- public void GetFileName_ReturnsValidFileName()
+ [DataTestMethod] // [Theory]
+ [DataRow("path/to/file.txt", "file.txt")]
+ [DataRow("file.txt", "file.txt")]
+ [DataRow("/file.txt", "file.txt")]
+ [DataRow("/test/file.txt", "file.txt")]
+ [DataRow("/test/file/", "")]
+ [DataRow("/test/file", "file")] // no backslash
+ public void GetFileName_ReturnsValidFileName(string input, string expectedFileName)
{
- // Arrange
- const string filePath = "path/to/file.txt";
- const string expectedFileName = "file.txt";
+ // Act
+ var actualFileName = PathHelper.GetFileName(input);
+ // Assert
+ Assert.AreEqual(expectedFileName, actualFileName);
+ }
+
+ [DataTestMethod] // [Theory]
+ [DataRow("path/to/file.txt", "file.txt")]
+ [DataRow("/test/file/", "")]
+ [DataRow("/test/file", "file")] // no backslash
+ [DataRow("", "")]
+ public void GetFileNameUnix_ReturnsValidFileName(string input, string expectedFileName)
+ {
// Act
- var actualFileName = PathHelper.GetFileName(filePath);
+ var actualFileName = PathHelper.GetFileNameUnix(input).ToString();
// Assert
Assert.AreEqual(expectedFileName, actualFileName);
}
[TestMethod]
- [ExpectedException(typeof(RegexMatchTimeoutException))]
- public async Task GetFileName_ReturnsFileName_WithMaliciousInput_UnixOnly()
+ [ExpectedException(typeof(ArgumentException))]
+ public void GetFileName_ReturnsFileName_WithMaliciousInput()
{
// Act and Assert
- var test = await
- StreamToStringHelper.StreamToStringAsync(
- new MemoryStream(CreateAnImage.Bytes.ToArray()));
- var test2 = await
- StreamToStringHelper.StreamToStringAsync(
- new MemoryStream(CreateAnImageA6600.Bytes.ToArray()));
-
- var result = string.Empty;
- for ( var i = 0; i < 200; i++ )
- {
- result += test + test2 + test + test;
- }
-
- PathHelper.GetFileName(result);
+ PathHelper.GetFileName(TestContentVeryLongString);
}
[TestMethod]
@@ -104,11 +103,11 @@ public void RemoveLatestBackslash_ReturnsBasePath_WhenBasePathIsRoot()
public void RemoveLatestSlash_RemovesLatestSlash_WhenSlashExists()
{
// Arrange
- string basePath = "/path/to/directory/";
- string expectedPath = "/path/to/directory";
+ const string basePath = "/path/to/directory/";
+ const string expectedPath = "/path/to/directory";
// Act
- string actualPath = PathHelper.RemoveLatestSlash(basePath);
+ var actualPath = PathHelper.RemoveLatestSlash(basePath);
// Assert
Assert.AreEqual(expectedPath, actualPath);
@@ -118,10 +117,10 @@ public void RemoveLatestSlash_RemovesLatestSlash_WhenSlashExists()
public void RemoveLatestSlash_DoesNotRemoveSlash_WhenSlashDoesNotExist()
{
// Arrange
- string basePath = "/path/to/directory";
+ const string basePath = "/path/to/directory";
// Act
- string actualPath = PathHelper.RemoveLatestSlash(basePath);
+ var actualPath = PathHelper.RemoveLatestSlash(basePath);
// Assert
Assert.AreEqual(basePath, actualPath);
@@ -131,7 +130,7 @@ public void RemoveLatestSlash_DoesNotRemoveSlash_WhenSlashDoesNotExist()
public void RemoveLatestSlash_ReturnsEmptyString_WhenBasePathIsNull()
{
// Act
- string actualPath = PathHelper.RemoveLatestSlash(null!);
+ var actualPath = PathHelper.RemoveLatestSlash(null!);
// Assert
Assert.AreEqual(string.Empty, actualPath);
@@ -141,10 +140,10 @@ public void RemoveLatestSlash_ReturnsEmptyString_WhenBasePathIsNull()
public void RemoveLatestSlash_ReturnsEmptyString_WhenBasePathIsEmpty()
{
// Arrange
- string basePath = string.Empty;
+ var basePath = string.Empty;
// Act
- string actualPath = PathHelper.RemoveLatestSlash(basePath);
+ var actualPath = PathHelper.RemoveLatestSlash(basePath);
// Assert
Assert.AreEqual(string.Empty, actualPath);
@@ -154,10 +153,10 @@ public void RemoveLatestSlash_ReturnsEmptyString_WhenBasePathIsEmpty()
public void RemoveLatestSlash_ReturnsEmptyString_WhenBasePathIsRoot()
{
// Arrange
- string basePath = "/";
+ const string basePath = "/";
// Act
- string actualPath = PathHelper.RemoveLatestSlash(basePath);
+ var actualPath = PathHelper.RemoveLatestSlash(basePath);
// Assert
Assert.AreEqual(string.Empty, actualPath);
@@ -263,7 +262,7 @@ public void PrefixDbSlash_WhenCalledWithAlreadyPrefixedPath_ReturnsPathWithoutCh
const string expected = "/test/subfolder/file.txt";
// Act
- string result = PathHelper.PrefixDbSlash(subPath);
+ var result = PathHelper.PrefixDbSlash(subPath);
// Assert
Assert.AreEqual(expected, result);
@@ -273,10 +272,10 @@ public void PrefixDbSlash_WhenCalledWithAlreadyPrefixedPath_ReturnsPathWithoutCh
public void TestRemovePrefixDbSlash_WithLeadingSlash()
{
// Arrange
- string subPath = "/path/to/file";
+ const string subPath = "/path/to/file";
// Act
- string result = PathHelper.RemovePrefixDbSlash(subPath);
+ var result = PathHelper.RemovePrefixDbSlash(subPath);
// Assert
Assert.AreEqual("path/to/file", result);
@@ -286,10 +285,10 @@ public void TestRemovePrefixDbSlash_WithLeadingSlash()
public void TestRemovePrefixDbSlash_WithoutLeadingSlash()
{
// Arrange
- string subPath = "path/to/file";
+ const string subPath = "path/to/file";
// Act
- string result = PathHelper.RemovePrefixDbSlash(subPath);
+ var result = PathHelper.RemovePrefixDbSlash(subPath);
// Assert
Assert.AreEqual("path/to/file", result);
@@ -302,7 +301,7 @@ public void TestRemovePrefixDbSlash_OnlyLeadingSlash()
const string subPath = "/";
// Act
- string result = PathHelper.RemovePrefixDbSlash(subPath);
+ var result = PathHelper.RemovePrefixDbSlash(subPath);
// Assert
Assert.AreEqual(string.Empty, result);
@@ -361,4 +360,65 @@ public void TestSplitInputFilePaths_WithValidInput_ReturnsArrayOfStrings()
Assert.AreEqual("/path/to/file1", result[0]);
Assert.AreEqual("/path/to/file2", result[1]);
}
+
+ private const string TestContentVeryLongString =
+ "this-is-a-really-long-slug-that-goes-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-and-on-and-on-and-on-and-on-and-on-and-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-and-on-and-and-on-and-on-and-on-and-on-and-and-on-and-on-and-on-and-" +
+ "and-on-and-on-and-on-and-on-and-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-and-on-and-on-and-on-and-on-" +
+ "and-and-on-and-on-and-on-and-on-and-on-and-on-and-and-on-and-on-and-on-and-and-on-" +
+ "and-on-and-on-and-and-on-and-on-and-on-and-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-" +
+ "and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and" +
+ "-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on" +
+ "-and-on-and-on-and-on-and-on-and-and-on-and-on-and-on-and-on-and-on-and-on-and-" +
+ "on-and-on-and-on-and-and-on-and-and-on-and-on-and-on-and-and-on-and-and-on-and-" +
+ "on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-and-on-and" +
+ "-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and" +
+ "-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and" +
+ "-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and" +
+ "-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and-and";
}