Files
cacti/lib/CactiMd5FileFinder.php
Thomas Vincent c77906b144 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>
2026-08-31 17:07:36 -04:00

93 lines
3.0 KiB
PHP

<?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);
}
}