<?php  
/**
 * AMF0 Parser
 * 
 * Based (pretty far) on the AMFPHP serializer. I actually started using the AMFPHP serializer and
 * mostly rewrote it.
 *
 * @author        Tommy Lacroix <lacroix.tommy@gmail.com>
 * @version     1.2.20081220
 * @copyright   Copyright (c) 2006-2008 Tommy Lacroix
 * @license        LGPL
 * @package     flvinfo2
 * @link         http://www.tommylacroix.com/2008/07/04/a-php-tool-to-modify-an-flv-meta-and-cuepoints-on-the-fly/
 */

// ---

/**
 * AMF0 Parser class
 * 
 * @author        Tommy Lacroix <lacroix.tommy@gmail.com>
 * @todo         Finish Date parsing
 */
class AMF0Parser {
    
// {{{ Type constants
    
const TYPE_NUMBER 0x00;
    const 
TYPE_BOOLEAN 0x01;
    const 
TYPE_STRING 0x02;
    const 
TYPE_OBJECT 0x03;
    const 
TYPE_MOVIECLIP 0x04;
    const 
TYPE_NULL 0x05;
    const 
TYPE_UNDEFINED 0x06;
    const 
TYPE_REFERENCE 0x07;
    const 
TYPE_MIXEDARRAY 0x08;
    const 
TYPE_OBJECT_TERM 0x09;
    const 
TYPE_ARRAY 0x0a;
    const 
TYPE_DATE 0x0b;
    const 
TYPE_LONGSTRING 0x0c;
    const 
TYPE_RECORDSET 0x0e;
    const 
TYPE_XML 0x0f;
    const 
TYPE_TYPED_OBJECT 0x10;
    const 
TYPE_AMF3 0x11;
    
// }}} Type constants
    
    /**
     * Endianess, 0x00 for big, 0x01 for little
     *
     * @access private
     * @var int
     */
    
private $endian;
    
    
/**
     * AMF0 Data
     *
     * @access private
     * @var string
     */
    
private $data;
    
    
/**
     * Index in data
     *
     * @access private
     * @var int
     */
    
private $index;
    
    
/**
     * Data length
     *
     * @access private
     * @var int
     */
    
private $dataLength;
    
    
/**
     * Constructor
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     string        $str        Optional initialization string
     * @return     AMF0Parser
     */
    
public function AMF0Parser($str false) {    
        
/**
         * Proceed to endianess detection. This will be needed by
         * double decoding because unpack doesn't allow the selection
         * of endianess when decoding doubles.
         */
        
        // Pack 1 in machine order
        
$tmp pack("L"1);
        
        
// Unpack it in big endian order
        
$tmp2 unpack("None",$tmp);
        
        
// Check if it's still one
        
if ($tmp2['one'] == 1$this->endian 0x00// Yes, big endian
            
else $this->endian 0x01;    // No, little endian
            
        // Initialize if needed
        
if ($str !== false) {
            
$this->initialize($str);
        }
    } 
// Constructor
    
    /**
     * Initialize data
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     string        $str    AMF0 Data
     */
    
public function initialize($str) {
        
$this->data $str;
        
$this->dataLength strlen($str);
        
$this->index 0;
    } 
// initialize function
    
    
    /**
     * Read all packets
     * 
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     string        $str    AMF0 data (optional, uses the initialized one if not given)
     * @return     array
     */
    
public function readAllPackets($str false) {
        
// Initialize if needed
        
if ($str !== false$this->initialize($str);
        
        
// Parse each packet
        
$ret = array();
        while (
$this->index $this->dataLength)
            
$ret[] = $this->readPacket();
            
        
// Return it
        
return $ret;
    } 
// readAllPackets function
    
    /**
     * Read a packet at current index
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     mixed
     */
    
public function readPacket() {    
        
// Get data code
        
$dataType ord($this->data[$this->index++]);
        
// Interpret
        
switch($dataType) {    
            case 
self::TYPE_NUMBER:        // Number 0x00
                
return $this->readNumber();
            case 
self::TYPE_BOOLEAN:     // Boolean 0x01
                
return $this->readBoolean();
            case 
self::TYPE_STRING:        // String 0x02
                
return $this->readString();
                break;
            case 
self::TYPE_OBJECT:        // Object 0x03
                
return $this->readObject();
                break;
            case 
self::TYPE_MOVIECLIP:        // MovieClip
                
throw new Exception("Unhandled AMF type: MovieClip (04)");
                break;
            case 
self::TYPE_NULL:        // NULL 0x05
                
return NULL;
            case 
self::TYPE_UNDEFINED:        // Undefined 0x06
                
return 'undefined';
            case 
self::TYPE_REFERENCE:        // Reference
                
throw new Exception("Unhandled AMF type: Reference (07)");
                break;
            case 
self::TYPE_MIXEDARRAY :     // Mixed array 0x08
                
return $this->readMixedArray();
                break;
            case 
self::TYPE_OBJECT_TERM:     // ObjectTerm
                
throw new Exception("Unhandled AMF type: ObjectTerm (09) -- should only happen in the getObject function");
                break;
            case 
self::TYPE_ARRAY:    // Array 0x0a
                
return $this->readArray();
                break;
            case 
self::TYPE_DATE:     // Date
                
throw new Exception("Unhandled AMF type: Date (0B)");
                break;
            case 
self::TYPE_LONGSTRING:     // LongString
                
return $this->readLongString();
                break;
            case 
TYPE_RECORDSET:     // Recordset
                
throw new Exception("Unhandled AMF type: Unsupported (0E)");
                break;
            case 
self::TYPE_XML:     // XML
                
return $this->readLongString();
                break;
            case 
self::TYPE_TYPED_OBJECT:     // Typed Object
                
return $this->readTypedObject();
                break;
            case 
TYPE_AMF3:     // AMF3
                
throw new Exception("Unhandled AMF type: AMF3 (11)");
                break;
            default:
                throw new 
Exception("Unhandled AMF type: unknown (0x".dechex($dataType).") at offset ".$this->index-1);
        }
    } 
// readPacket function
    
    /**
     * Read a string at current index
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     string
     */
    
public function readString() {
        
// Check if we have enough data
        
if (strlen($this->data) < $this->index+2) {
            return 
null;
        }
        
        
// Get length
        
$len unpack('nlen'substr($this->data,$this->index,2));
        
$this->index+=2;
        
        
// Get string
        
$val substr($this->data$this->index$len['len']);
        
$this->index += $len['len'];
        
        
// Return it
        
return $val;
    } 
// readString function
    
    /**
     * Read a long string at current index
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     string
     */
    
public function readLongString() {
        
// Check if we have enough data
        
if (strlen($this->data) < $this->index+4) {
            return 
null;
        }

        
// Get length
        
$len unpack('Nlen'substr($this->data,$this->index,4));
        
$this->index+=4;
        
        
// Get string
        
$val substr($this->data$this->index$len['len']);
        
$this->index += $len['len'];
        
        
// Return it
        
return $val;
    } 
// readLongString function
    
    /**
     * Read a number (double) at current index
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     double
     */
    
public function readNumber() {    
        
// Check if we have enough data
        
if (strlen($this->data) < $this->index+8) {
            return 
null;
        }

        
// Get the packet, big endian (8 bytes long)
        
$packed substr($this->data$this->index8);
        
$this->index += 8;
        
        
// Reverse it if we're little endian
        
if ($this->endian == 0x01$packed strrev($packed);

        
// Unpack it
        
$tmp unpack("dnumber"$packed);
        
        
// Return it
        
return $tmp['number'];
    } 
// readNumber function
    
    /**
     * Read a boolean at current index
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     boolean
     */
    
public function readBoolean() {
        return 
ord($this->data[$this->index++]) == 1;
    } 
// readBoolean function
    
    /**
     * Read an object at current index
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     stdClass
     */
    
public function readObject() {    
        
// Create return object we will add data to
        
$ret = new stdClass();
        
        do {
            
// Get key
            
$key $this->readString();
            
            
// Stop if no key was read (end of AMF0 stream)
            
if ($key === null) break;
            
            
// Check if we reached ObjectTerm (09)
            
$dataType ord($this->data[$this->index]);
            
            
// If it's not an Object Term, read the packet
            
if ($dataType != self::TYPE_OBJECT_TERM) {
                
// Get data
                
$val $this->readPacket();
                
                
// Store it
                
$ret->$key $val;
            }
        } while (
$dataType != 0x09);

        
// Skip the Object Term
        
$this->index += 1
        
        
// Return object
        
return $ret;
    } 
// readObject function
    
    /**
     * Read a typed object at current index
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     stdClass
     */
    
public function readTypedObject() {
        
// Get class name
        
$className $this->readString();
        
        
// Get object
        
$object $this->readObject();
        
        
// Save class name inside object
        
$object->__className $className;
        
        
// Return object
        
return $object;
    } 
// readTypedObject function

    /**
     * Read a mixed array at current position
     * 
     * Note: A mixed array is basically an object, but with a long integer describing its highest index at first.
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     array
     */
    
public function readMixedArray() {    
        
// Skip the index
        
$this->index += 4;
        
        
// Parse the object, but return it as an array
        
return get_object_vars($this->readObject());
    } 
// readMixedArray function
    
    /**
     * Get an indexed array ([0,1,2,3,4,...])
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     array
     */
    
public function readArray() {    
        
// Check if we have enough data
        
if (strlen($this->data) < $this->index+4) {
            return 
null;
        }

        
// Get item count
        
$len unpack('Nlen',substr($this->data,$this->index,4));
        
$this->index+=4;
        
        
// Get each packet
        
$ret = array();
        for(
$i=0;$i<$len['len'];$i++) $ret[] = $this->readPacket();
        
        
// Return the array
        
return $ret;
    } 
// readArray function

    /**
     * Read a date at current index
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     string
     */
    
public function readDate() {    
        
// Check if we have enough data
        
if (strlen($this->data) < $this->index+10) {
            return 
null;
        }

        
// Get the packet, big endian (8 bytes long)
        
$packed substr($this->data$this->index8);
        
$this->index += 8;
        
        
// Reverse it if we're little endian
        
if ($this->endian == 0x01$packed strrev($packed);

        
// Unpack it
        
$tmp unpack("dnumber"$packed);
        
$epoch $tmp['number'];

        
// Get timezone
        
$tmp unpack('nnumber'substr($this->data$this->index2));
        
$this->index += 2;
        
$timezone $tmp['number'];
        if (
$timezone 32768 == 32768) {
            
$timezone $timezone-65536;
        }

        
// make epoch GMT, and then convert to local time
        
$time $epoch;
        
$time += $timezone*60;    // Timezone is in seconds
        
$time += date('Z',$time);
        
        
// Return it
        
return date('r',$time);
    } 
// readDate function
    
    
    /**
     * Write a packet
     * 
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     mixed    $value
     * @param     int        $type        autodetected if none given
     * @return     string
     */
    
public function writePacket($value$type=false) {    
        if (
$type === false) {
            if ((
$value === true) || ($value === false)) $type self::TYPE_BOOLEAN;
            if (
is_numeric($value)) {
                
$type self::TYPE_NUMBER;
            } else if (
is_array($value)) {
                
$type self::TYPE_ARRAY;
                foreach (
array_keys($value) as $k) {
                    if (
preg_match(',[^0-9],',$k)) {
                        
$type self::TYPE_MIXEDARRAY;
                        break;
                    }
                }
                
// Test for mixed/indexed
            
} else if (is_object($value)) {
                
$type self::TYPE_OBJECT;
            } else if (
is_string($value)) {
                if (
strlen($value) < 65535) {
                    
$type self::TYPE_STRING;
                } else {
                    
$type self::TYPE_LONGSTRING;
                }
            } else if (
is_null($value)) {
                
$type self::TYPE_NULL;
            }
        } 
// if($type===false)
        
        
switch ($type) {
            case 
self::TYPE_NUMBER:
                return 
$this->writeNumber($value);
            case 
self::TYPE_STRING:
                return 
$this->writeString($value);
            case 
self::TYPE_LONGSTRING:
                return 
$this->writeLongString($value);
            case 
self::TYPE_NULL:
                return 
$this->writeNull();
            case 
self::TYPE_BOOLEAN:
                return 
$this->writeBoolean($value);
            case 
self::TYPE_ARRAY:
                return 
$this->writeArray($value);
            case 
self::TYPE_MIXEDARRAY:
                return 
$this->writeMixedArray($value);
            case 
self::TYPE_OBJECT:
                return 
$this->writeObject($value);
            default:
                throw new 
Exception('Unhandled AMF0 type: 0x'.dechex($type));
        } 
//switch($type)
    
// writePacket function
    
    /**
     * Write a string
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     string $str
     * @return     string
     */
    
public function writeString($str) {
        
// Write type
        
$value chr(self::TYPE_STRING);
        
        
// Write length
        
$value .= pack('n'strlen($str));
        
        
// Write string
        
$value .= $str;
        
        
// Return it
        
return $value;
    }        
    
    
/**
     * Write a long string
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     string $str
     * @return     string
     */
    
public function writeLongString($str) {
        
// Write type
        
$value chr(self::TYPE_LONGSTRING);
        
        
// Write length
        
$value .= pack('N'strlen($str));
        
        
// Write string
        
$value .= $str;
        
        
// Return it
        
return $value;
    } 
// writeLongString function
    
    /**
     * Write a XML
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     string $str
     * @return     string
     */
    
public function writeXML($str) {
        
// Write type
        
$value chr(self::TYPE_XML);
        
        
// Write length
        
$value .= pack('N'strlen($str));
        
        
// Write string
        
$value .= $str;
        
        
// Return it
        
return $value;
    } 
// writeXML function
    
    /**
     * Write a number
     *
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     integer $number
     * @return     string
     */
    
public function writeNumber($number) {
        
// Write type
        
$value chr(self::TYPE_NUMBER);
        
        
// Build packed
        
$packed pack('d'$number);
        
        
// Reverse it if we're little endian
        
if ($this->endian == 0x01$packed strrev($packed);
        
        
// Write packed
        
$value .= $packed;
        
        
// Return it
        
return $value;
    } 
// writeNumber function
    
    
    /**
     * Write a null
     * 
     * @author     Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @return     string
     */
    
public function writeNull() {
        
// Write type
        
$value chr(self::TYPE_NULL);

        
// Return it
        
return $value;
    } 
// writeNull function
    
    /**
     * Write a boolean
     * 
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     bool    $boolean
     * @return     string
     */
    
public function writeBoolean($boolean) {
        
// Write type
        
$value chr(self::TYPE_BOOLEAN);
        
        
// Write value
        
$value .= ($boolean chr(1) : chr(0));
        
        
// Return it
        
return $value;
    } 
// writeBoolean function
    
    /**
     * Write a mixed array
     * 
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     array    $array
     * @return     string
     */
    
public function writeMixedArray($array) {
        
// Write type
        
$value chr(self::TYPE_MIXEDARRAY);
        
        
// Write index
        
$value .= pack('N',count($array));
        
        
// Write as object
        
$value .= $this->writeObjectSub($array);
        
        
// Return it
        
return $value;
    } 
// writeMixedArray

    /**
     * Write an object
     * 
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     stdClass|array    $object
     * @return     string
     */
    
public function writeObject($object) {
        
// Write type
        
$value chr(self::TYPE_OBJECT);
        
        
// Write as object
        
$value .= $this->writeObjectSub(get_object_vars($object));
        
        
// Return it
        
return $value;
    } 
// writeObject function
    
    /**
     * Write a typed object
     * 
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     stdClass|array    $object
     * @return     string
     */
    
public function writeTypedObject($object$className false) {
        
// Write type
        
$value chr(self::TYPE_TYPED_OBJECT);
        
        
// Write class
        
if ($className === false) {
            if (isset(
$object->__className)) $className $object->__className;
                else 
$className get_class($object);
        }
        
$value .= $this->writeString($className);
        
        
// Write as object
        
$value .= $this->writeObjectSub(get_object_vars($object));
        
        
// Return it
        
return $value;
    } 
// writeTypedObject function
    
    /**
     * Write an object, without the leading type
     * 
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     private
     * @internal
     * @param     stdClass|array    $object
     * @return     string
     */
    
private function writeObjectSub($object) {
        
$output '';
        
        
// Write each element
        
foreach ($object as $key=>$value) {
            
// Write key
            
$output .= pack('n',strlen($key)).$key;
            
            
// Write value
            
$output .= $this->writePacket($value);
        }
        
        
// Write object term
        
$output .= pack('n',0);
        
$output .= chr(0x09);
        
        
// Return it
        
return $output;
    } 
// writeObjectSub function
    
    /**
     * Write an index array
     * 
      * @author    Tommy Lacroix <lacroix.tommy@gmail.com>
     * @access     public
     * @param     array    $array
     * @return     string
     */
    
public function writeArray($array) {
        
// Write type
        
$value chr(self::TYPE_ARRAY);
        
        
// Write length
        
$value .= pack('N'count($array));
        
        
// Write elements
        
foreach ($array as $value) {
            
$value .= $this->writePacket($value);
        }
        
        
// Return it
        
return $value;
    } 
// writeArray function
    
// AMF0Parser class