Unix path

master
Anton Smirnov 2021-10-31 23:36:41 +02:00
parent 93df980c8a
commit cfec611306
9 changed files with 286 additions and 60 deletions

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Arokettu\Path;
interface AbsolutePathInterface extends PathInterface
{
/**
* @param static|string $path
* @return static
*/
public function makeRelative($path, bool $strict = false): self;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Arokettu\Path;
/**
* @internal
*/
abstract class AbstractAbsolutePath extends AbstractPath implements AbsolutePathInterface
{
/**
* @param static|string $path
* @param bool $strict
* @return static
*/
public function makeRelative($path, bool $strict = false): self
{
throw new \BadMethodCallException('not implemented');
}
}

View File

@ -14,11 +14,11 @@ abstract class AbstractPath implements PathInterface
protected string $prefix;
protected \SplDoublyLinkedList $components;
abstract protected function parsePath(string $path): void;
abstract protected function parsePath(string $path, bool $strict): void;
public function __construct(string $path)
public function __construct(string $path, bool $strict = false)
{
$this->parsePath($path);
$this->parsePath($path, $strict);
}
/**
@ -101,12 +101,16 @@ abstract class AbstractPath implements PathInterface
continue;
}
if ($component === '..' && $prevComponent !== '..' && $prevComponent !== null) {
if (
$component === '..' &&
$prevComponent !== '..' && $prevComponent !== null && // leading ..'s
$componentsList->count() > 0 // beginning of the list
) {
$componentsList->pop();
} else {
$componentsList->push($component);
continue;
}
$componentsList->push($component);
$prevComponent = $component;
}
@ -146,6 +150,11 @@ abstract class AbstractPath implements PathInterface
return $this->toString();
}
public function getPrefix(): string
{
return $this->prefix;
}
public function getComponents(): array
{
return iterator_to_array($this->components, false);

31
src/FilesystemPath.php Normal file
View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Arokettu\Path;
abstract class FilesystemPath extends AbstractAbsolutePath
{
public function __construct(string $path, bool $strict = false)
{
if ($this instanceof WindowsPath || $this instanceof UnixPath) {
parent::__construct($path, $strict);
return;
}
throw new \LogicException('The class is not meant to be extended externally');
}
public static function parse(string $path, bool $strict = false): self
{
if (DIRECTORY_SEPARATOR === '\\') {
return new WindowsPath($path, $strict);
}
if (DIRECTORY_SEPARATOR === '/') {
return new UnixPath($path, $strict);
}
throw new \LogicException('Unknown directory separator: ' . DIRECTORY_SEPARATOR);
}
}

View File

@ -6,6 +6,7 @@ namespace Arokettu\Path;
interface PathInterface extends \Stringable
{
public function getPrefix(): string;
public function getComponents(): array;
public function toString(): string;

View File

@ -25,7 +25,7 @@ final class RelativePath extends AbstractPath implements RelativePathInterface
return new self($path, true);
}
protected function parsePath(string $path): void
protected function parsePath(string $path, bool $strict): void
{
$components = explode('/', $path);

37
src/UnixPath.php Normal file
View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Arokettu\Path;
final class UnixPath extends FilesystemPath
{
public static function parse(string $path, bool $strict = false): self
{
return new self($path, $strict);
}
protected function parsePath(string $path, bool $strict): void
{
if ($path[0] !== '/') {
throw new \InvalidArgumentException('Valid unix path must begin with a slash');
}
$components = explode('/', $path);
$parsedComponents = $this->normalize($components);
if ($parsedComponents[0] === '..') {
if ($strict) {
throw new \InvalidArgumentException('Path went beyond root');
}
do {
$parsedComponents->shift();
} while ($parsedComponents[0] === '..');
}
$this->prefix = '/';
$this->components = $parsedComponents;
}
}

View File

@ -14,16 +14,16 @@ class RelativePathTest extends TestCase
public function testCreate(): void
{
// 'absolute' relative path
$path = RelativePath::unix('/i/am/./skipme/../test/./path');
self::assertEquals('/i/am/test/path', $path->toString());
$path = RelativePath::unix('/i/am/./skipme/../test/./relative/path');
self::assertEquals('/i/am/test/relative/path', $path->toString());
// relative path from the current directory
$path = RelativePath::unix('./i/am/./skipme/../test/./path');
self::assertEquals('i/am/test/path', $path->toString());
$path = RelativePath::unix('./i/am/./skipme/../test/./relative/path');
self::assertEquals('i/am/test/relative/path', $path->toString());
// relative path from the parent directory
$path = RelativePath::unix('.././../i/am/./skipme/../test/./path');
self::assertEquals('../../i/am/test/path', $path->toString());
$path = RelativePath::unix('.././../i/am/./skipme/../test/./relative/path');
self::assertEquals('../../i/am/test/relative/path', $path->toString());
}
public function testCreateWindows(): void
@ -44,37 +44,37 @@ class RelativePathTest extends TestCase
public function testResolveRelative(): void
{
$paths = [
new RelativePath('/i/am/test/path'),
new RelativePath('i/am/test/path'),
new RelativePath('../../i/am/test/path'),
new RelativePath('../../../../../../../../i/am/test/path'),
new RelativePath('/i/am/test/relative/path'),
new RelativePath('i/am/test/relative/path'),
new RelativePath('../../i/am/test/relative/path'),
new RelativePath('../../../../../../../../i/am/test/relative/path'),
];
$relativePaths = $paths;
$matrix = [
[
'/i/am/test/path',
'/i/am/test/path/i/am/test/path',
'/i/am/i/am/test/path',
'/i/am/test/path',
'/i/am/test/relative/path',
'/i/am/test/relative/path/i/am/test/relative/path',
'/i/am/test/i/am/test/relative/path',
'/i/am/test/relative/path',
],
[
'/i/am/test/path',
'i/am/test/path/i/am/test/path',
'i/am/i/am/test/path',
'../../../../i/am/test/path',
'/i/am/test/relative/path',
'i/am/test/relative/path/i/am/test/relative/path',
'i/am/test/i/am/test/relative/path',
'../../../i/am/test/relative/path',
],
[
'/i/am/test/path',
'../../i/am/test/path/i/am/test/path',
'../../i/am/i/am/test/path',
'../../../../../../i/am/test/path',
'/i/am/test/relative/path',
'../../i/am/test/relative/path/i/am/test/relative/path',
'../../i/am/test/i/am/test/relative/path',
'../../../../../i/am/test/relative/path',
],
[
'/i/am/test/path',
'../../../../../../../../i/am/test/path/i/am/test/path',
'../../../../../../../../i/am/i/am/test/path',
'../../../../../../../../../../../../i/am/test/path',
'/i/am/test/relative/path',
'../../../../../../../../i/am/test/relative/path/i/am/test/relative/path',
'../../../../../../../../i/am/test/i/am/test/relative/path',
'../../../../../../../../../../../i/am/test/relative/path',
],
];
@ -90,37 +90,37 @@ class RelativePathTest extends TestCase
public function testResolveRelativeStrict(): void
{
$paths = [
new RelativePath('/i/am/test/path'),
new RelativePath('i/am/test/path'),
new RelativePath('../../i/am/test/path'),
new RelativePath('../../../../../../../../i/am/test/path'),
new RelativePath('/i/am/test/relative/path'),
new RelativePath('i/am/test/relative/path'),
new RelativePath('../../i/am/test/relative/path'),
new RelativePath('../../../../../../../../i/am/test/relative/path'),
];
$relativePaths = $paths;
$matrix = [
[
'/i/am/test/path',
'/i/am/test/path/i/am/test/path',
'/i/am/i/am/test/path',
'/i/am/test/relative/path',
'/i/am/test/relative/path/i/am/test/relative/path',
'/i/am/test/i/am/test/relative/path',
null,
],
[
'/i/am/test/path',
'i/am/test/path/i/am/test/path',
'i/am/i/am/test/path',
'../../../../i/am/test/path',
'/i/am/test/relative/path',
'i/am/test/relative/path/i/am/test/relative/path',
'i/am/test/i/am/test/relative/path',
'../../../i/am/test/relative/path',
],
[
'/i/am/test/path',
'../../i/am/test/path/i/am/test/path',
'../../i/am/i/am/test/path',
'../../../../../../i/am/test/path',
'/i/am/test/relative/path',
'../../i/am/test/relative/path/i/am/test/relative/path',
'../../i/am/test/i/am/test/relative/path',
'../../../../../i/am/test/relative/path',
],
[
'/i/am/test/path',
'../../../../../../../../i/am/test/path/i/am/test/path',
'../../../../../../../../i/am/i/am/test/path',
'../../../../../../../../../../../../i/am/test/path',
'/i/am/test/relative/path',
'../../../../../../../../i/am/test/relative/path/i/am/test/relative/path',
'../../../../../../../../i/am/test/i/am/test/relative/path',
'../../../../../../../../../../../i/am/test/relative/path',
],
];
@ -142,24 +142,29 @@ class RelativePathTest extends TestCase
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Relative path went beyond root');
$p = new RelativePath('/i/am/test/path');
$rp = new RelativePath('../../../../../../../../i/am/test/path');
$p = new RelativePath('/i/am/test/relative/path');
$rp = new RelativePath('../../../../../../../../i/am/test/relative/path');
$p->resolveRelative($rp, true);
}
public function testExternalRelativeImplementations(): void
{
$p = new RelativePath('i/am/test/path');
$p = new RelativePath('i/am/test/relative/path');
$rp1 = new class implements RelativePathInterface {
public function __toString(): string {
return '';
}
public function getPrefix(): string
{
return '';
}
public function getComponents(): array
{
return explode('/', '../../i/am/test/path');
return explode('/', '../../i/am/test/relative/path');
}
public function toString(): string
@ -179,13 +184,19 @@ class RelativePathTest extends TestCase
};
$rp2 = new class implements RelativePathInterface {
public function __toString(): string {
public function __toString(): string
{
return '';
}
public function getPrefix(): string
{
return '';
}
public function getComponents(): array
{
return explode('/', 'i/am/test/path');
return explode('/', 'i/am/test/relative/path');
}
public function toString(): string
@ -204,7 +215,7 @@ class RelativePathTest extends TestCase
}
};
self::assertEquals('i/am/i/am/test/path', $p->resolveRelative($rp1)->toString());
self::assertEquals('/i/am/test/path', $p->resolveRelative($rp2)->toString());
self::assertEquals('i/am/test/i/am/test/relative/path', $p->resolveRelative($rp1)->toString());
self::assertEquals('/i/am/test/relative/path', $p->resolveRelative($rp2)->toString());
}
}

102
tests/UnixPathTest.php Normal file
View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Arokettu\Path\Tests;
use Arokettu\Path\RelativePath;
use Arokettu\Path\UnixPath;
use PHPUnit\Framework\TestCase;
class UnixPathTest extends TestCase
{
public function testCreate(): void
{
$path1 = UnixPath::parse('/i/./am/./skipme/./.././test/./unix/path');
self::assertEquals('/i/am/test/unix/path', $path1->toString());
$path2 = UnixPath::parse('/invalid/level/of/nesting/../../../../../../../../../../i/am/test/unix/path');
self::assertEquals('/i/am/test/unix/path', $path2->toString());
$path3 = UnixPath::parse('/i/./am/./skipme/./.././test/./unix/path', true);
self::assertEquals('/i/am/test/unix/path', $path3->toString());
}
public function testCreateStrict(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Path went beyond root');
UnixPath::parse('/invalid/level/of/nesting/../../../../../../../../../../i/am/test/unix/path', true);
}
public function testCreateInvalid(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Valid unix path must begin with a slash');
UnixPath::parse('not/starting/with/slash', true);
}
public function testResolveRelative(): void
{
$path = UnixPath::parse('/i/am/test/unix/path');
$rp1 = new RelativePath('/i/am/test/relative/path');
self::assertEquals(
'/i/am/test/relative/path',
$path->resolveRelative($rp1)->toString()
);
$rp2 = new RelativePath('i/am/test/relative/path');
self::assertEquals(
'/i/am/test/unix/path/i/am/test/relative/path',
$path->resolveRelative($rp2)->toString()
);
$rp3 = new RelativePath('../../i/am/test/relative/path');
self::assertEquals(
'/i/am/test/i/am/test/relative/path',
$path->resolveRelative($rp3)->toString()
);
$rp4 = new RelativePath('../../../../../../../../i/am/test/relative/path');
self::assertEquals(
'/i/am/test/relative/path',
$path->resolveRelative($rp4)->toString()
);
}
public function testResolveRelativeStrict(): void
{
$path = UnixPath::parse('/i/am/test/unix/path');
$rp1 = new RelativePath('/i/am/test/relative/path');
self::assertEquals(
'/i/am/test/relative/path',
$path->resolveRelative($rp1, true)->toString()
);
$rp2 = new RelativePath('i/am/test/relative/path');
self::assertEquals(
'/i/am/test/unix/path/i/am/test/relative/path',
$path->resolveRelative($rp2, true)->toString()
);
$rp3 = new RelativePath('../../i/am/test/relative/path');
self::assertEquals(
'/i/am/test/i/am/test/relative/path',
$path->resolveRelative($rp3, true)->toString()
);
}
public function testResolveRelativeStrictInvalid(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Relative path went beyond root');
$path = UnixPath::parse('/i/am/test/unix/path');
$rp4 = new RelativePath('../../../../../../../../i/am/test/relative/path');
$path->resolveRelative($rp4, true);
}
}