Windows path
parent
6ddb3955af
commit
f94d565bb0
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Arokettu\Path;
|
||||
|
||||
use Arokettu\Path\Helpers\DataTypeHelper;
|
||||
|
||||
final class WindowsPath 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 (preg_match('@^[A-Za-z]:[\\\\/]@', $path, $matches)) {
|
||||
// DOS path
|
||||
$prefix = ucfirst($matches[0]); // uppercase drive letter
|
||||
$restOfPath = substr($path, \strlen($prefix));
|
||||
|
||||
$this->parseDOS($prefix, $restOfPath, $strict);
|
||||
} elseif (preg_match('@^\\\\\\\\[.?]\\\\([^\\\\/]+)\\\\@', $path, $matches)) {
|
||||
// UNC local volume
|
||||
$prefix = $matches[0];
|
||||
|
||||
// if the volume is a drive letter, uppercase it
|
||||
if (preg_match('/^[a-zA-Z]:$/', $matches[1])) {
|
||||
// \\?\C:\
|
||||
$prefix[4] = strtoupper($prefix[4]);
|
||||
}
|
||||
|
||||
$restOfPath = substr($path, \strlen($prefix));
|
||||
|
||||
$this->parseUNC($prefix, $restOfPath);
|
||||
} elseif (preg_match('@^\\\\\\\\[^.?\\\\/][^\\\\/]*\\\\|\\\\\\\\[.?][^\\\\/][^\\\\/]+\\\\@', $path, $matches)) {
|
||||
$prefix = $matches[0];
|
||||
$restOfPath = substr($path, \strlen($prefix));
|
||||
|
||||
$this->parseUNC($prefix, $restOfPath);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Unrecognized Windows path');
|
||||
}
|
||||
}
|
||||
|
||||
private function parseDOS(string $prefix, string $restOfPath, bool $strict): void
|
||||
{
|
||||
// forward slash is also a valid path separator in DOS paths
|
||||
// just also parse backslashes
|
||||
$components = explode('/', $restOfPath);
|
||||
$components = array_merge(
|
||||
...array_map(fn ($a) => explode('\\', $a), $components)
|
||||
);
|
||||
|
||||
$parsedComponents = $this->normalize($components);
|
||||
|
||||
if ($parsedComponents[0] === '..') {
|
||||
if ($strict) {
|
||||
throw new \InvalidArgumentException('Path went beyond root');
|
||||
}
|
||||
|
||||
do {
|
||||
$parsedComponents->shift();
|
||||
} while ($parsedComponents[0] === '..');
|
||||
}
|
||||
|
||||
// normalize prefix: use backslash
|
||||
$this->prefix = strtr($prefix, ['/' => '\\']);
|
||||
$this->components = $parsedComponents;
|
||||
}
|
||||
|
||||
// no $strict param, UNC is always strict
|
||||
private function parseUNC(string $prefix, string $restOfPath): void
|
||||
{
|
||||
if (str_contains($restOfPath, '/')) {
|
||||
throw new \InvalidArgumentException('Slashes are not allowed in UNC paths');
|
||||
}
|
||||
|
||||
$components = explode('\\', $restOfPath);
|
||||
|
||||
foreach ($components as $component) {
|
||||
if ($component === '.' || $component === '..') {
|
||||
throw new \InvalidArgumentException('. and .. are not allowed in UNC paths');
|
||||
}
|
||||
}
|
||||
|
||||
$this->prefix = $prefix;
|
||||
$this->components = DataTypeHelper::iterableToNewListInstance($components);
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->prefix . \iter\join('\\', $this->components);
|
||||
}
|
||||
|
||||
protected function buildRelative(\SplDoublyLinkedList $components): RelativePathInterface
|
||||
{
|
||||
$path = new RelativePath('.', true);
|
||||
$path->components = $components;
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Arokettu\Path\Tests;
|
||||
|
||||
use Arokettu\Path\RelativePath;
|
||||
use Arokettu\Path\UnixPath;
|
||||
use Arokettu\Path\WindowsPath;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class WindowsPathTest extends TestCase
|
||||
{
|
||||
public function testCreate(): void
|
||||
{
|
||||
// simple dos path
|
||||
$path = WindowsPath::parse('C:\\I\\Am\\Windows\\Path');
|
||||
self::assertEquals('C:\\I\\Am\\Windows\\Path', $path->toString());
|
||||
self::assertEquals('C:\\', $path->getPrefix());
|
||||
|
||||
// dos path to be normalized
|
||||
$path = WindowsPath::parse('z:/home/arokettu/wine/./path\\Games\\Something/../../Tools/file.exe');
|
||||
self::assertEquals('Z:\\home\\arokettu\\wine\\path\\Tools\\file.exe', $path->toString());
|
||||
self::assertEquals('Z:\\', $path->getPrefix());
|
||||
|
||||
// non strictly valid dos path
|
||||
$path = WindowsPath::parse('C:\\..\\..\\I\\Am\\Windows\\Path');
|
||||
self::assertEquals('C:\\I\\Am\\Windows\\Path', $path->toString());
|
||||
self::assertEquals('C:\\', $path->getPrefix());
|
||||
|
||||
// local unc path (drive letter)
|
||||
$path = WindowsPath::parse('\\\\.\\c:\\windows\\win.ini');
|
||||
self::assertEquals('\\\\.\\C:\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\.\\C:\\', $path->getPrefix());
|
||||
|
||||
// local unc path (guid)
|
||||
$path = WindowsPath::parse('\\\\?\\Volume{D4AF2203-A75B-4CB1-9B93-AE78EB9A50A5}\\windows\\win.ini');
|
||||
self::assertEquals('\\\\?\\Volume{D4AF2203-A75B-4CB1-9B93-AE78EB9A50A5}\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\?\\Volume{D4AF2203-A75B-4CB1-9B93-AE78EB9A50A5}\\', $path->getPrefix());
|
||||
|
||||
// remote share
|
||||
$path = WindowsPath::parse('\\\\MYPC\\c$\\windows\\win.ini');
|
||||
self::assertEquals('\\\\MYPC\\c$\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\MYPC\\', $path->getPrefix());
|
||||
|
||||
// remote share single char
|
||||
$path = WindowsPath::parse('\\\\M\\c$\\windows\\win.ini');
|
||||
self::assertEquals('\\\\M\\c$\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\M\\', $path->getPrefix());
|
||||
|
||||
// remote share starts with dot
|
||||
$path = WindowsPath::parse('\\\\.MYPC\\c$\\windows\\win.ini');
|
||||
self::assertEquals('\\\\.MYPC\\c$\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\.MYPC\\', $path->getPrefix());
|
||||
}
|
||||
|
||||
public function testCreateInvalidNotAWinPath(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unrecognized Windows path');
|
||||
|
||||
WindowsPath::parse('/home/arokettu');
|
||||
}
|
||||
|
||||
public function testCreateInvalidRelativeWithALetter(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unrecognized Windows path');
|
||||
|
||||
// technically valid but usually useless
|
||||
WindowsPath::parse('c:windows\win.ini');
|
||||
}
|
||||
|
||||
public function testCreateInvalidUNCWithSlash(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Slashes are not allowed in UNC paths');
|
||||
|
||||
// technically valid but usually useless
|
||||
WindowsPath::parse('\\\\MYPC\\c$/Windows');
|
||||
}
|
||||
|
||||
public function testCreateInvalidUNCWithDots(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('. and .. are not allowed in UNC paths');
|
||||
|
||||
// technically valid but usually useless
|
||||
WindowsPath::parse('\\\\MYPC\\c$\\Windows\\..\\Users\\');
|
||||
}
|
||||
|
||||
public function testCreateStrict(): void
|
||||
{
|
||||
// simple dos path
|
||||
$path = WindowsPath::parse('C:\\I\\Am\\Windows\\Path', true);
|
||||
self::assertEquals('C:\\I\\Am\\Windows\\Path', $path->toString());
|
||||
self::assertEquals('C:\\', $path->getPrefix());
|
||||
|
||||
// dos path to be normalized
|
||||
$path = WindowsPath::parse('z:/home/arokettu/wine/./path\\Games\\Something/../../Tools/file.exe', true);
|
||||
self::assertEquals('Z:\\home\\arokettu\\wine\\path\\Tools\\file.exe', $path->toString());
|
||||
self::assertEquals('Z:\\', $path->getPrefix());
|
||||
|
||||
// local unc path (drive letter)
|
||||
$path = WindowsPath::parse('\\\\.\\c:\\windows\\win.ini', true);
|
||||
self::assertEquals('\\\\.\\C:\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\.\\C:\\', $path->getPrefix());
|
||||
|
||||
// local unc path (guid)
|
||||
$path = WindowsPath::parse('\\\\?\\Volume{D4AF2203-A75B-4CB1-9B93-AE78EB9A50A5}\\windows\\win.ini', true);
|
||||
self::assertEquals('\\\\?\\Volume{D4AF2203-A75B-4CB1-9B93-AE78EB9A50A5}\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\?\\Volume{D4AF2203-A75B-4CB1-9B93-AE78EB9A50A5}\\', $path->getPrefix());
|
||||
|
||||
// remote share
|
||||
$path = WindowsPath::parse('\\\\MYPC\\c$\\windows\\win.ini', true);
|
||||
self::assertEquals('\\\\MYPC\\c$\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\MYPC\\', $path->getPrefix());
|
||||
|
||||
// remote share single char
|
||||
$path = WindowsPath::parse('\\\\M\\c$\\windows\\win.ini', true);
|
||||
self::assertEquals('\\\\M\\c$\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\M\\', $path->getPrefix());
|
||||
|
||||
// remote share starts with dot
|
||||
$path = WindowsPath::parse('\\\\.MYPC\\c$\\windows\\win.ini', true);
|
||||
self::assertEquals('\\\\.MYPC\\c$\\windows\\win.ini', $path->toString());
|
||||
self::assertEquals('\\\\.MYPC\\', $path->getPrefix());
|
||||
}
|
||||
|
||||
public function testCreateStrictInvalid(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Path went beyond root');
|
||||
|
||||
$path = WindowsPath::parse('C:\\..\\..\\I\\Am\\Windows\\Path', true);
|
||||
self::assertEquals('C:\\I\\Am\\Windows\\Path', $path->toString());
|
||||
self::assertEquals('C:\\', $path->getPrefix());
|
||||
}
|
||||
|
||||
public function testResolveRelative(): void
|
||||
{
|
||||
$path = WindowsPath::parse('c:\\i\\am\\test\\windows\\path');
|
||||
|
||||
$rp = RelativePath::windows('\\i\\am\\test\\relative\\path');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\relative\\path',
|
||||
$path->resolveRelative($rp)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('i\\am\\test\\relative\\path');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\windows\\path\\i\\am\\test\\relative\\path',
|
||||
$path->resolveRelative($rp)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('..\\..\\i\\am\\test\\relative\\path');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\i\\am\\test\\relative\\path',
|
||||
$path->resolveRelative($rp)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('..\\..\\..\\..\\..\\..\\..\\..\\i\\am\\test\\relative\\path');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\relative\\path',
|
||||
$path->resolveRelative($rp)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('..');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\windows',
|
||||
$path->resolveRelative($rp)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('.');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\windows\\path',
|
||||
$path->resolveRelative($rp)->toString()
|
||||
);
|
||||
}
|
||||
|
||||
public function testResolveRelativeStrict(): void
|
||||
{
|
||||
$path = WindowsPath::parse('c:\\i\\am\\test\\windows\\path');
|
||||
|
||||
$rp = RelativePath::windows('\\i\\am\\test\\relative\\path');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\relative\\path',
|
||||
$path->resolveRelative($rp, true)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('i\\am\\test\\relative\\path');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\windows\\path\\i\\am\\test\\relative\\path',
|
||||
$path->resolveRelative($rp, true)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('..\\..\\i\\am\\test\\relative\\path');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\i\\am\\test\\relative\\path',
|
||||
$path->resolveRelative($rp, true)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('..');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\windows',
|
||||
$path->resolveRelative($rp, true)->toString()
|
||||
);
|
||||
|
||||
$rp = RelativePath::windows('.');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\windows\\path',
|
||||
$path->resolveRelative($rp, true)->toString()
|
||||
);
|
||||
}
|
||||
|
||||
public function testResolveRelativeStrictInvalid(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Relative path went beyond root');
|
||||
|
||||
$path = WindowsPath::parse('c:\\i\\am\\test\\windows\\path');
|
||||
|
||||
$rp = RelativePath::windows('..\\..\\..\\..\\..\\..\\..\\..\\i\\am\\test\\relative\\path');
|
||||
self::assertEquals(
|
||||
'C:\\i\\am\\test\\relative\\path',
|
||||
$path->resolveRelative($rp, true)->toString()
|
||||
);
|
||||
}
|
||||
|
||||
public function testMakeRelative(): void
|
||||
{
|
||||
$paths = [
|
||||
WindowsPath::parse('C:\\i\\am\\test\\windows\\path'),
|
||||
WindowsPath::parse('C:\\i\\aM\\anOther\\Windows\\TEST\\path'),
|
||||
WindowsPath::parse('C:\\i\\Am'),
|
||||
WindowsPath::parse('C:\\I\\AM\\TEST\\WINDOWS\\PATH'), // different case, same path
|
||||
];
|
||||
|
||||
// simple latin case insensitive
|
||||
$equalFunction = fn ($a, $b) => strtoupper($a) === strtoupper($b);
|
||||
|
||||
$matrix = [
|
||||
[
|
||||
'.',
|
||||
'..\\..\\..\\anOther\\Windows\\TEST\\path',
|
||||
'..\\..\\..',
|
||||
'.',
|
||||
],
|
||||
[
|
||||
'..\\..\\..\\..\\test\\windows\\path',
|
||||
'.',
|
||||
'..\\..\\..\\..',
|
||||
'..\\..\\..\\..\\TEST\\WINDOWS\\PATH',
|
||||
],
|
||||
[
|
||||
'test\\windows\\path',
|
||||
'anOther\\Windows\\TEST\\path',
|
||||
'.',
|
||||
'TEST\\WINDOWS\\PATH',
|
||||
],
|
||||
[
|
||||
'.',
|
||||
'..\\..\\..\\anOther\\Windows\\TEST\\path',
|
||||
'..\\..\\..',
|
||||
'.',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($paths as $bpi => $bp) {
|
||||
foreach ($paths as $tpi => $tp) {
|
||||
$result = $matrix[$bpi][$tpi];
|
||||
|
||||
self::assertEquals($result, $bp->makeRelative($tp, $equalFunction)->toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testMakeRelativeWrongType(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('You can only make relative path from paths of same type and same prefix');
|
||||
|
||||
WindowsPath::parse('C:\\Windows')->makeRelative(UnixPath::parse('/i/am/test/unix/path'));
|
||||
}
|
||||
|
||||
public function testMakeRelativeWrongPrefix(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('You can only make relative path from paths of same type and same prefix');
|
||||
|
||||
WindowsPath::parse('C:\\Windows')->makeRelative(WindowsPath::parse('D:\\Windows'));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue