Convert file path to a file URI?

C#.NetPathUri

C# Problem Overview


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.

C# Solutions


Solution 1 - C#

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;

Solution 2 - C#

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.

Solution 3 - C#

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"

Solution 4 - C#

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

Solution 5 - C#

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

var uri = new System.Uri("C:\\foo", UriKind.Absolute);

Solution 6 - C#

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.

Solution 7 - C#

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTinisterView Question on Stackoverflow
Solution 1 - C#JaredParView Answer on Stackoverflow
Solution 2 - C#poizan42View Answer on Stackoverflow
Solution 3 - C#Bob StineView Answer on Stackoverflow
Solution 4 - C#MrCalvinView Answer on Stackoverflow
Solution 5 - C#Gavin GreenwaltView Answer on Stackoverflow
Solution 6 - C#IS4View Answer on Stackoverflow
Solution 7 - C#ThingsHappenView Answer on Stackoverflow