diff --git a/src/AbstractAbsolutePath.php b/src/AbstractAbsolutePath.php index cbd66fd..281aaaa 100644 --- a/src/AbstractAbsolutePath.php +++ b/src/AbstractAbsolutePath.php @@ -31,34 +31,52 @@ abstract class AbstractAbsolutePath extends AbstractPath implements AbsolutePath // optimize if the same instance if ($this === $targetPath || $this->components === $targetPath->components) { - return $this->buildRelative(DataTypeHelper::iterableToNewListInstance(['.'])); + return $this->buildRelative(DataTypeHelper::iterableToNewListInstance( + $this->components->count() > 0 && $this->components->top() === '' ? ['.', ''] : ['.'] + )); } - $length = min($this->components->count(), $targetPath->components->count()); + // strip trailing slash + $baseComponents = $this->components; + if ($baseComponents->count() > 0 && $baseComponents->top() === '') { + $baseComponents = clone $baseComponents; // clone when necessary + $baseComponents->pop(); + } + + // strip trailing slash + $targetComponents = clone $targetPath->components; // always clone + if ($targetComponents->count() > 0 && $targetComponents->top() === '') { + $targetComponents->pop(); + } + + $length = min($baseComponents->count(), $targetComponents->count()); $equals ??= fn($a, $b) => $a === $b; for ($i = 0; $i < $length; $i++) { - if (!$equals($this->components[$i], $targetPath->components[$i])) { + if (!$equals($baseComponents[$i], $targetComponents[$i])) { break; } } - $relativeComponents = clone $targetPath->components; - // delete $i components from the beginning (common prefix) for ($j = 0; $j < $i; $j++) { - $relativeComponents->shift(); + $targetComponents->shift(); } // add (baseLen - $i) .. elements - $numBaseDiff = $this->components->count() - $i; + $numBaseDiff = $baseComponents->count() - $i; for ($j = 0; $j < $numBaseDiff; $j++) { - $relativeComponents->unshift('..'); + $targetComponents->unshift('..'); } - $relativeComponents->unshift('.'); + // relative marker + $targetComponents->unshift('.'); + // trailing slash + if ($targetPath->components->count() && $targetPath->components->top() === '') { + $targetComponents->push(''); + } - return $this->buildRelative($relativeComponents); + return $this->buildRelative($targetComponents); } protected function buildRelative(\SplDoublyLinkedList $components): RelativePathInterface diff --git a/tests/UnixPathTest.php b/tests/UnixPathTest.php index 10223a9..6bde37e 100644 --- a/tests/UnixPathTest.php +++ b/tests/UnixPathTest.php @@ -169,7 +169,105 @@ class UnixPathTest extends TestCase foreach ($paths as $tpi => $tp) { $result = $matrix[$bpi][$tpi]; - self::assertEquals($result, $bp->makeRelative($tp)); + self::assertEquals($result, $bp->makeRelative($tp)->toString()); + } + } + } + + public function testMakeRelativeTrailingSlash(): void + { + $paths = [ + new UnixPath('/path/path1'), + new UnixPath('/path/path1/'), + new UnixPath('/path/path2'), + new UnixPath('/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 UnixPath('/'), + new UnixPath('/'), // same path, different instance + new UnixPath('/path'), + new UnixPath('/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)), + ); } } }