From 57417d768f1fcdb148b11b5b3becd54874a4f469 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 26 Sep 2026 12:05:22 +0200 Subject: [PATCH] Sort files before adding them to the phar so that the result does not depend on the order of directory entries --- src/PharBuilder.php | 17 +++++++- tests/PharBuilderTest.php | 91 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/PharBuilderTest.php diff --git a/src/PharBuilder.php b/src/PharBuilder.php index adbaacf..7e72b5b 100644 --- a/src/PharBuilder.php +++ b/src/PharBuilder.php @@ -105,7 +105,7 @@ public function build($filename, $stub) { $basedir = $this->basedir ? $this->basedir : $this->directories[0]; foreach($this->directories as $directory) { - $phar->buildFromIterator($this->scanner->__invoke($directory), $basedir); + $phar->buildFromIterator($this->sortedFiles($directory), $basedir); } if ($this->compression !== \Phar::NONE) { @@ -114,6 +114,21 @@ public function build($filename, $stub) { $phar->stopBuffering(); } + /** + * The order in which the filesystem returns directory entries is not defined, + * sorting them ensures that the same input always results in the same phar + */ + private function sortedFiles($directory) { + $files = array(); + foreach($this->scanner->__invoke($directory) as $file) { + $files[] = $file; + } + usort($files, function(\SplFileInfo $a, \SplFileInfo $b) { + return strcmp($a->getPathname(), $b->getPathname()); + }); + return new \ArrayIterator($files); + } + private function selectSignatureType() { if ($this->signatureType !== NULL) { return $this->supportedSignatureTypes[$this->signatureType]; diff --git a/tests/PharBuilderTest.php b/tests/PharBuilderTest.php new file mode 100644 index 0000000..eb6b951 --- /dev/null +++ b/tests/PharBuilderTest.php @@ -0,0 +1,91 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package Autoload + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license BSD License + */ + +namespace TheSeer\Autoload\Tests { + + use TheSeer\Autoload\PharBuilder; + use TheSeer\DirectoryScanner\DirectoryScanner; + + class PharBuilderTest extends \PHPUnit\Framework\TestCase { + + private $directory; + private $pharFiles = array(); + + public function setUp(): void { + if (ini_get('phar.readonly')) { + $this->markTestSkipped('phar.readonly must be disabled'); + } + $this->directory = realpath(__DIR__ . '/_data/dependency'); + } + + public function tearDown(): void { + foreach($this->pharFiles as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } + + public function testPharDoesNotDependOnOrderOfDirectoryEntries() { + $files = array(); + foreach(glob($this->directory . '/*.php') as $file) { + $files[] = new \SplFileInfo($file); + } + + $this->assertSame( + file_get_contents($this->buildPhar($files)), + file_get_contents($this->buildPhar(array_reverse($files))) + ); + } + + private function buildPhar(array $files) { + $scanner = $this->createMock(DirectoryScanner::class); + $scanner->method('__invoke')->willReturn(new \ArrayIterator($files)); + + $filename = sys_get_temp_dir() . '/' . uniqid('phpab-test-', true) . '.phar'; + $this->pharFiles[] = $filename; + + $builder = new PharBuilder($scanner, $this->directory); + $builder->setCompressionMode(\Phar::NONE); + $builder->addDirectory($this->directory); + $builder->setAliasName('test.phar'); + $builder->build($filename, '