mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-24 03:57:32 +09:00
* 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>
141 lines
5.4 KiB
PHP
141 lines
5.4 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\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);
|
|
});
|