<?php
/*
Public methods: BEncode::encode($data) -> bencodes $data
BEncode::decode($data) -> bdecodes $data
*/
/*
Error codes:
0 => no error
1 => BEncode::encode() wrong parameter type [errpos is -1]
2 => Invalid bencoded string ('i' has no corresponding 'e') [errpos is the position of 'i']
3 => Invalid bencoded string (in 'i*e', * is supposed to be a number) [errpos is the position of 'i']
4 => Invalid bencoded string (there's garbage in it) [errpos is the position of first garbage byte]
5 => Invalid bencoded string (the specified length for the string should be a number) [errpos is the position that represents the first character of the string's length]
6 => Invalid bencoded string (the length of the string is longer than the actual string) [see (5)]
7 => Invalid bencoded string (the keys for dictionary are supposed to be strings) [errpos points to the first byte of the key]
8 => Invalid bencoded string (couldn't find end mark for a list/dictionary) [errpos is -1]
*/
class BEncode {
var $errno=0, $encoded, $decoded;
/**
*
* Bencodes a string/array/int. This function fails only if you supply the wrong parameter type.
*
* @param mixed Can be string, int or array
* @return mixed Bencoded string on success, boolean false on failure.
*/
function encode ($data)
{
// string
if (is_string($data)) $this->encoded = strlen($data).':'.$data;
// int
else if (is_int($data)) $this->encoded = 'i'.$data.'e';
// list or dictionary
else if (is_array($data)) $this->encoded = $this->_encode_arr($data);
else {
$this->errno = 1;
$this->errpos=-1;
return false;
} // none of those, return error
return true;
}
/**
*
* bencodes an array into a list or dictionary
*
* @param Array the array that has to be converted to list/dictionary
* @return String a bencoded list/dictionary
* @access private
*/
function _encode_arr ($arr) {
$is_a_list = true;
foreach ($arr as $key=>$value)
if (!is_int($key)) { // we found a non-integer key, meaning we have a dictionary, not a list
$is_a_list = false;
break;
}
if ($is_a_list)
{
// we got a list
$ret = 'l'; // lists start with a l
ksort($arr, SORT_NUMERIC); // we sort the array numerically, by keys
foreach ($arr as $key) // we take each element of the array, adding it to the list
$ret .= $this->encode($key);
$ret .= 'e'; // lists end with an e
return $ret; // we return the bencoded list
} else {
// we got a dictionary
// the order doesn't seem to matter for dictionary, therefore we do not sort
$ret = 'd'; // dictionary starts with a d
foreach ($arr as $key=>$value) {
// the key of a dictionary in bencode MUST be a string, so we cast $key to string
$ret .= $this->encode(string($key)) . $this->encode($value);
}
$ret .= 'e'; // dictionary ends with an e
return $ret; // we return the bencoded dictionary
}
}
/**
*
* Decode a bencoded string
*
* @param string The bencoded string
* @return mixed On success, a string/array/int is returned. On failure, boolean false is returned.
*/
function decode ($str)
{
$endpos=0;
$ret = $this->_decode($str, 0, $endpos);
if ($ret === false) return false; // invalid bencoded string
/*else if ($endpos != strlen($str)-1) {
// garbage at the end ?
$this->errno = 4;
$this->errpos = $endpos;
return false;
}*/
else return $ret;
}
/**
*
* Decode a bencoded string starting at a specified position
*
* @param string The bencoded string
* @param integer Position where it starts bdecoding at
* @param &integer After return, it is filled with the position where it stopped bdecoding
* @return mixed
* @access private
*/
function _decode ($str, $startpos, &$endpos)
{
$ret = '';
switch ($str[$startpos]) {
case 'i': // integer
$ending_position = strpos($str, 'e', $startpos+1); // first found 'e' after position $startpos
if ($ending_position === false) {
$this->errno = 2;
$this->errpos = $startpos;
return false; // we could not find 'e', invalid bencoded string
}
$ret = substr($str, $startpos+1, $ending_position-$startpos-1); // everything after 'i' and before'e'
if (!ctype_digit($ret)) {
$this->errno = 3;
$this->errpos = $startpos;
return false; // this is not an int, invalid bencoded string
}
$endpos = $ending_position;
return (int)$ret;
break;
case 'l': // list
$endposition=0;
$ret = $this->_decode_arr($str, $startpos+1, $endposition, false);
if ($ret === false) return false; // error occured
$endpos=$endposition; return $ret; // success
break;
case 'd': // dictionary
$endposition=0;
$ret = $this->_decode_arr($str, $startpos+1, $endposition, true );
if ($ret === false) return false; // error occured
$endpos=$endposition; return $ret; // success
break;
default: // string or error
$colon_position = strpos($str, ':', $startpos+1);
if ($colon_position === false) {
// we couldn't find a colon, so this is not a string => error
$this->errno = 4;
$this->errpos = $startpos;
return false;
}
$before_colon = substr($str, $startpos, $colon_position-$startpos);
$after_colon = substr($str, $colon_position + 1);
if (!ctype_digit($before_colon)) {
// the size of the string is not an integer as it should be
$this->errno = 5;
$this->errpos = $startpos;
return false;
}
$string_size = &$before_colon;
$string = substr($after_colon, 0, $string_size);
if (strlen($string) != $string_size) {
// not the whole string is represented in the bencoded string
$this->errno = 6;
$this->errpos = $startpos;
return false;
}
$endpos = $colon_position+$string_size;
return $string;
}
}
/**
*
* Transforms a list or dictionary into an array
*
* @param string The bencoded string we have to parse
* @param integer Position to start parsing at (the position is after the position of 'l' or 'd')
* @param &integer Position it stopped parsing at (the position is the position of the 'e' character)
* @param boolean true if what we have to parse is a dictionary, false otherwise
* @access private
* @return mixed array on success, boolean false on failure
*/
function _decode_arr ($str, $startpos, &$endpos, $isdictionary = false)
{
$ret = array(); // what we're going to return on success
$element = 0;
while (1)
{
if ($str[$startpos] == 'e') break;
if ($startpos > strlen($str) -1)
{
// startpos got out of the string without finding an 'e' character
$this->errno = 8;
$this->errpos = -1;
return false;
}
$endposition=0;
$listvalue = $this->_decode($str, $startpos, $endposition);
if ($listvalue === false) return false;
if ($isdictionary) {
// we got a dictionary
if ($element % 2 == 0) {
$lastkey = $listvalue; // we got a key for array
if (!is_string($lastkey)) {
// the keys of a dictionary MUST be strings
$this->errno = 7;
$this->errpos = $startpos;
}
}
// we got a value for array, we add it into the array with the previously found key
else $ret[$lastkey] = $listvalue;
}
else $ret[] = $listvalue; // we got a list, we just push into the array
$startpos = $endposition+1;
$element++;
}
$endpos = $startpos; // $startpos is the position where we found the 'e'
return $ret;
}
}
$benc = new BEncode;
$a = $benc->decode(file_get_contents('a.torrent'));
//$a = $benc->decode('li1111915968ee');
if ($a===false)echo "error ".$benc->errno." ".$benc->errpos;
else echo print_r($a);
?>