@@ -18,6 +18,9 @@ public static class FileUri
1818 // \\?\folder => file://?/folder
1919 // \\.\folder => file://./folder
2020 private const string FileScheme = "file://" ;
21+ private const char UriPathSeparator = '/' ;
22+ private static readonly SearchValues < char > UnixDirectorySeparators = SearchValues . Create ( [ UriPathSeparator ] ) ;
23+ private static readonly SearchValues < char > WindowsDirectorySeparators = SearchValues . Create ( [ UriPathSeparator , '\\ ' ] ) ;
2124
2225 /// <summary>
2326 /// Encodes file name as URI.
@@ -28,7 +31,9 @@ public static class FileUri
2831 /// <exception cref="ArgumentException"><paramref name="fileName"/> is not fully-qualified.</exception>
2932 public static string Encode ( ReadOnlySpan < char > fileName , TextEncoderSettings ? settings = null )
3033 {
31- ThrowIfPartiallyQualified ( fileName ) ;
34+ if ( fileName . IsEmpty )
35+ throw new ArgumentException ( ExceptionMessages . FullyQualifiedPathExpected , nameof ( fileName ) ) ;
36+
3237 var encoder = settings is null ? UrlEncoder . Default : UrlEncoder . Create ( settings ) ;
3338 var maxLength = GetMaxEncodedLengthCore ( fileName , encoder ) ;
3439 using var buffer = ( uint ) maxLength <= ( uint ) SpanOwner < char > . StackallocThreshold
@@ -47,11 +52,11 @@ public static string Encode(ReadOnlySpan<char> fileName, TextEncoderSettings? se
4752 /// <param name="encoder">The encoder.</param>
4853 /// <returns>The maximum number of characters that can be produced by the encoder.</returns>
4954 public static int GetMaxEncodedLength ( ReadOnlySpan < char > fileName , UrlEncoder ? encoder = null )
50- => GetMaxEncodedLengthCore ( fileName , encoder ?? UrlEncoder . Default ) ;
55+ => fileName . IsEmpty ? 0 : GetMaxEncodedLengthCore ( fileName , encoder ?? UrlEncoder . Default ) ;
5156
5257 private static int GetMaxEncodedLengthCore ( ReadOnlySpan < char > fileName , UrlEncoder encoder )
5358 => FileScheme . Length
54- + Unsafe . BitCast < bool , byte > ( OperatingSystem . IsWindows ( ) )
59+ + Unsafe . BitCast < bool , byte > ( fileName [ 0 ] is not UriPathSeparator )
5560 + encoder . MaxOutputCharactersPerInputCharacter * fileName . Length ;
5661
5762 /// <summary>
@@ -65,45 +70,44 @@ private static int GetMaxEncodedLengthCore(ReadOnlySpan<char> fileName, UrlEncod
6570 /// <exception cref="ArgumentException"><paramref name="fileName"/> is not fully-qualified.</exception>
6671 public static bool TryEncode ( ReadOnlySpan < char > fileName , UrlEncoder ? encoder , Span < char > output , out int charsWritten )
6772 {
68- ThrowIfPartiallyQualified ( fileName ) ;
73+ if ( fileName . IsEmpty )
74+ throw new ArgumentException ( ExceptionMessages . FullyQualifiedPathExpected , nameof ( fileName ) ) ;
6975
7076 return TryEncodeCore ( fileName , encoder ?? UrlEncoder . Default , output , out charsWritten ) ;
7177 }
7278
73- [ StackTraceHidden ]
74- private static void ThrowIfPartiallyQualified ( ReadOnlySpan < char > fileName )
75- {
76- if ( ! Path . IsPathFullyQualified ( fileName ) )
77- throw new ArgumentException ( ExceptionMessages . FullyQualifiedPathExpected , nameof ( fileName ) ) ;
78- }
79-
8079 private static bool TryEncodeCore ( ReadOnlySpan < char > fileName , UrlEncoder encoder , Span < char > output , out int charsWritten )
8180 {
82- const char slash = '/' ;
83- const char driveSeparator = ':' ;
84- const char escapedDriveSeparatorChar = '|' ;
8581 var writer = new SpanWriter < char > ( output ) ;
8682 writer . Write ( FileScheme ) ;
8783
8884 bool endsWithTrailingSeparator ;
89- if ( ! OperatingSystem . IsWindows ( ) )
85+ SearchValues < char > directoryPathSeparators ;
86+ switch ( fileName )
9087 {
91- // nothing to do
92- }
93- else if ( fileName is [ '\\ ' , '\\ ' , .. var rest] ) // UNC path
94- {
95- fileName = rest ;
96- }
97- else if ( GetPathComponent ( ref fileName , out endsWithTrailingSeparator ) is [ .. var drive, driveSeparator ] )
98- {
99- writer . Add ( slash ) ;
100- writer. Write ( drive ) ;
101- writer. Write ( endsWithTrailingSeparator ? [ escapedDriveSeparatorChar , slash ] : [ escapedDriveSeparatorChar ] ) ;
88+ case [ UriPathSeparator , ..] : // Unix path
89+ directoryPathSeparators = UnixDirectorySeparators ;
90+ break ;
91+ case [ '\\ ' , '\\ ' , .. var rest] : // Windows UNC path
92+ directoryPathSeparators = WindowsDirectorySeparators ;
93+ fileName = rest ;
94+ break ;
95+ default : // Windows path
96+ const char driveSeparator = ':' ;
97+ const char escapedDriveSeparatorChar = '|' ;
98+ if ( GetPathComponent ( ref fileName , directoryPathSeparators = WindowsDirectorySeparators , out endsWithTrailingSeparator )
99+ is not [ .. var drive, driveSeparator ] )
100+ throw new ArgumentException ( ExceptionMessages . FullyQualifiedPathExpected , nameof ( fileName ) ) ;
101+
102+ writer . Add ( UriPathSeparator ) ;
103+ writer . Write ( drive ) ;
104+ writer . Write ( endsWithTrailingSeparator ? [ escapedDriveSeparatorChar , UriPathSeparator ] : [ escapedDriveSeparatorChar ] ) ;
105+ break ;
102106 }
103107
104- for ( ; ; writer . Add ( slash ) )
108+ for ( ; ; writer . Add ( UriPathSeparator ) )
105109 {
106- var component = GetPathComponent( ref fileName , out endsWithTrailingSeparator ) ;
110+ var component = GetPathComponent ( ref fileName , directoryPathSeparators , out endsWithTrailingSeparator ) ;
107111 if ( encoder . Encode ( component , writer . RemainingSpan , out _ , out charsWritten ) is not OperationStatus . Done )
108112 return false ;
109113
@@ -116,10 +120,10 @@ private static bool TryEncodeCore(ReadOnlySpan<char> fileName, UrlEncoder encode
116120 return true ;
117121 }
118122
119- private static ReadOnlySpan< char > GetPathComponent ( ref ReadOnlySpan < char > fileName , out bool endsWithTrailingSeparator )
123+ private static ReadOnlySpan < char > GetPathComponent ( ref ReadOnlySpan < char > fileName , SearchValues < char > directorySeparatorChars , out bool endsWithTrailingSeparator )
120124 {
121125 ReadOnlySpan < char > component ;
122- var index = fileName. IndexOf ( Path . DirectorySeparatorChar ) ;
126+ var index = fileName . IndexOfAny ( directorySeparatorChars ) ;
123127 if ( endsWithTrailingSeparator = index >= 0 )
124128 {
125129 component = fileName . Slice ( 0 , index ) ;
0 commit comments