From: Michael Wallner Date: Tue, 25 Aug 2015 15:13:22 +0000 (+0200) Subject: support for running .ext.phars without ext/phar X-Git-Tag: v4.1.0~19 X-Git-Url: https://git.m6w6.name/?a=commitdiff_plain;h=4af08c149c8b66e9fe8b9b0fb9f276e0745377d1;p=pharext%2Fpharext support for running .ext.phars without ext/phar --- diff --git a/Makefile b/Makefile index 161d2de..887b82d 100644 --- 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 diff --git a/bin/pharext b/bin/pharext index 7640c6d..321039e 100755 Binary files a/bin/pharext and b/bin/pharext differ diff --git a/build/create-phar.php b/build/create-phar.php index d2804a8..26ac6e9 100644 --- a/build/create-phar.php +++ b/build/create-phar.php @@ -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 index 0000000..6a9b4a4 --- /dev/null +++ b/src/pharext/Archive.php @@ -0,0 +1,257 @@ + 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"); + } +} diff --git a/src/pharext/Cli/Command.php b/src/pharext/Cli/Command.php index b733885..bc0afbb 100644 --- a/src/pharext/Cli/Command.php +++ b/src/pharext/Cli/Command.php @@ -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(); diff --git a/src/pharext/Installer.php b/src/pharext/Installer.php index 29fe271..0171eae 100644 --- a/src/pharext/Installer.php +++ b/src/pharext/Installer.php @@ -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); } } diff --git a/src/pharext/Packager.php b/src/pharext/Packager.php index bdb83bb..10542e4 100644 --- a/src/pharext/Packager.php +++ b/src/pharext/Packager.php @@ -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); diff --git a/src/pharext/Task/Extract.php b/src/pharext/Task/Extract.php index d68b426..3aa3f85 100644 --- a/src/pharext/Task/Extract.php +++ b/src/pharext/Task/Extract.php @@ -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; diff --git a/src/pharext/Task/PharBuild.php b/src/pharext/Task/PharBuild.php index 03ac42c..25dd7a4 100644 --- a/src/pharext/Task/PharBuild.php +++ b/src/pharext/Task/PharBuild.php @@ -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()); diff --git a/src/pharext/Task/PharCompress.php b/src/pharext/Task/PharCompress.php index ea0607f..78a9349 100644 --- a/src/pharext/Task/PharCompress.php +++ b/src/pharext/Task/PharCompress.php @@ -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; } } diff --git a/src/pharext_installer.php b/src/pharext_installer.php index 89e44a3..19386f2 100644 --- a/src/pharext_installer.php +++ b/src/pharext_installer.php @@ -1,11 +1,38 @@ +#!/usr/bin/env php +#include +#include +#include +#include + +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(); diff --git a/src/pharext_packager.php b/src/pharext_packager.php index f85c2f1..bdce719 100644 --- a/src/pharext_packager.php +++ b/src/pharext_packager.php @@ -1,10 +1,36 @@ +#!/usr/bin/env php -dphar.readonly=0 run($argc, $argv); + +__HALT_COMPILER();