<?php
/**
 * api: php
 * title: Phar generation
 * description: PHP script template to get Phar assembled
 * version: 0.3
 *
 * Used as template php script for -t phar building.
 * Extracts passed Ruby variables as PHP strings, so some
 * need constant(), strlen() or boolean interpretation.
 *
 */


#-- Parameters from phar.rb passed as JSON in argv[1]
extract(json_decode($_SERVER["argv"][1], TRUE));


/**
 * Create phar
 *  · Tempoarary filename.
 *  · FPM package name as Phar name.
 *  · Read complete staging dir.
 */
$p = new Phar($tmpf, 0, $name);
$p->startBuffering();
$p->buildFromDirectory($srcdir);


/**
 * Add stub
 *  · Use given file
 *  · Default init+index.php for native Phars.
 *  · Stub for zip/tar archive.
 */
if (strlen($stub) && file_exists($stub)) {
   $p->setStub(file_get_contents($stub));
}
elseif (constant($format) == Phar::PHAR) {
   $p->setDefaultStub("__init__.php", "index.php");
}
else {
   $p->setDefaultStub();
}


/**
 * Phar meta data
 *  · Use injected JSON blob.
 *  · Contains at least fpm package infos,
 *  · May contain additional fields - attrs{} from `-s src` or `composer` module
 *  · Also build and include a class `map` structure.
 */
if ($meta) {
   $meta["map"] = map_phar($p, $srcdir);
   $p->setMetadata($meta);
}


/**
 * Complete packaging
 *  · Set per-file compression
 *  · Add signature, if any.
 *  · Compressed outer archive hull (tar or phar).
 */
if (constant($filegz)) {
   $p->compressFiles(constant($filegz));
}
if (strlen($sign)) {
   $p->setSignatureAlgorithm(Phar::OPENSSL, file_get_contents($sign));
}
else {
   $p->setSignatureAlgorithm(Phar::SHA256);
}
// Whole-archive compression; output goes to a different filename. (Cleaned up in Ruby...)
$p->stopBuffering();
if (strlen($extout)) {
   $p->compress(constant($hullgz));
}




/**
 * Traverse Phar entries and augment Phar meta class/function/const `map`,
 * which lists identifiers as "vnd\pkg\class" => "internal-filename.php".
 *
 * Needs to manually reread the directory and files, because freshly-built
 * Phars` RecursiveDirectoryIterator + offsetGet() return zilch.
 *
 */
function map_phar($p, $dir) {

   // prepare empty map
   $map = array("class"=>array(), "function"=>array(), "const"=>array());
   
   // switch to staging dir, but keep old path as reference
   $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
   $it = new RegexIterator($it, "~^(?!.*tests?/).+\.php$~");
   
   // only read .php files
   foreach ($it as $fn) {

      // cut off $dir prefix      
      $int_fn = ltrim(substr($fn, strlen($dir)), "/");

      // collect identifiers
      $def = new RegexPhpIdentifierDeclarations(file_get_contents($fn));
      foreach ($def->identifiers() as $type=>$list) {
         $map[$type] = array_merge($map[$type], array_fill_keys($list, $int_fn));
      }
   }

   // prepare for PHP compliant case-insensitive lookups
   $map["class"] = array_change_key_case($map["class"]);
   $map["function"] = array_change_key_case($map["function"]);
   return $map;
}



/**
 * @source  shared.phar
 * @license Public Domain
 *
 * Shallow regex-lexing to uncover namespace/class/function identifiers.
 *
 * By relying on keyword context and a bit of block-level skipping, this still
 * uncovers correctly nested and deferred declaration constructs. Plain function
 * injections within methods however are overlooked. Dynamic declarations within
 * strings are ignored due to non-code being stripped beforehand.
 *
 * This approach doesn't assert any nesting/syntax correctness; as implementing
 * it per recursive subroutines wouldn't provide anything like a parse tree via
 * PCREs interface / and else inverted the speed advantage here.
 *
 */
class RegexPhpIdentifierDeclarations {

    /**
     * Regex all the things.
     *
     */
    public function __construct($source) {

        /**
         * Remove non-code sections (comments and strings actually),
         * but convert define() string into constant literal before.
         *
         */
        $source = preg_replace(
            "~\b define \s*\(\s* ([\"\']) ([\\w\\x7F-\\xFF]+) \\1 \s*, ~ix",
            "const $2 =", $source
        );
        $source = preg_replace("~
                (?: \A | \?\>) .*? \<\?(?:php|=)+?     # Open+closing PHP token
              | /\* .*? \*/                            # Multiline /* comments */
              | // \V*                                 # Singe line // comment
              | \# \V*                                 # Hash comment
              |  \" (?:[^\"\\\\] | \\\\.)* \"          # Double quoted string
              |  \' (?:[^\'\\\\] | \\\\.)* \'          # Single quoted string
              | <<<\s* (\w+) .+? ^\\1                  # Heredoc string
              | <<<\s* '(\V+)' .+? ^\\1                # Nowdoc string
            ~smix",
            "", $source
        );

        /** 
         * Match identifiers and skip class block {} structures. (While one could recurse
         * into methods or namespace{} blocks individually, practically only the outermost
         * interface is relevant for the autoloader.)
         *
         */
        preg_match_all("~
           (?: (?<![\\x7F-\\xFF]) \b )                 # Only match constructs at word breaks
           (?:
              namespace \s+
                  ([\\w\\x7F-\\xFF\\\\]+) \s* [{;]     # Namespace identifier
            | (?is:class|interface|trait) \s+
                  ([\\w\\x7F-\\xFF]+)  [^\{\}]*        # Class declaration
                  ((?>\{ (?: [^\{\}]* | (?-1) )*\}))   # Recursive {...} block skipping
            | function \s+
                  ([\\w\\x7F-\\xFF]+) \s* \(           # Plain functions
            | (?is: const\s+| define\s*\( )
                  ([\\w\\x7F-\\xFF]+) \s* [=,]         # Constants (const/define)
           )~ix",
           $source, $this->matches, PREG_SET_ORDER
        );
    }

    
    /**
     * Nested array of identifier strings
     *  → Namespaces in [1]
     *  → Classes in [2]
     *  → Function names in [4]
     *  → Constants in [5]
     *
     */
    var $matches = array();


    /**
     * Join matched namespace and construct strings into our beloved named identifier groups.
     *
     */
    public function identifiers() {

        // Result list, and current $ns namespace
        $r = array(
            "class" => array(),
            "function" => array(),
            "const" => array(),
        );
        $ns = "";

        /**
         * Check match group for entries.
         * Probe in order of likelihood, at least one will be there. And since identifiers
         * with leading zeros are invalid, the plain truthy test is preferrable to strlen.
         *
         */
        foreach ($this->matches as $name) {
            if ($name[1]) {
                $ns = $name[1] . "\\";
            }
            elseif ($name[2]) {
                $r["class"][] = $ns . $name[2];
            }
            elseif ($name[4]) {
                $r["function"][] = $ns . $name[4];
            }
            elseif ($name[5]) {
                $r["const"][] = $ns . $name[5];
            }
        }
        return $r;
    }
}


