Bencode.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. <?php
  2. /**
  3. * Contains a pair of recursive functions that implement the bencode data
  4. * encoding format.
  5. *
  6. * @author Michael J. I. Jackson <mjijackson@gmail.com>
  7. */
  8. class Bencode
  9. {
  10. /**
  11. * The version of the library.
  12. *
  13. * @var string
  14. */
  15. const VERSION = "1.0";
  16. /**
  17. * Bencodes the given data structure.
  18. *
  19. * @param mixed
  20. * @return string
  21. * @throws Exception
  22. */
  23. public static function encode($value)
  24. {
  25. if (is_null($value)) {
  26. return "0:";
  27. }
  28. if (is_int($value)) {
  29. return "i" . $value . "e";
  30. }
  31. if (is_string($value)) {
  32. return strlen($value) . ":" . $value;
  33. }
  34. if (is_array($value)) {
  35. if (self::isAssoc($value)) {
  36. ksort($value, SORT_STRING);
  37. $buffer = "d";
  38. foreach ($value as $key => $v) {
  39. $buffer .= self::encode(strval($key));
  40. $buffer .= self::encode($v);
  41. }
  42. $buffer .= "e";
  43. } else {
  44. ksort($value, SORT_NUMERIC);
  45. $buffer = "l";
  46. foreach ($value as $v) {
  47. $buffer .= self::encode($v);
  48. }
  49. $buffer .= "e";
  50. }
  51. return $buffer;
  52. }
  53. throw new Exception("Unable to encode data type: " . gettype($value));
  54. }
  55. /**
  56. * Decodes the given string. The second parameter is only used in recursion.
  57. *
  58. * @param string
  59. * @param int
  60. * @return mixed
  61. * @throws Exception
  62. */
  63. public static function decode($tokens, &$i=0)
  64. {
  65. if (is_string($tokens)) {
  66. $tokens = str_split($tokens);
  67. }
  68. switch ($tokens[$i]) {
  69. case "d":
  70. $dict = array();
  71. while (isset($tokens[++$i])) {
  72. if ($tokens[$i] == "e") {
  73. return $dict;
  74. } else {
  75. $key = self::decode($tokens, $i);
  76. if (isset($tokens[++$i])) {
  77. $dict[$key] = self::decode($tokens, $i);
  78. } else {
  79. throw new Exception("Dictionary key ($key) without a value at index $i");
  80. }
  81. }
  82. }
  83. throw new Exception("Unterminated dictionary at index $i");
  84. case "l":
  85. $list = array();
  86. while (isset($tokens[++$i])) {
  87. if ($tokens[$i] == "e") {
  88. return $list;
  89. } else {
  90. $list[] = self::decode($tokens, $i);
  91. }
  92. }
  93. throw new Exception("Unterminated list at index $i");
  94. case "i":
  95. $buffer = '';
  96. while (isset($tokens[++$i])) {
  97. if ($tokens[$i] == "e") {
  98. return intval($buffer);
  99. } elseif (ctype_digit($tokens[$i])) {
  100. $buffer .= $tokens[$i];
  101. } else {
  102. throw new Exception("Unexpected token while parsing integer at index $i: {$tokens[$i]}");
  103. }
  104. }
  105. throw new Exception("Unterminated integer at index $i");
  106. case ctype_digit($tokens[$i]):
  107. $length = $tokens[$i];
  108. while (isset($tokens[++$i])) {
  109. if ($tokens[$i] == ":") {
  110. break;
  111. } elseif (ctype_digit($tokens[$i])) {
  112. $length .= $tokens[$i];
  113. } else {
  114. throw new Exception("Unexpected token while parsing string length at index $i: {$tokens[$i]}");
  115. }
  116. }
  117. $end = $i + intval($length);
  118. $buffer = '';
  119. while (isset($tokens[++$i])) {
  120. if ($i <= $end) {
  121. $buffer .= $tokens[$i];
  122. if ($i == $end) {
  123. return $buffer;
  124. }
  125. }
  126. }
  127. throw new Exception("Unterminated string at index $i");
  128. }
  129. throw new Exception("Unexpected token at index $i: {$tokens[$i]}");
  130. }
  131. /**
  132. * Tells whether an array is associative or not. In order to be non-associative,
  133. * each of the array's key numbers must correspond exactly to it's position
  134. * in the array.
  135. *
  136. * @param array
  137. * @return bool
  138. */
  139. public static function isAssoc($array)
  140. {
  141. return count($array) !== array_reduce(array_keys($array), array("Bencode", "isAssocCallback"), 0);
  142. }
  143. /**
  144. * A callback function used by {@link isAssoc()}.
  145. *
  146. * @return int
  147. */
  148. protected static function isAssocCallback($a, $b)
  149. {
  150. return $a === $b ? $a + 1 : 0;
  151. }
  152. }
  153. /**
  154. * Shorthand for {@link Bencode::encode()}.
  155. *
  156. * @param mixed
  157. * @return string
  158. * @throws Exception
  159. * @see Bencode::encode()
  160. */
  161. function bencode($value)
  162. {
  163. return Bencode::encode($value);
  164. }
  165. /**
  166. * Shorthand for {@link Bencode::decode()}.
  167. *
  168. * @param string
  169. * @return mixed
  170. * @throws Exception
  171. * @see Bencode::decode()
  172. */
  173. function bdecode($value)
  174. {
  175. return Bencode::decode($value);
  176. }