diff --git a/src/StreamPath.php b/src/StreamPath.php new file mode 100644 index 0000000..eee361c --- /dev/null +++ b/src/StreamPath.php @@ -0,0 +1,39 @@ +normalize($components); + + if ($parsedComponents->count() > 0 && $parsedComponents[0] === '..') { + if ($strict) { + throw new \InvalidArgumentException('Path went beyond root'); + } + + do { + $parsedComponents->shift(); + } while ($parsedComponents[0] === '..'); + } + + $this->prefix = $prefix; + $this->components = $parsedComponents; + } +} diff --git a/src/UrlPath.php b/src/UrlPath.php index c39f23a..852d818 100644 --- a/src/UrlPath.php +++ b/src/UrlPath.php @@ -19,7 +19,7 @@ final class UrlPath extends AbstractAbsolutePath throw new \InvalidArgumentException('Url is malformed'); } - $path = $urlComponents['path'] ?? ''; + $urlPath = $urlComponents['path'] ?? ''; $prefix = ''; if (isset($urlComponents['scheme'])) { @@ -37,7 +37,7 @@ final class UrlPath extends AbstractAbsolutePath $prefix .= $urlComponents['host'] . '/'; } - $components = explode('/', $path); + $components = explode('/', $urlPath); $parsedComponents = $this->normalize($components); diff --git a/tests/StreamPathTest.php b/tests/StreamPathTest.php new file mode 100644 index 0000000..176a6ca --- /dev/null +++ b/tests/StreamPathTest.php @@ -0,0 +1,282 @@ +toString()); + + $path = StreamPath::parse('vfs://invalid/level/of/nesting/../../../../../../../../../../i/am/test/unix/path'); + self::assertEquals('vfs://i/am/test/unix/path', $path->toString()); + + $path = StreamPath::parse('vfs://i/./am/./skipme/./.././test/./unix/path', true); + self::assertEquals('vfs://i/am/test/unix/path', $path->toString()); + + // root path + $path = StreamPath::parse('vfs://', true); + self::assertEquals('vfs://', $path->toString()); + } + + public function testCreateStrict(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Path went beyond root'); + + StreamPath::parse('vfs://invalid/level/of/nesting/../../../../../../../../../../i/am/test/unix/path', true); + } + + public function testCreateInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The path does not appear to be a PHP stream path'); + + StreamPath::parse('not/starting/with/scheme', true); + } + + public function testResolveRelative(): void + { + $path = StreamPath::parse('vfs://i/am/test/unix/path'); + + $rp = new RelativePath('/i/am/test/relative/path'); + self::assertEquals( + 'vfs://i/am/test/relative/path', + $path->resolveRelative($rp)->toString() + ); + + $rp = new RelativePath('i/am/test/relative/path'); + self::assertEquals( + 'vfs://i/am/test/unix/path/i/am/test/relative/path', + $path->resolveRelative($rp)->toString() + ); + + $rp = new RelativePath('../../i/am/test/relative/path'); + self::assertEquals( + 'vfs://i/am/test/i/am/test/relative/path', + $path->resolveRelative($rp)->toString() + ); + + $rp = new RelativePath('../../../../../../../../i/am/test/relative/path'); + self::assertEquals( + 'vfs://i/am/test/relative/path', + $path->resolveRelative($rp)->toString() + ); + + $rp = new RelativePath('..'); + self::assertEquals( + 'vfs://i/am/test/unix', + $path->resolveRelative($rp)->toString() + ); + + $rp = new RelativePath('.'); + self::assertEquals( + 'vfs://i/am/test/unix/path', + $path->resolveRelative($rp)->toString() + ); + } + + public function testResolveRelativeStrict(): void + { + $path = StreamPath::parse('vfs://i/am/test/unix/path'); + + $rp = new RelativePath('/i/am/test/relative/path'); + self::assertEquals( + 'vfs://i/am/test/relative/path', + $path->resolveRelative($rp, true)->toString() + ); + + $rp = new RelativePath('i/am/test/relative/path'); + self::assertEquals( + 'vfs://i/am/test/unix/path/i/am/test/relative/path', + $path->resolveRelative($rp, true)->toString() + ); + + $rp = new RelativePath('../../i/am/test/relative/path'); + self::assertEquals( + 'vfs://i/am/test/i/am/test/relative/path', + $path->resolveRelative($rp, true)->toString() + ); + + $rp = new RelativePath('..'); + self::assertEquals( + 'vfs://i/am/test/unix', + $path->resolveRelative($rp)->toString() + ); + + $rp = new RelativePath('.'); + self::assertEquals( + 'vfs://i/am/test/unix/path', + $path->resolveRelative($rp)->toString() + ); + } + + public function testResolveRelativeStrictInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Relative path went beyond root'); + + $path = StreamPath::parse('vfs://i/am/test/unix/path'); + $rp4 = new RelativePath('../../../../../../../../i/am/test/relative/path'); + $path->resolveRelative($rp4, true); + } + + public function testMakeRelative(): void + { + $paths = [ + StreamPath::parse('vfs://i/am/test/unix/path'), + StreamPath::parse('vfs://i/am/another/unix/test/path'), + StreamPath::parse('vfs://i/am'), + StreamPath::parse('vfs://i/am/test/unix/path'), // different instance, same path + ]; + + $matrix = [ + [ + '.', + '../../../another/unix/test/path', + '../../..', + '.', + ], + [ + '../../../../test/unix/path', + '.', + '../../../..', + '../../../../test/unix/path', + ], + [ + 'test/unix/path', + 'another/unix/test/path', + '.', + 'test/unix/path', + ], + [ + '.', + '../../../another/unix/test/path', + '../../..', + '.', + ], + ]; + + foreach ($paths as $bpi => $bp) { + foreach ($paths as $tpi => $tp) { + $result = $matrix[$bpi][$tpi]; + + self::assertEquals($result, $bp->makeRelative($tp)->toString()); + } + } + } + + public function testMakeRelativeTrailingSlash(): void + { + $paths = [ + new StreamPath('vfs://path/path1'), + new StreamPath('vfs://path/path1/'), + new StreamPath('vfs://path/path2'), + new StreamPath('vfs://path/path2/'), + ]; + + $matrix = [ + [ + '.', + './', + '../path2', + '../path2/', + ], + [ + '.', + './', + '../path2', + '../path2/', + ], + [ + '../path1', + '../path1/', + '.', + './', + ], + [ + '../path1', + '../path1/', + '.', + './', + ], + ]; + + foreach ($paths as $bpi => $bp) { + foreach ($paths as $tpi => $tp) { + $result = $matrix[$bpi][$tpi]; + + self::assertEquals( + $result, + $bp->makeRelative($tp)->toString(), + sprintf('Unexpected relative of base %s and target %s', \strval($bp), \strval($tp)), + ); + } + } + } + + public function testMakeRelativeRoot(): void + { + $paths = [ + new StreamPath('vfs://'), + new StreamPath('vfs://'), // same path, different instance + new StreamPath('vfs://path'), + new StreamPath('vfs://path/'), + ]; + + $matrix = [ + [ + '.', + '.', + 'path', + 'path/', + ], + [ + '.', + '.', + 'path', + 'path/', + ], + [ + '..', + '..', + '.', + './', + ], + [ + '..', + '..', + '.', + './', + ], + ]; + + foreach ($paths as $bpi => $bp) { + foreach ($paths as $tpi => $tp) { + $result = $matrix[$bpi][$tpi]; + + self::assertEquals( + $result, + $bp->makeRelative($tp)->toString(), + sprintf('Unexpected relative of base %s and target %s', \strval($bp), \strval($tp)), + ); + } + } + } + + public function testMakeRelativeWrongType(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('You can only make relative path from paths of same type and same prefix'); + + StreamPath::parse('vfs://i/am/test/unix/path')->makeRelative(UnixPath::parse('/i/am/test/unix/path')); + } +} diff --git a/tests/UrlPathTest.php b/tests/UrlPathTest.php index 4eefe93..0aae44c 100644 --- a/tests/UrlPathTest.php +++ b/tests/UrlPathTest.php @@ -159,7 +159,7 @@ class UrlPathTest extends TestCase $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Relative path went beyond root'); - $path = $path = UrlPath::parse('https://example.com/i/am/test/url/'); + $path = UrlPath::parse('https://example.com/i/am/test/url/'); $rp = new RelativePath('../../../../../../../../i/am/test/relative/path'); $path->resolveRelative($rp, true); }