[code-review] Bencode.pm

Caleb Epstein <[email protected]> Wed, 1 Oct 2003 12:34:28 -0400
Newsgroups gmane.comp.lang.perl.code-review-ladder
Message-ID <[email protected]>
	I'm soliciting comments on the following Perl module,
	Bencode.pm, which implements in Perl "bencode"
	serialization and deserialization routines.  This format is
	documented on http://bitconjurer.org/BitTorrent/protocol.html

	Sample files in bencoded format are available at BitTorrent
	tracker sites like http://bt.etree.org/ (the .torrent files
	are bencoded).  I also have a sample file up at
	http://bklyn.org/~cae/emptyfile.torrent

	I know the code can generate and parse proper "bencoded"
	streams, but suffers from one small limitation that the Python
	implementation does not.  When encoding a HASH -> bencoded
	form, there is no way to tell if the user intends a value
	which contains all-digits to be encoded as an integer or a
	string.  I've chosen the IMHO sane course of encoding them as
	ints.

	I'm primarily concerned about areas of the code that might be
	performance bottlenecks.  Since all communications in the
	BitTorrent protocol are done in bencoded form, I would like
	for these routines to be as fast as they can.

	Thanks for your time,

-- 
              |               |      Telephone, n.:
Caleb Epstein |  bklyn . org  |      	An invention of the devil which
    cae at    | Brooklyn Dust |      	abrogates some of the advantages of
bklyn dot org |   Bunny Mfg.  |      	making a disagreeable person keep
              |               |      	his distance.
              |               |      		-- Ambrose Bierce
Bencode.pm (application/x-perl, 3.6 KB)
#!/usr/local/bin/perl -w
#
# Bencode.pm - implements BitTorrent "bencode"-style encoding and
# decoding of data structures
#
# Author: Caleb Epstein <cae at bklyn dot org>
#
# License: MIT Public License (see URL:
# http://www.opensource.org/licenses/mit-license.php
#
# $Id: Bencode.pm,v 1.3 2003/03/06 20:14:03 cae Exp $

use strict;

package Bencode;
require 5.006;			# Need @+

use vars qw(@ISA @EXPORT);
require Exporter;
@ISA = qw (Exporter);
@EXPORT = qw(bencode bdecode);

# decode_int - parse an integer in bencoded format
sub decode_int {
   my ($x, $f) = @_;

   die "decode_int: bad value at position $f [" .
     substr ($x, $f, 1) . "]"
       unless substr ($x, $f) =~ /^(0|-?[1-9][0-9]*)e/;

   return int ($1), $f + $+[0];
}

# decode_string - parse a string in bencoded format
sub decode_string {
   my ($x, $f) = @_;
   die "decode_string: bad value at position $f [" .
     substr ($x, $f, 1) . "]"
       unless substr ($x, $f) =~ /^(0|[1-9]\d*):/;
   my $len = int ($1);
   return substr ($x, $f + $+[0], $len), $f + $len + $+[0];
}

# decode_list - parse a list in bencoded format, returns array ref
sub decode_list {
   my ($x, $f) = @_;
   my @R;
   while (substr ($x, $f, 1) ne "e") {
      my $v;
      ($v, $f) = bdecode_rec ($x, $f);
      push (@R, $v);
   }
   return (\@R, $f + 1);
}

# decode_dict - parse a dictionary in bencoded format, returns hash ref
sub decode_dict {
   my ($x, $f) = @_;
   my %R;
   my $lastkey;
   while (substr ($x, $f, 1) ne "e") {
      my $k;
      ($k, $f) = decode_string ($x, $f);
      die "decode_dict: bad dictionary sorting ($lastkey > $k)"
	if defined $lastkey and $lastkey gt $k;
      $lastkey = $k;
      my $v;
      ($v, $f) = bdecode_rec ($x, $f);
      $R{$k} = $v;
   }
   return (\%R, $f + 1);
}

# bdecode_rec - parse one item in a bencoded string
sub bdecode_rec {
   my ($x, $f) = @_;
   my $t = substr ($x, $f, 1);

   if ($t eq "i") {
      return decode_int ($x, $f + 1);
   } elsif ($t eq "l") {
      return decode_list ($x, $f + 1);
   } elsif ($t eq "d") {
      return decode_dict ($x, $f + 1);
   } else {
      return decode_string ($x, $f);
   }
}

# bdecode - parse a bencoded string and return the results.  Can be
# scalar, array, hash, or a complex nested structure of same
sub bdecode {
   my $x = shift;
   my ($r, $l);
   eval { ($r, $l) = bdecode_rec ($x, 0) };
   if ($@) {
      warn "bdecode: $@\n";
   } elsif ($l != length $x) {
      warn "bdecode: encoded data too long ($l != " .
	length ($x) . ")\n";
   }
   return $r;
}

# bencode_rec - handle the real work of bencoding a variable or
# reference.  There are some minor differences between the Perl
# implementation and Python here.  Notably, Perl doesn't distinguish
# between "123" and 123, so its not possible to know to encode a
# string of digits as a string.  It gets saved as an integer.
sub bencode_rec {
   my ($x, $b) = @_;

   if (not ref $x) {
      if ($x =~ /^(0|-?[1-9]\d*)$/) {
	 $b .= "i${x}e";
      } else {
	 $b .= length ($x) . ":" . $x;
      }
   } elsif (ref $x eq "ARRAY") {
      $b .= "l";
      foreach my $e (@{$x}) {
	 $b = bencode_rec ($e, $b);
      }
      $b .= "e";
   } elsif (ref $x eq "HASH") {
      $b .= "d";
      foreach my $key (sort keys %{$x}) {
	 $b = bencode_rec ($key, $b);
	 $b = bencode_rec ($x->{$key}, $b);
      }
      $b .= "e";
   } else {
      die "bencode_rec: invalid input $x (" . ref ($x) . ")";
   }
   $b;
}

# bencode - encode a scalar or complex structure in BitTorrent bencode
# format
sub bencode {
   my $x = shift;
   my $r = eval { bencode_rec ($x, "") };
   if ($@) {
      warn "bencode: $@\n";
   }
   $r;
}

1;