diff --git a/Directory.Build.props b/Directory.Build.props
index 53695a6..ba95efb 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -25,7 +25,7 @@
all
- 3.6.133
+ 3.6.139
diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj
index 9ce1493..e884ab0 100644
--- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj
+++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj
@@ -13,18 +13,18 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
-
-
-
-
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs
new file mode 100644
index 0000000..2ae5051
--- /dev/null
+++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs
@@ -0,0 +1,271 @@
+#if NET48_OR_GREATER || NETSTANDARD2_0
+
+using System.IO.Abstractions;
+using System;
+using System.IO;
+
+namespace AnakinRaW.CommonUtilities.FileSystem;
+
+// Mostly copied from https://github.com/dotnet/runtime
+public static partial class PathExtensions
+{
+ // \\?\, \\.\, \??\
+ internal const int DevicePrefixLength = 4;
+ // \\
+ internal const int UncPrefixLength = 2;
+ // \\?\UNC\, \\.\UNC\
+ internal const int UncExtendedPrefixLength = 8;
+
+ ///
+ /// Returns the file name and extension of a file path that is represented by a read-only character span.
+ ///
+ ///
+ /// A read-only span that contains the path from which to obtain the file name and extension.
+ /// The characters after the last directory separator character in .
+ ///
+ /// Under .NET Framework and .NET Standard 2.0 this method behaves like .NET Core for file names with ":", such as
+ /// "C:\file.txt:stream" --> "file.txt:stream" whereas in .NET Framework the result would be "stream".
+ ///
+ public static ReadOnlySpan GetFileName(this IPath _, ReadOnlySpan path)
+ {
+ if (IsUnixLikePlatform)
+ {
+ // While this case should not get reached, we add a safeguard and fallback to the .NET routine.
+ return _.GetFileName(path.ToString()).AsSpan();
+ }
+
+ var root = _.GetPathRoot(path).Length;
+
+ // We don't want to cut off "C:\file.txt:stream" (i.e. should be "file.txt:stream")
+ // but we *do* want "C:Foo" => "Foo". This necessitates checking for the root.
+
+ var i = _.DirectorySeparatorChar == _.AltDirectorySeparatorChar
+ ? path.LastIndexOf(_.DirectorySeparatorChar)
+ : path.LastIndexOfAny(_.DirectorySeparatorChar, _.AltDirectorySeparatorChar);
+
+ return path.Slice(i < root ? root : i + 1);
+ }
+
+ ///
+ /// Returns the characters between the last separator and last (.) in the path.
+ ///
+ public static ReadOnlySpan GetFileNameWithoutExtension(this IPath _, ReadOnlySpan path)
+ {
+ var fileName = _.GetFileName(path);
+ var lastPeriod = fileName.LastIndexOf('.');
+ return lastPeriod < 0 ?
+ fileName : // No extension was found
+ fileName.Slice(0, lastPeriod);
+ }
+
+ ///
+ /// Gets the root directory information from the path contained in the specified character span.
+ ///
+ ///
+ /// A read-only span of characters containing the path from which to obtain root directory information.
+ /// A read-only span of characters containing the root directory of .
+ public static ReadOnlySpan GetPathRoot(this IPath _, ReadOnlySpan path)
+ {
+ if (IsUnixLikePlatform)
+ {
+ // While this case should not get reached, we add a safeguard and fallback to the .NET routine.
+ return _.GetPathRoot(path.ToString()).AsSpan();
+ }
+
+ if (IsEffectivelyEmpty(path))
+ return ReadOnlySpan.Empty;
+
+ var pathRoot = GetRootLength(path);
+ return pathRoot <= 0 ? ReadOnlySpan.Empty : path.Slice(0, pathRoot);
+ }
+
+ ///
+ /// Returns if the path specified is absolute. This method does no
+ /// validation of the path.
+ ///
+ ///
+ /// Handles paths that use the alternate directory separator. It is a frequent mistake to
+ /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
+ /// "C:a" is drive relative-meaning that it will be resolved against the current directory
+ /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
+ /// will not be used to modify the path).
+ ///
+ ///
+ /// A file path.
+ /// if the path is fixed to a specific drive or UNC path; if the path is relative to the current drive or working directory.
+ public static bool IsPathFullyQualified(this IPath _, ReadOnlySpan path)
+ {
+ if (IsUnixLikePlatform)
+ {
+ // While this case should not get reached, we add a safeguard and fallback to the .NET routine.
+ return _.IsPathRooted(path.ToString());
+ }
+
+ if (path.Length < 2)
+ return false;
+
+ if (IsAnyDirectorySeparator(path[0]))
+ return path[1] == '?' || IsAnyDirectorySeparator(path[1]);
+
+ return path.Length >= 3
+ && path[1] == VolumeSeparatorChar
+ && IsAnyDirectorySeparator(path[2])
+ && IsValidDriveChar(path[0]);
+ }
+
+ ///
+ /// Returns if the path specified is absolute. This method does no
+ /// validation of the path.
+ ///
+ ///
+ /// Handles paths that use the alternate directory separator. It is a frequent mistake to
+ /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
+ /// "C:a" is drive relative-meaning that it will be resolved against the current directory
+ /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
+ /// will not be used to modify the path).
+ ///
+ ///
+ /// A file path.
+ /// if the path is fixed to a specific drive or UNC path; if the path is relative to the current drive or working directory.
+ /// is .
+ public static bool IsPathFullyQualified(this IPath _, string path)
+ {
+ if (path == null)
+ throw new ArgumentNullException(nameof(path));
+ return IsPathFullyQualified(_, path.AsSpan());
+ }
+
+ ///
+ /// Returns a value that indicates whether a file path contains a root.
+ ///
+ ///
+ /// The path to test.
+ /// if contains a root; otherwise, .
+ public static bool IsPathRooted(this IPath _, ReadOnlySpan path)
+ {
+ var length = path.Length;
+ return (length >= 1 && IsAnyDirectorySeparator(path[0]))
+ || (length >= 2 && IsValidDriveChar(path[0]) && path[1] == VolumeSeparatorChar);
+ }
+
+ internal static bool IsEffectivelyEmpty(ReadOnlySpan path)
+ {
+ if (path.IsEmpty)
+ return true;
+
+ foreach (var c in path)
+ {
+ if (c != ' ')
+ return false;
+ }
+ return true;
+ }
+
+ ///
+ /// Gets the length of the root of the path (drive, share, etc.).
+ ///
+ internal static int GetRootLength(ReadOnlySpan path)
+ {
+ var pathLength = path.Length;
+ var i = 0;
+
+ var deviceSyntax = IsDevice(path);
+ var deviceUnc = deviceSyntax && IsDeviceUNC(path);
+
+ if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsAnyDirectorySeparator(path[0]))
+ {
+ // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
+ if (deviceUnc || (pathLength > 1 && IsAnyDirectorySeparator(path[1])))
+ {
+ // UNC (\\?\UNC\ or \\), scan past server\share
+
+ // Start past the prefix ("\\" or "\\?\UNC\")
+ i = deviceUnc ? UncExtendedPrefixLength : UncPrefixLength;
+
+ // Skip two separators at most
+ var n = 2;
+ while (i < pathLength && (!IsAnyDirectorySeparator(path[i]) || --n > 0))
+ i++;
+ }
+ else
+ {
+ // Current drive rooted (e.g. "\foo")
+ i = 1;
+ }
+ }
+ else if (deviceSyntax)
+ {
+ // Device path (e.g. "\\?\.", "\\.\")
+ // Skip any characters following the prefix that aren't a separator
+ i = DevicePrefixLength;
+ while (i < pathLength && !IsAnyDirectorySeparator(path[i]))
+ i++;
+
+ // If there is another separator take it, as long as we have had at least one
+ // non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\")
+ if (i < pathLength && i > DevicePrefixLength && IsAnyDirectorySeparator(path[i]))
+ i++;
+ }
+ else if (pathLength >= 2
+ && path[1] == VolumeSeparatorChar
+ && IsValidDriveChar(path[0]))
+ {
+ // Valid drive specified path ("C:", "D:", etc.)
+ i = 2;
+
+ // If the colon is followed by a directory separator, move past it (e.g "C:\")
+ if (pathLength > 2 && IsAnyDirectorySeparator(path[2]))
+ i++;
+ }
+ return i;
+ }
+
+ ///
+ /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
+ ///
+ private static bool IsDevice(ReadOnlySpan path)
+ {
+ // If the path begins with any two separators is will be recognized and normalized and prepped with
+ // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
+ return IsExtended(path)
+ ||
+ (
+ path.Length >= DevicePrefixLength
+ && IsAnyDirectorySeparator(path[0])
+ && IsAnyDirectorySeparator(path[1])
+ && (path[2] == '.' || path[2] == '?')
+ && IsAnyDirectorySeparator(path[3])
+ );
+ }
+
+ ///
+ /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\)
+ ///
+ private static bool IsDeviceUNC(ReadOnlySpan path)
+ {
+ return path.Length >= UncExtendedPrefixLength
+ && IsDevice(path)
+ && IsAnyDirectorySeparator(path[7])
+ && path[4] == 'U'
+ && path[5] == 'N'
+ && path[6] == 'C';
+ }
+
+ ///
+ /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
+ /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
+ /// and path length checks.
+ ///
+ private static bool IsExtended(ReadOnlySpan path)
+ {
+ // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
+ // Skipping of normalization will *only* occur if back slashes ('\') are used.
+ return path.Length >= DevicePrefixLength
+ && path[0] == '\\'
+ && (path[1] == '\\' || path[1] == '?')
+ && path[2] == '?'
+ && path[3] == '\\';
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs
index db1757e..e058430 100644
--- a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs
+++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs
@@ -6,16 +6,18 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AnakinRaW.CommonUtilities.FileSystem.Normalization;
+using AnakinRaW.CommonUtilities.FileSystem.Utilities;
namespace AnakinRaW.CommonUtilities.FileSystem;
+// ReSharper disable once PartialTypeWithSinglePart
// Based on https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs
// and https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Common/PathUtil/PathUtility.cs
// and https://github.com/dotnet/runtime
///
/// Provides extension methods for the class.
///
-public static class PathExtensions
+public static partial class PathExtensions
{
private const string ThisDirectory = ".";
private const string ParentRelativeDirectory = "..";
@@ -47,7 +49,7 @@ public static bool HasTrailingDirectorySeparator(this IPath _, string path)
}
///
- /// Determines whether the specified path ends with a directory path separator
+ /// Determines whether the specified path ends with a directory path separator.
///
///
/// The path to check.
@@ -71,17 +73,17 @@ private static bool IsAnyDirectorySeparator(char c)
}
///
- /// Checks whether a path is rooted, but not absolute, to a drive e.g, "C:" or "C:my/path"
+ /// Checks whether a character span is rooted, but not absolute, to a drive e.g, "C:" or "C:my/path"
///
///
/// Only works on Windows. For Linux systems, this method will always return .
///
/// The file system's path instance.
- /// The path to check.
+ /// The character span to check.
/// If is drive relative the drive's letter will be stored into this variable.
/// if is not drive relative.
/// Return if is relative, but not absolute to a drive; otherwise, .
- public static bool IsDriveRelative(this IPath fsPath, string? path, [NotNullWhen(true)] out char? driveLetter)
+ public static bool IsDriveRelative(this IPath fsPath, ReadOnlySpan path, [NotNullWhen(true)] out char? driveLetter)
{
driveLetter = null;
@@ -110,14 +112,28 @@ public static bool IsDriveRelative(this IPath fsPath, string? path, [NotNullWhen
return false;
}
- internal static string TrimTrailingSeparators(ReadOnlySpan path, DirectorySeparatorKind separatorKind = DirectorySeparatorKind.System)
+ ///
+ /// Checks whether a path is rooted, but not absolute, to a drive e.g, "C:" or "C:my/path"
+ ///
+ ///
+ /// Only works on Windows. For Linux systems, this method will always return .
+ ///
+ /// The file system's path instance.
+ /// The path to check.
+ /// If is drive relative the drive's letter will be stored into this variable.
+ /// if is not drive relative.
+ /// Return if is relative, but not absolute to a drive; otherwise, .
+ public static bool IsDriveRelative(this IPath fsPath, string? path, [NotNullWhen(true)] out char? driveLetter)
+ {
+ return fsPath.IsDriveRelative(path.AsSpan(), out driveLetter);
+ }
+
+ internal static ReadOnlySpan TrimTrailingSeparators(ReadOnlySpan path, DirectorySeparatorKind separatorKind = DirectorySeparatorKind.System)
{
var lastSeparator = path.Length;
while (lastSeparator > 0 && IsAnyDirectorySeparator(path[lastSeparator - 1], separatorKind))
lastSeparator -= 1;
- if (lastSeparator != path.Length)
- path = path.Slice(0, lastSeparator);
- return path.ToString();
+ return lastSeparator != path.Length ? path.Slice(0, lastSeparator) : path;
}
@@ -149,26 +165,25 @@ private static bool IsAnyDirectorySeparator(char c, DirectorySeparatorKind separ
}
}
- internal static string EnsureTrailingSeparatorInternal(string input)
+
+ internal static void EnsureTrailingSeparatorInternal(ref ValueStringBuilder stringBuilder)
{
- if (input.Length == 0 || IsAnyDirectorySeparator(input[input.Length - 1]))
- return input;
+ if (stringBuilder.Length == 0 || IsAnyDirectorySeparator(stringBuilder[stringBuilder.Length - 1]))
+ return;
// Use the existing slashes in the path, if they're consistent
- var hasPrimarySlash = input.IndexOf(DirectorySeparatorChar) >= 0;
- var hasAlternateSlash = input.IndexOf(AltDirectorySeparatorChar) >= 0;
-
- if (hasPrimarySlash && !hasAlternateSlash)
- return input + DirectorySeparatorChar;
+ var hasPrimarySlash = stringBuilder.RawChars.IndexOf(DirectorySeparatorChar) >= 0;
+ var hasAlternateSlash = stringBuilder.RawChars.IndexOf(AltDirectorySeparatorChar) >= 0;
if (!hasPrimarySlash && hasAlternateSlash)
- return input + AltDirectorySeparatorChar;
+ {
+ stringBuilder.Append(AltDirectorySeparatorChar);
+ return;
+ }
- // If there are no slashes, or they are inconsistent, use the current platform's primary slash.
- return input + DirectorySeparatorChar;
+ stringBuilder.Append(DirectorySeparatorChar);
}
-
///
/// Returns a relative path from a path to given root or if is not rooted.
/// In contrast to .NET's Path.GetRelativePath(string, string), if is not rooted this method returns .
@@ -187,10 +202,10 @@ public static string GetRelativePathEx(this IPath fsPath, string root, string pa
// Root should always be absolute
root = fsPath.GetFullPath(root);
- root = TrimTrailingSeparators(root.AsSpan());
+ root = TrimTrailingSeparators(root.AsSpan()).ToString();
path = fsPath.GetFullPath(path);
- var trimmedPath = TrimTrailingSeparators(path.AsSpan());
+ var trimmedPath = TrimTrailingSeparators(path.AsSpan()).ToString();
var rootParts = GetPathParts(root);
var pathParts = GetPathParts(trimmedPath);
@@ -213,35 +228,44 @@ public static string GetRelativePathEx(this IPath fsPath, string root, string pa
if (index == 0)
return path;
- var relativePath = string.Empty;
+ var sb = new ValueStringBuilder(stackalloc char[260]);
// add backup notation for remaining base path levels beyond the index
var remainingParts = rootParts.Length - index;
if (remainingParts > 0)
{
for (var i = 0; i < remainingParts; i++)
- relativePath = relativePath + ParentRelativeDirectory + DirectorySeparatorStr;
+ {
+ sb.Append(ParentRelativeDirectory);
+ sb.Append(DirectorySeparatorChar);
+ }
}
if (index < pathParts.Length)
{
// add the rest of the full path parts
for (var i = index; i < pathParts.Length; i++)
- relativePath = CombinePathsUnchecked(relativePath, pathParts[i]);
+ CombinePathsUnchecked(ref sb, pathParts[i]);
- if (endsWithTrailingPathSeparator)
- relativePath = EnsureTrailingSeparatorInternal(relativePath);
+ if (endsWithTrailingPathSeparator)
+ EnsureTrailingSeparatorInternal(ref sb);
}
else
{
- if (!string.IsNullOrEmpty(relativePath))
- relativePath = TrimTrailingSeparators(relativePath.AsSpan());
+ if (sb.Length > 0)
+ {
+ if (HasTrailingDirectorySeparator(sb.AsSpan()))
+ sb.Length -= 1;
+ }
}
- if (relativePath == string.Empty)
+ if (sb.Length == 0)
return ThisDirectory;
- return relativePath;
+
+ var result = sb.ToString();
+ sb.Dispose();
+ return result;
}
private static string[] GetPathParts(string path)
@@ -277,14 +301,17 @@ internal static bool PathsEqual(string path1, string path2)
return PathsEqual(path1, path2, Math.Max(path1.Length, path2.Length));
}
- private static string CombinePathsUnchecked(string root, string relativePath)
+ private static void CombinePathsUnchecked(ref ValueStringBuilder sb, string relativePath)
{
- if (root == string.Empty)
- return relativePath;
- var c = root[root.Length - 1];
- if (!IsAnyDirectorySeparator(c) && c != VolumeSeparatorChar)
- return root + DirectorySeparatorStr + relativePath;
- return root + relativePath;
+ if (sb.Length == 0)
+ {
+ sb.Append(relativePath);
+ return;
+ }
+ var c = sb[sb.Length - 1];
+ if (!IsAnyDirectorySeparator(c) && c != VolumeSeparatorChar)
+ sb.Append(DirectorySeparatorChar);
+ sb.Append(relativePath);
}
///
@@ -332,67 +359,6 @@ public static bool IsChildOf(this IPath _, string basePath, string candidate)
&& (IsAnyDirectorySeparator(fullBase[fullBase.Length - 1]) || IsAnyDirectorySeparator(fullCandidate[fullBase.Length]));
}
-#if !NET && !NETsta
-
- ///
- /// Returns if the path specified is absolute. This method does no
- /// validation of the path.
- ///
- ///
- /// Handles paths that use the alternate directory separator. It is a frequent mistake to
- /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
- /// "C:a" is drive relative- meaning that it will be resolved against the current directory
- /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
- /// will not be used to modify the path).
- ///
- ///
- /// A file path.
- /// if the path is fixed to a specific drive or UNC path; if the path is relative to the current drive or working directory.
- public static bool IsPathFullyQualified(this IPath _, ReadOnlySpan path)
- {
- if (IsUnixLikePlatform)
- {
- // While this case should not get reached, we add a safeguard and fallback to the .NET routine.
- return _.IsPathRooted(path.ToString());
- }
-
- if (path.Length < 2)
- return false;
-
- if (IsAnyDirectorySeparator(path[0]))
- return path[1] == '?' || IsAnyDirectorySeparator(path[1]);
-
- return path.Length >= 3
- && path[1] == VolumeSeparatorChar
- && IsAnyDirectorySeparator(path[2])
- && IsValidDriveChar(path[0]);
- }
-
- ///
- /// Returns if the path specified is absolute. This method does no
- /// validation of the path.
- ///
- ///
- /// Handles paths that use the alternate directory separator. It is a frequent mistake to
- /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
- /// "C:a" is drive relative-meaning that it will be resolved against the current directory
- /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
- /// will not be used to modify the path).
- ///
- ///
- /// A file path.
- /// if the path is fixed to a specific drive or UNC path; if the path is relative to the current drive or working directory.
- /// is .
- public static bool IsPathFullyQualified(this IPath _, string path)
- {
- if (path == null)
- throw new ArgumentNullException(nameof(path));
- return IsPathFullyQualified(_, path.AsSpan());
- }
-
-#endif
-
-
internal static bool IsValidDriveChar(char value)
{
return (uint)((value | 0x20) - 'a') <= 'z' - 'a';
diff --git a/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs b/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs
index 1096c3b..5b9cdd5 100644
--- a/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs
+++ b/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
+using AnakinRaW.CommonUtilities.FileSystem.Utilities;
namespace AnakinRaW.CommonUtilities.FileSystem.Normalization;
@@ -20,77 +21,121 @@ public static class PathNormalizer
/// The normalization failed due to an internal error.
public static string Normalize(string path, PathNormalizeOptions options)
{
- if (string.IsNullOrEmpty(path))
- {
- if (path is null)
- throw new ArgumentNullException(path);
- throw new ArgumentException("The value cannot be an empty string.", path);
- }
-
- path = options.TrailingDirectorySeparatorBehavior switch
- {
- TrailingDirectorySeparatorBehavior.Trim => PathExtensions.TrimTrailingSeparators(path.AsSpan(), options.UnifySeparatorKind),
- TrailingDirectorySeparatorBehavior.Ensure => PathExtensions.EnsureTrailingSeparatorInternal(path),
- _ => path
- };
+ var stringBuilder = new ValueStringBuilder(stackalloc char[260]);
+ Normalize(path.AsSpan(), ref stringBuilder, options);
+ var result = stringBuilder.ToString();
+ stringBuilder.Dispose();
+ return result;
+ }
- // Only do for DirectorySeparatorKind.System, cause for other kinds it will be done at the very end anyway.
- if (options.UnifyDirectorySeparators && options.UnifySeparatorKind == DirectorySeparatorKind.System)
- path = GetPathWithDirectorySeparator(path, DirectorySeparatorKind.System);
+ ///
+ /// Normalizes a given character span that represents a file path to a destination according to given normalization rules.
+ ///
+ /// The path to normalize
+ /// The destination span which contains the normalized path.
+ /// The options how to normalize.
+ /// The number of characters written into the destination span.
+ /// This method populates even if is too small.
+ /// If the is too small.
+ public static int Normalize(ReadOnlySpan path, Span destination, PathNormalizeOptions options)
+ {
+ var stringBuilder = new ValueStringBuilder(destination);
+ Normalize(path, ref stringBuilder, options);
+ if (!stringBuilder.TryCopyTo(destination, out var charsWritten))
+ throw new ArgumentException("Cannot copy to destination span", nameof(destination));
+ return charsWritten;
+ }
- path = NormalizeCasing(path, options.UnifyCase);
+ internal static void Normalize(ReadOnlySpan path, ref ValueStringBuilder sb, PathNormalizeOptions options)
+ {
+ if (path == null)
+ throw new ArgumentNullException(nameof(path));
+ if (path.Length == 0)
+ throw new ArgumentException(nameof(path));
+
+ switch (options.TrailingDirectorySeparatorBehavior)
+ {
+ case TrailingDirectorySeparatorBehavior.Trim:
+ var trimmedPath = PathExtensions.TrimTrailingSeparators(path, options.UnifySeparatorKind);
+ sb.Append(trimmedPath);
+ break;
+ case TrailingDirectorySeparatorBehavior.Ensure:
+ sb.Append(path);
+ PathExtensions.EnsureTrailingSeparatorInternal(ref sb);
+ break;
+ case TrailingDirectorySeparatorBehavior.None:
+ sb.Append(path);
+ break;
+ default:
+ throw new IndexOutOfRangeException();
+ }
- // NB: As previous steps may add new separators (such as GetFullPath) we need to re-apply slash normalization
- // if the desired DirectorySeparatorKind is not DirectorySeparatorKind.System
- if (options.UnifyDirectorySeparators && options.UnifySeparatorKind != DirectorySeparatorKind.System)
- path = GetPathWithDirectorySeparator(path, options.UnifySeparatorKind);
+ // As the trailing directory normalization might add new separators, this step must come after.
+ if (options.UnifyDirectorySeparators)
+ GetPathWithDirectorySeparator(sb.RawChars, options.UnifySeparatorKind);
- return path;
+ NormalizeCasing(sb.RawChars, options.UnifyCase);
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static string NormalizeCasing(string path, UnifyCasingKind casing)
+ private static bool RequiresCasing(UnifyCasingKind casingOption)
{
- if (casing == UnifyCasingKind.None)
- return path;
+ if (casingOption == UnifyCasingKind.None)
+ return false;
+ if (!PathExtensions.IsFileSystemCaseInsensitive.Value && !casingOption.IsForce())
+ return false;
+ return true;
+ }
- if (!PathExtensions.IsFileSystemCaseInsensitive.Value && !casing.IsForce())
- return path;
+ private static unsafe void NormalizeCasing(Span path, UnifyCasingKind casing)
+ {
+ if (!RequiresCasing(casing))
+ return;
- if (casing is UnifyCasingKind.LowerCaseForce or UnifyCasingKind.LowerCase)
- return path.ToLowerInvariant();
+ delegate* transformation;
+ if (casing is UnifyCasingKind.LowerCase or UnifyCasingKind.LowerCaseForce)
+ transformation = &ToLower;
+ else
+ transformation = &ToUpper;
- if (casing is UnifyCasingKind.UpperCase or UnifyCasingKind.UpperCaseForce)
- return path.ToUpperInvariant();
+ for (var i = 0; i < path.Length; i++)
+ {
+ var c = path[i];
+ path[i] = transformation(c);
+ }
+ }
- throw new ArgumentOutOfRangeException(nameof(casing));
+ private static char ToLower(char c)
+ {
+ return char.ToLowerInvariant(c);
}
- private static string GetPathWithDirectorySeparator(string path, DirectorySeparatorKind separatorKind)
+ private static char ToUpper(char c)
{
- switch (separatorKind)
- {
- case DirectorySeparatorKind.System:
- return PathExtensions.IsUnixLikePlatform ? GetPathWithForwardSlashes(path) : GetPathWithBackSlashes(path);
- case DirectorySeparatorKind.Windows:
- return GetPathWithBackSlashes(path);
- case DirectorySeparatorKind.Linux:
- return GetPathWithForwardSlashes(path);
- default:
- throw new ArgumentOutOfRangeException(nameof(separatorKind));
- }
+ return char.ToUpperInvariant(c);
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static string GetPathWithBackSlashes(string path)
+ private static void GetPathWithDirectorySeparator(Span pathSpan, DirectorySeparatorKind separatorKind)
{
- return path.Replace('/', '\\');
+ var separatorChar = GetSeparatorChar(separatorKind);
+
+ for (var i = 0; i < pathSpan.Length; i++)
+ {
+ var c = pathSpan[i];
+ if (c is '\\' or '/')
+ pathSpan[i] = separatorChar;
+ }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static string GetPathWithForwardSlashes(string path)
+ private static char GetSeparatorChar(DirectorySeparatorKind separatorKind)
{
- return path.Replace('\\', '/');
+ return separatorKind switch
+ {
+ DirectorySeparatorKind.System => PathExtensions.IsUnixLikePlatform ? '/' : '\\',
+ DirectorySeparatorKind.Windows => '\\',
+ DirectorySeparatorKind.Linux => '/',
+ _ => throw new ArgumentOutOfRangeException(nameof(separatorKind))
+ };
}
private static bool IsForce(this UnifyCasingKind casing)
diff --git a/src/CommonUtilities.FileSystem/src/Properties/AssemblyInfo.cs b/src/CommonUtilities.FileSystem/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..5c2a33d
--- /dev/null
+++ b/src/CommonUtilities.FileSystem/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("AnakinRaW.Commonutilities.FileSystem.Test")]
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs b/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs
new file mode 100644
index 0000000..1e4e1ce
--- /dev/null
+++ b/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs
@@ -0,0 +1,305 @@
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace AnakinRaW.CommonUtilities.FileSystem.Utilities;
+
+// Copied from https://github.com/dotnet/runtime
+internal ref struct ValueStringBuilder
+{
+ private char[]? _arrayToReturnToPool;
+ private Span _chars;
+ private int _pos;
+
+ public ValueStringBuilder(Span initialBuffer)
+ {
+ _arrayToReturnToPool = null;
+ _chars = initialBuffer;
+ _pos = 0;
+ }
+
+ public ValueStringBuilder(int initialCapacity)
+ {
+ _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity);
+ _chars = _arrayToReturnToPool;
+ _pos = 0;
+ }
+
+ public int Length
+ {
+ get => _pos;
+ set
+ {
+ Debug.Assert(value >= 0);
+ Debug.Assert(value <= _chars.Length);
+ _pos = value;
+ }
+ }
+
+ public int Capacity => _chars.Length;
+
+ public void EnsureCapacity(int capacity)
+ {
+ // This is not expected to be called this with negative capacity
+ Debug.Assert(capacity >= 0);
+
+ // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception.
+ if ((uint)capacity > (uint)_chars.Length)
+ Grow(capacity - _pos);
+ }
+
+ ///
+ /// Get a pinnable reference to the builder.
+ /// Does not ensure there is a null char after
+ /// This overload is pattern matched in the C# 7.3+ compiler so you can omit
+ /// the explicit method call, and write eg "fixed (char* c = builder)"
+ ///
+ public ref char GetPinnableReference()
+ {
+ return ref MemoryMarshal.GetReference(_chars);
+ }
+
+ ///
+ /// Get a pinnable reference to the builder.
+ ///
+ /// Ensures that the builder has a null char after
+ public ref char GetPinnableReference(bool terminate)
+ {
+ if (terminate)
+ {
+ EnsureCapacity(Length + 1);
+ _chars[Length] = '\0';
+ }
+ return ref MemoryMarshal.GetReference(_chars);
+ }
+
+ public ref char this[int index]
+ {
+ get
+ {
+ Debug.Assert(index < _pos);
+ return ref _chars[index];
+ }
+ }
+
+ public override string ToString()
+ {
+ var s = _chars.Slice(0, _pos).ToString();
+ Dispose();
+ return s;
+ }
+
+ /// Returns the underlying storage of the builder.
+ public Span RawChars => _chars;
+
+ ///
+ /// Returns a span around the contents of the builder.
+ ///
+ /// Ensures that the builder has a null char after
+ public ReadOnlySpan AsSpan(bool terminate)
+ {
+ if (terminate)
+ {
+ EnsureCapacity(Length + 1);
+ _chars[Length] = '\0';
+ }
+ return _chars.Slice(0, _pos);
+ }
+
+ public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos);
+ public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start);
+ public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length);
+
+ public bool TryCopyTo(Span destination, out int charsWritten)
+ {
+ if (_chars.Slice(0, _pos).TryCopyTo(destination))
+ {
+ charsWritten = _pos;
+ Dispose();
+ return true;
+ }
+ else
+ {
+ charsWritten = 0;
+ Dispose();
+ return false;
+ }
+ }
+
+ public void Insert(int index, char value, int count)
+ {
+ if (_pos > _chars.Length - count)
+ {
+ Grow(count);
+ }
+
+ var remaining = _pos - index;
+ _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
+ _chars.Slice(index, count).Fill(value);
+ _pos += count;
+ }
+
+ public void Insert(int index, string? s)
+ {
+ if (s == null)
+ return;
+
+ var count = s.Length;
+
+ if (_pos > _chars.Length - count)
+ Grow(count);
+
+ var remaining = _pos - index;
+ _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
+ s
+#if !NET
+ .AsSpan()
+#endif
+ .CopyTo(_chars.Slice(index));
+ _pos += count;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Append(char c)
+ {
+ var pos = _pos;
+ var chars = _chars;
+ if ((uint)pos < (uint)chars.Length)
+ {
+ chars[pos] = c;
+ _pos = pos + 1;
+ }
+ else
+ GrowAndAppend(c);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Append(string? s)
+ {
+ if (s == null)
+ return;
+
+ var pos = _pos;
+ if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
+ {
+ _chars[pos] = s[0];
+ _pos = pos + 1;
+ }
+ else
+ AppendSlow(s);
+ }
+
+ private void AppendSlow(string s)
+ {
+ var pos = _pos;
+ if (pos > _chars.Length - s.Length)
+ Grow(s.Length);
+
+ s
+#if !NET
+ .AsSpan()
+#endif
+ .CopyTo(_chars.Slice(pos));
+ _pos += s.Length;
+ }
+
+ public void Append(char c, int count)
+ {
+ if (_pos > _chars.Length - count)
+ Grow(count);
+
+ var dst = _chars.Slice(_pos, count);
+ for (var i = 0; i < dst.Length; i++)
+ dst[i] = c;
+ _pos += count;
+ }
+
+ public unsafe void Append(char* value, int length)
+ {
+ var pos = _pos;
+ if (pos > _chars.Length - length)
+ Grow(length);
+
+ var dst = _chars.Slice(_pos, length);
+ for (var i = 0; i < dst.Length; i++)
+ dst[i] = *value++;
+ _pos += length;
+ }
+
+ public void Append(scoped ReadOnlySpan value)
+ {
+ var pos = _pos;
+ if (pos > _chars.Length - value.Length)
+ Grow(value.Length);
+
+ value.CopyTo(_chars.Slice(_pos));
+ _pos += value.Length;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span AppendSpan(int length)
+ {
+ var origPos = _pos;
+ if (origPos > _chars.Length - length)
+ Grow(length);
+
+ _pos = origPos + length;
+ return _chars.Slice(origPos, length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void GrowAndAppend(char c)
+ {
+ Grow(1);
+ Append(c);
+ }
+
+ ///
+ /// Resize the internal buffer either by doubling current buffer size or
+ /// by adding to
+ /// whichever is greater.
+ ///
+ ///
+ /// Number of chars requested beyond current position.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void Grow(int additionalCapacityBeyondPos)
+ {
+ Debug.Assert(additionalCapacityBeyondPos > 0);
+ Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed.");
+
+ const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
+
+ // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try
+ // to double the size if possible, bounding the doubling to not go beyond the max array length.
+ var newCapacity = (int)Math.Max(
+ (uint)(_pos + additionalCapacityBeyondPos),
+ Math.Min((uint)_chars.Length * 2, ArrayMaxLength));
+
+ // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
+ // This could also go negative if the actual required length wraps around.
+ var poolArray = ArrayPool.Shared.Rent(newCapacity);
+
+ _chars.Slice(0, _pos).CopyTo(poolArray);
+
+ var toReturn = _arrayToReturnToPool;
+ _chars = _arrayToReturnToPool = poolArray;
+ if (toReturn != null)
+ {
+ ArrayPool.Shared.Return(toReturn);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Dispose()
+ {
+ var toReturn = _arrayToReturnToPool;
+ this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
+ if (toReturn != null)
+ {
+ ArrayPool.Shared.Return(toReturn);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs b/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs
index 4997034..89307d9 100644
--- a/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs
+++ b/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs
@@ -27,8 +27,13 @@ public class WindowsFileNameValidator : FileNameValidator
//(char)31,
];
- ///
- public override FileNameValidationResult IsValidFileName(ReadOnlySpan fileName)
+ ///
+ /// Checks whether a string represent a valid file name
+ ///
+ /// The string to validate.
+ /// Determines whether the check shall include Windows reserved file names (e.g, AUX, LPT1, etc.).
+ ///
+ public FileNameValidationResult IsValidFileName(ReadOnlySpan fileName, bool checkWindowsReservedNames)
{
if (fileName.Length == 0)
return FileNameValidationResult.NullOrEmpty;
@@ -41,17 +46,27 @@ public override FileNameValidationResult IsValidFileName(ReadOnlySpan file
if (ContainsInvalidChars(fileName))
return FileNameValidationResult.InvalidCharacter;
+ if (checkWindowsReservedNames)
+ {
#if NET7_0_OR_GREATER
- if (RegexInvalidName.IsMatch(fileName))
- return FileNameValidationResult.SystemReserved;
+ if (RegexInvalidName.IsMatch(fileName))
+ return FileNameValidationResult.SystemReserved;
#else
- if (RegexInvalidName.IsMatch(fileName.ToString()))
- return FileNameValidationResult.SystemReserved;
+ if (RegexInvalidName.IsMatch(fileName.ToString()))
+ return FileNameValidationResult.SystemReserved;
#endif
+ }
+
return FileNameValidationResult.Valid;
}
+ ///
+ public override FileNameValidationResult IsValidFileName(ReadOnlySpan fileName)
+ {
+ return IsValidFileName(fileName, true);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool ContainsInvalidChars(ReadOnlySpan value)
{
diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj
index cce7be3..55a109a 100644
--- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj
+++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj
@@ -11,10 +11,11 @@
AnakinRaW.CommonUtilities.FileSystem.Test
AnakinRaW.CommonUtilities.FileSystem.Test
+ true
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -22,15 +23,15 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/src/CommonUtilities.FileSystem/test/PathAssert.cs b/src/CommonUtilities.FileSystem/test/PathAssert.cs
new file mode 100644
index 0000000..b86642c
--- /dev/null
+++ b/src/CommonUtilities.FileSystem/test/PathAssert.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace AnakinRaW.CommonUtilities.FileSystem.Test;
+
+internal static class PathAssert
+{
+ public static void Equal(ReadOnlySpan expected, ReadOnlySpan actual)
+ {
+ if (!actual.SequenceEqual(expected))
+ throw Xunit.Sdk.EqualException.ForMismatchedValues(expected.ToString(), actual.ToString());
+ }
+
+ public static void Empty(ReadOnlySpan actual)
+ {
+ if (actual.Length > 0)
+ throw Xunit.Sdk.NotEmptyException.ForNonEmptyCollection();
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs
new file mode 100644
index 0000000..c5cf540
--- /dev/null
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using AnakinRaW.CommonUtilities.Testing;
+using Xunit;
+
+namespace AnakinRaW.CommonUtilities.FileSystem.Test;
+
+public class GetFileNameTest
+{
+ private readonly IFileSystem _fileSystem = new System.IO.Abstractions.FileSystem();
+
+ public static TheoryData TestData_GetFileName => new()
+ {
+ { ".", "." },
+ { "..", ".." },
+ { "file", "file" },
+ { "file.", "file." },
+ { "file.exe", "file.exe" },
+ { " . ", " . " },
+ { " .. ", " .. " },
+ { "fi le", "fi le" },
+ { Path.Combine("baz", "file.exe"), "file.exe" },
+ { Path.Combine("baz", "file.exe") + Path.AltDirectorySeparatorChar, "" },
+ { Path.Combine("bar", "baz", "file.exe"), "file.exe" },
+ { Path.Combine("bar", "baz", "file.exe") + Path.DirectorySeparatorChar, "" }
+ };
+
+
+ [Theory, MemberData(nameof(TestData_GetFileName))]
+ public void GetFileName_Span(string path, string expected)
+ {
+ PathAssert.Equal(expected.AsSpan(), _fileSystem.Path.GetFileName(path.AsSpan()));
+ Assert.Equal(expected, _fileSystem.Path.GetFileName(path));
+ }
+
+ public static IEnumerable
public abstract class Pipeline : DisposableObject, IPipeline
{
+ private CancellationTokenSource? _linkedCancellationTokenSource;
+
///
/// The service provider of the .
///
@@ -33,6 +35,11 @@ public abstract class Pipeline : DisposableObject, IPipeline
///
public bool PipelineFailed { get; protected set; }
+ ///
+ /// Gets a value indicating the pipeline shall abort execution on the first received error.
+ ///
+ protected virtual bool FailFast => false;
+
///
///
///
@@ -62,7 +69,20 @@ public virtual async Task RunAsync(CancellationToken token = default)
try
{
- await RunCoreAsync(token).ConfigureAwait(false);
+
+ try
+ {
+ _linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
+ await RunCoreAsync(_linkedCancellationTokenSource.Token).ConfigureAwait(false);
+ }
+ finally
+ {
+ if (_linkedCancellationTokenSource is not null)
+ {
+ _linkedCancellationTokenSource.Dispose();
+ _linkedCancellationTokenSource = null;
+ }
+ }
}
catch (Exception)
{
@@ -71,6 +91,14 @@ public virtual async Task RunAsync(CancellationToken token = default)
}
}
+ ///
+ /// Cancels the pipeline
+ ///
+ public void Cancel()
+ {
+ _linkedCancellationTokenSource?.Cancel();
+ }
+
///
public override string ToString()
{
@@ -104,4 +132,17 @@ protected void ThrowIfAnyStepsFailed(IEnumerable steps)
if (failedBuildSteps.Any())
throw new StepFailureException(failedBuildSteps);
}
+
+ ///
+ /// The default event handler that can be used when an error occurs within a step.
+ /// is set to . When is , the pipeline gets cancelled.
+ ///
+ /// The sender of the event.
+ /// The event arguments.
+ protected virtual void OnError(object sender, StepErrorEventArgs e)
+ {
+ PipelineFailed = true;
+ if (FailFast || e.Cancel)
+ Cancel();
+ }
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/SimplePipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/SimplePipeline.cs
index 9b1f8a5..59caf22 100644
--- a/src/CommonUtilities.SimplePipeline/src/Pipelines/SimplePipeline.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/SimplePipeline.cs
@@ -11,12 +11,12 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline;
///
/// The type of the step runner.
public abstract class SimplePipeline : Pipeline where TRunner : StepRunner
-{
- private readonly bool _failFast;
-
- private CancellationTokenSource? _linkedCancellationTokenSource;
+{
private IRunner _buildRunner = null!;
+ ///
+ protected override bool FailFast { get; }
+
///
/// Initializes a new instance of the class.
///
@@ -27,7 +27,7 @@ public abstract class SimplePipeline : Pipeline where TRunner : StepRun
///
protected SimplePipeline(IServiceProvider serviceProvider, bool failFast = true) : base(serviceProvider)
{
- _failFast = failFast;
+ FailFast = failFast;
}
///
@@ -66,19 +66,12 @@ protected override async Task RunCoreAsync(CancellationToken token)
{
try
{
- _linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
-
_buildRunner.Error += OnError;
- await _buildRunner.RunAsync(_linkedCancellationTokenSource.Token).ConfigureAwait(false);
+ await _buildRunner.RunAsync(token).ConfigureAwait(false);
}
finally
{
_buildRunner.Error -= OnError;
- if (_linkedCancellationTokenSource is not null)
- {
- _linkedCancellationTokenSource.Dispose();
- _linkedCancellationTokenSource = null;
- }
}
if (!PipelineFailed)
@@ -87,18 +80,6 @@ protected override async Task RunCoreAsync(CancellationToken token)
ThrowIfAnyStepsFailed(_buildRunner.Steps);
}
- ///
- /// Called when an error occurs within a step.
- ///
- /// The sender of the event.
- /// The event arguments.
- protected virtual void OnError(object sender, StepErrorEventArgs e)
- {
- PipelineFailed = true;
- if (_failFast || e.Cancel)
- _linkedCancellationTokenSource?.Cancel();
- }
-
///
protected override void DisposeManagedResources()
{
diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj
index a54128b..a6f2cf3 100644
--- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj
+++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj
@@ -13,18 +13,18 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/src/CommonUtilities.SimplePipeline/test/PipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs
similarity index 63%
rename from src/CommonUtilities.SimplePipeline/test/PipelineTest.cs
rename to src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs
index ae69cdd..7348528 100644
--- a/src/CommonUtilities.SimplePipeline/test/PipelineTest.cs
+++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs
@@ -5,7 +5,7 @@
using Moq.Protected;
using Xunit;
-namespace AnakinRaW.CommonUtilities.SimplePipeline.Test;
+namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines;
public class PipelineTest
{
@@ -34,12 +34,12 @@ public async Task Test_Run()
};
pipeline.Protected().Setup>("PrepareCoreAsync").Returns(Task.FromResult(true));
-
+
await pipeline.Object.RunAsync();
await pipeline.Object.RunAsync();
pipeline.Protected().Verify>("PrepareCoreAsync", Times.Exactly(1));
- pipeline.Protected().Verify("RunCoreAsync", Times.Exactly(2), false, (CancellationToken) default);
+ pipeline.Protected().Verify("RunCoreAsync", Times.Exactly(2), false, ItExpr.IsAny());
}
[Fact]
@@ -58,7 +58,7 @@ public async Task Test_Prepare_Run()
await pipeline.Object.RunAsync();
pipeline.Protected().Verify>("PrepareCoreAsync", Times.Exactly(1));
- pipeline.Protected().Verify("RunCoreAsync", Times.Exactly(2), false, (CancellationToken)default);
+ pipeline.Protected().Verify("RunCoreAsync", Times.Exactly(2), false, ItExpr.IsAny());
}
[Fact]
@@ -89,7 +89,7 @@ public async Task Test_Prepare_Disposed_ThrowsObjectDisposedException()
pipeline.Object.Dispose();
pipeline.Object.Dispose();
- await Assert.ThrowsAsync(async() => await pipeline.Object.PrepareAsync());
+ await Assert.ThrowsAsync(async () => await pipeline.Object.PrepareAsync());
}
[Fact]
@@ -106,4 +106,49 @@ public async Task Test_Run_Disposed_ThrowsObjectDisposedException()
await Assert.ThrowsAsync(async () => await pipeline.Object.RunAsync());
}
+
+ [Fact]
+ public async Task Test_Cancel()
+ {
+ var sp = new Mock().Object;
+ var pipeline = new Mock(sp)
+ {
+ CallBase = true
+ };
+
+ var callbackRunTsc = new TaskCompletionSource();
+ var cancelledTsc = new TaskCompletionSource();
+
+ CancellationToken runToken = default;
+
+ var callbackTask = Task.Run(async () =>
+ {
+ await callbackRunTsc.Task;
+ Assert.True(runToken.CanBeCanceled);
+
+ await cancelledTsc.Task;
+ Assert.True(runToken.IsCancellationRequested);
+ });
+
+ pipeline.Protected().Setup>("PrepareCoreAsync").Returns(Task.FromResult(true));
+ pipeline.Protected().Setup("RunCoreAsync", ItExpr.IsAny())
+ .Callback((CancellationToken token) =>
+ {
+ runToken = token;
+ callbackRunTsc.SetResult(0);
+ }).Returns(callbackTask);
+
+
+ var pipelineTask = pipeline.Object.RunAsync();
+
+ pipeline.Object.Cancel();
+ cancelledTsc.SetResult(0);
+
+ await pipelineTask;
+
+ Assert.True(callbackTask.IsCompleted);
+
+ pipeline.Protected().Verify>("PrepareCoreAsync", Times.Exactly(1));
+ pipeline.Protected().Verify("RunCoreAsync", Times.Exactly(1), false, ItExpr.IsAny());
+ }
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerRunnerTest.cs
index eb800cf..536d4be 100644
--- a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerRunnerTest.cs
+++ b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerRunnerTest.cs
@@ -12,7 +12,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners;
public class ParallelProducerConsumerRunnerTest
{
[Fact]
- public void Test_Run_WaitNotFinished()
+ public async void Test_Run_WaitNotFinished()
{
var sc = new ServiceCollection();
var runner = new ParallelProducerConsumerRunner(2, sc.BuildServiceProvider());
@@ -23,24 +23,25 @@ public void Test_Run_WaitNotFinished()
runner.AddStep(s1.Object);
runner.AddStep(s2.Object);
- var ran1 = false;
+ var tsc1 = new TaskCompletionSource();
+ var tsc2 = new TaskCompletionSource();
+
s1.Setup(t => t.Run(default)).Callback(() =>
{
- ran1 = true;
+ tsc1.SetResult(1);
});
- var ran2 = false;
s2.Setup(t => t.Run(default)).Callback(() =>
{
- ran2 = true;
+ tsc2.SetResult(1);
});
-
-
+
+
_ = runner.RunAsync(default);
- Assert.Throws(() => runner.Wait(TimeSpan.FromSeconds(2)));
+ await tsc1.Task;
+ await tsc1.Task;
- Assert.True(ran1);
- Assert.True(ran2);
+ Assert.Throws(() => runner.Wait(TimeSpan.FromSeconds(2)));
}
[Fact]
diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj
index c065950..1933daa 100644
--- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj
+++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj
@@ -21,9 +21,9 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/CommonUtilities/src/Hashing/HashingService.cs b/src/CommonUtilities/src/Hashing/HashingService.cs
index 720ad34..a5e680e 100644
--- a/src/CommonUtilities/src/Hashing/HashingService.cs
+++ b/src/CommonUtilities/src/Hashing/HashingService.cs
@@ -169,20 +169,25 @@ public byte[] GetHash(string stringData, Encoding encoding, HashTypeKey hashType
///
public int GetHash(string stringData, Encoding encoding, Span destination, HashTypeKey hashType)
+ {
+ return GetHash(stringData.AsSpan(), destination, encoding, hashType);
+ }
+
+ ///
+ public int GetHash(ReadOnlySpan stringData, Span destination, Encoding encoding, HashTypeKey hashType)
{
if (stringData == null)
throw new ArgumentNullException(nameof(stringData));
if (encoding == null)
throw new ArgumentNullException(nameof(encoding));
- var stringSpan = stringData.AsSpan();
- var maxByteSize = encoding.GetMaxByteCount(stringSpan.Length);
+ var maxByteSize = encoding.GetMaxByteCount(stringData.Length);
byte[]? encodedBytes = null;
try
{
var buffer = maxByteSize > 256 ? encodedBytes = ArrayPool.Shared.Rent(maxByteSize) : stackalloc byte[maxByteSize];
- var bytesToHash = encoding.GetBytesReadOnly(stringSpan, buffer);
+ var bytesToHash = encoding.GetBytesReadOnly(stringData, buffer);
return GetHash(bytesToHash, destination, hashType);
}
diff --git a/src/CommonUtilities/src/Hashing/IHashingService.cs b/src/CommonUtilities/src/Hashing/IHashingService.cs
index f94f462..81cacac 100644
--- a/src/CommonUtilities/src/Hashing/IHashingService.cs
+++ b/src/CommonUtilities/src/Hashing/IHashingService.cs
@@ -94,6 +94,19 @@ public interface IHashingService
/// The buffer in is too small to hold the calculated hash size.
int GetHash(string stringData, Encoding encoding, Span destination, HashTypeKey hashType);
+ ///
+ /// Computes the hash of a character span using the specified algorithm.
+ ///
+ /// The character span to hash.
+ /// The buffer to receive the hash value.
+ /// The encoding to interpret the string
+ /// The hash type data.
+ /// The total number of bytes written to .
+ /// The buffer in destination is too small to hold the calculated hash size.
+ /// is .
+ /// The buffer in is too small to hold the calculated hash size.
+ int GetHash(ReadOnlySpan stringData, Span destination, Encoding encoding, HashTypeKey hashType);
+
///
/// Asynchronously computes the hash of a file using the specified algorithm.
///
diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj
index e66bc5f..1e07088 100644
--- a/src/CommonUtilities/test/CommonUtilities.Test.csproj
+++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj
@@ -14,18 +14,18 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
-
-
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/src/CommonUtilities/test/Hashing/HashingServiceTest.cs b/src/CommonUtilities/test/Hashing/HashingServiceTest.cs
index 4dccb91..6ed58ea 100644
--- a/src/CommonUtilities/test/Hashing/HashingServiceTest.cs
+++ b/src/CommonUtilities/test/Hashing/HashingServiceTest.cs
@@ -66,6 +66,7 @@ public void Test_GetHash_ProviderNotFound_ThrowsHashProviderNotFoundException()
Assert.Throws(() => _hashingService.GetHash(someStream, notExistingProvider));
Assert.Throws(() => _hashingService.GetHash("", Encoding.ASCII, notExistingProvider));
Assert.Throws(() => _hashingService.GetHash("", Encoding.ASCII, new Span(someDestination), notExistingProvider));
+ Assert.Throws(() => _hashingService.GetHash("".AsSpan(), new Span(someDestination), Encoding.ASCII, notExistingProvider));
Assert.Throws(() => _hashingService.GetHash(someSource, notExistingProvider));
}
@@ -100,6 +101,7 @@ public void Test_GetHash_AlwaysOneProvider_DestinationTooShort_ThrowsIndexOutOfR
Assert.Throws(() => _hashingService.GetHash(new ReadOnlySpan(someSource), someDestination.AsSpan(), provider));
Assert.Throws(() => _hashingService.GetHash(someStream, someDestination.AsSpan(), provider));
Assert.Throws(() => _hashingService.GetHash("", Encoding.ASCII, new Span(someDestination), provider));
+ Assert.Throws(() => _hashingService.GetHash("".AsSpan(), new Span(someDestination), Encoding.ASCII, provider));
}
[Fact]
@@ -133,6 +135,7 @@ public void Test_GetHash_WrongOutputSizeProvider_HashWrongSize_ThrowsInvalidOper
Assert.Throws(() => _hashingService.GetHash(someStream, provider));
Assert.Throws(() => _hashingService.GetHash("", Encoding.ASCII, provider));
Assert.Throws(() => _hashingService.GetHash("", Encoding.ASCII, new Span(someDestination), provider));
+ Assert.Throws(() => _hashingService.GetHash("".AsSpan(), new Span(someDestination), Encoding.ASCII, provider));
Assert.Throws(() => _hashingService.GetHash(someSource, provider));
}
@@ -185,6 +188,9 @@ public void Test_GetHash_AlwaysOneProvider()
Assert.Equal(1, _hashingService.GetHash("", Encoding.ASCII, new Span(destination), provider));
Assert.Equal(expectedHashJoint, destination);
+ Assert.Equal(1, _hashingService.GetHash("".AsSpan(), new Span(destination), Encoding.ASCII, provider));
+ Assert.Equal(expectedHashJoint, destination);
+
Assert.Equal(expectedHashExact, _hashingService.GetHash(someSource, provider));
}
@@ -258,6 +264,9 @@ public void Test_GetHash_DefaultProviders(HashTypeKey hashType, string input, st
Assert.Equal(expectedSize, _hashingService.GetHash(input, Encoding.ASCII, new Span(destination), hashType));
Assert.Equal(expectedHash, destination);
+ Assert.Equal(expectedSize, _hashingService.GetHash(input.AsSpan(), new Span(destination), Encoding.ASCII, hashType));
+ Assert.Equal(expectedHash, destination);
+
Assert.Equal(expectedHash, _hashingService.GetHash(someSource, hashType));
}