From 4d85e217c30ebcc67618e0978cd99544f7aeb634 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Sat, 16 Feb 2019 18:46:59 +0100 Subject: [PATCH 1/6] Extract the ZIP utility functions from ArtifactRepository --- .../Repository/ArtifactRepository.php | 66 +------------ src/Composer/Util/Zip.php | 96 +++++++++++++++++++ 2 files changed, 100 insertions(+), 62 deletions(-) create mode 100644 src/Composer/Util/Zip.php diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index 079d34c54..223ea4aef 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -16,6 +16,7 @@ use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\LoaderInterface; +use Composer\Util\Zip; /** * @author Serge Smertin @@ -80,73 +81,14 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito } } - /** - * Find a file by name, returning the one that has the shortest path. - * - * @param \ZipArchive $zip - * @param string $filename - * @return bool|int - */ - private function locateFile(\ZipArchive $zip, $filename) - { - $indexOfShortestMatch = false; - $lengthOfShortestMatch = -1; - - for ($i = 0; $i < $zip->numFiles; $i++) { - $stat = $zip->statIndex($i); - if (strcmp(basename($stat['name']), $filename) === 0) { - $directoryName = dirname($stat['name']); - if ($directoryName == '.') { - //if composer.json is in root directory - //it has to be the one to use. - return $i; - } - - if (strpos($directoryName, '\\') !== false || - strpos($directoryName, '/') !== false) { - //composer.json files below first directory are rejected - continue; - } - - $length = strlen($stat['name']); - if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) { - //Check it's not a directory. - $contents = $zip->getFromIndex($i); - if ($contents !== false) { - $indexOfShortestMatch = $i; - $lengthOfShortestMatch = $length; - } - } - } - } - - return $indexOfShortestMatch; - } - private function getComposerInformation(\SplFileInfo $file) { - $zip = new \ZipArchive(); - if ($zip->open($file->getPathname()) !== true) { + $composerFile = Zip::findFile($file->getPathname(), 'composer.json'); + + if (null === $composerFile) { return false; } - if (0 == $zip->numFiles) { - $zip->close(); - - return false; - } - - $foundFileIndex = $this->locateFile($zip, 'composer.json'); - if (false === $foundFileIndex) { - $zip->close(); - - return false; - } - - $configurationFileName = $zip->getNameIndex($foundFileIndex); - $zip->close(); - - $composerFile = "zip://{$file->getPathname()}#$configurationFileName"; $json = file_get_contents($composerFile); $package = JsonFile::parseJson($json, $composerFile); diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php new file mode 100644 index 000000000..1dfd99d39 --- /dev/null +++ b/src/Composer/Util/Zip.php @@ -0,0 +1,96 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * @author Jordi Boggiano + */ +class Zip +{ + /** + * Finds the path to a file inside a ZIP archive. + * + * @param $pathToZip + * @param $filename + * + * @return string|null + */ + public static function findFile($pathToZip, $filename) + { + $zip = new \ZipArchive(); + if ($zip->open($pathToZip) !== true) { + return null; + } + + if (0 == $zip->numFiles) { + $zip->close(); + + return null; + } + + $foundFileIndex = static::locateFile($zip, $filename); + if (false === $foundFileIndex) { + $zip->close(); + + return null; + } + + $configurationFileName = $zip->getNameIndex($foundFileIndex); + $zip->close(); + + return "zip://{$pathToZip}#$configurationFileName"; + } + + /** + * Find a file by name, returning the one that has the shortest path. + * + * @param \ZipArchive $zip + * @param string $filename + * @return bool|int + */ + private static function locateFile(\ZipArchive $zip, $filename) + { + $indexOfShortestMatch = false; + $lengthOfShortestMatch = -1; + + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + if (strcmp(basename($stat['name']), $filename) === 0) { + $directoryName = dirname($stat['name']); + if ($directoryName == '.') { + //if composer.json is in root directory + //it has to be the one to use. + return $i; + } + + if (strpos($directoryName, '\\') !== false || + strpos($directoryName, '/') !== false) { + //composer.json files below first directory are rejected + continue; + } + + $length = strlen($stat['name']); + if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) { + //Check it's not a directory. + $contents = $zip->getFromIndex($i); + if ($contents !== false) { + $indexOfShortestMatch = $i; + $lengthOfShortestMatch = $length; + } + } + } + } + + return $indexOfShortestMatch; + } +} From 9de07bed1b3f4b96e0da90f2caab525073cf5048 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Mon, 25 Feb 2019 08:01:38 +0100 Subject: [PATCH 2/6] Fixed docblocks --- src/Composer/Util/Zip.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php index 1dfd99d39..a14eea924 100644 --- a/src/Composer/Util/Zip.php +++ b/src/Composer/Util/Zip.php @@ -20,8 +20,8 @@ class Zip /** * Finds the path to a file inside a ZIP archive. * - * @param $pathToZip - * @param $filename + * @param string $pathToZip + * @param string $filename * * @return string|null */ @@ -55,7 +55,8 @@ class Zip * Find a file by name, returning the one that has the shortest path. * * @param \ZipArchive $zip - * @param string $filename + * @param string $filename + * * @return bool|int */ private static function locateFile(\ZipArchive $zip, $filename) From 05d6b2178542857131011e3e46a8165414abc8aa Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Mon, 25 Feb 2019 08:02:04 +0100 Subject: [PATCH 3/6] Use self:: for private method --- src/Composer/Util/Zip.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php index a14eea924..2e0333ac0 100644 --- a/src/Composer/Util/Zip.php +++ b/src/Composer/Util/Zip.php @@ -38,7 +38,7 @@ class Zip return null; } - $foundFileIndex = static::locateFile($zip, $filename); + $foundFileIndex = self::locateFile($zip, $filename); if (false === $foundFileIndex) { $zip->close(); From 0d0cb53f314eb7422f16b479e2efd145c730362f Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Fri, 1 Mar 2019 11:06:03 +0100 Subject: [PATCH 4/6] Adjust Zip Util to only find the root composer.json --- src/Composer/Repository/ArtifactRepository.php | 2 +- src/Composer/Util/Zip.php | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index 223ea4aef..2358f9205 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -83,7 +83,7 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito private function getComposerInformation(\SplFileInfo $file) { - $composerFile = Zip::findFile($file->getPathname(), 'composer.json'); + $composerFile = Zip::findComposerJson($file->getPathname()); if (null === $composerFile) { return false; diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php index 2e0333ac0..fcad76604 100644 --- a/src/Composer/Util/Zip.php +++ b/src/Composer/Util/Zip.php @@ -18,15 +18,19 @@ namespace Composer\Util; class Zip { /** - * Finds the path to a file inside a ZIP archive. + * Finds the path to the root composer.json inside a ZIP archive. * * @param string $pathToZip * @param string $filename * * @return string|null */ - public static function findFile($pathToZip, $filename) + public static function findComposerJson($pathToZip) { + if (!extension_loaded('zip')) { + throw new \RuntimeException('The Zip Util requires PHP\'s zip extension'); + } + $zip = new \ZipArchive(); if ($zip->open($pathToZip) !== true) { return null; @@ -38,7 +42,7 @@ class Zip return null; } - $foundFileIndex = self::locateFile($zip, $filename); + $foundFileIndex = self::locateFile($zip, 'composer.json'); if (false === $foundFileIndex) { $zip->close(); @@ -68,7 +72,7 @@ class Zip $stat = $zip->statIndex($i); if (strcmp(basename($stat['name']), $filename) === 0) { $directoryName = dirname($stat['name']); - if ($directoryName == '.') { + if ($directoryName === '.') { //if composer.json is in root directory //it has to be the one to use. return $i; From a91fd20673843e40e7d692f5f40fe3132912b030 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Mon, 4 Mar 2019 09:54:35 +0100 Subject: [PATCH 5/6] Return the composer.json content instead of a zip:// path --- src/Composer/Repository/ArtifactRepository.php | 8 +++----- src/Composer/Util/Zip.php | 13 ++++++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index 2358f9205..aff80e4cd 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -83,15 +83,13 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito private function getComposerInformation(\SplFileInfo $file) { - $composerFile = Zip::findComposerJson($file->getPathname()); + $json = Zip::getComposerJson($file->getPathname()); - if (null === $composerFile) { + if (null === $json) { return false; } - $json = file_get_contents($composerFile); - - $package = JsonFile::parseJson($json, $composerFile); + $package = JsonFile::parseJson($json, $file->getPathname().'#composer.json'); $package['dist'] = array( 'type' => 'zip', 'url' => strtr($file->getPathname(), '\\', '/'), diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php index fcad76604..6698616ff 100644 --- a/src/Composer/Util/Zip.php +++ b/src/Composer/Util/Zip.php @@ -18,14 +18,14 @@ namespace Composer\Util; class Zip { /** - * Finds the path to the root composer.json inside a ZIP archive. + * Gets content of the root composer.json inside a ZIP archive. * * @param string $pathToZip * @param string $filename * * @return string|null */ - public static function findComposerJson($pathToZip) + public static function getComposerJson($pathToZip) { if (!extension_loaded('zip')) { throw new \RuntimeException('The Zip Util requires PHP\'s zip extension'); @@ -49,10 +49,17 @@ class Zip return null; } + $content = null; $configurationFileName = $zip->getNameIndex($foundFileIndex); + $stream = $zip->getStream($configurationFileName); + + if (false !== $stream) { + $content = stream_get_contents($stream); + } + $zip->close(); - return "zip://{$pathToZip}#$configurationFileName"; + return $content; } /** From 0e2215dc6c40c67ca720fe4863eaff5624d8ead1 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Mon, 4 Mar 2019 11:08:59 +0100 Subject: [PATCH 6/6] Added full unit test coverage --- src/Composer/Util/Zip.php | 2 +- .../Composer/Test/Util/Fixtures/Zip/empty.zip | Bin 0 -> 22 bytes .../Test/Util/Fixtures/Zip/folder.zip | Bin 0 -> 314 bytes .../Test/Util/Fixtures/Zip/multiple.zip | Bin 0 -> 2569 bytes .../Test/Util/Fixtures/Zip/nojson.zip | Bin 0 -> 134 bytes .../Composer/Test/Util/Fixtures/Zip/root.zip | Bin 0 -> 194 bytes .../Test/Util/Fixtures/Zip/subfolder.zip | Bin 0 -> 1328 bytes tests/Composer/Test/Util/ZipTest.php | 117 ++++++++++++++++++ 8 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/Composer/Test/Util/Fixtures/Zip/empty.zip create mode 100644 tests/Composer/Test/Util/Fixtures/Zip/folder.zip create mode 100644 tests/Composer/Test/Util/Fixtures/Zip/multiple.zip create mode 100644 tests/Composer/Test/Util/Fixtures/Zip/nojson.zip create mode 100644 tests/Composer/Test/Util/Fixtures/Zip/root.zip create mode 100644 tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip create mode 100644 tests/Composer/Test/Util/ZipTest.php diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php index 6698616ff..8c79d106c 100644 --- a/src/Composer/Util/Zip.php +++ b/src/Composer/Util/Zip.php @@ -13,7 +13,7 @@ namespace Composer\Util; /** - * @author Jordi Boggiano + * @author Andreas Schempp */ class Zip { diff --git a/tests/Composer/Test/Util/Fixtures/Zip/empty.zip b/tests/Composer/Test/Util/Fixtures/Zip/empty.zip new file mode 100644 index 0000000000000000000000000000000000000000..15cb0ecb3e219d1701294bfdf0fe3f5cb5d208e7 GIT binary patch literal 22 NcmWIWW@Tf*000g10H*)| literal 0 HcmV?d00001 diff --git a/tests/Composer/Test/Util/Fixtures/Zip/folder.zip b/tests/Composer/Test/Util/Fixtures/Zip/folder.zip new file mode 100644 index 0000000000000000000000000000000000000000..72b17b542f11eaaf47e413832e50a5538eb75e9c GIT binary patch literal 314 zcmWIWW@h1H0D+>Q6hANnO0X~pFr?+@>xV}0Fsyl76SD${zcPw21ORo2FmM22Fq#fQ zsE*|P+=Be#)FQpC;`}_A_B^Qe)z5+$n3nE2awkMpn|0}yKQ(`s98pqT7o`U@n4P0O zuXwu@&;cME;LXS+%8bi#JTSL9ymbUIAx`ChI~AfE;ZS6g1sM>!moyqdb)z{OVid^P T0p6@^AS;-FuoFnn25}ewNhC+U literal 0 HcmV?d00001 diff --git a/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip b/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip new file mode 100644 index 0000000000000000000000000000000000000000..db8c50302d78e7512c043c4598fd62f518af151c GIT binary patch literal 2569 zcmWIWW@h1H0D9J_iOYJ9Y%ed5Eum*72cZaN; zaO1hyoCT}rHJO$_(7EzMR7prBVnNskyOY0^Ds%kJwjF24`F0_sf&IaR+?UBr$ITWk zIBoJHw=et7;;j7DKSRxJuOFOPWPgm;YD-4jPyO)b75;*!*R2!J-G1ZSj^|&WJ^lFQ zWsTOVpKe!P@t|$qy1u1*_P$Z zvM=ps&z`pNhrzl%+Hq%qa`wMc760+NN7~K(FCKsWzf*^O&(yDb<~`i2ATjaWtqr-; zcQZc7?E9#?;6#6G`i~d*(ws90894P&B}z0J}C&6u`A1#~!j)?6D_-u2nA{XGA6^h$g2eBqefhbjzQbzgzyynQ}!*S#@2M9@x2TPq(|s0i6oM2s&*) zb0=Up7d^*_qBz%3A0@y+84i(itoaTZ2)Ly8a(kQTY2Gbh$V+fN$+?4pHGIMMi;A}d zH;DuVs5Vtj`Wn_%pDE+daO1;{_y!(@gT2OflWvH$tTQ{O*dpN6QJcK3h17he(UnQoQ2wV@9g5>*eL1`PU;LD zxtoANhd-%<0s$q>LZTfJj)c>$0xl!a(lBz6QY{TbQX+byKuw8;`WPt@3CQR8j6inDU&pP;Mj(3#VGP1U$c32%iibX+8-tP#vHK0ZDC0pg@vUPu z6C{8UMIyv|pt1}(ph2b}ms*lYrYvdPie?I0I^lp9kH{g0p0ZILrp1ixFi@t2I1H4u zksSsq`m&J915p%*rJ$LDk`iFK7THngDFNBIzmAzKNGSmnjff%zJyl?jP6ZU#t-)m+ zTC}1%6k;+c^pG>h~n5SP$Mynf|UUQ-mGk({K^Z2 L*MQ+J3+4d;JZ$R> literal 0 HcmV?d00001 diff --git a/tests/Composer/Test/Util/Fixtures/Zip/nojson.zip b/tests/Composer/Test/Util/Fixtures/Zip/nojson.zip new file mode 100644 index 0000000000000000000000000000000000000000..e536b956ce1f7c30410c96e53079a7a272bee0d6 GIT binary patch literal 134 zcmWIWW@h1H0D;-TDSluElwe^HU`Wf)*AI>0VYvOiCgvIte`OS52=HcP5@p7vhX-ba d!&^rX6Ji1f+=KvcRyL40BM{mFX(JGa0RSi#7c&3= literal 0 HcmV?d00001 diff --git a/tests/Composer/Test/Util/Fixtures/Zip/root.zip b/tests/Composer/Test/Util/Fixtures/Zip/root.zip new file mode 100644 index 0000000000000000000000000000000000000000..fd08f4d34dca94997e0a368500e9db6d9458ee46 GIT binary patch literal 194 zcmWIWW@Zs#-~htlpcFp_B*4ocz>u7uTaaIzTBMg%oSzpO!NV~BZB0xb5PxM9VOaev zh=FP8o+EccRJB=`ZuwL5cgYbY)pb#N3<2Kk9QAp{+ogcUfpCB~Ba9<*fOmYO^o~FaN4*AxTAik* z2{)dL%~_!BcW{<YzU)LgyQ-2i#pKZKtBI5c|F|oE_%ktS7iS*FE=|M50!NPqi(^6jtqz$w;lm-d`6t`T9;Jrwua zFSm-5p}cqtKcnZLNQ*=1`#}c0c612b^@tNyd=1r@oS$2eUz}Q`msOmf2TR3y zZy|xW`dJVI)6zXh?u4jnvo788r{?dHBTB04qV&KHt + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\Zip; +use PHPUnit\Framework\TestCase; + +/** + * @author Andreas Schempp + */ +class ZipTest extends TestCase +{ + public function testThrowsExceptionIfZipExcentionIsNotLoaded() + { + if (extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is loaded.'); + } + + $this->setExpectedException('\RuntimeException', 'The Zip Util requires PHP\'s zip extension'); + + Zip::getComposerJson(''); + } + + public function testReturnsNullifTheZipIsNotFound() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/invalid.zip'); + + $this->assertNull($result); + } + + public function testReturnsNullIfTheZipIsEmpty() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/empty.zip'); + + $this->assertNull($result); + } + + public function testReturnsNullIfTheZipHasNoComposerJson() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/nojson.zip'); + + $this->assertNull($result); + } + + public function testReturnsNullIfTheComposerJsonIsInASubSubfolder() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/subfolder.zip'); + + $this->assertNull($result); + } + + public function testReturnsComposerJsonInZipRoot() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/root.zip'); + + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } + + public function testReturnsComposerJsonInFirstFolder() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/folder.zip'); + + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } + + public function testReturnsRootComposerJsonAndSkipsSubfolders() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple.zip'); + + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } +}