Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Ramstack.Globbing
[![NuGet](https://img.shields.io/nuget/v/Ramstack.Globbing.svg)](https://nuget.org/packages/Ramstack.Globbing)
[![MIT](https://img.shields.io/github/license/rameel/ramstack.globbing)](https://github.com/rameel/ramstack.globbing/blob/main/LICENSE)

Fast and zero-allocation .NET globbing library for matching file paths using [glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)).
No external dependencies.
Expand Down Expand Up @@ -215,9 +217,9 @@ await foreach (string filePath in enumeration)

## Supported versions

| | Version |
|------|---------|
| .NET | 6, 7, 8 |
| | Version |
|------|------------|
| .NET | 6, 7, 8, 9 |

## Contributions

Expand Down
2 changes: 1 addition & 1 deletion Ramstack.Globbing.Tests/SimdConfigurationTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Runtime.Intrinsics.X86;
using System.Runtime.Intrinsics.X86;

namespace Ramstack.Globbing;

Expand Down
1 change: 1 addition & 0 deletions Ramstack.Globbing.Tests/Traversal/PathHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public partial class PathHelperTests
[TestCase("directory_1/directory_2", 2)]
[TestCase("directory_1/directory_2/", 2)]
[TestCase("///directory_1/directory_2////", 2)]
[TestCase("/1/2/3/4/5/6/project/src/tests", 9)]
public void CountPathSegments(string path, int expected)
{
Assert.That(
Expand Down
99 changes: 51 additions & 48 deletions Ramstack.Globbing/Internal/PathHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,18 @@ public static bool IsPartialMatch(ReadOnlySpan<char> path, string[] patterns, Ma
public static int CountPathSegments(scoped ReadOnlySpan<char> path, MatchFlags flags)
{
var count = 0;
var iterator = new PathSegmentIterator();
ref var s = ref Unsafe.AsRef(in MemoryMarshal.GetReference(path));
var iterator = new PathSegmentIterator(path.Length);
var length = path.Length;

while (true)
{
var r = iterator.GetNext(ref s, flags);
var r = iterator.GetNext(ref s, length, flags);

if (r.start != r.final)
count++;

if (r.final == path.Length)
if (r.final == length)
break;
}

Expand All @@ -101,17 +103,18 @@ public static ReadOnlySpan<char> GetPartialPattern(string pattern, MatchFlags fl
if (depth < 1)
depth = 1;

var iterator = new PathSegmentIterator();
ref var s = ref Unsafe.AsRef(in pattern.GetPinnableReference());
var iterator = new PathSegmentIterator(pattern.Length);
var length = pattern.Length;

while (true)
{
var r = iterator.GetNext(ref s, flags);
var r = iterator.GetNext(ref s, length, flags);
if (r.start != r.final)
depth--;

if (depth < 1
|| r.final == pattern.Length
|| r.final == length
|| IsGlobStar(ref s, r.start, r.final))
return MemoryMarshal.CreateReadOnlySpan(ref s, r.final);
}
Expand Down Expand Up @@ -197,24 +200,11 @@ static void ConvertPathToPosixStyleImpl(ref char p, nint length)
/// </returns>
private static Vector256<ushort> CreateAllowEscaping256Bitmask(MatchFlags flags)
{
// Here is a small trick to avoid branching.
// To reduce the number of required instructions, we convert the value `Windows`,
// which equals 2, into a bitmask that allows escaping characters.
// Windows (2) (No character escaping):
// 0000 0010 >> 1 = 0000 0001
// 0000 0001 & 0000 0001 = 0000 0001
// 0000 0001 - 1 = 0000 0000
// Any other value will simply convert to 0.
// Unix (4) (Allow escaping characters)
// 0000 0100 >> 1 = 0000 0010
// 0000 0010 & 0000 0001 = 0000 0000
// 0000 0000 - 1 = 1111 1111
// Next, during the check, we can simply use the Avx2.AndNot instruction instead of Avx2.And:
// Avx2.AndNot(
// allowEscaping,
// Avx2.CompareEqual(chunk, backslash)))
Debug.Assert(MatchFlags.Windows == (MatchFlags)2);
return Vector256.Create(((uint)flags >> 1 & 1) - 1).AsUInt16();
var mask = Vector256<ushort>.Zero;
if (flags != MatchFlags.Windows)
mask = Vector256<ushort>.AllBitsSet;

return mask;
}

/// <summary>
Expand All @@ -226,8 +216,11 @@ private static Vector256<ushort> CreateAllowEscaping256Bitmask(MatchFlags flags)
/// </returns>
private static Vector128<ushort> CreateAllowEscaping128Bitmask(MatchFlags flags)
{
Debug.Assert(MatchFlags.Windows == (MatchFlags)2);
return Vector128.Create(((uint)flags >> 1 & 1) - 1).AsUInt16();
var mask = Vector128<ushort>.Zero;
if (flags != MatchFlags.Windows)
mask = Vector128<ushort>.AllBitsSet;

return mask;
}

/// <summary>
Expand Down Expand Up @@ -278,23 +271,22 @@ ref Unsafe.As<char, byte>(ref Unsafe.Add(ref destination, offset)),
/// </summary>
private struct PathSegmentIterator
{
private nint _last;
private int _last;
private nint _position;
private uint _mask;
private readonly nint _length;

/// <summary>
/// Initializes a new instance of the <see cref="PathSegmentIterator"/> structure.
/// </summary>
/// <param name="length">The path length.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PathSegmentIterator(int length) =>
(_last, _length) = (-1, (nint)(uint)length);
public PathSegmentIterator() =>
_last = -1;

/// <summary>
/// Retrieves the next segment of the path.
/// </summary>
/// <param name="source">A reference to the starting character of the path.</param>
/// <param name="length">The total number of characters in the input path starting from <paramref name="source"/>.</param>
/// <param name="flags">The flags indicating the type of path separators to match.</param>
/// <returns>
/// A tuple containing the start and end indices of the next path segment.
Expand All @@ -303,39 +295,49 @@ public PathSegmentIterator(int length) =>
/// The end of the iteration is indicated by <c>final</c> being equal to the length of the path.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public (int start, int final) GetNext(ref char source, MatchFlags flags)
public (int start, int final) GetNext(ref char source, int length, MatchFlags flags)
{
//
// Number of bits per char (ushort) in the MoveMask output
//
const uint BitsPerChar = 0b11;

var start = _last + 1;

while (_position < _length)
while ((int)_position < length)
{
if ((Avx2.IsSupported || Sse2.IsSupported) && _mask != 0)
{
var offset = BitOperations.TrailingZeroCount(_mask);
_last = _position + (nint)((uint)offset >> 1);
_last = (int)(_position + (nint)((uint)offset >> 1));

//
// Clear the bits for the current separator to process the next position in the mask
//
_mask &= ~(BitsPerChar << offset);
_mask &= ~(0b_11u << offset);

//
// Advance position to the next chunk when no separators remain in the mask
//
if (_mask == 0)
_position += Avx2.IsSupported
{
//
// https://github.com/dotnet/runtime/issues/117416
//
// Precompute the stride size instead of calculating it inline
// to avoid stack spilling. For some unknown reason, the JIT
// fails to optimize properly when this is written inline, like so:
// _position += Avx2.IsSupported
// ? Vector256<ushort>.Count
// : Vector128<ushort>.Count;
//

var stride = Avx2.IsSupported
? Vector256<ushort>.Count
: Vector128<ushort>.Count;

return ((int)start, (int)_last);
_position += stride;
}

return (start, _last);
}

if (Avx2.IsSupported && _position + Vector256<ushort>.Count <= _length)
if (Avx2.IsSupported && (int)_position + Vector256<ushort>.Count <= length)
{
var chunk = LoadVector256(ref source, _position);
var allowEscapingMask = CreateAllowEscaping256Bitmask(flags);
Expand All @@ -362,7 +364,7 @@ public PathSegmentIterator(int length) =>
if (_mask == 0)
_position += Vector256<ushort>.Count;
}
else if (Sse2.IsSupported && !Avx2.IsSupported && _position + Vector128<ushort>.Count <= _length)
else if (Sse2.IsSupported && !Avx2.IsSupported && (int)_position + Vector128<ushort>.Count <= length)
{
var chunk = LoadVector128(ref source, _position);
var allowEscapingMask = CreateAllowEscaping128Bitmask(flags);
Expand Down Expand Up @@ -391,20 +393,21 @@ public PathSegmentIterator(int length) =>
}
else
{
for (; _position < _length; _position++)
for (; (int)_position < length; _position++)
{
var ch = Unsafe.Add(ref source, _position);
if (ch == '/' || (ch == '\\' && flags == MatchFlags.Windows))
{
_last = _position;
_last = (int)_position;
_position++;
return ((int)start, (int)_last);

return (start, _last);
}
}
}
}

return ((int)start, (int)_length);
return (start, length);
}
}

Expand Down
7 changes: 4 additions & 3 deletions Ramstack.Globbing/Traversal/FileTreeAsyncEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ IAsyncEnumerator<TResult> IAsyncEnumerable<TResult>.GetAsyncEnumerator(Cancellat

private async IAsyncEnumerable<TResult> EnumerateAsync(CancellationTokenSource? source, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var chars = ArrayPool<char>.Shared.Rent(512);

try
{
var chars = ArrayPool<char>.Shared.Rent(FileTreeEnumerable<TEntry, TResult>.DefaultBufferCapacity);

var queue = new Queue<(TEntry Directory, string Path)>();
queue.Enqueue((_directory, ""));

Expand All @@ -110,10 +110,11 @@ private async IAsyncEnumerable<TResult> EnumerateAsync(CancellationTokenSource?
yield return ResultSelector(entry);
}
}

ArrayPool<char>.Shared.Return(chars);
}
finally
{
ArrayPool<char>.Shared.Return(chars);
source?.Dispose();
}
}
Expand Down
7 changes: 6 additions & 1 deletion Ramstack.Globbing/Traversal/FileTreeEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public sealed class FileTreeEnumerable<TEntry, TResult> : IEnumerable<TResult>
{
private readonly TEntry _directory;

/// <summary>
/// The default capacity of the character buffer for paths rented from the shared array pool.
/// </summary>
internal const int DefaultBufferCapacity = 512;

/// <summary>
/// Gets or sets the glob patterns to include in the enumeration.
/// </summary>
Expand Down Expand Up @@ -72,7 +77,7 @@ IEnumerator IEnumerable.GetEnumerator() =>

private IEnumerable<TResult> Enumerate()
{
var chars = ArrayPool<char>.Shared.Rent(512);
var chars = ArrayPool<char>.Shared.Rent(DefaultBufferCapacity);

var queue = new Queue<(TEntry Directory, string Path)>();
queue.Enqueue((_directory, ""));
Expand Down