diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 771e5ef..9fb13a8 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -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
@@ -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
@@ -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
diff --git a/src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs b/src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs
index c521e0f..4b5f608 100644
--- a/src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs
+++ b/src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs
@@ -1,20 +1,18 @@
-using System.Diagnostics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
namespace Ramstack.FileSystem.Utilities;
///
/// Tokenizes a file path into its constituent components.
///
-/// The file path to tokenize.
+/// The path of the file to tokenize.
internal readonly struct PathTokenizer(string path)
{
///
/// Returns an enumerator that iterates through the collection.
///
///
- /// An enumerator that can be used to iterate through the collection.
+ /// An enumerator to iterate through the collection.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() =>
@@ -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;
///
/// Initializes a new instance of the structure.
///
/// The file path to tokenize.
- public Enumerator(string path) =>
+ internal Enumerator(string path) =>
(_path, _count) = (path, -1);
///
/// Gets the current path component.
///
- public ReadOnlySpan 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 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];
///
/// Advances the enumerator to the next path component.
@@ -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;
}
diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualPath.cs b/src/Ramstack.FileSystem.Abstractions/VirtualPath.cs
index 7b4c001..6ecb3be 100644
--- a/src/Ramstack.FileSystem.Abstractions/VirtualPath.cs
+++ b/src/Ramstack.FileSystem.Abstractions/VirtualPath.cs
@@ -1,7 +1,6 @@
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using Ramstack.FileSystem.Utilities;
@@ -345,8 +344,17 @@ public static bool HasTrailingSlash(string path) =>
/// otherwise, .
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool HasLeadingSlash(ReadOnlySpan path) =>
- path.Length != 0 && (path[0] == '/' || path[0] == '\\');
+ public static bool HasLeadingSlash(ReadOnlySpan path)
+ {
+ if (path.Length != 0)
+ {
+ var ch = path[0];
+ if (ch == '/' || ch == '\\')
+ return true;
+ }
+
+ return false;
+ }
///
/// Determines whether the specified path string ends in a directory separator.
@@ -361,8 +369,9 @@ public static bool HasTrailingSlash(ReadOnlySpan 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;