Question

Is there a reliable way to calculate the inverse of Path.Combine()?

Path.Combine("c:\folder", "subdirectory\something.txt") might return something like "c:\folder\subdirectory\something.text". What I want is the inverse, a function where Path.GetRelativeUrl("c:\folder", "c:\folder\subdirectory\something.text") would return something like ""subdirectory\something.txt".

One solution is to do string comparisons and trim the roots, but this would not work when the same path is expressed in different ways (use of ".." or "~1" in path expression).

Was it helpful?

Solution

Alright so in my case I don't have some of the tougher edge cases (fullPath and relativePath mixing network map locations, super long filenames). What I ended up doing was creating the class below

public class PathUtil
{
    static public string NormalizeFilepath(string filepath)
    {
        string result = System.IO.Path.GetFullPath(filepath).ToLowerInvariant();

        result = result.TrimEnd(new [] { '\\' });

        return result;
    }

    public static string GetRelativePath(string rootPath, string fullPath)
    {
        rootPath = NormalizeFilepath(rootPath);
        fullPath = NormalizeFilepath(fullPath);

        if (!fullPath.StartsWith(rootPath))
            throw new Exception("Could not find rootPath in fullPath when calculating relative path.");

        return "." + fullPath.Substring(rootPath.Length);
    }
}

It seems to work pretty well. At least, it passes these NUnit tests:

[TestFixture]
public class PathUtilTest
{
    [Test]
    public void TestDifferencesInCapitolizationDontMatter()
    {
        string format1 = PathUtil.NormalizeFilepath("c:\\windows\\system32");
        string format2 = PathUtil.NormalizeFilepath("c:\\WindowS\\System32");

        Assert.AreEqual(format1, format2);
    }

    [Test]
    public void TestDifferencesDueToBackstepsDontMatter()
    {
        string format1 = PathUtil.NormalizeFilepath("c:\\windows\\system32");
        string format2 = PathUtil.NormalizeFilepath("c:\\Program Files\\..\\Windows\\System32");

        Assert.AreEqual(format1, format2);
    }

    [Test]
    public void TestDifferencesInFinalSlashDontMatter()
    {
        string format1 = PathUtil.NormalizeFilepath("c:\\windows\\system32");
        string format2 = PathUtil.NormalizeFilepath("c:\\windows\\system32\\");

        Console.WriteLine(format1);
        Console.WriteLine(format2);

        Assert.AreEqual(format1, format2);
    }

    [Test]
    public void TestCanCalculateRelativePath()
    {
        string rootPath = "c:\\windows";
        string fullPath = "c:\\windows\\system32\\wininet.dll";
        string expectedResult = ".\\system32\\wininet.dll";

        string result = PathUtil.GetRelativePath(rootPath, fullPath);

        Assert.AreEqual(expectedResult, result);
    }

    [Test]
    public void TestThrowsExceptionIfRootDoesntMatchFullPath()
    {
        string rootPath = "c:\\windows";
        string fullPath = "c:\\program files\\Internet Explorer\\iexplore.exe";

        try
        {
            PathUtil.GetRelativePath(rootPath, fullPath);
        }
        catch (Exception)
        {
            return;
        }

        Assert.Fail("Exception expected");
    }
}

The test cases rely on certain files existing.. these files are common on most Windows installs but your mileage may vary.

OTHER TIPS

I tried finding a way to do this with long file paths but I'm just not getting satisfactory results because you lose canonicalization of paths in Win32 when you use the long path versions of the standard file system calls. So this solution doesn't necessarily work with things longer than 260 characters, but it's managed code and brain dead simple otherwise.

string path1 = @"c:\folder\subdirectory\something.text";
string path2 = @"c:\folder\foo\..\something.text";
Uri value = new Uri(path1);
Uri value2 = new Uri(path2);
Uri result = value.MakeRelativeUri(value2);
Console.WriteLine(result.OriginalString);

Which gives

../something.text

Now the 8.3 names (short names) for paths is a different matter. It's my understanding that those paths are stored in the file system and you have to use win32 to get them. Plus they can be disabled so there is no guarantee that they are there. To get the long path from a short path you call GetLongPathName in the Kernel32.dll. This also means the file has to exist.

If you want to do that then this site is your friend. GetLongPathName

I have done it with following function. First parameter is the directory from were we are looking, second parameter is the destination path. Both paths can be relative. The function is not optimized but does its job.

private string _GetRelativePath(string fromPath, string toPath)
{
  string fromFull = Path.Combine(Environment.CurrentDirectory, fromPath);
  string toFull = Path.Combine(Environment.CurrentDirectory, toPath);

  List<string> fromParts = new List<string> 
        fromFull.Split(Path.DirectorySeparatorChar));
  List<string> toParts = 
        new List<string>(toFull.Split(Path.DirectorySeparatorChar));

  fromParts.RemoveAll(string.IsNullOrEmpty);
  toParts.RemoveAll(string.IsNullOrEmpty);

  // Remove all the same parts in front
  bool areRelative = false;
  while ( fromParts.Count > 0 && toParts.Count > 0 &&
        StringComparer.OrdinalIgnoreCase.Compare(fromParts[0], toParts[0]) == 0 )
  {
        fromParts.RemoveAt(0);
        toParts.RemoveAt(0);

        areRelative = true;
  }

  if ( !areRelative )
        return toPath;

  // Number of remaining fromParts is number of parent dirs
  StringBuilder ret = new StringBuilder();

  for ( int i = 0; i < fromParts.Count; i++ )
  {
        if ( ret.Length > 0 )
              ret.Append(Path.DirectorySeparatorChar);

        ret.Append("..");
  }

  // And the remainder of toParts
  foreach (string part in toParts)
  {
        if ( ret.Length > 0 )
              ret.Append(Path.DirectorySeparatorChar);

        ret.Append(part);
  }

  return ret.ToString();
}

Try Path.GetFullPath first, and then string comparison.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top