support for running .ext.phars without ext/phar
authorMichael Wallner <mike@php.net>
Tue, 25 Aug 2015 15:13:22 +0000 (17:13 +0200)
committerMichael Wallner <mike@php.net>
Tue, 25 Aug 2015 15:13:22 +0000 (17:13 +0200)
12 files changed:
Makefile
bin/pharext
build/create-phar.php
src/pharext/Archive.php [new file with mode: 0644]
src/pharext/Cli/Command.php
src/pharext/Installer.php
src/pharext/Packager.php
src/pharext/Task/Extract.php
src/pharext/Task/PharBuild.php
src/pharext/Task/PharCompress.php
src/pharext_installer.php
src/pharext_packager.php

index 161d2deaa6d68bc20df3ce1455d79cac49e4d823..887b82d974f9a534bc7718cdb0fa9b3c86507f9a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -28,4 +28,9 @@ release:
        cp build/Metadata.php.in src/pharext/Metadata.php && \
        git ci -am "back to dev"
 
+archive-test: bin/pharext
+       ./bin/pharext -vpgs ../apfd.git
+       -../php-5.5/sapi/cli/php ./apfd-1.0.1.ext.phar
+       -./apfd-1.0.1.ext.phar
+
 .PHONY: all clean test release
index 7640c6dff29c0b68c23dd6bfd4acd8e6fae23539..321039ee1d5d6cace5c5162a40bef6c0a884a922 100755 (executable)
Binary files a/bin/pharext and b/bin/pharext differ
index d2804a8c00fe12e2ffd09c3573dabbb66a13aaeb..26ac6e93899ece94ad582c5fa6359e19be51cc97 100644 (file)
@@ -4,14 +4,13 @@
  * Creates bin/pharext, invoked through the Makefile
  */
 
-set_include_path(dirname(__DIR__)."/src");
+set_include_path(dirname(__DIR__)."/src:".get_include_path());
 spl_autoload_register(function($c) {
        return include strtr($c, "\\_", "//") . ".php";
 });
 
-$file = (new pharext\Task\PharBuild(null, pharext\Metadata::all() + [
+$file = (new pharext\Task\PharBuild(null, __DIR__."/../src/pharext_packager.php", pharext\Metadata::all() + [
        "name" => "pharext",
-       "stub" => "pharext_packager.php",
        "license" => file_get_contents(__DIR__."/../LICENSE")
 ], false))->run();
 
diff --git a/src/pharext/Archive.php b/src/pharext/Archive.php
new file mode 100644 (file)
index 0000000..6a9b4a4
--- /dev/null
@@ -0,0 +1,257 @@
+<?php
+
+namespace pharext;
+
+use ArrayAccess;
+use pharext\Exception;
+
+class Archive implements ArrayAccess
+{
+       const HALT_COMPILER = "\137\137\150\141\154\164\137\143\157\155\160\151\154\145\162\50\51\73";
+       const SIGNED = 0x10000;
+       const SIG_MD5    = 0x0001;
+       const SIG_SHA1   = 0x0002;
+       const SIG_SHA256 = 0x0003;
+       const SIG_SHA512 = 0x0004;
+       const SIG_OPENSSL= 0x0010;
+
+       private static $siglen = [
+               self::SIG_MD5    => 16,
+               self::SIG_SHA1   => 20,
+               self::SIG_SHA256 => 32,
+               self::SIG_SHA512 => 64,
+               self::SIG_OPENSSL=> 0
+       ];
+
+       private static $sigalg = [
+               self::SIG_MD5    => "md5",
+               self::SIG_SHA1   => "sha1",
+               self::SIG_SHA256 => "sha256",
+               self::SIG_SHA512 => "sha512",
+               self::SIG_OPENSSL=> "openssl"
+       ];
+
+       private static $sigtyp = [
+               self::SIG_MD5    => "MD5",
+               self::SIG_SHA1   => "SHA-1",
+               self::SIG_SHA256 => "SHA-256",
+               self::SIG_SHA512 => "SHA-512",
+               self::SIG_OPENSSL=> "OpenSSL",
+       ];
+
+       const PERM_FILE_MASK = 0x01ff;
+       const COMP_FILE_MASK = 0xf000;
+       const COMP_GZ_FILE   = 0x1000;
+       const COMP_BZ2_FILE  = 0x2000;
+
+       const COMP_PHAR_MASK= 0xf000;
+       const COMP_PHAR_GZ  = 0x1000;
+       const COMP_PHAR_BZ2 = 0x2000;
+
+       private $file;
+       private $fd;
+       private $stub;
+       private $manifest;
+       private $signature;
+       private $extracted;
+
+       function __construct($file = null) {
+               if (strlen($file)) {
+                       $this->open($file);
+               }
+       }
+
+       function open($file) {
+               if (!$this->fd = @fopen($this->file = $file, "r")) {
+                       throw new Exception;
+               }
+               $this->stub = $this->readStub();
+               $this->manifest = $this->readManifest();
+               $this->signature = $this->readSignature();
+       }
+
+       function extract() {
+               return $this->extracted ?: $this->extractTo(new Tempdir("archive"));
+       }
+
+       function extractTo($dir) {
+               if ((string) $this->extracted == (string) $dir) {
+                       return $this->extracted;
+               }
+               foreach ($this->manifest["entries"] as $file => $entry) {
+                       fseek($this->fd, $this->manifest["offset"]+$entry["offset"]);
+                       $path = $dir."/$file";
+                       $dirn = dirname($path);
+                       if (!is_dir($dirn) && !@mkdir($dirn, 0777, true)) {
+                               throw new Exception;
+                       }
+                       if (!$fd = @fopen($path, "w")) {
+                               throw new Exception;
+                       }
+                       switch ($entry["flags"] & self::COMP_FILE_MASK) {
+                       case self::COMP_GZ_FILE:
+                               if (!@stream_filter_append($fd, "zlib.inflate")) {
+                                       throw new Exception;
+                               }
+                               break;
+                       case self::COMP_BZ2_FILE:
+                               if (!@stream_filter_append($fd, "bz2.decompress")) {
+                                       throw new Exception;
+                               }
+                               break;
+                       }
+                       if ($entry["osize"] != ($copied = stream_copy_to_stream($this->fd, $fd, $entry["csize"]))) {
+                               throw new Exception("Copied '$copied' of '$file', expected '{$entry["osize"]}' from '{$entry["csize"]}");
+                       }
+                       fclose($fd);
+
+                       $crc = hexdec(hash_file("crc32b", $path));
+                       if ($crc !== $entry["crc32"]) {
+                               throw new Exception("CRC mismatch of '$file': '$crc' != '{$entry["crc32"]}");
+                       }
+
+                       chmod($path, $entry["flags"] & self::PERM_FILE_MASK);
+                       touch($path, $entry["stamp"]);
+               }
+               return $this->extracted = $dir;
+       }
+
+       function offsetExists($o) {
+               return isset($this->entries[$o]);
+       }
+
+       function offsetGet($o) {
+               $this->extract();
+               return new \SplFileInfo($this->extracted."/$o");
+       }
+
+       function offsetSet($o, $v) {
+               throw new Exception("Archive is read-only");
+       }
+
+       function offsetUnset($o) {
+               throw new Exception("Archive is read-only");
+       }
+
+       function getSignature() {
+               /* compatible with Phar::getSignature() */
+               return [
+                       "hash_type" => self::$sigtyp[$this->signature["flags"]],
+                       "hash" => strtoupper(bin2hex($this->signature["hash"])),
+               ];
+       }
+
+       function getPath() {
+               /* compatible with Phar::getPath() */
+               return new \SplFileInfo($this->file);
+       }
+
+       function getMetadata($key = null) {
+               if (isset($key)) {
+                       return $this->manifest["meta"][$key];
+               }
+               return $this->manifest["meta"];
+       }
+
+       private function readStub() {
+               $stub = "";
+               while (!feof($this->fd)) {
+                       $line = fgets($this->fd);
+                       $stub .= $line;
+                       if (false !== stripos($line, self::HALT_COMPILER)) {
+                               /* check for '?>' on a separate line */
+                               if ('?>' === fread($this->fd, 2)) {
+                                       $stub .= '?>' . fgets($this->fd);
+                               } else {
+                                       fseek($this->fd, -2, SEEK_CUR);
+                               }
+                               break;
+                       }
+               }
+               return $stub;
+       }
+
+       private function readManifest() {
+               $current = ftell($this->fd);
+               $header = unpack("Vlen/Vnum/napi/Vflags", fread($this->fd, 14));
+               if (($alias = current(unpack("V", fread($this->fd, 4))))) {
+                       $alias = fread($this->fd, $alias);
+               }
+               if (($meta = current(unpack("V", fread($this->fd, 4))))) {
+                       $meta = unserialize(fread($this->fd, $meta));
+               }
+               $entries = [];
+               for ($i = 0; $i < $header["num"]; ++$i) {
+                       $this->readEntry($entries);
+               }
+               $offset = ftell($this->fd);
+               if (($length = $offset - $current - 4) != $header["len"]) {
+                       throw new Exception("Manifest length read was '$length', expected '{$header["len"]}'");
+               }
+               return $header + compact("alias", "meta", "entries", "offset");
+       }
+
+       private function readEntry(array &$entries) {
+               if (!count($entries)) {
+                       $offset = 0;
+               } else {
+                       $last = end($entries);
+                       $offset = $last["offset"] + $last["csize"];
+               }
+               if (($file = current(unpack("V", fread($this->fd, 4))))) {
+                       $file = fread($this->fd, $file);
+               }
+               if ($file === 0 || !strlen($file)) {
+                       throw new Exception("Empty file name encountered at offset '$offset'");
+               }
+               $header = unpack("Vosize/Vstamp/Vcsize/Vcrc32/Vflags", fread($this->fd, 20));
+               if (($meta = current(unpack("V", fread($this->fd, 4))))) {
+                       $meta = unserialize(fread($this->fd, $meta));
+               } else {
+                       $meta = [];
+               }
+               $entries[$file] =  $header + compact("meta", "offset");
+       }
+
+       private function readSignature() {
+               fseek($this->fd, -8, SEEK_END);
+               $sig = unpack("Vflags/Z4magic", fread($this->fd, 8));
+               $end = ftell($this->fd);
+
+               if ($sig["magic"] !== "GBMB") {
+                       throw new Exception("Invalid signature magic value '{$sig["magic"]}");
+               }
+
+               switch ($sig["flags"]) {
+               case self::SIG_OPENSSL:
+                       fseek($this->fd, -12, SEEK_END);
+                       if (($hash = current(unpack("V", fread($this->fd, 4))))) {
+                               $offset = 4 + $hash;
+                               fseek($this->fd, -$offset, SEEK_CUR);
+                               $hash = fread($this->fd, $hash);
+                               fseek($this->fd, 0, SEEK_SET);
+                               $valid = openssl_verify(fread($this->fd, $end - $offset - 8),
+                                       $hash, file_get_contents($this->file.".pubkey")) === 1;
+                       }
+                       break;
+
+               case self::SIG_MD5:
+               case self::SIG_SHA1:
+               case self::SIG_SHA256:
+               case self::SIG_SHA512:
+                       $offset = 8 + self::$siglen[$sig["flags"]];
+                       fseek($this->fd, -$offset, SEEK_END);
+                       $hash = fread($this->fd, self::$siglen[$sig["flags"]]);
+                       $algo = hash_init(self::$sigalg[$sig["flags"]]);
+                       fseek($this->fd, 0, SEEK_SET);
+                       hash_update_stream($algo, $this->fd, $end - $offset);
+                       $valid = hash_final($algo, true) === $hash;
+                       break;
+
+               default:
+                       throw new Exception("Invalid signature type '{$sig["flags"]}");
+               }
+
+               return $sig + compact("hash", "valid");
+       }
+}
index b733885a26a9c39e52d1d15544a2a631b2e889ce..bc0afbbf374ac4bfca064ff1215f5fd8ffcdcbab 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace pharext\Cli;
 
+use pharext\Archive;
 use pharext\Cli\Args as CliArgs;
 
 use Phar;
@@ -42,7 +43,11 @@ trait Command
         * @return mixed
         */
        public function metadata($key = null) {
-               $running = new Phar(Phar::running(false));
+               if (extension_loaded("Phar")) {
+                       $running = new Phar(Phar::running(false));
+               } else {
+                       $running = new Archive(PHAREXT_PHAR);
+               }
 
                if ($key === "signature") {
                        $sig = $running->getSignature();
index 29fe271c6bec2e9588b0cef342ba2d8bbeb8901d..0171eaeb6ce695bd7211ce8faa998ca6f2e17f67 100644 (file)
@@ -68,7 +68,7 @@ class Installer implements Command
                }
        }
        
-       private function extract(Phar $phar) {
+       private function extract($phar) {
                $temp = (new Task\Extract($phar))->run($this->verbosity());
                $this->cleanup[] = new Task\Cleanup($temp);
                return $temp;
@@ -90,13 +90,17 @@ class Installer implements Command
 
        private function load() {
                $list = new SplObjectStorage();
-               $phar = new Phar(Phar::running(false));
+               $phar = extension_loaded("Phar") 
+                       ? new Phar(Phar::running(false))
+                       : new Archive(PHAREXT_PHAR);
                $temp = $this->extract($phar);
 
                foreach ($phar as $entry) {
                        $dep_file = $entry->getBaseName();
                        if (fnmatch("*.ext.phar*", $dep_file)) {
-                               $dep_phar = new Phar("$temp/$dep_file");
+                               $dep_phar = extension_loaded("Phar")
+                                       ? new Phar("$temp/$dep_file")
+                                       : new Archive("$temp/$dep_file");
                                $list[$dep_phar] = $this->extract($dep_phar);
                        }
                }
index bdb83bb4515cdebc7f1e4b7bdb37c5a87d28081f..10542e489a74611366a2033e9e0600e8afad71fa 100644 (file)
@@ -247,10 +247,9 @@ class Packager implements Command
                                "name" => $this->args->name,
                                "release" => $this->args->release,
                                "license" => $this->source->getLicense(),
-                               "stub" => "pharext_installer.php",
                                "type" => $this->args->zend ? "zend_extension" : "extension",
                        ]);
-                       $file = (new Task\PharBuild($this->source, $meta))->run($this->verbosity());
+                       $file = (new Task\PharBuild($this->source, __DIR__."/../pharext_installer.php", $meta))->run($this->verbosity());
                } catch (\Exception $e) {
                        $this->error("%s\n", $e->getMessage());
                        exit(self::EBUILD);
index d68b426dd129a4ac5d6956f33cf4a9ecbd4281b2..3aa3f85950707841e0263d5a8ff441905fd91fad 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace pharext\Task;
 
+use pharext\Archive;
 use pharext\Task;
 use pharext\Tempdir;
 
@@ -22,7 +23,7 @@ class Extract implements Task
         * @param mixed $source archive location
         */
        public function __construct($source) {
-               if ($source instanceof Phar || $source instanceof PharData) {
+               if ($source instanceof Phar || $source instanceof PharData || $source instanceof Archive) {
                        $this->source = $source;
                } else {
                        $this->source = new PharData($source);
@@ -37,6 +38,9 @@ class Extract implements Task
                if ($verbose) {
                        printf("Extracting %s ...\n", basename($this->source->getPath()));
                }
+               if ($this->source instanceof Archive) {
+                       return $this->source->extract();
+               }
                $dest = new Tempdir("extract");
                $this->source->extractTo($dest);
                return $dest;
index 03ac42c1019b5d6505eebd0f80b7ef07f9d92206..25dd7a42a911ba4f6e7195e5654e8985030f2b1e 100644 (file)
@@ -19,6 +19,11 @@ class PharBuild implements Task
         */
        private $source;
 
+       /**
+        * @var string
+        */
+       private $stub;
+
        /**
         * @var array
         */
@@ -31,11 +36,13 @@ class PharBuild implements Task
 
        /**
         * @param SourceDir $source extension source directory
+        * @param string $stub path to phar stub
         * @param array $meta phar meta data
         * @param bool $readonly whether the stub has -dphar.readonly=1 set
         */
-       public function __construct(SourceDir $source = null, array $meta = null, $readonly = true) {
+       public function __construct(SourceDir $source = null, $stub, array $meta = null, $readonly = true) {
                $this->source = $source;
+               $this->stub = $stub;
                $this->meta = $meta;
                $this->readonly = $readonly;
        }
@@ -57,12 +64,12 @@ class PharBuild implements Task
 
                if ($this->meta) {
                        $phar->setMetadata($this->meta);
-                       if (isset($this->meta["stub"])) {
-                               $phar->setDefaultStub($this->meta["stub"]);
-                               $phar->setStub("#!/usr/bin/php -dphar.readonly=" .
-                                       intval($this->readonly) ."\n".
-                                       $phar->getStub());
-                       }
+               }
+               if (is_file($this->stub)) {
+                       $stub = preg_replace_callback('/^#include <([^>]+)>/m', function($includes) {
+                               return file_get_contents($includes[1], true, null, 5);
+                       }, file_get_contents($this->stub));
+                       $phar->setStub($stub);
                }
 
                $phar->buildFromIterator((new Task\BundleGenerator)->run());
index ea0607f39a6e9291a91204b9cdcfa7a97d0a2e20..78a93493018dd3eded7797c670bf87483567219b 100644 (file)
@@ -58,12 +58,10 @@ class PharCompress implements Task
                if ($verbose) {
                        printf("Compressing %s ...\n", basename($this->package->getPath()));
                }
+               /* stop shebang */
+               $stub = $this->package->getStub();
                $phar = $this->package->compress($this->encoding);
-               $meta = $phar->getMetadata();
-               if (isset($meta["stub"])) {
-                       /* drop shebang */
-                       $phar->setDefaultStub($meta["stub"]);
-               }
+               $phar->setStub(substr($stub, strpos($stub, "\n")+1));
                return $this->file . $this->extension;
        }
 }
index 89e44a3ec62073acea09f0386900e66ee3008bb6..19386f203b87632bf1c1eea7d9841008cb1e14c4 100644 (file)
@@ -1,11 +1,38 @@
+#!/usr/bin/env php
 <?php
+
 /**
  * The installer sub-stub for extension phars
  */
 
+namespace pharext;
+
+define("PHAREXT_PHAR", __FILE__);
+
 spl_autoload_register(function($c) {
        return include strtr($c, "\\_", "//") . ".php";
 });
 
-$installer = new pharext\Installer();
+#include <pharext/Exception.php>
+#include <pharext/Tempname.php>
+#include <pharext/Tempfile.php>
+#include <pharext/Tempdir.php>
+#include <pharext/Archive.php>
+
+namespace pharext;
+
+if (extension_loaded("Phar")) {
+       \Phar::interceptFileFuncs();
+       \Phar::mapPhar();
+       $phardir = "phar://".__FILE__;
+} else {
+       $archive = new Archive(__FILE__);
+       $phardir = $archive->extract();
+}
+
+set_include_path("$phardir:". get_include_path());
+
+$installer = new Installer();
 $installer->run($argc, $argv);
+
+__HALT_COMPILER();
index f85c2f18bdf76da25196cf77abcc141947b945c3..bdce719fbe72ae36d6ccc1655cd106a65f668dd7 100644 (file)
@@ -1,10 +1,36 @@
+#!/usr/bin/env php -dphar.readonly=0
 <?php
+
 /**
  * The packager sub-stub for bin/pharext
  */
+
+namespace pharext;
+
 spl_autoload_register(function($c) {
        return include strtr($c, "\\_", "//") . ".php";
 });
 
-$packager = new pharext\Packager();
+set_include_path('phar://' . __FILE__ .":". get_include_path());
+
+if (!extension_loaded("Phar")) {
+       fprintf(STDERR, "ERROR: Phar extension not loaded\n\n");
+       fprintf(STDERR, "\tPlease load the phar extension in your php.ini\n".
+                                       "\tor rebuild PHP with the --enable-phar flag.\n\n");
+       exit(1);
+}
+
+if (ini_get("phar.readonly")) {
+       fprintf(STDERR, "ERROR: Phar is configured read-only\n\n");
+       fprintf(STDERR, "\tPlease specify phar.readonly=0 in your php.ini\n".
+                                       "\tor run this command with php -dphar.readonly=0\n\n");
+       exit(1);
+}
+
+\Phar::interceptFileFuncs();
+\Phar::mapPhar();
+
+$packager = new Packager();
 $packager->run($argc, $argv);
+
+__HALT_COMPILER();