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
18 changes: 15 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: |
6.x
7.x
8.x
9.x
- name: Checkout
uses: actions/checkout@v4
- name: Build
Expand All @@ -42,7 +46,11 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: |
6.x
7.x
8.x
9.x
- name: Checkout
uses: actions/checkout@v4
- name: Build
Expand All @@ -65,7 +73,11 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: |
6.x
7.x
8.x
9.x
- name: Checkout
uses: actions/checkout@v4
- name: Build
Expand Down
45 changes: 20 additions & 25 deletions src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Ramstack.FileSystem.Utilities;

/// <summary>
/// Tokenizes a file path into its constituent components.
/// </summary>
/// <param name="path">The file path to tokenize.</param>
/// <param name="path">The path of the file to tokenize.</param>
internal readonly struct PathTokenizer(string path)
{
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// An enumerator to iterate through the collection.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() =>
Expand All @@ -38,30 +36,31 @@ public static PathTokenizer Tokenize(string path) =>
public struct Enumerator
{
private readonly string _path;
private nint _start;
private nint _count;
private int _start;
private int _count;

/// <summary>
/// Initializes a new instance of the <see cref="Enumerator"/> structure.
/// </summary>
/// <param name="path">The file path to tokenize.</param>
public Enumerator(string path) =>
internal Enumerator(string path) =>
(_path, _count) = (path, -1);

/// <summary>
/// Gets the current path component.
/// </summary>
public ReadOnlySpan<char> Current
{
get
{
Debug.Assert(_path.AsSpan((int)_start, (int)_count).Length >= 0);

return MemoryMarshal.CreateReadOnlySpan(
ref Unsafe.Add(ref Unsafe.AsRef(in _path.GetPinnableReference()), _start),
(int)_count);
}
}
public ReadOnlySpan<char> Current =>
//
// Using AsSpan(_start) followed by slicing is more efficient
// than AsSpan(_start, _count) because:
// 1) MoveNext already validated _start bounds
// 2) We only need to check _count <= length (simpler than checking start+count)
//
// The alternative AsSpan(start, count) does a combined bounds check
// which the JIT can't optimize away:
// (ulong)(uint)_start + (ulong)(uint)_count <= (ulong)(uint)Length
//
_path.AsSpan(_start)[.._count];

/// <summary>
/// Advances the enumerator to the next path component.
Expand All @@ -75,17 +74,13 @@ public bool MoveNext()
{
_start = _start + _count + 1;

if ((int)_start < _path.Length)
if ((uint)_start < (uint)_path.Length)
{
Debug.Assert(_path.AsSpan((int)_start, _path.Length - (int)_start).Length >= 0);

var s = MemoryMarshal.CreateReadOnlySpan(
ref Unsafe.Add(ref Unsafe.AsRef(in _path.GetPinnableReference()), _start),
_path.Length - (int)_start);
var s = _path.AsSpan(_start);

_count = s.IndexOfAny('/', '\\');
if (_count < 0)
_count = (nint)(uint)s.Length;
_count = s.Length;

return true;
}
Expand Down
19 changes: 14 additions & 5 deletions src/Ramstack.FileSystem.Abstractions/VirtualPath.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

using Ramstack.FileSystem.Utilities;

Expand Down Expand Up @@ -345,8 +344,17 @@ public static bool HasTrailingSlash(string path) =>
/// otherwise, <see langword="false" />.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasLeadingSlash(ReadOnlySpan<char> path) =>
path.Length != 0 && (path[0] == '/' || path[0] == '\\');
public static bool HasLeadingSlash(ReadOnlySpan<char> path)
{
if (path.Length != 0)
{
var ch = path[0];
if (ch == '/' || ch == '\\')
return true;
}

return false;
}

/// <summary>
/// Determines whether the specified path string ends in a directory separator.
Expand All @@ -361,8 +369,9 @@ public static bool HasTrailingSlash(ReadOnlySpan<char> path)
{
if (path.Length != 0)
{
var ch = Unsafe.Add(ref MemoryMarshal.GetReference(path), (nint)(uint)path.Length - 1);
return ch == '/' || ch == '\\';
var ch = path[^1];
if (ch == '/' || ch == '\\')
return true;
}

return false;
Expand Down