diff --git a/src/basename.php b/src/basename.php index e294907..038b516 100644 --- a/src/basename.php +++ b/src/basename.php @@ -1,4 +1,5 @@ []]); [$options, $args, $errors] = coreutilsValidateInput('basename', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('basename'); return $result; } $multiple = ($options['multiple'] ?? false) || array_key_exists('suffix', $options); if (!$args || (!$multiple && count($args) > 2)) { - coreutilsAddError($result, coreutilsError('basename', 'invalid-operands', - $args ? 'expected NAME [SUFFIX]; use -a for multiple names' : 'missing operand'), 2); + coreutilsAddError($result, coreutilsError( + 'basename', + 'invalid-operands', + $args ? 'expected NAME [SUFFIX]; use -a for multiple names' : 'missing operand' + ), 2); return $result; } $suffix = $options['suffix'] ?? ''; - if (!$multiple && count($args) === 2) $suffix = array_pop($args); + if (!$multiple && count($args) === 2) { + $suffix = array_pop($args); + } if (strpos($suffix, "\0") !== false) { - coreutilsAddError($result, coreutilsError('basename', 'invalid-value', - 'suffix must not contain NUL bytes'), 2); + coreutilsAddError($result, coreutilsError( + 'basename', + 'invalid-value', + 'suffix must not contain NUL bytes' + ), 2); return $result; } foreach ($args as $arg) { diff --git a/src/bootstrap.php b/src/bootstrap.php index b001ba5..42daa26 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -1,4 +1,5 @@ [], 'created' => [], 'skipped' => []]); [$options, $args, $errors] = coreutilsValidateInput('cp', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($errors) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($errors) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('cp'); return $result; @@ -80,7 +86,8 @@ function cp(array $input, ?string $cwd = null): array { } /** Separator-aware comparison of canonical paths, including Windows case folding. */ -function coreutilsCopyWithin(string $path, string $directory): bool { +function coreutilsCopyWithin(string $path, string $directory): bool +{ if (DIRECTORY_SEPARATOR === '\\') { $path = strtolower(str_replace('\\', '/', $path)); $directory = strtolower(str_replace('\\', '/', $directory)); @@ -89,7 +96,8 @@ function coreutilsCopyWithin(string $path, string $directory): bool { return $path === $directory || strpos($path, $directory . '/') === 0; } -function coreutilsCopyEntry(string $source, string $target, array &$result, array &$written): void { +function coreutilsCopyEntry(string $source, string $target, array &$result, array &$written): void +{ $recursive = $result['options']['recursive'] ?? false; $stat = coreutilsLstat($source, $warning); if ($stat === false) { @@ -99,7 +107,7 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra $type = $stat['mode'] & 0170000; // Ordinary cp follows a source link; recursive cp reproduces the link itself. if ($type === 0120000 && !$recursive) { - $stat = coreutilsFsCall(fn() => stat($source), $warning); + $stat = coreutilsFsCall(fn () => stat($source), $warning); if ($stat === false) { coreutilsAddError($result, coreutilsFsError('cp', 'stat link target', $source, $warning)); return; @@ -114,16 +122,18 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra coreutilsAddError($result, coreutilsError('cp', 'recursive-required', "omitting directory '$source'; use -r", $source)); return; } - $parent = coreutilsFsCall(fn() => realpath(dirname($target))); + $parent = coreutilsFsCall(fn () => realpath(dirname($target))); if ($parent === false || !coreutilsIsDirectory($parent)) { coreutilsAddError($result, coreutilsError('cp', 'invalid-parent', "destination parent does not exist: '$target'", $target)); return; } $key = $parent . DIRECTORY_SEPARATOR . basename($target); - if (DIRECTORY_SEPARATOR === '\\') $key = strtolower($key); + if (DIRECTORY_SEPARATOR === '\\') { + $key = strtolower($key); + } $targetStat = coreutilsLstat($target); $targetType = $targetStat === false ? null : $targetStat['mode'] & 0170000; - $canonical = coreutilsFsCall(fn() => realpath($source)); + $canonical = coreutilsFsCall(fn () => realpath($source)); $sameInode = $targetStat !== false && $stat['ino'] !== 0 && $stat['ino'] === $targetStat['ino'] && $stat['dev'] === $targetStat['dev']; if ($sameInode || ($canonical !== false && coreutilsCopyWithin($key, $canonical) @@ -148,13 +158,13 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra return; } if ($type === 0040000) { - $entries = coreutilsFsCall(fn() => scandir($source), $warning); + $entries = coreutilsFsCall(fn () => scandir($source), $warning); if ($entries === false) { coreutilsAddError($result, coreutilsFsError('cp', 'read directory', $source, $warning)); return; } if ($targetStat === false) { - if (!coreutilsFsCall(fn() => mkdir($target, 0777), $warning)) { + if (!coreutilsFsCall(fn () => mkdir($target, 0777), $warning)) { coreutilsAddError($result, coreutilsFsError('cp', 'create directory', $target, $warning)); return; } @@ -168,7 +178,7 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra return; } if ($type === 0120000) { - $link = coreutilsFsCall(fn() => readlink($source), $warning); + $link = coreutilsFsCall(fn () => readlink($source), $warning); if ($link === false) { coreutilsAddError($result, coreutilsFsError('cp', 'read link', $source, $warning)); return; @@ -177,9 +187,9 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra coreutilsAddError($result, coreutilsError('cp', 'unsupported-link', 'symlink() is unavailable', $source)); return; } - $success = coreutilsFsCall(fn() => symlink($link, $target), $warning); + $success = coreutilsFsCall(fn () => symlink($link, $target), $warning); } else { - $success = coreutilsFsCall(fn() => copy($source, $target), $warning); + $success = coreutilsFsCall(fn () => copy($source, $target), $warning); } if (!$success) { coreutilsAddError($result, coreutilsFsError('cp', 'copy to ' . $target, $source, $warning)); diff --git a/src/dirname.php b/src/dirname.php index 4a201a1..c18157a 100644 --- a/src/dirname.php +++ b/src/dirname.php @@ -1,4 +1,5 @@ []]); [$options, $args, $errors] = coreutilsValidateInput('dirname', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('dirname'); return $result; diff --git a/src/find.php b/src/find.php index ca1bfa9..df31438 100644 --- a/src/find.php +++ b/src/find.php @@ -1,4 +1,5 @@ []]); [$options, $args, $errors] = coreutilsValidateInput('find', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('find'); return $result; @@ -22,8 +28,11 @@ function find(array $input, ?string $cwd = null): array { $accepted = explode(',', $list); foreach ($accepted as $type) { if (!in_array($type, ['f', 'd', 'l'], true)) { - coreutilsAddError($result, coreutilsError('find', 'invalid-type', - 'type must be f, d, l or a comma-separated list of these types'), 2); + coreutilsAddError($result, coreutilsError( + 'find', + 'invalid-type', + 'type must be f, d, l or a comma-separated list of these types' + ), 2); return $result; } } @@ -54,32 +63,48 @@ function find(array $input, ?string $cwd = null): array { } $type = $typeCodes[$stat['mode'] & 0170000] ?? '?'; if ($probe !== $path && $type !== 'd') { - coreutilsAddError($result, coreutilsError('find', 'invalid-path', - "cannot inspect '$name': trailing separator requires a real directory", $name)); + coreutilsAddError($result, coreutilsError( + 'find', + 'invalid-path', + "cannot inspect '$name': trailing separator requires a real directory", + $name + )); continue; } $matches = $types === null || in_array($type, $types, true); // Match the displayed entry's basename, including a starting . or .. . $leaf = rtrim($name, $separators); - if (DIRECTORY_SEPARATOR === '\\') $leaf = str_replace('\\', '/', $leaf); + if (DIRECTORY_SEPARATOR === '\\') { + $leaf = str_replace('\\', '/', $leaf); + } $slash = strrpos($leaf, '/'); - if ($slash !== false) $leaf = substr($leaf, $slash + 1); - if ($leaf === '') $leaf = DIRECTORY_SEPARATOR; + if ($slash !== false) { + $leaf = substr($leaf, $slash + 1); + } + if ($leaf === '') { + $leaf = DIRECTORY_SEPARATOR; + } foreach ($patterns as $pattern) { - if (!$matches) break; + if (!$matches) { + break; + } $matches = fnmatch($pattern, $leaf); } if ($matches) { $result['data']['entries'][] = ['name' => $name, 'path' => $path, 'type' => $type]; } - if ($type !== 'd') continue; - $children = coreutilsFsCall(fn() => scandir($path, SCANDIR_SORT_ASCENDING), $warning); + if ($type !== 'd') { + continue; + } + $children = coreutilsFsCall(fn () => scandir($path, SCANDIR_SORT_ASCENDING), $warning); if ($children === false) { coreutilsAddError($result, coreutilsFsError('find', 'read directory', $name, $warning)); continue; } foreach (array_reverse($children) as $child) { - if ($child === '.' || $child === '..') continue; + if ($child === '.' || $child === '..') { + continue; + } $stack[] = [rtrim($name, $separators) . DIRECTORY_SEPARATOR . $child, rtrim($path, $separators) . DIRECTORY_SEPARATOR . $child]; } diff --git a/src/lib/errorHandling.php b/src/lib/errorHandling.php index aaca5d8..4c94066 100644 --- a/src/lib/errorHandling.php +++ b/src/lib/errorHandling.php @@ -1,4 +1,5 @@ $code, 'message' => $command . ': ' . $message, 'path' => $path]; } -function coreutilsResult(string $command, array $data = []): array { +function coreutilsResult(string $command, array $data = []): array +{ return [ 'command' => $command, 'status' => 0, @@ -42,13 +45,15 @@ function coreutilsResult(string $command, array $data = []): array { ]; } -function coreutilsAddError(array &$result, array $error, int $status = 1): void { +function coreutilsAddError(array &$result, array $error, int $status = 1): void +{ $result['errors'][] = $error; $result['status'] = max($result['status'], $status); } /** Capture filesystem warnings locally; always restore the caller's handler. */ -function coreutilsFsCall(callable $operation, ?string &$warning = null) { +function coreutilsFsCall(callable $operation, ?string &$warning = null) +{ $warning = null; set_error_handler(function ($severity, $message) use (&$warning) { $warning = $message; @@ -61,9 +66,14 @@ function coreutilsFsCall(callable $operation, ?string &$warning = null) { } } -function coreutilsFsError(string $command, string $operation, string $path, ?string $warning): array { +function coreutilsFsError(string $command, string $operation, string $path, ?string $warning): array +{ // Keep the actual diagnostic; do not label every failure "Permission denied". $detail = $warning === null ? 'Operation failed' : preg_replace('/^[^:]+\(\):\s*/', '', $warning); - return coreutilsError($command, 'filesystem-error', - "cannot $operation '$path': $detail", $path); + return coreutilsError( + $command, + 'filesystem-error', + "cannot $operation '$path': $detail", + $path + ); } diff --git a/src/lib/filesystem.php b/src/lib/filesystem.php index 6eafe29..311205b 100644 --- a/src/lib/filesystem.php +++ b/src/lib/filesystem.php @@ -1,4 +1,5 @@ realpath($cwd), $warning); - if ($resolved === false || !coreutilsFsCall(fn() => is_dir($resolved), $warning)) { + $resolved = coreutilsFsCall(fn () => realpath($cwd), $warning); + if ($resolved === false || !coreutilsFsCall(fn () => is_dir($resolved), $warning)) { $warning = $warning ?? 'Working directory does not exist or is inaccessible'; return false; } @@ -44,9 +48,12 @@ function coreutilsWorkingDirectory(?string $cwd, ?string &$warning = null) { } /** Preserve the final component and .. semantics: realpath() would dereference links. */ -function coreutilsResolvePath(string $path, string $cwd): string { +function coreutilsResolvePath(string $path, string $cwd): string +{ if (DIRECTORY_SEPARATOR === '\\') { - if (preg_match('~^[a-zA-Z]:[/\\\\]|^[/\\\\]{2}~', $path)) return $path; + if (preg_match('~^[a-zA-Z]:[/\\\\]|^[/\\\\]{2}~', $path)) { + return $path; + } if ($path[0] === '/' || $path[0] === '\\') { // A root-relative Windows path belongs to the drive/share of the explicit cwd. preg_match('~^(?:[a-zA-Z]:|[/\\\\]{2}[^/\\\\]+[/\\\\][^/\\\\]+)~', $cwd, $root); @@ -58,12 +65,14 @@ function coreutilsResolvePath(string $path, string $cwd): string { return rtrim($cwd, DIRECTORY_SEPARATOR === '\\' ? '/\\' : '/') . DIRECTORY_SEPARATOR . $path; } -function coreutilsLstat(string $path, ?string &$warning = null) { +function coreutilsLstat(string $path, ?string &$warning = null) +{ clearstatcache(true, $path); - return coreutilsFsCall(fn() => lstat($path), $warning); + return coreutilsFsCall(fn () => lstat($path), $warning); } -function coreutilsIsDirectory(string $path): bool { +function coreutilsIsDirectory(string $path): bool +{ clearstatcache(true, $path); - return coreutilsFsCall(fn() => is_dir($path)); + return coreutilsFsCall(fn () => is_dir($path)); } diff --git a/src/lib/format.php b/src/lib/format.php index 60d3c74..1f3eec8 100644 --- a/src/lib/format.php +++ b/src/lib/format.php @@ -1,4 +1,5 @@ $function($id)) : false; + $record = function_exists($function) ? coreutilsFsCall(fn () => $function($id)) : false; return $cache[$id] = $record === false ? (string) $id : (string) $record['name']; } -function coreutilsFormatEntries(array $entries, array $settings, $formatter, array &$users, array &$groups): array { +function coreutilsFormatEntries(array $entries, array $settings, $formatter, array &$users, array &$groups): array +{ $rows = []; $widths = []; foreach ($entries as $entry) { @@ -70,15 +79,23 @@ function coreutilsFormatEntries(array $entries, array $settings, $formatter, arr continue; } $row = [$entry['permissions'], (string) $entry['nlink']]; - if ($settings['owner']) $row[] = coreutilsIdentity($entry['uid'], false, $users); - if ($settings['group']) $row[] = coreutilsIdentity($entry['gid'], true, $groups); + if ($settings['owner']) { + $row[] = coreutilsIdentity($entry['uid'], false, $users); + } + if ($settings['group']) { + $row[] = coreutilsIdentity($entry['gid'], true, $groups); + } $row[] = $settings['size'] === 'bytes' ? (string) $entry['size'] : coreutilsHumanSize($entry['size'], $settings['size'] === 'si'); $row[] = coreutilsFormatDate($entry['mtime'], $formatter); - if ($entry['target'] !== null) $name .= ' -> ' . coreutilsQuoteName($entry['target']); + if ($entry['target'] !== null) { + $name .= ' -> ' . coreutilsQuoteName($entry['target']); + } $row[] = $name; foreach ($row as $i => $cell) { - if ($i !== count($row) - 1) $widths[$i] = max($widths[$i] ?? 0, strlen($cell)); + if ($i !== count($row) - 1) { + $widths[$i] = max($widths[$i] ?? 0, strlen($cell)); + } } $rows[] = $row; } @@ -94,9 +111,12 @@ function coreutilsFormatEntries(array $entries, array $settings, $formatter, arr } /** Format both diagnostics and output as plain text; the original result stays reusable. */ -function coreutilsText(array $result): string { +function coreutilsText(array $result): string +{ $lines = array_column($result['errors'], 'message'); - if ($result['help'] !== null) return implode("\n", $lines) . ($lines ? "\n" : '') . $result['help']; + if ($result['help'] !== null) { + return implode("\n", $lines) . ($lines ? "\n" : '') . $result['help']; + } if (in_array($result['command'], ['basename', 'dirname'], true)) { $separator = ($result['options']['zero'] ?? false) ? "\0" : "\n"; $values = array_column($result['data']['entries'], 'output'); @@ -112,9 +132,15 @@ function coreutilsText(array $result): string { $lines = array_merge($lines, coreutilsFormatEntries($files, $settings, $formatter, $users, $groups)); $headers = count($files) + count($directories) > 1; foreach ($directories as $index => $directory) { - if ($files || $index > 0) $lines[] = ''; - if ($headers) $lines[] = coreutilsQuoteName($directory['name']) . ':'; - if (!$directory['readable']) continue; + if ($files || $index > 0) { + $lines[] = ''; + } + if ($headers) { + $lines[] = coreutilsQuoteName($directory['name']) . ':'; + } + if (!$directory['readable']) { + continue; + } if ($settings['long']) { $blocks = $directory['blocks']; $total = $blocks === null ? '?' : ($settings['size'] === 'bytes' @@ -151,7 +177,8 @@ function coreutilsText(array $result): string { } /** Escape the complete presentation, including names, link targets and errors. */ -function coreutilsHtml(array $result): string { +function coreutilsHtml(array $result): string +{ return '
'
         . htmlspecialchars(coreutilsText($result), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '
'; } diff --git a/src/lib/locale.php b/src/lib/locale.php index 75f3f1e..62933e7 100644 --- a/src/lib/locale.php +++ b/src/lib/locale.php @@ -1,4 +1,5 @@ compare($left, $right); return $comparison === false ? strcmp($left, $right) : $comparison; } -function coreutilsDateFormatter(?string $locale) { - if (!class_exists('IntlDateFormatter')) return null; +function coreutilsDateFormatter(?string $locale) +{ + if (!class_exists('IntlDateFormatter')) { + return null; + } try { return new IntlDateFormatter( $locale === null || $locale === 'C' || $locale === 'POSIX' ? 'en_US_POSIX' : $locale, @@ -59,7 +65,8 @@ function coreutilsDateFormatter(?string $locale) { } } -function coreutilsFormatDate(int $timestamp, $formatter = null): string { +function coreutilsFormatDate(int $timestamp, $formatter = null): string +{ if ($formatter !== null) { $formatted = $formatter->format($timestamp); if ($formatted !== false) { diff --git a/src/lib/options.php b/src/lib/options.php index 74687cb..86b9b02 100644 --- a/src/lib/options.php +++ b/src/lib/options.php @@ -1,4 +1,5 @@ [short spelling, long spelling, requires a value]. */ -function coreutilsOptionDefinitions(string $command): array { +function coreutilsOptionDefinitions(string $command): array +{ $common = ['help' => [null, 'help', false]]; if ($command === 'basename') { return [ @@ -99,7 +101,8 @@ function coreutilsOptionDefinitions(string $command): array { return []; } -function coreutilsHelp(string $command): string { +function coreutilsHelp(string $command): string +{ if ($command === 'basename') { return "Usage: basename [OPTION]... NAME [SUFFIX]\n" . " or: basename -a [OPTION]... NAME...\n" diff --git a/src/lib/parser.php b/src/lib/parser.php index 962946b..5af4553 100644 --- a/src/lib/parser.php +++ b/src/lib/parser.php @@ -1,4 +1,5 @@ $command, 'options' => [], 'args' => [], 'errors' => []]; @@ -105,8 +108,12 @@ function parseCommand(string $input): array { } $short = $long = []; foreach ($definitions as $name => [$s, $l, $takesValue]) { - if ($s !== null) $short[$s] = $name; - if ($l !== null) $long[$l] = $name; + if ($s !== null) { + $short[$s] = $name; + } + if ($l !== null) { + $long[$l] = $name; + } } $endOfOptions = false; $findPredicates = false; @@ -114,13 +121,19 @@ function parseCommand(string $input): array { $token = $tokens[$i]; if ($endOfOptions || $token === '' || $token === '-' || $token[0] !== '-') { if ($findPredicates) { - $parsed['errors'][] = coreutilsError($command, 'unexpected-path', - 'paths must precede -type and -name predicates', $token); + $parsed['errors'][] = coreutilsError( + $command, + 'unexpected-path', + 'paths must precede -type and -name predicates', + $token + ); continue; } $parsed['args'][] = $token; // basename's second operand is a literal suffix, even if it starts with '-'. - if ($command === 'basename') $endOfOptions = true; + if ($command === 'basename') { + $endOfOptions = true; + } continue; } if ($token === '--') { @@ -171,14 +184,17 @@ function parseCommand(string $input): array { unset($parsed['options'][$name]); $parsed['options'][$name] = $value; } - if ($takesValue) break; + if ($takesValue) { + break; + } } } return $parsed; } /** Validate parsed or programmatically supplied input before any filesystem changes. */ -function coreutilsValidateInput(string $command, array $input): array { +function coreutilsValidateInput(string $command, array $input): array +{ $errors = $input['errors'] ?? []; if (($input['command'] ?? $command) !== $command && !$errors) { $errors[] = coreutilsError($command, 'invalid-command', 'input belongs to a different command'); diff --git a/src/lib/pathnames.php b/src/lib/pathnames.php index 0faaac2..32a668f 100644 --- a/src/lib/pathnames.php +++ b/src/lib/pathnames.php @@ -1,13 +1,19 @@ scandir($parent)); + if ($parent === $path) { + return false; + } + if (coreutilsLstat($parent) === false) { + return coreutilsRemovalMissing($parent); + } + if (!coreutilsIsDirectory($parent)) { + return false; + } + $names = coreutilsFsCall(fn () => scandir($parent)); return $names !== false && !in_array(basename($path), $names, true); } /** Refuse roots, dot operands, the explicit cwd and its ancestors. Final links stay links. */ -function coreutilsRemovalProtected(string $path, string $cwd, bool $directory): bool { +function coreutilsRemovalProtected(string $path, string $cwd, bool $directory): bool +{ $name = basename($path); - if ($path === '' || $name === '.' || $name === '..') return true; - if (!$directory) return false; - $canonical = coreutilsFsCall(fn() => realpath($path)); - if ($canonical === false) return true; - if (dirname($canonical) === $canonical) return true; + if ($path === '' || $name === '.' || $name === '..') { + return true; + } + if (!$directory) { + return false; + } + $canonical = coreutilsFsCall(fn () => realpath($path)); + if ($canonical === false) { + return true; + } + if (dirname($canonical) === $canonical) { + return true; + } if (DIRECTORY_SEPARATOR === '\\') { $canonical = strtolower(str_replace('\\', '/', $canonical)); $cwd = strtolower(str_replace('\\', '/', $cwd)); @@ -54,7 +71,8 @@ function coreutilsRemovalProtected(string $path, string $cwd, bool $directory): return $canonical === $cwd || strpos($cwd, rtrim($canonical, '/') . '/') === 0; } -function coreutilsRemoveEntry(string $path, string $cwd, array &$result): void { +function coreutilsRemoveEntry(string $path, string $cwd, array &$result): void +{ $command = $result['command']; $separators = DIRECTORY_SEPARATOR === '\\' ? '/\\' : '/'; $trimmed = rtrim($path, $separators); @@ -86,19 +104,23 @@ function coreutilsRemoveEntry(string $path, string $cwd, array &$result): void { coreutilsAddError($result, coreutilsError($command, 'recursive-required', "cannot remove directory '$path'; use -r", $path)); return; } - $names = coreutilsFsCall(fn() => scandir($trimmed), $warning); + $names = coreutilsFsCall(fn () => scandir($trimmed), $warning); if ($names === false) { coreutilsAddError($result, coreutilsFsError($command, 'read directory', $path, $warning)); return; } $errorCount = count($result['errors']); foreach ($names as $name) { - if ($name !== '.' && $name !== '..') coreutilsRemoveEntry($trimmed . DIRECTORY_SEPARATOR . $name, $cwd, $result); + if ($name !== '.' && $name !== '..') { + coreutilsRemoveEntry($trimmed . DIRECTORY_SEPARATOR . $name, $cwd, $result); + } + } + if (count($result['errors']) !== $errorCount) { + return; } - if (count($result['errors']) !== $errorCount) return; } - $success = $directory ? coreutilsFsCall(fn() => rmdir($trimmed), $warning) - : coreutilsFsCall(fn() => unlink($trimmed), $warning); + $success = $directory ? coreutilsFsCall(fn () => rmdir($trimmed), $warning) + : coreutilsFsCall(fn () => unlink($trimmed), $warning); if (!$success) { coreutilsAddError($result, coreutilsFsError($command, 'remove', $path, $warning)); return; @@ -107,12 +129,17 @@ function coreutilsRemoveEntry(string $path, string $cwd, array &$result): void { $result['data']['removed'][] = $trimmed; } -function coreutilsRemove(string $command, array $input, ?string $cwd): array { +function coreutilsRemove(string $command, array $input, ?string $cwd): array +{ $result = coreutilsResult($command, ['removed' => [], 'skipped' => []]); [$options, $args, $errors] = coreutilsValidateInput($command, $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($errors) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($errors) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp($command); return $result; @@ -134,10 +161,14 @@ function coreutilsRemove(string $command, array $input, ?string $cwd): array { $count = count($result['data']['removed']); coreutilsRemoveEntry($path, $cwd, $result); if ($command !== 'rmdir' || !($options['parents'] ?? false) - || count($result['data']['removed']) === $count) break; + || count($result['data']['removed']) === $count) { + break; + } $parent = dirname(rtrim($path, DIRECTORY_SEPARATOR === '\\' ? '/\\' : '/')); // Parent removal stops before the cwd or filesystem root, including aliases. - if (coreutilsRemovalProtected($parent, $cwd, true)) break; + if (coreutilsRemovalProtected($parent, $cwd, true)) { + break; + } $path = $parent; } } diff --git a/src/ls.php b/src/ls.php index 3148a9f..0498913 100644 --- a/src/ls.php +++ b/src/ls.php @@ -1,4 +1,5 @@ 's', 0120000 => 'l', 0100000 => '-', 0060000 => 'b', 0040000 => 'd', 0020000 => 'c', 0010000 => 'p']; $text = $types[$mode & 0170000] ?? '?'; @@ -44,10 +46,13 @@ function coreutilsSymbolicPerms(int $mode): string { return $text; } -function coreutilsLsSettings(array $options): array { +function coreutilsLsSettings(array $options): array +{ $size = 'bytes'; foreach ($options as $name => $enabled) { - if ($enabled && ($name === 'human-readable' || $name === 'si')) $size = $name; + if ($enabled && ($name === 'human-readable' || $name === 'si')) { + $size = $name; + } } return [ 'long' => ($options['long'] ?? false) || ($options['omit-owner'] ?? false) || ($options['omit-group'] ?? false), @@ -57,11 +62,12 @@ function coreutilsLsSettings(array $options): array { ]; } -function coreutilsLsEntry(string $name, string $path, array $stat, array &$result): array { +function coreutilsLsEntry(string $name, string $path, array $stat, array &$result): array +{ $type = coreutilsSymbolicPerms($stat['mode']); $target = null; if ($type[0] === 'l') { - $target = coreutilsFsCall(fn() => readlink($path), $warning); + $target = coreutilsFsCall(fn () => readlink($path), $warning); if ($target === false) { coreutilsAddError($result, coreutilsFsError('ls', 'read symbolic link', $name, $warning)); $target = null; @@ -78,13 +84,18 @@ function coreutilsLsEntry(string $name, string $path, array $stat, array &$resul } /** Return metadata with raw names and sizes. Presentation is handled by coreutilsText/Html. */ -function ls(array $input, ?string $cwd = null, ?string $locale = null): array { +function ls(array $input, ?string $cwd = null, ?string $locale = null): array +{ $result = coreutilsResult('ls', ['files' => [], 'directories' => []]); $result['locale'] = $locale; [$options, $args, $errors] = coreutilsValidateInput('ls', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('ls'); return $result; @@ -96,7 +107,7 @@ function ls(array $input, ?string $cwd = null, ?string $locale = null): array { } $settings = coreutilsLsSettings($options); $collator = coreutilsCollator($locale); - $compare = fn($a, $b) => coreutilsCompare($a['name'], $b['name'], $collator); + $compare = fn ($a, $b) => coreutilsCompare($a['name'], $b['name'], $collator); $directories = []; foreach ($args ?: ['.'] as $arg) { $path = coreutilsResolvePath($arg, $base); @@ -123,7 +134,7 @@ function ls(array $input, ?string $cwd = null, ?string $locale = null): array { usort($directories, $compare); foreach ($directories as $directory) { $group = $directory + ['entries' => [], 'blocks' => 0, 'readable' => true]; - $items = coreutilsFsCall(fn() => scandir($directory['path'], SCANDIR_SORT_NONE), $warning); + $items = coreutilsFsCall(fn () => scandir($directory['path'], SCANDIR_SORT_NONE), $warning); if ($items === false) { coreutilsAddError($result, coreutilsFsError('ls', 'open directory', $directory['name'], $warning)); $group['blocks'] = null; @@ -131,14 +142,17 @@ function ls(array $input, ?string $cwd = null, ?string $locale = null): array { $result['data']['directories'][] = $group; continue; } - $items = array_values(array_filter($items, fn($name) => ($options['all'] ?? false) || $name[0] !== '.')); - usort($items, fn($a, $b) => coreutilsCompare($a, $b, $collator)); + $items = array_values(array_filter($items, fn ($name) => ($options['all'] ?? false) || $name[0] !== '.')); + usort($items, fn ($a, $b) => coreutilsCompare($a, $b, $collator)); if ($options['group-directories-first'] ?? false) { $dirs = $files = []; foreach ($items as $name) { $full = $directory['path'] . DIRECTORY_SEPARATOR . $name; - if (coreutilsIsDirectory($full)) $dirs[] = $name; - else $files[] = $name; + if (coreutilsIsDirectory($full)) { + $dirs[] = $name; + } else { + $files[] = $name; + } } $items = array_merge($dirs, $files); } diff --git a/src/mkdir.php b/src/mkdir.php index 915fbb2..9d1cc1b 100644 --- a/src/mkdir.php +++ b/src/mkdir.php @@ -1,4 +1,5 @@ mkdir($path, 0777), $warning)) { + if ($parent !== $path && !coreutilsMakeParents($parent, $created, $warning)) { + return false; + } + if (coreutilsIsDirectory($path)) { + return true; + } + if (!coreutilsFsCall(fn () => mkdir($path, 0777), $warning)) { return coreutilsIsDirectory($path); // Another caller may have created the parent. } $created[] = $path; $stat = coreutilsLstat($path, $warning); - if ($stat === false) return false; + if ($stat === false) { + return false; + } if (DIRECTORY_SEPARATOR !== '\\' && ($stat['mode'] & 0300) !== 0300) { $mode = ($stat['mode'] & 07777) | 0300; - return coreutilsFsCall(fn() => chmod($path, $mode), $warning); + return coreutilsFsCall(fn () => chmod($path, $mode), $warning); } return true; } /** Return created/existing paths, errors and status. No output, chdir() or session access. */ -function _mkdir(array $input, ?string $cwd = null): array { +function _mkdir(array $input, ?string $cwd = null): array +{ $result = coreutilsResult('mkdir', ['created' => [], 'existing' => []]); [$options, $args, $errors] = coreutilsValidateInput('mkdir', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('mkdir'); return $result; @@ -85,7 +100,7 @@ function _mkdir(array $input, ?string $cwd = null): array { coreutilsAddError($result, coreutilsFsError('mkdir', 'create directory', $arg, $warning)); continue; } - if (!coreutilsFsCall(fn() => mkdir($path, $mode), $warning)) { + if (!coreutilsFsCall(fn () => mkdir($path, $mode), $warning)) { if ($recursive && coreutilsIsDirectory($path)) { $result['data']['existing'][] = $path; } else { @@ -95,7 +110,7 @@ function _mkdir(array $input, ?string $cwd = null): array { } $result['data']['created'][] = $path; // Only an explicit -m overrides the umask. Never chmod an existing directory. - if ($explicitMode && !coreutilsFsCall(fn() => chmod($path, $mode), $warning)) { + if ($explicitMode && !coreutilsFsCall(fn () => chmod($path, $mode), $warning)) { coreutilsAddError($result, coreutilsFsError('mkdir', 'set permissions on', $arg, $warning)); } } diff --git a/src/mv.php b/src/mv.php index d4e9641..548ee0b 100644 --- a/src/mv.php +++ b/src/mv.php @@ -1,4 +1,5 @@ [], 'skipped' => []]); [$options, $args, $errors] = coreutilsValidateInput('mv', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($errors) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($errors) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('mv'); return $result; @@ -51,7 +57,9 @@ function mv(array $input, ?string $cwd = null): array { } $noClobber = false; foreach ($options as $name => $enabled) { - if ($enabled && ($name === 'force' || $name === 'no-clobber')) $noClobber = $name === 'no-clobber'; + if ($enabled && ($name === 'force' || $name === 'no-clobber')) { + $noClobber = $name === 'no-clobber'; + } } $separators = DIRECTORY_SEPARATOR === '\\' ? '/\\' : '/'; $destination = coreutilsResolvePath(array_pop($args), $cwd); @@ -77,8 +85,8 @@ function mv(array $input, ?string $cwd = null): array { continue; } $target = $directory ? rtrim($destination, $separators) . DIRECTORY_SEPARATOR . $name : $destination; - $parent = coreutilsFsCall(fn() => realpath(dirname($target))); - $sourceParent = coreutilsFsCall(fn() => realpath(dirname($source))); + $parent = coreutilsFsCall(fn () => realpath(dirname($target))); + $sourceParent = coreutilsFsCall(fn () => realpath(dirname($source))); if ($parent === false || !coreutilsIsDirectory($parent)) { coreutilsAddError($result, coreutilsError('mv', 'invalid-parent', "destination parent does not exist: '$target'", $target)); continue; @@ -101,7 +109,7 @@ function mv(array $input, ?string $cwd = null): array { continue; } if ($isDirectory) { - $canonical = coreutilsFsCall(fn() => realpath($source)); + $canonical = coreutilsFsCall(fn () => realpath($source)); $prefix = rtrim((string) $canonical, $separators) . DIRECTORY_SEPARATOR; $comparisonParent = $parent . DIRECTORY_SEPARATOR; if (DIRECTORY_SEPARATOR === '\\') { @@ -117,12 +125,12 @@ function mv(array $input, ?string $cwd = null): array { coreutilsAddError($result, coreutilsError('mv', 'type-mismatch', "cannot replace '$target' with a different entry type", $target)); continue; } - $parentStat = coreutilsFsCall(fn() => stat($parent)); + $parentStat = coreutilsFsCall(fn () => stat($parent)); if ($parentStat === false || $stat['dev'] !== $parentStat['dev']) { coreutilsAddError($result, coreutilsError('mv', 'cross-device', "cannot move '$operand': cross-filesystem moves are not supported", $operand)); continue; } - if (!coreutilsFsCall(fn() => rename($source, $target), $warning)) { + if (!coreutilsFsCall(fn () => rename($source, $target), $warning)) { coreutilsAddError($result, coreutilsFsError('mv', 'move to ' . $target, $operand, $warning)); continue; } diff --git a/src/pwd.php b/src/pwd.php index a74f379..4967b62 100644 --- a/src/pwd.php +++ b/src/pwd.php @@ -1,4 +1,5 @@ null]); [$options, $args, $errors] = coreutilsValidateInput('pwd', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('pwd'); return $result; } if ($args) { - coreutilsAddError($result, coreutilsError('pwd', 'unexpected-operand', - 'this command does not accept operands'), 2); + coreutilsAddError($result, coreutilsError( + 'pwd', + 'unexpected-operand', + 'this command does not accept operands' + ), 2); return $result; } $logical = false; @@ -28,10 +37,13 @@ function pwd(array $input, ?string $cwd = null): array { } } - $directory = $cwd ?? coreutilsFsCall(fn() => getcwd(), $warning); + $directory = $cwd ?? coreutilsFsCall(fn () => getcwd(), $warning); if ($directory === false) { - coreutilsAddError($result, coreutilsError('pwd', 'invalid-cwd', - $warning ?? 'Cannot determine working directory')); + coreutilsAddError($result, coreutilsError( + 'pwd', + 'invalid-cwd', + $warning ?? 'Cannot determine working directory' + )); return $result; } // Refresh cached resolutions when a link has changed between calls. @@ -42,19 +54,27 @@ function pwd(array $input, ?string $cwd = null): array { return $result; } $result['data']['path'] = $physical; - if (!$logical) return $result; + if (!$logical) { + return $result; + } // An explicit application cwd takes precedence over the process environment. $candidate = $cwd ?? getenv('PWD'); - if (!is_string($candidate) || $candidate === '' || strpos($candidate, "\0") !== false) return $result; + if (!is_string($candidate) || $candidate === '' || strpos($candidate, "\0") !== false) { + return $result; + } $windows = DIRECTORY_SEPARATOR === '\\'; $absolute = $windows ? preg_match('~^(?:[a-zA-Z]:[/\\\\]|[/\\\\]{2}[^/\\\\]+[/\\\\][^/\\\\]+)~', $candidate) : $candidate[0] === '/'; - if (!$absolute) return $result; + if (!$absolute) { + return $result; + } $components = explode('/', $windows ? str_replace('\\', '/', $candidate) : $candidate); - if (in_array('.', $components, true) || in_array('..', $components, true)) return $result; - $resolved = coreutilsFsCall(fn() => realpath($candidate)); + if (in_array('.', $components, true) || in_array('..', $components, true)) { + return $result; + } + $resolved = coreutilsFsCall(fn () => realpath($candidate)); if ($resolved !== false && ($windows ? strcasecmp($resolved, $physical) === 0 : $resolved === $physical)) { $result['data']['path'] = $candidate; } diff --git a/src/rm.php b/src/rm.php index b24cd04..baee13b 100644 --- a/src/rm.php +++ b/src/rm.php @@ -1,4 +1,5 @@ fixture(function ($base) { + +$tests['cp copies binary contents without output or source changes'] = fn () => fixture(function ($base) { $bytes = "a\0b\xff\n"; file_put_contents($base . '/source', $bytes); - ob_start(); $result = cp(parseCommand('cp source target'), $base); same('', ob_get_clean()); - same(0, $result['status']); same($bytes, file_get_contents($base . '/source')); + ob_start(); + $result = cp(parseCommand('cp source target'), $base); + same('', ob_get_clean()); + same(0, $result['status']); + same($bytes, file_get_contents($base . '/source')); same($bytes, file_get_contents($base . '/target')); same([['source' => $base . '/source', 'destination' => $base . '/target']], $result['data']['copied']); same('', coreutilsText($result)); }); -$tests['cp overwrites by default and skips with no-clobber'] = fn() => fixture(function ($base) { - file_put_contents($base . '/a', 'new'); file_put_contents($base . '/b', 'old'); +$tests['cp overwrites by default and skips with no-clobber'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'new'); + file_put_contents($base . '/b', 'old'); $result = cp(parseCommand('cp --no-clobber a b'), $base); - same(0, $result['status']); same(1, count($result['data']['skipped'])); same('old', file_get_contents($base . '/b')); - same(0, cp(parseCommand('cp a b'), $base)['status']); same('new', file_get_contents($base . '/b')); + same(0, $result['status']); + same(1, count($result['data']['skipped'])); + same('old', file_get_contents($base . '/b')); + same(0, cp(parseCommand('cp a b'), $base)['status']); + same('new', file_get_contents($base . '/b')); same('new', file_get_contents($base . '/a')); }); -$tests['cp multiple sources require a directory and continue after errors'] = fn() => fixture(function ($base) { - file_put_contents($base . '/a', 'a'); file_put_contents($base . '/b', 'b'); mkdir($base . '/out'); - same(1, cp(parseCommand('cp a b missing'), $base)['status']); check(!file_exists($base . '/missing')); +$tests['cp multiple sources require a directory and continue after errors'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'a'); + file_put_contents($base . '/b', 'b'); + mkdir($base . '/out'); + same(1, cp(parseCommand('cp a b missing'), $base)['status']); + check(!file_exists($base . '/missing')); $result = cp(parseCommand('cp a absent b out'), $base); - same(1, $result['status']); same(2, count($result['data']['copied'])); - same('a', file_get_contents($base . '/out/a')); same('b', file_get_contents($base . '/out/b')); + same(1, $result['status']); + same(2, count($result['data']['copied'])); + same('a', file_get_contents($base . '/out/a')); + same('b', file_get_contents($base . '/out/b')); }); -$tests['cp validates all input before writing and help ignores cwd'] = fn() => fixture(function ($base) { +$tests['cp validates all input before writing and help ignores cwd'] = fn () => fixture(function ($base) { file_put_contents($base . '/a', 'a'); foreach (['cp a', 'cp -z a b', 'cp a ""', 'cp a b https://example.com', 'cp a "unterminated'] as $command) { - same(2, cp(parseCommand($command), $base)['status']); check(!file_exists($base . '/b')); + same(2, cp(parseCommand($command), $base)['status']); + check(!file_exists($base . '/b')); } same(2, cp(['args' => ['a', 'b'], 'options' => ['recursive' => 1]], $base)['status']); same(1, cp(parseCommand('cp a b'), $base . '/missing')['status']); - $result = cp(parseCommand('cp --help'), '/nonexistent'); same(0, $result['status']); + $result = cp(parseCommand('cp --help'), '/nonexistent'); + same(0, $result['status']); check(strpos(coreutilsText($result), 'Usage: cp') === 0); }); -$tests['cp recursive copies hidden files and empty directories'] = fn() => fixture(function ($base) { - mkdir($base . '/tree'); mkdir($base . '/tree/empty'); mkdir($base . '/tree/sub'); - file_put_contents($base . '/tree/.hidden', 'hidden'); file_put_contents($base . '/tree/sub/file', 'data'); - same(1, cp(parseCommand('cp tree out'), $base)['status']); check(!file_exists($base . '/out')); - $result = cp(parseCommand('cp --recursive tree/ out'), $base); same(0, $result['status']); - same(3, count($result['data']['created'])); same(2, count($result['data']['copied'])); - check(is_dir($base . '/out/empty')); same('hidden', file_get_contents($base . '/out/.hidden')); +$tests['cp recursive copies hidden files and empty directories'] = fn () => fixture(function ($base) { + mkdir($base . '/tree'); + mkdir($base . '/tree/empty'); + mkdir($base . '/tree/sub'); + file_put_contents($base . '/tree/.hidden', 'hidden'); + file_put_contents($base . '/tree/sub/file', 'data'); + same(1, cp(parseCommand('cp tree out'), $base)['status']); + check(!file_exists($base . '/out')); + $result = cp(parseCommand('cp --recursive tree/ out'), $base); + same(0, $result['status']); + same(3, count($result['data']['created'])); + same(2, count($result['data']['copied'])); + check(is_dir($base . '/out/empty')); + same('hidden', file_get_contents($base . '/out/.hidden')); same('data', file_get_contents($base . '/out/sub/file')); check(is_dir($base . '/tree/empty')); }); -$tests['cp merges directories and no-clobber skips only existing leaves'] = fn() => fixture(function ($base) { - mkdir($base . '/tree'); mkdir($base . '/out'); mkdir($base . '/out/tree'); - file_put_contents($base . '/tree/a', 'new'); file_put_contents($base . '/tree/b', 'b'); - file_put_contents($base . '/out/tree/a', 'old'); file_put_contents($base . '/out/tree/keep', 'keep'); - $result = cp(parseCommand('cp -rnv tree out'), $base); same(0, $result['status']); - same(1, count($result['data']['skipped'])); same('old', file_get_contents($base . '/out/tree/a')); - same('b', file_get_contents($base . '/out/tree/b')); same('keep', file_get_contents($base . '/out/tree/keep')); - same(0, cp(parseCommand('cp -r tree out'), $base)['status']); same('new', file_get_contents($base . '/out/tree/a')); +$tests['cp merges directories and no-clobber skips only existing leaves'] = fn () => fixture(function ($base) { + mkdir($base . '/tree'); + mkdir($base . '/out'); + mkdir($base . '/out/tree'); + file_put_contents($base . '/tree/a', 'new'); + file_put_contents($base . '/tree/b', 'b'); + file_put_contents($base . '/out/tree/a', 'old'); + file_put_contents($base . '/out/tree/keep', 'keep'); + $result = cp(parseCommand('cp -rnv tree out'), $base); + same(0, $result['status']); + same(1, count($result['data']['skipped'])); + same('old', file_get_contents($base . '/out/tree/a')); + same('b', file_get_contents($base . '/out/tree/b')); + same('keep', file_get_contents($base . '/out/tree/keep')); + same(0, cp(parseCommand('cp -r tree out'), $base)['status']); + same('new', file_get_contents($base . '/out/tree/a')); }); -$tests['cp rejects self-copy and descendant destinations without mutation'] = fn() => fixture(function ($base) { - file_put_contents($base . '/a', 'original'); mkdir($base . '/tree'); mkdir($base . '/tree/sub'); - same(1, cp(parseCommand('cp a a'), $base)['status']); same('original', file_get_contents($base . '/a')); +$tests['cp rejects self-copy and descendant destinations without mutation'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'original'); + mkdir($base . '/tree'); + mkdir($base . '/tree/sub'); + same(1, cp(parseCommand('cp a a'), $base)['status']); + same('original', file_get_contents($base . '/a')); foreach (['cp -r tree tree', 'cp -r tree tree/sub', 'cp -r tree tree/new', 'cp -r . out'] as $command) { same(1, cp(parseCommand($command), $base)['status']); } - check(!file_exists($base . '/tree/tree')); check(!file_exists($base . '/tree/new')); check(!file_exists($base . '/out')); + check(!file_exists($base . '/tree/tree')); + check(!file_exists($base . '/tree/new')); + check(!file_exists($base . '/out')); }); -$tests['cp detects same-file hard links'] = fn() => fixture(function ($base) { +$tests['cp detects same-file hard links'] = fn () => fixture(function ($base) { file_put_contents($base . '/a', 'keep'); - skipUnless(function_exists('link') && coreutilsFsCall(fn() => link($base . '/a', $base . '/alias')), 'hard links unavailable'); - same(1, cp(parseCommand('cp a alias'), $base)['status']); same('keep', file_get_contents($base . '/a')); + skipUnless(function_exists('link') && coreutilsFsCall(fn () => link($base . '/a', $base . '/alias')), 'hard links unavailable'); + same(1, cp(parseCommand('cp a alias'), $base)['status']); + same('keep', file_get_contents($base . '/a')); }); -$tests['cp ordinary follows file links but recursive preserves links and cycles'] = fn() => fixture(function ($base) { - symlinkSupport($base); file_put_contents($base . '/file', 'content'); symlink('file', $base . '/link'); - same(0, cp(parseCommand('cp link copy'), $base)['status']); check(!is_link($base . '/copy')); +$tests['cp ordinary follows file links but recursive preserves links and cycles'] = fn () => fixture(function ($base) { + symlinkSupport($base); + file_put_contents($base . '/file', 'content'); + symlink('file', $base . '/link'); + same(0, cp(parseCommand('cp link copy'), $base)['status']); + check(!is_link($base . '/copy')); same('content', file_get_contents($base . '/copy')); - mkdir($base . '/tree'); symlink('missing', $base . '/tree/broken'); symlink('.', $base . '/tree/cycle'); + mkdir($base . '/tree'); + symlink('missing', $base . '/tree/broken'); + symlink('.', $base . '/tree/cycle'); same(0, cp(parseCommand('cp -r tree out'), $base)['status']); - same('missing', readlink($base . '/out/broken')); same('.', readlink($base . '/out/cycle')); - same(0, cp(parseCommand('cp -r link duplicate-link'), $base)['status']); same('file', readlink($base . '/duplicate-link')); - same(1, cp(parseCommand('cp tree/broken missing-copy'), $base)['status']); check(!file_exists($base . '/missing-copy')); + same('missing', readlink($base . '/out/broken')); + same('.', readlink($base . '/out/cycle')); + same(0, cp(parseCommand('cp -r link duplicate-link'), $base)['status']); + same('file', readlink($base . '/duplicate-link')); + same(1, cp(parseCommand('cp tree/broken missing-copy'), $base)['status']); + check(!file_exists($base . '/missing-copy')); }); -$tests['cp refuses destination links and supports no-clobber for broken links'] = fn() => fixture(function ($base) { - symlinkSupport($base); file_put_contents($base . '/a', 'new'); file_put_contents($base . '/b', 'old'); - symlink('b', $base . '/target'); symlink('missing', $base . '/broken'); - same(1, cp(parseCommand('cp a target'), $base)['status']); same('old', file_get_contents($base . '/b')); - same(1, cp(parseCommand('cp a broken'), $base)['status']); check(!file_exists($base . '/missing')); - $result = cp(parseCommand('cp -n a broken'), $base); same(0, $result['status']); same(1, count($result['data']['skipped'])); +$tests['cp refuses destination links and supports no-clobber for broken links'] = fn () => fixture(function ($base) { + symlinkSupport($base); + file_put_contents($base . '/a', 'new'); + file_put_contents($base . '/b', 'old'); + symlink('b', $base . '/target'); + symlink('missing', $base . '/broken'); + same(1, cp(parseCommand('cp a target'), $base)['status']); + same('old', file_get_contents($base . '/b')); + same(1, cp(parseCommand('cp a broken'), $base)['status']); + check(!file_exists($base . '/missing')); + $result = cp(parseCommand('cp -n a broken'), $base); + same(0, $result['status']); + same(1, count($result['data']['skipped'])); }); -$tests['cp blocks recursion through destination aliases and does not traverse nested target links'] = fn() => fixture(function ($base) { - symlinkSupport($base); mkdir($base . '/tree'); mkdir($base . '/tree/sub'); symlink('tree/sub', $base . '/alias'); - same(1, cp(parseCommand('cp -r tree alias'), $base)['status']); check(!file_exists($base . '/tree/sub/tree')); - mkdir($base . '/out'); mkdir($base . '/out/tree'); mkdir($base . '/elsewhere'); - symlink('../../elsewhere', $base . '/out/tree/sub'); file_put_contents($base . '/tree/sub/a', 'a'); - same(1, cp(parseCommand('cp -r tree out'), $base)['status']); check(!file_exists($base . '/elsewhere/a')); +$tests['cp blocks recursion through destination aliases and does not traverse nested target links'] = fn () => fixture(function ($base) { + symlinkSupport($base); + mkdir($base . '/tree'); + mkdir($base . '/tree/sub'); + symlink('tree/sub', $base . '/alias'); + same(1, cp(parseCommand('cp -r tree alias'), $base)['status']); + check(!file_exists($base . '/tree/sub/tree')); + mkdir($base . '/out'); + mkdir($base . '/out/tree'); + mkdir($base . '/elsewhere'); + symlink('../../elsewhere', $base . '/out/tree/sub'); + file_put_contents($base . '/tree/sub/a', 'a'); + same(1, cp(parseCommand('cp -r tree out'), $base)['status']); + check(!file_exists($base . '/elsewhere/a')); same(1, cp(parseCommand('cp -r alias/ copy'), $base)['status']); }); -$tests['cp rejects type collisions, missing parents and special files'] = fn() => fixture(function ($base) { - mkdir($base . '/tree'); mkdir($base . '/out'); mkdir($base . '/out/a'); file_put_contents($base . '/a', 'a'); - same(1, cp(parseCommand('cp a out'), $base)['status']); check(is_dir($base . '/out/a')); - same(1, cp(parseCommand('cp -r tree a'), $base)['status']); same('a', file_get_contents($base . '/a')); +$tests['cp rejects type collisions, missing parents and special files'] = fn () => fixture(function ($base) { + mkdir($base . '/tree'); + mkdir($base . '/out'); + mkdir($base . '/out/a'); + file_put_contents($base . '/a', 'a'); + same(1, cp(parseCommand('cp a out'), $base)['status']); + check(is_dir($base . '/out/a')); + same(1, cp(parseCommand('cp -r tree a'), $base)['status']); + same('a', file_get_contents($base . '/a')); same(1, cp(parseCommand('cp a missing/file'), $base)['status']); same(1, cp(parseCommand('cp a missing/'), $base)['status']); - if (function_exists('posix_mkfifo') && coreutilsFsCall(fn() => posix_mkfifo($base . '/fifo', 0600))) { + if (function_exists('posix_mkfifo') && coreutilsFsCall(fn () => posix_mkfifo($base . '/fifo', 0600))) { same(1, cp(parseCommand('cp fifo copy'), $base)['status']); same(1, cp(parseCommand('cp a fifo'), $base)['status']); } }); -$tests['cp handles absolute paths, dash names, cwd and escaped verbose text'] = fn() => fixture(function ($base) { +$tests['cp handles absolute paths, dash names, cwd and escaped verbose text'] = fn () => fixture(function ($base) { file_put_contents($base . '/-a', 'a'); - $cwd = getcwd(); $locale = setlocale(LC_ALL, 0); $mask = umask(); - $result = cp(parseCommand('cp -v -- -a ""'), $base); same(0, $result['status']); + $cwd = getcwd(); + $locale = setlocale(LC_ALL, 0); + $mask = umask(); + $result = cp(parseCommand('cp -v -- -a ""'), $base); + same(0, $result['status']); check(strpos(coreutilsHtml($result), '<new name>') !== false); check(strpos(coreutilsText($result), ' -> ') !== false); same(0, cp(['args' => [$base . '/-a', $base . '/absolute']], $base)['status']); - chdir($base); try { same(0, cp(parseCommand('cp -- -a default-cwd'))['status']); } finally { chdir($cwd); } - same($cwd, getcwd()); same($locale, setlocale(LC_ALL, 0)); same($mask, umask()); + chdir($base); + try { + same(0, cp(parseCommand('cp -- -a default-cwd'))['status']); + } finally { + chdir($cwd); + } + same($cwd, getcwd()); + same($locale, setlocale(LC_ALL, 0)); + same($mask, umask()); }); -$tests['cp refuses overwriting an earlier copy with a second source of the same name'] = fn() => fixture(function ($base) { - mkdir($base . '/one'); mkdir($base . '/two'); mkdir($base . '/out'); - file_put_contents($base . '/one/a', 'one'); file_put_contents($base . '/two/a', 'two'); - same(1, cp(parseCommand('cp one/a two/a out'), $base)['status']); same('one', file_get_contents($base . '/out/a')); - same('one', file_get_contents($base . '/one/a')); same('two', file_get_contents($base . '/two/a')); +$tests['cp refuses overwriting an earlier copy with a second source of the same name'] = fn () => fixture(function ($base) { + mkdir($base . '/one'); + mkdir($base . '/two'); + mkdir($base . '/out'); + file_put_contents($base . '/one/a', 'one'); + file_put_contents($base . '/two/a', 'two'); + same(1, cp(parseCommand('cp one/a two/a out'), $base)['status']); + same('one', file_get_contents($base . '/out/a')); + same('one', file_get_contents($base . '/one/a')); + same('two', file_get_contents($base . '/two/a')); }); diff --git a/tests/find.php b/tests/find.php index 9dfed47..e78c04e 100644 --- a/tests/find.php +++ b/tests/find.php @@ -23,8 +23,9 @@ same(2, find($input)['status']); } }; -$tests['find defaults to dot and traverses hidden nested entries in order'] = fn() => fixture(function ($base) { - mkdir($base . '/dir'); mkdir($base . '/dir/empty'); +$tests['find defaults to dot and traverses hidden nested entries in order'] = fn () => fixture(function ($base) { + mkdir($base . '/dir'); + mkdir($base . '/dir/empty'); file_put_contents($base . '/dir/item', 'x'); file_put_contents($base . '/.hidden', 'x'); $cwd = getcwd(); @@ -34,22 +35,26 @@ same($cwd, getcwd()); same(0, $result['status']); $expected = ['.', './.hidden', './dir', './dir/empty', './dir/item']; - $expected = array_map(fn($name) => str_replace('/', DIRECTORY_SEPARATOR, $name), $expected); + $expected = array_map(fn ($name) => str_replace('/', DIRECTORY_SEPARATOR, $name), $expected); same($expected, array_column($result['data']['entries'], 'name')); same(['d', 'f', 'd', 'd', 'f'], array_column($result['data']['entries'], 'type')); same(implode("\n", $expected) . "\n", coreutilsText($result)); - same(['./.hidden', './dir/item'], array_map(fn($entry) => str_replace(DIRECTORY_SEPARATOR, '/', $entry['name']), - find(parseCommand('find -type f'), $base)['data']['entries'])); + same(['./.hidden', './dir/item'], array_map( + fn ($entry) => str_replace(DIRECTORY_SEPARATOR, '/', $entry['name']), + find(parseCommand('find -type f'), $base)['data']['entries'] + )); same(3, count(find(parseCommand('find -type d'), $base)['data']['entries'])); same([], find(parseCommand('find -type l'), $base)['data']['entries']); chdir($base); same($result['data'], find(parseCommand('find'))['data']); }); -$tests['find lists links including broken links without following directory cycles'] = fn() => fixture(function ($base) { +$tests['find lists links including broken links without following directory cycles'] = fn () => fixture(function ($base) { symlinkSupport($base); - mkdir($base . '/dir'); file_put_contents($base . '/file', 'x'); + mkdir($base . '/dir'); + file_put_contents($base . '/file', 'x'); symlink('../', $base . '/dir/cycle'); - symlink('missing', $base . '/broken'); symlink('file', $base . '/file-link'); + symlink('missing', $base . '/broken'); + symlink('file', $base . '/file-link'); symlink('dir', $base . '/dir-link'); same(7, count(find(parseCommand('find'), $base)['data']['entries'])); foreach (['f' => 1, 'd' => 2, 'l' => 4, 'f,l' => 5, 'd,l' => 6, 'f,d,l' => 7, 'l,f,l' => 5] as $types => $count) { @@ -61,10 +66,12 @@ same(['dir-link', 'broken'], array_column($result['data']['entries'], 'name')); same(1, find(parseCommand('find dir-link/'), $base)['status']); }); -$tests['find supports multiple roots absolute paths and partial failures'] = fn() => fixture(function ($base) { - mkdir($base . '/empty'); file_put_contents($base . '/file', 'x'); +$tests['find supports multiple roots absolute paths and partial failures'] = fn () => fixture(function ($base) { + mkdir($base . '/empty'); + file_put_contents($base . '/file', 'x'); $result = find(['args' => ['missing', 'file', $base . '/empty']], $base); - same(1, $result['status']); same(1, count($result['errors'])); + same(1, $result['status']); + same(1, count($result['errors'])); same(['file', $base . '/empty'], array_column($result['data']['entries'], 'name')); same(coreutilsResolvePath('file', $base), $result['data']['entries'][0]['path']); same(0, find(parseCommand('find empty/'), $base)['status']); @@ -72,7 +79,7 @@ same(1, find(parseCommand('find'), $base . '/missing')['status']); same(0, find(parseCommand('find file'), $base)['status']); }); -$tests['find handles quoted dash-prefixed names and escapes HTML'] = fn() => fixture(function ($base) { +$tests['find handles quoted dash-prefixed names and escapes HTML'] = fn () => fixture(function ($base) { file_put_contents($base . '/-type', 'x'); file_put_contents($base . '/two words', 'x'); same(['-type'], array_column(find(parseCommand('find -- -type'), $base)['data']['entries'], 'name')); @@ -84,9 +91,11 @@ check(strpos(coreutilsHtml($result), '<tag>') !== false); check(strpos(coreutilsHtml($result), '') === false); }); -$tests['find reports unreadable directories and continues other roots'] = fn() => fixture(function ($base) { - mkdir($base . '/denied'); mkdir($base . '/good'); chmod($base . '/denied', 0000); - skipUnless(coreutilsFsCall(fn() => scandir($base . '/denied')) === false, 'process can bypass filesystem permissions'); +$tests['find reports unreadable directories and continues other roots'] = fn () => fixture(function ($base) { + mkdir($base . '/denied'); + mkdir($base . '/good'); + chmod($base . '/denied', 0000); + skipUnless(coreutilsFsCall(fn () => scandir($base . '/denied')) === false, 'process can bypass filesystem permissions'); $result = find(parseCommand('find denied good -type d'), $base); same(1, $result['status']); same(['denied', 'good'], array_column($result['data']['entries'], 'name')); @@ -119,7 +128,7 @@ same(['logical' => true, 'physical' => true], parseCommand('pwd -LP')['options']); }; -$tests['find intersects repeated types instead of overwriting or joining them'] = fn() => fixture(function ($base) { +$tests['find intersects repeated types instead of overwriting or joining them'] = fn () => fixture(function ($base) { mkdir($base . '/dir'); file_put_contents($base . '/file', 'x'); foreach (['find . -type f,d -type f', 'find . --type=f,d --type=f'] as $command) { @@ -140,7 +149,7 @@ same(['l', 'f'], array_column(find(parseCommand('find . -type f,l'), $base)['data']['entries'], 'type')); }); -$tests['find name matches basenames and traverses nonmatching directories'] = fn() => fixture(function ($base) { +$tests['find name matches basenames and traverses nonmatching directories'] = fn () => fixture(function ($base) { mkdir($base . '/unmatched'); mkdir($base . '/folder.php'); foreach (['.hidden.php', 'test1.php', 'test2.php', 'test3.txt', 'UPPER.PHP'] as $file) { @@ -151,7 +160,7 @@ 'find . --name="*.php" --type=f'] as $command) { $result = find(parseCommand($command), $base); same(0, $result['status']); - same($expected, array_map(fn($entry) => basename($entry['name']), $result['data']['entries'])); + same($expected, array_map(fn ($entry) => basename($entry['name']), $result['data']['entries'])); } same(4, count(find(parseCommand('find . -name "*.php"'), $base)['data']['entries'])); same([], find(parseCommand('find . -name "unmatched/*.php"'), $base)['data']['entries']); @@ -160,14 +169,14 @@ same(['unmatched/'], array_column(find(parseCommand('find unmatched/ -name unmatched'), $base)['data']['entries'], 'name')); }); -$tests['find name supports wildcards sets escapes and repeated predicates'] = fn() => fixture(function ($base) { +$tests['find name supports wildcards sets escapes and repeated predicates'] = fn () => fixture(function ($base) { foreach (['test1.php', 'test2.php', 'testa.php', 'other.php', 'two words', '[tag]'] as $file) { file_put_contents($base . '/' . $file, 'x'); } $matches = function ($command) use ($base) { $result = find(parseCommand($command), $base); same(0, $result['status']); - return array_map(fn($entry) => basename($entry['name']), $result['data']['entries']); + return array_map(fn ($entry) => basename($entry['name']), $result['data']['entries']); }; same(['test1.php', 'test2.php', 'testa.php'], $matches('find . -name "test?.php"')); same(['test1.php', 'test2.php'], $matches('find . -name "test[1-2].php"')); @@ -179,7 +188,7 @@ same(['*.php', 'test*'], parseCommand('find . -name "*.php" -name "test*"')['options']['name']); }); -$tests['find name tests link names including broken links without following them'] = fn() => fixture(function ($base) { +$tests['find name tests link names including broken links without following them'] = fn () => fixture(function ($base) { symlinkSupport($base); mkdir($base . '/real'); file_put_contents($base . '/real/target.php', 'x'); @@ -188,15 +197,17 @@ symlink('real/target.php', $base . '/other.txt'); $result = find(parseCommand('find . -type l -name "*.php"'), $base); same(0, $result['status']); - same(['alias.php', 'broken.php'], array_map(fn($entry) => basename($entry['name']), $result['data']['entries'])); + same(['alias.php', 'broken.php'], array_map(fn ($entry) => basename($entry['name']), $result['data']['entries'])); same(3, count(find(parseCommand('find . -name "*.php"'), $base)['data']['entries'])); }); -$tests['find canonical predicates accept strings and nonempty arrays only'] = fn() => fixture(function ($base) { +$tests['find canonical predicates accept strings and nonempty arrays only'] = fn () => fixture(function ($base) { file_put_contents($base . '/test.php', 'x'); $input = ['options' => ['type' => ['f,d', 'f'], 'name' => ['*.php', 'test*']]]; - same(find(parseCommand('find -type f,d -name "*.php" -type f -name "test*"'), $base)['data'], - find($input, $base)['data']); + same( + find(parseCommand('find -type f,d -name "*.php" -type f -name "test*"'), $base)['data'], + find($input, $base)['data'] + ); foreach (['type', 'name'] as $option) { foreach ([[], [null], [['f']], ['f', true], false, null, 1, "x\0y", ["x\0y"]] as $value) { $result = find(['options' => [$option => $value]], '/does-not-exist'); diff --git a/tests/mv.php b/tests/mv.php index a14a09d..6dd42e6 100644 --- a/tests/mv.php +++ b/tests/mv.php @@ -1,5 +1,6 @@ fixture(function ($base) { + +$tests['mv renames silently and returns structured paths'] = fn () => fixture(function ($base) { file_put_contents($base . '/old', 'content'); ob_start(); $result = mv(parseCommand('mv old new'), $base); @@ -10,54 +11,69 @@ same([['source' => $base . '/old', 'destination' => $base . '/new']], $result['data']['moved']); same('', coreutilsText($result)); }); -$tests['mv moves multiple files and a populated directory'] = fn() => fixture(function ($base) { - mkdir($base . '/out'); mkdir($base . '/tree'); - file_put_contents($base . '/tree/child', 'child'); file_put_contents($base . '/file', 'file'); +$tests['mv moves multiple files and a populated directory'] = fn () => fixture(function ($base) { + mkdir($base . '/out'); + mkdir($base . '/tree'); + file_put_contents($base . '/tree/child', 'child'); + file_put_contents($base . '/file', 'file'); same(0, mv(parseCommand('mv file tree out'), $base)['status']); same('child', file_get_contents($base . '/out/tree/child')); same('file', file_get_contents($base . '/out/file')); }); -$tests['mv clobber precedence and verbose escaping'] = fn() => fixture(function ($base) { - file_put_contents($base . '/a', 'new'); file_put_contents($base . '/b', 'old'); +$tests['mv clobber precedence and verbose escaping'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'new'); + file_put_contents($base . '/b', 'old'); $result = mv(parseCommand('mv -fn a b'), $base); - same(0, $result['status']); same(1, count($result['data']['skipped'])); + same(0, $result['status']); + same(1, count($result['data']['skipped'])); same('old', file_get_contents($base . '/b')); same(0, mv(parseCommand('mv -nf a b'), $base)['status']); same('new', file_get_contents($base . '/b')); $result = mv(parseCommand('mv -v b ""'), $base); - same(0, $result['status']); check(strpos(coreutilsText($result), ' -> ') !== false); + same(0, $result['status']); + check(strpos(coreutilsText($result), ' -> ') !== false); check(strpos(coreutilsHtml($result), '<new name>') !== false); }); -$tests['mv default overwrites files and supports absolute paths'] = fn() => fixture(function ($base) { - file_put_contents($base . '/a', 'a'); file_put_contents($base . '/b', 'b'); +$tests['mv default overwrites files and supports absolute paths'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'a'); + file_put_contents($base . '/b', 'b'); same(0, mv(['args' => [$base . '/a', $base . '/b']], $base)['status']); same('a', file_get_contents($base . '/b')); }); -$tests['mv invalid input has no side effects and help needs no cwd'] = fn() => fixture(function ($base) { +$tests['mv invalid input has no side effects and help needs no cwd'] = fn () => fixture(function ($base) { file_put_contents($base . '/a', 'a'); foreach (['mv a', 'mv -z a b', 'mv a ""', 'mv a b https://example.com'] as $command) { - same(2, mv(parseCommand($command), $base)['status']); check(file_exists($base . '/a')); + same(2, mv(parseCommand($command), $base)['status']); + check(file_exists($base . '/a')); } same(0, mv(parseCommand('mv --help'), '/missing')['status']); check(strpos(coreutilsText(mv(parseCommand('mv --help'))), 'Usage: mv') === 0); }); -$tests['mv errors preserve sources and continue independent operands'] = fn() => fixture(function ($base) { - file_put_contents($base . '/a', 'a'); mkdir($base . '/out'); +$tests['mv errors preserve sources and continue independent operands'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'a'); + mkdir($base . '/out'); foreach (['mv a a', 'mv a missing/b', 'mv a absent/', 'mv a missing out/file'] as $command) { - same(1, mv(parseCommand($command), $base)['status']); check(file_exists($base . '/a')); + same(1, mv(parseCommand($command), $base)['status']); + check(file_exists($base . '/a')); } $result = mv(parseCommand('mv missing a out'), $base); - same(1, $result['status']); same(1, count($result['data']['moved'])); + same(1, $result['status']); + same(1, count($result['data']['moved'])); same('a', file_get_contents($base . '/out/a')); }); -$tests['mv rejects self descendants and preserves nonempty destinations'] = fn() => fixture(function ($base) { - mkdir($base . '/tree'); mkdir($base . '/tree/sub'); file_put_contents($base . '/tree/a', 'a'); +$tests['mv rejects self descendants and preserves nonempty destinations'] = fn () => fixture(function ($base) { + mkdir($base . '/tree'); + mkdir($base . '/tree/sub'); + file_put_contents($base . '/tree/a', 'a'); same(1, mv(parseCommand('mv tree tree/sub'), $base)['status']); - mkdir($base . '/out'); mkdir($base . '/out/tree'); file_put_contents($base . '/out/tree/b', 'b'); + mkdir($base . '/out'); + mkdir($base . '/out/tree'); + file_put_contents($base . '/out/tree/b', 'b'); same(1, mv(parseCommand('mv tree out'), $base)['status']); - same('a', file_get_contents($base . '/tree/a')); same('b', file_get_contents($base . '/out/tree/b')); + same('a', file_get_contents($base . '/tree/a')); + same('b', file_get_contents($base . '/out/tree/b')); }); -$tests['mv preserves source links and no-clobber detects broken targets'] = fn() => fixture(function ($base) { +$tests['mv preserves source links and no-clobber detects broken targets'] = fn () => fixture(function ($base) { symlinkSupport($base); symlink('missing', $base . '/broken'); same(0, mv(parseCommand('mv broken renamed'), $base)['status']); @@ -65,23 +81,35 @@ file_put_contents($base . '/a', 'a'); same(1, count(mv(parseCommand('mv -n a renamed'), $base)['data']['skipped'])); same(0, mv(parseCommand('mv a renamed'), $base)['status']); - check(!is_link($base . '/renamed')); same('a', file_get_contents($base . '/renamed')); - mkdir($base . '/dir'); symlink('dir', $base . '/link'); + check(!is_link($base . '/renamed')); + same('a', file_get_contents($base . '/renamed')); + mkdir($base . '/dir'); + symlink('dir', $base . '/link'); same(1, mv(parseCommand('mv link/ elsewhere'), $base)['status']); same(0, mv(parseCommand('mv link moved-link'), $base)['status']); - check(is_dir($base . '/dir')); same('dir', readlink($base . '/moved-link')); + check(is_dir($base . '/dir')); + same('dir', readlink($base . '/moved-link')); }); -$tests['mv rejects descendant via link and accepts destination directory link'] = fn() => fixture(function ($base) { - symlinkSupport($base); mkdir($base . '/tree'); mkdir($base . '/tree/sub'); symlink('tree/sub', $base . '/alias'); +$tests['mv rejects descendant via link and accepts destination directory link'] = fn () => fixture(function ($base) { + symlinkSupport($base); + mkdir($base . '/tree'); + mkdir($base . '/tree/sub'); + symlink('tree/sub', $base . '/alias'); same(1, mv(parseCommand('mv tree alias'), $base)['status']); - file_put_contents($base . '/a', 'a'); same(0, mv(parseCommand('mv a alias'), $base)['status']); + file_put_contents($base . '/a', 'a'); + same(0, mv(parseCommand('mv a alias'), $base)['status']); same('a', file_get_contents($base . '/tree/sub/a')); }); -$tests['mv refuses duplicate output names and supports --'] = fn() => fixture(function ($base) { - mkdir($base . '/one'); mkdir($base . '/two'); mkdir($base . '/out'); - file_put_contents($base . '/one/a', 'one'); file_put_contents($base . '/two/a', 'two'); +$tests['mv refuses duplicate output names and supports --'] = fn () => fixture(function ($base) { + mkdir($base . '/one'); + mkdir($base . '/two'); + mkdir($base . '/out'); + file_put_contents($base . '/one/a', 'one'); + file_put_contents($base . '/two/a', 'two'); same(1, mv(parseCommand('mv one/a two/a out'), $base)['status']); - same('one', file_get_contents($base . '/out/a')); same('two', file_get_contents($base . '/two/a')); + same('one', file_get_contents($base . '/out/a')); + same('two', file_get_contents($base . '/two/a')); file_put_contents($base . '/-a', 'dash'); - same(0, mv(parseCommand('mv -- -a -b'), $base)['status']); same('dash', file_get_contents($base . '/-b')); + same(0, mv(parseCommand('mv -- -a -b'), $base)['status']); + same('dash', file_get_contents($base . '/-b')); }); diff --git a/tests/pwd.php b/tests/pwd.php index 1f018b5..b5afe11 100644 --- a/tests/pwd.php +++ b/tests/pwd.php @@ -1,6 +1,6 @@ fixture(function ($base) { +$tests['pwd physical default and explicit cwd have no output or process changes'] = fn () => fixture(function ($base) { $cwd = getcwd(); $environment = getenv('PWD'); ob_start(); @@ -16,7 +16,7 @@ same(realpath($base), pwd(parseCommand('pwd --'), $base)['data']['path']); }); -$tests['pwd preserves logical directory links and resolves physical ones'] = fn() => fixture(function ($base) { +$tests['pwd preserves logical directory links and resolves physical ones'] = fn () => fixture(function ($base) { symlinkSupport($base); mkdir($base . '/real'); mkdir($base . '/real/child'); @@ -36,7 +36,7 @@ same(realpath($logical), pwd(['options' => ['logical' => false]], $logical)['data']['path']); }); -$tests['pwd logical uses PWD only when it identifies the current process directory'] = fn() => fixture(function ($base) { +$tests['pwd logical uses PWD only when it identifies the current process directory'] = fn () => fixture(function ($base) { symlinkSupport($base); mkdir($base . '/real'); symlink('real', $base . '/alias'); @@ -63,7 +63,7 @@ } }); -$tests['pwd relative and dot paths retain filesystem symlink semantics'] = fn() => fixture(function ($base) { +$tests['pwd relative and dot paths retain filesystem symlink semantics'] = fn () => fixture(function ($base) { symlinkSupport($base); mkdir($base . '/real'); mkdir($base . '/real/child'); @@ -88,7 +88,7 @@ } }; -$tests['pwd invalid cwd and broken links produce controlled reusable errors'] = fn() => fixture(function ($base) { +$tests['pwd invalid cwd and broken links produce controlled reusable errors'] = fn () => fixture(function ($base) { file_put_contents($base . '/file', 'x'); foreach (['', $base . '/missing', $base . '/file', "bad\0path", 'file://' . $base] as $directory) { foreach (['pwd -L', 'pwd -P'] as $command) { @@ -105,7 +105,7 @@ same(1, pwd(parseCommand('pwd -L'), $base . '/broken')['status']); }); -$tests['pwd repeated calls observe changed link targets'] = fn() => fixture(function ($base) { +$tests['pwd repeated calls observe changed link targets'] = fn () => fixture(function ($base) { symlinkSupport($base); mkdir($base . '/one'); mkdir($base . '/two'); @@ -116,7 +116,7 @@ same(realpath($base . '/two'), pwd(parseCommand('pwd -P'), $base . '/alias')['data']['path']); }); -$tests['pwd text preserves spaces and HTML escapes the raw path'] = fn() => fixture(function ($base) { +$tests['pwd text preserves spaces and HTML escapes the raw path'] = fn () => fixture(function ($base) { skipUnless(DIRECTORY_SEPARATOR !== '\\', 'Windows forbids angle brackets in filenames'); $path = $base . '/'; mkdir($path); diff --git a/tests/removal.php b/tests/removal.php index 8a7bd7f..d137232 100644 --- a/tests/removal.php +++ b/tests/removal.php @@ -1,94 +1,164 @@ fixture(function ($base) { - file_put_contents($base . '/a', 'a'); file_put_contents($base . '/keep', 'keep'); - ob_start(); $result = rm(parseCommand('rm a'), $base); same('', ob_get_clean()); - same(0, $result['status']); same([$base . '/a'], $result['data']['removed']); - same('', coreutilsText($result)); check(!file_exists($base . '/a')); same('keep', file_get_contents($base . '/keep')); + +$tests['rm removes files silently and records absolute paths'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'a'); + file_put_contents($base . '/keep', 'keep'); + ob_start(); + $result = rm(parseCommand('rm a'), $base); + same('', ob_get_clean()); + same(0, $result['status']); + same([$base . '/a'], $result['data']['removed']); + same('', coreutilsText($result)); + check(!file_exists($base . '/a')); + same('keep', file_get_contents($base . '/keep')); }); -$tests['rm requires recursive for directories even with force'] = fn() => fixture(function ($base) { - mkdir($base . '/dir'); file_put_contents($base . '/dir/a', 'a'); - foreach (['rm dir', 'rm -f dir'] as $command) same(1, rm(parseCommand($command), $base)['status']); +$tests['rm requires recursive for directories even with force'] = fn () => fixture(function ($base) { + mkdir($base . '/dir'); + file_put_contents($base . '/dir/a', 'a'); + foreach (['rm dir', 'rm -f dir'] as $command) { + same(1, rm(parseCommand($command), $base)['status']); + } same('a', file_get_contents($base . '/dir/a')); }); -$tests['rm recursive handles nested and hidden entries in child-first order'] = fn() => fixture(function ($base) { - mkdir($base . '/dir'); mkdir($base . '/dir/sub'); mkdir($base . '/dir/empty'); - file_put_contents($base . '/dir/sub/a', 'a'); file_put_contents($base . '/dir/.hidden', 'h'); - $result = rm(parseCommand('rm -rv dir/'), $base); same(0, $result['status']); - same(5, count($result['data']['removed'])); same($base . '/dir', end($result['data']['removed'])); - check(!file_exists($base . '/dir')); check(strpos(coreutilsText($result), 'removed ') === 0); +$tests['rm recursive handles nested and hidden entries in child-first order'] = fn () => fixture(function ($base) { + mkdir($base . '/dir'); + mkdir($base . '/dir/sub'); + mkdir($base . '/dir/empty'); + file_put_contents($base . '/dir/sub/a', 'a'); + file_put_contents($base . '/dir/.hidden', 'h'); + $result = rm(parseCommand('rm -rv dir/'), $base); + same(0, $result['status']); + same(5, count($result['data']['removed'])); + same($base . '/dir', end($result['data']['removed'])); + check(!file_exists($base . '/dir')); + check(strpos(coreutilsText($result), 'removed ') === 0); }); -$tests['rm force ignores proven missing paths and no operands only'] = fn() => fixture(function ($base) { - same(2, rm(parseCommand('rm'), $base)['status']); same(0, rm(parseCommand('rm -f'), '/missing')['status']); +$tests['rm force ignores proven missing paths and no operands only'] = fn () => fixture(function ($base) { + same(2, rm(parseCommand('rm'), $base)['status']); + same(0, rm(parseCommand('rm -f'), '/missing')['status']); same(1, rm(parseCommand('rm missing'), $base)['status']); $result = rm(parseCommand('rm -f missing missing-parent/child'), $base); - same(0, $result['status']); same(2, count($result['data']['skipped'])); + same(0, $result['status']); + same(2, count($result['data']['skipped'])); file_put_contents($base . '/file', 'x'); - same(1, rm(parseCommand('rm -f file/child'), $base)['status']); same('x', file_get_contents($base . '/file')); + same(1, rm(parseCommand('rm -f file/child'), $base)['status']); + same('x', file_get_contents($base . '/file')); }); -$tests['removal validates all syntax before deleting anything and supports help'] = fn() => fixture(function ($base) { - file_put_contents($base . '/a', 'a'); mkdir($base . '/empty'); +$tests['removal validates all syntax before deleting anything and supports help'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'a'); + mkdir($base . '/empty'); foreach (['rm' => 'rm', 'rmdir' => '_rmdir'] as $command => $function) { $operand = $command === 'rm' ? 'a' : 'empty'; foreach ([' -z ' . $operand, ' ' . $operand . ' ""', ' ' . $operand . ' https://example.com'] as $suffix) { - same(2, $function(parseCommand($command . $suffix), $base)['status']); check(file_exists($base . '/' . $operand)); + same(2, $function(parseCommand($command . $suffix), $base)['status']); + check(file_exists($base . '/' . $operand)); } same(2, $function(['args' => [$operand], 'options' => ['verbose' => 1]], $base)['status']); same(1, $function(parseCommand($command . ' ' . $operand), $base . '/absent')['status']); - $result = $function(parseCommand($command . ' --help'), '/missing'); same(0, $result['status']); + $result = $function(parseCommand($command . ' --help'), '/missing'); + same(0, $result['status']); check(strpos(coreutilsText($result), 'Usage: ' . $command) === 0); } }); -$tests['removal continues after failures and preserves partial results'] = fn() => fixture(function ($base) { - file_put_contents($base . '/a', 'a'); file_put_contents($base . '/b', 'b'); - $result = rm(parseCommand('rm a absent b'), $base); same(1, $result['status']); same(2, count($result['data']['removed'])); - mkdir($base . '/full'); file_put_contents($base . '/full/keep', 'keep'); mkdir($base . '/empty'); - $result = _rmdir(parseCommand('rmdir full empty'), $base); same(1, $result['status']); same([$base . '/empty'], $result['data']['removed']); +$tests['removal continues after failures and preserves partial results'] = fn () => fixture(function ($base) { + file_put_contents($base . '/a', 'a'); + file_put_contents($base . '/b', 'b'); + $result = rm(parseCommand('rm a absent b'), $base); + same(1, $result['status']); + same(2, count($result['data']['removed'])); + mkdir($base . '/full'); + file_put_contents($base . '/full/keep', 'keep'); + mkdir($base . '/empty'); + $result = _rmdir(parseCommand('rmdir full empty'), $base); + same(1, $result['status']); + same([$base . '/empty'], $result['data']['removed']); same('keep', file_get_contents($base . '/full/keep')); }); -$tests['removal protects roots dots cwd and cwd ancestors before traversal'] = fn() => fixture(function ($base) { - mkdir($base . '/sub'); file_put_contents($base . '/keep', 'keep'); +$tests['removal protects roots dots cwd and cwd ancestors before traversal'] = fn () => fixture(function ($base) { + mkdir($base . '/sub'); + file_put_contents($base . '/keep', 'keep'); foreach (['rm -rf' => 'rm', 'rmdir' => '_rmdir'] as $command => $function) { foreach (['/', '.', '..', 'sub/..', $base, dirname($base)] as $path) { $result = $function(parseCommand($command . ' "' . $path . '"'), $base); - same(1, $result['status']); same([], $result['data']['removed']); + same(1, $result['status']); + same([], $result['data']['removed']); } } same('keep', file_get_contents($base . '/keep')); }); -$tests['rm never follows final symlinks including broken links and directory cycles'] = fn() => fixture(function ($base) { - symlinkSupport($base); mkdir($base . '/keep'); file_put_contents($base . '/keep/a', 'a'); - symlink('keep', $base . '/link'); symlink('missing', $base . '/broken'); - same(1, rm(parseCommand('rm -rf link/'), $base)['status']); check(is_link($base . '/link')); - same(0, rm(parseCommand('rm link broken'), $base)['status']); check(!is_link($base . '/link')); check(!is_link($base . '/broken')); - mkdir($base . '/tree'); symlink('../keep', $base . '/tree/outside'); symlink('.', $base . '/tree/cycle'); - same(0, rm(parseCommand('rm -r tree'), $base)['status']); same('a', file_get_contents($base . '/keep/a')); +$tests['rm never follows final symlinks including broken links and directory cycles'] = fn () => fixture(function ($base) { + symlinkSupport($base); + mkdir($base . '/keep'); + file_put_contents($base . '/keep/a', 'a'); + symlink('keep', $base . '/link'); + symlink('missing', $base . '/broken'); + same(1, rm(parseCommand('rm -rf link/'), $base)['status']); + check(is_link($base . '/link')); + same(0, rm(parseCommand('rm link broken'), $base)['status']); + check(!is_link($base . '/link')); + check(!is_link($base . '/broken')); + mkdir($base . '/tree'); + symlink('../keep', $base . '/tree/outside'); + symlink('.', $base . '/tree/cycle'); + same(0, rm(parseCommand('rm -r tree'), $base)['status']); + same('a', file_get_contents($base . '/keep/a')); }); -$tests['rmdir removes only empty real directories including trailing slash'] = fn() => fixture(function ($base) { - mkdir($base . '/empty'); file_put_contents($base . '/file', 'keep'); +$tests['rmdir removes only empty real directories including trailing slash'] = fn () => fixture(function ($base) { + mkdir($base . '/empty'); + file_put_contents($base . '/file', 'keep'); same(2, _rmdir(parseCommand('rmdir'), $base)['status']); - same(1, _rmdir(parseCommand('rmdir file'), $base)['status']); same('keep', file_get_contents($base . '/file')); - $result = _rmdir(parseCommand('rmdir empty/'), $base); same(0, $result['status']); same([$base . '/empty'], $result['data']['removed']); + same(1, _rmdir(parseCommand('rmdir file'), $base)['status']); + same('keep', file_get_contents($base . '/file')); + $result = _rmdir(parseCommand('rmdir empty/'), $base); + same(0, $result['status']); + same([$base . '/empty'], $result['data']['removed']); }); -$tests['rmdir rejects links with and without trailing slash'] = fn() => fixture(function ($base) { - symlinkSupport($base); mkdir($base . '/empty'); symlink('empty', $base . '/link'); - foreach (['link', 'link/'] as $name) same(1, _rmdir(parseCommand('rmdir ' . $name), $base)['status']); - check(is_dir($base . '/empty')); check(is_link($base . '/link')); +$tests['rmdir rejects links with and without trailing slash'] = fn () => fixture(function ($base) { + symlinkSupport($base); + mkdir($base . '/empty'); + symlink('empty', $base . '/link'); + foreach (['link', 'link/'] as $name) { + same(1, _rmdir(parseCommand('rmdir ' . $name), $base)['status']); + } + check(is_dir($base . '/empty')); + check(is_link($base . '/link')); }); -$tests['rmdir parents stops before cwd and reports a nonempty parent'] = fn() => fixture(function ($base) { - mkdir($base . '/a'); mkdir($base . '/a/b'); mkdir($base . '/a/b/c'); - $result = _rmdir(parseCommand('rmdir -pv a/b/c'), $base); same(0, $result['status']); - same([$base . '/a/b/c', $base . '/a/b', $base . '/a'], $result['data']['removed']); check(is_dir($base)); - mkdir($base . '/a'); mkdir($base . '/a/b'); file_put_contents($base . '/a/keep', 'keep'); - $result = _rmdir(parseCommand('rmdir -p a/b'), $base); same(1, $result['status']); same([$base . '/a/b'], $result['data']['removed']); +$tests['rmdir parents stops before cwd and reports a nonempty parent'] = fn () => fixture(function ($base) { + mkdir($base . '/a'); + mkdir($base . '/a/b'); + mkdir($base . '/a/b/c'); + $result = _rmdir(parseCommand('rmdir -pv a/b/c'), $base); + same(0, $result['status']); + same([$base . '/a/b/c', $base . '/a/b', $base . '/a'], $result['data']['removed']); + check(is_dir($base)); + mkdir($base . '/a'); + mkdir($base . '/a/b'); + file_put_contents($base . '/a/keep', 'keep'); + $result = _rmdir(parseCommand('rmdir -p a/b'), $base); + same(1, $result['status']); + same([$base . '/a/b'], $result['data']['removed']); same('keep', file_get_contents($base . '/a/keep')); }); -$tests['removal supports dash names absolute operands escaped output and default cwd'] = fn() => fixture(function ($base) { - $cwd = getcwd(); $locale = setlocale(LC_ALL, 0); $mask = umask(); - file_put_contents($base . '/-a', 'a'); same(0, rm(parseCommand('rm -- -a'), $base)['status']); - mkdir($base . '/'); $result = _rmdir(parseCommand('rmdir -v ""'), $base); - same(0, $result['status']); check(strpos(coreutilsHtml($result), '<empty>') !== false); - file_put_contents($base . '/absolute', 'a'); same(0, rm(['args' => [$base . '/absolute']], $base)['status']); - mkdir($base . '/empty'); chdir($base); - try { same(0, _rmdir(parseCommand('rmdir empty'))['status']); } finally { chdir($cwd); } - same($cwd, getcwd()); same($locale, setlocale(LC_ALL, 0)); same($mask, umask()); +$tests['removal supports dash names absolute operands escaped output and default cwd'] = fn () => fixture(function ($base) { + $cwd = getcwd(); + $locale = setlocale(LC_ALL, 0); + $mask = umask(); + file_put_contents($base . '/-a', 'a'); + same(0, rm(parseCommand('rm -- -a'), $base)['status']); + mkdir($base . '/'); + $result = _rmdir(parseCommand('rmdir -v ""'), $base); + same(0, $result['status']); + check(strpos(coreutilsHtml($result), '<empty>') !== false); + file_put_contents($base . '/absolute', 'a'); + same(0, rm(['args' => [$base . '/absolute']], $base)['status']); + mkdir($base . '/empty'); + chdir($base); + try { + same(0, _rmdir(parseCommand('rmdir empty'))['status']); + } finally { + chdir($cwd); + } + same($cwd, getcwd()); + same($locale, setlocale(LC_ALL, 0)); + same($mask, umask()); }); diff --git a/tests/run.php b/tests/run.php index 2469665..f28d6c3 100644 --- a/tests/run.php +++ b/tests/run.php @@ -1,8 +1,11 @@ symlink('missing', $base . '/symlink-probe')); + $success = coreutilsFsCall(fn () => symlink('missing', $base . '/symlink-probe')); skipUnless($success, 'runtime cannot create symlinks'); unlink($base . '/symlink-probe'); } -function names(array $result): array { +function names(array $result): array +{ return array_column($result['data']['directories'][0]['entries'], 'name'); } @@ -114,19 +134,19 @@ function names(array $result): array { check(count(parseCommand($input)['errors']) > 0, $input); } }; -$tests['missing option values are rejected before creation'] = fn() => fixture(function ($base) { +$tests['missing option values are rejected before creation'] = fn () => fixture(function ($base) { foreach (['mkdir target -m', 'mkdir target --mode', 'mkdir -m -- target'] as $command) { same(2, _mkdir(parseCommand($command), $base)['status']); check(!file_exists($base . '/target')); } }); -$tests['invalid modes and missing operands never create directories'] = fn() => fixture(function ($base) { +$tests['invalid modes and missing operands never create directories'] = fn () => fixture(function ($base) { foreach (['mkdir', 'mkdir -m888 bad', 'mkdir --mode=u=rwx bad', 'mkdir --mode= bad', 'mkdir -m"755\n" bad'] as $command) { same(2, _mkdir(parseCommand($command), $base)['status']); check(!file_exists($base . '/bad')); } }); -$tests['a malformed later operand prevents all mutations'] = fn() => fixture(function ($base) { +$tests['a malformed later operand prevents all mutations'] = fn () => fixture(function ($base) { same(2, _mkdir(parseCommand('mkdir first ""'), $base)['status']); check(!file_exists($base . '/first')); same(2, _mkdir(['args' => ['first', "bad\0name"]], $base)['status']); @@ -144,7 +164,7 @@ function names(array $result): array { same(2, _mkdir(['args' => [$path]])['status']); } }; -$tests['both commands use the explicit cwd, independent of sessions'] = fn() => fixture(function ($base) { +$tests['both commands use the explicit cwd, independent of sessions'] = fn () => fixture(function ($base) { $_SESSION['cwd'] = $base . '/not-the-working-directory'; $cwd = getcwd(); ob_start(); @@ -156,27 +176,27 @@ function names(array $result): array { same($cwd, getcwd()); unset($_SESSION); }); -$tests['absolute operands are not prefixed with cwd'] = fn() => fixture(function ($base) { +$tests['absolute operands are not prefixed with cwd'] = fn () => fixture(function ($base) { same(0, _mkdir(['args' => [$base . '/absolute']], __DIR__)['status']); same(0, ls(['args' => [$base . '/absolute']], __DIR__)['status']); }); -$tests['default cwd uses getcwd without requiring a session'] = fn() => fixture(function ($base) { +$tests['default cwd uses getcwd without requiring a session'] = fn () => fixture(function ($base) { chdir($base); same(0, _mkdir(parseCommand('mkdir default'))['status']); same(['default'], names(ls(parseCommand('ls')))); }); -$tests['invalid cwd is a controlled error'] = fn() => fixture(function ($base) { +$tests['invalid cwd is a controlled error'] = fn () => fixture(function ($base) { same(1, ls(parseCommand('ls'), $base . '/missing')['status']); same(1, _mkdir(parseCommand('mkdir no'), $base . '/missing')['status']); }); -$tests['mkdir reports partial success and continues after a filesystem error'] = fn() => fixture(function ($base) { +$tests['mkdir reports partial success and continues after a filesystem error'] = fn () => fixture(function ($base) { file_put_contents($base . '/file', 'x'); $result = _mkdir(parseCommand('mkdir file/child good'), $base); same(1, $result['status']); same(1, count($result['errors'])); same([$base . '/good'], $result['data']['created']); }); -$tests['mkdir existing directory requires parents option'] = fn() => fixture(function ($base) { +$tests['mkdir existing directory requires parents option'] = fn () => fixture(function ($base) { mkdir($base . '/existing'); same(1, _mkdir(parseCommand('mkdir existing'), $base)['status']); $result = _mkdir(parseCommand('mkdir -p existing'), $base); @@ -184,14 +204,14 @@ function names(array $result): array { same([], $result['data']['created']); same([$base . '/existing'], $result['data']['existing']); }); -$tests['default mkdir respects umask'] = fn() => fixture(function ($base) { +$tests['default mkdir respects umask'] = fn () => fixture(function ($base) { nativePermissionSupport($base); umask(0077); same(0, _mkdir(parseCommand('mkdir private'), $base)['status']); same(0700, modeOf($base . '/private')); same(0077, umask()); }); -$tests['explicit mode overrides umask only for the final directory'] = fn() => fixture(function ($base) { +$tests['explicit mode overrides umask only for the final directory'] = fn () => fixture(function ($base) { nativePermissionSupport($base); umask(0022); same(0, _mkdir(parseCommand('mkdir -p -m0700 parent/child'), $base)['status']); @@ -201,7 +221,7 @@ function names(array $result): array { same(0, _mkdir(parseCommand('mkdir -m0770 exact'), $base)['status']); same(0770, modeOf($base . '/exact')); }); -$tests['existing parents and final directories retain their permissions'] = fn() => fixture(function ($base) { +$tests['existing parents and final directories retain their permissions'] = fn () => fixture(function ($base) { skipUnless(DIRECTORY_SEPARATOR !== '\\', 'POSIX permission semantics are unavailable on Windows'); mkdir($base . '/existing'); chmod($base . '/existing', 0750); @@ -209,89 +229,103 @@ function names(array $result): array { same(0750, modeOf($base . '/existing')); same(0700, modeOf($base . '/existing/new')); }); -$tests['recursive parents keep owner write/search under restrictive umask'] = fn() => fixture(function ($base) { +$tests['recursive parents keep owner write/search under restrictive umask'] = fn () => fixture(function ($base) { nativePermissionSupport($base); umask(0777); same(0, _mkdir(parseCommand('mkdir -p -m0700 restrictive/child'), $base)['status']); same(0300, modeOf($base . '/restrictive')); same(0700, modeOf($base . '/restrictive/child')); }); -$tests['recursive creation and dash-prefixed names work'] = fn() => fixture(function ($base) { +$tests['recursive creation and dash-prefixed names work'] = fn () => fixture(function ($base) { $result = _mkdir(parseCommand('mkdir --parents a/b/c -- -dash'), $base); same(0, $result['status']); same(4, count($result['data']['created'])); check(is_dir($base . '/a/b/c')); check(is_dir($base . '/-dash')); }); -$tests['ls filters hidden entries and supports all'] = fn() => fixture(function ($base) { +$tests['ls filters hidden entries and supports all'] = fn () => fixture(function ($base) { file_put_contents($base . '/visible', 'x'); file_put_contents($base . '/.hidden', 'x'); same(['visible'], names(ls(parseCommand('ls'), $base))); same(['.', '..', '.hidden', 'visible'], names(ls(parseCommand('ls -a'), $base))); }); -$tests['directory grouping retains sorted order within groups'] = fn() => fixture(function ($base) { - foreach (['z-dir', 'b-dir'] as $dir) mkdir($base . '/' . $dir); - foreach (['a-file', 'y-file'] as $file) file_put_contents($base . '/' . $file, 'x'); +$tests['directory grouping retains sorted order within groups'] = fn () => fixture(function ($base) { + foreach (['z-dir', 'b-dir'] as $dir) { + mkdir($base . '/' . $dir); + } + foreach (['a-file', 'y-file'] as $file) { + file_put_contents($base . '/' . $file, 'x'); + } same(['b-dir', 'z-dir', 'a-file', 'y-file'], names(ls(parseCommand('ls --group-directories-first'), $base))); }); -$tests['single directory long listing always has a total'] = fn() => fixture(function ($base) { +$tests['single directory long listing always has a total'] = fn () => fixture(function ($base) { same("total 0\n", coreutilsText(ls(parseCommand('ls -l'), $base))); }); -$tests['standalone file blocks do not contaminate directory totals'] = fn() => fixture(function ($base) { +$tests['standalone file blocks do not contaminate directory totals'] = fn () => fixture(function ($base) { file_put_contents($base . '/large', str_repeat('x', 32768)); mkdir($base . '/empty'); $result = ls(parseCommand('ls -l large empty'), $base); same(0, $result['data']['directories'][0]['blocks']); check(strpos(coreutilsText($result), "empty:\ntotal 0\n") !== false); }); -$tests['separate directory totals agree with their metadata'] = fn() => fixture(function ($base) { - mkdir($base . '/a'); mkdir($base . '/b'); +$tests['separate directory totals agree with their metadata'] = fn () => fixture(function ($base) { + mkdir($base . '/a'); + mkdir($base . '/b'); file_put_contents($base . '/a/one', str_repeat('x', 8192)); file_put_contents($base . '/b/two', 'y'); $result = ls(parseCommand('ls -l a b'), $base); foreach ($result['data']['directories'] as $dir) { - if ($dir['entries'][0]['blocks'] !== null) same($dir['entries'][0]['blocks'], $dir['blocks']); - else same(null, $dir['blocks']); + if ($dir['entries'][0]['blocks'] !== null) { + same($dir['entries'][0]['blocks'], $dir['blocks']); + } else { + same(null, $dir['blocks']); + } } }); -$tests['broken and ordinary links retain link metadata'] = fn() => fixture(function ($base) { +$tests['broken and ordinary links retain link metadata'] = fn () => fixture(function ($base) { symlinkSupport($base); file_put_contents($base . '/target', str_repeat('x', 100)); symlink('target', $base . '/link'); symlink('missing', $base . '/broken'); $result = ls(parseCommand('ls -l link broken'), $base); same(0, $result['status']); - foreach ($result['data']['files'] as $entry) same('l', $entry['permissions'][0]); + foreach ($result['data']['files'] as $entry) { + same('l', $entry['permissions'][0]); + } check(strpos(coreutilsText($result), 'broken -> missing') !== false); check(strpos(coreutilsText($result), 'link -> target') !== false); same(6, $result['data']['files'][1]['size']); }); -$tests['directory symlinks follow only in short listing or with trailing slash'] = fn() => fixture(function ($base) { +$tests['directory symlinks follow only in short listing or with trailing slash'] = fn () => fixture(function ($base) { symlinkSupport($base); - mkdir($base . '/target'); file_put_contents($base . '/target/item', 'x'); + mkdir($base . '/target'); + file_put_contents($base . '/target/item', 'x'); symlink('target', $base . '/link'); same(['item'], names(ls(parseCommand('ls link'), $base))); same('l', ls(parseCommand('ls -l link'), $base)['data']['files'][0]['permissions'][0]); same(['item'], names(ls(parseCommand('ls -l link/'), $base))); }); -$tests['path resolution preserves symlink/.. semantics'] = fn() => fixture(function ($base) { +$tests['path resolution preserves symlink/.. semantics'] = fn () => fixture(function ($base) { symlinkSupport($base); - mkdir($base . '/real'); mkdir($base . '/real/child'); + mkdir($base . '/real'); + mkdir($base . '/real/child'); file_put_contents($base . '/real/marker', 'ok'); symlink('real/child', $base . '/link'); $result = ls(parseCommand('ls -l link/../marker'), $base); same(0, $result['status']); same(2, $result['data']['files'][0]['size']); }); -$tests['mkdir follows directory links and rejects dangling links'] = fn() => fixture(function ($base) { +$tests['mkdir follows directory links and rejects dangling links'] = fn () => fixture(function ($base) { symlinkSupport($base); - mkdir($base . '/real'); symlink('real', $base . '/link'); symlink('absent', $base . '/broken'); + mkdir($base . '/real'); + symlink('real', $base . '/link'); + symlink('absent', $base . '/broken'); same(0, _mkdir(parseCommand('mkdir -p link/new'), $base)['status']); check(is_dir($base . '/real/new')); same(1, _mkdir(parseCommand('mkdir -p broken'), $base)['status']); check(is_link($base . '/broken')); }); -$tests['ls errors are reusable and do not leak into a later call'] = fn() => fixture(function ($base) { +$tests['ls errors are reusable and do not leak into a later call'] = fn () => fixture(function ($base) { file_put_contents($base . '/present', 'ok'); $first = ls(parseCommand('ls -l missing present'), $base); same(1, $first['status']); @@ -300,23 +334,24 @@ function names(array $result): array { same(0, $second['status']); same(['present'], names($second)); }); -$tests['unreadable directory returns an error without breaking the caller'] = fn() => fixture(function ($base) { +$tests['unreadable directory returns an error without breaking the caller'] = fn () => fixture(function ($base) { skipUnless(DIRECTORY_SEPARATOR !== '\\', 'POSIX permission semantics are unavailable on Windows'); - mkdir($base . '/denied'); chmod($base . '/denied', 0000); - $readable = coreutilsFsCall(fn() => scandir($base . '/denied')); + mkdir($base . '/denied'); + chmod($base . '/denied', 0000); + $readable = coreutilsFsCall(fn () => scandir($base . '/denied')); skipUnless($readable === false, 'process can bypass filesystem permissions'); $result = ls(parseCommand('ls -l denied'), $base); same(1, $result['status']); same(false, $result['data']['directories'][0]['readable']); }); -$tests['HTML renderer escapes errors and preserves raw result data'] = fn() => fixture(function ($base) { +$tests['HTML renderer escapes errors and preserves raw result data'] = fn () => fixture(function ($base) { $result = ls(['args' => ['']], $base); $html = coreutilsHtml($result); check(strpos($html, ' fixture(function ($base) { +$tests['HTML renderer escapes real file names and link targets'] = fn () => fixture(function ($base) { skipUnless(DIRECTORY_SEPARATOR !== '\\', 'Windows forbids angle brackets in filenames'); $name = ''; file_put_contents($base . '/' . $name, 'x'); @@ -328,12 +363,12 @@ function names(array $result): array { symlink($name, $base . '/link'); check(strpos(coreutilsHtml(ls(parseCommand('ls -l link'), $base)), ' fixture(function ($base) { +$tests['text renderer quotes embedded newlines'] = fn () => fixture(function ($base) { skipUnless(DIRECTORY_SEPARATOR !== '\\', 'Windows forbids newline filenames'); file_put_contents($base . "/a\nb", 'x'); same('"a\\nb"' . "\n", coreutilsText(ls(parseCommand('ls'), $base))); }); -$tests['long output works without assuming intl or POSIX'] = fn() => fixture(function ($base) { +$tests['long output works without assuming intl or POSIX'] = fn () => fixture(function ($base) { file_put_contents($base . '/data', 'x'); foreach (['ls -l', 'ls -o', 'ls -g', 'ls -lG', 'ls -go', 'ls -lh', 'ls -l --si'] as $command) { $result = ls(parseCommand($command), $base, 'pt_BR'); @@ -358,18 +393,24 @@ function names(array $result): array { }; $tests['filesystem warning capture restores the caller error handler'] = function () { $called = false; - set_error_handler(function () use (&$called) { $called = true; return true; }); + set_error_handler(function () use (&$called) { + $called = true; + return true; + }); try { try { - coreutilsFsCall(function () { throw new RuntimeException('probe'); }); - } catch (RuntimeException $expected) {} + coreutilsFsCall(function () { + throw new RuntimeException('probe'); + }); + } catch (RuntimeException $expected) { + } trigger_error('handler probe', E_USER_WARNING); same(true, $called); } finally { restore_error_handler(); } }; -$tests['Unix cwd ending in a backslash remains intact'] = fn() => fixture(function ($base) { +$tests['Unix cwd ending in a backslash remains intact'] = fn () => fixture(function ($base) { skipUnless(DIRECTORY_SEPARATOR !== '\\', 'Unix filename test'); mkdir($base . '/tail\\'); same(0, _mkdir(parseCommand('mkdir inside'), $base . '/tail\\')['status']);