5

script.php

$filename = realpath(sprintf("%s/%s", getcwd(), $argv[1]));
var_dump($filename);

Let's try some things

[/foo/bar/bof] $ php script.php ../foo.txt
string(16) "/foo/bar/foo.txt"

[/foo/bar/bof] $ php script.php ../nonexistent.txt
bool(false)

Dammit! realpath is returning false because the file doesn't exist.

What I'd like to see for the ../nonexsitent.txt is

string(24) "/foo/bar/nonexistent.txt"

How can I get the canonicalized path for any relative path in PHP?

Note: I saw some questions regarding resolving symlink paths. Answers to these questions are not applicable to my question.

Thank you
  • 107,507
  • 28
  • 191
  • 224
  • strip off the 'bad' filename, leaving only the directory component, then realpath on that. – Marc B May 11 '14 at 05:06
  • @MarcB, to handle corner cases, it looks like it takes quite a bit more work. See my answer below. I'm really hoping there's a better way to do this. – Thank you May 11 '14 at 05:29

2 Answers2

0

This is the best I could come up with

function canonicalize_path($path, $cwd=null) {

  // don't prefix absolute paths
  if (substr($path, 0, 1) === "/") {
    $filename = $path;
  }

  // prefix relative path with $root
  else {
    $root      = is_null($cwd) ? getcwd() : $cwd;
    $filename  = sprintf("%s/%s", $root, $path);
  }

  // get realpath of dirname
  $dirname   = dirname($filename);
  $canonical = realpath($dirname);

  // trigger error if $dirname is nonexistent
  if ($canonical === false) {
    trigger_error(sprintf("Directory `%s' does not exist", $dirname), E_USER_ERROR);
  }

  // prevent double slash "//" below
  if ($canonical === "/") $canonical = null;

  // return canonicalized path
  return sprintf("%s/%s", $canonical, basename($filename));
}

It requires that all directories in the path exist. The basename of the path is the only part that can be nonexistent.

An error will be thrown in the event that the dirname doesn't exist.

Thank you
  • 107,507
  • 28
  • 191
  • 224
0

I created this one:

$path_canonicalize = function($str, $started = false) use(&$path_canonicalize)
{
    $str = str_replace('/', DIRECTORY_SEPARATOR, $str).DIRECTORY_SEPARATOR;

    if (!$started)
        $str = preg_replace("/".preg_quote(DIRECTORY_SEPARATOR, "'".DIRECTORY_SEPARATOR."'")."{2,}/", DIRECTORY_SEPARATOR, $str);

    $pos = strpos($str, '..'.DIRECTORY_SEPARATOR);
    if ($pos !== false)
    {
        $part = trim(substr($str, 0, $pos), DIRECTORY_SEPARATOR);
        $str = $path_canonicalize(trim(substr($part, 0, strrpos($part, DIRECTORY_SEPARATOR)), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.trim(substr($str, $pos+3), DIRECTORY_SEPARATOR), true);
    }
    return rtrim($str, DIRECTORY_SEPARATOR);
};

/*
Try those cases to check the consistency:
$str = __DIR__.'/template//////../header//..';
$str = __DIR__.'/template///..///../header//..';
$str = __DIR__.'/template/../header/..';
$str = __DIR__.'/template/../header/../';
$str = __DIR__.'/template/../header/..//';
$str = __DIR__.'/template/../header/..///';
$str = __DIR__.'/template/../header/..///..';
$str = __DIR__.'/template/../header/..///../';
$str = __DIR__.'/template\\..\\header\\..';
*/
$str = __DIR__.'/template/../header/..///..//';
echo 'original: '.$str.PHP_EOL;
echo 'normalized: '.$path_canonicalize($str).PHP_EOL;

Some concerns:

  1. The routine do not check if the given path is relative or absolute.
  2. Recommended to inform the absolute path, however works for relative paths too. The routine treat all as string and not as filesystem.
  3. Final result removes the directory separator from the beginning and end of a string.
  4. Do not supports single dot ./ or /.
Daniel Omine
  • 131
  • 1
  • 4