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#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#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#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#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#7674: Enforce tree access when creating, deleting and renaming tree nodes
|
||||||
-issue#7682: Revoke the remember-me token and terminate other sessions on logout
|
-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(__DIR__ . '/../include/cli_check.php');
|
||||||
require_once(CACTI_PATH_LIBRARY . '/CactiFilesystem.php');
|
require_once(CACTI_PATH_LIBRARY . '/CactiFilesystem.php');
|
||||||
|
require_once(CACTI_PATH_LIBRARY . '/CactiMd5FileFinder.php');
|
||||||
|
|
||||||
// process calling arguments
|
// process calling arguments
|
||||||
$parms = $_SERVER['argv'];
|
$parms = $_SERVER['argv'];
|
||||||
@@ -151,7 +154,23 @@ foreach ($ignore_files as $ignore) {
|
|||||||
}
|
}
|
||||||
$ignore_regex = "~($ignore_regex)~";
|
$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) {
|
if ($create) {
|
||||||
$output = '';
|
$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
|
* display_version - displays version information
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
"stevenmaguire/oauth2-keycloak": "^6.1.0",
|
"stevenmaguire/oauth2-keycloak": "^6.1.0",
|
||||||
"stevenmaguire/oauth2-microsoft": "^2.2",
|
"stevenmaguire/oauth2-microsoft": "^2.2",
|
||||||
"symfony/filesystem": "^7.4",
|
"symfony/filesystem": "^7.4",
|
||||||
|
"symfony/finder": "^6.4 || ^7.4",
|
||||||
"symfony/http-foundation": "^7.4",
|
"symfony/http-foundation": "^7.4",
|
||||||
"symfony/mailer": "^7.4",
|
"symfony/mailer": "^7.4",
|
||||||
"symfony/notifier": "^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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "8b11d855049443d645aa8a3cdfb2ed37",
|
"content-hash": "ff3b05dc45eb4a6c840368e2384f1bea",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "doctrine/lexer",
|
"name": "doctrine/lexer",
|
||||||
@@ -2281,6 +2281,74 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-08-21T12:09:28+00:00"
|
"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",
|
"name": "symfony/http-foundation",
|
||||||
"version": "v7.4.17",
|
"version": "v7.4.17",
|
||||||
@@ -7644,74 +7712,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-08-21T12:09:28+00:00"
|
"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",
|
"name": "symfony/options-resolver",
|
||||||
"version": "v7.4.8",
|
"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