diff --git a/readme.md b/readme.md index d068e31..9d1ef94 100644 --- a/readme.md +++ b/readme.md @@ -1,37 +1,43 @@ [![CI](https://github.com/olinox14/path-php/actions/workflows/php.yml/badge.svg)](https://github.com/olinox14/path-php/actions/workflows/php.yml) [![Coverage Status](https://coveralls.io/repos/github/olinox14/path-php/badge.svg?branch=master)](https://coveralls.io/github/olinox14/path-php?branch=master) +[![Version](http://poser.pugx.org/olinox14/path-php/version)](https://packagist.org/packages/olinox14/path-php) +[![License](http://poser.pugx.org/olinox14/path-php/license)](https://packagist.org/packages/olinox14/path-php) +[![PHP Version Require](http://poser.pugx.org/olinox14/path-php/require/php)](https://packagist.org/packages/olinox14/path-php) # Path-php -> This library is still under development, DO NOT USE IN PRODUCTION. Contributions, however, are welcome. +> This library is still under development, **USE WITH CAUTION**. -An intuitive and object-oriented library for file and path operations, inspired by the path.py python library. +An **intuitive**, **standalone**, and **object-oriented** library for file and path operations, +inspired by the ['path' python library](https://path.readthedocs.io/en/latest/api.html#path.Path.parts). parent(); - - // Display the liste of the subdirectories of this directory - var_dump( - $dir->dirs() - ); + // Get the parent directory of the current script file and list its subdirs + $script = new Path(__file__); + $dir = $script->parent(); + var_dump($dir->dirs()); - // Get the path of the working directory + + // Get the path of the working directory, iterate over its files and change their permissions $path = new Path('.'); - // Iterate over the files in this directory and change the permissions of these files to 755 foreach($path->files() as $file) { $file->chmod(755); } - // Create a new path by adding a file's name to the previous path - $newPath = $path->append('readme.md'); + + // Put content into a file + $path = (new Path('.'))->append('readme.md'); - // Display the absolute path of this file - var_dump($newPath->absPath()); + $path->putContent('new readme content'); + + // And many more... + + +Full documentation : [API Documentation](https://olinox14.github.io/path-php/classes/Path-Path.html) + ## Requirement @@ -68,10 +74,6 @@ your current script lies into .md files : } } -## Documentation - -> [API Documentation](https://olinox14.github.io/path-php/classes/Path-Path.html) - ## Contribute ### Git branching @@ -155,16 +157,3 @@ And then run phpdoc with : ## Licence Path-php is under the [MIT](http://opensource.org/licenses/MIT) licence. - -## Roadmap - -0.1.6 : - -* [ ] multi os compat (windows) -* [ ] handle protocols (ftp, sftp, file, smb, http, ...etc) -* [ ] handle unc paths (windows) -* [ ] improve error management and tracebacks -* [ ] add 'ignore' and 'errorOnExistingDestination' to the copyTree method -* [ ] review copyTree performances -* [ ] study the interest of implementing a 'mergeTree' method -* [ ] study the interest of mimic the perms of the source when using the copy, copyTree and move methods \ No newline at end of file diff --git a/src/BuiltinProxy.php b/src/BuiltinProxy.php index 23c91e4..41ae031 100644 --- a/src/BuiltinProxy.php +++ b/src/BuiltinProxy.php @@ -12,6 +12,15 @@ */ class BuiltinProxy { + public static string $DIRECTORY_SEPARATOR = DIRECTORY_SEPARATOR; + + public function getHome(): string + { + return PHP_OS_FAMILY == 'Windows' ? + $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH'] : + $_SERVER['HOME']; + } + public function date(string $format, int $time): string { return date($format, $time); diff --git a/src/Path.php b/src/Path.php index 6730580..93ab622 100644 --- a/src/Path.php +++ b/src/Path.php @@ -43,17 +43,61 @@ public static function join(string|self $path, string|self ...$parts): self $parts = array_map(fn ($p) => (string)$p, $parts); foreach ($parts as $part) { - if (str_starts_with($part, DIRECTORY_SEPARATOR)) { + if (str_starts_with($part, BuiltinProxy::$DIRECTORY_SEPARATOR)) { $path = $part; - } elseif (!$path || str_ends_with($path, DIRECTORY_SEPARATOR)) { + } elseif (!$path || str_ends_with($path, BuiltinProxy::$DIRECTORY_SEPARATOR)) { $path .= $part; } else { - $path .= DIRECTORY_SEPARATOR . $part; + $path .= BuiltinProxy::$DIRECTORY_SEPARATOR . $part; } } return new self($path); } + /** + * Split the pathname path into a pair (drive, tail) where drive is either a mount point or the empty string. + * On systems which do not use drive specifications, drive will always be the empty string. In all cases, + * drive + tail will be the same as path. + * + * On Windows, splits a pathname into drive/UNC sharepoint and relative path. + * + * If the path contains a drive letter, drive will contain everything up to and including the colon: + * + * Path::splitDrive("c:/dir") + * >>> ["c:", "/dir"] + * + * If the path contains a UNC path, drive will contain the host name and share: + * + * Path::splitDrive("//host/computer/dir") + * >>> ["//host/computer", "/dir"] + * + * @param string|self $path The path with the drive. + * @return array An array containing the drive and the path. + */ + public static function splitDrive(string|self $path): array + { + $path = (string)$path; + + $matches = []; + + preg_match('/(^[a-zA-Z]:)(.*)/', $path, $matches); + if ($matches) { + return array_slice($matches, -2); + } + + $rx = + BuiltinProxy::$DIRECTORY_SEPARATOR === '/' ? + '/(^\/\/[\w\-\s]{2,15}\/[\w\-\s]+)(.*)/' : + '/(^\\\\\\\\[\w\-\s]{2,15}\\\[\w\-\s]+)(.*)/'; + + preg_match($rx, $path, $matches); + if ($matches) { + return array_slice($matches, -2); + } + + return ['', $path]; + } + public function __construct(string|self $path) { $this->builtin = new BuiltinProxy(); @@ -300,9 +344,8 @@ public function name(): string /** * Normalize the case of a pathname. * - * On Windows, convert all characters in the pathname to lowercase, and also convert - * forward slashes to backward slashes. On other operating systems, - * return the path unchanged. + * Convert all characters in the pathname to lowercase, and also convert + * forward slashes to backward slashes. * * @return self The instance of the current object. */ @@ -310,7 +353,7 @@ public function normCase(): self { return $this->cast( strtolower( - str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $this->path()) + str_replace(['/', '\\'], BuiltinProxy::$DIRECTORY_SEPARATOR, $this->path()) ) ); } @@ -322,33 +365,26 @@ public function normCase(): self * and A/foo/../B all become A/B. This string manipulation may change the meaning of a path that contains * symbolic links. On Windows, it converts forward slashes to backward slashes. To normalize case, use normcase(). * - * // TODO: becare of the normcase when we're getting to the windows compat - * * @return self A new instance of the class with the normalized path. */ public function normPath(): self { - $path = $this->normCase()->path(); + $path = str_replace(['/', '\\'], BuiltinProxy::$DIRECTORY_SEPARATOR, $this->path()); - // TODO: handle case where path start with // if (empty($path)) { return $this->cast('.'); } // Also tests some special cases we can't really do anything with - if (!str_contains($path, '/') || $path === '/' || '.' === $path || '..' === $path) { + if (!str_contains($path, BuiltinProxy::$DIRECTORY_SEPARATOR) || $path === '/' || '.' === $path || '..' === $path) { return $this->cast($path); } - $path = rtrim($path, '/'); + $path = rtrim($path, BuiltinProxy::$DIRECTORY_SEPARATOR); - // Extract the scheme if any - $scheme = null; - if (strpos($path, '://')) { - list($scheme, $path) = explode('://', $path, 2); - } + [$prefix, $path] = self::splitDrive($path); - $parts = explode('/', $path); + $parts = explode(BuiltinProxy::$DIRECTORY_SEPARATOR, $path); $newParts = []; foreach ($parts as $part) { @@ -376,13 +412,13 @@ public function normPath(): self } // Rebuild path - $newPath = implode('/', $newParts); - - // Add scheme if any - if ($scheme !== null) { - $newPath = $scheme . '://' . $newPath; + if ($prefix) { + array_shift($newParts); // Get rid of the leading empty string resulting from slitDrive result + array_unshift($newParts, rtrim($prefix, BuiltinProxy::$DIRECTORY_SEPARATOR)); } + $newPath = implode(BuiltinProxy::$DIRECTORY_SEPARATOR, $newParts); + return $this->cast($newPath); } @@ -1019,14 +1055,20 @@ public function expand(): self * Expands the user directory in the file path. * * @return self The modified instance with the expanded user path. + * @throws IOException */ public function expandUser(): self { if (!str_starts_with($this->path(), '~/')) { return $this; } + $home = $this->builtin->getHome(); - $home = $this->cast($_SERVER['HOME']); + if (!$home) { + throw new IOException("Error while getting home directory"); + } + + $home = $this->cast($home); return $home->append(substr($this->path(), 2)); } @@ -1180,6 +1222,7 @@ public function rmdir(bool $recursive = false, bool $permissive = false): void * * @throws FileNotFoundException * @throws IOException + * @throws FileExistsException */ public function rename(string|self $newPath): self { @@ -1328,7 +1371,8 @@ public function chunks(int $chunk_size = 8192): Generator */ public function isAbs(): bool { - return str_starts_with($this->path, '/'); + [$drive, $path] = Path::splitDrive($this->path()); + return !empty($drive) || str_starts_with($path, '/'); } /** @@ -1591,16 +1635,27 @@ public function symlink(string | self $newLink): self */ public function parts(): array { + [$prefix, $path] = self::splitDrive($this->path()); $parts = []; - if (str_starts_with($this->path, DIRECTORY_SEPARATOR)) { - $parts[] = DIRECTORY_SEPARATOR; + + if ($prefix) { + $path = ltrim($path, BuiltinProxy::$DIRECTORY_SEPARATOR); + } elseif (str_starts_with($path, BuiltinProxy::$DIRECTORY_SEPARATOR)) { + $parts[] = BuiltinProxy::$DIRECTORY_SEPARATOR; + } + + $parts += explode(BuiltinProxy::$DIRECTORY_SEPARATOR, $path); + + if ($prefix) { + array_unshift($parts, $prefix); } - $parts += explode(DIRECTORY_SEPARATOR, $this->path); return $parts; } /** * Compute a version of this path that is relative to another path. + * This method relies on the php `realpath` method and then requires the path to refer to + * an existing file. * * @param string|Path $basePath * @return self @@ -1621,8 +1676,8 @@ public function getRelativePath(string|self $basePath): self throw new FileNotFoundException("$basePath does not exist or unable to get a real path"); } - $pathParts = explode(DIRECTORY_SEPARATOR, $path); - $baseParts = explode(DIRECTORY_SEPARATOR, $realBasePath); + $pathParts = explode(BuiltinProxy::$DIRECTORY_SEPARATOR, $path); + $baseParts = explode(BuiltinProxy::$DIRECTORY_SEPARATOR, $realBasePath); while (count($pathParts) && count($baseParts) && ($pathParts[0] == $baseParts[0])) { array_shift($pathParts); @@ -1631,10 +1686,10 @@ public function getRelativePath(string|self $basePath): self return $this->cast( str_repeat( - '..' . DIRECTORY_SEPARATOR, + '..' . BuiltinProxy::$DIRECTORY_SEPARATOR, count($baseParts) ) . implode( - DIRECTORY_SEPARATOR, + BuiltinProxy::$DIRECTORY_SEPARATOR, $pathParts ) ); diff --git a/tests/unit/PathTest.php b/tests/unit/PathTest.php index 71a96c7..79a4cf7 100644 --- a/tests/unit/PathTest.php +++ b/tests/unit/PathTest.php @@ -28,6 +28,7 @@ class PathTest extends TestCase public function setUp(): void { + BuiltinProxy::$DIRECTORY_SEPARATOR = '/'; $this->builtin = $this->getMockBuilder(BuiltinProxy::class)->getMock(); } @@ -66,6 +67,38 @@ public function testJoin(): void ); } + public function testSplitDrive(): void + { + BuiltinProxy::$DIRECTORY_SEPARATOR = '/'; + + $this->assertEquals( + ['', '/home/user'], + Path::splitDrive('/home/user') + ); + + $this->assertEquals( + ['', 'home/user'], + Path::splitDrive('home/user') + ); + + $this->assertEquals( + ['//network/computer', '/home/user'], + Path::splitDrive('//network/computer/home/user') + ); + + BuiltinProxy::$DIRECTORY_SEPARATOR = '\\'; + + $this->assertEquals( + ['c:', '\\dir\\foo'], + Path::splitDrive('c:\\dir\\foo') + ); + + $this->assertEquals( + ['\\\\network\\computer', '\\home\\dir'], + Path::splitDrive('\\\\network\\computer\\home\\dir') + ); + } + public function testCastWithString(): void { $path = $this->getMock('bar', 'cast'); @@ -592,137 +625,235 @@ public function testNormCase(): void public function testNormPath(): void { $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('/foo/bar'); - $normed = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed->method('path')->willReturn('/foo/bar'); - $path->expects(self::once())->method('normCase')->willReturn($normed); + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2->method('path')->willReturn('/foo/bar'); - $path->expects(self::once())->method('cast')->with('/foo/bar')->willReturn($normed2); + $path + ->expects(self::once()) + ->method('cast') + ->with('/foo/bar') + ->willReturn($newPath); $this->assertEquals( - '/foo/bar', - $path->normPath()->path() + $newPath, + $path->normPath() + ); + } + + public function testNormPathWithBackSlashes(): void { + $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('\\foo\\bar'); + + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); + + $path + ->expects(self::once()) + ->method('cast') + ->with('/foo/bar') + ->willReturn($newPath); + + $this->assertEquals( + $newPath, + $path->normPath() + ); + } + + public function testNormPathWithEmptyPath(): void { + $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn(''); + + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); + + $path + ->expects(self::once()) + ->method('cast') + ->with('.') + ->willReturn($newPath); + + $this->assertEquals( + $newPath, + $path->normPath() ); } public function testNormPathNoSep(): void { $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('foo'); - $normed = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed->method('path')->willReturn('foo'); - $path->expects(self::once())->method('normCase')->willReturn($normed); + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2->method('path')->willReturn('foo'); - $path->expects(self::once())->method('cast')->with('foo')->willReturn($normed2); + $path + ->expects(self::once()) + ->method('cast') + ->with('foo') + ->willReturn($newPath); $this->assertEquals( - 'foo', - $path->normPath()->path() + $newPath, + $path->normPath() ); } public function testNormPathIsRoot(): void { $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('/'); - $normed = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed->method('path')->willReturn('/'); - $path->expects(self::once())->method('normCase')->willReturn($normed); + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2->method('path')->willReturn('/'); - $path->expects(self::once())->method('cast')->with('/')->willReturn($normed2); + $path + ->expects(self::once()) + ->method('cast') + ->with('/') + ->willReturn($newPath); $this->assertEquals( - '/', - $path->normPath()->path() + $newPath, + $path->normPath() ); } public function testNormPathIsCurrent(): void { $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('..'); - $normed = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed->method('path')->willReturn('..'); - $path->expects(self::once())->method('normCase')->willReturn($normed); + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2->method('path')->willReturn('..'); - $path->expects(self::once())->method('cast')->with('..')->willReturn($normed2); + $path + ->expects(self::once()) + ->method('cast') + ->with('..') + ->willReturn($newPath); $this->assertEquals( - '..', - $path->normPath()->path() + $newPath, + $path->normPath() ); } - public function testNormPathIsParent(): void { + public function testNormPathWithTrailingSep(): void { $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('/foo/bar/'); - $normed = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed->method('path')->willReturn('.'); - $path->expects(self::once())->method('normCase')->willReturn($normed); + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2->method('path')->willReturn('.'); - $path->expects(self::once())->method('cast')->with('.')->willReturn($normed2); + $path + ->expects(self::once()) + ->method('cast') + ->with('/foo/bar') + ->willReturn($newPath); $this->assertEquals( - '.', - $path->normPath()->path() + $newPath, + $path->normPath() ); } - public function testNormPathIsEmpty(): void { + public function testNormPathWithDrive(): void { $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('c:\\foo\\..\\bar'); - $normed = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed->method('path')->willReturn(''); - $path->expects(self::once())->method('normCase')->willReturn($normed); + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2->method('path')->willReturn('.'); - $path->expects(self::once())->method('cast')->with('.')->willReturn($normed2); + $path + ->expects(self::once()) + ->method('cast') + ->with('c:/bar') + ->willReturn($newPath); $this->assertEquals( - '.', - $path->normPath()->path() + $newPath, + $path->normPath() + ); + } + + public function testNormPathWithUNC(): void { + $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('//network/computer/home/../var'); + + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); + + $path + ->expects(self::once()) + ->method('cast') + ->with('//network/computer/var') + ->willReturn($newPath); + + $this->assertEquals( + $newPath, + $path->normPath() + ); + } + + public function testNormPathIsParent(): void { + $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('.'); + + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); + + $path + ->expects(self::once()) + ->method('cast') + ->with('.') + ->willReturn($newPath); + + $this->assertEquals( + $newPath, + $path->normPath() ); } public function testNormPathWithParentPart(): void { $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('/bar'); - $normed = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed->method('path')->willReturn('/foo/../bar'); - $path->expects(self::once())->method('normCase')->willReturn($normed); + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2->method('path')->willReturn('/bar'); - $path->expects(self::once())->method('cast')->with('/bar')->willReturn($normed2); + $path + ->expects(self::once()) + ->method('cast') + ->with('/bar') + ->willReturn($newPath); $this->assertEquals( - '/bar', - $path->normPath()->path() + $newPath, + $path->normPath() + ); + } + + public function testNormPathWithRelativeParts(): void { + $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('/bar/dir1/./dir2/../file.ext'); + + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); + + $path + ->expects(self::once()) + ->method('cast') + ->with('/bar/dir1/file.ext') + ->willReturn($newPath); + + $this->assertEquals( + $newPath, + $path->normPath() ); } public function testNormPathWithLeadingParentPart(): void { $path = $this->getMock('', 'normPath'); + $path->method('path')->willReturn('../bar'); - $normed = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed->method('path')->willReturn('../foo/../bar'); - $path->expects(self::once())->method('normCase')->willReturn($normed); + $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $normed2->method('path')->willReturn('../bar'); - $path->expects(self::once())->method('cast')->with('../bar')->willReturn($normed2); + $path + ->expects(self::once()) + ->method('cast') + ->with('../bar') + ->willReturn($newPath); $this->assertEquals( - '../bar', - $path->normPath()->path() + $newPath, + $path->normPath() ); } @@ -2811,15 +2942,21 @@ public function testExpand(): void ); } + /** + * @throws IOException + */ public function testExpandUser(): void { $path = $this->getMock('~/file.ext', 'expandUser'); $path->method('path')->willReturn('~/file.ext'); - $expandedPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); + $this->builtin->expects(self::once())->method('getHome')->willReturn('/home/foo'); $homePath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); - $path->method('cast')->with($_SERVER['HOME'])->willReturn($homePath); + + $path->method('cast')->with('/home/foo')->willReturn($homePath); + + $expandedPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock(); $homePath ->expects(self::once()) @@ -2844,6 +2981,21 @@ public function testExpandUserNothingToExpand(): void ); } + /** + * @throws IOException + */ + public function testExpandUserNoHome(): void + { + $path = $this->getMock('~/file.ext', 'expandUser'); + $path->method('path')->willReturn('~/file.ext'); + + $this->builtin->expects(self::once())->method('getHome')->willReturn(''); + + $this->expectException(IOException::class); + + $path->expandUser(); + } + public function testExpandVars(): void { $path = $this->getMock('~/file.ext', 'expandVars'); @@ -3801,6 +3953,7 @@ public function testChunksWithClosingError(): void public function testIsAbs(): void { $path = $this->getMock('/foo/file.ext', 'isAbs'); + $path->method('path')->willReturn('/foo/file.ext'); $this->assertTrue( $path->isAbs() @@ -3810,12 +3963,33 @@ public function testIsAbs(): void public function testIsAbsWithRelative(): void { $path = $this->getMock('foo/file.ext', 'isAbs'); + $path->method('path')->willReturn('foo/file.ext'); $this->assertFalse( $path->isAbs() ); } + public function testIsAbsWithDrive(): void + { + $path = $this->getMock('c:/foo/file.ext', 'isAbs'); + $path->method('path')->willReturn('c:/foo/file.ext'); + + $this->assertTrue( + $path->isAbs() + ); + } + + public function testIsAbsWithUNC(): void + { + $path = $this->getMock('//server/computer/foo/file.ext', 'isAbs'); + $path->method('path')->willReturn('//server/computer/foo/file.ext'); + + $this->assertTrue( + $path->isAbs() + ); + } + /** * @throws IOException * @throws FileNotFoundException @@ -4538,6 +4712,7 @@ public function testSymLinkWithError(): void public function testParts(): void { $path = $this->getMock('foo/bar/file.ext', 'parts'); + $path->method('path')->willReturn('foo/bar/file.ext'); $this->assertEquals( ['foo', 'bar', 'file.ext'], @@ -4548,6 +4723,7 @@ public function testParts(): void public function testPartsWithLeadingSlash(): void { $path = $this->getMock('/foo/bar/file.ext', 'parts'); + $path->method('path')->willReturn('/foo/bar/file.ext'); $this->assertEquals( ['/', 'foo', 'bar', 'file.ext'], @@ -4555,6 +4731,30 @@ public function testPartsWithLeadingSlash(): void ); } + public function testPartsWithDrive(): void + { + BuiltinProxy::$DIRECTORY_SEPARATOR = '\\'; + + $path = $this->getMock('', 'parts'); + $path->method('path')->willReturn('c:\\bar\\file.ext'); + + $this->assertEquals( + ['c:', 'bar', 'file.ext'], + $path->parts() + ); + } + + public function testPartsWithUNC(): void + { + $path = $this->getMock('', 'parts'); + $path->method('path')->willReturn('//network/computer/home/dir'); + + $this->assertEquals( + ['//network/computer', 'home', 'dir'], + $path->parts() + ); + } + /** * @throws IOException * @throws FileNotFoundException