* trailing slash preservation logic

* test root paths
master
Anton Smirnov 11 months ago
parent f9c6cd5340
commit 90db790146
  1. 24
      src/AbstractPath.php
  2. 6
      src/RelativePath.php
  3. 2
      src/UnixPath.php
  4. 2
      src/WindowsPath.php
  5. 69
      tests/RelativePathTest.php
  6. 16
      tests/UnixPathTest.php
  7. 13
      tests/WindowsPathTest.php

@ -45,6 +45,11 @@ abstract class AbstractPath implements PathInterface
$components = clone $this->components;
// remove trailing slash
if ($components->top() === '') {
$components->pop();
}
$numComponents = \count($relativeComponents);
for ($i = 0; $i < $numComponents; $i++) {
if ($relativeComponents[$i] === '.') {
@ -72,10 +77,22 @@ abstract class AbstractPath implements PathInterface
protected function normalize(array $components): \SplDoublyLinkedList
{
$numComponents = \count($components);
// skip empties in the beginning
for ($i = 0; $i < $numComponents; $i++) {
if ($components[$i] !== '') {
break;
}
}
$componentsList = new \SplDoublyLinkedList();
$component = null; // also stores last component ignoring $prevComponent logic
$prevComponent = null;
foreach ($components as $component) {
for ($j = $i; $j < $numComponents; $j++) {
$component = $components[$j];
if ($component === '.' || $component === '') {
continue;
}
@ -93,6 +110,11 @@ abstract class AbstractPath implements PathInterface
$prevComponent = $component;
}
// trailing slash logic
if ($component === '') {
$componentsList->push('');
}
return $componentsList;
}

@ -58,7 +58,7 @@ final class RelativePath extends AbstractPath implements RelativePathInterface
$parsedComponents = $this->normalize($components);
// absolute-ish relative path
$isRoot = $path[0] === '/' || $this->windows && $path[0] === '\\';
$isRoot = \strlen($path) > 0 && ($path[0] === '/' || $this->windows && $path[0] === '\\');
if (!$isRoot) {
$parsedComponents->unshift('.');
}
@ -69,7 +69,7 @@ final class RelativePath extends AbstractPath implements RelativePathInterface
public function isRoot(): bool
{
return $this->components[0] !== '.' && $this->components[0] !== '..';
return $this->components->count() === 0 || $this->components[0] !== '.' && $this->components[0] !== '..';
}
public function toString(): string
@ -77,7 +77,7 @@ final class RelativePath extends AbstractPath implements RelativePathInterface
$directorySeparator = $this->windows ? '\\' : '/';
$components = $this->components;
if ($components[0] === '.' && $components->count() > 1) {
if ($components->count() > 1 && $components[0] === '.' && $components[1] !== '') {
$components = clone $components;
$components->shift();
}

@ -21,7 +21,7 @@ final class UnixPath extends FilesystemPath
$parsedComponents = $this->normalize($components);
if ($parsedComponents[0] === '..') {
if ($parsedComponents->count() > 0 && $parsedComponents[0] === '..') {
if ($strict) {
throw new \InvalidArgumentException('Path went beyond root');
}

@ -55,7 +55,7 @@ final class WindowsPath extends FilesystemPath
$parsedComponents = $this->normalize($components);
if ($parsedComponents[0] === '..') {
if ($parsedComponents->count() > 0 && $parsedComponents[0] === '..') {
if ($strict) {
throw new \InvalidArgumentException('Path went beyond root');
}

@ -30,6 +30,24 @@ class RelativePathTest extends TestCase
$path = RelativePath::unix('.');
self::assertEquals('.', $path->toString());
// test empty
$path = RelativePath::unix('');
self::assertEquals('.', $path->toString());
// preserve trailing slash
$path = RelativePath::unix('./');
self::assertEquals('./', $path->toString());
$path = RelativePath::unix('../');
self::assertEquals('../', $path->toString());
$path = RelativePath::unix('path/');
self::assertEquals('path/', $path->toString());
// root path
$path = RelativePath::unix('/');
self::assertEquals('/', $path->toString());
}
public function testCreateWindows(): void
@ -57,7 +75,6 @@ class RelativePathTest extends TestCase
new RelativePath('..'),
new RelativePath('.'),
];
$relativePaths = $paths;
$matrix = [
[
@ -111,7 +128,7 @@ class RelativePathTest extends TestCase
];
foreach ($paths as $pi => $p) {
foreach ($relativePaths as $rpi => $rp) {
foreach ($paths as $rpi => $rp) {
$matrixResult = $matrix[$pi][$rpi];
self::assertEquals($matrixResult, $p->resolveRelative($rp)->toString());
@ -127,7 +144,6 @@ class RelativePathTest extends TestCase
new RelativePath('../../i/am/test/relative/path'),
new RelativePath('../../../../../../../../i/am/test/relative/path'),
];
$relativePaths = $paths;
$matrix = [
[
@ -157,7 +173,7 @@ class RelativePathTest extends TestCase
];
foreach ($paths as $pi => $p) {
foreach ($relativePaths as $rpi => $rp) {
foreach ($paths as $rpi => $rp) {
$matrixResult = $matrix[$pi][$rpi];
if ($matrixResult === null) {
@ -169,6 +185,51 @@ class RelativePathTest extends TestCase
}
}
public function testResolveRelativeTrailingSlash(): void
{
$paths = [
new RelativePath('../path/path1'),
new RelativePath('../path/path1/'),
new RelativePath('../path/path2'),
new RelativePath('../path/path2/'),
];
$matrix = [
[
'../path/path/path1',
'../path/path/path1/',
'../path/path/path2',
'../path/path/path2/',
],
[
'../path/path/path1',
'../path/path/path1/',
'../path/path/path2',
'../path/path/path2/',
],
[
'../path/path/path1',
'../path/path/path1/',
'../path/path/path2',
'../path/path/path2/',
],
[
'../path/path/path1',
'../path/path/path1/',
'../path/path/path2',
'../path/path/path2/',
],
];
foreach ($paths as $pi => $p) {
foreach ($paths as $rpi => $rp) {
$matrixResult = $matrix[$pi][$rpi];
self::assertEquals($matrixResult, $p->resolveRelative($rp)->toString());
}
}
}
public function testResolveRelativeStrictAssertion(): void
{
$this->expectException(\InvalidArgumentException::class);

@ -13,14 +13,18 @@ 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());
$path = UnixPath::parse('/i/./am/./skipme/./.././test/./unix/path');
self::assertEquals('/i/am/test/unix/path', $path->toString());
$path2 = UnixPath::parse('/invalid/level/of/nesting/../../../../../../../../../../i/am/test/unix/path');
self::assertEquals('/i/am/test/unix/path', $path2->toString());
$path = UnixPath::parse('/invalid/level/of/nesting/../../../../../../../../../../i/am/test/unix/path');
self::assertEquals('/i/am/test/unix/path', $path->toString());
$path3 = UnixPath::parse('/i/./am/./skipme/./.././test/./unix/path', true);
self::assertEquals('/i/am/test/unix/path', $path3->toString());
$path = UnixPath::parse('/i/./am/./skipme/./.././test/./unix/path', true);
self::assertEquals('/i/am/test/unix/path', $path->toString());
// root path
$path = UnixPath::parse('/', true);
self::assertEquals('/', $path->toString());
}
public function testCreateStrict(): void

@ -52,6 +52,11 @@ class WindowsPathTest extends TestCase
$path = WindowsPath::parse('\\\\.MYPC\\c$\\windows\\win.ini');
self::assertEquals('\\\\.MYPC\\c$\\windows\\win.ini', $path->toString());
self::assertEquals('\\\\.MYPC\\', $path->getPrefix());
// root path
$path = WindowsPath::parse('X:\\');
self::assertEquals('X:\\', $path->toString());
self::assertEquals('X:\\', $path->getPrefix());
}
public function testCreateInvalidNotAWinPath(): void
@ -71,6 +76,14 @@ class WindowsPathTest extends TestCase
WindowsPath::parse('c:windows\win.ini');
}
public function testCreateInvalidRoot(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unrecognized Windows path');
WindowsPath::parse('X:');
}
public function testCreateInvalidUNCWithSlash(): void
{
$this->expectException(\InvalidArgumentException::class);

Loading…
Cancel
Save