4

Since .NET doesn't include an API to make relative paths, I've used Uri's MakeRelativeUri method instead. This works, but I've encountered several cases in which it doesn't due to the fact the Uri is escaped. So I've patched up that too:

public static string MakeRelativePath(string basePath, string tgtPath) {
    return
        Uri.UnescapeDataString(
            new Uri(basePath, UriKind.Absolute)
                .MakeRelativeUri(new Uri(tgtPath, UriKind.Absolute))
            .ToString()
        ).Replace('/', Path.DirectorySeparatorChar);
}

This versions seems to work, but it leaves me feeling a little quesy: aren't there any valid local filesystem paths that this gratuitous unescaping might corrupt?

Related: How to get relative path from absolute path The answers to that question do not address the issue of unusual characters and escaping at all, and as such don't answer this question.

Community
  • 1
  • 1
Eamon Nerbonne
  • 47,023
  • 20
  • 101
  • 166
  • possible duplicate of [How to get relative path from absolute path](http://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path) – David Heffernan Jun 05 '11 at 16:00
  • @Eamon There are some good answers here: http://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path If it was me I'd go for the solution that P/Invokes to `PathRelativePathTo()` – David Heffernan Jun 05 '11 at 16:00
  • Yeah, I see an answer that's also using Uri, though without escaping, i.e. borked. P/Invoking; well, that's slow and platform-dependent, meaning you may run into x86/x64 issues, non-fulltrust issues, and of course things like silverlight/XNA don't necessarily support it. It'd work, but it's not my first choice. – Eamon Nerbonne Jun 05 '11 at 18:55
  • @Eamon I can see that. Me I'm Windows centric and so P/Invoking is fine. I don't see speed being an issue - you aren't going to be calling this inside your app's hot loop are you?!! – David Heffernan Jun 05 '11 at 18:59
  • I have done something similar in a custom search indexer, but at just a few hundred thousand paths, it's probably not a large issue. Still; it's nice not to have the worry in the first place. Btw, does PathRelativePathTo do any I/O and/or normalization? MSDN doesn't say. – Eamon Nerbonne Jun 05 '11 at 19:25
  • 1
    `PathRelativePathTo` doesn't touch the file system so far as I am aware. It's purely text based. If you want to know how it's implemented you can always look at the Wine source code! – David Heffernan Jun 05 '11 at 19:27
  • Thanks, I absolutely love little ideas like that! – Eamon Nerbonne Jun 05 '11 at 19:28

1 Answers1

4

Instead of escaping, unescaping and replacing, you could just use the underlying algorithm used by System.Uri and the PathDifference method. Here it is, recovered via Reflector and modified for slightly better readability. It has also been modified to use backslashes for DOS-style paths instead of forward slashes for URIs, and the comparison is always case-insensitive.

static string PathDifference(string path1, string path2)
{
    int c = 0;  //index up to which the paths are the same
    int d = -1; //index of trailing slash for the portion where the paths are the same

    while (c < path1.Length && c < path2.Length)
    {
        if (char.ToLowerInvariant(path1[c]) != char.ToLowerInvariant(path2[c]))
        {
            break;
        }

        if (path1[c] == '\\')
        {
            d = c;
        }

        c++;
    }

    if (c == 0)
    {
        return path2;
    }

    if (c == path1.Length && c == path2.Length)
    {
        return string.Empty;
    }


    System.Text.StringBuilder builder = new System.Text.StringBuilder();

    while (c < path1.Length)
    {
        if (path1[c] == '\\')
        {
            builder.Append(@"..\");
        }
        c++;
    }

    if (builder.Length == 0 && path2.Length - 1 == d)
    {
        return @".\";
    }

    return builder.ToString() + path2.Substring(d + 1);
}

The expectation for the inputs seems to be that if either path represents a directory, it must have a trailing backslash. Obviously, the paths must be non-null as well.

Here are some example inputs and outputs... see if it meets your needs.

Path1                   Path2               Output
C:\test\path1\path2\    C:\test\            ..\..\
C:\test\path1\file      C:\test\            ..\
C:\test\path1\path2\    C:\                 ..\..\..\
C:\test\path1\path2\    D:\                 D:\
C:\test\path1\path2\    C:\test\path1\pathA ..\pathA
C:\test\                C:\test\    
C:\test\                C:\test\file        file
C:\test\file            C:\test\            .\
C:\test\path #1!\path2\ C:\test\            ..\..\
Michael Petito
  • 12,891
  • 4
  • 40
  • 54
  • yeah, the trailing backslash thing is fairly inherent, if you think about it - the algo doesn't actually have access to the filesystem, right? – Eamon Nerbonne Jun 05 '11 at 18:45
  • I'm not entirely comfortable lifting this via reflector - I'm not a copyright lawyer, and who knows what's OK... – Eamon Nerbonne Jun 05 '11 at 18:48
  • Btw thanks for the effort, it's certainly an interesting approach! – Eamon Nerbonne Jun 05 '11 at 18:48
  • ...and it shows that `Uri` isn't doing any kind of normalization, which is a little unfortunate here; e.g. comparing `C:\test\path1\path2\` to `C:\test\\path1\pathA` doesn't do what it should. On the other hand, `C:\test\xy\..\path1\pathA` does work with the `MakeRelativeUri` above but not here... – Eamon Nerbonne Jun 05 '11 at 18:59