10

Apparently, realpath is very buggy. In PHP 5.3.1, it causes random crashes. In 5.3.0 and less, realpath randomly fails and returns false (for the same string of course), plus it always fails on realpath-ing the same string twice/more (and of course, it works the first time).

Also, it is so buggy in earlier PHP versions, that it is completely unusable. Well...it already is, since it's not consistent.

Anyhow, what options do I have? Maybe rewrite it by myself? Is this advisable?

Christian
  • 27,509
  • 17
  • 111
  • 155
  • please go to [bugs.php.net](http://bugs.php.net "PHP's Bugtracker") and see if the errors you encounter are already listed. If not, please file a bug report to have them fixed. – Gordon Oct 29 '10 at 07:33
  • They are documented, however, even if they weren't a patch can't help earlier ("stable") PHP versions...I need to work on something that actually works. – Christian Oct 29 '10 at 07:37
  • 3
    care to share links to the bug reports? – Pekka Oct 29 '10 at 07:52
  • Google: "site:bugs.php.net realpath" #39367 #14049 seem interesting. – Christian Oct 29 '10 at 08:06

6 Answers6

28

Thanks to Sven Arduwie's code (pointed out by Pekka) and some modification, I've built a (hopefully) better implementation:

/**
 * This function is to replace PHP's extremely buggy realpath().
 * @param string The original path, can be relative etc.
 * @return string The resolved path, it might not exist.
 */
function truepath($path){
    // whether $path is unix or not
    $unipath=strlen($path)==0 || $path{0}!='/';
    // attempts to detect if path is relative in which case, add cwd
    if(strpos($path,':')===false && $unipath)
        $path=getcwd().DIRECTORY_SEPARATOR.$path;
    // resolve path parts (single dot, double dot and double delimiters)
    $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
    $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
    $absolutes = array();
    foreach ($parts as $part) {
        if ('.'  == $part) continue;
        if ('..' == $part) {
            array_pop($absolutes);
        } else {
            $absolutes[] = $part;
        }
    }
    $path=implode(DIRECTORY_SEPARATOR, $absolutes);
    // resolve any symlinks
    if(file_exists($path) && linkinfo($path)>0)$path=readlink($path);
    // put initial separator that could have been lost
    $path=!$unipath ? '/'.$path : $path;
    return $path;
}

NB: Unlike PHP's realpath, this function does not return false on error; it returns a path which is as far as it could to resolving these quirks.

Note 2: Apparently some people can't read properly. Truepath() does not work on network resources including UNC and URLs. It works for the local file system only.

Community
  • 1
  • 1
Christian
  • 27,509
  • 17
  • 111
  • 155
  • Looks good. I tested it a little bit on Windows and it works fine, even across drive letters. – Pekka Oct 29 '10 at 09:11
  • Thanks a lot for the testing! Knowing it is very stable is of huge importance to my project. – Christian Oct 29 '10 at 09:27
  • @Christian I noticed that if you provide a protocol, only one slash is returned when two are expected. ex: `truepath("http://www.foobar.com/");` will return `http:/www.foobar.com` – Justin Bull Aug 15 '11 at 23:25
  • @JustinBull - That would be because it was designed to work on real files. – Christian Aug 15 '11 at 23:54
  • 1
    -1 Yeah, sure, less buggy. Just from glancing I can see that it is wrong for UNC paths and it probably doesn't get many other edge cases right either. – NikiC Sep 16 '11 at 15:16
  • 1
    @NikiC That function is being used on 5 different servers each of which `realpath()` failed for various reasons. What were you saying about "less buggy"? – Christian Sep 16 '11 at 15:20
  • 1
    **DANGEROUS, DO NOT USE**. Who cares about UNC paths? There are there **serious problems** with this function, and I'm not talking about the horrible code formatting. It **``doesn't correctly resolve symbolic links``**, which is a **``major security problem``**! The return part may also lack the starting slash which is another major security issue. It also doesn't conform to POSIX in a bunch of other ways. PHP's realpath() isn't very good, but this is **much** worse! – Martin Tournoij Feb 09 '12 at 23:59
  • 2
    @Carpetsmoker - Huh? What do you mean it doesn't resolve symbolic links? What does `readlink()` do according to you? Also, I never said nor wanted it to resolve UNC paths. And with regards to POSIX...huh? – Christian Feb 10 '12 at 19:31
  • ``readlink()`` doesn't work if any intermediate path is a link. For example if ``test`` is a symlink in to ``/etc/`` in ``/var/www/test/file/`` it won't be resolved. This is because this is not ``readlink()``'s job, but ``realpath()``'s. -- ``realpath()`` is a standard function as defined in the POSIX standard: http://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html – Martin Tournoij Feb 12 '12 at 02:55
  • That's not true. It's been working since at least PHP 5.1, which I've tested out. Also, as I mentioned above this whole function has been in use for quite some time, without issues. If what you said is correct, it would have shown up a long time ago. – Christian Feb 12 '12 at 10:03
  • Also, why all this fuss about security? Accepting paths from user input is a security issue by itself, I don't see this scaremongering realistic. – Christian Feb 12 '12 at 10:05
  • Unfortunately when I feed this function the unix path `/usr/texbin/` I get `/usr\texbin`, which isn't right. BTW texbin is a symlink made by mklink on Win 7. – McGafter Jul 03 '13 at 21:51
  • @McGafter That's strange. Why are you validating a unix path on a dos/windows system? It will obviously break... – Christian Jul 05 '13 at 19:42
  • I'm using code which normally runs on a Mac, but I just want to run a copy of it on Windows without having to change code back and forth. – McGafter Jul 09 '13 at 08:02
  • I understand your concern but it's not possible directly; you have to manually convert the path across different platforms. For example, assuming the path is `C:/myapp/test` and the current application is in `C:/myapp` you can use this instead of the path: `$dir = __DIR__ . DIRECTORY_SEPARATOR . 'test';` – Christian Jul 09 '13 at 17:32
  • 2
    The function has one bug: if the path is relative and cwd is being added, unipath needs to become false again- otherwise leading / will be missing from the result: // attempts to detect if path is relative in which case, add cwd if(strpos($path,':')===false && $unipath) { $path=getcwd().DIRECTORY_SEPARATOR.$path; $unipath=false; } – andig Jul 13 '13 at 13:06
  • I would use is_link($path) instead of linkinfo($path) because when you hit the actual link itself linkinfo($path) > 0 but readlink() fails. With is_link() it works in all cases. – Dom Dec 22 '16 at 13:05
  • Sorry, ignore my last comment, because that does not work either. Problem is I get a 'PHP Warning: readlink(): Invalid argument' when the path does not contain a symlink on OSX. E.g. I run truepath('Users'), I am greeted with the above error. Still looking for a solution. – Dom Dec 22 '16 at 13:30
  • When using this function in PHP7.4 (and above), use ```$path[0]``` instead of ```$path{0}``` in order to avoid error: "Array and string offset access syntax with curly braces is deprecated". – Bazardshoxer Jan 28 '20 at 07:59
4

here is the modified code that supports UNC paths as well

static public function truepath($path)
{
    // whether $path is unix or not
    $unipath = strlen($path)==0 || $path{0}!='/';
    $unc = substr($path,0,2)=='\\\\'?true:false;
    // attempts to detect if path is relative in which case, add cwd
    if(strpos($path,':') === false && $unipath && !$unc){
        $path=getcwd().DIRECTORY_SEPARATOR.$path;
        if($path{0}=='/'){
            $unipath = false;
        }
    }

    // resolve path parts (single dot, double dot and double delimiters)
    $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
    $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
    $absolutes = array();
    foreach ($parts as $part) {
        if ('.'  == $part){
            continue;
        }
        if ('..' == $part) {
            array_pop($absolutes);
        } else {
            $absolutes[] = $part;
        }
    }
    $path = implode(DIRECTORY_SEPARATOR, $absolutes);
    // resolve any symlinks
    if( function_exists('readlink') && file_exists($path) && linkinfo($path)>0 ){
        $path = readlink($path);
    }
    // put initial separator that could have been lost
    $path = !$unipath ? '/'.$path : $path;
    $path = $unc ? '\\\\'.$path : $path;
    return $path;
}
Pavel Perna
  • 359
  • 2
  • 10
2

I know this is an old thread, but it is really helpful.

I meet a weird Phar::interceptFileFuncs issue when I implemented relative path in phpctags, the realpath() is really really buggy inside phar.

Thanks this thread give me some lights, here comes with my implementation based on christian's implemenation from this thread and this comments.

Hope it works for you.

function relativePath($from, $to)
{
    $fromPath = absolutePath($from);
    $toPath = absolutePath($to);

    $fromPathParts = explode(DIRECTORY_SEPARATOR, rtrim($fromPath, DIRECTORY_SEPARATOR));
    $toPathParts = explode(DIRECTORY_SEPARATOR, rtrim($toPath, DIRECTORY_SEPARATOR));
    while(count($fromPathParts) && count($toPathParts) && ($fromPathParts[0] == $toPathParts[0]))
    {
        array_shift($fromPathParts);
        array_shift($toPathParts);
    }
    return str_pad("", count($fromPathParts)*3, '..'.DIRECTORY_SEPARATOR).implode(DIRECTORY_SEPARATOR, $toPathParts);
}

function absolutePath($path)
{
    $isEmptyPath    = (strlen($path) == 0);
    $isRelativePath = ($path{0} != '/');
    $isWindowsPath  = !(strpos($path, ':') === false);

    if (($isEmptyPath || $isRelativePath) && !$isWindowsPath)
        $path= getcwd().DIRECTORY_SEPARATOR.$path;

    // resolve path parts (single dot, double dot and double delimiters)
    $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
    $pathParts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
    $absolutePathParts = array();
    foreach ($pathParts as $part) {
        if ($part == '.')
            continue;

        if ($part == '..') {
            array_pop($absolutePathParts);
        } else {
            $absolutePathParts[] = $part;
        }
    }
    $path = implode(DIRECTORY_SEPARATOR, $absolutePathParts);

    // resolve any symlinks
    if (file_exists($path) && linkinfo($path)>0)
        $path = readlink($path);

    // put initial separator that could have been lost
    $path= (!$isWindowsPath ? '/'.$path : $path);

    return $path;
}
Community
  • 1
  • 1
Mark Wu
  • 81
  • 1
  • 2
2

For those Zend users out there, THIS answer may help you, as it did me:

$path = APPLICATION_PATH . "/../directory";
$realpath = new Zend_Filter_RealPath(new Zend_Config(array('exists' => false)));
$realpath = $realpath->filter($path);
axiom82
  • 636
  • 1
  • 7
  • 15
1

I have never heard of such massive problems with realpath() (I always thought that it just interfaces some underlying OS functionality - would be interested in some links), but the User Contributed Notes to the manual page have a number of alternative implementations. Here is one that looks okay.

Of course, it's not guaranteed these implementations take care of all cross-platform quirks and issues, so you'd have to do thorough testing to see whether it suits your needs.

As far as I can see though, none of them returns a canonicalized path, they only resolve relative paths. If you need that, I'm not sure whether you can get around realpath() (except perhaps executing a (system-dependent) console command that gives you the full path.)

Pekka
  • 442,112
  • 142
  • 972
  • 1,088
0

On Windows 7, the code works fine. On Linux, there is a problem in that the path generated starts with (in my case) home/xxx when it should start with /home/xxx ... ie the initial /, indicating the root folder, is missing. The problem is not so much with this function, but with what getcwd returns in Linux.

  • 2
    Ah, I did encounter and fix that problem but forgot to update the answer. I've updated it now. Thanks. By the way, next time please add a comment to my answer rather than create a new answer altogether. :) – Christian Sep 06 '11 at 12:58
  • Sorry, cant see how to comment on your answer. However, this revised version now doesnt work on either Windows or Linux. I find that $unipath is being set for both OSs ... and in in case, I dont understand the logic of the statement '$unipath=strlen($path)==0 || $path{0}!='/'; (why would either of these signify the use of Unix ???). Also, surely in the stament $path=!$unipath ? '/'.$path : $path; the ! is incorrect and should be removed ??? – Andrew Fry Sep 07 '11 at 22:34
  • The first part checks whether that path is empty and starts with a separator or not. The name is wrong, it should be "nonunixpath" or something. Taking this into consideration, the final statement is correct; `path = !nonunixpath ? '/'.path : path;`. – Christian Sep 08 '11 at 13:08