<?php

/**
 * Mariph
 * Marshalled Ruby in PHP
 * 
 * Whatever you call it, this allows you to read data stored in Ruby's
 * Marshal format. For the process: Mariph_Tokenizer takes raw string
 * data in the marshal format, and turns it into an array of tokens.
 * Mariph_Interpreter then takes those tokens and turns them into properly
 * organized PHP data structures.
 * 
 * When writing this, I only needed a system to read data, and not write.
 * So, the ability to write in Ruby's marshal format is not here, and may
 * not be for some time. Additionally, this does not parse all data types
 * listed in the marshal spec - only the ones I have personally needed.
 * The project's original purpose was only to parse RPG Maker XP data files.
 *
 * Quick usage: $data = mariph_load($marshalString);
 *
 * @copyright Copyright (c) 2009 Steven Harris
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
 */

/**
 * Quick one-call function to generate a PHP representation of marshalled
 * Ruby data. Each top-level element in the data will be an element in the
 * returned array. If the string has only one top-level element, it will
 * merely be in $array[0].
 *
 * @param  string $data
 * @return array
 */
function mariph_load($data)
{
    
$tokenizer   = new Mariph_Tokenizer($data);
    
$interpreter = new Mariph_Interpreter($tokenizer->tokens);
    
    return 
$interpreter->data;
}

/**
 * This class receives an array of tokens, and stores the results in
 * its $data property.
 */
class Mariph_Interpreter
{
    
/**
     * Array of raw tokens created from Mariph_Tokenizer.
     *
     * @var array
     */
    
protected $tokens = array();
    
    
/**
     * Container for parsed data. Each top level element in the dumpfile
     * is an element in this array.
     *
     * @var array
     */
    
public $data = array();
    
    
/**
     * Current parsing index.
     *
     * @var int
     */
    
protected $idx  0;
    
    
/**
     * Receives an array of tokens, and populates $this->data
     *
     * @param  array $tokens
     */
    
public function __construct(array $tokens)
    {
        
$this->tokens $tokens;
        
$len sizeof($this->tokens);
        
        while (
$this->idx $len) {
            
$this->data[] = $this->parse();
            
$this->idx++;
        }
    }
    
    
/**
     * Top-level parsing function. Depending on the token type, either handles
     * the token itself, or passes it on to another sub-function.
     *
     * @return mixed
     */
    
protected function parse()
    {
        
$token $this->token();
        
$type  $token[0];
        
        
// basic type - just handle it here
        
if ($type === 'nil' OR $type === 'bool' OR $type === 'int' OR $type === 'float'
            
OR $type === 'string' OR $type === 'regexp' OR $type === 'symbol')
        {
            return 
$token[1];
        }
        
        
// pass arrays, hashes, objects, and user-defined to a sub function
        
if ($type === 'array') {
            return 
$this->parseArray($token[1]);
        }
        
        if (
$type === 'hash') {
            return 
$this->parseHash($token[1]);
        }
        
        if (
$type === 'object') {
            return 
$this->parseObject($token[1], $token[2]);
        }
        
        if (
$type === 'user') {
            return 
$this->parseUser($token[1], $token[2]);
        }
    }
    
    
/**
     * Parse an array from the token list, given the number of elements.
     *
     * @param  int $count
     * @return array
     */
    
protected function parseArray($count)
    {
        
$array = array();
        for (
$i 0$i $count$i++) {
            
$this->idx++;
            
$array[] = $this->parse();
        }
        
        return 
$array;
    }
    
    
/**
     * Parse out a hash from the token list, given the number of elements.
     * Returns a Mariph_Hash object, allowing for the more flexible key types
     * that Ruby supports.
     *
     * @param  int $count
     * @return Mariph_Hash
     */
    
protected function parseHash($count)
    {
        
$hash = new Mariph_Hash();
        for (
$i 0$i $count$i++) {
            
$this->idx++;
            
$key $this->parse();
            
$this->idx++;
            
$value $this->parse();
            
$hash->set($key$value);
        }
        
        return 
$hash;
    }
    
    
/**
     * Parses an object, given its name and the number of properties. Returns
     * an stdClass, with the class name stored in a __mariphName property.
     *
     * @param  string $name
     * @param  int    $count
     * @return stdClass
     */
    
protected function parseObject($name$count)
    {
        
$obj = new stdClass;
        
$obj->__mariphName $name;
        
        for (
$i 0$i $count$i++) {
            
$this->idx++;
            
$key str_replace('@'''$this->parse());
            
$this->idx++;
            
$value $this->parse();
            
$obj->$key $value;
        }
        
        return 
$obj;
    }
    
    
/**
     * Parses a user-defined data type. The class name will be the same as
     * its Ruby name, with "::" being changed into "__". For example, a
     * class stored as Blah::Foo would need a PHP equivalent of Blah__Foo.
     *
     * In addition, the class must have a static method named rubyLoad(),
     * which should take the raw data, and return an instance of the class.
     *
     * @param  string $name
     * @param  string $rawData
     * @return mixed
     * @throws Exception If class is non-existant or invalid.
     */
    
protected function parseUser($name$rawData)
    {
        
$name str_replace('::''__'$name);
        if (!
class_exists($name)) {
            throw new 
Exception("Class $name does not exist.");
        }
        
        if (!
is_callable(array($name'rubyLoad'))) {
            throw new 
Exception("Class $name must have a rubyLoad method.");
        }
        
        return 
call_user_func(array($name'rubyLoad'), $rawData);
    }
    
    
/**
     * Returns the token at the current parse position.
     *
     * @return array
     */
    
protected function token()
    {
        return 
$this->tokens[$this->idx];
    }
}

/*
    == Token Types ==
    
    nil:    array('nil', null)
    true:   array('bool', true)
    false:  array('bool', false)
    int:    array('int', $value)
    float:  array('float', $value)
    string: array('string', $value)
    symbol: array('symbol', $value)
    regexp: array('regexp', (string) $value)
    array:  array('array', $numElements)
    hash:   array('hash', $numElements)
    object: array('object', $name, $numElements)
    user:   array('user', $name, $rawData)
*/

/**
 * Takes marshalled Ruby data, and turns it into an array of tokens.
 */
class Mariph_Tokenizer
{
    
/**
     * Symbol reference table.
     *
     * @var array
     */
    
protected $symbolRef = array();
    
    
/**
     * Raw marshal data.
     *
     * @var string
     */
    
protected $data '';
    
    
/**
     * Current parsing index.
     *
     * @var int
     */
    
protected $idx 0;
    
    
/**
     * Token list, populated when parsing.
     *
     * @var array
     */
    
public $tokens = array();
    
    
/**
     * Takes the given marshal data and populates $this->tokens
     *
     * @param  string $data
     */
    
public function __construct($data)
    {
        
$this->data substr($data2);
        
$len strlen($this->data);
        
        while (
$this->idx $len) {
            
$this->nextToken();
            
$this->idx++;
        }
    }
    
    
/**
     * Reads the next piece of data into a token.
     */
    
protected function nextToken()
    {
        switch (
$this->ch())
        {
            case 
'0':
                
$this->tokens[] = array('nil'null);
                break;
            case 
'T':
                
$this->tokens[] = array('bool'true);
                break;
            case 
'F':
                
$this->tokens[] = array('bool'false);
                break;
            case 
'i':
                
$this->tokens[] = array('int'$this->readPackedInt());
                break;
            case 
'f':
                
$this->tokens[] = array('float', (float) $this->readString());
                break;
            case 
'"':
                
$this->tokens[] = array('string'$this->readString());
                break;
            case 
':':
                
$this->tokens[] = array('symbol'$this->readSymbol());
                break;
            case 
';':
                
$this->tokens[] = array('symbol'$this->readSymlink());
                break;
            case 
'/':
                
$this->tokens[] = array('regexp'$this->readString());
                break;
            case 
'[':
                
$this->tokens[] = array('array'$this->readPackedInt());
                break;
            case 
'{':
                
$this->tokens[] = array('hash'$this->readPackedInt());
                break;
            case 
'o':
                
// symbol name, then # elements
                
$this->tokens[] = array('object',
                                        
$this->readSymbolOrSymlink(),
                                        
$this->readPackedInt());
                break;
            case 
'u':
                
// symbol name, then raw data
                
$this->tokens[] = array('user',
                                        
$this->readSymbolOrSymlink(),
                                        
$this->readString());
                break;
        }
    }
    
    
/**
     * Returns the character at the current parsing position.
     *
     * @return string
     */
    
protected function ch()
    {
        return 
$this->data[$this->idx];
    }
    
    
/**
     * Reads a packed integer from the data.
     *
     * @return int
     */
    
protected function readPackedInt()
    {
        
$this->idx++;
        
$first = (ord($this->ch()) ^ 128) - 128;
        
        
// simple forms
        
if ($first === 0) {
            return 
0;
        }
        else if (
$first 4) {
            return 
$first 5;
        }
        else if (
$first < -4) {
            return 
$first 5;
        }
        
        
// advanced...
        // read $first bytes ahead and build an int in LE order
        
$int 0;
        for (
$i 0$i abs($first); $i++) {
            
$this->idx++;
            
$int |= ord($this->ch()) << ($i);
        }
        
        return (
$first 0) ? -$int $int;
    }
    
    
/**
     * Reads a string (packed int + byte sequence) from the data.
     *
     * @return string
     */
    
protected function readString()
    {
        
$len $this->readPackedInt();
        
$this->idx++;
        
        
$str substr($this->data$this->idx$len);
        
$this->idx += $len 1;
        
        return 
$str;
    }
    
    
/**
     * Reads a symbol from the data, and stores it in symbolRef.
     *
     * @return string
     */
    
protected function readSymbol()
    {
        
$symbol $this->readString();
        
$this->symbolRef[] = $symbol;
        
        return 
$symbol;
    }
    
    
/**
     * Reads a symlink from the data, and returns the symbol it references.
     *
     * @return string
     */
    
protected function readSymlink()
    {
        
$pos $this->readPackedInt();
        return 
$this->symbolRef[$pos];
    }
    
    
/**
     * Reads either a symbol or symlink, depending on the next character.
     * Commonly called after finding an object or user-defined class.
     *
     * @return string
     */
    
protected function readSymbolOrSymlink()
    {
        
$this->idx++;
        if (
$this->ch() === ':') {
            return 
$this->readSymbol();
        }
        else {
            return 
$this->readSymlink();
        }
    }
}

/**
 * Utility class to handle a hash with arbitrary keys.
 */ 
class Mariph_Hash implements Countable
{
    
/**
     * Array of keys.
     *
     * @var array
     */
    
protected $keys = array();
    
    
/**
     * Array of values.
     *
     * @var array
     */
    
protected $values = array();
    
    
/**
     * Array of hashed keys.
     *
     * @var array
     */
    
protected $hashes = array();
    
    
/**
     * Current position in the hash, for iteration.
     *
     * @var int
     */
    
protected $pos 0;
    
    
/**
     * Constructs a new instance of Mariph_Hash, optionally initializing
     * with the given array of elements.
     *
     * @param  array $init
     */
    
public function __construct(array $init = array())
    {
        
$this->keys   array_keys($init);
        
$this->values array_values($init);
        
        foreach (
$this->keys AS $k) {
            
$this->hashes[] = $this->encodeKey($k);
        }
    }
    
    
/**
     * Sets a value to a given key. Overwrites the existing value,
     * if the key already exists.
     *
     * @param  mixed $key
     * @param  mixed $value
     * @return Mariph_Hash
     */
    
public function set($key$value)
    {
        
// see if the key exists
        
$index array_search($this->encodeKey($key), $this->hashes);
        
        
// if it exists, update the value. else, insert new info.
        
if ($index !== false) {
            
$this->values[$index] = $value;
        }
        else {
            
$this->keys[]   = $key;
            
$this->values[] = $value;
            
$this->hashes[] = $this->encodeKey($key);
        }
        
        return 
$this;
    }
    
    
/**
     * Retrieves a value from the hash, given a key. If the key does not
     * exist, returns the value given for $default.
     *
     * @param  mixed $key
     * @param  mixed $default
     * @return mixed
     */
    
public function get($key$default null)
    {
        
$index array_search($this->encodeKey($key), $this->hashes);
        return (
$index !== false) ? $this->values[$index] : $default;
    }
    
    
/**
     * Encodes the given key into a string representation.
     *
     * @param  mixed $key
     * @return string
     */
    
protected function encodeKey($key)
    {
        return 
md5(serialize($key));
    }
    
    
/**
     * Iterates through the hash, calling $callback on each element.
     *
     * @param  callback $callback Callback which takes (value, key) as args.
     */
    
public function iterate($callback)
    {
        
$size $this->count();
        for (
$i 0$i $size$i++) {
            
call_user_func($callback$this->values[$i], $this->keys[$i]);
        }
    }
    
    
/**
     * Returns the ($key, $value) pair at the current position, similar
     * to the standard each() function. Returns false if the hash position
     * is at the end. Increments the position afterwards.
     *
     * Example:
     * while (list($key, $value) = $hash->each()) {
     *   var_dump($key, $value);
     * }
     *
     * @return array ($key, $val)
     */
    
public function each()
    {
        if (
$this->pos >= $this->count()) {
            return 
false;
        }
        
        
$key $this->keys[$this->pos];
        
$val $this->values[$this->pos];
        
$this->pos++;
        
        return array(
$key$val);
    }
    
    
/**
     * Resets the position to zero.
     */
    
public function reset()
    {
        
$this->pos 0;
    }
    
    
/**
     * Implementation for Countable. Returns the number of elements stored
     * in the instance.
     *
     * @return int
     */
    
public function count()
    {
        return 
sizeof($this->hashes);
    }
}

/**
 * Implementation of the RGSS Table class.
 */
class Table
{
    protected 
$data  = array();
    public 
$sizeX 0;
    public 
$sizeY 0;
    public 
$sizeZ 0;
    
    public function 
__construct($x$y 1$z 1)
    {
        
$this->sizeX $x;
        
$this->sizeY $y;
        
$this->sizeZ $z;
        
$this->data  array_fill(0$x $y $z0);
    }
    
    public function 
get($x$y 0$z 0)
    {
        return 
$this->data[$x + ($y $this->sizeX) + ($z $this->sizeX $this->sizeY)];
    }
    
    public function 
set()
    {
        
$args func_get_args();
        
$x $args[0];
        
$y sizeof($args) > $args[1] : 0;
        
$z sizeof($args) > $args[2] : 0;
        
$v array_pop($args);
        
        
$this->data[$x + ($y $this->sizeX) + ($z $this->sizeX $this->sizeY)] = $v;
    }
    
    public static function 
rubyLoad($raw)
    {
        
// unpack table header (size, x elements, y elements, z elements)
        
$a unpack('Vsize/Vnx/Vny/Vnz'substr($raw016));
        
        
$d = array();
        
$p 20;
        
$l strlen($raw);
        while (
$p $l) {
            
$v   unpack('v'$raw[$p] . $raw[$p 1]);
            
$d[] = $v[1];
            
$p  += 2;
        }
        
        
$t = new Table($a['nx'], $a['ny'], $a['nz']);
        
$n 0;
        for (
$z 0$z $a['nz']; $z++) {
            for (
$y 0$y $a['ny']; $y++) {
                for (
$x 0$x $a['nx']; $x++) {
                    
$t->set($x$y$z$d[$n]);
                    
$n++;
                }
            }
        }
        
        return 
$t;
    }
}

/*
    Original RGSS Table._load
    
    def _load(s)
        size = s[0,4].unpack('L')[0]
        nx   = s[4,4].unpack('L')[0]
        ny   = s[8,4].unpack('L')[0]
        nz   = s[12,4].unpack('L')[0]
        data = []
        pointer = 20
        loop do
            data.push((s[pointer,2] + "\000\000").unpack('L')[0])
            pointer += 2
            break if pointer > s.size - 1
        end
        t = Table.new(nx, ny, nz)
        n = 0
        for z in 0...nz
            for y in 0...ny
                for x in 0...nx
                    t[x,y,z] = data[n]
                    n += 1
                end
            end
        end
        t
    end
*/