Skip to content

Commit 10b3900

Browse files
authored
Merge pull request #17 from rameel/refactoring
Vectorized certain path manipulation methods
2 parents d0b2a15 + e6b4fa2 commit 10b3900

File tree

5 files changed

+512
-132
lines changed

5 files changed

+512
-132
lines changed

Ramstack.Globbing.Tests/Utilities/PathHelperTests.Converting.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ namespace Ramstack.Globbing.Utilities;
33
partial class PathHelperTests
44
{
55
[Test]
6-
public void ConvertToForwardSlashes_NothingChange()
6+
public void ConvertPathToPosixStyle_NothingChange()
77
{
88
for (var n = 0; n < 512; n++)
99
{
@@ -12,14 +12,14 @@ public void ConvertToForwardSlashes_NothingChange()
1212
span.Fill('a');
1313

1414
var expected = original.ToString();
15-
PathHelper.ConvertToForwardSlashes(span);
15+
PathHelper.ConvertPathToPosixStyle(span);
1616

1717
Assert.That(original.ToString(), Is.EqualTo(expected));
1818
}
1919
}
2020

2121
[Test]
22-
public void ConvertToForwardSlashes_ChangesAll()
22+
public void ConvertPathToPosixStyle_ChangesAll()
2323
{
2424
for (var n = 0; n < 512; n++)
2525
{
@@ -28,14 +28,14 @@ public void ConvertToForwardSlashes_ChangesAll()
2828
span.Fill('\\');
2929

3030
var expected = original.ToString().Replace('\\', '/').Replace('@', '\\');
31-
PathHelper.ConvertToForwardSlashes(span);
31+
PathHelper.ConvertPathToPosixStyle(span);
3232

3333
Assert.That(original.ToString().Replace('@', '\\'), Is.EqualTo(expected));
3434
}
3535
}
3636

3737
[Test]
38-
public void ConvertToForwardSlashes_ForwardSlahes()
38+
public void ConvertPathToPosixStyle_ForwardSlashes()
3939
{
4040
for (var n = 0; n < 512; n++)
4141
{
@@ -44,14 +44,14 @@ public void ConvertToForwardSlashes_ForwardSlahes()
4444
span.Fill('/');
4545

4646
var expected = original.ToString().Replace('\\', '/').Replace('@', '\\');
47-
PathHelper.ConvertToForwardSlashes(span);
47+
PathHelper.ConvertPathToPosixStyle(span);
4848

4949
Assert.That(original.ToString().Replace('@', '\\'), Is.EqualTo(expected));
5050
}
5151
}
5252

5353
[Test]
54-
public void ConvertToForwardSlashes_RareChanges()
54+
public void ConvertPathToPosixStyle_RareChanges()
5555
{
5656
for (var n = 0; n < 512; n++)
5757
{
@@ -62,7 +62,7 @@ public void ConvertToForwardSlashes_RareChanges()
6262
span[i] = '\\';
6363

6464
var expected = original.ToString().Replace('\\', '/').Replace('@', '\\');
65-
PathHelper.ConvertToForwardSlashes(span);
65+
PathHelper.ConvertPathToPosixStyle(span);
6666

6767
Assert.That(original.ToString().Replace('@', '\\'), Is.EqualTo(expected));
6868
}

Ramstack.Globbing.Tests/Utilities/PathHelperTests.cs

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@ public partial class PathHelperTests
77
[TestCase("//", 1)]
88
[TestCase("/dir1", 1)]
99
[TestCase("dir1", 1)]
10+
[TestCase("dir1/", 1)]
11+
[TestCase("/directory_1", 1)]
12+
[TestCase("directory_1", 1)]
13+
[TestCase("directory_1/", 1)]
1014
[TestCase("/dir1/dir2/", 2)]
1115
[TestCase("dir1/dir2", 2)]
1216
[TestCase("dir1/dir2/", 2)]
1317
[TestCase("///dir1/dir2////", 2)]
14-
public void CountPathSegmentsTests(string path, int expected)
18+
[TestCase("/directory_1/directory_2/", 2)]
19+
[TestCase("directory_1/directory_2", 2)]
20+
[TestCase("directory_1/directory_2/", 2)]
21+
[TestCase("///directory_1/directory_2////", 2)]
22+
public void CountPathSegments(string path, int expected)
1523
{
1624
Assert.That(
1725
PathHelper.CountPathSegments(path, MatchFlags.Windows),
@@ -82,7 +90,7 @@ public void CountPathSegmentsTests(string path, int expected)
8290
[TestCase("dir1/**/dir2/dir3", 2, "dir1/**")]
8391
[TestCase("dir1/**/dir2/dir3", 3, "dir1/**")]
8492
[TestCase("dir1/**/dir2/dir3", 4, "dir1/**")]
85-
public void GetPartialPatternTests(string path, int depth, string expected)
93+
public void GetPartialPattern(string path, int depth, string expected)
8694
{
8795
Assert.That(
8896
PathHelper.GetPartialPattern(path, MatchFlags.Windows, depth).ToString(),
@@ -99,4 +107,116 @@ public void GetPartialPatternTests(string path, int depth, string expected)
99107
PathHelper.GetPartialPattern(path, MatchFlags.Windows, depth).ToString(),
100108
Is.EqualTo(expected));
101109
}
110+
111+
[Test]
112+
public void CountPathSegments_Generated()
113+
{
114+
foreach (var (path, slash, f) in GeneratePaths())
115+
{
116+
var expected = Math.Max(1, path.Split(slash, StringSplitOptions.RemoveEmptyEntries).Length);
117+
var count = PathHelper.CountPathSegments(path, f);
118+
119+
Assert.That(count, Is.EqualTo(expected));
120+
}
121+
}
122+
123+
[Test]
124+
public void GetPartialPattern_Generated()
125+
{
126+
foreach (var (path, slash, f) in GeneratePaths().OrderBy(_ => Random.Shared.Next()).Take(500))
127+
{
128+
var count = PathHelper.CountPathSegments(path, f);
129+
for (var depth = 1; depth <= count * 2; depth++)
130+
{
131+
var result = PathHelper
132+
.GetPartialPattern(path, f, depth)
133+
.ToString()
134+
.Split(slash, StringSplitOptions.RemoveEmptyEntries);
135+
136+
var expected = path
137+
.Split(slash, StringSplitOptions.RemoveEmptyEntries)
138+
.Take(depth);
139+
140+
Assert.That(result, Is.EquivalentTo(expected));
141+
}
142+
}
143+
}
144+
145+
[Test]
146+
public void SearchPathSeparator()
147+
{
148+
for (var n = 0; n < 5000; n++)
149+
{
150+
var p0 = new string('a', n);
151+
152+
var p1 = p0 + "\\";
153+
var index1 = PathHelper.SearchPathSeparator(p1, MatchFlags.Windows);
154+
var index2 = PathHelper.SearchPathSeparator(p1, MatchFlags.Unix);
155+
156+
Assert.That(index1, Is.EqualTo(p1.IndexOf('\\')), $"length: {n}");
157+
Assert.That(index2, Is.EqualTo(-1), $"length: {n}");
158+
159+
var p2 = p0 + "/";
160+
var index3 = PathHelper.SearchPathSeparator(p2, MatchFlags.Windows);
161+
var index4 = PathHelper.SearchPathSeparator(p2, MatchFlags.Unix);
162+
163+
Assert.That(index3, Is.EqualTo(p2.IndexOf('/')), $"length: {n}");
164+
Assert.That(index4, Is.EqualTo(index3), $"length: {n}");
165+
}
166+
}
167+
168+
[Test]
169+
public void SearchPathSeparator_Nothing()
170+
{
171+
var source = new string('a', 5000);
172+
173+
for (var n = 0; n < 5000; n++)
174+
{
175+
var p = source.AsSpan(0, n);
176+
var index1 = PathHelper.SearchPathSeparator(p, MatchFlags.Windows);
177+
var index2 = PathHelper.SearchPathSeparator(p, MatchFlags.Unix);
178+
179+
Assert.That(index1, Is.EqualTo(-1));
180+
Assert.That(index2, Is.EqualTo(-1));
181+
}
182+
}
183+
184+
private static IEnumerable<(string path, char slash, MatchFlags flags)> GeneratePaths()
185+
{
186+
var flags = new[]
187+
{
188+
('/', MatchFlags.Unix),
189+
('/', MatchFlags.Windows),
190+
('\\', MatchFlags.Windows)
191+
};
192+
193+
foreach (var (s, f) in flags)
194+
{
195+
for (var r = 1; r < 5; r++)
196+
for (var n = 1; n < 37; n++)
197+
{
198+
var slash = new string(s, r);
199+
var segments = new string[n];
200+
201+
for (var l = 0; l < 43; l++)
202+
{
203+
segments.AsSpan().Fill(new string('a', l));
204+
205+
var path = string.Join(slash, segments);
206+
for (var v = 0; v < 4; v++)
207+
{
208+
path = v switch
209+
{
210+
0 => path,
211+
1 => slash + path,
212+
2 => slash + path + slash,
213+
_ => path + slash
214+
};
215+
216+
yield return (path, s, f);
217+
}
218+
}
219+
}
220+
}
221+
}
102222
}

Ramstack.Globbing/Matcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public static bool IsMatch(string path, string pattern, MatchFlags flags = Match
132132
/// <returns>
133133
/// <see langword="true" /> if the pattern matches the path; otherwise, <see langword="false" />.
134134
/// </returns>
135-
public static bool IsMatch(ReadOnlySpan<char> path, ReadOnlySpan<char> pattern, MatchFlags flags = MatchFlags.Auto)
135+
public static bool IsMatch(scoped ReadOnlySpan<char> path, scoped ReadOnlySpan<char> pattern, MatchFlags flags = MatchFlags.Auto)
136136
{
137137
return IsMatchImpl(
138138
ref MemoryMarshal.GetReference(path),
@@ -225,7 +225,7 @@ static int Length(char* s, char* e)
225225
// This occurs because:
226226
// - Each '*' character occupies exactly 2 bytes in UTF-16.
227227
// - The value of each character (0x002A) is symmetrical with respect to byte order.
228-
if (Length(p, pe) == 2 && Unsafe.Read<int>(p) == ('*' << 16 | '*'))
228+
if (p + 2 == pe && Unsafe.ReadUnaligned<int>(p) == ('*' << 16 | '*'))
229229
{
230230
p = SkipSlash<TFlags>(pe, pend);
231231

Ramstack.Globbing/Traversal/Files.Utilities.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,16 @@ private static bool IsPartialMatch(ReadOnlySpan<char> path, string[] patterns, M
138138

139139
private static void WriteRelativePath(ref FileSystemEntry entry, scoped Span<char> buffer)
140140
{
141-
var directoryLength = entry.Directory.Length;
142141
var rootLength = entry.RootDirectory.Length;
143-
var relativeLength = directoryLength - rootLength;
142+
var directoryLength = entry.Directory.Length;
143+
var start = directoryLength - rootLength;
144144

145145
entry.Directory.Slice(rootLength).CopyTo(buffer);
146-
buffer[relativeLength ] = '/';
147-
148-
buffer = buffer.Slice(relativeLength + 1);
146+
buffer[start ] = '/';
147+
buffer = buffer.Slice(start + 1);
148+
entry.FileName.CopyTo(buffer);
149149

150150
Debug.Assert(buffer.Length == entry.FileName.Length);
151-
entry.FileName.CopyTo(buffer);
152151
}
153152

154153
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -169,6 +168,6 @@ private static void UpdatePathSeparators(scoped Span<char> path, MatchFlags flag
169168
// Otherwise, the backslash (\) in the path will be treated as an escape character,
170169
// and as a result, the `Unix` flag will essentially not work on a Windows system.
171170
if (Path.DirectorySeparatorChar == '\\' && flags == MatchFlags.Unix)
172-
PathHelper.ConvertToForwardSlashes(path);
171+
PathHelper.ConvertPathToPosixStyle(path);
173172
}
174173
}

0 commit comments

Comments
 (0)