[DRE-commits] r3365 - in tools/ruby-support: . examples examples/libtorrent-ruby-0.3 examples/libtorrent-ruby-0.3/debian examples/libtorrent-ruby-0.3/doc examples/libtorrent-ruby-0.3/rubytorrent

lucas at alioth.debian.org lucas at alioth.debian.org
Sat Apr 4 15:26:31 UTC 2009


Author: lucas
Date: 2009-04-04 15:26:31 +0000 (Sat, 04 Apr 2009)
New Revision: 3365

Added:
   tools/ruby-support/examples/
   tools/ruby-support/examples/libtorrent-ruby-0.3/
   tools/ruby-support/examples/libtorrent-ruby-0.3/COPYING
   tools/ruby-support/examples/libtorrent-ruby-0.3/README
   tools/ruby-support/examples/libtorrent-ruby-0.3/ReleaseNotes.txt
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/changelog
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/compat
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/control
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/copyright
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.docs
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.examples
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.install
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules
   tools/ruby-support/examples/libtorrent-ruby-0.3/debian/watch
   tools/ruby-support/examples/libtorrent-ruby-0.3/doc/
   tools/ruby-support/examples/libtorrent-ruby-0.3/doc/api.txt
   tools/ruby-support/examples/libtorrent-ruby-0.3/doc/design.txt
   tools/ruby-support/examples/libtorrent-ruby-0.3/dump-metainfo.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/dump-peers.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/make-metainfo.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer-ncurses.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/bencoding.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/controller.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/message.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/metainfo.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/package.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/peer.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/server.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/tracker.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/typedstruct.rb
   tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/util.rb
   tools/ruby-support/examples/libtorrent-ruby_0.3.orig.tar.gz
Log:
added an example lib ported to ruby-support

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/COPYING
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/COPYING	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/COPYING	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,340 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/README
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/README	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/README	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,21 @@
+Try it out
+----------
+
+RubyTorrent is primarily a library. See doc/api.txt for an example of how to
+use it in your Ruby applications. There are also a few executable scripts for
+you to play around with.
+
+rtpeer.rb: downloads a BitTorrent package.
+
+rtpeer-ncurses.rb: a nicer, ncurses version of the same. (The standard Ruby
+curses library appears not to play nicely with Threads, so we can't use it.)
+
+dump-metainfo.rb: takes a .torrent metainfo file and spits out everything about
+it.
+
+make-metainfo.rb: creates a .torrent file.
+
+dump-peers.rb: takes a .torrent metainfo file, connects to the tracker, and
+displays all the peers. (hack.)
+
+-- William <wmorgan-rubytorrent at masanjin.net>

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/ReleaseNotes.txt
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/ReleaseNotes.txt	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/ReleaseNotes.txt	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,25 @@
+Release notes for 0.3:
+
+Many more bug fixes. Speed is now basically comparable to Bram's client---at
+least in my limited experiments. 
+
+The following are known issues with this release:
+
+- Ruby threads don't play well with curses. Non-blocking getch hangs.
+  See [ruby-talk:130620]. So we use ncurses.
+
+- Ruby threads don't play well with TCP sockets on Windows. There is a
+  20-second *global* freeze every time an outgoing connection is made to a
+  non-responsive host. See [ruby-talk:129578], [ruby-core:04364]. As you can
+  imagine, this can be quite a performance hit in a program that can make
+  potentially hundreds of such connections. In fact, it renders RubyTorrent
+  almost useless on Windows. A patch exists (indeed, has existed for many
+  months), and if I bug Matz maybe it'll get in to 1.8.3. :)
+
+- Ruby threads don't play well with writing data over TCP sockets. At least,
+  that's what I glean from [ruby-talk:130480], and it might explain the
+  occasional freezing behavior I see (3 to 30 seconds, sporadic) under heavy
+  loads in Linux.
+
+Other than that :) everything works. I think.
+

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/changelog
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/changelog	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/changelog	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,12 @@
+libtorrent-ruby (0.3-2) unstable; urgency=low
+
+  * Fix architecture.
+  * Do not use the uploaders rule anymore.
+
+ -- Arnaud Cornet <arnaud.cornet at gmail.com>  Sun, 15 Apr 2007 11:24:25 +0200
+
+libtorrent-ruby (0.3-1) unstable; urgency=low
+
+  * Initial release.
+
+ -- Arnaud Cornet <arnaud.cornet at gmail.com>  Sat, 04 Nov 2006 17:53:07 +0100

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/compat
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/compat	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/compat	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1 @@
+5

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/control
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/control	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/control	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,14 @@
+Source: libtorrent-ruby
+Section: libs
+Priority: optional
+Maintainer: Arnaud Cornet <arnaud.cornet at gmail.com>
+Uploaders: Debian Ruby Extras Maintainers <pkg-ruby-extras-maintainers at lists.alioth.debian.org>
+Standards-Version: 3.7.2
+Build-Depends: ruby-pkg-tools (>= 0.8), cdbs, debhelper (>= 5.0.37.2), ruby, ruby-support
+
+Package: ruby-torrent
+Architecture: all
+Depends: ${misc:Depends}
+Description: BitTorrent library in Ruby
+ RubyTorrent is a pure-Ruby BitTorrent peer library and toolset. You can use
+ it to download or serve files over BitTorrent from any Ruby program.

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/copyright
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/copyright	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/copyright	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,437 @@
+This package was debianized by Arnaud Cornet <arnaud.cornet at gmail.com>
+on Sat, 04 Nov 2006 17:53:07 +0100
+
+The current Debian maintainer is Arnaud Cornet <arnaud.cornet at gmail.com>.
+
+It was downloaded from http://rubytorrent.rubyforge.org/
+
+Upstream Author: William Morgan <ruby at cs.stanford.edu>
+
+Copyright: 2003-2005 William Morgan
+
+ RubyTorrent is free software; you can redistribute it and/or modify it
+ under the terms of version 2 of the GNU General Public License as
+ published by the Free Software Foundation.
+
+ RubyTorrent is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License (in the file COPYING) for more details.
+
+On Debian systems, the complete text of the GNU General Public
+License, version 2, can be found in /usr/share/common-licenses/GPL-2.
+
+The file doc/api.txt is Copyright 2005 William Morgan
+
+ Permission is granted to copy, distribute and/or modify this document
+ under the terms of the GNU Free Documentation License, Version 1.2; with
+ no Invariant Sections, no Front-Cover Texts, and no Back-Covers.
+
+The GNU Free Documentation License, Version 1.2, is appended at the end of
+this file.
+
+The Debian packaging is Copyright 2006 Arnaud Cornet <arnaud.cornet at gmail.com> 
+and is licensed under the GPL, see above.
+
+----
+
+GNU Free Documentation License
+******************************
+
+		GNU Free Documentation License
+		  Version 1.2, November 2002
+
+
+ Copyright (C) 2000,2001,2002  Free Software Foundation, Inc.
+     51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+0. PREAMBLE
+
+The purpose of this License is to make a manual, textbook, or other
+functional and useful document "free" in the sense of freedom: to
+assure everyone the effective freedom to copy and redistribute it,
+with or without modifying it, either commercially or noncommercially.
+Secondarily, this License preserves for the author and publisher a way
+to get credit for their work, while not being considered responsible
+for modifications made by others.
+
+This License is a kind of "copyleft", which means that derivative
+works of the document must themselves be free in the same sense.  It
+complements the GNU General Public License, which is a copyleft
+license designed for free software.
+
+We have designed this License in order to use it for manuals for free
+software, because free software needs free documentation: a free
+program should come with manuals providing the same freedoms that the
+software does.  But this License is not limited to software manuals;
+it can be used for any textual work, regardless of subject matter or
+whether it is published as a printed book.  We recommend this License
+principally for works whose purpose is instruction or reference.
+
+
+1. APPLICABILITY AND DEFINITIONS
+
+This License applies to any manual or other work, in any medium, that
+contains a notice placed by the copyright holder saying it can be
+distributed under the terms of this License.  Such a notice grants a
+world-wide, royalty-free license, unlimited in duration, to use that
+work under the conditions stated herein.  The "Document", below,
+refers to any such manual or work.  Any member of the public is a
+licensee, and is addressed as "you".  You accept the license if you
+copy, modify or distribute the work in a way requiring permission
+under copyright law.
+
+A "Modified Version" of the Document means any work containing the
+Document or a portion of it, either copied verbatim, or with
+modifications and/or translated into another language.
+
+A "Secondary Section" is a named appendix or a front-matter section of
+the Document that deals exclusively with the relationship of the
+publishers or authors of the Document to the Document's overall subject
+(or to related matters) and contains nothing that could fall directly
+within that overall subject.  (Thus, if the Document is in part a
+textbook of mathematics, a Secondary Section may not explain any
+mathematics.)  The relationship could be a matter of historical
+connection with the subject or with related matters, or of legal,
+commercial, philosophical, ethical or political position regarding
+them.
+
+The "Invariant Sections" are certain Secondary Sections whose titles
+are designated, as being those of Invariant Sections, in the notice
+that says that the Document is released under this License.  If a
+section does not fit the above definition of Secondary then it is not
+allowed to be designated as Invariant.  The Document may contain zero
+Invariant Sections.  If the Document does not identify any Invariant
+Sections then there are none.
+
+The "Cover Texts" are certain short passages of text that are listed,
+as Front-Cover Texts or Back-Cover Texts, in the notice that says that
+the Document is released under this License.  A Front-Cover Text may
+be at most 5 words, and a Back-Cover Text may be at most 25 words.
+
+A "Transparent" copy of the Document means a machine-readable copy,
+represented in a format whose specification is available to the
+general public, that is suitable for revising the document
+straightforwardly with generic text editors or (for images composed of
+pixels) generic paint programs or (for drawings) some widely available
+drawing editor, and that is suitable for input to text formatters or
+for automatic translation to a variety of formats suitable for input
+to text formatters.  A copy made in an otherwise Transparent file
+format whose markup, or absence of markup, has been arranged to thwart
+or discourage subsequent modification by readers is not Transparent.
+An image format is not Transparent if used for any substantial amount
+of text.  A copy that is not "Transparent" is called "Opaque".
+
+Examples of suitable formats for Transparent copies include plain
+ASCII without markup, Texinfo input format, LaTeX input format, SGML
+or XML using a publicly available DTD, and standard-conforming simple
+HTML, PostScript or PDF designed for human modification.  Examples of
+transparent image formats include PNG, XCF and JPG.  Opaque formats
+include proprietary formats that can be read and edited only by
+proprietary word processors, SGML or XML for which the DTD and/or
+processing tools are not generally available, and the
+machine-generated HTML, PostScript or PDF produced by some word
+processors for output purposes only.
+
+The "Title Page" means, for a printed book, the title page itself,
+plus such following pages as are needed to hold, legibly, the material
+this License requires to appear in the title page.  For works in
+formats which do not have any title page as such, "Title Page" means
+the text near the most prominent appearance of the work's title,
+preceding the beginning of the body of the text.
+
+A section "Entitled XYZ" means a named subunit of the Document whose
+title either is precisely XYZ or contains XYZ in parentheses following
+text that translates XYZ in another language.  (Here XYZ stands for a
+specific section name mentioned below, such as "Acknowledgements",
+"Dedications", "Endorsements", or "History".)  To "Preserve the Title"
+of such a section when you modify the Document means that it remains a
+section "Entitled XYZ" according to this definition.
+
+The Document may include Warranty Disclaimers next to the notice which
+states that this License applies to the Document.  These Warranty
+Disclaimers are considered to be included by reference in this
+License, but only as regards disclaiming warranties: any other
+implication that these Warranty Disclaimers may have is void and has
+no effect on the meaning of this License.
+
+
+2. VERBATIM COPYING
+
+You may copy and distribute the Document in any medium, either
+commercially or noncommercially, provided that this License, the
+copyright notices, and the license notice saying this License applies
+to the Document are reproduced in all copies, and that you add no other
+conditions whatsoever to those of this License.  You may not use
+technical measures to obstruct or control the reading or further
+copying of the copies you make or distribute.  However, you may accept
+compensation in exchange for copies.  If you distribute a large enough
+number of copies you must also follow the conditions in section 3.
+
+You may also lend copies, under the same conditions stated above, and
+you may publicly display copies.
+
+
+3. COPYING IN QUANTITY
+
+If you publish printed copies (or copies in media that commonly have
+printed covers) of the Document, numbering more than 100, and the
+Document's license notice requires Cover Texts, you must enclose the
+copies in covers that carry, clearly and legibly, all these Cover
+Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
+the back cover.  Both covers must also clearly and legibly identify
+you as the publisher of these copies.  The front cover must present
+the full title with all words of the title equally prominent and
+visible.  You may add other material on the covers in addition.
+Copying with changes limited to the covers, as long as they preserve
+the title of the Document and satisfy these conditions, can be treated
+as verbatim copying in other respects.
+
+If the required texts for either cover are too voluminous to fit
+legibly, you should put the first ones listed (as many as fit
+reasonably) on the actual cover, and continue the rest onto adjacent
+pages.
+
+If you publish or distribute Opaque copies of the Document numbering
+more than 100, you must either include a machine-readable Transparent
+copy along with each Opaque copy, or state in or with each Opaque copy
+a computer-network location from which the general network-using
+public has access to download using public-standard network protocols
+a complete Transparent copy of the Document, free of added material.
+If you use the latter option, you must take reasonably prudent steps,
+when you begin distribution of Opaque copies in quantity, to ensure
+that this Transparent copy will remain thus accessible at the stated
+location until at least one year after the last time you distribute an
+Opaque copy (directly or through your agents or retailers) of that
+edition to the public.
+
+It is requested, but not required, that you contact the authors of the
+Document well before redistributing any large number of copies, to give
+them a chance to provide you with an updated version of the Document.
+
+
+4. MODIFICATIONS
+
+You may copy and distribute a Modified Version of the Document under
+the conditions of sections 2 and 3 above, provided that you release
+the Modified Version under precisely this License, with the Modified
+Version filling the role of the Document, thus licensing distribution
+and modification of the Modified Version to whoever possesses a copy
+of it.  In addition, you must do these things in the Modified Version:
+
+A. Use in the Title Page (and on the covers, if any) a title distinct
+   from that of the Document, and from those of previous versions
+   (which should, if there were any, be listed in the History section
+   of the Document).  You may use the same title as a previous version
+   if the original publisher of that version gives permission.
+B. List on the Title Page, as authors, one or more persons or entities
+   responsible for authorship of the modifications in the Modified
+   Version, together with at least five of the principal authors of the
+   Document (all of its principal authors, if it has fewer than five),
+   unless they release you from this requirement.
+C. State on the Title page the name of the publisher of the
+   Modified Version, as the publisher.
+D. Preserve all the copyright notices of the Document.
+E. Add an appropriate copyright notice for your modifications
+   adjacent to the other copyright notices.
+F. Include, immediately after the copyright notices, a license notice
+   giving the public permission to use the Modified Version under the
+   terms of this License, in the form shown in the Addendum below.
+G. Preserve in that license notice the full lists of Invariant Sections
+   and required Cover Texts given in the Document's license notice.
+H. Include an unaltered copy of this License.
+I. Preserve the section Entitled "History", Preserve its Title, and add
+   to it an item stating at least the title, year, new authors, and
+   publisher of the Modified Version as given on the Title Page.  If
+   there is no section Entitled "History" in the Document, create one
+   stating the title, year, authors, and publisher of the Document as
+   given on its Title Page, then add an item describing the Modified
+   Version as stated in the previous sentence.
+J. Preserve the network location, if any, given in the Document for
+   public access to a Transparent copy of the Document, and likewise
+   the network locations given in the Document for previous versions
+   it was based on.  These may be placed in the "History" section.
+   You may omit a network location for a work that was published at
+   least four years before the Document itself, or if the original
+   publisher of the version it refers to gives permission.
+K. For any section Entitled "Acknowledgements" or "Dedications",
+   Preserve the Title of the section, and preserve in the section all
+   the substance and tone of each of the contributor acknowledgements
+   and/or dedications given therein.
+L. Preserve all the Invariant Sections of the Document,
+   unaltered in their text and in their titles.  Section numbers
+   or the equivalent are not considered part of the section titles.
+M. Delete any section Entitled "Endorsements".  Such a section
+   may not be included in the Modified Version.
+N. Do not retitle any existing section to be Entitled "Endorsements"
+   or to conflict in title with any Invariant Section.
+O. Preserve any Warranty Disclaimers.
+
+If the Modified Version includes new front-matter sections or
+appendices that qualify as Secondary Sections and contain no material
+copied from the Document, you may at your option designate some or all
+of these sections as invariant.  To do this, add their titles to the
+list of Invariant Sections in the Modified Version's license notice.
+These titles must be distinct from any other section titles.
+
+You may add a section Entitled "Endorsements", provided it contains
+nothing but endorsements of your Modified Version by various
+parties--for example, statements of peer review or that the text has
+been approved by an organization as the authoritative definition of a
+standard.
+
+You may add a passage of up to five words as a Front-Cover Text, and a
+passage of up to 25 words as a Back-Cover Text, to the end of the list
+of Cover Texts in the Modified Version.  Only one passage of
+Front-Cover Text and one of Back-Cover Text may be added by (or
+through arrangements made by) any one entity.  If the Document already
+includes a cover text for the same cover, previously added by you or
+by arrangement made by the same entity you are acting on behalf of,
+you may not add another; but you may replace the old one, on explicit
+permission from the previous publisher that added the old one.
+
+The author(s) and publisher(s) of the Document do not by this License
+give permission to use their names for publicity for or to assert or
+imply endorsement of any Modified Version.
+
+
+5. COMBINING DOCUMENTS
+
+You may combine the Document with other documents released under this
+License, under the terms defined in section 4 above for modified
+versions, provided that you include in the combination all of the
+Invariant Sections of all of the original documents, unmodified, and
+list them all as Invariant Sections of your combined work in its
+license notice, and that you preserve all their Warranty Disclaimers.
+
+The combined work need only contain one copy of this License, and
+multiple identical Invariant Sections may be replaced with a single
+copy.  If there are multiple Invariant Sections with the same name but
+different contents, make the title of each such section unique by
+adding at the end of it, in parentheses, the name of the original
+author or publisher of that section if known, or else a unique number.
+Make the same adjustment to the section titles in the list of
+Invariant Sections in the license notice of the combined work.
+
+In the combination, you must combine any sections Entitled "History"
+in the various original documents, forming one section Entitled
+"History"; likewise combine any sections Entitled "Acknowledgements",
+and any sections Entitled "Dedications".  You must delete all sections
+Entitled "Endorsements".
+
+
+6. COLLECTIONS OF DOCUMENTS
+
+You may make a collection consisting of the Document and other documents
+released under this License, and replace the individual copies of this
+License in the various documents with a single copy that is included in
+the collection, provided that you follow the rules of this License for
+verbatim copying of each of the documents in all other respects.
+
+You may extract a single document from such a collection, and distribute
+it individually under this License, provided you insert a copy of this
+License into the extracted document, and follow this License in all
+other respects regarding verbatim copying of that document.
+
+
+7. AGGREGATION WITH INDEPENDENT WORKS
+
+A compilation of the Document or its derivatives with other separate
+and independent documents or works, in or on a volume of a storage or
+distribution medium, is called an "aggregate" if the copyright
+resulting from the compilation is not used to limit the legal rights
+of the compilation's users beyond what the individual works permit.
+When the Document is included in an aggregate, this License does not
+apply to the other works in the aggregate which are not themselves
+derivative works of the Document.
+
+If the Cover Text requirement of section 3 is applicable to these
+copies of the Document, then if the Document is less than one half of
+the entire aggregate, the Document's Cover Texts may be placed on
+covers that bracket the Document within the aggregate, or the
+electronic equivalent of covers if the Document is in electronic form.
+Otherwise they must appear on printed covers that bracket the whole
+aggregate.
+
+
+8. TRANSLATION
+
+Translation is considered a kind of modification, so you may
+distribute translations of the Document under the terms of section 4.
+Replacing Invariant Sections with translations requires special
+permission from their copyright holders, but you may include
+translations of some or all Invariant Sections in addition to the
+original versions of these Invariant Sections.  You may include a
+translation of this License, and all the license notices in the
+Document, and any Warranty Disclaimers, provided that you also include
+the original English version of this License and the original versions
+of those notices and disclaimers.  In case of a disagreement between
+the translation and the original version of this License or a notice
+or disclaimer, the original version will prevail.
+
+If a section in the Document is Entitled "Acknowledgements",
+"Dedications", or "History", the requirement (section 4) to Preserve
+its Title (section 1) will typically require changing the actual
+title.
+
+
+9. TERMINATION
+
+You may not copy, modify, sublicense, or distribute the Document except
+as expressly provided for under this License.  Any other attempt to
+copy, modify, sublicense or distribute the Document is void, and will
+automatically terminate your rights under this License.  However,
+parties who have received copies, or rights, from you under this
+License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+
+10. FUTURE REVISIONS OF THIS LICENSE
+
+The Free Software Foundation may publish new, revised versions
+of the GNU Free Documentation License from time to time.  Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.  See
+http://www.gnu.org/copyleft/.
+
+Each version of the License is given a distinguishing version number.
+If the Document specifies that a particular numbered version of this
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that specified version or
+of any later version that has been published (not as a draft) by the
+Free Software Foundation.  If the Document does not specify a version
+number of this License, you may choose any version ever published (not
+as a draft) by the Free Software Foundation.
+
+
+ADDENDUM: How to use this License for your documents
+
+To use this License in a document you have written, include a copy of
+the License in the document and put the following copyright and
+license notices just after the title page:
+
+    Copyright (c)  YEAR  YOUR NAME.
+    Permission is granted to copy, distribute and/or modify this document
+    under the terms of the GNU Free Documentation License, Version 1.2
+    or any later version published by the Free Software Foundation;
+    with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
+    A copy of the license is included in the section entitled "GNU
+    Free Documentation License".
+
+If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
+replace the "with...Texts." line with this:
+
+    with the Invariant Sections being LIST THEIR TITLES, with the
+    Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
+
+If you have Invariant Sections without Cover Texts, or some other
+combination of the three, merge those two alternatives to suit the
+situation.
+
+If your document contains nontrivial examples of program code, we
+recommend releasing these examples in parallel under your choice of
+free software license, such as the GNU General Public License,
+to permit their use in free software.

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.docs
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.docs	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.docs	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,2 @@
+doc/*
+ReleaseNotes.txt

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.examples
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.examples	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.examples	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,5 @@
+dump-metainfo.rb
+dump-peers.rb
+make-metainfo.rb
+rtpeer-ncurses.rb
+rtpeer.rb

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.install
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.install	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.install	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1 @@
+rubytorrent* /usr/lib/ruby/vendor_ruby/1.8

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,6 @@
+#!/usr/bin/make -f
+
+include /usr/share/cdbs/1/rules/debhelper.mk
+
+$(patsubst %,binary-post-install/%,$(DEB_ALL_PACKAGES))::
+		dh_ruby -p$(cdbs_curpkg)


Property changes on: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules
___________________________________________________________________
Name: svn:executable
   + *

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/watch
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/watch	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/watch	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,2 @@
+version=3
+http://rubyforge.org/frs/?group_id=498 .*rubytorrent-(.*)\.t.*

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/doc/api.txt
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/doc/api.txt	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/doc/api.txt	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,289 @@
+RubyTorrent Documentation
+
+Introduction
+------------
+
+RubyTorrent is a pure-Ruby BitTorrent library. You can use RubyTorrent
+in your Ruby applications to download and serve files via the
+BitTorrent protocol.  More information about BitTorrent can be found
+at http://bittorrent.com/.
+
+There's a lot going behind the scenes, but using this library is
+pretty simple: on the surface, RubyTorrent simply lets you download a
+file or set of files, given an initial .torrent filename or URL.
+
+I recommend you take a look at rtpeer.rb for an example Ruby BitTorrent
+peer that uses all this stuff.
+
+Synopsis
+--------
+
+require "rubytorrent"
+
+# simple
+bt = RubyTorrent::BitTorrent.new(filename)
+bt.on_event(self, :complete) { puts "done!" }
+
+# more complex
+mi = RubyTorrent::MetaInfo.from_location(url)
+package = RubyTorrent::Package.new(mi, dest)
+bt = RubyTorrent::BitTorrent.new(mi, package)
+thread = Thread.new do
+  until bt.complete?
+    puts "#{bt.percent_completed}% done"
+    sleep 5
+  end
+end
+bt.on_event(self, :complete) { puts "done!" }
+thread.join
+
+Overview
+--------
+
+There are three top-level classes you should be familiar with in the
+RubyTorrent module: BitTorrent, MetaInfo and Package. BitTorrent is
+the main interface; MetaInfo and Package classes allow you more
+control over the details, but they're completely options and the
+BitTorrent class will do reasonable things if you don't use them.
+
+RubyTorrent has a very event-driven interface; all methods are
+non-blocking and the BitTorrent class generates notifications of all
+interesting events, which you can subscribe to. See the documentation
+on BitTorrent#on_event() below for how to subscribe to events.
+
+RubyTorrent::MetaInfo
+---------------------
+
+This class represents the contents of the .torrent file or URL.
+
+CLASS METHODS
+
+from_location(location, http_proxy=ENV["http_proxy"])
+  Creates a MetaInfo object from a filename or URL.
+
+  Arguments:
+   location: a filename or a URL of a .torrent file.
+   http_proxy: is the HTTP proxy to be used in the case that
+    "location" is a URL (nil for none).
+
+  Returns:
+    A MetaInfo object.
+
+  Throws:
+    RubyTorrent::MetaInfoFormatError,
+    RubyTorrent::BEncodingError,
+    RubyTorrent::TypedStructError
+      if the contents of the file/url are not a BitTorrent metainfo file.
+    IOError, SystemCallError
+      if reading the contents of the file/url failed for system-level
+      issues.
+
+from_stream(stream)
+  Creates a MetaInfo object from a readable IO stream.
+
+  Arguments:
+    stream: a readable IO stream, e.g. an opened File.
+
+  Returns:
+    A MetaInfo object.
+
+  Throws:
+    same as RubyTorrent::Metainfo#from_location
+
+INSTANCE METHODS
+
+single?
+  Returns true if this .torrent contains a single file, false otherwise.
+
+multiple?
+  The opposite of single?
+
+RubyTorrent::Package
+--------------------
+
+This class represents the target file or files on disk.
+
+CLASS METHODS
+
+new(info, out=nil, validity_assumption=nil, path_sep="/") # optional block
+  Creates a Package.
+
+  Arguments:
+   info: a MetaInfo object.
+   out: if info.single?, this should be a File object corresponding to
+     the target file on disk. If info.multiple?, this should be a Dir
+     object corresponding to the target directory on disk. If nil, the
+     original filename (for single-file .torrents) or the current
+     directory (for multi-file .torrents) will be used.
+   validity_assumption: if nil, make no assumptions about the validity of
+     any files on disk. If true, assume all files on disk are complete
+     and valid. If false, assume all files on disk are incomplete and
+     invalid.  This can be used to speed up start time by skipping all
+     examination of current disk contents: if you're just starting a
+     download, you can use false; if you're serving a complete file or
+     set of files, you can use true.
+   path_sep: how to join path-name components to make paths. "/"
+     should work on both Windows and Unix worlds; I'm not sure about
+     other OSs.
+
+  Block:
+    If given, yields a "Piece" object when checking the files on
+    disk. This object has complete?() and valid?() methods. This is
+    really only useful for updating the user on the status of the
+    Package creation, which can take a long time for large files (I/O
+    time and SHA1 calculations).
+
+  Throws:
+    IOError, if the file access fails.
+
+RubyTorrent::BitTorrent
+-----------------------
+
+The main BitTorrent peer protocol interface.
+
+CLASS METHODS
+
+new(metainfo, package=nil, :host, :port, :dlratelim, :ulratelim, :http_proxy)
+  Creates a BitTorrent peer.
+
+  Arguments (all symbol arguments are optional hash pseudo-keyword arguments):
+    metainfo: a String, IO or MetaInfo object corresponding to a .torrent file.
+      In the case of a String or IO object, a MetaInfo object will be implictly
+      created with default arguments.
+    package: a Package, or nil. In the case on nil, a new Package will be
+      implicitly created with default arguments.
+    :host: the host to report to the tracker, if the source IP address of the
+      HTTP request is not correct (for weird IP masquerading issues, I suppose).
+    :port: the port to report to the tracker, if the port the BitTorrent peer is
+      listening on is not correct (likewise).
+    :dlratelim: the download rate limit in bytes/sec. This limit right now is
+      applied on a per-peer basis to the average download rate. In the future
+      this might change to something stricter/more useful.
+    :ulratelim: likewise, for the upload rate limit.
+    :http_proxy: the http_proxy used for connecting to the tracker, or nil
+      or unspecified for ENV["http_proxy"].
+
+  Throws:
+    All of the exceptions thrown by MetaInfo.new and Package.new.
+
+INSTANCE METHODS
+
+running?
+  Returns whether this client is running or not.
+
+ip
+  Returns the IP address the client is bound to, as a String (possibly "0.0.0.0")
+
+port
+  Returns the port the client is bound to.
+
+complete?
+  Returns whether the file on disk is complete or not.
+
+bytes_completed
+  Returns the number of bytes completed.
+
+total_bytes
+  Returns the total number of bytes in the target file/fileset.
+  
+percent_completed
+  Returns the percent of bytes completed.
+
+pieces_completed
+  Returns the number of BitTorrent "pieces" completed.
+
+num_pieces
+  Returns the total number of BitTorrent pieces.
+
+tracker
+  Returns the URL of the tracker being used, or nil if no tracker can be reached.
+
+num_possible_peers
+  Returns the number of peers we've read from the tracker, or nil if no tracker
+  can be reached. This is typically capped at 50.
+
+peer_info
+  Returns an array of hashes, one per current peer, with the following symbols
+  as keys:
+    :name: the peer name (probably "ip address/port")
+    :seed: true if the peer is a seed, false if it's a leecher
+    :dlamt, :ulamt: the number of bytes downloaded from /uploaded to this peer
+    :dlrate, :ulrate: the bytes/sec downloaded from/uploaded to this peer
+    :pending_send, :pending_recv: the number of blocks pending for send/receive
+    :interested, :peer_interested: who's interested in the other's pieces
+    :choking, :peer_choking: who's choking whom
+    :snubbing: whether we're snubbing this peer
+    :we_desire, :they_desire: number of pieces one has that the other wants
+  A lot of this stuff has to do with the internals of the BitTorrent wire
+  protocol, so it's mainly useful for debugging.
+
+shutdown
+  Shuts down this particular client.
+
+shutdown_all
+  Shuts down all clients.
+
+on_event(who, *events) # mandatory block
+  Registers a notification for one or more events. When one of the
+  events occurs, the block will be called. The first argument to the
+  block will be the source of the event (in this case a
+  RubyTorrent::BitTorrent object); the other arguments are dependent
+  on the block itself.
+
+  Arguments:
+    who: should be "self"
+    events: one or more event symbols (see EVENTS below)
+
+unregister_events(who, *events)
+  Unregisters event notifications. All blocks added with on_event(who, ...)
+  will be removed if they have an event in "events". If "events" is nil,
+  all blocks registered with on_event(who, ...) will be removed.
+
+  Arguments:
+    who: the same argument as was passed to on_event()
+    events: one or more event symbols.
+
+EVENTS
+
+:trying_peer |source, peer|
+  We're trying to connect to the peer "peer" (a String: "ip addr/port").
+
+:forgetting_peer |source, peer|
+  We're couldn't connect to the peer.
+ 
+:added_peer |source, peer|
+  We successfully connected to the peer.
+
+:removed_peer |source, peer|
+  We dropped our connection to the peer.
+
+:received_block |source, block, peer|
+  We received a block "block" from peer.
+
+:sent_block |source, block, peer|
+  We sent a block "block" to peer.
+
+:have_piece |source, piece|
+  We've successfully downloaded a complete piece "piece".
+
+:discarded_piece |source, piece|
+  We had to discard piece "piece" because of checksum errors
+
+:complete |source|
+  We've downloaded the entire file! Hooray!
+
+:tracker_connected |source, url|
+  We connected to tracker "url".
+
+:tracker_lost
+  We couldn't connect to tracker "url" after previously having connected to it.
+
+COPYRIGHT
+---------
+
+Copyright 2005 William Morgan
+Permission is granted to copy, distribute and/or modify this document under the
+terms of the GNU Free Documentation License, Version 1.2; with no Invariant
+Sections, no Front-Cover Texts, and no Back-Covers. A copy of the license may
+be found at http://www.gnu.org/licenses/fdl.html.
+

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/doc/design.txt
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/doc/design.txt	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/doc/design.txt	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,59 @@
+RubyTorrent Design
+------------------
+
+This is pretty sketchy at the moment but it might help if you want to
+do some hacking.
+
+                   
+               +---------+      +----------------+
+disk <::::::::>| Package |    /-| PeerConnection |<=== network ===> peer
+               +---------+    | +----------------+
+                    |         |
+ +---------+  +------------+ /  +----------------+
+ | Tracker |--| Controller |----| PeerConnection |<=== network ===> peer
+ +---------+  +------------+ \  +----------------+
+             /                |         .
+ +--------+ /                 |         .
+ | Server |-                  |         .
+ +--------+ \  
+             \ +---------+
+             | | Package |<:::::::> disk
+             | +---------+
+             |      |
+ +---------+ \+------------+    +----------------+
+ | Tracker |--| Controller |----| PeerConnection |<=== network ===> peer
+ +---------+  +------------+ \  +----------------+
+                             |
+                             |  +----------------+
+                             |--| PeerConnection |<=== network ===> peer
+                             |  +----------------+
+                                        .
+                                        .
+                                        .
+
+                              
+Each .torrent download is associated with a Package. A Package is
+composed of several Pieces, each corresponding to a BitTorrent
+piece. A Package provides simple aggregate operations over all the
+Pieces. Each Piece handles writing to and reading from disk (across
+potentially multiple file pointers), as well as dividing its data into
+one or more Blocks. Each Block is an in-memory section of a Piece and
+corresponds to the BitTorrent piece, transferrable across the network.
+
+One Server coordinates all BitTorrent downloads. It maintains several
+Controllers, one per .torrent download. The server handles all
+handshaking. It accepts incoming connections, shunting them to the
+appropriate Controller, and creates outgoing ones at the Controllers'
+behest. Each connection to a peer is maintained by a PeerConnection,
+which keeps track of the peer's state and the connection state.
+PeerConnections get empty Blocks from their Controller and send
+requests for them across the wire, and, upon receiving requests from
+the peer, get full Blocks from the Package and transmit them back.
+
+The Controller also keeps a Tracker object, which it uses to
+communicate with the tracker.
+
+PeerConnections are completely reactive, and are tightly integrated
+with their Controller. They rely on the Controller's heartbeat thread
+to trigger any time-dependent events, and also for propagating any
+messages to other peers.

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/dump-metainfo.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/dump-metainfo.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/dump-metainfo.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,55 @@
+## dump-metainfo.rb -- command-line .torrent dumper
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'rubytorrent'
+
+def dump_metainfoinfo(mii)
+  if mii.single?
+      <<EOS
+       length: #{mii.length / 1024}kb
+     filename: #{mii.name}
+EOS
+  else
+    mii.files.map do |f|
+        <<EOS
+   - filename: #{File.join(mii.name, f.path)}
+       length: #{f.length}
+EOS
+    end.join + "\n"
+  end + <<EOS
+ piece length: #{mii.piece_length / 1024}kb 
+       pieces: #{mii.pieces.length / 20}
+EOS
+end
+
+def dump_metainfo(mi)
+    <<EOS
+#{dump_metainfoinfo(mi.info).chomp}
+     announce: #{mi.announce}
+announce-list: #{(mi.announce_list.nil? ? "<not specified>" : mi.announce_list.map { |x| x.join(', ') }.join('; '))}
+creation date: #{mi.creation_date || "<not specified>"}
+   created by: #{mi.created_by || "<not specified>"}
+      comment: #{mi.comment || "<not specified>"}
+EOS
+end
+
+if ARGV.length == 1
+  fn = ARGV[0]
+  begin
+    puts dump_metainfo(RubyTorrent::MetaInfo.from_location(fn))
+  rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
+    puts "Can't parse #{fn}: maybe not a .torrent file?"
+  end
+else
+  puts "Usage: dump-metainfo <filename>"
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/dump-peers.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/dump-peers.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/dump-peers.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,45 @@
+## dump-peers.rb -- command-line peer lister
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent"
+
+def die(x); $stderr << "#{x}\n" && exit(-1); end
+def dump_peer(p)
+  "#{(p.peer_id.nil? ? '<not specified>' : p.peer_id.inspect)} on #{p.ip}:#{p.port}"
+end
+  
+fn = ARGV.shift or raise "first argument must be .torrent file"
+
+mi = nil
+begin
+  mi = RubyTorrent::MetaInfo.from_location(fn)
+rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
+  die "error parsing metainfo file #{fn}---maybe not a .torrent?"
+end
+
+# complete abuse
+mi.trackers.each do |track|
+  puts "#{track}:"
+
+  tc = RubyTorrent::TrackerConnection.new(track, mi.info.sha1, mi.info.total_length, 9999, "rubytorrent.dumppeer") # complete abuse, i know
+  begin
+    tc.force_refresh
+    puts "<no peers>" if tc.peers.length == 0
+    tc.peers.each do |p|
+      puts dump_peer(p)
+    end
+  rescue RubyTorrent::TrackerError => e
+    puts "error connecting to tracker: #{e.message}"
+  end
+end
+

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/make-metainfo.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/make-metainfo.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/make-metainfo.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,211 @@
+## make-metainfo.rb -- interactive .torrent creater
+## Copyright 2005 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'digest/sha1'
+require "rubytorrent"
+
+def die(x); $stderr << "#{x}\n" && exit(-1); end
+def syntax
+  %{
+  Syntax: make-metainfo.rb [<file or directory>]+
+
+  make-metainfo is an interactive program for creating .torrent files from a set
+  of files or directories. any directories specified will be scanned recursively.
+  }
+end
+
+def find_files(f)
+  if FileTest.directory? f
+    Dir.new(f).entries.map { |x| find_files(File.join(f, x)) unless x =~ /^\.[\.\/]*$/}.compact
+  else
+    f
+  end
+end
+
+class Numeric
+  def to_size_s
+    if self < 1024
+      "#{self.round}b"
+    elsif self < 1024**2
+      "#{(self / 1024.0).round}kb"
+    elsif self < 1024**3
+      "#{(self / (1024.0**2)).round}mb"
+    else
+      "#{(self / (1024.0**3)).round}gb"
+    end
+  end
+end
+
+def read_pieces(files, length)
+  buf = ""
+  files.each do |f|
+    File.open(f) do |fh|
+      begin
+        read = fh.read(length - buf.length)
+        if (buf.length + read.length) == length
+          yield(buf + read)
+          buf = ""
+        else
+          buf += read
+        end
+      end until fh.eof?
+    end
+  end
+
+  yield buf
+end 
+
+die syntax if ARGV.length == 0
+
+puts "Scanning..."
+files = ARGV.map { |f| find_files f }.flatten
+single = files.length == 1
+
+puts "Building #{(single ? 'single' : 'multi')}-file .torrent for #{files.length} file#{(single ? '' : 's')}."
+
+mi = RubyTorrent::MetaInfo.new
+mii = RubyTorrent::MetaInfoInfo.new
+
+maybe_name = if single
+               ARGV[0]
+             else
+               (File.directory?(ARGV[0]) ? File.basename(ARGV[0]) : File.basename(File.dirname(ARGV[0])))
+             end
+puts
+print %{Default output file/directory name (enter for "#{maybe_name}"): }
+name = $stdin.gets.chomp
+mii.name = (name == "" ? maybe_name : name)
+puts %{We'll use "#{mii.name}".}
+
+puts
+puts "Measuring..."
+length = nil
+if single
+  length = mii.length = files.inject(0) { |s, f| s + File.size(f) }
+else
+  mii.files = []
+  length = files.inject(0) do |s, f|
+    miif = RubyTorrent::MetaInfoInfoFile.new
+    miif.length = File.size f
+    miif.path = f.split File::SEPARATOR
+    miif.path = miif.path[1, miif.path.length - 1] if miif.path[0] == mii.name
+    mii.files << miif
+    s + miif.length
+  end
+end
+
+puts <<EOS
+
+The file is #{length.to_size_s}. What piece size would you like? A smaller piece size
+will result in a larger .torrent file; a larger piece size may cause
+transfer inefficiency. Common sizes are 256, 512, and 1024kb.
+
+Hint: for this .torrent,
+EOS
+
+size = nil
+[64, 128, 256, 512, 1024, 2048, 4096].each do |size|
+  num_pieces = (length.to_f / size / 1024.0).ceil
+  tsize = num_pieces.to_f * 20.0 + 100
+  puts "  - piece size of #{size}kb => #{num_pieces} pieces and .torrent size of approx. #{tsize.to_size_s}."
+  break if tsize < 10240
+end
+
+maybe_plen = [size, 256].min
+begin
+  print "Piece size in kb (enter for #{maybe_plen}k): "
+  plen = $stdin.gets.chomp
+end while plen !~ /^\d*$/
+
+plen = (plen == "" ? maybe_plen : plen.to_i)
+
+mii.piece_length = plen * 1024
+num_pieces = (length.to_f / mii.piece_length.to_f).ceil
+puts "Using piece size of #{plen}kb => .torrent size of approx. #{(num_pieces * 20.0).to_size_s}."
+
+print "Calculating #{num_pieces} piece SHA1s... " ; $stdout.flush
+
+mii.pieces = ""
+i = 0
+read_pieces(files, mii.piece_length) do |piece|
+  mii.pieces += Digest::SHA1.digest(piece)
+  i += 1
+  if (i % 100) == 0
+    print "#{(i.to_f / num_pieces * 100.0).round}%... "; $stdout.flush
+  end
+end
+puts "done"
+
+mi.info = mii
+puts <<EOS
+
+Enter the tracker URL or URLs that will be hosting the .torrent
+file. These are typically of the form:
+
+  http://tracker.example.com:6969/announce
+
+Multiple trackers may be partitioned into tiers; clients will try all
+servers (in random order) from an earlier tier before trying those of
+a later tier. See http://home.elp.rr.com/tur/multitracker-spec.txt
+for details.
+
+Enter the tracker URL(s) now. Separate multiple tracker URLs on the
+same tier with spaces. Enter a blank line when you're done.
+
+(Note that if you have multiple trackers, some clients may only use
+the first one, so that should be the one capable of handling the most
+traffic.)
+EOS
+
+tier = 0
+trackers = []
+begin
+  print "Tier #{tier} tracker(s): "
+  these = $stdin.gets.chomp.split(/\s+/)
+  trackers.push these unless these.length == 0
+  tier += 1 unless these.length == 0
+end while (these.length != 0) || (tier == 0)
+
+mi.announce = URI.parse(trackers[0][0])
+mi.announce_list = trackers.map do |tier|
+  tier.map { |x| URI.parse(x) }
+end unless (trackers.length == 1) && (trackers[0].length == 1)
+
+puts <<EOS
+
+Enter any comments. No one will probably ever see these. End with a blank line.
+EOS
+comm = ""
+while true
+  s = $stdin.gets.chomp
+  break if s == ""
+  comm += s + "\n"
+end
+mi.comment = comm.chomp unless comm == ""
+
+mi.created_by = "RubyTorrent make-metainfo (http://rubytorrent.rubyforge.org)"
+mi.creation_date = Time.now
+
+maybe_name = "#{mii.name}.torrent"
+begin
+  print "Output filename (enter for #{maybe_name}): "
+  name = $stdin.gets.chomp
+end while name.length == ""
+
+name = (name == "" ? maybe_name : name)
+File.open(name, "w") do |f|
+  f.write mi.to_bencoding
+end
+
+puts "Succesfully created #{name}"
+

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer-ncurses.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer-ncurses.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer-ncurses.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,339 @@
+## rtpeer-ncurses.rb -- RubyTorrent ncurses BitTorrent peer.
+## Copyright 2005 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent"
+require "ncurses"
+require "optparse"
+
+def die(x); $stderr << "#{x}\n" && exit(-1); end
+
+dlratelim = nil
+ulratelim = nil
+
+opts = OptionParser.new do |opts|
+  opts.banner = 
+%{Usage: rtpeer-ncurses [options] <torrent> [<target>]
+
+rtpeer-ncurses is a very simple ncurses-based BitTorrent peer. You can use it
+to download .torrents or to seed them.
+
+<torrent> is a .torrent filename or URL.
+<target> is a file or directory on disk. If not specified, the default value
+  from <torrent> will be used.
+[options] are:
+}
+
+  opts.on("-l", "--log FILENAME",
+          "Log events to FILENAME (for debugging)") do |fn|
+    RubyTorrent::log_output_to(fn)
+  end
+
+  opts.on("-d", "--downlimit LIMIT", Integer,
+          "Limit download rate to LIMIT kb/s") do |x|
+    dlratelim = x * 1024
+  end
+
+  opts.on("-u", "--uplimit LIMIT", Integer,
+          "Limit upload rate to LIMIT kb/s") do |x|
+    ulratelim = x * 1024
+  end
+
+  opts.on_tail("-h", "--help", "Show this message") do
+    puts opts
+    exit
+  end
+end
+
+opts.parse!(ARGV)
+proxy = ENV["http_proxy"]
+torrent = ARGV.shift or (puts opts; exit)
+dest = ARGV.shift
+
+class Numeric
+  def to_sz
+    if self < 1024
+      "#{self.round}b"
+    elsif self < 1024 ** 2
+      "#{(self / 1024 ).round}k"
+    elsif self < 1024 ** 3
+      sprintf("%.1fm", self.to_f / (1024 ** 2))
+    else
+      sprintf("%.2fg", self.to_f / (1024 ** 3))
+    end
+  end
+
+  MIN = 60
+  HOUR = 60 * MIN
+  DAY = 24 * HOUR
+
+  def to_time
+    if self < MIN
+      sprintf("0:%02d", self)
+    elsif self < HOUR
+      sprintf("%d:%02d", self / MIN, self % MIN)
+    elsif self < DAY
+      sprintf("%d:%02d:%02d", self / HOUR, (self % HOUR) / MIN, (self % HOUR) % MIN)
+    else
+      sprintf("%dd %d:%02d:%02d", self / DAY, (self % DAY) / HOUR, ((self % DAY) % HOUR) / MIN, ((self % DAY) % HOUR) % MIN)
+    end
+  end
+end
+
+class NilClass
+  def to_time; "--:--"; end
+  def to_sz; "-"; end
+end
+
+class Display
+  STALL_SECS = 15
+
+  attr_accessor :fn, :dest, :status,:dlamt, :ulamt, :dlrate, :ulrate,
+                :conn_peers, :fail_peers, :untried_peers, :tracker,
+                :errcount, :completed, :total, :rate, :use_rate
+
+  def initialize(window)
+    @window = window
+    @need_update = true
+
+    @fn = ""
+    @dest = ""
+    @status = ""
+    @completed = 0
+    @total = 0
+    @dlamt = 0
+    @dlrate = 0
+    @ulamt = 0
+    @ulrate = 0
+    @rate = 0
+    @conn_peers = 0
+    @fail_peers = 0
+    @untried_peers = 0
+    @tracker = "not connected"
+    @errcount = 0
+    @dlblocks = 0
+    @ulblocks = 0
+
+    @got_blocks = 0
+    @sent_blocks = 0
+    @last_got_block = nil
+    @last_sent_block = nil
+    @start_time = nil
+
+    @use_rate = false
+  end
+
+  def got_block
+    @got_blocks += 1
+    @last_got_block = Time.now
+  end
+
+  def sent_block
+    @sent_blocks += 1
+    @last_sent_block = Time.now
+  end
+
+  def sigwinch_handler(sig = nil)
+    @need_update = true
+  end
+
+  def start_timer
+    @start_time = Time.now
+  end
+
+  def draw
+    if @need_update
+      update_size 
+      @window.erase
+    end
+
+    complete_width = [@cols - 23, 0].max
+    complete_ticks = ((@completed.to_f / @total) * complete_width)
+
+    elapsed = (@start_time ? Time.now - @start_time : nil)
+    rate = (use_rate ? @rate : @dlrate)
+    remaining = rate && (rate > 0 ? (@total - @completed).to_f / rate : nil)
+
+    dlstall = @last_got_block && ((Time.now - @last_got_block) > STALL_SECS)
+    ulstall = @last_sent_block && ((Time.now - @last_sent_block) > STALL_SECS)
+      
+    line = 1
+    [
+      "Contents: #@fn",
+      "    Dest: #@dest",
+      "",
+      "  Status: #@status",
+      "Progress: [" + ("#" * complete_ticks),
+      "    Time: elapsed #{elapsed.to_time}, remaining #{remaining.to_time}",
+      "Download: #{@dlamt.to_sz} at #{dlstall ? '(stalled)' : @dlrate.to_sz + '/s'}",
+      "  Upload: #{@ulamt.to_sz} at #{ulstall ? '(stalled)' : @ulrate.to_sz + '/s'}",
+      "   Peers: connected to #@conn_peers (#@fail_peers failed, #@untried_peers untried)",
+      " Tracker: #@tracker",
+      "  Errors: #@errcount",
+    ].each do |s|
+      break if line > @rows
+      @window.mvaddnstr(line, 2, s + (" " * @cols), @cols - 4)
+      line += 1
+    end
+
+    ## progress bar tail
+    @window.mvaddstr(5, @cols - 11, sprintf("] %.2f%%  ", (@completed.to_f / @total) * 100.0))
+    @window.mvaddnstr(7, 31, "|" + ("#" * (@dlrate / 1024)) + (" " * @cols), @cols - 31 - 2)
+    @window.mvaddnstr(8, 31, "|" + ('#' * (@ulrate / 1024)) + (" " * @cols), @cols - 31 - 2)
+
+    @window.box(0,0)
+
+#    @got_blocks -= 1 unless @got_blocks == 0
+#    @sent_blocks -= 1 unless @sent_blocks == 0
+  end
+
+  private
+
+  def update_size
+    rows = []
+    cols = []
+    ## jesus CHRIST this is a shitty interface.
+    @window.getmaxyx(rows, cols)
+    @rows = rows[0]
+    @cols = cols[0]
+    @need_update = false
+  end
+end
+
+begin
+  mi = RubyTorrent::MetaInfo.from_location(torrent, proxy)
+rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
+  die %{Error: can\'t parse metainfo file "#{torrent}"---maybe not a .torrent?}
+rescue RubyTorrent::TypedStructError => e
+  $stderr << <<EOS
+error parsing metainfo file, and it's likely something I should know about.
+please email the torrent file to wmorgan-rubytorrent-bug at masanjin.net,
+along with this backtrace: (this is RubyTorrent version #{RubyTorrent::VERSION})
+EOS
+
+  raise e
+rescue IOError, SystemCallError => e
+  $stderr.puts %{Error: can't read file "#{torrent}": #{e.message}}
+  exit
+end
+
+unless dest.nil?
+  if FileTest.directory?(dest) && mi.info.single?
+    dest = File.join(dest, mi.info.name)
+  elsif FileTest.file?(dest) && mi.info.multiple?
+    die %{Error: .torrent contains multiple files, but "#{dest}" is a single file (must be a directory)}
+  end
+end
+
+def handle_any_input(display)
+  case(Ncurses.getch())
+  when ?q, ?Q
+    Ncurses.curs_set(1)
+    Ncurses.endwin()
+    exit
+  when Ncurses::KEY_RESIZE
+    display.sigwinch_handler
+  end
+end
+
+Ncurses.initscr
+
+begin
+  Ncurses.nl()
+  Ncurses.noecho()
+  Ncurses.curs_set(0)
+  Ncurses.stdscr.nodelay(true)
+  Ncurses.timeout(0)
+
+  display = Display.new Ncurses.stdscr
+  display.status = "checking file on disk..."
+  display.dest = File.expand_path(dest || mi.info.name) + (mi.single? ? "" : "/")
+  if mi.single?
+    display.fn = "#{mi.info.name} (#{mi.info.length.to_sz} in one file)"
+  else
+    display.fn = "#{mi.info.name}/ (#{mi.info.total_length.to_sz} in #{mi.info.files.length} files)"
+  end
+  display.total = mi.info.num_pieces * mi.info.piece_length
+  display.completed = 0
+  display.draw; Ncurses.refresh
+
+  display.use_rate = true
+  display.start_timer
+  num_pieces = 0
+  start = Time.now
+  every = 10
+  package = RubyTorrent::Package.new(mi, dest) do |piece|
+    num_pieces += 1
+    if (num_pieces % every) == 0
+      display.completed = (num_pieces * mi.info.piece_length)
+      display.rate = display.completed.to_f / (Time.now - start)
+      handle_any_input display
+      display.draw; Ncurses.refresh
+    end
+  end
+
+  display.status = "starting peer..."
+  display.use_rate = false
+  display.draw; Ncurses.refresh
+  bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy, :dlratelim => dlratelim, :ulratelim => ulratelim)
+
+  connecting = true
+  bt.on_event(self, :received_block) do |s, b, peer|
+    display.got_block
+    connecting = false
+  end
+  bt.on_event(self, :sent_block) do |s, b, peer|
+    display.sent_block
+    connecting = false
+  end
+  bt.on_event(self, :discarded_piece) { |s, p| display.errcount += 1 }
+  bt.on_event(self, :tracker_connected) do |s, url|
+    display.tracker = url
+    display.untried_peers = bt.num_possible_peers
+  end
+  bt.on_event(self, :tracker_lost) { |s, url| display.tracker = "can't connect to #{url}" }
+  bt.on_event(self, :forgetting_peer) { |s, p| display.fail_peers += 1 }
+  bt.on_event(self, :removed_peer, :added_peer) do |s, p|
+    if (display.conn_peers = bt.num_active_peers) == 0
+      connecting = true
+    end
+  end
+  bt.on_event(self, :added_peer) { |s, p| display.conn_peers += 1 }
+  bt.on_event(self, :trying_peer) { |s, p| display.untried_peers -= 1 unless display.untried_peers == 0 }
+
+  display.total = bt.total_bytes
+  display.start_timer
+
+  while true
+    handle_any_input(display)
+
+    display.status = if bt.complete?
+                       "seeding (download complete)"
+                     elsif connecting
+                       "connecting to peers"
+                     else
+                       "downloading"
+                     end
+    display.draw; Ncurses.refresh
+
+    display.dlamt = bt.dlamt
+    display.dlrate = bt.dlrate
+    display.ulamt = bt.ulamt
+    display.ulrate = bt.ulrate
+    display.completed = bt.bytes_completed
+    display.draw; Ncurses.refresh
+    sleep(0.5)
+  end
+ensure
+  Ncurses.curs_set(1)
+  Ncurses.endwin()
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,125 @@
+## rtpeer.rb -- RubyTorrent line-mode BitTorrent peer.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent"
+
+Thread.abort_on_exception = true # make debugging easier
+
+def die(x); $stderr << "#{x}\n" && exit(-1); end
+def syntax
+  %{
+  Syntax: rtpeer <.torrent filename or URL> [<destination file or directory>]
+
+  rtpeer is a very simple line-based BitTorrent peer. You can use it
+  to download .torrents or to seed them, but it's mainly good for debugging.
+  }
+end
+
+class Numeric
+  def k; (self.to_f / 1024.0); end
+  def f(format="0.0")
+    sprintf("%#{format.to_s}f", self)
+  end
+end
+
+proxy = ENV["http_proxy"]
+torrent = ARGV.shift or die syntax
+dest = ARGV.shift
+
+puts "reading torrent..."
+begin
+  mi = RubyTorrent::MetaInfo.from_location(torrent, proxy)
+rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
+  die %{Error: can't parse metainfo file "#{torrent}"---maybe not a .torrent?}
+rescue RubyTorrent::TypedStructError => e
+  $stderr << <<EOS
+error parsing metainfo file, and it's likely something I should know about.
+please email the torrent file to wmorgan-rubytorrent-bug at masanjin.net,
+along with this backtrace: (this is RubyTorrent version #{RubyTorrent::VERSION})
+EOS
+
+  raise e
+rescue IOError, SystemCallError => e
+  die %{Error: can't read file "#{torrent}": #{e.message}}
+end
+
+unless dest.nil?
+  if FileTest.directory?(dest) && mi.info.single?
+    dest = File.join(dest, mi.info.name)
+  elsif FileTest.file?(dest) && mi.info.multiple?
+    die %{Error: .torrent contains multiple files, but "#{dest}" is a single file (must be a directory)}
+  end
+end
+print "checking file status: " ; $stdout.flush
+package = RubyTorrent::Package.new(mi, dest) do |piece|
+  print(piece.complete? && piece.valid? ? "#" : ".")
+  $stdout.flush
+end
+puts " done"
+
+puts "starting peer..."
+bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy) #, :dlratelim => 20*1024, :ulratelim => 10*1024)
+
+unless $DEBUG # these are duplicated by debugging information
+  bt.on_event(self, :trying_peer) { |s, p| puts "trying peer #{p}" }
+  bt.on_event(self, :forgetting_peer) { |s, p| puts "couldn't connect to peer #{p}" }
+  bt.on_event(self, :removed_peer) { |s, p| puts "disconnected from peer #{p}" }
+end
+bt.on_event(self, :added_peer) { |s, p| puts "connected to peer #{p}" }
+bt.on_event(self, :received_block) { |s, b, peer| puts "<- got block #{b} from peer #{peer}, now #{package.pieces[b.pindex].percent_done.f}% done and #{package.pieces[b.pindex].percent_claimed.f}% claimed" }
+bt.on_event(self, :sent_block) { |s, b, peer| puts "-> sent block #{b} to peer #{peer}" }
+bt.on_event(self, :requested_block) { |s, b, peer| puts "-- requested block #{b} from #{peer}" }
+bt.on_event(self, :have_piece) { |s, p| puts "***** got complete and valid piece #{p}" }
+bt.on_event(self, :discarded_piece) { |s, p| puts "XXXXX checksum error on piece #{p}, discarded" }
+bt.on_event(self, :tracker_connected) { |s, url| puts "[tracker] connected to tracker #{url}" }
+bt.on_event(self, :tracker_lost) { |s, url| puts "[tracker] couldn't connect to tracker #{url}" }
+bt.on_event(self, :complete) do
+  puts <<EOS
+*********************
+* download complete *
+*********************
+EOS
+end
+
+puts "listening on #{bt.ip} port #{bt.port}"
+
+thread = nil
+## not sure if this works on windows, but it's just a nicety anyways.
+Signal.trap("INT") do
+  Signal.trap("INT", "DEFAULT") # second ^C will really kill us
+  thread.kill unless thread.nil?
+  bt.shutdown_all
+end unless $DEBUG
+
+thread = Thread.new do
+  while true
+    puts "-" * 78
+    ps = bt.peer_info.sort_by { |h| h[:start_time] }.reverse
+    puts <<EOS
+downloaded #{bt.dlamt.k.f}k @ #{bt.dlrate.k.f}kb/s, uploaded #{bt.ulamt.k.f}k @ #{bt.ulrate.k.f}kb/s
+completed: #{bt.pieces_completed} / #{bt.num_pieces} pieces = #{bt.bytes_completed.k.f}kb / #{bt.total_bytes.k.f}kb = #{bt.percent_completed.f}%
+tracker: #{bt.tracker || "not connected"}
+connected to #{ps.length} / #{bt.num_possible_peers} possible peers:
+EOS
+    ps.each do |p|
+      puts <<EOS
+dl #{p[:dlamt].k.f(4.0)}kb @#{p[:dlrate].k.f(3.0)}kb/s, ul #{p[:ulamt].k.f(4.0)}kb @#{p[:ulrate].k.f(3.0)}kb/s, i: #{(p[:interested] ? 'y' : 'n')}/#{(p[:peer_interested] ? 'y' : 'n')} (#{p[:we_desire].f(4.0)}/#{p[:they_desire].f(4.0)}) c: #{(p[:choking] ? 'y' : 'n')}/#{(p[:peer_choking] ? 'y' : 'n')} p: #{p[:pending_send]}/#{p[:pending_recv]}#{(p[:snubbing] ? ', snub' : '')} #{p[:name]}
+EOS
+    end
+    puts "-" * 78
+    sleep 30
+  end
+end
+
+thread.join
+puts "done"

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/bencoding.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/bencoding.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/bencoding.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,174 @@
+## bencoding.rb -- parse and generate bencoded values.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'uri'
+require 'digest/sha1'
+ 
+module RubyTorrent
+
+## we mess in the users' namespaces in this file. there's no good way
+## around it. i don't think it's too egregious though.
+
+class BEncodingError < StandardError; end
+
+class BStream
+  include Enumerable
+
+  @@classes = []
+
+  def initialize(s)
+    @s = s
+  end
+
+  def self.register_bencoded_class(c)
+    @@classes.push c
+  end
+
+  def each
+    happy = true
+    begin
+      happy = false
+      c = @s.getc
+      @@classes.each do |klass|
+        if klass.bencoded? c
+          o = klass.parse_bencoding(c, @s)
+          happy = true
+          yield o
+          break
+        end
+      end unless c.nil?
+      unless happy
+        @s.ungetc c unless c.nil?
+      end
+    end while happy
+    self
+  end
+end
+
+end
+
+class String
+  def to_bencoding
+    self.length.to_s + ":" + self.to_s
+  end
+
+  def self.bencoded?(c)
+    (?0 .. ?9).include? c
+  end
+
+  def self.parse_bencoding(c, s)
+    lens = c.chr
+    while ((x = s.getc) != ?:)
+      unless (?0 .. ?9).include? x
+        s.ungetc x
+        raise RubyTorrent::BEncodingError, "invalid bencoded string length #{lens} + #{x}" 
+      end
+      lens += x.chr
+    end
+    raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless lens.length <= 20
+    len = lens.to_i
+    raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless len >= 0
+    (len > 0 ? s.read(len) : "")
+  end
+
+  RubyTorrent::BStream.register_bencoded_class self
+end
+
+class Integer
+  def to_bencoding
+    "i" + self.to_s + "e"
+  end
+
+  def self.bencoded?(c)
+    c == ?i
+  end
+
+  def self.parse_bencoding(c, s)
+    ints = ""
+    while ((x = s.getc.chr) != 'e')
+      raise RubyTorrent::BEncodingError, "invalid bencoded integer #{x.inspect}" unless x =~ /\d|-/
+      ints += x
+    end
+    raise RubyTorrent::BEncodingError, "invalid integer #{ints} (too long)" unless ints.length <= 20
+    int = ints.to_i
+    raise RubyTorrent::BEncodingError, %{can't parse bencoded integer "#{ints}"} if (int == 0) && (ints !~ /^0$/) #'
+    int
+  end
+
+  RubyTorrent::BStream.register_bencoded_class self
+end
+
+class Time
+  def to_bencoding
+    self.to_i.to_bencoding
+  end
+end
+
+module URI
+  def to_bencoding
+    self.to_s.to_bencoding
+  end
+end
+
+class Array
+  def to_bencoding
+    "l" + self.map { |e| e.to_bencoding }.join + "e"
+  end
+
+  def self.bencoded?(c)
+    c == ?l
+  end
+
+  def self.parse_bencoding(c, s)
+    ret = RubyTorrent::BStream.new(s).map { |x| x }
+    raise RubyTorrent::BEncodingError, "missing list terminator" unless s.getc == ?e
+    ret
+  end
+
+  RubyTorrent::BStream.register_bencoded_class self
+end
+
+class Hash
+  def to_bencoding
+    "d" + keys.sort.map do |k|
+      v = self[k]
+      if v.nil?
+        nil
+      else
+        [k.to_bencoding, v.to_bencoding].join
+      end
+    end.compact.join + "e"
+  end
+
+  def self.bencoded?(c)
+    c == ?d
+  end
+
+  def self.parse_bencoding(c, s)
+    ret = {}
+    key = nil
+    RubyTorrent::BStream.new(s).each do |x|
+      if key == nil
+        key = x
+      else
+        ret[key] = x
+        key = nil
+      end
+    end
+
+    raise RubyTorrent::BEncodingError, "no dictionary terminator" unless s.getc == ?e
+    ret
+  end
+
+  RubyTorrent::BStream.register_bencoded_class self
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/controller.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/controller.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/controller.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,610 @@
+## controller.rb -- cross-peer logic.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'socket'
+require 'thread'
+
+module RubyTorrent
+
+## keeps pieces in order
+class PieceOrder
+  POP_RECALC_THRESH = 20 # popularity of all pieces is recalculated
+                         # (expensive sort) when this number of pieces
+                         # have arrived at any of the peers, OR:
+  POP_RECALC_LIMIT = 30 # ... when this many seconds have passed when
+                        # at least one piece has changed in
+                        # popularity, or if we're in fuseki mode.
+  def initialize(package)
+    @package = package
+    @order = nil
+    @num_changed = 0
+    @pop = Array.new(@package.pieces.length, 0)
+    @jitter = Array.new(@package.pieces.length) { rand }
+    @m = Mutex.new
+    @last_recalc = nil
+  end
+
+  ## increment the popularity of a piece
+  def inc(i) 
+    @m.synchronize do
+      @pop[i.to_i] += 1
+      @num_changed += 1
+    end
+  end
+
+  ## increment the popularity of multiple pieces
+  def inc_all(bitfield, inc=1)
+    @m.synchronize do
+      bitfield.each_index do |i| 
+        if bitfield[i]
+          @pop[i] += inc
+          @num_changed += 1
+        end
+      end
+    end
+  end
+
+  def dec_all(bitfield)
+    inc_all(bitfield, -1)
+  end
+
+  def each(in_fuseki, num_peers)
+    if (@num_changed > POP_RECALC_THRESH) || @last_recalc.nil? || (((@num_changed > 0) || in_fuseki) && ((Time.now - @last_recalc) > POP_RECALC_LIMIT))
+      rt_debug "* reordering pieces: (#@num_changed changed, last recalc #{(@last_recalc.nil? ? '(never)' : (Time.now - @last_recalc).round)}s ago)..."
+      recalc_order(in_fuseki, num_peers)
+    end
+
+    @order.each { |i| yield i }
+  end
+
+  private
+
+  def recalc_order(in_fuseki, num_peers)
+    @m.synchronize do
+      @num_changed = 0
+      @order = (0 ... @pop.length).sort_by do |i|
+        p = @package.pieces[i]
+        @jitter[i] +
+          if p.started? && !p.complete? # always try to complete a started piece
+            pri = -1 + p.unclaimed_bytes.to_f / p.length
+            rt_debug "   piece #{i} is started but not completed => priority #{pri} (#{p.percent_claimed.round}% claimed, #{p.percent_done.round}% done)"
+            pri
+          elsif p.complete? # don't need these
+#            puts "   piece #{i} is completed => #{@pop.length}"
+            @pop.length # a big number
+          elsif in_fuseki # distance from (# peers) / 2
+#            puts "     piece #{i} has fuseki score #{(@pop[i] - (num_peers / 2)).abs}"
+            (@pop[i] - (num_peers / 2)).abs
+          else
+#            puts "     piece #{i} has popularity #{@pop[i]}"
+            @pop[i]
+          end
+      end
+    end
+    @last_recalc = Time.now
+    rt_debug "* new piece priority: " + @order[0...15].map { |x| x.to_s }.join(', ') + " ..."
+  end
+end
+
+## The Controller manages all PeerConnections for a single Package. It
+## instructs them to request blocks, and tells them whether to choke
+## their connections or not. It also reports progress to the tracker.
+##
+## Incoming, post-handshake peer connections are added by the Server
+## via calling add_connection; deciding to accept these is the
+## Controller's responsibility, as is connecting to any new peers.
+class Controller
+  include EventSource
+  extend AttrReaderQ, MinIntervalMethods
+
+  ## general behavior parameters
+  HEARTBEAT = 5 # seconds between iterations of the heartbeat
+  MAX_PEERS = 15 # hard limit on the number of peers
+  ENDGAME_PIECE_THRESH = 5  # (wild guess) number of pieces remaining
+                            # before we trigger end-game mode
+  FUSEKI_PIECE_THRESH = 2 # number of pieces we must have before
+                          # getting out of fuseki mode. in fuseki
+                          # ("opening", if you're not a weiqi/go fan)
+                          # mode, rather than ranking pieces by
+                          # rarity, we rank them by how distant their
+                          # popularity is from (# peers) / 2, and we're
+                          # also stingly in handing out requests.
+  SPAWN_NEW_PEER_THRESH = 0.75 # portion of the download rate above
+                               # which we'll stop making new peer
+                               # connections
+  RATE_WINDOW = 20 # window size (in seconds) of the rate calculation.
+                   # presumably this should be the same as the window
+                   # used in the RateMeter class.
+
+  ## tracker parameters. when we can't access a tracker, we retry at
+  ## DEAD_TRACKER_INITIAL_DELAY seconds and double that after every
+  ## failure, capping at DEAD_TRACKER_MAX_DELAY.
+  DEAD_TRACKER_INITIAL_INTERVAL = 5
+  DEAD_TRACKER_MAX_INTERVAL = 3600
+
+  ## single peer parameters
+  KEEPALIVE_INTERVAL = 120 # seconds of silence before sending a keepalive
+  SILENT_DEATH_INTERVAL = 240 # seconds of silence before we drop a peer
+  BOREDOM_DEATH_INTERVAL = 120 # seconds of existence with no downloaded data
+                               # at which we drop a peer in favor of
+                               # an incoming peer (unless the package
+                               # is complete)
+
+  BLOCK_SIZE = 2**15 # send this size blocks. need to find out more
+                     # about this parameter: how does it affect
+                     # transfer rates?
+
+  ## antisnubbing
+  ANTISNUB_RATE_THRESH = 1024 # if the total bytes/second across all
+                              # peers falls below this threshold, we
+                              # trigger anti-snubbing mode
+  ANTISNUB_INTERVAL = 60 # seconds of no blocks from a peer before we
+                         # add an optimistic unchoke slot when in
+                         # anti-snubbing mode.
+
+  ## choking and optimistic unchoking parameters
+  NUM_FRIENDS = 4 # number of peers unchoked due to high download rates
+  CALC_FRIENDS_INTERVAL = 10 # seconds between recalculating choked
+                             # status for each peer
+  CALC_OPTUNCHOKES_INTERVAL = 30 # seconds between reassigning
+                                 # optimistic unchoked status
+  NUM_OPTUNCHOKES = 1  # number of optimistic unchoke slots
+                       # (not including any temporary ones
+                       # generated in anti-snubbing mode.
+  NEW_OPTUNCHOKE_PROB = 0.5 # peers are ranked by the age of
+                                    # their connection, and optimistic
+                                    # unchoking slots are given with
+                                    # probability p*(1-p)^r, where r
+                                    # is the rank and p is this number.
+
+  attr_accessor :package, :info_hash, :tracker, :ulratelim, :dlratelim,
+                :http_proxy
+  attr_reader_q :running
+  event :trying_peer, :forgetting_peer, :added_peer, :removed_peer,
+        :received_block, :sent_block, :have_piece, :discarded_piece,
+        :tracker_connected, :tracker_lost, :requested_block
+
+  def initialize(server, package, info_hash, trackers, dlratelim=nil, ulratelim=nil, http_proxy=ENV["http_proxy"])
+    @server = server
+    @info_hash = info_hash
+    @package = package
+    @trackers = trackers
+    @http_proxy = http_proxy
+
+    @dlratelim = dlratelim
+    @ulratelim = ulratelim
+
+    @peers = [].extend(ArrayShuffle)
+    @peers_m = Mutex.new
+    @thread = nil
+
+    @tracker = nil
+    @last_tracker_attempt = nil
+    @tracker_delay = DEAD_TRACKER_INITIAL_INTERVAL
+
+    ## friends
+    @num_friends = 0
+    @num_optunchokes = 0
+    @num_snubbed = 0
+
+    ## keep track of the popularity of the pieces so as to assign
+    ## blocks optimally to peers.
+    @piece_order = PieceOrder.new @package
+
+    @running = false
+  end
+
+  def dlrate; @peers.inject(0) { |s, p| s + p.dlrate }; end
+  def ulrate; @peers.inject(0) { |s, p| s + p.ulrate }; end
+  def dlamt; @peers.inject(0) { |s, p| s + p.dlamt }; end
+  def ulamt; @peers.inject(0) { |s, p| s + p.ulamt }; end
+  def num_peers; @peers.length; end
+
+  def start
+    raise "already" if @running
+
+    find_tracker
+
+    @in_endgame = false
+    @in_antisnub = false
+    @in_fuseki = false
+    @running = true
+    @thread = Thread.new do
+      while @running
+        step
+        sleep HEARTBEAT
+      end
+    end
+
+    @peers.each { |p| p.start unless p.running? }
+
+    self
+  end
+
+  def shutdown
+    @running = false
+    @tracker.stopped unless @tracker.nil? rescue TrackerError
+    @thread.join(0.2)
+    @peers.each { |c| c.shutdown }
+    self
+  end
+
+  def to_s
+    "<#{self.class}: package #{@package}>"
+  end
+
+  ## this could be called at any point by the Server, if it receives
+  ## incoming peer connections.
+  def add_peer(p)
+    accept = true
+
+    if @peers.length >= MAX_PEERS && !@package.complete?
+      oldp = @peers.find { |x| !x.running? || ((x.dlamt == 0) && ((Time.now - x.start_time) > BOREDOM_DEATH_INTERVAL)) }
+
+      if oldp
+        rt_debug "killing peer for being boring: #{oldp}" 
+        oldp.shutdown
+      else
+        rt_debug "too many peers, ignoring #{p}"
+        p.shutdown
+        accept = false
+      end
+    end
+
+    if accept
+      p.on_event(self, :received_block) { |peer, block| received_block(block, peer) }
+      p.on_event(self, :peer_has_piece) { |peer, piece| peer_has_piece(piece, peer) }
+      p.on_event(self, :peer_has_pieces) { |peer, bitfield| peer_has_pieces(bitfield, peer) }
+      p.on_event(self, :sent_block) { |peer, block| send_event(:sent_block, block, peer.name) }
+      p.on_event(self, :requested_block) { |peer, block| send_event(:requested_block, block, peer.name) }
+
+      @peers_m.synchronize do
+        @peers.push p
+        ## it's important not to call p.start (which triggers the
+        ## bitfield message) until it's been added to @peer, such that
+        ## any :have messages that might happen from other peers in
+        ## the mean time are propagated to it.
+        ##
+        ## of course that means we need to call p.start within the
+        ## mutex context so that the reaper section of the heartbeat
+        ## doesn't kill it between push and start.
+        ##
+        ## ah, the joys of threaded programming.
+        p.start if @running
+      end
+
+      send_event(:added_peer, p.name)
+    end
+  end
+
+  def received_block(block, peer)
+    if @in_endgame
+      @peers_m.synchronize { @peers.each { |p| p.cancel block if p.running? && (p != peer)} }
+    end
+    send_event(:received_block, block, peer.name)
+
+    piece = @package.pieces[block.pindex] # find corresponding piece
+    if piece.complete?
+      if piece.valid?
+        @peers_m.synchronize { @peers.each { |peer| peer.have_piece piece } }
+        send_event(:have_piece, piece)
+      else
+        rt_warning "#{self}: received data for #{piece} does not match SHA1 hash, discarding"
+        send_event(:discarded_piece, piece)
+        piece.discard
+      end
+    end
+  end
+
+  def peer_has_piece(piece, peer)
+    @piece_order.inc piece.index
+  end
+
+  def peer_has_pieces(bitfield, peer)
+    @piece_order.inc_all bitfield
+  end
+
+  ## yield all desired blocks, in order of desire. called by peers to
+  ## refill their queues.
+  def claim_blocks
+    @piece_order.each(@in_fuseki, @peers.length) do |i|
+      p = @package.pieces[i]
+      next if p.complete?
+#      rt_debug "+ considering piece #{p}"
+      if @in_endgame
+        p.each_empty_block(BLOCK_SIZE) { |b| yield b }
+      else
+        p.each_unclaimed_block(BLOCK_SIZE) do |b|
+          if yield b
+            p.claim_block b
+            return if @in_fuseki # fuseki shortcut
+          end
+        end
+      end
+    end
+  end
+
+  def forget_blocks(blocks)
+#    rt_debug "#{self}: forgetting blocks #{blocks.join(', ')}"
+    blocks.each { |b| @package.pieces[b.pindex].unclaim_block b }
+  end
+
+  def peer_info
+    @peers.map do |p|
+      next nil unless p.running?
+      {:name => p.name, :seed => p.peer_complete?,
+       :dlamt => p.dlamt, :ulamt => p.ulamt,
+       :dlrate => p.dlrate, :ulrate => p.ulrate,
+       :pending_send => p.pending_send, :pending_recv => p.pending_recv,
+       :interested => p.interested?, :peer_interested => p.peer_interested?,
+       :choking => p.choking?, :peer_choking => p.peer_choking?,
+       :snubbing => p.snubbing?,
+       :we_desire => @package.pieces.inject(0) do |s, piece|
+          s + (!piece.complete? && p.piece_available?(piece.index) ? 1 : 0)
+        end,
+       :they_desire => @package.pieces.inject(0) do |s, piece|
+          s + (piece.complete? && !p.piece_available?(piece.index) ? 1 : 0)
+        end,
+       :start_time => p.start_time
+      }
+    end.compact
+  end
+
+  private
+
+  def find_tracker
+    return if @tracker || (@last_tracker_attempt && (Time.now - @last_tracker_attempt) < @tracker_delay)
+
+    @last_tracker_attempt = Time.now
+    Thread.new do
+      @trackers.each do |tracker|
+        break if @tracker
+        rt_debug "trying tracker #{tracker}"
+        tc = TrackerConnection.new(tracker, @info_hash, @package.size, @server.port, @server.id, nil, 50, @http_proxy)
+        begin
+          @tracker = tc.started
+          tc.already_completed if @package.complete?
+          @tracker_delay = DEAD_TRACKER_INITIAL_INTERVAL
+          send_event(:tracker_connected, tc.url)
+        rescue TrackerError => e
+          rt_debug "couldn't connect: #{e.message}"
+        end
+      end
+    end
+
+    @tracker_delay = [@tracker_delay * 2, DEAD_TRACKER_MAX_INTERVAL].min if @tracker.nil?
+    rt_warning "couldn't connect to tracker, next try in #@tracker_delay seconds" if @tracker.nil?
+  end
+
+  def add_a_peer
+    return false if @tracker.nil? || (@peers.length >= MAX_PEERS) || @package.complete? || (@num_friends >= NUM_FRIENDS) || (@dlratelim && (dlrate > (@dlratelim * SPAWN_NEW_PEER_THRESH)))
+
+    @tracker.peers.shuffle.each do |peer|
+#        rt_debug "]] comparing: #{peer.ip} vs #{@server.ip} and #{peer.port} vs #{@server.port} (tried? #{peer.tried?})"
+      next if peer.tried? || ((peer.ip == @server.ip) && (peer.port == @server.port)) rescue next
+
+      peername = "#{peer.ip}:#{peer.port}"
+      send_event(:trying_peer, peername)
+
+      Thread.new do # this may ultimately result in a call to add_peer
+        sleep rand(10)
+        rt_debug "=> making outgoing connection to #{peername}"
+        begin
+          peer.tried = true
+          socket = TCPSocket.new(peer.ip, peer.port)
+          @server.add_connection(peername, self, socket)
+        rescue SocketError, SystemCallError, Timeout::Error => e
+          rt_debug "couldn't connect to #{peername}: #{e}"
+          send_event(:forgetting_peer, peername)
+        end
+      end
+      break
+    end
+    true
+  end
+
+  def refresh_tracker
+    return if @tracker.nil?
+
+    @tracker.downloaded = dlamt
+    @tracker.uploaded = ulamt
+    @tracker.left = @package.size - @package.bytes_completed
+    begin
+      @tracker.refresh
+    rescue TrackerError
+      send_event(:tracker_lost, @tracker.url)
+      @tracker = nil
+      find_tracker # find a new one
+    end
+  end
+
+  def calc_friends
+    @num_friends = 0
+
+    if @package.complete?
+      @peers.sort_by { |p| -p.ulrate }.each do |p|
+        next if p.snubbing? || !p.running?
+        p.choke = (@num_friends >= NUM_FRIENDS)
+        @num_friends += 1 if p.peer_interested?
+      end
+    else
+      @peers.sort_by { |p| -p.dlrate }.each do |p|
+        next if p.snubbing? || !p.running?
+        p.choke = (@num_friends >= NUM_FRIENDS)
+        @num_friends += 1 if p.peer_interested?
+      end
+    end
+  end
+  min_interval :calc_friends, CALC_FRIENDS_INTERVAL
+
+  def calc_optunchokes
+    rt_debug "* calculating optimistic unchokes..."
+    @num_optunchokes = 0
+
+    if @in_antisnub
+      ## count up the number of our fair weather friends: peers who
+      ## are interested and whom we're not choking, but who haven't
+      ## sent us a block for ANTISNUB_INTERVAL seconds. for each of
+      ## these, we add an extra optimistic unchoking slot to our usual
+      ## NUM_OPTUNCHOKES slots. in actuality that's the number of
+      ## friends PLUS the number of optimistic unchokes who are
+      ## snubbing us, but that's not a big deal, as long as we cap the
+      ## number of extra slots at NUM_FRIENDS.
+      @num_optunchokes -= @peers.inject(0) { |s, p| s + (p.running? && p.peer_interested? && !p.choking? && (Time.now - (p.last_recv_block_time || p.start_time) > ANTISNUB_INTERVAL) ? 1 : 0) }
+      @num_optunchokes = [-NUM_FRIENDS, @num_optunchokes].max
+      rt_debug "* anti-snubbing mode, #{- at num_optunchokes} extra optimistic unchoke slots"
+    end
+
+    ## i love ruby
+    @peers.find_all { |p| p.running? }.sort_by { |p| p.start_time }.reverse.each do |p|
+      break if @num_optunchokes >= NUM_OPTUNCHOKES
+      next if p.snubbing?
+#      rt_debug "* considering #{p}: #{p.peer_interested?} and #{@num_optunchokes < NUM_OPTUNCHOKES} and #{rand(0.999) < NEW_OPTUNCHOKE_PROB}"
+      if p.peer_interested? && (rand < NEW_OPTUNCHOKE_PROB)
+        rt_debug "  #{p}: awarded optimistic unchoke"
+        p.choke = false
+        @num_optunchokes += 1
+      end
+    end
+  end
+  min_interval :calc_optunchokes, CALC_OPTUNCHOKES_INTERVAL
+
+  ## the "heartbeat". all time-based actions are triggered here.
+  def step
+    ## see if we should be in antisnubbing mode
+    if !@package.complete? && (dlrate < ANTISNUB_RATE_THRESH)
+      rt_debug "= dl rate #{dlrate} < #{ANTISNUB_RATE_THRESH}, in antisnub mode" if !@in_antisnub
+      @in_antisnub = true
+    else
+      rt_debug "= dl rate #{dlrate} >= #{ANTISNUB_RATE_THRESH}, out of antisnub mode" if @in_antisnub
+      @in_antisnub = false
+    end
+
+    ## see if we should be in fuseki mode
+    if !@package.complete? && (@package.pieces_completed < FUSEKI_PIECE_THRESH)
+      rt_debug "= num pieces #{@package.pieces_completed} < #{FUSEKI_PIECE_THRESH}, in fuseki mode" if !@in_fuseki
+      @in_fuseki = true
+    else
+      rt_debug "= num pieces #{@package.pieces_completed} >= #{FUSEKI_PIECE_THRESH}, out of fuseki mode" if @in_fuseki
+      @in_fuseki = false
+    end
+
+    ## see if we should be in endgame mode
+    if @package.complete?
+      rt_debug "= left endgame mode" if @in_endgame
+      @in_endgame = false
+    elsif (@package.pieces.length - @package.pieces_completed) <= ENDGAME_PIECE_THRESH
+      rt_debug "= have #{@package.pieces_completed} pieces, in endgame mode"
+      @in_endgame = true
+    end
+
+#     puts "  heartbeat: dlrate #{(dlrate / 1024.0).round}kb/s (lim #{(@dlratelim ? (@dlratelim / 1024.0).round : 'none')}) ulrate #{(ulrate / 1024.0).round}kb/s (lim #{(@ulratelim ? (@ulratelim / 1024.0).round : 'none')}) endgame? #@in_endgame antisnubbing? #@in_antisnub fuseki? #@in_fuseki"
+#      @package.pieces.each do |p|
+#        next if p.complete? || !p.started?
+#        l1 = 0
+#        p.each_unclaimed_block(9999999) { |b| l1 += b.length }
+#        l2 = 0
+#        p.each_empty_block(9999999) { |b| l2 += b.length }
+#        puts "  heartbeat: #{p.index}: #{l1} unclaimed bytes, #{l2} unfilled bytes"
+#      end
+
+    ## find a tracker if we aren't already connected to one
+    find_tracker if @tracker.nil?
+
+    if @package.complete? # if package is complete...
+      ## kill all peers who are complete as well, as per bram's client
+      @peers.each { |p| p.shutdown if p.peer_complete? }
+      @tracker.completed unless @tracker.nil? || @tracker.sent_completed?
+      ## reopen all files as readonly (dunno why, just seems like a
+      ## good idea)
+      @package.reopen_ro unless @package.ro?
+    end
+
+    ## kill any silent connections, and anyone who hasn't sent or
+    ## received data in a long time.
+    @peers_m.synchronize do
+      @peers.each do |p|
+        next unless p.running?
+        if ((Time.now - (p.last_send_time || p.start_time)) > SILENT_DEATH_INTERVAL)
+          rt_warning "shutting down peer #{p} for silence/boredom"
+          p.shutdown 
+        end
+      end
+    end
+
+    ## discard any dead connections
+    @peers_m.synchronize do
+      @peers.delete_if do |p|
+        !p.running? && begin
+          p.unregister_events self
+          @piece_order.dec_all p.peer_pieces                 
+          rt_debug "burying corpse of #{p}"
+          send_event(:removed_peer, p)
+          true
+        end
+      end
+    end
+
+    ## get more peers from the tracker, if all of the following are true:
+    ## a) the package is incomplete (i.e. we're downloading, not uploading)
+    ## b) we're connected to a tracker
+    ## c) we've tried all the peers we've gotten so far
+    ## d) the tracker hasn't already reported the maximum number of peers
+    if !@package.complete? && @tracker && (@tracker.peers.inject(0) { |s, p| s + (p.tried? ? 0 : 1) } == 0) && (@tracker.numwant <= @tracker.peers.length)
+      rt_debug "* getting more peers from the tracker"
+      @tracker.numwant += 50
+      unless @tracker.in_force_refresh
+        Thread.new do
+          begin
+            @tracker.force_refresh
+          rescue TrackerError
+          end
+        end
+      end
+    end
+
+    ## add peer if necessary
+    3.times { add_a_peer } # there's no place like home
+
+
+    ## iterate choking policy
+    calc_friends
+    calc_optunchokes
+
+    ## this is needed. sigh.
+    break unless @running
+
+    ## send keepalives 
+    @peers_m.synchronize { @peers.each { |p| p.send_keepalive if p.running? && p.last_send_time && ((Time.now - p.last_send_time) > KEEPALIVE_INTERVAL) } }
+
+    ## now we apportion our bandwidth amongst all the peers. we'll go
+    ## through them at random, dump everything we can, and move on iff
+    ## we don't expect to hit our bandwidth cap.
+    dllim = @dlratelim.nil? ? nil : (@dlratelim.to_f * (RATE_WINDOW.to_f + HEARTBEAT)) - (dlrate.to_f * RATE_WINDOW)
+    ullim = @ulratelim.nil? ? nil : (@ulratelim.to_f * (RATE_WINDOW.to_f + HEARTBEAT)) - (ulrate.to_f * RATE_WINDOW)
+    dl = ul = 0
+    @peers.shuffle.each do |p|
+      break if (dllim && (dl >= dllim)) || (ullim && (ul >= ullim))
+      if p.running?
+        pdl, pul = p.send_blocks_and_reqs(dllim && (dllim - dl), ullim && (ullim - ul))
+        dl += pdl
+        ul += pul
+      end
+    end
+
+    ## refresh tracker stats
+    refresh_tracker if @tracker
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/message.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/message.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/message.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,128 @@
+## message.rb -- peer wire protocol message parsing/composition
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+## we violate the users' namespaces here. but it's not in too
+## egregious of a way, and it's a royal pita to remove, so i'm keeping
+## it in for the time being.
+class String
+  def from_fbbe # four-byte big-endian integer
+    raise "fbbe must be four-byte string (got #{self.inspect})" unless length == 4
+    (self[0] << 24) + (self[1] << 16) + (self[2] << 8) + self[3]
+  end
+end
+
+class Integer
+  def to_fbbe # four-byte big-endian integer
+    raise "fbbe must be < 2^32" unless self <= 2**32
+    raise "fbbe must be >= 0" unless self >= 0
+    s = "    "
+    s[0] = (self >> 24) % 256
+    s[1] = (self >> 16) % 256
+    s[2] = (self >>  8) % 256
+    s[3] = (self      ) % 256
+    s
+  end
+end
+
+module RubyTorrent
+
+module StringExpandBits
+  include StringMapBytes
+
+  def expand_bits # just for debugging purposes
+    self.map_bytes do |b|
+      (0 .. 7).map { |i| ((b & (1 << (7 - i))) == 0 ? "0" : "1") }
+    end.flatten.join
+  end
+end
+
+class Message
+  WIRE_IDS = [:choke, :unchoke, :interested, :uninterested, :have, :bitfield,
+              :request, :piece, :cancel]
+
+  attr_accessor :id
+
+  def initialize(id, args=nil)
+    @id = id
+    @args = args
+  end
+
+  def method_missing(meth)
+    if @args.has_key? meth
+      @args[meth]
+    else
+      raise %{no such argument "#{meth}" to message #{self.to_s}}
+    end
+  end
+
+  def to_wire_form
+    case @id
+    when :keepalive
+      0.to_fbbe
+    when :choke, :unchoke, :interested, :uninterested
+      1.to_fbbe + WIRE_IDS.index(@id).chr
+    when :have
+      5.to_fbbe + 4.chr + @args[:index].to_fbbe
+    when :bitfield
+      (@args[:bitfield].length + 1).to_fbbe + 5.chr + @args[:bitfield]
+    when :request, :cancel
+      13.to_fbbe + WIRE_IDS.index(@id).chr + @args[:index].to_fbbe +
+        @args[:begin].to_fbbe + @args[:length].to_fbbe
+    when :piece
+      (@args[:length] + 9).to_fbbe + 7.chr + @args[:index].to_fbbe +
+        @args[:begin].to_fbbe
+    else
+      raise "unknown message type #{id}"
+    end
+  end
+
+  def self.from_wire_form(idnum, argstr)
+    type = WIRE_IDS[idnum]
+
+    case type
+    when :choke, :unchoke, :interested, :uninterested
+      raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.nil? or (argstr.length == 0)
+      Message.new(type)
+
+    when :have
+      raise ProtocolError, "invalid length #{str.length} for #{type} message" unless argstr.length == 4
+      Message.new(type, {:index => argstr[0,4].from_fbbe})
+      
+    when :bitfield
+      Message.new(type, {:bitfield => argstr})
+
+    when :request, :cancel
+      raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 12
+      Message.new(type, {:index => argstr[0,4].from_fbbe,
+                         :begin => argstr[4,4].from_fbbe,
+                         :length => argstr[8,4].from_fbbe})
+    when :piece
+      raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 8
+      Message.new(type, {:index => argstr[0,4].from_fbbe,
+                         :begin => argstr[4,4].from_fbbe})
+    else
+      raise "unknown message #{type.inspect}"
+    end
+  end
+
+  def to_s
+    case @id
+    when :bitfield
+      %{bitfield <#{@args[:bitfield].extend(StringExpandBits).expand_bits}>}
+    else
+      %{#@id#{@args.nil? ? "" : "(" + @args.map { |k, v| "#{k}=#{v.to_s.inspect}" }.join(", ") + ")"}}
+    end
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/metainfo.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/metainfo.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/metainfo.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,214 @@
+## metainfo.rb -- parsed .torrent file
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent/typedstruct"
+require 'uri'
+require 'open-uri'
+require 'digest/sha1'
+
+## MetaInfo file is the parsed form of the .torrent file that people
+## send around. It contains a MetaInfoInfo and possibly some
+## MetaInfoInfoFile objects.
+module RubyTorrent
+
+class MetaInfoFormatError < StandardError; end
+
+class MetaInfoInfoFile
+  def initialize(dict=nil)
+    @s = TypedStruct.new do |s|
+      s.field :length => Integer, :md5sum => String, :sha1 => String,
+              :path => String
+      s.array :path
+      s.required :length, :path
+    end
+
+    @dict = dict
+    unless dict.nil?
+      @s.parse dict
+      check
+    end
+  end
+
+  def method_missing(meth, *args)
+    @s.send(meth, *args)
+  end
+
+  def check
+    raise MetaInfoFormatError, "invalid file length" unless @s.length >= 0
+  end
+
+  def to_bencoding
+    check
+    (@dict || @s).to_bencoding
+  end
+end
+
+class MetaInfoInfo
+  def initialize(dict=nil)
+    @s = TypedStruct.new do |s|
+      s.field :length => Integer, :md5sum => String, :name => String,
+              :piece_length => Integer, :pieces => String,
+              :files => MetaInfoInfoFile, :sha1 => String
+      s.label :piece_length => "piece length"
+      s.required :name, :piece_length, :pieces
+      s.array :files
+      s.coerce :files => lambda { |x| x.map { |y| MetaInfoInfoFile.new(y) } }
+    end
+
+    @dict = dict
+    unless dict.nil?
+      @s.parse dict
+      check
+      if dict["sha1"]
+        ## this seems to always be off. don't know how it's supposed
+        ## to be calculated, so fuck it.
+#        puts "we have #{sha1.inspect}, they have #{dict['sha1'].inspect}"
+#        rt_warning "info hash SHA1 mismatch" unless dict["sha1"] == sha1
+#        raise MetaInfoFormatError, "info hash SHA1 mismatch" unless dict["sha1"] == sha1
+      end
+    end
+  end
+
+  def check
+    raise MetaInfoFormatError, "invalid file length" unless @s.length.nil? || @s.length >= 0
+    raise MetaInfoFormatError, "one (and only one) of 'length' (single-file torrent) or 'files' (multi-file torrent) must be specified" if (@s.length.nil? && @s.files.nil?) || (!@s.length.nil? && !@s.files.nil?)
+    if single?
+      length = @s.length
+    else
+      length = @s.files.inject(0) { |s, x| s + x.length }
+    end
+    raise MetaInfoFormatError, "invalid metainfo file: length #{length} > (#{@s.pieces.length / 20} pieces * #{@s.piece_length})" unless length <= (@s.pieces.length / 20) * @s.piece_length
+    raise MetaInfoFormatError, "invalid metainfo file: pieces length = #{@s.pieces.length} not a multiple of 20" unless (@s.pieces.length % 20) == 0
+  end
+
+  def to_bencoding
+    check
+    (@dict || @s).to_bencoding
+  end
+
+  def sha1
+    if @s.dirty
+      @sha1 = Digest::SHA1.digest(self.to_bencoding)
+      @s.dirty = false
+    end
+    @sha1
+  end
+
+  def single?
+    !length.nil?
+  end
+
+  def multiple?
+    length.nil?
+  end
+
+  def total_length
+    if single?
+      length
+    else
+      files.inject(0) { |a, f| a + f.length }
+    end
+  end
+
+  def num_pieces
+    pieces.length / 20
+  end
+
+  def method_missing(meth, *args)
+    @s.send(meth, *args)
+  end
+end
+
+class MetaInfo
+  def initialize(dict=nil)
+    raise TypeError, "argument must be a Hash (maybe see MetaInfo.from_location)" unless dict.is_a? Hash
+    @s = TypedStruct.new do |s|
+      s.field :info => MetaInfoInfo, :announce => URI::HTTP,
+              :announce_list => Array, :creation_date => Time,
+              :comment => String, :created_by => String, :encoding => String
+      s.label :announce_list => "announce-list", :creation_date => "creation date",
+              :created_by => "created by"
+      s.array :announce_list
+      s.coerce :info => lambda { |x| MetaInfoInfo.new(x) },
+               :creation_date => lambda { |x| Time.at(x) },
+               :announce => lambda { |x| URI.parse(x) },
+               :announce_list => lambda { |x| x.map { |y| y.map { |z| URI.parse(z) } } }
+    end
+
+    @dict = dict
+    unless dict.nil?
+      @s.parse dict
+      check
+    end
+  end
+
+  def single?; info.single?; end
+  def multiple?; info.multiple?; end
+
+  def check
+    if @s.announce_list
+      @s.announce_list.each do |tier|
+        tier.each { |track| raise MetaInfoFormatError, "expecting HTTP URL in announce-list, got #{track} instead" unless track.is_a? URI::HTTP }
+      end
+    end
+  end
+
+  def self.from_bstream(bs)
+    dict = nil
+    bs.each do |e|
+      if dict == nil
+        dict = e
+      else
+        raise MetaInfoFormatError, "too many bencoded elements for metainfo file (just need one)"
+      end
+    end
+
+    raise MetaInfoFormatError, "bencoded element must be a dictionary, got a #{dict.class}" unless dict.kind_of? ::Hash
+
+    MetaInfo.new(dict)
+  end
+
+  ## either a filename or a URL
+  def self.from_location(fn, http_proxy=ENV["http_proxy"])
+    if http_proxy # lame!
+      open(fn, "rb", :proxy => http_proxy) { |f| from_bstream(BStream.new(f)) }
+    else
+      open(fn, "rb") { |f| from_bstream(BStream.new(f)) }
+    end
+  end
+
+  def self.from_stream(s)
+    from_bstream(BStream.new(s))
+  end
+
+  def method_missing(meth, *args)
+    @s.send(meth, *args)
+  end
+
+  def to_bencoding
+    check
+    (@dict || @s).to_bencoding
+  end
+
+  def trackers
+    if announce_list && (announce_list.length > 0)
+      announce_list.map do |tier|
+        tier.extend(ArrayShuffle).shuffle
+      end.flatten
+    else
+      [announce]
+    end
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/package.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/package.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/package.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,595 @@
+## package.rb -- RubyTorrent <=> filesystem interface.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'thread'
+require 'digest/sha1'
+
+## A Package is the connection between the network and the
+## filesystem. There is one Package per torrent. Each Package is
+## composed of one or more Pieces, as determined by the MetaInfoInfo
+## object, and each Piece is composed of one or more Blocks, which are
+## transmitted over the PeerConnection with :piece comments.
+
+module RubyTorrent
+
+## Range plus a lot of utility methods
+class AwesomeRange < Range
+  def initialize(start, endd=nil, exclude_end=false)
+    case start
+    when Integer
+      raise ArgumentError, "both start and endd must be specified" if endd.nil?
+      super(start, endd, exclude_end)
+    when Range
+      super(start.first, start.last, start.exclude_end?)
+    else
+      raise ArgumentError, "start should be an Integer or a Range, is a #{start.class}"
+    end
+  end
+
+  ## range super-set: does this range encompass 'o'?
+  def rss?(o)
+    (first <= o.first) &&
+      ((last > o.last) || (o.exclude_end? && (last == o.last)))
+  end
+
+  ## range intersection
+  def rint(o)
+    ## three cases. either:
+    ## a) our left endpoint is within o
+    if ((first >= o.first) &&
+      ((first < o.last) || (!o.exclude_end? && (first == o.last))))
+      if last < o.last
+        AwesomeRange.new(first, last, exclude_end?)
+      elsif last > o.last
+        AwesomeRange.new(first, o.last, o.exclude_end?)
+      else # ==
+        AwesomeRange.new(first, last, exclude_end? || o.exclude_end?)
+      end
+    ## b) our right endpoint is within o
+    elsif (((last > o.first) || (!exclude_end? && (last == o.first))) && ((last < o.last) || (!o.exclude_end? && (last == o.last))))
+      AwesomeRange.new([first, o.first].max, last, exclude_end?)
+    ## c) we encompass o
+    elsif rss?(o)
+      o
+    else
+      nil
+    end
+  end
+  
+  ## range continuity
+  def rcont?(o)
+    (first == o.last) || (last == o.first) || (rint(o) != nil)
+  end
+
+  ## range union: only valid for continuous ranges
+  def runion(o)
+    if last > o.last
+      AwesomeRange.new([first, o.first].min, last, exclude_end?)
+    elsif o.last > last
+      AwesomeRange.new([first, o.first].min, o.last, o.exclude_end?)
+    else # equal
+      AwesomeRange.new([first, o.first].min, last, (exclude_end? && o.exclude_end?))
+    end
+  end
+
+  ## range difference. returns an array of 0, 1 or 2 ranges.
+  def rdiff(o)
+    return [] if o == self
+    ret = []
+    int = rint o
+    return [] if int == self
+    return [self] if int == nil
+    raise RangeError, "can't subtract a range that doesn't have an exclusive end" unless int.exclude_end?
+    if int.first > first
+      ret << AwesomeRange.new(first, int.first, true)
+    end
+    ret + [AwesomeRange.new(int.last, last, exclude_end?)]
+  end
+end
+
+## a Covering is a set of non-overlapping ranges within a given start
+## point and endpoint.
+class Covering
+  attr_accessor :domain, :ranges
+
+  ## 'domain' should be an AwesomeRange determining the start and end
+  ## point. 'ranges' should be an array of non-overlapping
+  ## AwesomeRanges sorted by start point.
+  def initialize(domain, ranges=[])
+    @domain = domain
+    @ranges = ranges
+  end
+
+  def complete!; @ranges = [@domain]; self; end
+  def complete?; @ranges == [@domain]; end
+  def empty!; @ranges = []; self; end
+  def empty?; @ranges == []; end
+
+  ## given a covering of size N and a new range 'r', returns a
+  ## covering of size 0 <= s <= N + 1 that doesn't cover the range
+  ## given by 'r'.
+  def poke(r)
+    raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r
+    Covering.new(@domain, @ranges.inject([]) do |set, x|
+      if x.rint(r) != nil
+        set + x.rdiff(r)
+      else
+        set + [x]
+      end
+    end)
+  end
+
+  ## given a covering of size N and a new range 'r', returns a
+  ## covering of size 0 < s <= N + 1 that also covers the range 'r'.
+  def fill(r)
+    raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r
+    Covering.new(@domain, @ranges.inject([]) do |set, x|
+      ## r contains the result of the continuing merge. if r is nil,
+      ## then we've already added it, so we just copy x.
+      if r.nil? then set + [x] else
+        ## otoh, if r is there, we try and merge in the current
+        ## element.
+        if r.rcont? x
+          ## if we can merge, keep the union in r and don't add
+          ## anything
+          r = r.runion x
+          set
+        ## if we can't merge it, we'll see if it's time to add it. we
+        ## know that r and x don't overlap because r.mergable?(x) was
+        ## false, so we can simply compare the start points to see
+        ## whether it should come before x.
+        elsif r.first < x.first
+          s = set + [r, x] # add both 
+          r = nil
+          s
+        else set + [x] ## no merging or adding, so we just copy x.
+        end
+      end
+    ## if 'r' still hasn't been added, it should be the last element,
+    ## we add it here.
+    end.push(r).compact)
+  end
+
+  ## given an array of non-overlapping ranges sorted by start point,
+  ## and a range 'domain', returns the first range from 'domain' not
+  ## covered by any range in the array.
+  def first_gap(domain=@domain)
+    start = domain.first
+    endd = nil
+    excl = nil
+    @ranges.each do |r|
+      next if r.last < start
+
+      if r.first > start # found a gap
+        if r.first < domain.last
+          return AwesomeRange.new(start, r.first, false)
+        else # r.first >= domain.last, so use domain's exclusion
+          return AwesomeRange.new(start, domain.last, domain.exclude_end?)
+        end
+      else # r.first <= start
+        start = r.last unless r.last < start
+        break if start > domain.last
+      end
+    end
+
+    if (start >= domain.last)
+      ## entire domain was covered
+      nil
+    else
+      ## tail end of the domain uncovered
+      AwesomeRange.new(start, domain.last, domain.exclude_end?)
+    end
+  end
+
+  def ==(o); o.domain == self.domain && o.ranges == self.ranges; end
+end
+
+## Blocks are very simple chunks of data which exist solely in
+## memory. they are the basic currency of the bittorrent protocol. a
+## Block can be divided into "chunks" (no intelligence there; it's
+## solely for the purposes of buffered reading/writing) and one or
+## more Blocks comprises a Piece.
+class Block
+  attr_accessor :pindex, :begin, :length, :data, :requested
+
+  def initialize(pindex, beginn, length)
+    @pindex = pindex
+    @begin = beginn
+    @length = length
+    @data = nil
+    @requested = false
+    @time = nil
+  end
+
+  def requested?; @requested; end
+  def have_length; @data.length; end
+  def complete?; @data && (@data.length == @length); end
+  def mark_time; @time = Time.now; end
+  def time_elapsed; Time.now - @time; end
+
+  def to_s
+    "<block: p[#{@pindex}], #@begin + #@length #{(data.nil? || (data.length == 0) ? 'emp' : (complete? ? 'cmp' : 'inc'))}>"
+  end
+
+  ## chunk can only be added to blocks in order
+  def add_chunk(chunk)
+    @data = "" if @data.nil?
+    raise "adding chunk would result in too much data (#{@data.length} + #{chunk.length} > #@length)" if (@data.length + chunk.length) > @length
+    @data += chunk
+    self
+  end
+
+  def each_chunk(blocksize)
+    raise "each_chunk called on incomplete block" unless complete?
+    start = 0
+    while(start < @length)
+      yield data[start, [blocksize, @length - start].min]
+      start += blocksize
+    end
+  end
+
+  def ==(o)
+    o.is_a?(Block) && (o.pindex == self.pindex) && (o.begin == self.begin) &&
+      (o.length == self.length)
+  end
+end
+
+## a Piece is the basic unit of the .torrent metainfo file (though not
+## of the bittorrent protocol). Pieces store their data directly on
+## disk, so many operations here will be slow. each Piece stores data
+## in one or more file pointers.
+##
+## unlike Blocks and Packages, which are either complete or
+## incomplete, a Piece can be complete but not valid, if the SHA1
+## check fails. thus, a call to piece.complete? is not sufficient to
+## determine whether the data is ok to use or not.
+##
+## Pieces handle all the trickiness involved with Blocks: taking in
+## Blocks from arbitrary locations, writing them out to the correct
+## set of file pointers, keeping track of which sections of the data
+## have been filled, claimed but not filled, etc.
+class Piece
+  include EventSource
+
+  attr_reader :index, :start, :length
+  event :complete
+
+  def initialize(index, sha1, start, length, files, validity_assumption=nil)
+    @index = index
+    @sha1 = sha1
+    @start = start
+    @length = length
+    @files = files # array of [file pointer, mutex, file length]
+    @valid = nil
+
+    ## calculate where we start and end in terms of the file pointers.
+    @start_index = 0
+    sum = 0
+    while(sum + @files[@start_index][2] <= @start)
+      sum += @files[@start_index][2]
+      @start_index += 1
+    end
+    ## now sum + @files[@start_index][2] > start, and sum <= start
+    @start_offset = @start - sum
+
+    ## sections of the data we have
+    @have = Covering.new(AwesomeRange.new(0 ... @length)).complete!
+    @valid = validity_assumption
+    @have.empty! unless valid?
+
+    ## sections of the data someone has laid claim to but hasn't yet
+    ## provided. a super-set of @have.
+    @claimed = Covering.new(AwesomeRange.new(0 ... @length))
+
+    ## protects @claimed, @have
+    @state_m = Mutex.new
+  end
+
+  def to_s
+    "<piece #@index: #@start + #@length #{(complete? ? 'cmp' : 'inc')}>"
+  end
+
+  def complete?; @have.complete?; end
+  def started?; !@claimed.empty? || !@have.empty?; end
+
+  def discard # discard all data
+    @state_m.synchronize do
+      @have.empty!
+      @claimed.empty!
+    end
+    @valid = false
+  end
+
+  def valid?
+    return @valid unless @valid.nil?
+    return (@valid = false) unless complete?
+
+    data = read_bytes(0, @length)
+    if (data.length != @length)
+      @valid = false
+    else
+      @valid = (Digest::SHA1.digest(data) == @sha1)
+    end
+  end
+
+  def unclaimed_bytes
+    r = 0
+    each_gap(@claimed) { |start, len| r += len }
+    r
+  end
+
+  def empty_bytes
+    r = 0
+    each_gap(@have) { |start, len| r += len }
+    r
+  end
+
+  def percent_claimed; 100.0 * (@length.to_f - unclaimed_bytes) / @length; end
+  def percent_done; 100.0 * (@length.to_f - empty_bytes) / @length; end
+
+  def each_unclaimed_block(max_length)
+    raise "no unclaimed blocks in a complete piece" if complete?
+
+    each_gap(@claimed, max_length) do |start, len|
+      yield Block.new(@index, start, len)
+    end
+  end
+
+  def each_empty_block(max_length)
+    raise "no empty blocks in a complete piece" if complete?
+
+    each_gap(@have, max_length) do |start, len|
+      yield Block.new(@index, start, len)
+    end
+  end
+
+  def claim_block(b)
+    @state_m.synchronize do
+      @claimed = @claimed.fill AwesomeRange.new(b.begin ... (b.begin + b.length))
+    end
+  end
+
+  def unclaim_block(b)
+    @state_m.synchronize do
+      @claimed = @claimed.poke AwesomeRange.new(b.begin ... (b.begin + b.length))
+    end
+  end
+
+  ## for a complete Piece, returns a complete Block of specified size
+  ## and location.
+  def get_complete_block(beginn, length)
+    raise "can't make block from incomplete piece" unless complete?
+    raise "invalid parameters #{beginn}, #{length}" unless (length > 0) && (beginn + length) <= @length
+
+    b = Block.new(@index, beginn, length)
+    b.add_chunk read_bytes(beginn, length) # returns b
+  end
+
+  ## we don't do any checking that this block has been claimed or not.
+  def add_block(b)
+    @valid = nil
+    write = false
+    new_have = @state_m.synchronize { @have.fill AwesomeRange.new(b.begin ... (b.begin + b.length)) }
+    if new_have != @have
+      @have = new_have
+      write = true
+    end
+    
+    write_bytes(b.begin, b.data) if write
+    send_event(:complete) if complete?
+  end
+
+  private
+
+  ## yields successive gaps from 'array' between 0 and @length
+  def each_gap(covering, max_length=nil)
+    return if covering.complete?
+
+    range_first = 0
+    while true
+      range = covering.first_gap(range_first ... @length)
+      break if range.nil? || (range.first == range.last)
+      start = range.first
+
+      while start < range.last
+        len = range.last - start
+        len = max_length if max_length && (max_length < len)
+        yield start, len
+        start += len
+      end
+
+      range_first = range.last
+    end
+  end
+
+  def write_bytes(start, data); do_bytes(start, 0, data); end
+  def read_bytes(start, length); do_bytes(start, length, nil); end
+
+  ## do the dirty work of splitting the read/writes across multiple
+  ## file pointers to possibly incomplete, possibly overcomplete files
+  def do_bytes(start, length, data)
+    raise ArgumentError, "invalid start" if (start < 0) || (start > @length)
+#    raise "invalid length" if (length < 0) || (start + length > @length)
+
+    start += @start_offset
+    index = @start_index
+    sum = 0
+    while(sum + @files[index][2] <= start)
+      sum += @files[index][2]
+      index += 1
+    end
+    offset = start - sum
+
+    done = 0
+    abort = false
+    if data.nil?
+      want = length
+      ret = ""
+    else
+      want = data.length
+      ret = 0
+    end
+    while (done < want) && !abort
+      break if index > @files.length
+      fp, mutex, size = @files[index]
+      mutex.synchronize do
+        fp.seek offset
+        here = [want - done, size - offset].min
+        if data.nil?
+#          puts "> reading #{here} bytes from #{index} at #{offset}"
+          s = fp.read here
+#          puts "> got #{(s.nil? ? s.inspect : s.length)} bytes"
+          if s.nil?
+            abort = true
+          else
+            ret += s
+            abort = true if s.length < here
+#            puts "fp.tell is #{fp.tell}, size is #{size}, eof #{fp.eof?}"
+            if (fp.tell == size) && !fp.eof?
+              rt_warning "file #{index}: not at eof after #{size} bytes, truncating"
+              fp.truncate(size - 1)
+            end
+          end
+        else
+#          puts "> writing #{here} bytes to #{index} at #{offset}"
+          x = fp.write data[done, here]
+          ret += here
+#          @files[index][0].flush
+        end
+        done += here
+      end
+      index += 1
+      offset = 0
+    end
+
+    ret
+  end
+end
+
+## finally, the Package. one Package per Controller so we don't do any
+## thread safety stuff in here.
+class Package
+  include EventSource
+
+  attr_reader :pieces, :size
+  event :complete
+
+  def initialize(metainfo, out=nil, validity_assumption=nil)
+    info = metainfo.info
+
+    created = false
+    out ||= info.name
+    case out
+    when File
+      raise ArgumentError, "'out' cannot be a File for a multi-file .torrent" if info.multiple?
+      fstream = out
+    when Dir
+      raise ArgumentError, "'out' cannot be a Dir for a single-file .torrent" if info.single?
+      fstream = out
+    when String
+      if info.single?
+        rt_debug "output file is #{out}"
+        begin
+          fstream = File.open(out, "rb+")
+        rescue Errno::ENOENT
+	  created = true
+          fstream = File.open(out, "wb+")
+        end
+      else
+        rt_debug "output directory is #{out}"
+        unless File.exists? out
+          Dir.mkdir(out)
+          created = true
+        end
+        fstream = Dir.open(out)
+      end
+    else
+      raise ArgumentError, "'out' should be a File, Dir or String object, is #{out.class}"
+    end
+
+    @ro = false
+    @size = info.total_length
+    if info.single?
+      @files = [[fstream, Mutex.new, info.length]]
+    else
+      @files = info.files.map do |finfo|
+        path = File.join(finfo.path[0, finfo.path.length - 1].inject(fstream.path) do |path, el|
+          dir = File.join(path, el)
+          unless File.exist? dir
+            rt_debug "making directory #{dir}"
+            Dir.mkdir dir
+          end
+          dir
+        end, finfo.path[finfo.path.length - 1])
+        rt_debug "opening #{path}..."
+        [open_file(path), Mutex.new, finfo.length]
+      end
+    end
+
+    i = 0
+    @pieces = info.pieces.unpack("a20" * (info.pieces.length / 20)).map do |hash|
+      start = (info.piece_length * i)
+      len = [info.piece_length, @size - start].min
+      p = Piece.new(i, hash, start, len, @files, (created ? false : validity_assumption))
+      p.on_event(self, :complete) { send_event(:complete) if complete? }
+      yield p if block_given?
+      (i += 1) && p
+    end
+
+    reopen_ro if complete?
+  end
+
+  def ro?; @ro; end
+  def reopen_ro
+    raise "called on incomplete package" unless complete?
+    return if @ro
+    
+    rt_debug "reopening all files with mode r"
+    @files = @files.map do |fp, mutex, size|
+      [fp.reopen(fp.path, "rb"), mutex, size]
+    end
+    @ro = true
+  end
+
+  def complete?; @pieces.detect { |p| !p.complete? || !p.valid? } == nil; end
+
+  def bytes_completed
+    @pieces.inject(0) { |s, p| s + (p.complete? ? p.length : 0) }
+  end
+
+  def pieces_completed
+    @pieces.inject(0) { |s, p| s + (p.complete? ? 1 : 0) }
+  end
+
+  def percent_completed
+    100.0 * pieces_completed.to_f / @pieces.length.to_f
+  end
+
+  def num_pieces; @pieces.length; end
+
+  def to_s
+    "<#{self.class} size #@size>"
+  end
+
+  private
+
+  def open_file(path)
+    begin
+      File.open(path, "rb+")
+    rescue Errno::ENOENT
+      File.open(path, "wb+")
+    end
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/peer.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/peer.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/peer.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,536 @@
+## peer.rb -- bitttorrent peer ("wire") protocol.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'socket'
+require 'thread'
+require "rubytorrent/message"
+
+module RubyTorrent
+
+module ArrayToBitstring
+  def to_bitstring
+    ret = "\0"
+    bit = 7
+    map do |b|
+      if bit == -1
+        ret += "\0"
+        bit = 7
+      end
+      ret[ret.length - 1] |= (1 << bit) if b
+      bit -= 1
+    end
+    ret
+  end
+end
+
+module ArrayDelete2
+  ## just like delete but returns the *array* element deleted rather
+  ## than the argument. someone should file an rcr.
+  def delete2(el)
+    i = index el
+    unless i.nil?
+      ret = self[i]
+      delete_at i
+      ret
+    else
+      nil
+    end
+  end
+end
+
+module StringToBarray
+  include StringMapBytes
+  def to_barray
+    self.map_bytes do |b|
+      (0 .. 7).map { |i| (b & (1 << (7 - i))) != 0 }
+    end.flatten
+  end
+end
+
+## estimate a rate. basically copied from bram's code.
+class RateMeter
+  attr_reader :amt
+
+  def initialize(window=20)
+    @window = window.to_f
+    @amt = 0
+    @rate = 0
+    @last = @since = Time.now - 1
+    @m = Mutex.new
+  end
+
+  def add(new_amt)
+    now = Time.now
+    @m.synchronize do
+      @amt += new_amt
+      @rate = ((@rate * (@last - @since)) + new_amt).to_f / (now - @since)
+      @last = now
+      @since = [@since, now - @window].max
+    end
+  end
+
+  def rate
+    (@rate * (@last - @since)).to_f / (Time.now - @since)
+  end
+
+  def bytes_until(new_rate)
+    [(new_rate.to_f * (Time.now - @since)) - (@rate * (@last - @since)), 0].max
+  end
+end
+
+class ProtocolError < StandardError; end
+
+## The PeerConnection object deals with all the protocol issues. It
+## keeps state information as to the connection and the peer. It is
+## tightly integrated with the Controller object.
+##
+## Remember to be "strict in what you send, lenient in what you
+## accept".
+class PeerConnection
+  extend AttrReaderQ
+  include EventSource
+
+  attr_reader :peer_pieces, :name
+  attr_reader_q :running, :choking, :interested, :peer_choking,
+                :peer_interested, :snubbing
+  event :peer_has_piece, :peer_has_pieces, :received_block, :sent_block,
+        :requested_block
+
+  BUFSIZE = 8192
+  MAX_PEER_REQUESTS = 5 # how many peer requests to keep queued
+  MAX_REQUESTS = 5 # how many requests for blocks to keep current
+  MIN_REQUESTS = 1 # get more blocks from controller when this limit is reached
+  REQUEST_TIMEOUT = 60 # number of seconds after sending a request before we
+                       # decide it's been forgotten
+
+  def initialize(name, controller, socket, package)
+    @name = name
+    @controller = controller
+    @socket = socket
+    @package = package
+    @running = false
+
+    ## my state
+    @want_blocks = [].extend(ArrayDelete2) # blocks i want
+    @want_blocks_m = Mutex.new
+    @choking = true
+    @interested = false
+    @snubbing = false
+
+    ## peer's state
+    @peer_want_blocks = [].extend(ArrayDelete2)
+    @peer_choking = true # assumption of initial condition
+    @peer_interested = false # ditto
+    @peer_pieces = Array.new(@package.num_pieces, false) # ditto
+    @peer_virgin = true # does the peer have any pieces at all?
+
+    ## connection stats
+    @dlmeter = RateMeter.new
+    @ulmeter = RateMeter.new
+
+    @send_q = Queue.new # output thread takes messages from here and
+                        # puts them on the wire
+  end
+
+  def pending_recv; @want_blocks.find_all { |b| b.requested? }.length; end
+  def pending_send; @peer_want_blocks.length; end
+
+  def start
+    @running = true
+    @time = {:start => Time.now}
+
+    Thread.new do # start input thread
+      begin
+        while @running; input_thread_step; end
+      rescue SystemCallError, IOError, ProtocolError => e
+        rt_debug "#{self} (input): #{e.message}, releasing #{@want_blocks.length} claimed blocks and dying"
+#        rt_debug e.backtrace.join("\n")
+        @running = false
+        @controller.forget_blocks @want_blocks
+      end
+    end
+
+    Thread.new do # start output thread
+      begin
+        while @running; output_thread_step; end
+      rescue SystemCallError, IOError, ProtocolError => e
+        rt_debug "#{self} (output): #{e.message}, releasing #{@want_blocks.length} claimed blocks and dying"
+#        rt_debug e.backtrace.join("\n")
+        @running = false
+        @controller.forget_blocks @want_blocks
+      end
+    end
+
+    ## queue the initial messages
+    queue_message(:bitfield, {:bitfield => @package.pieces.map { |p| p.complete? }.extend(ArrayToBitstring).to_bitstring})
+
+    ## and that's it. if peer sends a bitfield, we'll send an
+    ## interested and start requesting blocks at that point.  if they
+    ## don't, it means they don't have any pieces, so we can just sit
+    ## tight.
+    self
+  end
+
+  ## the Controller calls this from heartbeat thread to tell us
+  ## whether to choke or not.
+  def choke=(now_choke)
+    queue_message(now_choke ? :choke : :unchoke) unless @choking == now_choke
+    @choking = now_choke
+  end
+
+  ## the Controller calls this from heartbeat thread to tell us
+  ## whether to snub or not.
+  def snub=(now_snub)
+    unless @snubbing = now_snub
+      @snubbing = now_snub
+      choke = true if @snubbing
+    end
+  end
+
+  def peer_complete?; @peer_pieces.all?; end
+  def last_send_time; @time[:send]; end
+  def last_recv_time; @time[:recv]; end
+  def last_send_block_time; @time[:send_block]; end
+  def last_recv_block_time; @time[:recv_block]; end
+  def start_time; @time[:start]; end
+  def dlrate; @dlmeter.rate; end
+  def ulrate; @ulmeter.rate; end
+  def dlamt; @dlmeter.amt; end
+  def ulamt; @ulmeter.amt; end
+  def piece_available?(index); @peer_pieces[index]; end
+  def to_s; "<peer: #@name>"; end
+
+  ## called by Controller in the event that a request needs to be
+  ## rescinded.
+  def cancel(block)
+    wblock = @want_blocks_m.synchronize { @want_blocks.delete2 block }
+    unless wblock.nil? || !wblock.requested?
+      rt_debug "#{self}: sending cancel for #{wblock}"
+      queue_message(:cancel, {:index => wblock.pindex, :begin => wblock.begin,
+                              :length => wblock.length})
+    end
+    get_want_blocks unless wblock.nil?
+  end
+
+  def shutdown
+    rt_debug "#{self.to_s}: shutting down"
+    @running = false
+    @socket.close rescue nil
+  end
+
+  ## Controller calls this to tell us that a complete piece has been
+  ## received.
+  def have_piece(piece)
+    queue_message(:have, {:index => piece.index})
+  end
+
+  ## Controller calls this to tell us to send a keepalive
+  def send_keepalive
+#    rt_debug "* sending keepalive!"
+    queue_message(:keepalive)
+  end
+
+  ## this is called both by input_thread_step and by the controller's
+  ## heartbeat thread. it sends as many pending blocks as it can while
+  ## keeping the amount below 'ullim', and sends as many requests as
+  ## it can while keeping the amount below 'dllim'.
+  ## 
+  ## returns the number of bytes requested and sent
+  def send_blocks_and_reqs(dllim=nil, ullim=nil)
+    sent_bytes = 0
+    reqd_bytes = 0
+
+    @want_blocks_m.synchronize do
+      @want_blocks.each do |b|
+#        puts "[][] #{self}: #{b} is #{b.requested? ? 'requested' : 'NOT requested'} and has time_elapsed of #{b.requested? ? b.time_elapsed.round : 'n/a'}s"
+        if b.requested? && (b.time_elapsed > REQUEST_TIMEOUT)
+          rt_warning "#{self}: for block #{b}, time elapsed since request is #{b.time_elapsed} > #{REQUEST_TIMEOUT}, assuming peer forgot about it"
+          @want_blocks.delete b
+          @controller.forget_blocks [b]
+        end
+      end
+    end
+
+    ## send :requests
+    unless @peer_choking || !@interested
+      @want_blocks_m.synchronize do
+        @want_blocks.each do |b|
+          break if dllim && (reqd_bytes >= dllim)
+          next if b.requested?
+          
+          if @package.pieces[b.pindex].complete?
+            # not sure that this will ever happen, but...
+            rt_warning "#{self}: deleting scheduled block for already-complete piece #{b}"
+            @want_blocks.delete b
+            next
+          end
+
+          queue_message(:request, {:index => b.pindex, :begin => b.begin,
+                                   :length => b.length})
+          reqd_bytes += b.length
+          b.requested = true
+          b.mark_time
+          send_event(:requested_block, b)
+        end
+      end
+    end
+
+    ## send blocks
+#    rt_debug "sending blocks. choking? #@choking, choked? #@peer_choking, ul rate #{ulrate}b/s, limit #@ulmeterlim" unless @peer_want_blocks.empty?
+    unless @choking || !@peer_interested
+      while !@peer_want_blocks.empty?
+        break if ullim && (sent_bytes >= ullim)
+        if (b = @peer_want_blocks.shift)
+          sent_bytes += b.length
+          @send_q.push b
+          @time[:send_block] = Time.now
+          send_event(:sent_block, b)
+        end
+      end
+    end
+
+    get_want_blocks
+
+    [reqd_bytes, sent_bytes]
+  end
+
+  private
+
+  ## re-calculate whether we're interested or not. triggered by
+  ## received :have and :bitfield messages.
+  def recalc_interested
+    show_interest = !@peer_virgin || (@package.pieces.detect do |p|
+      !p.complete? && @peer_pieces[p.index]
+    end) != nil
+
+    queue_message(show_interest ? :interested : :uninterested) unless show_interest == @interested
+    if ((@interested = show_interest) == false)
+      @want_blocks_m.synchronize do
+        @controller.forget_blocks @want_blocks
+        @want_blocks.clear
+      end
+    end
+  end
+
+  ## take a message/block from the send_q and place it on the wire. blocking.
+  def output_thread_step
+    obj = @send_q.deq
+    case obj
+    when Message
+#      rt_debug "output: sending message #{obj}" + (obj.id == :request ? " (request queue size #{@want_blocks.length})" : "")
+      send_bytes obj.to_wire_form
+      @time[:send] = Time.now
+    when Block
+#      rt_debug "output: sending block #{obj}"
+      send_bytes Message.new(:piece, {:length => obj.length, :index => obj.pindex, :begin => obj.begin}).to_wire_form
+      obj.each_chunk(BUFSIZE) { |c| send_bytes c }
+      @time[:send] = Time.now
+      @ulmeter.add obj.length
+#      rt_debug "sent block #{obj} ul rate now #{(ulrate / 1024.0).round}kb/s"
+    else
+      raise "don't know what to do with #{obj}"
+    end
+  end
+
+  ## take bits from the wire and respond to them. blocking.
+  def input_thread_step
+    case (obj = read_from_wire)
+    when Block
+      handle_block obj
+    when Message
+      handle_message obj
+    else
+      raise "don't know what to do with #{obj.inspect}"
+    end
+
+    ## to enable immediate response, if there are no rate limits,
+    ## we'll send the blocks and reqs right here. otherwise, the
+    ## controller will call this at intervals.
+    send_blocks_and_reqs if @controller.dlratelim.nil? && @controller.ulratelim.nil?
+  end
+
+  ## take bits from the wire and make a message/block out of them. blocking.
+  def read_from_wire
+    len = nil
+    while (len = recv_bytes(4).from_fbbe) == 0
+      @time[:recv] = Time.now
+#      rt_debug "* hey, a keepalive!"
+    end
+
+    id = recv_bytes(1)[0]
+
+    if Message::WIRE_IDS[id] == :piece # add a block
+      len -= 9
+      m = Message.from_wire_form(id, recv_bytes(8))
+      b = Block.new(m.index, m.begin, len)
+      while len > 0
+        thislen = [BUFSIZE, len].min
+        b.add_chunk recv_bytes(thislen)
+        len -= thislen
+      end
+      @time[:recv] = @time[:recv_block] = Time.now
+      b
+    else # add a message
+      m = Message.from_wire_form(id, recv_bytes(len - 1))
+#      rt_debug "input: read message #{m}"
+      @time[:recv] = Time.now
+      m
+    end
+  end
+
+  def handle_block(block)
+    wblock = @want_blocks_m.synchronize { @want_blocks.delete2 block }
+
+    return rt_warning("#{self}: peer sent unrequested (possibly cancelled) block #{block}") if wblock.nil? || !wblock.requested?
+
+    @dlmeter.add block.have_length
+#    rt_debug "received block #{block}, dl rate now #{(dlrate / 1024.0).round}kb/s"
+
+    piece = @package.pieces[block.pindex] # find corresponding piece
+    piece.add_block block
+    send_event(:received_block, block)
+    get_want_blocks
+  end
+
+  def send_bytes(s)
+    if s.nil?
+      raise "can't send nil"
+    elsif s.length > 0
+      @socket.send(s, 0)
+    end
+  end
+
+  def recv_bytes(len)
+    if len < 0
+      raise "can't recv negative bytes"
+    elsif len == 0
+      ""
+    elsif len > 512 * 1024 # 512k
+      raise ProtocolError, "read size too big."
+    else
+      r = ""
+      zeros = 0
+      while r.length < len
+        x = @socket.recv(len - r.length)
+        raise IOError, "zero bytes received" if x.length == 0
+        r += x
+      end
+      r
+    end
+  end
+
+  def handle_message(m)
+    case m.id
+    when :choke
+#      rt_debug "#{self}: peer choking (was #{@peer_choking})"
+      @peer_choking = true
+      @want_blocks_m.synchronize do
+        @controller.forget_blocks @want_blocks
+        @want_blocks.clear
+      end
+
+    when :unchoke
+#      rt_debug "#{self}: peer not choking (was #{@peer_choking})"
+      @peer_choking = false
+
+    when :interested
+#      rt_debug "peer interested (was #{@peer_interested})"
+      @peer_interested = true
+
+    when :uninterested
+#      rt_debug "peer not interested (was #{@peer_interested})"
+      @peer_interested = false
+      
+    when :have
+#      rt_debug "peer has piece #{m.index}"
+      rt_warning "#{self}: peer already has piece #{m.index}" if @peer_pieces[m.index]
+      @peer_pieces[m.index] = true
+      @peer_virgin = false
+      send_event(:peer_has_piece, m)
+      recalc_interested
+
+    when :bitfield
+#      rt_debug "peer reports bitfield #{m.bitfield.inspect}"
+      barray = m.bitfield.extend(StringToBarray).to_barray
+
+      expected_pieces = @package.num_pieces - (@package.num_pieces % 8) + ((@package.num_pieces % 8) == 0 ? 0 : 8)
+      raise ProtocolError, "invalid length in bitfield message (package has #{@package.num_pieces} pieces; bitfield should be size #{expected_pieces} but is #{barray.length} pieces)" unless barray.length == expected_pieces
+
+      @peer_pieces.each_index { |i| @peer_pieces[i] = barray[i] }
+      @peer_virgin = false
+      send_event(:peer_has_pieces, barray)
+      recalc_interested
+      get_want_blocks
+
+    when :request
+      return rt_warning("#{self}: peer requests invalid piece #{m.index}") unless m.index < @package.num_pieces
+      return rt_warning("#{self}: peer requests a block but we're choking") if @choking
+      return rt_warning("#{self}: peer requests a block but isn't interested") unless @peer_interested
+      return rt_warning("#{self}: peer requested too many blocks, ignoring") if @peer_want_blocks.length > MAX_PEER_REQUESTS
+
+      piece = @package.pieces[m.index]
+      return rt_warning("#{self}: peer requests unavailable block from piece #{piece}") unless piece.complete?
+
+      @peer_want_blocks.push piece.get_complete_block(m.begin, m.length)
+
+    when :piece
+      raise "can't handle piece here"
+
+    when :cancel
+      b = Block.new(m.index, m.begin, m.length)
+#      rt_debug "peer cancels #{b}"
+      if @peer_want_blocks.delete2(b) == nil
+        rt_warning "#{self}: peer wants to cancel unrequested block #{b}"
+      end
+
+    else
+      raise "unknown message #{type}"
+    end
+  end
+
+  ## queues a message for delivery. (for :piece messages, this
+  ## transmits everything but the piece itself)
+  def queue_message(id, args=nil)
+    @send_q.push Message.new(id, args)
+  end
+
+  ## talks to Controller and get some new blocks to request. could be
+  ## slow. this is presumably called whenever the queue of requests is
+  ## too small.
+  def get_want_blocks
+    return if (@want_blocks.length >= MIN_REQUESTS) || @peer_virgin || @peer_choking || !@interested
+
+    rej_count = 0
+    acc_count = 0
+    @controller.claim_blocks do |b|
+      break if @want_blocks.length >= MAX_REQUESTS
+      if @peer_pieces[b.pindex] && !@want_blocks.member?(b)
+        rt_debug "! #{self}: starting new piece #{@package.pieces[b.pindex]}" unless @package.pieces[b.pindex].started?
+
+#        rt_debug "#{self}: added to queue block #{b}"
+#        puts "#{self}: claimed block #{b}"
+        @want_blocks.push b
+        acc_count += 1
+        true
+      else
+#        puts "#{self}: cont offers block #{b} but peer has? #{@peer_pieces[b.pindex]} i already want? #{@want_blocks.member? b}" if rej_count < 10
+        rej_count += 1
+        false
+      end
+    end
+ #   puts "#{self}: ... and #{rej_count} more (peer has #{@peer_pieces.inject(0) { |s, p| s + (p ? 1 : 0) }} pieces)... " if rej_count >= 10
+#    puts "#{self}: accepted #{acc_count} blocks, rejected #{rej_count} blocks"
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/server.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/server.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/server.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,166 @@
+## server.rb -- make/receive and handshake all new peer connections.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'socket'
+require "rubytorrent/tracker"
+require "rubytorrent/controller"
+require "rubytorrent/peer"
+
+module RubyTorrent
+
+## The Server coordinates all Packages available on the machine. It
+## instantiates one Controller for each Package. It's also responsible
+## for the creation of all TCP connections---it sets up the TCP
+## socket, receives incoming connections, validates handshakes, and
+## hands them off to the appropriate Controller; it also creates
+## outgoing connections (typically at Controllers' requests) and sends
+## the handshake.
+class Server
+  attr_reader :port, :id, :http_proxy
+
+  VERSION = 0
+  PORT_RANGE=(6881 .. 6889)
+
+  def initialize(hostname=nil, port=nil, http_proxy=ENV["http_proxy"])
+    @http_proxy = http_proxy
+    @server = nil
+    if port.nil?
+      @port = PORT_RANGE.detect do |p|
+        begin
+          @server = TCPServer.new(hostname, p)
+          @port = p
+        rescue Errno::EADDRINUSE
+          @server = nil
+        end
+        !@server.nil?
+      end
+      raise Errno::EADDRINUSE, "ports #{PORT_RANGE}" unless @port
+    else
+      @server = TCPServer.new(hostname, port)
+      @port = port
+    end
+
+    @id = "rubytor" + VERSION.chr + (1 .. 12).map { |x| rand(256).chr }.join
+    @controllers = {}
+  end
+
+  def ip; @server.addr[3]; end
+
+  def add_torrent(mi, package, dlratelim=nil, ulratelim=nil)
+    @controllers[mi.info.sha1] = Controller.new(self, package, mi.info.sha1, mi.trackers, dlratelim, ulratelim, @http_proxy)
+    @controllers[mi.info.sha1].start
+  end
+
+  def add_connection(name, cont, socket)
+    begin
+      shake_hands(socket, cont.info_hash)
+      peer = PeerConnection.new(name, cont, socket, cont.package)
+      cont.add_peer peer
+    rescue ProtocolError => e
+      socket.close rescue nil
+    end
+  end
+
+  def start
+    @shutdown = false
+    @thread = Thread.new do
+      begin
+        while !@shutdown; receive; end
+      rescue IOError, StandardError
+        rt_warning "**** socket receive error, retrying"
+        sleep 5
+        retry
+      end
+    end
+    self
+  end
+
+  def shutdown
+    return if @shutdown
+    @shutdown = true
+    @server.close rescue nil
+
+    @thread.join(0.2)
+    @controllers.each { |hash, cont| cont.shutdown }
+    self
+  end
+
+  def to_s
+    "<#{self.class}: port #{port}, peer_id #{@id.inspect}>"
+  end
+
+  private
+
+  def receive # blocking
+    ssocket = @server.accept
+    Thread.new do
+      socket = ssocket
+
+      begin
+        rt_debug "<= incoming connection from #{socket.peeraddr[2]}:#{socket.peeraddr[1]}"
+        hash, peer_id = shake_hands(socket, nil)
+        cont = @controllers[hash]
+        peer = PeerConnection.new("#{socket.peeraddr[2]}:#{socket.peeraddr[1]}", cont, socket, cont.package)
+        cont.add_peer peer
+      rescue SystemCallError, ProtocolError => e
+        rt_debug "killing incoming connection: #{e}"
+        socket.close rescue nil
+      end
+    end
+  end
+
+  ## if info_hash is nil here, the socket is treated as an incoming
+  ## connection---it will wait for the peer's info_hash and respond
+  ## with the same if it corresponds to a current download, otherwise
+  ## it will raise a ProtocolError.
+  ##
+  ## if info_hash is not nil, the socket is treated as an outgoing
+  ## connection, and it will send the info_hash immediately.
+  def shake_hands(sock, info_hash)
+#    rt_debug "initiating #{(info_hash.nil? ? 'incoming' : 'outgoing')} handshake..."
+    sock.send("\023BitTorrent protocol\0\0\0\0\0\0\0\0", 0);
+    sock.send("#{info_hash}#{@id}", 0) unless info_hash.nil?
+
+    len = sock.recv(1)[0]
+#    rt_debug "length #{len.inspect}"
+    raise ProtocolError, "invalid handshake length byte #{len.inspect}" unless len == 19
+
+    name = sock.recv(19)
+#    rt_debug "name #{name.inspect}"
+    raise ProtocolError, "invalid handshake protocol string #{name.inspect}" unless name == "BitTorrent protocol"
+
+    reserved = sock.recv(8)
+#    rt_debug "reserved: #{reserved.inspect}" 
+   # ignore for now
+
+    their_hash = sock.recv(20)
+#    rt_debug "their info hash: #{their_hash.inspect}"
+
+    if info_hash.nil?
+      raise ProtocolError, "client requests package we don't have: hash=#{their_hash.inspect}" unless @controllers.has_key? their_hash
+      info_hash = their_hash
+      sock.send("#{info_hash}#{@id}", 0)
+    else
+      raise ProtocolError, "mismatched info hashes: us=#{info_hash.inspect}, them=#{their_hash.inspect}" unless info_hash == their_hash
+    end
+
+    peerid = sock.recv(20)
+#    rt_debug "peer id: #{peerid.inspect}"
+    raise ProtocolError, "connected to self" if peerid == @id
+
+#    rt_debug "== handshake complete =="
+    [info_hash, peerid]
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/tracker.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/tracker.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/tracker.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,225 @@
+## tracker.rb -- bittorrent tracker protocol.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'open-uri'
+require 'timeout'
+require "rubytorrent"
+
+module RubyTorrent
+
+module HashAddition
+  def +(o)
+    ret = self.dup
+    o.each { |k, v| ret[k] = v }
+    ret
+  end
+end
+
+## am i insane or does 'uniq' not use == or === for some insane
+## reason? wtf is that about?
+module ArrayUniq2
+  def uniq2
+    ret = []
+    each { |x| ret.push x unless ret.member? x }
+    ret
+  end
+end
+
+class TrackerResponsePeer
+  attr_writer :tried
+
+  def initialize(dict=nil)
+    @s = TypedStruct.new do |s|
+      s.field :peer_id => String, :ip => String, :port => Integer
+      s.required :ip, :port
+      s.label :peer_id => "peer id"
+    end
+
+    @s.parse(dict) unless dict.nil?
+    @connected = false
+    @tried = false
+  end
+
+  def tried?; @tried; end
+
+  def method_missing(meth, *args)
+    @s.send(meth, *args)
+  end
+
+  def ==(o); (self.ip == o.ip) && (self.port == o.port); end
+
+  def to_s
+    %{<#{self.class}: ip=#{self.ip}, port=#{self.port}>}
+  end
+end
+
+class TrackerResponse
+  def initialize(dict=nil)
+    @s = TypedStruct.new do |s|
+      s.field :interval => Integer, :complete => Integer,
+              :incomplete => Integer, :peers => TrackerResponsePeer
+      s.array :peers
+      s.required :peers #:interval, :complete, :incomplete, :peers
+      s.coerce :peers => lambda { |x| make_peers x }
+    end
+
+    @s.parse(dict) unless dict.nil?
+
+    peers.extend ArrayShuffle
+  end
+
+  def method_missing(meth, *args)
+    @s.send(meth, *args)
+  end
+
+  private
+
+  def make_peers(x)
+    case x
+    when Array
+      x.map { |e| TrackerResponsePeer.new e }.extend(ArrayUniq2).uniq2
+    when String
+      x.unpack("a6" * (x.length / 6)).map do |y|
+        TrackerResponsePeer.new({"ip" => (0..3).map { |i| y[i] }.join('.'),
+                                 "port" => (y[4] << 8) + y[5] })
+      end.extend(ArrayUniq2).uniq2
+    else
+      raise "don't know how to make peers array from #{x.class}"
+    end
+  end
+end
+
+class TrackerError < StandardError; end
+
+class TrackerConnection
+  attr_reader :port, :left, :peer_id, :last_conn_time, :url, :in_force_refresh
+  attr_accessor :uploaded, :downloaded, :left, :numwant
+
+  def initialize(url, info_hash, length, port, peer_id, ip=nil, numwant=50, http_proxy=ENV["http_proxy"])
+    @url = url
+    @hash = info_hash
+    @length = length
+    @port = port
+    @uploaded = @downloaded = @left = 0
+    @ip = ip
+    @numwant = numwant
+    @peer_id = peer_id
+    @http_proxy = http_proxy
+    @state = :stopped
+    @sent_completed = false
+    @last_conn_time = nil
+    @tracker_data = nil
+    @compact = true
+    @in_force_refresh = false
+  end
+
+  def already_completed; @sent_completed = true; end
+  def sent_completed?; @sent_completed; end
+
+  def started
+    return unless @state == :stopped
+    @state = :started
+    @tracker_data = send_tracker "started"
+    self
+  end
+
+  def stopped
+    return unless @state == :started
+    @state = :stopped
+    @tracker_data = send_tracker "stopped"
+    self
+  end
+  
+  def completed
+    return if @sent_completed
+    @tracker_data = send_tracker "completed"
+    @sent_completed = true
+    self
+  end
+
+  def refresh
+    return unless (Time.now - @last_conn_time) >= (interval || 0)
+    @tracker_data = send_tracker nil
+  end
+
+  def force_refresh
+    return if @in_force_refresh
+    @in_force_refresh = true
+    @tracker_data = send_tracker nil
+    @in_force_refresh = false
+  end
+
+  [:interval, :seeders, :leechers, :peers].each do |m|
+    class_eval %{
+      def #{m}
+        if @tracker_data then @tracker_data.#{m} else nil end
+      end
+    }
+  end
+
+  private
+
+  def send_tracker(event)
+    resp = nil
+    if @compact
+      resp = get_tracker_response({ :event => event, :compact => 1 })
+      if resp["failure reason"]
+        @compact = false
+      end
+    end
+       
+    resp = get_tracker_response({ :event => event }) unless resp
+    raise TrackerError, "tracker reports error: #{resp['failure reason']}" if resp["failure reason"]
+    
+    TrackerResponse.new(resp)
+  end
+
+  def get_tracker_response(opts)
+    target = @url.dup
+    opts.extend HashAddition
+    opts += {:info_hash => @hash, :peer_id => @peer_id,
+      :port => @port, :uploaded => @uploaded, :downloaded => @downloaded,
+      :left => @left, :numwant => @numwant, :ip => @ip}
+    target.query = opts.map do |k, v|
+      unless v.nil?
+        ek = URI.escape(k.to_s) # sigh
+        ev = URI.escape(v.to_s, /[^a-zA-Z0-9]/)
+        "#{ek}=#{ev}"
+      end
+    end.compact.join "&"
+
+    rt_debug "connecting to #{target.to_s} ..."
+
+    ret = nil
+    begin
+      target.open(:proxy => @http_proxy) do |resp|
+        BStream.new(resp).each do |e|
+          if ret.nil?
+            ret = e
+          else
+            raise TrackerError, "don't understand tracker response (too many objects)"
+          end
+        end
+      end
+    rescue SocketError, EOFError, OpenURI::HTTPError, RubyTorrent::TrackerError, Timeout::Error, SystemCallError, NoMethodError => e
+      raise TrackerError, e.message
+    end
+    @last_conn_time = Time.now
+
+    raise TrackerError, "empty tracker response" if ret.nil?
+    raise TrackerError, "don't understand tracker response (not a dict)" unless ret.kind_of? ::Hash
+    ret
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/typedstruct.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/typedstruct.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/typedstruct.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,132 @@
+## typedstruct.rb -- type-checking struct, for bencoded objects.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent/bencoding"
+
+module RubyTorrent
+
+module ArrayToH
+  def to_h
+    inject({}) { |h, (k, v)| h[k] = v; h } # found this neat trick on the internet
+  end
+end
+
+module HashMapHash
+  def map_hash
+    a = map { |k, v| yield k, v }.extend(ArrayToH).to_h
+  end
+end
+
+class TypedStructError < StandardError; end
+
+## type-checking struct meant for easy translation from and to
+## bencoded dicts.
+class TypedStruct
+  attr_accessor :dirty
+  attr_reader :fields # writer below
+
+  def initialize
+    @required = {}
+    @label = {}
+    @coerce = {}
+    @field = {}
+    @array = {}
+    @dirty = false
+    @values = {}
+
+    yield self if block_given?
+
+    @field.each do |f, type|
+      @required[f] ||= false
+      @label[f] ||= f.to_s
+      @array[f] ||= false
+    end
+  end
+
+  def method_missing(meth, *args)
+    if meth.to_s =~ /^(.*?)=$/
+#      p [meth, args]
+
+      f = $1.intern
+      raise ArgumentError, "no such value #{f}" unless @field.has_key? f
+
+      type = @field[f]
+      o = args[0]
+      if @array[f]
+        raise TypeError, "for #{f}, expecting Array, got #{o.class}" unless o.kind_of? ::Array
+        o.each { |e| raise TypeError, "for elements of #{f}, expecting #{type}, got #{e.class}" unless e.kind_of? type }
+        @values[f] = o
+        @dirty = true
+      else
+        raise TypeError, "for #{f}, expecting #{type}, got #{o.class}" unless o.kind_of? type
+        @values[f] = o
+        @dirty = true
+      end
+    else
+      raise ArgumentError, "no such value #{meth}" unless @field.has_key? meth
+#      p [meth, @values[meth]]
+
+      @values[meth]
+    end
+  end
+
+  [:required, :array].each do |f|
+    class_eval %{
+      def #{f}(*args)
+        args.each do |x|
+          raise %q{unknown field "\#{x}" in #{f} list} unless @field[x]
+          @#{f}[x] = true
+        end
+      end
+    }
+  end
+
+  [:field , :label, :coerce].each do |f|
+    class_eval %{
+      def #{f}(hash)
+        hash.each { |k, v| @#{f}[k] = v }
+      end
+    }
+  end
+
+  ## given a Hash from a bencoded dict, parses it according to the
+  ## rules you've set up with field, required, label, etc.
+  def parse(dict)
+    @required.each do |f, reqd|
+      flabel = @label[f]
+      raise TypedStructError, "missing required parameter #{flabel} (dict has #{dict.keys.join(', ')})" if reqd && !(dict.member? flabel)
+
+      if dict.member? flabel
+        v = dict[flabel]
+        if @coerce.member? f
+          v = @coerce[f][v]
+        end
+        if @array[f]
+          raise TypeError, "for #{flabel}, expecting Array, got #{v.class} instead" unless v.kind_of? ::Array
+        end
+        self.send("#{f}=", v)
+      end
+    end
+
+    ## disabled the following line as applications seem to put tons of
+    ## weird fields in their .torrent files.
+    # dict.each { |k, v| raise TypedStructError, %{unknown field "#{k}"} unless @field.member?(k.to_sym) || @label.values.member?(k) }
+  end
+
+  def to_bencoding
+    @required.each { |f, reqd| raise ArgumentError, "missing required parameter #{f}" if reqd && self.send(f).nil? }
+    @field.extend(HashMapHash).map_hash { |f, type| [@label[f], self.send(f)] }.to_bencoding
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/util.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/util.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/util.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,186 @@
+## util.rb -- miscellaneous RubyTorrent utility modules.
+## Copyright 2005 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+def rt_debug(*args)
+  if $DEBUG || RubyTorrent.log
+    stream = RubyTorrent.log || $stdout
+    stream << args.join << "\n" 
+    stream.flush
+  end
+end
+
+def rt_warning(*args)
+  if $DEBUG || RubyTorrent.log
+    stream = RubyTorrent.log || $stderr
+    stream << "warning: " << args.join << "\n" 
+    stream.flush
+  end
+end
+
+module RubyTorrent
+
+ at log = nil
+def log_output_to(fn)
+  @log = File.open(fn, "w")
+end
+attr_reader :log
+module_function :log_output_to, :log
+
+
+## parse final hash of pseudo-keyword arguments
+def get_args(rest, *names)
+  hash = rest.find { |x| x.is_a? Hash }
+  if hash
+    rest.delete hash
+    hash.each { |k, v| raise ArgumentError, %{unknown argument "#{k}"} unless names.include?(k) }
+  end
+
+  [hash || {}, rest]
+end
+module_function :get_args
+
+## "events": very similar to Observable, but cleaner, IMO. events are
+## listened to and sent in instance space, but registered in class
+## space. example:
+##
+## class C
+##   include EventSource
+##   event :goat, :boat
+##
+##   def send_events
+##     send_event :goat
+##     send_event(:boat, 3)
+##   end
+## end
+##
+## c = C.new
+## c.on_event(:goat) { puts "got goat!" }
+## c.on_event(:boat) { |x| puts "got boat: #{x}" }
+##
+## Defining them in class space is not really necessary, except as an
+## error-checking mechanism.
+module EventSource
+  def on_event(who, *events, &b)
+    @event_handlers ||= Hash.new { [] }
+    events.each do |e|
+      raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
+      @event_handlers[e] <<= [who, b]
+    end
+    nil
+  end
+
+  def send_event(e, *args)
+    raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
+    @event_handlers ||= Hash.new { [] }
+    @event_handlers[e].each { |who, proc| proc[self, *args] }
+    nil
+  end
+
+  def unregister_events(who, *events)
+    @event_handlers.each do |event, handlers|
+      handlers.each do |ewho, proc|
+        if (ewho == who) && (events.empty? || events.member?(event))
+          @event_handlers[event].delete [who, proc]
+        end
+      end
+    end
+    nil
+  end
+
+  def relay_event(who, *events)
+    @event_handlers ||= Hash.new { [] }
+    events.each do |e|
+      raise "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
+      raise "unknown event #{e} for #{who.class}" unless (who.class.class_eval "@@event_has")[e]
+      @event_handlers[e] <<= [who, lambda { |s, *a| who.send_event e, *a }]
+    end
+    nil
+  end
+
+  def self.append_features(mod)
+    super(mod)
+    mod.class_eval %q{
+      @@event_has ||= Hash.new(false)
+      def self.event(*args)
+        args.each { |a| @@event_has[a] = true }
+      end
+    }
+  end
+end
+
+## ensure that a method doesn't execute more frequently than some
+## number of seconds. e.g.:
+##
+## def meth
+##   ...
+## end
+## min_iterval :meth, 10
+##
+## ensures that "meth" won't be executed more than once every 10
+## seconds.
+module MinIntervalMethods
+  def min_interval(meth, int)
+    class_eval %{
+      @@min_interval ||= {}
+      @@min_interval[:#{meth}] = [nil, #{int.to_i}]
+      alias :min_interval_#{meth} :#{meth}
+      def #{meth}(*a, &b)
+        last, int = @@min_interval[:#{meth}]
+        unless last && ((Time.now - last) < int)
+          min_interval_#{meth}(*a, &b) 
+          @@min_interval[:#{meth}][0] = Time.now
+        end
+      end
+    }
+  end
+end
+
+## boolean attributes now get question marks in their accessors
+## don't forget to 'extend' rather than 'include' this one
+module AttrReaderQ
+  def attr_reader_q(*args)
+    args.each { |v| class_eval "def #{v}?; @#{v}; end" }
+  end
+  
+  def attr_writer_q(*args)
+    args.each { |v| attr_writer v }
+  end
+  
+  def attr_accessor_q(*args)
+    attr_reader_q args
+    attr_writer_q args
+  end
+end
+
+module ArrayShuffle
+  def shuffle!
+    each_index do |i|
+      j = i + rand(self.size - i);
+      self[i], self[j] = self[j], self[i]
+    end
+  end
+
+  def shuffle
+    self.clone.shuffle! # dup doesn't preserve shuffle! method
+  end
+end
+
+module StringMapBytes
+  def map_bytes
+    ret = []
+    each_byte { |x| ret.push(yield(x)) }
+    ret
+  end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent.rb	                        (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent.rb	2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,94 @@
+## rubytorrent.rb -- top-level RubyTorrent file.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'rubytorrent/util'
+require 'rubytorrent/bencoding'
+require 'rubytorrent/metainfo'
+require 'rubytorrent/tracker'
+require 'rubytorrent/package'
+require 'rubytorrent/server'
+
+require "socket"
+Socket.do_not_reverse_lookup = true
+
+module RubyTorrent
+  VERSION = 0.3
+
+## the top-level class for RubyTorrent.
+class BitTorrent
+  include EventSource
+  event :trying_peer, :forgetting_peer, :added_peer, :removed_peer,
+        :received_block, :sent_block, :have_piece, :discarded_piece, :complete,
+        :tracker_connected, :tracker_lost, :requested_block
+
+  @@server = nil
+
+  ## hash arguments: host, port, dlratelim, ulratelim
+  def initialize(metainfo, *rest)
+    args, rest = RubyTorrent::get_args(rest, :host, :port, :dlratelim, :ulratelim, :http_proxy)
+    out = rest.shift
+    raise ArgumentError, "wrong number of arguments (expected 0/1, got #{rest.length})" unless rest.empty?
+
+    case metainfo
+    when MetaInfo
+      @metainfo = metainfo
+    when String
+      @metainfo = MetaInfo.from_location(metainfo)
+    when IO
+      @metainfo = MetaInfo.from_stream(metainfo)
+    else
+      raise ArgumentError, "'metainfo' should be a String, IO or RubyTorrent::MetaInfo object"
+    end
+
+    case out
+    when Package
+      @package = out
+    else
+      @package = Package.new(@metainfo, out)
+    end
+
+    unless @@server
+      @@server = RubyTorrent::Server.new(args[:host], args[:port], args[:http_proxy])
+      @@server.start
+    end
+
+    @cont = @@server.add_torrent(@metainfo, @package, args[:dlratelim], args[:ulratelim])
+
+    @cont.relay_event self, :trying_peer, :forgetting_peer, :added_peer,
+                            :removed_peer, :received_block, :sent_block,
+                            :have_piece, :discarded_piece, :tracker_connected,
+                            :tracker_lost, :requested_block
+    @package.relay_event self, :complete
+  end
+
+  def ip; @@server.ip; end
+  def port; @@server.port; end
+  def peer_info; @cont.peer_info; end
+  def shutdown; @cont.shutdown; end
+  def shutdown_all; @@server.shutdown; end
+  def complete?; @package.complete?; end
+  def bytes_completed; @package.bytes_completed; end
+  def percent_completed; @package.percent_completed; end
+  def pieces_completed; @package.pieces_completed; end
+  def dlrate; @cont.dlrate; end
+  def ulrate; @cont.ulrate; end
+  def dlamt; @cont.dlamt; end
+  def ulamt; @cont.ulamt; end
+  def num_pieces; @package.num_pieces; end
+  def tracker; (@cont.tracker ? @cont.tracker.url : nil); end
+  def num_possible_peers; (@cont.tracker ? @cont.tracker.peers.length : 0); end
+  def num_active_peers; @cont.num_peers; end
+  def total_bytes; @package.size; end
+end
+
+end

Added: tools/ruby-support/examples/libtorrent-ruby_0.3.orig.tar.gz
===================================================================
(Binary files differ)


Property changes on: tools/ruby-support/examples/libtorrent-ruby_0.3.orig.tar.gz
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream




More information about the Pkg-ruby-extras-commits mailing list