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;