diff --git a/src/Psl/Filesystem/Exception/ExceptionInterface.php b/src/Psl/Filesystem/Exception/ExceptionInterface.php new file mode 100644 index 000000000..62c9103da --- /dev/null +++ b/src/Psl/Filesystem/Exception/ExceptionInterface.php @@ -0,0 +1,11 @@ + $files + * @param string|int $group + * + * @throws Exception\RuntimeException If unable to change the ownership for + * the given file. + * + * @internal + */ +function change_group(iterable $files, $group, bool $recursive = false): void +{ + foreach ($files as $file) { + if ($recursive && Filesystem\is_directory($file) && !Filesystem\is_symbolic_link($file)) { + change_group(Filesystem\read_directory($file), $group, true); + } + + if (Filesystem\is_symbolic_link($file)) { + $fun = static fn(): bool => lchgrp($file, $group); + } else { + $fun = static fn(): bool => chgrp($file, $group); + } + + [$success, $error] = Internal\box($fun); + if (!$success) { + throw new Exception\RuntimeException(Str\format( + 'Failed to change the group for file "%s": %s', + $file, + $error ?? 'internal error.', + )); + } + } +} diff --git a/src/Psl/Filesystem/Internal/change_owner.php b/src/Psl/Filesystem/Internal/change_owner.php new file mode 100644 index 000000000..199d89081 --- /dev/null +++ b/src/Psl/Filesystem/Internal/change_owner.php @@ -0,0 +1,46 @@ + $files + * @param string|int $user + * + * @throws Exception\RuntimeException If unable to change the ownership for + * the given file. + * + * @internal + */ +function change_owner(iterable $files, $user, bool $recursive = false): void +{ + foreach ($files as $file) { + if ($recursive && Filesystem\is_directory($file) && !Filesystem\is_symbolic_link($file)) { + change_owner(Filesystem\read_directory($file), $user, true); + } + + if (Filesystem\is_symbolic_link($file)) { + $fun = static fn(): bool => lchown($file, $user); + } else { + $fun = static fn(): bool => chown($file, $user); + } + + [$success, $error] = Internal\box($fun); + if (!$success) { + throw new Exception\RuntimeException(Str\format( + 'Failed to change owner for file "%s": %s', + $file, + $error ?? 'internal error.', + )); + } + } +} diff --git a/src/Psl/Filesystem/Internal/change_permissions.php b/src/Psl/Filesystem/Internal/change_permissions.php new file mode 100644 index 000000000..aa41b00bf --- /dev/null +++ b/src/Psl/Filesystem/Internal/change_permissions.php @@ -0,0 +1,40 @@ + $files + * + * @throws Exception\RuntimeException If unable to change the ownership for + * the given file. + * + * @internal + */ +function change_permissions(iterable $files, int $permission, bool $recursive = false): void +{ + foreach ($files as $file) { + if ($recursive && Filesystem\is_directory($file) && !Filesystem\is_symbolic_link($file)) { + change_permissions(Filesystem\read_directory($file), $permission, true); + } + + [$success, $error] = Internal\box(static fn(): bool => chmod($file, $permission)); + // @codeCoverageIgnoreStart + if (!$success) { + throw new Exception\RuntimeException(Str\format( + 'Failed to change permissions for file "%s": %s', + $file, + $error ?? 'internal error.', + )); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/src/Psl/Filesystem/canonicalize.php b/src/Psl/Filesystem/canonicalize.php new file mode 100644 index 000000000..7342a3f0c --- /dev/null +++ b/src/Psl/Filesystem/canonicalize.php @@ -0,0 +1,18 @@ + fopen($source, 'rb')); + // @codeCoverageIgnoreStart + if (false === $source_stream) { + throw new Exception\RuntimeException('Failed to open $source file for reading'); + } + // @codeCoverageIgnoreEnd + + /** + * @psalm-suppress InvalidArgument - callable is not pure.. + */ + $destination_stream = Internal\suppress(static fn() => fopen($destination, 'wb')); + // @codeCoverageIgnoreStart + if (false === $destination_stream) { + throw new Exception\RuntimeException('Failed to open $destination file for writing.'); + } + // @codeCoverageIgnoreEnd + + $copied_bytes = stream_copy_to_stream($source_stream, $destination_stream); + fclose($source_stream); + fclose($destination_stream); + + $total_bytes = file_size($source); + + // preserve executable permission bits + change_permissions( + $destination, + get_permissions($destination) | (get_permissions($source) & 0111) + ); + + // @codeCoverageIgnoreStart + if ($copied_bytes !== $total_bytes) { + throw new Exception\RuntimeException(Str\format( + 'Failed to copy the whole content of "%s" to "%s" ( %g of %g bytes copied ).', + $source, + $destination, + $copied_bytes, + $total_bytes + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_directory.php b/src/Psl/Filesystem/create_directory.php new file mode 100644 index 000000000..f9756a957 --- /dev/null +++ b/src/Psl/Filesystem/create_directory.php @@ -0,0 +1,32 @@ + mkdir($directory, $permissions, true) + ); + + // @codeCoverageIgnoreStart + if (false === $result && !is_directory($directory)) { + throw new Exception\RuntimeException(Str\format( + 'Failed to create directory "%s": %s.', + $directory, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_file.php b/src/Psl/Filesystem/create_file.php new file mode 100644 index 000000000..ba8f9a7f0 --- /dev/null +++ b/src/Psl/Filesystem/create_file.php @@ -0,0 +1,51 @@ + touch($filename); + } elseif (null === $access_time) { + $fun = static fn(): bool => touch($filename, $time); + } else { + $time = $time ?? $access_time; + + $fun = static fn(): bool => touch($filename, $time, Math\maxva($access_time, $time)); + } + + /** @psalm-suppress MissingThrowsDocblock */ + $directory = get_directory($filename); + if (!is_directory($directory)) { + create_directory($directory); + } + + [$result, $error_message] = Internal\box($fun); + // @codeCoverageIgnoreStart + if (false === $result && !is_file($filename)) { + throw new Exception\RuntimeException(Str\format( + 'Failed to create file "%s": %s.', + $filename, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_hard_link.php b/src/Psl/Filesystem/create_hard_link.php new file mode 100644 index 000000000..e918ad2ef --- /dev/null +++ b/src/Psl/Filesystem/create_hard_link.php @@ -0,0 +1,55 @@ + link($source, $destination)); + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to create hard link "%s" from "%s": %s.', + $destination, + $source, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_symbolic_link.php b/src/Psl/Filesystem/create_symbolic_link.php new file mode 100644 index 000000000..4069ae074 --- /dev/null +++ b/src/Psl/Filesystem/create_symbolic_link.php @@ -0,0 +1,53 @@ + symlink($source, $destination)); + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to create symbolic link "%s" from "%s": %s.', + $destination, + $source, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_temporary_file.php b/src/Psl/Filesystem/create_temporary_file.php new file mode 100644 index 000000000..91220f2a5 --- /dev/null +++ b/src/Psl/Filesystem/create_temporary_file.php @@ -0,0 +1,58 @@ + is_symbolic_link($node) + ); + + Iter\apply($symbolic_nodes, static fn(string $node) => delete_file($node)); + Iter\apply( + $nodes, + Fun\when( + static fn(string $node) => is_directory($node), + static fn(string $node) => delete_directory($node, true), + static fn(string $node) => delete_file($node), + ) + ); + } + + [$result, $error_message] = Internal\box(static fn() => rmdir($directory)); + // @codeCoverageIgnoreStart + if (false === $result && is_directory($directory)) { + throw new Exception\RuntimeException(Str\format( + 'Failed to delete directory "%s": %s.', + $directory, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/delete_file.php b/src/Psl/Filesystem/delete_file.php new file mode 100644 index 000000000..938799d9d --- /dev/null +++ b/src/Psl/Filesystem/delete_file.php @@ -0,0 +1,34 @@ + unlink($filename)); + // @codeCoverageIgnoreStart + if (false === $result && is_file($filename)) { + throw new Exception\RuntimeException(Str\format( + 'Failed to delete file "%s": %s.', + $filename, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/exists.php b/src/Psl/Filesystem/exists.php new file mode 100644 index 000000000..dab93a2d5 --- /dev/null +++ b/src/Psl/Filesystem/exists.php @@ -0,0 +1,22 @@ + filesize($filename)); + if (false === $size) { + throw new Exception\RuntimeException(Str\format( + 'Error reading the size of file "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $size; +} diff --git a/src/Psl/Filesystem/get_access_time.php b/src/Psl/Filesystem/get_access_time.php new file mode 100644 index 000000000..49b710fe6 --- /dev/null +++ b/src/Psl/Filesystem/get_access_time.php @@ -0,0 +1,40 @@ + fileatime($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the access time of "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/get_basename.php b/src/Psl/Filesystem/get_basename.php new file mode 100644 index 000000000..e526c81e0 --- /dev/null +++ b/src/Psl/Filesystem/get_basename.php @@ -0,0 +1,30 @@ + filectime($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the change time of "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/get_directory.php b/src/Psl/Filesystem/get_directory.php new file mode 100644 index 000000000..52f165bbf --- /dev/null +++ b/src/Psl/Filesystem/get_directory.php @@ -0,0 +1,33 @@ + 0, '$levels must be a positive integer, %d given.', $levels); + + return dirname($path, $levels); +} diff --git a/src/Psl/Filesystem/get_extension.php b/src/Psl/Filesystem/get_extension.php new file mode 100644 index 000000000..9d7bfb363 --- /dev/null +++ b/src/Psl/Filesystem/get_extension.php @@ -0,0 +1,19 @@ + filegroup($filename) + ); + + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve group of file "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + + return $result; +} diff --git a/src/Psl/Filesystem/get_inode.php b/src/Psl/Filesystem/get_inode.php new file mode 100644 index 000000000..02773c2e1 --- /dev/null +++ b/src/Psl/Filesystem/get_inode.php @@ -0,0 +1,40 @@ + fileinode($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the inode of "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/get_modification_time.php b/src/Psl/Filesystem/get_modification_time.php new file mode 100644 index 000000000..f86dab1f8 --- /dev/null +++ b/src/Psl/Filesystem/get_modification_time.php @@ -0,0 +1,41 @@ + filemtime($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the modification time of "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/get_owner.php b/src/Psl/Filesystem/get_owner.php new file mode 100644 index 000000000..321eea223 --- /dev/null +++ b/src/Psl/Filesystem/get_owner.php @@ -0,0 +1,38 @@ + fileowner($filename) + ); + + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve owner of file "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + + return $result; +} diff --git a/src/Psl/Filesystem/get_permissions.php b/src/Psl/Filesystem/get_permissions.php new file mode 100644 index 000000000..682441715 --- /dev/null +++ b/src/Psl/Filesystem/get_permissions.php @@ -0,0 +1,40 @@ + fileperms($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve permissions of file "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/is_directory.php b/src/Psl/Filesystem/is_directory.php new file mode 100644 index 000000000..fee1fa922 --- /dev/null +++ b/src/Psl/Filesystem/is_directory.php @@ -0,0 +1,25 @@ + + * + * @throws Psl\Exception\InvariantViolationException If the directory specified by + * $directory does not exist, or is not readable. + */ +function read_directory(string $directory): array +{ + Psl\invariant(exists($directory), '$directory does not exists.'); + Psl\invariant(is_directory($directory), '$directory is not a directory.'); + Psl\invariant(is_readable($directory), '$directory is not readable.'); + + /** @var list */ + return Vec\values(new FilesystemIterator( + $directory, + FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS + )); +} diff --git a/src/Psl/Filesystem/read_file.php b/src/Psl/Filesystem/read_file.php new file mode 100644 index 000000000..3e04c1963 --- /dev/null +++ b/src/Psl/Filesystem/read_file.php @@ -0,0 +1,51 @@ + file_get_contents($file, false, null, $offset) + ); + } else { + [$content, $error] = Internal\box( + static fn() => file_get_contents($file, false, null, $offset, $length) + ); + } + + // @codeCoverageIgnoreStart + if (false === $content || null !== $error) { + throw new Exception\RuntimeException(Str\format( + 'Failed to read file "%s": %s.', + $file, + $error ?? 'internal error', + )); + } + // @codeCoverageIgnoreEnd + + return $content; +} diff --git a/src/Psl/Filesystem/read_symbolic_link.php b/src/Psl/Filesystem/read_symbolic_link.php new file mode 100644 index 000000000..da7e05889 --- /dev/null +++ b/src/Psl/Filesystem/read_symbolic_link.php @@ -0,0 +1,44 @@ + readlink($symbolic_link) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the target of symbolic link "%s": %s', + $symbolic_link, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/write_file.php b/src/Psl/Filesystem/write_file.php new file mode 100644 index 000000000..802dbcf53 --- /dev/null +++ b/src/Psl/Filesystem/write_file.php @@ -0,0 +1,65 @@ + file_put_contents($file, $content, $flags) + ); + + // @codeCoverageIgnoreStart + if (false === $written || null !== $error) { + throw new Exception\RuntimeException(Str\format( + 'Failed to write to file "%s": %s.', + $file, + $error ?? 'internal error', + )); + } + + $length = Str\Byte\length($content); + if ($written !== $length) { + throw new Exception\RuntimeException(Str\format( + 'Failed to write the whole content to "%s" ( %g of %g bytes written ).', + $file, + $written, + $length, + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php index 5ca5ac58b..4a38e4b9a 100644 --- a/src/Psl/Internal/Loader.php +++ b/src/Psl/Internal/Loader.php @@ -48,6 +48,7 @@ final class Loader 'Psl\Str\ALPHABET_ALPHANUMERIC', 'Psl\Password\DEFAULT_ALGORITHM', 'Psl\Password\BCRYPT_ALGORITHM', + 'Psl\Filesystem\SEPARATOR', ]; public const FUNCTIONS = [ @@ -141,6 +142,7 @@ final class Loader 'Psl\Internal\boolean', 'Psl\Internal\type', 'Psl\Internal\suppress', + 'Psl\Internal\box', 'Psl\Internal\validate_offset', 'Psl\Internal\validate_offset_lower_bound', 'Psl\Internal\internal_encoding', @@ -459,6 +461,44 @@ final class Loader 'Psl\Shell\escape_command', 'Psl\Shell\escape_argument', 'Psl\Shell\execute', + 'Psl\Filesystem\Internal\change_group', + 'Psl\Filesystem\Internal\change_owner', + 'Psl\Filesystem\Internal\change_permissions', + 'Psl\Filesystem\change_group', + 'Psl\Filesystem\change_owner', + 'Psl\Filesystem\change_permissions', + 'Psl\Filesystem\copy', + 'Psl\Filesystem\create_directory', + 'Psl\Filesystem\create_file', + 'Psl\Filesystem\delete_directory', + 'Psl\Filesystem\delete_file', + 'Psl\Filesystem\exists', + 'Psl\Filesystem\file_size', + 'Psl\Filesystem\get_group', + 'Psl\Filesystem\get_owner', + 'Psl\Filesystem\get_permissions', + 'Psl\Filesystem\get_basename', + 'Psl\Filesystem\get_directory', + 'Psl\Filesystem\get_extension', + 'Psl\Filesystem\get_filename', + 'Psl\Filesystem\is_directory', + 'Psl\Filesystem\is_file', + 'Psl\Filesystem\is_symbolic_link', + 'Psl\Filesystem\is_readable', + 'Psl\Filesystem\is_writable', + 'Psl\Filesystem\canonicalize', + 'Psl\Filesystem\is_executable', + 'Psl\Filesystem\read_directory', + 'Psl\Filesystem\read_file', + 'Psl\Filesystem\read_symbolic_link', + 'Psl\Filesystem\write_file', + 'Psl\Filesystem\create_temporary_file', + 'Psl\Filesystem\create_hard_link', + 'Psl\Filesystem\create_symbolic_link', + 'Psl\Filesystem\get_access_time', + 'Psl\Filesystem\get_change_time', + 'Psl\Filesystem\get_modification_time', + 'Psl\Filesystem\get_inode', ]; public const INTERFACES = [ @@ -481,6 +521,7 @@ final class Loader 'Psl\Type\Exception\ExceptionInterface', 'Psl\Regex\Exception\ExceptionInterface', 'Psl\Shell\Exception\ExceptionInterface', + 'Psl\Filesystem\Exception\ExceptionInterface', ]; public const TRAITS = []; @@ -532,6 +573,7 @@ final class Loader 'Psl\Shell\Exception\FailedExecutionException', 'Psl\Shell\Exception\RuntimeException', 'Psl\Shell\Exception\PossibleAttackException', + 'Psl\Filesystem\Exception\RuntimeException', ]; private const TYPE_CONSTANTS = 1; diff --git a/src/Psl/Internal/box.php b/src/Psl/Internal/box.php new file mode 100644 index 000000000..2c2933c2c --- /dev/null +++ b/src/Psl/Internal/box.php @@ -0,0 +1,53 @@ +cacheDirectory = Type\string()->assert(Filesystem\canonicalize(Str\join([ + __DIR__, '..', '..', '.cache' + ], Filesystem\SEPARATOR))); + + $this->directory = Str\join([$this->cacheDirectory, $this->function], Filesystem\SEPARATOR); + Filesystem\create_directory($this->directory); + $this->directoryPermissions = Filesystem\get_permissions($this->directory) & 0777; + + static::assertTrue(Filesystem\exists($this->directory)); + static::assertTrue(Filesystem\is_directory($this->directory)); + } + + protected function tearDown(): void + { + Filesystem\change_permissions($this->directory, $this->directoryPermissions); + Filesystem\delete_directory($this->directory, true); + + static::assertFalse(Filesystem\is_directory($this->directory)); + } + + protected static function runOnlyOnLinux(): void + { + if ('Linux' !== PHP_OS_FAMILY) { + static::markTestSkipped('Test can only be executed on linux.'); + } + } + + protected static function runOnlyUsingRoot(): void + { + $user = Env\get_var('USER'); + if (null === $user || 'root' === $user) { + return; + } + + static::markTestSkipped('Test can only be executed by a superuser.'); + } +} diff --git a/tests/Psl/Filesystem/CopyTest.php b/tests/Psl/Filesystem/CopyTest.php new file mode 100644 index 000000000..4a3b6689f --- /dev/null +++ b/tests/Psl/Filesystem/CopyTest.php @@ -0,0 +1,58 @@ +directory, 'hello.txt'], Filesystem\SEPARATOR); + $markdown_file = Str\join([$this->directory, 'hello.md'], Filesystem\SEPARATOR); + + Filesystem\write_file($text_file, 'Hello, World!'); + Filesystem\copy($text_file, $markdown_file); + + static::assertSame('Hello, World!', Filesystem\read_file($markdown_file)); + } + + public function testCopyOverwrite(): void + { + $text_file = Str\join([$this->directory, 'hello.txt'], Filesystem\SEPARATOR); + $markdown_file = Str\join([$this->directory, 'hello.md'], Filesystem\SEPARATOR); + + Filesystem\write_file($text_file, 'Hello, World!'); + Filesystem\write_file($markdown_file, '# Hello, World!'); + Filesystem\copy($text_file, $markdown_file); + + static::assertSame('Hello, World!', Filesystem\read_file($text_file)); + static::assertSame('# Hello, World!', Filesystem\read_file($markdown_file)); + + Filesystem\copy($text_file, $markdown_file, true); + + static::assertSame('Hello, World!', Filesystem\read_file($text_file)); + static::assertSame('Hello, World!', Filesystem\read_file($markdown_file)); + } + + public function testCopyExecutableBits(): void + { + $shell_file = Str\join([$this->directory, 'hello.sh'], Filesystem\SEPARATOR); + + Filesystem\create_file($shell_file); + Filesystem\change_permissions($shell_file, 0557); + + static::assertTrue(Filesystem\is_executable($shell_file)); + + $shell_file_copy = Str\join([$this->directory, 'hey.sh'], Filesystem\SEPARATOR); + + Filesystem\copy($shell_file, $shell_file_copy); + + static::assertTrue(Filesystem\is_executable($shell_file_copy)); + } +} diff --git a/tests/Psl/Filesystem/FileTest.php b/tests/Psl/Filesystem/FileTest.php new file mode 100644 index 000000000..28e3cb945 --- /dev/null +++ b/tests/Psl/Filesystem/FileTest.php @@ -0,0 +1,177 @@ +directory); + + static::assertTrue(Filesystem\is_file($file)); + static::assertSame($this->directory, Filesystem\get_directory($file)); + } + + public function testTemporaryFileWithPrefix(): void + { + $file = Filesystem\create_temporary_file($this->directory, 'foo'); + + static::assertTrue(Filesystem\is_file($file)); + static::assertSame($this->directory, Filesystem\get_directory($file)); + static::assertStringContainsString('foo', Filesystem\get_filename($file)); + } + + public function testTemporaryFileIsCreateInTempDirectoryByDefault(): void + { + $file = Filesystem\create_temporary_file(); + + static::assertSame(Env\temp_dir(), Filesystem\get_directory($file)); + } + + public function testTemporaryFileThrowsForNonExistingDirectory(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$directory is not a directory.'); + + Filesystem\create_temporary_file(__FILE__); + } + + public function testTemporaryFileThrowsForPrefixWithSeparator(): void + { + $prefix = Str\join(['a', 'b'], Filesystem\SEPARATOR); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$prefix should not contain a directory separator ( "' . Filesystem\SEPARATOR . '" ).'); + + Filesystem\create_temporary_file($this->directory, $prefix); + } + + public function testCreateFileAndParentDirectory(): void + { + $directory = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + $file = Str\join([$directory, 'write.txt'], Filesystem\SEPARATOR); + + static::assertFalse(Filesystem\is_directory($directory)); + + Filesystem\create_file($file); + + static::assertTrue(Filesystem\is_directory($directory)); + static::assertTrue(Filesystem\is_file($file)); + } + + public function testWriteFile(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + static::assertFileDoesNotExist($file); + + Filesystem\write_file($file, 'Hello!'); + + static::assertFileExists($file); + + static::assertStringEqualsFile($file, 'Hello!'); + + Filesystem\write_file($file, 'Hello'); + + static::assertStringEqualsFile($file, 'Hello'); + + Filesystem\write_file($file, ', World!', true); + + static::assertStringEqualsFile($file, 'Hello, World!'); + + Filesystem\delete_file($file); + } + + public function testWriteFileThrowsForDirectories(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$file is not a file.'); + + Filesystem\write_file($this->directory, 'hello'); + } + + public function testWriteFileThrowsForNonWritableFiles(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + Filesystem\create_file($file); + Filesystem\change_permissions($file, 0111); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$file is not writeable.'); + + Filesystem\write_file($file, 'hello'); + } + + public function testReadFile(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + Filesystem\write_file($file, 'PHP Standard Library'); + Filesystem\write_file($file, ' - a modern, consistent, centralized', true); + Filesystem\write_file($file, ' well-typed set of APIs for PHP programmers.', true); + + $content = Filesystem\read_file($file, 0, 20); + + static::assertSame('PHP Standard Library', $content); + + $content = Filesystem\read_file($file, 84, 16); + + static::assertSame('PHP programmers.', $content); + } + + public function testFileModificationAndAccessTime(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + $modification_time = time() - 3600; + $access_time = time() - 1800; + + Filesystem\create_file($file, $modification_time, $access_time); + + static::assertSame($modification_time, Filesystem\get_modification_time($file)); + static::assertSame($access_time, Filesystem\get_access_time($file)); + } + + public function testFileAccessTime(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + $access_time = time() - 1800; + + Filesystem\create_file($file, null, $access_time); + + static::assertSame($access_time, Filesystem\get_modification_time($file)); + static::assertSame($access_time, Filesystem\get_access_time($file)); + } + + public function testFileModificationTime(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + $modification_time = time() - 3600; + + Filesystem\create_file($file, $modification_time); + + static::assertSame($modification_time, Filesystem\get_modification_time($file)); + static::assertSame($modification_time, Filesystem\get_access_time($file)); + } + + public function testFileChangeTime(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + static::assertEqualsWithDelta(time(), Filesystem\get_change_time($file), 1.0); + } +} diff --git a/tests/Psl/Filesystem/GroupOwnershipTest.php b/tests/Psl/Filesystem/GroupOwnershipTest.php new file mode 100644 index 000000000..75d12f854 --- /dev/null +++ b/tests/Psl/Filesystem/GroupOwnershipTest.php @@ -0,0 +1,49 @@ +directory, 'foo'], Filesystem\SEPARATOR); + $bar = Str\join([$this->directory, 'bar'], Filesystem\SEPARATOR); + + Filesystem\create_file($foo); + Filesystem\create_file($bar); + + $group = Filesystem\get_group($foo); + + Filesystem\change_group($this->directory, $group, true); + + static::assertSame($group, Filesystem\get_group($foo)); + static::assertSame($group, Filesystem\get_group($bar)); + static::assertSame($group, Filesystem\get_group($this->directory)); + } + + public function testChangeGroupThrowsIfFileDoesNotExist(): void + { + static::runOnlyOnLinux(); + static::runOnlyUsingRoot(); + + $foo = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + + $group = Filesystem\get_group($this->directory); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$filename does not exist.'); + + Filesystem\change_group($foo, $group, true); + } +} diff --git a/tests/Psl/Filesystem/LinkTest.php b/tests/Psl/Filesystem/LinkTest.php new file mode 100644 index 000000000..7179160c0 --- /dev/null +++ b/tests/Psl/Filesystem/LinkTest.php @@ -0,0 +1,187 @@ +directory, 'write.txt'], Filesystem\SEPARATOR); + $symlink = Str\join([$this->directory, 'symlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_symbolic_link($file, $symlink); + + static::assertTrue(Filesystem\exists($symlink)); + static::assertTrue(Filesystem\is_symbolic_link($symlink)); + + static::assertSame($file, Filesystem\read_symbolic_link($symlink)); + } + + public function testSymbolicLinkAlreadyExists(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $symlink = Str\join([$this->directory, 'symlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_symbolic_link($file, $symlink); + + static::assertTrue(Filesystem\exists($symlink)); + static::assertTrue(Filesystem\is_symbolic_link($symlink)); + + static::assertSame($file, Filesystem\read_symbolic_link($symlink)); + + Filesystem\create_symbolic_link($file, $symlink); + + static::assertTrue(Filesystem\exists($symlink)); + static::assertTrue(Filesystem\is_symbolic_link($symlink)); + + static::assertSame($file, Filesystem\read_symbolic_link($symlink)); + } + + public function testSymbolicLinkOverwrite(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $symbolic_link = Str\join([$this->directory, 'symbolic_link.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + Filesystem\create_file($symbolic_link); + + static::assertFalse(Filesystem\is_symbolic_link($symbolic_link)); + + Filesystem\create_symbolic_link($file, $symbolic_link); + + static::assertTrue(Filesystem\is_symbolic_link($symbolic_link)); + static::assertSame($file, Filesystem\read_symbolic_link($symbolic_link)); + + $file = Str\join([$this->directory, 'foo', 'bar'], Filesystem\SEPARATOR); + $symbolic_link = Str\join([$this->directory, 'foo', 'baz'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + Filesystem\create_directory($symbolic_link); + + static::assertFalse(Filesystem\is_symbolic_link($symbolic_link)); + + Filesystem\create_symbolic_link($file, $symbolic_link); + + static::assertTrue(Filesystem\is_symbolic_link($symbolic_link)); + static::assertSame($file, Filesystem\read_symbolic_link($symbolic_link)); + } + + public function testSymbolicLinkCreatesDestinationsDirectory(): void + { + self::runOnlyOnLinux(); + + $directory = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $symbolic_link = Str\join([$directory, 'symbolic.txt'], Filesystem\SEPARATOR); + + static::assertFalse(Filesystem\is_directory($directory)); + + Filesystem\create_file($file); + Filesystem\create_symbolic_link($file, $symbolic_link); + + static::assertTrue(Filesystem\is_directory($directory)); + } + + public function testHardLink(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertTrue(Filesystem\exists($hardlink)); + static::assertFalse(Filesystem\is_symbolic_link($hardlink)); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } + + public function testHardLinkAlreadyExists(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertTrue(Filesystem\exists($hardlink)); + static::assertFalse(Filesystem\is_symbolic_link($hardlink)); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } + + public function testHardLinkCreatesDestinationDirectory(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $destination_directory = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + $hardlink = Str\join([$destination_directory, 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + static::assertFalse(Filesystem\is_directory($destination_directory)); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertTrue(Filesystem\is_directory($destination_directory)); + static::assertTrue(Filesystem\exists($hardlink)); + static::assertFalse(Filesystem\is_symbolic_link($hardlink)); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } + + public function testHardLinkOverwrite(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + Filesystem\create_file($hardlink); + + static::assertNotSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + + $file = Str\join([$this->directory, 'foo', 'bar'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'foo', 'baz'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + Filesystem\create_directory($hardlink); + + static::assertNotSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } + + public function testHardLinkCreatesDestinationsDirectory(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'baz', 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } +} diff --git a/tests/Psl/Filesystem/OwnershipTest.php b/tests/Psl/Filesystem/OwnershipTest.php new file mode 100644 index 000000000..aa953989d --- /dev/null +++ b/tests/Psl/Filesystem/OwnershipTest.php @@ -0,0 +1,49 @@ +directory, 'foo'], Filesystem\SEPARATOR); + $bar = Str\join([$this->directory, 'bar'], Filesystem\SEPARATOR); + + Filesystem\create_file($foo); + Filesystem\create_file($bar); + + $owner = Filesystem\get_owner($foo); + + Filesystem\change_owner($this->directory, $owner, true); + + static::assertSame($owner, Filesystem\get_owner($foo)); + static::assertSame($owner, Filesystem\get_owner($bar)); + static::assertSame($owner, Filesystem\get_owner($this->directory)); + } + + public function testChangeOwnerThrowsIfFileDoesNotExist(): void + { + static::runOnlyOnLinux(); + static::runOnlyUsingRoot(); + + $foo = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + + $owner = Filesystem\get_owner($this->directory); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$filename does not exist.'); + + Filesystem\change_owner($foo, $owner, true); + } +} diff --git a/tests/Psl/Filesystem/PathTest.php b/tests/Psl/Filesystem/PathTest.php new file mode 100644 index 000000000..af9c9fad5 --- /dev/null +++ b/tests/Psl/Filesystem/PathTest.php @@ -0,0 +1,67 @@ +expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$levels must be a positive integer, -3 given.'); + + Filesystem\get_directory('/home/azjezz/Projects/psl/src/Psl', -3); + } +} diff --git a/tests/Psl/Filesystem/PermissionsTest.php b/tests/Psl/Filesystem/PermissionsTest.php new file mode 100644 index 000000000..2cb690d92 --- /dev/null +++ b/tests/Psl/Filesystem/PermissionsTest.php @@ -0,0 +1,77 @@ +directory, 'foo.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($filename); + + $permissions = Filesystem\get_permissions($filename) & 0777; + + try { + Filesystem\change_permissions($filename, 0444); + + static::assertTrue(Filesystem\is_readable($filename)); + static::assertFalse(Filesystem\is_writable($filename)); + static::assertFalse(Filesystem\is_executable($filename)); + + Filesystem\change_permissions($filename, 0222); + + static::assertTrue(Filesystem\is_writable($filename)); + static::assertFalse(Filesystem\is_readable($filename)); + static::assertFalse(Filesystem\is_executable($filename)); + + Filesystem\change_permissions($filename, 0111); + + static::assertTrue(Filesystem\is_executable($filename)); + static::assertFalse(Filesystem\is_writable($filename)); + static::assertFalse(Filesystem\is_readable($filename)); + + Filesystem\change_permissions($filename, 0666); + + static::assertTrue(Filesystem\is_writable($filename)); + static::assertTrue(Filesystem\is_readable($filename)); + static::assertFalse(Filesystem\is_executable($filename)); + + Filesystem\change_permissions($filename, 0777); + + static::assertTrue(Filesystem\is_writable($filename)); + static::assertTrue(Filesystem\is_readable($filename)); + static::assertTrue(Filesystem\is_executable($filename)); + } finally { + Filesystem\change_permissions($filename, $permissions); + } + } + + public function testChangePermissionsRecursively(): void + { + $directory = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + $filename = Str\join([$this->directory, 'foo', 'bar.txt'], Filesystem\SEPARATOR); + + Filesystem\create_directory($directory); + Filesystem\create_file($filename); + + Filesystem\change_permissions($filename, 0666); + + static::assertFalse(Filesystem\is_executable($filename)); + static::assertTrue(Filesystem\is_writable($filename)); + static::assertTrue(Filesystem\is_readable($filename)); + + Filesystem\change_permissions($directory, 0777, true); + + static::assertTrue(Filesystem\is_executable($filename)); + static::assertTrue(Filesystem\is_writable($filename)); + static::assertTrue(Filesystem\is_readable($filename)); + } +} diff --git a/tests/Psl/Filesystem/ReadDirectoryTest.php b/tests/Psl/Filesystem/ReadDirectoryTest.php new file mode 100644 index 000000000..f9842920a --- /dev/null +++ b/tests/Psl/Filesystem/ReadDirectoryTest.php @@ -0,0 +1,73 @@ +directory, 'hello.txt' + ], Filesystem\SEPARATOR)); + + Filesystem\create_directory(Str\join([ + $this->directory, 'foo' + ], Filesystem\SEPARATOR)); + + $children = Filesystem\read_directory($this->directory); + + static::assertCount(2, $children); + static::assertSame([ + Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR), + Str\join([$this->directory, 'hello.txt'], Filesystem\SEPARATOR), + ], Vec\sort($children)); + } + + public function testReadDirectoryThrowsIfDirectoryDoesNotExist(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$directory does not exists.'); + + Filesystem\read_directory(Env\temp_dir() . '/foo-bar-baz'); + } + + public function testReadDirectoryThrowsIfNotDirectory(): void + { + $filename = Str\join([ + $this->directory, 'hello.txt' + ], Filesystem\SEPARATOR); + + Filesystem\create_file($filename); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$directory is not a directory.'); + + Filesystem\read_directory($filename); + } + + public function testReadDirectoryThrowsIfNotReadable(): void + { + Filesystem\change_permissions($this->directory, 0077); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$directory is not readable.'); + + try { + Filesystem\read_directory($this->directory); + } finally { + // restore $this->directory permissions, otherwise we won't + // be able to delete it. + Filesystem\change_permissions($this->directory, 0777); + } + } +}