How to get canonicalized path (realpath) of nonexistent file in PHP?
Asked Answered
S

3

7

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 ../nonexistent.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.

Symbolics answered 11/5, 2014 at 4:41 Comment(2)
strip off the 'bad' filename, leaving only the directory component, then realpath on that.Rozanneroze
@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.Symbolics
S
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.

Symbolics answered 11/5, 2014 at 4:46 Comment(0)
L
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 /.
Lorileelorilyn answered 11/1, 2016 at 9:5 Comment(0)
M
0

simple method which resolves any .. and . in a path - works only for directories (it will always add the directory separator to the end)

function getCanonicalPath($path, $dirSep = "/") {
  $isAbsolute = str_starts_with($path, $dirSep);
  $pathParts = explode($dirSep, $path);
  $resultingParts = [];
  for ($i = 0; $i < count($pathParts); $i++) {
    $pathPart = $pathParts[$i];
    switch(trim($pathPart)){
      case "":
      case ".":
        // do nothing
        break;
      case "..":
        array_pop($resultingParts);
        break;
      default:
        $resultingParts[] = $pathPart;
        break;
    }
  }
  $ret = implode($dirSep, $resultingParts) . $dirSep;
  if ($isAbsolute) {
    $ret = "/" . $ret;
  }
  return $ret;
}
Meitner answered 30/8 at 11:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.