/contrib/php/bencode.inc.php
PHP | 360 lines | 315 code | 24 blank | 21 comment | 18 complexity | 1fc163ba8ac4cca987f8bdbaba911f6c MD5 | raw file
Possible License(s): GPL-3.0, GPL-2.0, BSD-3-Clause
- <?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;
- }