<?php
/*
  Bencoding OOP wrapper for PHP | by Proger_XP | In public domain
    http://proger.i-forge.net/BEncoded/7Tn

  Based on lightenc.php functions from
    http://wiki.theory.org/Decoding_encoding_bencoded_data_with_PHP
*/

/* BEncoded classes by Proger_XP */
class BEncoded {
  public 
$nodes;
  public 
$tempChar;

  static function 
Decode($str) {
    
$res bdecode($str);
    if (
$res === null) {
      throw new 
EBEncode(null'Cannot decode bencoded data.''string: '.$str);
    } else {
      return 
$res;
    }
  }

  static function 
Encode($data$tempChar null) {
    
$res bencode($data$tempChar);
    if (
$res === null) {
      throw new 
EBEncode(null'Cannot encode bencoded data of type '.gettype($data).'.');
    }
    return 
$res;
  }

  static function 
TypeOf($value) {
    if (
is_scalar($value) or $value === null) {
      return ((
is_int($value) or is_float($value)) ? 'int' 'str');
    } else {
      return empty(
$value['isDct']) ? 'list' 'dict';
    }
  }

  static function 
HashOf($nodes$raw false) {
    return 
strtoupper(sha1(self::Encode($nodes), $raw));
  }

  
/* Instance methods */

  
function __construct($str null) {
    if (
$str !== null) {
      
$this->FromString($str);
    }
  }

  function 
FromString($str) {
    
$nodes self::Decode($str);

    if (!
is_array($nodes)) {
      throw new 
EBEncode($this'Cannot load bencoded string - it decodes to'.
                                
' a non-array ('.gettype($nodes).').');
    }

    
$this->nodes = &$nodes;
  }

    function 
FromFile($file) {
      
$str file_get_contents($file);
      if (!
is_string($str)) {
        throw new 
EBEncode($this'File to load bencoded file from doesn\'t exist.''file: '.$file);
      }
      
$this->FromString($str);
    }

  function 
ToString() {
    return 
self::Encode($this->nodes$this->tempChar);
  }

    function 
ToFile($file) {
      
$bytes file_put_contents($file$this->ToStringLOCK_EX);
      if (!
is_int($bytes)) {
        throw new 
EBEncode($this'Cannot save bencoded file.''dest file: '.$file);
      }
      return 
$bytes;
    }

  
// returns a shallow copy of root; to operate directly $this->nodes can be used.
  
function Root() { return $this->nodes; }

  
// returns null if node doesn't exist. $name = "/" or "" returns (sets/deletes) root.
  
function ValueOf($name) { return $this->Alter($name); }
  
// alias to ValueOf():
  
function Get($name) { return $this->ValueOf($name); }

  function 
Set($name$value) { return $this->Alter($name'set'$value); }
  function 
Copy($src$dest) { return $this->Set($dest$this->ValueOf($src)); }

  function 
Exchange($node_1$node_2) {
    
$temp $this->ValueOf($node_2);
    
$this->Set($node_2$this->ValueOf($node_1));
    
$this->Set($node_1$temp);
  }

  function 
Delete($name) { return $this->Alter($name'delete'); }

    
// $op: (g)et / (s)et, returns new value / (d)elete.
    
protected function Alter($name$op 'get'$arg null) {
      
$lastSlash strpbrk(mb_substr($name, -1), '\\/');

      
$name trimstrtr($name'\\''/'), '/' );
      
$path $name === '' ? array() : explode('/'$name);

      
$parent = &$this->nodes;
      while (
$path and is_array($parent)) {
        
$value = &$parent[array_shift($path)];

        if (
$op[0] === 'd') {
          if (!
$path and $lastSlash == is_array($value)) {
            
$value null;
          }
        } elseif (
$op[0] === 's') {
          if (
$value === null and $path) {
            
$value = array();
            if (( (string) 
$path[0] ) !== '0') {
              
$value['isDct'] = true;
            }
          }
        }

        
$parent = &$value;
      }

      if (
$op[0] === 's') {
        
$parent $arg;
      } elseif (
$op[0] === 'd' and !$name) {
        
$parent = array();
      }

      return 
$parent;
    }

  function 
Export($name '') { return $this->Dump$this->ValueOf($name) ); }

    function 
Dump($value$prefix '') {
      
$type self::TypeOf($value);

      if (
$type === 'int') {
        return 
is_float($value) ? sprintf('%1.1f'$value) : $value;
      } elseif (
$type === 'str') {
        return 
var_export($valuetrue);
      } else {
        
$res '';

          
$isDict $type === 'dict';
          foreach (
$value as $key => &$item) {
            if (!
bskip($key$item$this->tempChar)) {
              
$res .= $prefix;
              
$res .= $isDict "$key:" "#$key";
              
$res .= is_array($item) ? "\n" '  ';
              
$res .= $this->Dump($item"$prefix  ")."\n";
            }
          }

        return 
substr($res0, -1);
      }
    }

  
// type: int|str|list|dict; other type throws exception.
  
function NewNode($type$name) {
    switch (
$type strtolower($type)) {
    case 
'int':   return $this->Set($name0);
    case 
'str':   return $this->Set($name'');
    case 
'list':  return $this->Set($name, array());
    case 
'dict':  return $this->Set($name, array('isDct' => true));
    default:      throw new 
EBEncode($this'Cannot create bencoded node because type '.$type.' is unknown.');
    }
  }

  function 
SetEmpty($name) {
    
$value $this->ValueOf($name);

    if (
is_int($value) or is_float($value)) {
      
$value 0;
    } elseif (
is_string($value) or $value === null) {
      
$value '';
    } elseif (empty(
$value['isDct'])) {
      
$value = array();
    } else {
      
$value = array('isDct' => true);
    }

    return 
$this->Set($name$value);
  }

  function 
Cast($name$asType$onlyIfNum false) {
    
$value $this->ValueOf($name);
    if (
$value === null) {
      throw new 
EBEncode($this'Cannot cast node '.$name.' into '.$asType.' because node doesn\'t exist.');
    }

    
$asType strtolower($asType);
    if (!
in_array($asType, array('int''str''list''dict'))) {
      throw new 
EBEncode($this'Cannot cast node "'.$name.'" because new type ('.$asType.') is invalid.');
    }

    
$type self::TypeOf($value);
    if (
$type !== $asType) {
      if (
$type === 'int' or $type === 'str') {
        switch (
$asType) {
        
// str -> int:
        
case 'int':
          if (!
is_numeric($value)) {
            if (!
$onlyIfNum) {
              throw new 
EBEncode($this'Cannot cast string "'.$value.' to integer because it\'s not a number.');
            }
          } else {
            
$value = (float) $value;
          }

          break;

        
// int -> str:
        
case 'str':   $value = (string) $value; break;
        case 
'list':  $value = array(=> $value); break;
        case 
'dict':  $value = array('isDct' => true=> $value); break;
        }
      } elseif (
$asType === 'int' or $asType === 'str') {
        throw new 
EBException($this'Casting list/dict node "'.$name.'" into int/str isn\'t allowed.');
      } elseif (
$asType === 'dict') {   // list -> dict
        
$value['isDct'] = true;
      } else {                          
// dict -> list
        
unset($value['isDct']);
      }

      
$this->Set($name$value);
    }

    return 
$value;
  }

  function 
TempChar($new null) {
    
$new === null or $this->tempChar $new === '' null $new;
    return 
$this->tempChar;
  }

  function 
InfoHash($raw false) {
    
$info = &$this->nodes['info'];
    if (empty(
$info)) {
      throw new 
EBEncode($this'Cannot calculate info hash because there is no \'info\' dictionary.');
    } else {
      return 
self::HashOf($info$raw);
    }
  }
}

  class 
EBEncode extends Exception {
    public 
$obj;

    function 
__construct($bencObj$msg$details ''$previous null) {
      
$this->obj $bencObj;
      
parent::__construct(rtrim($msg'.').": $details"$previous);
    }
  }

/* lightenc.php */
function bdecode($s, &$pos=0) {
  if(
$pos>=strlen($s)) {
    return 
null;
  }
  switch(
$s[$pos]){
  case 
'd':
    
$pos++;
    
$retval=array();
    while (
$s[$pos]!='e'){
      
$key=bdecode($s$pos);
      
$val=bdecode($s$pos);
      if (
$key===null || $val===null)
        break;
      
$retval[$key]=$val;
    }
    
$retval["isDct"]=true;
    
$pos++;
    return 
$retval;

  case 
'l':
    
$pos++;
    
$retval=array();
    while (
$s[$pos]!='e'){
      
$val=bdecode($s$pos);
      if (
$val===null)
        break;
      
$retval[]=$val;
    }
    
$pos++;
    return 
$retval;

  case 
'i':
    
$pos++;
    
$digits=strpos($s'e'$pos)-$pos;
    
// Proger_XP: changed (int) -> (float) to avoid trimming of values exceeding
    //            signed int's max value (2147483647).
    
$val=(float)substr($s$pos$digits);
    
$pos+=$digits+1;
    return 
$val;

//  case "0": case "1": case "2": case "3": case "4":
//  case "5": case "6": case "7": case "8": case "9":
  
default:
    
$digits=strpos($s':'$pos)-$pos;
    if (
$digits<|| $digits >20)
      return 
null;
    
$len=(float)substr($s$pos$digits);
    
$pos+=$digits+1;
    
$str=substr($s$pos$len);
    
$pos+=$len;
    
//echo "pos: $pos str: [$str] len: $len digits: $digits\n";
    
return (string)$str;
  }
  return 
null;
}

// Proger_XP: added added skipping for null values and $tempChar prefix for list/dicts.
function bencode(&$d$tempChar null){
  if(
is_array($d)){
    
$ret="l";
    
$isDict=!empty($d["isDct"]);
    if(
$isDict){
      
$ret="d";
      
// this is required by the specs, and BitTornado actualy chokes on unsorted dictionaries
      
ksort($dSORT_STRING);
    }
    foreach(
$d as $key=>$value) {
      if(
$isDict){
        
// skip the isDct element, only if it's set by us
        
if (!bskip($key$value$tempChar)) {
          
$ret .= strlen($key).":$key";
        }
      } elseif (!
is_int($key) and !is_float($key) and trim($key'0..9') !== '') {
                
// Proger_XP: added exception raising for non-numeric list keys.
                
throw new EBEncode(null'Cannot bencode() a list - it contains a non-numeric key "'.$key.'".');
            }

      if (
is_string($value)) {
        
$ret.=strlen($value).":".$value;
      } elseif (
is_int($value) or is_float($value)){
        
$ret.="i${value}e";
      } else {
        
$ret.=bencode ($value);
      }
    }
    return 
$ret."e";
  } elseif (
is_string($d)) { // fallback if we're given a single bencoded string or int
    
return strlen($d).":".$d;
  } elseif (
is_int($d) or is_float($d)) {
    return 
"i${d}e";
  } else {
    return 
null;
  }
}

// bskip by Proger_XP.
function bskip($key, &$value$tempChar null) {
  return (
$key === 'isDct' and $value) or $value === null or strpos($key$tempChar) === 0;
}