Skip to content

Commit

Permalink
Added Utility\Path wrapper class.
Browse files Browse the repository at this point in the history
Utility\Path::info($path) - enhanced pathinfo() function.
 - always returns all components;
 - correctly report absence of extension for dot-files;
 - adds missing dot to exceptions.

Utility\Path::normalize() - provides path normalization.
 - handles both *NIX and Windows (X:...) style paths;
 - handles leaking relative paths if explicitly allowed;
 - tripping an exception if relative path is trying to escape itself;
 - allows customizable output directory separator.
  • Loading branch information
AnrDaemon committed Sep 7, 2018
1 parent 310c379 commit 673800b
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 57 deletions.
59 changes: 2 additions & 57 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,61 +1,6 @@
------------------------------------------------------------------------
r900 | anrdaemon | 2018-09-04 19:03:27 +0300 (Tue, 04 Sep 2018) | 3 lines
r904 | anrdaemon | 2018-09-07 02:58:13 +0300 (Fri, 07 Sep 2018) | 2 lines

* Net\Browser: Improved docblocks for method callers.
= Sync'd .todo.

------------------------------------------------------------------------
r880 | anrdaemon | 2018-09-01 06:56:56 +0300 (Sat, 01 Sep 2018) | 2 lines

+ Net\Browser: Added __clone and __destruct handlers.

------------------------------------------------------------------------
r879 | anrdaemon | 2018-09-01 06:35:50 +0300 (Sat, 01 Sep 2018) | 2 lines

+ HTTP PUT and custom request method implementations.

------------------------------------------------------------------------
r878 | anrdaemon | 2018-09-01 06:07:17 +0300 (Sat, 01 Sep 2018) | 2 lines

* Moved common request steps into a helper method.

------------------------------------------------------------------------
r877 | anrdaemon | 2018-09-01 05:52:07 +0300 (Sat, 01 Sep 2018) | 3 lines

* Fixed Net\Browser::setOpt to behave and report.
+ Added CurlOptions helper to better report errors.

------------------------------------------------------------------------
r876 | anrdaemon | 2018-09-01 01:27:16 +0300 (Sat, 01 Sep 2018) | 2 lines

+ Implement ability to return basic request status as a single array.

------------------------------------------------------------------------
r875 | anrdaemon | 2018-09-01 01:23:26 +0300 (Sat, 01 Sep 2018) | 4 lines

+ Added a (b)lo(a)t of docblocks to Net\Browser.
* Prepared put and custom requests for existence.
Sad but I can't make it any better than that.

------------------------------------------------------------------------
r874 | anrdaemon | 2018-08-31 20:02:00 +0300 (Fri, 31 Aug 2018) | 2 lines

* Repaired initial docblock, removing legacy behavior references.

------------------------------------------------------------------------
r873 | anrdaemon | 2018-08-31 18:44:46 +0300 (Fri, 31 Aug 2018) | 2 lines

* Used assertEquals where applicable for better failure representation.

------------------------------------------------------------------------
r872 | anrdaemon | 2018-08-31 18:42:04 +0300 (Fri, 31 Aug 2018) | 3 lines

* Reordered methods.
* Added PHPUnit v7 compatible exception trap.

------------------------------------------------------------------------
r871 | anrdaemon | 2018-08-31 16:20:04 +0300 (Fri, 31 Aug 2018) | 2 lines

* Use static $url in tests.
+ Added Utility\Path class to fix/enhance standard functions.

------------------------------------------------------------------------
126 changes: 126 additions & 0 deletions src/Utility/Path.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace AnrDaemon\Utility;

class Path
{
/** Creates fixed pathinfo structure
*
* Meaning, "$dirname/$filename$extension" eq. rtrim($path, "\\/").
*
* Contrary to the {@see \pathinfo() pathinfo()}, all members of the structure always set.
*
* @param string $path The path to get info on.
* @return array Fixed pathinfo structure.
*/
public static function info($path)
{
$p = pathinfo($path);
if(empty($p["filename"]))
{
$p["filename"] = $p["basename"];
unset($p["extension"]);
}
if(!empty($p["extension"]))
{
$p["extension"] = ".{$p["extension"]}";
}

return $p + ["extension" => ""];
}

/** Path normalizer part examinator.
* @internal
* @throws \UnexpectedValueException if relative path is trying to escape above current directory, unless explicitly allowed.
*/
protected static function examine($part, array &$array, $path_relative, $allow_escape = false)
{
if($part === '.')
{
return;
}

if($part !== '..')
{
$array[] = $part;
return;
}

// $part == '..', handle escaping.
$last = end($array);
if($last === '..')
{ // Escaping is allowed and we're already on the run.
$array[] = $part;
return;
}

if($last !== false)
{ // $last element exists - move up the stack.
array_pop($array);
return;
}

if(!$path_relative)
{ // Path is not relative - skip updir.
return;
}

if(!$allow_escape)
throw new \UnexpectedValueException('Attempt to traverse outside the root directory.');

$array[] = $part;
}

/** Normalize path string, removing '.'/'..'/empty components.
*
* Warning: This function is NOT intended to handle URL's ("//host/path")!
* Please use {@see \parse_url() parse_url()} first.
*
* @param string $path The path to normalize.
* @param bool $allow_escape Is the path relative? Defaults to autodetect. Paths declared explicitly relative get slightly different treatment.
* @param string $directory_separator Output directory separator. Defaults to DIRECTORY_SEPARATOR.
* @return string The normalized string.
* @throws \UnexpectedValueException if relative path is trying to escape above current directory, unless explicitly allowed.
*/
public static function normalize($path, $allow_escape = false, $directory_separator = DIRECTORY_SEPARATOR)
{
$path = (string)$path;
if($path === '')
return $path;

$disk = null;
$path_relative = false;

// If path is not explicitly relative, test if it's an absolute and possibly Windows path
// Convert first byte to uppercase.
$char = ord($path[0]) & 0xdf;
if($char & 0x80)
{ // Multibyte character - path is relative
$path_relative = true;
}
// Windows disk prefix "{A..Z}:"
elseif(strlen($path) > 1 && $char > 0x40 && $char < 0x5b && $path[1] === ':')
{
if(strlen($path) === 2)
return $path;

$disk = substr($path, 0, 2);
$path = substr($path, 2);
}

if($path[0] !== "/" && $path[0] !== "\\")
{ // First byte is not a slash
$path_relative = true;
}

$ta = [];
$part = strtok($path, "/\\");
while(false !== $part)
{
static::examine($part, $ta, $path_relative, $allow_escape);
$part = strtok("/\\");
}

return $disk . ($path_relative ? '' : $directory_separator) . join($directory_separator, $ta);
}
}
74 changes: 74 additions & 0 deletions test/Utility/PathTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace AnrDaemon\Tests\Utility;

use AnrDaemon\Utility\Path;
use PHPUnit\Framework\TestCase;

final class PathTest
extends TestCase
{
public function defaultPairsProvider()
{
$data = [
"empty path" => array("", ""),
"root" => array('/', '/'),
"relative" => array("foo", "foo"),
"dot dir" => array('/foo/bar', '/foo/./bar'),
"absolute path inescapable" => array('/', '/Foo/Bar/../../../..'),
"multi slash #1" => array('/', '//'),
"multi slash #2" => array('/', '///'),
"multi slash #3" => array('/Foo', '///Foo'),
"otherdir" => array('/bar', '/foo/../bar/'),
"windows disk only" => array("D:", "D:"),
"windows disk root" => array('c:/', 'c:\\'),
"windows disk relative" => array("e:g", "e:g"),
"windows multi slash #1" => array('c:/foo/bar', 'c:/foo//bar'),
"windows multi slash #2" => array('C:/foo/bar', 'C://foo//bar'),
"windows multi slash #3" => array('C:/foo/bar', 'C:///foo//bar'),
"windows otherdir" => array('C:/bar', 'C:/foo/../bar'),
];

return $data;
}

public function escapablePairsProvider()
{
$data = [
"simple" => array('../foo', '../foo'),
"simple otherdir" => array('../bar', '../foo/../bar'),
"chained escape" => array("../../bar", "a/../../b/../../bar"),
"double collapse" => array('../src', 'Foo/Bar/../../../src'),
"windows otherdir" => array('c:../b', 'c:.\\..\\a\\..\\b'),
];

return $data;
}

/** Test normalization of standard pairs
*
* @dataProvider defaultPairsProvider
*/
public function testNormalizeStandardPair($target, $path)
{
$this->assertTrue($target === Path::normalize($path, null, "/"));
}

/** Test normalization of escapable pairs
*
* @dataProvider escapablePairsProvider
*/
public function testNormalizeEscapingPair($target, $path)
{
$this->assertTrue($target === Path::normalize($path, true, "/"));
}

/** Test exception on escape attempt
*
* @expectedException \UnexpectedValueException
*/
public function testExceptionOnEscapeAttempt()
{
$path = Path::normalize("..", false);
}
}

0 comments on commit 673800b

Please sign in to comment.