245

Does the .NET Framework have any methods for converting a path (e.g. "C:\whatever.txt") into a file URI (e.g. "file:///C:/whatever.txt")?

The System.Uri class has the reverse (from a file URI to absolute path), but nothing as far as I can find for converting to a file URI.

Also, this is not an ASP.NET application.

DavidRR
  • 18,291
  • 25
  • 109
  • 191
Tinister
  • 11,097
  • 6
  • 35
  • 36

8 Answers8

345

The System.Uri constructor has the ability to parse full file paths and turn them into URI style paths. So you can just do the following:

var uri = new System.Uri("c:\\foo");
var converted = uri.AbsoluteUri;
Pierre Arnaud
  • 10,212
  • 11
  • 77
  • 108
JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • 99
    `var path = new Uri("file:///C:/whatever.txt").LocalPath;` turns a Uri back into a local filepath too for anyone that needs this. – Pondidum Nov 08 '12 at 16:06
  • 2
    As a note. Those kind of Uri is clickable in VS output and R# unit tests output at session windows – AlfeG Jul 03 '13 at 07:15
  • 13
    This is unfortunately not correct. For example `new Uri(@"C:\%51.txt").AbsoluteUri` gives you `"file:///C:/Q.txt"` instead of `"file:///C:/%2551.txt"` – poizan42 Mar 01 '16 at 20:57
  • 7
    this will not work with path with spaces, ie: "C:\test folder\whatever.txt" – Quad Coders Dec 05 '16 at 20:04
  • 4
    Nor will this work with paths that contain a # character. – lewis Feb 24 '20 at 15:28
  • 2
    Setting the dontEscape parameter to true in the Uri constructor made the # work for me, as an anchor, but the documentation says the constructor is deprecated. It may be better to construct the URI and append the anchor to AbsoluteUri later (.NET 5) – A. Niese Apr 15 '21 at 22:35
  • More correct fix to the problem at: https://stackoverflow.com/a/74852300/5903844 – Tommaso Ercole Dec 19 '22 at 15:19
59

What no-one seems to realize is that none of the System.Uri constructors correctly handles certain paths with percent signs in them.

new Uri(@"C:\%51.txt").AbsoluteUri;

This gives you "file:///C:/Q.txt" instead of "file:///C:/%2551.txt".

Neither values of the deprecated dontEscape argument makes any difference, and specifying the UriKind gives the same result too. Trying with the UriBuilder doesn't help either:

new UriBuilder() { Scheme = Uri.UriSchemeFile, Host = "", Path = @"C:\%51.txt" }.Uri.AbsoluteUri

This returns "file:///C:/Q.txt" as well.

As far as I can tell the framework is actually lacking any way of doing this correctly.

We can try to it by replacing the backslashes with forward slashes and feed the path to Uri.EscapeUriString - i.e.

new Uri(Uri.EscapeUriString(filePath.Replace(Path.DirectorySeparatorChar, '/'))).AbsoluteUri

This seems to work at first, but if you give it the path C:\a b.txt then you end up with file:///C:/a%2520b.txt instead of file:///C:/a%20b.txt - somehow it decides that some sequences should be decoded but not others. Now we could just prefix with "file:///" ourselves, however this fails to take UNC paths like \\remote\share\foo.txt into account - what seems to be generally accepted on Windows is to turn them into pseudo-urls of the form file://remote/share/foo.txt, so we should take that into account as well.

EscapeUriString also has the problem that it does not escape the '#' character. It would seem at this point that we have no other choice but making our own method from scratch. So this is what I suggest:

public static string FilePathToFileUrl(string filePath)
{
  StringBuilder uri = new StringBuilder();
  foreach (char v in filePath)
  {
    if ((v >= 'a' && v <= 'z') || (v >= 'A' && v <= 'Z') || (v >= '0' && v <= '9') ||
      v == '+' || v == '/' || v == ':' || v == '.' || v == '-' || v == '_' || v == '~' ||
      v > '\xFF')
    {
      uri.Append(v);
    }
    else if (v == Path.DirectorySeparatorChar || v == Path.AltDirectorySeparatorChar)
    {
      uri.Append('/');
    }
    else
    {
      uri.Append(String.Format("%{0:X2}", (int)v));
    }
  }
  if (uri.Length >= 2 && uri[0] == '/' && uri[1] == '/') // UNC path
    uri.Insert(0, "file:");
  else
    uri.Insert(0, "file:///");
  return uri.ToString();
}

This intentionally leaves + and : unencoded as that seems to be how it's usually done on Windows. It also only encodes latin1 as Internet Explorer can't understand unicode characters in file urls if they are encoded.

poizan42
  • 1,461
  • 17
  • 22
  • Is there a nuget that includes this with a liberal license? It’s a pity no proper way for this exists in the framework and keeping copypasta updated is hard too… – binki Oct 26 '16 at 20:48
  • 4
    You can use the code above under the terms of the MIT license (I don't believe something that short should even be copyrightable, but now you have an explicit grant) – poizan42 Oct 26 '16 at 23:16
  • 1
    Uri.EscapeDataString(string) is favored over EscapeUriString. – Tommy May 11 '21 at 12:54
  • @Tommy That function does something different, and it won't work here, since `/` should be left unescaped. – IS4 Nov 05 '22 at 00:53
  • Unfortunately this does not take into account the fact that URI are Unicode strings and before encoding them you should convert to UTF8. There is a easier fix to this problem https://stackoverflow.com/a/74852300/5903844 – Tommaso Ercole Dec 19 '22 at 15:19
  • @TommasoErcole I addressed this: "It also only encodes latin1 as Internet Explorer can't understand unicode characters in file urls if they are encoded.". File uri's won't work with some shell apis on Windows if you utf8 encode and escape. – poizan42 Dec 20 '22 at 12:58
  • @poizan42 my bad, I missed the last sentence in the post :(. But I would not choose to base my implementation on outdated APIs like the Shell APIs. – Tommaso Ercole Dec 21 '22 at 15:20
  • @TommasoErcole The shell api is the basis of Windows Explorer and Open/Save dialogs and really the whole Windows Desktop experience (and ShellExecute is used everytime you open a file on Windows). It is part of the core of Windows and very very far from being outdated. – poizan42 Aug 15 '23 at 14:47
20

The solutions above do not work on Linux.

Using .NET Core, attempting to execute new Uri("/home/foo/README.md") results in an exception:

Unhandled Exception: System.UriFormatException: Invalid URI: The format of the URI could not be determined.
   at System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind)
   at System.Uri..ctor(String uriString)
   ...

You need to give the CLR some hints about what sort of URL you have.

This works:

Uri fileUri = new Uri(new Uri("file://"), "home/foo/README.md");

...and the string returned by fileUri.ToString() is "file:///home/foo/README.md"

This works on Windows, too.

new Uri(new Uri("file://"), @"C:\Users\foo\README.md").ToString()

...emits "file:///C:/Users/foo/README.md"

Bob Stine
  • 905
  • 10
  • 13
  • 2
    If you know the path is absolute you can use `new Uri("/path/to/file", UriKind.Absolute);` – Minijack May 15 '19 at 06:35
  • From what I can tell this only fails on Windows (as it is a relative URI which cannot be resolved) but works just fine on Linux (see https://dotnetfiddle.net/E50mF1). Talking about .NET Core 3.1 though, older versions may throw. – Honza R Oct 30 '20 at 14:41
8

VB.NET:

Dim URI As New Uri("D:\Development\~AppFolder\Att\1.gif")

Different outputs:

URI.AbsolutePath   ->  D:/Development/~AppFolder/Att/1.gif  
URI.AbsoluteUri    ->  file:///D:/Development/~AppFolder/Att/1.gif  
URI.OriginalString ->  D:\Development\~AppFolder\Att\1.gif  
URI.ToString       ->  file:///D:/Development/~AppFolder/Att/1.gif  
URI.LocalPath      ->  D:\Development\~AppFolder\Att\1.gif

One liner:

New Uri("D:\Development\~AppFolder\Att\1.gif").AbsoluteUri

Output: file:///D:/Development/~AppFolder/Att/1.gif

KyleMit
  • 30,350
  • 66
  • 462
  • 664
MrCalvin
  • 1,675
  • 1
  • 19
  • 27
  • 2
    `AbsoluteUri` is correct one because it encodes also spaces to %20. – psulek Feb 12 '15 at 10:22
  • 1
    I’m convinced that this suffers from the same problems described in [the answer that talks about special character handling](http://stackoverflow.com/a/35734486/429091). – binki Oct 26 '16 at 20:46
4

At least in .NET 4.5+ you can also do:

var uri = new System.Uri("C:\\foo", UriKind.Absolute);
Gavin Greenwalt
  • 325
  • 4
  • 10
  • 1
    Don't you risk getting a `UriFormatException` one day? – berezovskyi Dec 03 '15 at 07:33
  • 4
    This does not work correctly either, `new Uri(@"C:\%51.txt",UriKind.Absolute).AbsoluteUri` returns `"file:///C:/Q.txt"` instead of `"file:///C:/%2551.txt"` – poizan42 Mar 01 '16 at 20:58
1

UrlCreateFromPath to the rescue! Well, not entirely, as it doesn't support extended and UNC path formats, but that's not so hard to overcome:

public static Uri FileUrlFromPath(string path)
{
    const string prefix = @"\\";
    const string extended = @"\\?\";
    const string extendedUnc = @"\\?\UNC\";
    const string device = @"\\.\";
    const StringComparison comp = StringComparison.Ordinal;

    if(path.StartsWith(extendedUnc, comp))
    {
        path = prefix+path.Substring(extendedUnc.Length);
    }else if(path.StartsWith(extended, comp))
    {
        path = prefix+path.Substring(extended.Length);
    }else if(path.StartsWith(device, comp))
    {
        path = prefix+path.Substring(device.Length);
    }

    int len = 1;
    var buffer = new StringBuilder(len);
    int result = UrlCreateFromPath(path, buffer, ref len, 0);
    if(len == 1) Marshal.ThrowExceptionForHR(result);

    buffer.EnsureCapacity(len);
    result = UrlCreateFromPath(path, buffer, ref len, 0);
    if(result == 1) throw new ArgumentException("Argument is not a valid path.", "path");
    Marshal.ThrowExceptionForHR(result);
    return new Uri(buffer.ToString());
}

[DllImport("shlwapi.dll", CharSet=CharSet.Auto, SetLastError=true)]
static extern int UrlCreateFromPath(string path, StringBuilder url, ref int urlLength, int reserved);

In case the path starts with with a special prefix, it gets removed. Although the documentation doesn't mention it, the function outputs the length of the URL even if the buffer is smaller, so I first obtain the length and then allocate the buffer.

Some very interesting observation I had is that while "\\device\path" is correctly transformed to "file://device/path", specifically "\\localhost\path" is transformed to just "file:///path".

The WinApi function managed to encode special characters, but leaves Unicode-specific characters unencoded, unlike the Uri construtor. In that case, AbsoluteUri contains the properly encoded URL, while OriginalString can be used to retain the Unicode characters.

IS4
  • 11,945
  • 2
  • 47
  • 86
0

Unfortunately @poizan42 answer does not take into account the fact that we live in a Unicode world and it's too restrictive according to RFC3986. The accepted answer of @pierre-arnaud and @jaredpar relies on the System.Uri constructor that has to take care for too many components of the Uri to be able to manage the variability of file names and it fails poorly on percent character and others cases. The other answers are simplicistics or simply unuseful. The best one would have been @is4, but after posting the first version of this post I tested it together in the test case I wrote for mine and it fails on many Unicode characters.

In my case I started looking into @poizan42 code and the various answer commenting what was working and what not, so I took a slightly different approach.

First I consider the input string to be a valid file path, so I programmatically created path in my test using all the valid unicode characters and surrogate pairs. With this I verified that at least Path.GetInvalidFileNameChars() seems to return the correct set at least in Windows. Then I passed these paths to a method that I implemented following the ABNF rules for path that you can find at page 22 of https://www.ietf.org/rfc/rfc3986.txt.

I compare the results of it with what the UriBuilder was generating and this is the resulting fix:

public static string FilePathToFileUrl(string path)
{
    return new UriBuilder("file",string.Empty)
        {
            Path = path
                .Replace("%",$"%{(int)'%':X2}")
                .Replace("[",$"%{(int)'[':X2}")
                .Replace("]",$"%{(int)']':X2}"),
        }
        .Uri
        .AbsoluteUri;
}

This is totally unoptimized and perform three replaces, so feel free to convert it to Span or StringBuilder.

Tommaso Ercole
  • 443
  • 5
  • 7
-2

The workaround is simple. Just use the Uri().ToString() method and percent-encode white-spaces, if any, afterwards.

string path = new Uri("C:\my exampleㄓ.txt").ToString().Replace(" ", "%20");

properly returns file:///C:/my%20exampleㄓ.txt