Skip to content

Commit 25c99bb

Browse files
committed
Added file uri encoder
1 parent 5f85195 commit 25c99bb

File tree

4 files changed

+151
-1
lines changed

4 files changed

+151
-1
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Text.Encodings.Web;
2+
using DotNext.Buffers;
3+
4+
namespace DotNext.IO;
5+
6+
public sealed class FileUriTests : Test
7+
{
8+
public static TheoryData<string, string> GetPaths()
9+
{
10+
var data = new TheoryData<string, string>();
11+
if (OperatingSystem.IsWindows())
12+
{
13+
data.Add(@"C:\without\whitespace", @"C:\without\whitespace");
14+
data.Add(@"C:\with whitespace", @"C:\with whitespace");
15+
data.Add(@"C:\with\trailing\backslash", @"C:\with\trailing\backslash");
16+
data.Add(@"C:\with\trailing\backslash and space", @"C:\with\trailing\backslash and space");
17+
data.Add(@"C:\with\..\relative\.\components\", @"C:\relative\components\");
18+
data.Add(@"C:\with\..\relative\.\components\", @"C:\relative\components\");
19+
data.Add(@"C:\with\specials\chars\#\$\", @"C:\with\specials\chars\#\$\");
20+
data.Add(@"C:\с\кириллицей", @"C:\с\кириллицей");
21+
data.Add(@"\\unc\path", @"\\unc\path");
22+
data.Add(@"\\?\dos\device", @"\\?\dos\device");
23+
data.Add(@"\\.\dos\device", @"\\.\dos\device");
24+
}
25+
else
26+
{
27+
data.Add("/without/whitespace", "/without/whitespace");
28+
data.Add("/with whitespace", "/with whitespace");
29+
data.Add("/with/trailing/slash/", "/with/trailing/slash/");
30+
data.Add("/with/trailing/slash and space/", "/with/trailing/slash and space/");
31+
data.Add("/with/../relative/./components/", "/relative/components/");
32+
data.Add("/with/special/chars/?/>/</", "/with/special/chars/?/>/</");
33+
data.Add("/с/кириллицей", "/с/кириллицей");
34+
}
35+
36+
return data;
37+
}
38+
39+
[Theory]
40+
[MemberData(nameof(GetPaths))]
41+
public static void EncodeAsUri(string fileName, string expected)
42+
{
43+
var uri = FileUri.Encode(fileName);
44+
Equal(expected, uri.LocalPath);
45+
}
46+
47+
[Theory]
48+
[MemberData(nameof(GetPaths))]
49+
public static void EncodeAsUriChars(string fileName, string expected)
50+
{
51+
Span<char> buffer = stackalloc char[256];
52+
True(FileUri.TryEncode(fileName, UrlEncoder.Default, buffer, out var charsWritten));
53+
54+
var uri = new Uri(buffer.Slice(0, charsWritten).ToString(), UriKind.Absolute);
55+
Equal(expected, uri.LocalPath);
56+
}
57+
}

src/DotNext/ExceptionMessages.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,6 @@ internal static string NoResult<TError>(TError errorCode)
5656
internal static string EndOfBuffer(long remaining) => string.Format(Resources.GetString("EndOfBuffer")!, remaining);
5757

5858
internal static string OverlappedRange => Resources.GetString("OverlappedRange")!;
59+
60+
internal static string FullyQualifiedPathExpected => Resources.GetString("FullyQualifiedPathExpected")!;
5961
}

src/DotNext/ExceptionMessages.restext

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ KeyAlreadyExists=The key already exists
1919
ObjectMustNotBeArray=The object must not be represented by the array
2020
NoResult=The result of the operation is unavailable. Error code is {0}
2121
EndOfBuffer=The buffer has no data to read but the caller expects {0} extra elements
22-
OverlappedRange=The ranges are overlapped
22+
OverlappedRange=The ranges are overlapped
23+
FullyQualifiedPathExpected=The path must be fully-qualified

src/DotNext/IO/FileUri.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Buffers;
2+
using System.Diagnostics;
3+
using System.Text.Encodings.Web;
4+
using DotNext.Buffers;
5+
6+
namespace DotNext.IO;
7+
8+
/// <summary>
9+
/// Represents operations to work with <c>file://</c> scheme.
10+
/// </summary>
11+
public static class FileUri
12+
{
13+
private const string FileScheme = "file://";
14+
15+
/// <summary>
16+
/// Encodes file name as URI.
17+
/// </summary>
18+
/// <param name="fileName">The fully-qualified file name.</param>
19+
/// <param name="settings">The encoding settings.</param>
20+
/// <returns><paramref name="fileName"/> as <see cref="Uri"/>.</returns>
21+
/// <exception cref="ArgumentException"><paramref name="fileName"/> is not fully-qualified.</exception>
22+
public static Uri Encode(ReadOnlySpan<char> fileName, TextEncoderSettings? settings = null)
23+
{
24+
ThrowIfNotFullyQualified(fileName);
25+
var encoder = settings is null ? UrlEncoder.Default : UrlEncoder.Create(settings);
26+
var maxLength = FileScheme.Length + encoder.MaxOutputCharactersPerInputCharacter * fileName.Length;
27+
using var buffer = (uint)maxLength <= (uint)SpanOwner<char>.StackallocThreshold
28+
? stackalloc char[maxLength]
29+
: new SpanOwner<char>(maxLength);
30+
31+
TryEncodeCore(fileName, encoder, buffer.Span, out var writtenCount);
32+
return new(buffer.Span.Slice(0, writtenCount).ToString(), UriKind.Absolute);
33+
}
34+
35+
/// <summary>
36+
/// Tries to encode file name as URI.
37+
/// </summary>
38+
/// <param name="fileName">The fully-qualified file name.</param>
39+
/// <param name="encoder">The encoder that is used to encode the file name.</param>
40+
/// <param name="output">The output buffer.</param>
41+
/// <param name="charsWritten">The number of characters written to <paramref name="output"/>.</param>
42+
/// <returns><see langword="true"/> if <paramref name="fileName"/> is encoded successfully; otherwise, <see langword="false"/>.</returns>
43+
/// <exception cref="ArgumentException"><paramref name="fileName"/> is not fully-qualified.</exception>
44+
public static bool TryEncode(ReadOnlySpan<char> fileName, UrlEncoder? encoder, Span<char> output, out int charsWritten)
45+
{
46+
ThrowIfNotFullyQualified(fileName);
47+
48+
return TryEncodeCore(fileName, encoder ?? UrlEncoder.Default, output, out charsWritten);
49+
}
50+
51+
[StackTraceHidden]
52+
private static void ThrowIfNotFullyQualified(ReadOnlySpan<char> fileName)
53+
{
54+
if (!Path.IsPathFullyQualified(fileName))
55+
throw new ArgumentException(ExceptionMessages.FullyQualifiedPathExpected, nameof(fileName));
56+
}
57+
58+
private static bool TryEncodeCore(ReadOnlySpan<char> fileName, UrlEncoder encoder, Span<char> output, out int charsWritten)
59+
{
60+
var result = false;
61+
var writer = new SpanWriter<char>(output);
62+
writer.Write(FileScheme);
63+
while (!fileName.IsEmpty)
64+
{
65+
var index = fileName.IndexOf(Path.DirectorySeparatorChar);
66+
ReadOnlySpan<char> component;
67+
if (index >= 0)
68+
{
69+
component = fileName.Slice(0, index);
70+
fileName = fileName.Slice(index + 1);
71+
}
72+
else
73+
{
74+
component = fileName;
75+
fileName = default;
76+
}
77+
78+
result = encoder.Encode(component, writer.RemainingSpan, out _, out charsWritten) is OperationStatus.Done;
79+
if (!result)
80+
break;
81+
82+
writer.Advance(charsWritten);
83+
if (index >= 0)
84+
writer.Add(Path.DirectorySeparatorChar);
85+
}
86+
87+
charsWritten = writer.WrittenCount;
88+
return result;
89+
}
90+
}

0 commit comments

Comments
 (0)