<?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->ToString, LOCK_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 = trim( strtr($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($value, true);
      } 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($res, 0, -1);
      }
    }

  // type: int|str|list|dict; other type throws exception.
  function NewNode($type, $name) {
    switch ($type = strtolower($type)) {
    case 'int':   return $this->Set($name, 0);
    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(0 => $value); break;
        case 'dict':  $value = array('isDct' => true, 0 => $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<0 || $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($d, SORT_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;
}
