mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
Use a maintained finder for recursive MD5 discovery (#7718)
* Use Symfony Finder for MD5 traversal Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore: add the CHANGELOG entry Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(md5sum): reject a bad ignore pattern instead of ignoring every file preg_match() returns false on a bad pattern and false is not 0, so the old test skipped the whole tree and wrote an empty manifest. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * style: use a line comment to satisfy php-cs-fixer Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: resolve includes through Cacti path constants Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(md5): enforce full finder coverage Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Move Finder import before CLI bootstrap Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * build: lock Symfony Finder runtime dependency Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * build: leave Composer autoload metadata generated Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: run Finder coverage on PHP 8.2 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix: widen the symfony/finder constraint for PHP 8.5 Pinning ^6.4 left the dev dependency graph unsatisfiable on PHP 8.5 in the native-dependencies job. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
1 parent
3045b4665e
commit
c77906b144
8 files changed
+405
-129
No files matched your search
@@ -0,0 +1,66 @@
|
||||
name: MD5 Finder Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- '.github/workflows/md5-finder-coverage.yml'
|
||||
- '.github/actions/setup-php-composer/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'lib/CactiMd5FileFinder.php'
|
||||
- 'phpunit-md5-finder-coverage.xml'
|
||||
- 'tests/Unit/CactiMd5FileFinderTest.php'
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- '.github/workflows/md5-finder-coverage.yml'
|
||||
- '.github/actions/setup-php-composer/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'lib/CactiMd5FileFinder.php'
|
||||
- 'phpunit-md5-finder-coverage.xml'
|
||||
- 'tests/Unit/CactiMd5FileFinderTest.php'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: md5-finder-coverage-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSER_ALLOW_SUPERUSER: 1
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: MD5 finder 100% coverage
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup locked PHP 8.2 dependencies
|
||||
uses: ./.github/actions/setup-php-composer
|
||||
with:
|
||||
php-version: '8.2'
|
||||
coverage: xdebug
|
||||
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring
|
||||
dependency-mode: locked-md5-finder-coverage
|
||||
install-args: --prefer-dist --no-progress --no-interaction
|
||||
|
||||
- name: Enforce line coverage
|
||||
env:
|
||||
XDEBUG_MODE: coverage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
include/vendor/bin/pest \
|
||||
--configuration=phpunit-md5-finder-coverage.xml \
|
||||
--coverage \
|
||||
--coverage-text \
|
||||
--min=100
|
||||
@@ -139,6 +139,7 @@ Cacti CHANGELOG
|
||||
-issue#7536: Consolidate the four hand-written poller_output_boost inserts into one shared helper
|
||||
-issue#7535: Consolidate the poller_item delete call sites behind poller_item_delete_for_data_source() and poller_item_delete_for_host()
|
||||
-issue#7576: Stop passing an unset path_php_binary into string parameters
|
||||
-issue#7718: Discover files recursively for the MD5 listing with Symfony Finder
|
||||
-issue#7673: Require a POST and a CSRF token for the delete actions that were reachable over GET
|
||||
-issue#7674: Enforce tree access when creating, deleting and renaming tree nodes
|
||||
-issue#7682: Revoke the remember-me token and terminate other sessions on logout
|
||||
|
||||
+20
-60
@@ -23,8 +23,11 @@
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
|
||||
|
||||
require(__DIR__ . '/../include/cli_check.php');
|
||||
require_once(CACTI_PATH_LIBRARY . '/CactiFilesystem.php');
|
||||
require_once(CACTI_PATH_LIBRARY . '/CactiMd5FileFinder.php');
|
||||
|
||||
// process calling arguments
|
||||
$parms = $_SERVER['argv'];
|
||||
@@ -151,7 +154,23 @@ foreach ($ignore_files as $ignore) {
|
||||
}
|
||||
$ignore_regex = "~($ignore_regex)~";
|
||||
|
||||
$file_array = dirToArray('', $base_dir, $ignore_regex);
|
||||
$excluded_directories = ['.git', 'cache', 'log', 'plugins', 'rra'];
|
||||
$file_finder = new CactiMd5FileFinder();
|
||||
$debug_callback = null;
|
||||
|
||||
if (!$quiet && $debug) {
|
||||
print PHP_EOL . "Searching '$base_dir' ..." . PHP_EOL;
|
||||
$debug_callback = static function (string $message) : void {
|
||||
print $message . PHP_EOL;
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
$file_array = $file_finder->findHashes($base_dir, $ignore_regex, $excluded_directories, $debug_callback);
|
||||
} catch (DirectoryNotFoundException | InvalidArgumentException $e) {
|
||||
printf('ERROR: %s' . PHP_EOL, $e->getMessage());
|
||||
exit(6);
|
||||
}
|
||||
|
||||
if ($create) {
|
||||
$output = '';
|
||||
@@ -237,65 +256,6 @@ if ($create) {
|
||||
}
|
||||
}
|
||||
|
||||
function dirToArray(mixed $dir, string $base, string $ignore) : array {
|
||||
global $debug, $quiet;
|
||||
|
||||
$result = [];
|
||||
|
||||
$fulldir = $base;
|
||||
|
||||
if ($dir != '') {
|
||||
$fulldir .= DIRECTORY_SEPARATOR . $dir;
|
||||
}
|
||||
|
||||
$fulldir = realpath($fulldir);
|
||||
|
||||
if (str_contains($fulldir, $base)) {
|
||||
if (is_dir($fulldir)) {
|
||||
$cdir = scandir($fulldir);
|
||||
} else {
|
||||
$cdir = [];
|
||||
}
|
||||
|
||||
if (!$quiet && $debug) {
|
||||
print PHP_EOL . "Searching '$fulldir' ..." . PHP_EOL;
|
||||
}
|
||||
|
||||
$dir_list = [];
|
||||
|
||||
foreach ($cdir as $key => $value) {
|
||||
$fullpath = $fulldir . DIRECTORY_SEPARATOR . $value;
|
||||
$partpath = substr($fullpath,strlen($base));
|
||||
|
||||
if (preg_match($ignore,$partpath) == 0) {
|
||||
if (is_dir($fullpath)) {
|
||||
$dir_list[] = $partpath;
|
||||
} else {
|
||||
$md5_sum = @md5_file($fullpath);
|
||||
|
||||
if (!$quiet && $debug) {
|
||||
print "[$md5_sum] $value" . PHP_EOL;
|
||||
}
|
||||
$result[substr($partpath,1)] = $md5_sum;
|
||||
}
|
||||
} else {
|
||||
if (!$quiet && $debug) {
|
||||
print "[ Ignored] $value" . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dir_list as $partpath) {
|
||||
$result = array_merge($result, dirToArray($partpath, $base, $ignore));
|
||||
}
|
||||
} elseif (!$quiet && ($debug || !strlen($dir))) {
|
||||
$value = substr($dir,strlen(dirname($dir)) + 1);
|
||||
print "[ Outside Base, Ignored] $value" . PHP_EOL;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* display_version - displays version information
|
||||
*
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"stevenmaguire/oauth2-keycloak": "^6.1.0",
|
||||
"stevenmaguire/oauth2-microsoft": "^2.2",
|
||||
"symfony/filesystem": "^7.4",
|
||||
"symfony/finder": "^6.4 || ^7.4",
|
||||
"symfony/http-foundation": "^7.4",
|
||||
"symfony/mailer": "^7.4",
|
||||
"symfony/notifier": "^7.4",
|
||||
|
||||
Generated
+69
-69
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "8b11d855049443d645aa8a3cdfb2ed37",
|
||||
"content-hash": "ff3b05dc45eb4a6c840368e2384f1bea",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/lexer",
|
||||
@@ -2281,6 +2281,74 @@
|
||||
],
|
||||
"time": "2026-08-21T12:09:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v6.4.44",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "211b28d13d044dacdc00c1629a3bbcff27dc793a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/211b28d13d044dacdc00c1629a3bbcff27dc793a",
|
||||
"reference": "211b28d13d044dacdc00c1629a3bbcff27dc793a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/filesystem": "^6.0|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Finder\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v6.4.44"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-21T10:00:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v7.4.17",
|
||||
@@ -7644,74 +7712,6 @@
|
||||
],
|
||||
"time": "2026-08-21T12:09:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v6.4.44",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "211b28d13d044dacdc00c1629a3bbcff27dc793a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/211b28d13d044dacdc00c1629a3bbcff27dc793a",
|
||||
"reference": "211b28d13d044dacdc00c1629a3bbcff27dc793a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/filesystem": "^6.0|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Finder\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v6.4.44"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-21T10:00:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v7.4.8",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
|
||||
|
||||
final class CactiMd5FileFinder {
|
||||
public function findHashes(string $basePath, string $ignoreRegex, array $excludedDirectories = [], ?Closure $debug = null) : array {
|
||||
if (!is_dir($basePath) || !is_readable($basePath)) {
|
||||
throw new DirectoryNotFoundException("The directory '$basePath' does not exist or is not readable.");
|
||||
}
|
||||
|
||||
/* preg_match() returns false on a bad pattern, and false is not 0, so an
|
||||
* unchecked test would quietly ignore every file and write an empty manifest. */
|
||||
// the handler swallows the compile warning; the return value is the verdict
|
||||
set_error_handler(static fn () : bool => true);
|
||||
$patternIsValid = preg_match($ignoreRegex, '') !== false;
|
||||
restore_error_handler();
|
||||
|
||||
if (!$patternIsValid) {
|
||||
throw new InvalidArgumentException("The ignore pattern '$ignoreRegex' is not a valid regular expression.");
|
||||
}
|
||||
|
||||
$finder = (new Finder())
|
||||
->files()
|
||||
->ignoreDotFiles(false)
|
||||
->ignoreVCS(false)
|
||||
->ignoreUnreadableDirs()
|
||||
->exclude($excludedDirectories)
|
||||
->in($basePath);
|
||||
|
||||
$hashes = [];
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePathname());
|
||||
|
||||
if (preg_match($ignoreRegex, '/' . $relativePath) === 1) {
|
||||
if ($debug !== null) {
|
||||
$debug('[ Ignored] ' . $file->getFilename());
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$md5 = @md5_file($file->getPathname());
|
||||
|
||||
if ($debug !== null) {
|
||||
$debug("[$md5] " . $file->getFilename());
|
||||
}
|
||||
|
||||
$hashes[$relativePath] = $md5;
|
||||
}
|
||||
|
||||
uksort($hashes, self::compareManifestPaths(...));
|
||||
|
||||
return $hashes;
|
||||
}
|
||||
|
||||
private static function compareManifestPaths(string $left, string $right) : int {
|
||||
$leftParts = explode('/', $left);
|
||||
$rightParts = explode('/', $right);
|
||||
$partCount = min(count($leftParts), count($rightParts));
|
||||
|
||||
for ($index = 0; $index < $partCount; $index++) {
|
||||
$leftIsFile = $index === count($leftParts) - 1;
|
||||
$rightIsFile = $index === count($rightParts) - 1;
|
||||
|
||||
if ($leftIsFile !== $rightIsFile) {
|
||||
return $leftIsFile ? -1 : 1;
|
||||
}
|
||||
|
||||
$comparison = strcmp($leftParts[$index], $rightParts[$index]);
|
||||
|
||||
if ($comparison !== 0) {
|
||||
return $comparison;
|
||||
}
|
||||
}
|
||||
|
||||
return count($leftParts) <=> count($rightParts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<phpunit bootstrap="tests/bootstrap-unit.php" beStrictAboutOutputDuringTests="false">
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<env name="CACTI_TEST_BOOTSTRAP" value="1"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<file>lib/CactiMd5FileFinder.php</file>
|
||||
</include>
|
||||
</source>
|
||||
<testsuites>
|
||||
<testsuite name="MD5 finder coverage">
|
||||
<file>tests/Unit/CactiMd5FileFinderTest.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
|
||||
|
||||
require_once(CACTI_PATH_LIBRARY . '/CactiMd5FileFinder.php');
|
||||
|
||||
beforeEach(function () {
|
||||
$this->filesystem = new Filesystem();
|
||||
$this->directory = sys_get_temp_dir() . '/cacti-finder-' . bin2hex(random_bytes(8));
|
||||
$this->outside = $this->directory . '-sibling';
|
||||
$this->filesystem->mkdir([
|
||||
$this->directory . '/nested',
|
||||
$this->directory . '/nested/deeper',
|
||||
$this->directory . '/cache',
|
||||
$this->directory . '/nested/plugins',
|
||||
$this->outside,
|
||||
]);
|
||||
$this->filesystem->dumpFile($this->directory . '/visible.php', 'visible');
|
||||
$this->filesystem->dumpFile($this->directory . '/.env', 'hidden');
|
||||
$this->filesystem->dumpFile($this->directory . '/ignored.log', 'log');
|
||||
$this->filesystem->dumpFile($this->directory . '/nested/file.php', 'nested');
|
||||
$this->filesystem->dumpFile($this->directory . '/nested/deeper/file.php', 'deeper');
|
||||
$this->filesystem->dumpFile($this->directory . '/cache/cache.php', 'cache');
|
||||
$this->filesystem->dumpFile($this->directory . '/nested/plugins/plugin.php', 'plugin');
|
||||
$this->filesystem->dumpFile($this->outside . '/outside.php', 'outside');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$this->filesystem->remove([$this->directory, $this->outside]);
|
||||
});
|
||||
|
||||
function cactiFinderIgnoreRegex() : string {
|
||||
return '~((\.log$)|(/\.htaccess$))~';
|
||||
}
|
||||
|
||||
test('findHashes returns stable relative paths and hashes', function () {
|
||||
$finder = new CactiMd5FileFinder();
|
||||
$hashes = $finder->findHashes($this->directory, cactiFinderIgnoreRegex(), ['cache', 'plugins']);
|
||||
|
||||
expect(array_keys($hashes))->toBe(['.env', 'visible.php', 'nested/file.php', 'nested/deeper/file.php'])
|
||||
->and($hashes['visible.php'])->toBe(md5('visible'))
|
||||
->and($hashes['nested/file.php'])->toBe(md5('nested'))
|
||||
->and($hashes['nested/deeper/file.php'])->toBe(md5('deeper'));
|
||||
});
|
||||
|
||||
test('findHashes preserves hidden files when they are not ignored', function () {
|
||||
$finder = new CactiMd5FileFinder();
|
||||
$hashes = $finder->findHashes($this->directory, cactiFinderIgnoreRegex(), ['cache', 'plugins']);
|
||||
|
||||
expect($hashes)->toHaveKey('.env');
|
||||
});
|
||||
|
||||
test('findHashes reports filtered files through the debug callback', function () {
|
||||
$messages = [];
|
||||
$finder = new CactiMd5FileFinder();
|
||||
$finder->findHashes(
|
||||
$this->directory,
|
||||
cactiFinderIgnoreRegex(),
|
||||
['cache', 'plugins'],
|
||||
static function (string $message) use (&$messages) : void {
|
||||
$messages[] = $message;
|
||||
}
|
||||
);
|
||||
|
||||
expect($messages)->toContain('[ Ignored] ignored.log')
|
||||
->and(implode("\n", $messages))->toContain(md5('visible'));
|
||||
});
|
||||
|
||||
test('findHashes does not follow a sibling-prefix symlink target', function () {
|
||||
if (DIRECTORY_SEPARATOR === '\\') {
|
||||
$this->markTestSkipped('Symlink creation is not consistently available on Windows.');
|
||||
}
|
||||
|
||||
if (!@symlink($this->outside, $this->directory . '/outside-link')) {
|
||||
$this->markTestSkipped('Symlink creation is not permitted in this environment.');
|
||||
}
|
||||
|
||||
$finder = new CactiMd5FileFinder();
|
||||
$hashes = $finder->findHashes($this->directory, cactiFinderIgnoreRegex(), ['cache', 'plugins']);
|
||||
|
||||
expect($hashes)->not->toHaveKey('outside-link/outside.php');
|
||||
});
|
||||
|
||||
test('findHashes rejects a missing root directory', function () {
|
||||
$finder = new CactiMd5FileFinder();
|
||||
|
||||
expect(fn () => $finder->findHashes($this->directory . '/missing', cactiFinderIgnoreRegex()))
|
||||
->toThrow(DirectoryNotFoundException::class);
|
||||
});
|
||||
|
||||
test('findHashes rejects an unreadable root directory', function () {
|
||||
if (DIRECTORY_SEPARATOR === '\\') {
|
||||
$this->markTestSkipped('POSIX directory permissions are not available on Windows.');
|
||||
}
|
||||
|
||||
chmod($this->directory, 0000);
|
||||
clearstatcache(true, $this->directory);
|
||||
|
||||
if (is_readable($this->directory)) {
|
||||
chmod($this->directory, 0700);
|
||||
$this->markTestSkipped('The test process can bypass directory permissions.');
|
||||
}
|
||||
|
||||
try {
|
||||
$finder = new CactiMd5FileFinder();
|
||||
|
||||
expect(fn () => $finder->findHashes($this->directory, cactiFinderIgnoreRegex()))
|
||||
->toThrow(DirectoryNotFoundException::class);
|
||||
} finally {
|
||||
chmod($this->directory, 0700);
|
||||
}
|
||||
});
|
||||
|
||||
test('findHashes rejects an ignore pattern that is not a valid regex', function () {
|
||||
$finder = new CactiMd5FileFinder();
|
||||
|
||||
// an unchecked preg_match() would return false here and ignore every file
|
||||
expect(fn () => $finder->findHashes($this->directory, '/unterminated'))
|
||||
->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
test('the manifest comparator treats identical paths as equal', function () {
|
||||
$comparator = new ReflectionMethod(CactiMd5FileFinder::class, 'compareManifestPaths');
|
||||
$comparator->setAccessible(true);
|
||||
|
||||
expect($comparator->invoke(null, 'nested/file.php', 'nested/file.php'))->toBe(0);
|
||||
});
|
||||
Reference in new issue
Block a user