diff --git a/src/filesystem/Filesystem.php b/src/filesystem/Filesystem.php --- a/src/filesystem/Filesystem.php +++ b/src/filesystem/Filesystem.php @@ -853,6 +853,53 @@ } /** + * Retrieve the relative location between two absolute paths. + * + * This function returns the relative path between two given absolute paths. + * The implementation was based on a post on + * [[http://stackoverflow.com/a/2638272/1369417 | StackOverflow]]. + * + * @param string The source destination. + * @param string The target destination. + * @return string The relative path between the source and target + * destinations. + * @task path + */ + public static function relativePath($from, $to) { + // Some compatibility fixes for Windows paths. + $from = is_dir($from) ? rtrim($from, '\/').'/' : $from; + $to = is_dir($to) ? rtrim($to, '\/').'/' : $to; + $from = str_replace('\\', '/', $from); + $to = str_replace('\\', '/', $to); + + $from = explode('/', $from); + $to = explode('/', $to); + + $rel_path = $to; + + foreach ($from as $depth => $dir) { + // Find first non-matching directory. + if ($dir === $to[$depth]) { + // Ignore this directory. + array_shift($rel_path); + } else { + // Get number of remaining directories to $from. + $remaining = count($from) - $depth; + if ($remaining > 1) { + // Add traversals up to first matching directory. + $pad_length = (count($rel_path) + $remaining - 1) * -1; + $rel_path = array_pad($rel_path, $pad_length, '..'); + break; + } + } + } + + // `DIRECTORY_SEPARATOR` is not necessary. See + // http://us2.php.net/manual/en/ref.filesystem.php#73954. + return implode('/', $rel_path); + } + + /** * Test whether a path is descendant from some root path after resolving all * symlinks and removing artifacts. Both paths must exists for the relation * to obtain. A path is always a descendant of itself as long as it exists. diff --git a/src/filesystem/__tests__/FilesystemTestCase.php b/src/filesystem/__tests__/FilesystemTestCase.php --- a/src/filesystem/__tests__/FilesystemTestCase.php +++ b/src/filesystem/__tests__/FilesystemTestCase.php @@ -121,45 +121,47 @@ } } - public function testisDescendant() { + public function testRelativePath() { $test_cases = array( array( - __FILE__, - dirname(__FILE__), - true, + '/', + '/foo/bar/baz', + 'foo/bar/baz', ), array( - dirname(__FILE__), - dirname(dirname(__FILE__)), - true, + '/foo', + '/foo/bar/baz', + 'bar/baz', ), array( - dirname(__FILE__), - phutil_get_library_root_for_path(__FILE__), - true, + '/foo/bar/baz', + '/foo/foobar', + '../foobar', ), + + // Windows paths array( - dirname(dirname(__FILE__)), - dirname(__FILE__), - false, + 'c:\\', + 'c:\\Windows\\System32\\Drivers\\etc\\hosts', + 'Windows/System32/Drivers/etc/hosts', ), array( - dirname(__FILE__).'/quack', - dirname(__FILE__), - false, + 'c:\\Users\\Bill Gates\\', + 'c:\\Windows\\System32\\Drivers\\etc\\hosts', + '../../Windows/System32/Drivers/etc/hosts', ), ); foreach ($test_cases as $test_case) { - list($path, $root, $expected) = $test_case; + list($from, $to, $expected) = $test_case; $this->assertEqual( $expected, - Filesystem::isDescendant($path, $root), + Filesystem::relativePath($from, $to), sprintf( - 'Filesystem::isDescendant(%s, %s)', - phutil_var_export($path), - phutil_var_export($root))); + 'Filesystem::relativePath(%s, %s)', + phutil_var_export($from), + phutil_var_export($to))); } }