2

It appears that there is no standard way to calculate LocalPath from a relative URI (this property is valid only for absolute URIs), to be used in conjunction with Path.Combine, for example to combine it with a file mask (*.ext). Problem is that MakeRelativeUri produces something similar to my%20folder/, instead of my folder\.

Here is a workaround that I found:

Module Module1
  Sub Main()
    Dim path1 As String = "C:\my folder\"
    Dim path2 As String = "C:\"
    MsgBox(GetPathDiff(path1, path2)) 'outputs "my folder\" (without quotes)
  End Sub

  Private Function GetPathDiff(path1 As String, path2 As String) As String
    Dim uri1 As New Uri(path1)
    Dim uri2 As New Uri(path2)
    Dim uri3 As Uri = uri2.MakeRelativeUri(uri1)
    Return Uri.UnescapeDataString(uri3.OriginalString).Replace("/", "\")
  End Function
End Module

I find it a rather clumsy way, and there might be some hidden stones I did not yet stumble upon, i.e. this method being not 100% stable for different use cases.

Is there a better way to do it?

Victor Zakharov
  • 25,801
  • 18
  • 85
  • 151
  • Why are you using Uri here? From your code it looks like you're trying to deal with file paths. That's not really Uri's focus - the only reason it has any support for that is because of file: URIs. The value it's giving you is correct - if you look at uri1 and uri2 here in the debugger you'll see they're file:///C:/my%20folder/ and file:///C:/ so the correct relative URI is indeed my%20folder. Presumably you're looking for a relative filesystem path? That's not really a concept in the world of Uris, so it's going to be clumsy. – Ian Griffiths Feb 07 '13 at 21:35
  • @IanGriffiths: I found an implementation to do path diff using URI somewhere on the net, which seemed to be a rather clean one, until now. I still think it's better than other solutions out there, in terms of lines of code metric. – Victor Zakharov Feb 07 '13 at 21:47

2 Answers2

2

(Note: not so much an answer as a lament, but I hope some of this is informative.)

If what you're trying to do here is get a relative path between two filesystem paths, then you're best off sticking with filesytem APIs.

Initially the question at Calculating the path relative to some root- the inverse of Path.Combine looked to be aiming at the same thing as you, and before I applied an EDIT to this paragraph, I suggested looking at it. But on closer inspection, it turns out the solutions there at the time I write this aren't that great.

The worry I have with Uri here is that it's designed around rules for URI paths, which are not necessarily the same as those for filesystem paths. For example, the URI spec says that a path segment of "." is "intended for use at the beginning of a relative-path reference" whereas with a filesystem path, it's perfectly legal (if slightly weird) to have them in the middle of a path. E.g. c:\.\a\.\b\.\c is legal and means the same as c:\a\b\c.

Filesystem canonicalization is notoriously easy to get wrong, so there may well be more subtle issues than this.

So in theory, a filesystem-specific API would be better than using code designed to handle URIs in the hope that it will produce results that work on a filesystem. In practice, .NET doesn't seem to supply a filesystem-aware API for calculating relative paths, and surprisingly, the Win32 API for this exact purpose, PathRelativePathTo, gets that "." issue wrong...

Community
  • 1
  • 1
Ian Griffiths
  • 14,302
  • 2
  • 64
  • 88
1

[editedit] Ok, after a bit of contemplation, I did come up with an alternative, but it may not be palatable to all:

void Main()
{
    var path1 = @"C:\Program Files\Internet Explorer\";
    var path2 = @"C:\temp\";
    var sb = new StringBuilder(1000);
    PathRelativePathTo(sb, path1, 0, path2, 0);
    sb.ToString().Dump();
}

/*
BOOL PathRelativePathTo(
  _Out_  LPTSTR pszPath,
  _In_   LPCTSTR pszFrom,
  _In_   DWORD dwAttrFrom,
  _In_   LPCTSTR pszTo,
  _In_   DWORD dwAttrTo
);
*/
[DllImport("Shlwapi.dll")]
[return:MarshalAs(UnmanagedType.Bool)]
public static extern bool PathRelativePathTo(
    [Out] StringBuilder result,
    [In] string pathFrom,
    [In] int dwAttrFrom,
    [In] string pathTo,
    [In] int dwAttrTo);

Ooh, just had an idea - does this get you (more or less) what you need?

public string PathDiff(string path1, string path2)
{
    var replace1 = path1.Replace(path2, string.Empty);
    var replace2 = path2.Replace(path1, string.Empty);
    return Path.IsPathRooted(replace1) ? replace2 : replace1;
}

Or possibly better:

public string PathDiff(string path1, string path2)
{
    return path1.Length > path2.Length ? 
        path1.Replace(path2, string.Empty) : 
        path2.Replace(path1, string.Empty);
}

(edit: derp, hit submit too soon):

There's no built-in relative path helpers, unfortunately, but you've basically got it with what you've got, like so:

var path1 = @"C:\dev\src\release\Frontend\";
var path2 = @"C:\dev\src\";

var path1Uri = new Uri(path1);
var path2Uri = new Uri(path2);

var from1to2 = path1Uri.MakeRelativeUri(path2Uri).OriginalString;
var from2to1 = path2Uri.MakeRelativeUri(path1Uri).OriginalString;

Console.WriteLine("To go from {0} to {1}, you need to {2}", path1, path2, from1to2);
Console.WriteLine("To go from {0} to {1}, you need to {2}", path2, path1, from2to1);

Output:

To go from C:\dev\src\release\Frontend\ to C:\dev\src\, you need to ../../
To go from C:\dev\src\ to C:\dev\src\release\Frontend\, you need to release/Frontend/

Now, as for the slash differences "\" vs "/", if you wrap the end results in Path.GetFullPath, it will auto-resolve the differences:

Console.WriteLine(Path.GetFullPath(Path.Combine(path1, from1to2)));
Console.WriteLine(Path.GetFullPath(Path.Combine(path2, from2to1)));

Output:

C:\dev\src\
C:\dev\src\release\Frontend\
JerKimball
  • 16,584
  • 3
  • 43
  • 55
  • What about escaping, i.e. stuff like `%20`? Also, I need to stay at relative path level, so I cannot do `GetFullPath`. It is used as a parameter for `winrar` process, which otherwise gets the directory structure wrong. – Victor Zakharov Feb 07 '13 at 18:57
  • @Neolisk Ah, if you need the relative path format, I'm not sure there is an alternative to `Uri.UnescapeDataString`...I'll ponder it a bit, but you may already have the "best" solution. – JerKimball Feb 07 '13 at 19:03
  • I think your PathDiff can only handle situations when one path is a child of another one. URI approach works regardless of how paths are nested. Consider this example: "C:\my folder\123\7876" minus "C:\test\879\123" should equal this "..\..\my folder\123\7876". – Victor Zakharov Feb 07 '13 at 21:52
  • Another example is "C:\my folder\" minus "X:\" which results in "C:\my folder\", meaning `MakeRelativeUri` tried its best to resolve a relative path, but only absolute path works in this case, and it figured that out nicely. – Victor Zakharov Feb 07 '13 at 22:00
  • Yeah, it's a pity that isn't "baked in" to the IO namespace, but I think what you have is probably the best solution for your scenario...of I think of anything, I'll edit here, but not hopeful... – JerKimball Feb 07 '13 at 22:06
  • @Neolisk Hey, thought of one other option...have edited answer to show – JerKimball Feb 07 '13 at 22:39
  • WinAPI? Looks promising - I'll check that later when I have a chance. +1 for your effort. – Victor Zakharov Feb 07 '13 at 23:02