diff --git a/apps/i2psnark/COPYING b/apps/i2psnark/COPYING
new file mode 100644
index 0000000000000000000000000000000000000000..d60c31a97a544b53039088d14fe9114583c0efc3
--- /dev/null
+++ b/apps/i2psnark/COPYING
@@ -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.
diff --git a/apps/i2psnark/TODO b/apps/i2psnark/TODO
new file mode 100644
index 0000000000000000000000000000000000000000..6f89c0f50749b1a8d3d6ab80fd419d2613448c69
--- /dev/null
+++ b/apps/i2psnark/TODO
@@ -0,0 +1,24 @@
+- I2PSnark:
+  - add multitorrent support by checking the metainfo hash in the
+    PeerAcceptor and feeding it off to the appropriate coordinator
+  - add a web interface
+
+- BEncode
+  - Byte array length indicator can overflow.
+  - Support really big BigNums (only 256 chars allowed now)
+  - Better BEValue toString(). Uses stupid heuristic now for debugging.
+  - Implemented bencoding.
+  - Remove application level hack to calculate sha1 hash for metainfo
+    (But can it be done as efficiently?)
+
+- Storage
+  - Check file name filter.
+
+- TrackerClient
+  - Support undocumented &numwant= request.
+
+- PeerCoordinator
+  - Disconnect from other seeds as soon as you are a seed yourself.
+
+- Text UI
+  - Make it completely silent.
diff --git a/apps/i2psnark/authors.snark b/apps/i2psnark/authors.snark
new file mode 100644
index 0000000000000000000000000000000000000000..00aecc25bad57c000240bb7d795c56801b722041
--- /dev/null
+++ b/apps/i2psnark/authors.snark
@@ -0,0 +1 @@
+Mark Wielaard <mark@klomp.org>
diff --git a/apps/i2psnark/changelog.snark b/apps/i2psnark/changelog.snark
new file mode 100644
index 0000000000000000000000000000000000000000..75304f17c59890cbc2891c6a33923fd14ba9d11a
--- /dev/null
+++ b/apps/i2psnark/changelog.snark
@@ -0,0 +1,487 @@
+2003-06-27 14:24  Mark Wielaard  <mark@klomp.org>
+
+	* README: Update version number and explain new features.
+
+2003-06-27 13:51  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile, org/klomp/snark/GnomeInfoWindow.java,
+	org/klomp/snark/GnomePeerList.java,
+	org/klomp/snark/PeerCoordinator.java,
+	org/klomp/snark/SnarkGnome.java: Add GnomeInfoWindow.
+
+2003-06-27 00:37  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Snark.java: Implement 'info' and 'list' commands.
+
+2003-06-27 00:05  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile, org/klomp/snark/GnomePeerList.java,
+	org/klomp/snark/SnarkGnome.java: Add GnomePeerList to show state of
+	connected peers.
+
+2003-06-27 00:04  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: Peer.java, PeerID.java: Make Comparable.
+
+2003-06-23 23:32  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerMonitorTask.java: Correctly update
+	lastDownloaded and lastUploaded.
+
+2003-06-23 23:20  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Snark.java: When checking storage use the
+	MetaInfo from the storage.
+
+2003-06-23 21:47  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Storage.java: Fill piece hashes, not info hashes.
+
+2003-06-23 21:42  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/MetaInfo.java: New package private
+	getPieceHashes() method.
+
+2003-06-22 19:49  Mark Wielaard  <mark@klomp.org>
+
+	* README, TODO, org/klomp/snark/Snark.java: Add new command line
+	switch --no-commands.  Don't read interactive commands or show
+	usage info.
+
+2003-06-22 19:26  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile, org/klomp/snark/PeerCheckerTask.java,
+	org/klomp/snark/PeerMonitorTask.java, org/klomp/snark/Snark.java:
+	Split peer statistic reporting from PeerCheckerTask into
+	PeerMonitorTask.  Use new task in Snark text ui.
+
+2003-06-22 18:32  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Snark.java: Only print peer id when debug level
+	is INFO or higher.
+
+2003-06-22 18:00  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/ShutdownListener.java: Add new ShutdownListener
+	interface.
+
+2003-06-22 17:18  Mark Wielaard  <mark@klomp.org>
+
+	* TODO: Text UI item to not read from stdin.
+
+2003-06-22 17:18  Mark Wielaard  <mark@klomp.org>
+
+	* snark-gnome.sh: kaffe java-gnome support (but crashes hard at the
+	moment).
+
+2003-06-22 14:04  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile, org/klomp/snark/CoordinatorListener.java,
+	org/klomp/snark/PeerCoordinator.java,
+	org/klomp/snark/ProgressListener.java, org/klomp/snark/Snark.java,
+	org/klomp/snark/SnarkGnome.java,
+	org/klomp/snark/SnarkShutdown.java, org/klomp/snark/Storage.java,
+	org/klomp/snark/StorageListener.java: Split ProgressListener into
+	Storage, Coordinator and Shutdown listener.
+
+2003-06-20 19:06  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: PeerCoordinator.java, Snark.java,
+	SnarkGnome.java, Storage.java: Progress listeners for both Storage
+	and PeerCoordinator.
+
+2003-06-20 14:50  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile, org/klomp/snark/PeerCoordinator.java,
+	org/klomp/snark/ProgressListener.java,
+	org/klomp/snark/SnarkGnome.java: Add ProgressListener.
+
+2003-06-20 13:22  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/SnarkGnome.java: Add Pieces collected field.
+
+2003-06-20 12:26  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: PeerCoordinator.java, PeerListener.java,
+	PeerState.java: Add PeerListener.downloaded() which gets called on
+	chunk updates.	Keep PeerCoordinator.downloaded up to date using
+	this remove adjusting in gotPiece() except when we receive a bad
+	piece.
+
+2003-06-16 00:27  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile, snark-gnome.sh, org/klomp/snark/Snark.java,
+	org/klomp/snark/SnarkGnome.java: Start of a Gnome GUI.
+
+2003-06-05 13:19  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerCoordinator.java: Don't remove a BAD piece
+	from the wantedPieces list.  Revert to synchronizing on
+	wantedPieces for all relevant sections.
+
+2003-06-03 21:09  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Snark.java: Only call readLine() when !quit. 
+	Always print exception when fatal() is called.
+
+2003-06-01 23:12  Mark Wielaard  <mark@klomp.org>
+
+	* README: Set release version to 0.4.
+
+2003-06-01 22:59  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerConnectionIn.java: Handle negative length
+	prefixes (terminates connection).
+
+2003-06-01 21:34  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: Snark.java, SnarkShutdown.java: Implement
+	correct shutdown and read commands from stdin.
+
+2003-06-01 21:34  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/TrackerInfo.java: Check that interval and peers
+	list actually exist.
+
+2003-06-01 21:33  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Storage.java: Implement close().
+
+2003-06-01 21:05  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Fix debug logging.
+
+2003-06-01 20:55  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerCoordinator.java: Implement halt().
+
+2003-06-01 20:55  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/ConnectionAcceptor.java: Rename stop() to halt().
+
+2003-06-01 17:35  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Drop lock on this when calling
+	addRequest() from havePiece().
+
+2003-06-01 14:46  Mark Wielaard  <mark@klomp.org>
+
+	* README, org/klomp/snark/ConnectionAcceptor.java,
+	org/klomp/snark/HttpAcceptor.java, org/klomp/snark/Peer.java,
+	org/klomp/snark/PeerCheckerTask.java,
+	org/klomp/snark/PeerConnectionIn.java,
+	org/klomp/snark/PeerConnectionOut.java,
+	org/klomp/snark/PeerCoordinator.java,
+	org/klomp/snark/PeerState.java, org/klomp/snark/Snark.java,
+	org/klomp/snark/SnarkShutdown.java, org/klomp/snark/Storage.java,
+	org/klomp/snark/Tracker.java, org/klomp/snark/TrackerClient.java:
+	Add debug/log level.
+
+2003-05-31 23:04  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java: Use
+	just one lock (peers) for all synchronization (even for
+	wantedPieces).	Let PeerChecker handle real disconnect and keep
+	count of uploaders.
+
+2003-05-31 22:29  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: Peer.java, PeerConnectionIn.java: Set state to
+	null on first disconnect() call. So always check whether it might
+	already be null. Helps disconnect check.
+
+2003-05-31 22:27  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerConnectionOut.java: Don't explicitly close
+	the DataOutputStream (if another thread is using it libgcj seems to
+	not like it very much).
+
+2003-05-30 21:33  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerConnectionOut.java: Cancel
+	(un)interested/(un)choke when (inverse) is still in send queue. 
+	Remove pieces from send queue when choke message is actaully send.
+
+2003-05-30 19:32  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Make sure listener.wantPiece(int)
+	is never called while lock on this is held.
+
+2003-05-30 19:00  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerConnectionOut.java: Indentation cleanup.
+
+2003-05-30 17:50  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Storage.java: Only synchronize on bitfield as
+	long as necessary.
+
+2003-05-30 17:43  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Tracker.java: Identing cleanup.
+
+2003-05-30 16:32  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Better error message.
+
+2003-05-30 15:11  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Make sure not to hold the lock on
+	this when calling the listener to prevent deadlocks. Implement
+	handling and sending of cancel messages.
+
+2003-05-30 14:50  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerCoordinator.java: First check if we still
+	want a piece before trying to add it to the Storage.
+
+2003-05-30 14:49  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerConnectionOut.java: Implement
+	sendCancel(Request). Add cancelRequest(int, int, int).
+
+2003-05-30 14:46  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Request.java: Add hashCode() and equals(Object)
+	methods.
+
+2003-05-30 14:45  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Peer.java: Fix wheter -> whether javadoc
+	comments.  Mark state null immediatly after calling
+	listener.disconnected().  Call PeerState.havePiece() not
+	PeerConnectionOut.sendHave() directly.
+
+2003-05-25 19:23  Mark Wielaard  <mark@klomp.org>
+
+	* TODO: Add PeerCoordinator TODO for connecting to seeds.
+
+2003-05-23 12:12  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile: Create class files with jikes again.
+
+2003-05-18 22:01  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java:
+	Prefer to (optimistically) unchoke first those peers that unchoked
+	us.  And make sure to not unchoke a peer that we just choked.
+
+2003-05-18 21:48  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Peer.java: Fix isChoked() to not always return
+	true.
+
+2003-05-18 14:46  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: Peer.java, PeerCheckerTask.java,
+	PeerCoordinator.java, PeerState.java: Remove separate Peer
+	downloading/uploading states. Keep choke and interest always up to
+	date. Uploading is now just when we are not choking the peer. 
+	Downloading is now defined as being unchoked and interesting. 
+	CHECK_PERIOD is now 20 seconds. MAX_CONNECTIONS is now 24. 
+	MAX_DOWNLOADERS doesn't exists anymore. We download whenever we can
+	from peers.
+
+2003-05-18 13:57  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerConnectionOut.java: Remove piece messages
+	from queue when we are choking.  (They will have to be rerequested
+	when we unchoke the peer again.)
+
+2003-05-15 00:08  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Ignore missed chunk requests,
+	don't requeue them.
+
+2003-05-15 00:06  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Request.java: Add sanity check
+
+2003-05-10 15:47  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Snark.java: Add extra '(' to usage message.
+
+2003-05-10 15:22  Mark Wielaard  <mark@klomp.org>
+
+	* README: Set version to 0.3 (The Bakers Tale).
+
+2003-05-10 15:17  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Mention received piece in warning
+	message.
+
+2003-05-10 03:20  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: PeerConnectionIn.java, PeerState.java,
+	Request.java: Remove currentRequest and handle all piece messages
+	from the lastRequested list.
+
+2003-05-09 20:02  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Fix nothing requested warning
+	message.
+
+2003-05-09 19:59  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerConnectionOut.java: Piece messages are big.
+	So if there are other (control) messages make sure they are send
+	first. Also remove request messages from the queue if we are
+	currently being choked to prevent them from being send even if we
+	get unchoked a little later. (Since we will resent them anyway in
+	that case.)
+
+2003-05-09 18:33  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: Peer.java, PeerCheckerTask.java,
+	PeerCoordinator.java, PeerID.java: New definition of PeerID.equals
+	(port + address + id) and new method PeerID.sameID (only id). These
+	are used to really see if we already have a connection to a certain
+	peer (active setup vs passive setup).
+
+2003-05-08 03:05  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Use Snark.debug() not
+	System.out.println().
+
+2003-05-06 20:29  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: s/noting/nothing/
+
+2003-05-06 20:28  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile: s/lagacy/legacy/
+
+2003-05-05 23:17  Mark Wielaard  <mark@klomp.org>
+
+	* README: Set version to 0.2, explain new functionality and add
+	examples.
+
+2003-05-05 22:42  Mark Wielaard  <mark@klomp.org>
+
+	* .cvsignore, Makefile, org/klomp/snark/StaticSnark.java: Enable
+	-static binary creation.
+
+2003-05-05 22:42  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Tracker.java: Disable --ip support.
+
+2003-05-05 21:02  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: HttpAcceptor.java, PeerCheckerTask.java,
+	PeerCoordinator.java, TrackerClient.java: Use Snark.debug() not
+	System.out.println().
+
+2003-05-05 21:01  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerConnectionIn.java: Be prepared to handle the
+	case where currentRequest is null.
+
+2003-05-05 21:00  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Snark.java: Improve argument parsing errors.
+
+2003-05-05 21:00  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile: Use gcj -C again for creating the class files.
+
+2003-05-05 09:24  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Just clear outstandingRequests,
+	never make it null.
+
+2003-05-05 02:55  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/TrackerClient.java: Always retry both first
+	started event and every other event as long the TrackerClient is
+	not stopped.
+
+2003-05-05 02:54  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Snark.java: Remove double assignment port.
+
+2003-05-05 02:54  Mark Wielaard  <mark@klomp.org>
+
+	* TODO: Add Tracker TODO item.
+
+2003-05-04 23:38  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: ConnectionAcceptor.java, MetaInfo.java,
+	Snark.java, Storage.java, Tracker.java: Add info hash calcultation
+	to MetaInfo.  Add torrent creation to Storage.	Add ip parameter
+	handling to Tracker.  Make ConnectionAcceptor handle
+	null/non-existing HttpAcceptors.  Add debug output, --ip handling
+	and all the above to Snark.
+
+2003-05-04 23:36  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/TrackerClient.java: Handle all failing requests
+	the same (print a warning).
+
+2003-05-03 15:46  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: Peer.java, PeerID.java, TrackerInfo.java: Split
+	Peer and PeerID a little more.
+
+2003-05-03 15:44  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/MetaInfo.java: Add reannounce() and
+	getTorrentData().
+
+2003-05-03 15:38  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java:
+	More concise verbose/debug output.  Always use addUpDownloader() to
+	set peers upload or download state to true.
+
+2003-05-03 13:38  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/TrackerClient.java: Compile fixes.
+
+2003-05-03 13:32  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/TrackerClient.java: Only generate fatal() call on
+	first Tracker access. Otherwise just print a warning error message.
+
+2003-05-03 03:10  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerState.java: Better handle resending
+	outstanding pieces and try to recover better from unrequested
+	pieces.
+
+2003-05-02 21:33  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile, org/klomp/snark/HttpAcceptor.java,
+	org/klomp/snark/MetaInfo.java, org/klomp/snark/PeerID.java,
+	org/klomp/snark/Snark.java, org/klomp/snark/Tracker.java,
+	org/klomp/snark/TrackerClient.java,
+	org/klomp/snark/bencode/BEncoder.java: Add Tracker, PeerID and
+	BEncoder.
+
+2003-05-01 20:17  Mark Wielaard  <mark@klomp.org>
+
+	* Makefile, org/klomp/snark/ConnectionAcceptor.java,
+	org/klomp/snark/HttpAcceptor.java, org/klomp/snark/Peer.java,
+	org/klomp/snark/PeerAcceptor.java, org/klomp/snark/Snark.java: Add
+	ConnectionAcceptor that handles both PeerAcceptor and HttpAcceptor.
+
+2003-05-01 18:39  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/PeerCoordinator.java: connected() synchronize on
+	peers.
+
+2003-04-28 02:56  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/SnarkShutdown.java: Wait some time before
+	returning...
+
+2003-04-28 02:56  Mark Wielaard  <mark@klomp.org>
+
+	* TODO: More items.
+
+2003-04-28 02:56  Mark Wielaard  <mark@klomp.org>
+
+	* org/klomp/snark/Snark.java: Calculate real random ID.
+
+2003-04-27  Mark Wielaard  <mark@klomp.org>
+
+	* snark: Initial (0.1) version.
diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml
new file mode 100644
index 0000000000000000000000000000000000000000..94e97bbc05cbcc1c47f182575d78083c7f1b33ff
--- /dev/null
+++ b/apps/i2psnark/java/build.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project basedir="." default="all" name="i2psnark">
+    <target name="all" depends="clean, build" />
+    <target name="build" depends="builddep, jar" />
+    <target name="builddep">
+        <ant dir="../../ministreaming/java/" target="build" />
+	<!-- ministreaming will build core -->
+    </target>
+    <target name="compile">
+        <mkdir dir="./build" />
+        <mkdir dir="./build/obj" />
+        <javac 
+            srcdir="./src" 
+            debug="true" deprecation="on" source="1.3" target="1.3" 
+            destdir="./build/obj" 
+            classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" />
+    </target>
+    <target name="jar" depends="builddep, compile">
+        <jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class">
+            <manifest>
+                <attribute name="Main-Class" value="org.klomp.snark.Snark" />
+                <attribute name="Class-Path" value="i2p.jar jbigi.jar mstreaming.jar streaming.jar" />
+            </manifest>
+        </jar>
+    </target>
+    <target name="clean">
+        <delete dir="./build" />
+    </target>
+    <target name="cleandep" depends="clean">
+        <ant dir="../../ministreaming/java/" target="distclean" />
+    </target>
+    <target name="distclean" depends="clean">
+        <ant dir="../../ministreaming/java/" target="distclean" />
+    </target>
+</project>
diff --git a/apps/i2psnark/java/src/org/klomp/snark/BitField.java b/apps/i2psnark/java/src/org/klomp/snark/BitField.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e3f8d1a76669845cd700148841e065c1edf3432
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/BitField.java
@@ -0,0 +1,131 @@
+/* BitField - Container of a byte array representing set and unset bits.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.util.Iterator;
+import java.util.Set;
+import java.util.HashSet;
+
+/**
+ * Container of a byte array representing set and unset bits.
+ */
+public class BitField
+{
+
+  private final byte[] bitfield;
+  private final int size;
+
+  /**
+   * Creates a new BitField that represents <code>size</code> unset bits.
+   */
+  public BitField(int size)
+  {
+    this.size = size;
+    int arraysize = ((size-1)/8)+1;
+    bitfield = new byte[arraysize];
+  }
+
+  /**
+   * Creates a new BitField that represents <code>size</code> bits
+   * as set by the given byte array. This will make a copy of the array.
+   * Extra bytes will be ignored.
+   *
+   * @exception ArrayOutOfBoundsException if give byte array is not large
+   * enough.
+   */
+  public BitField(byte[] bitfield, int size)
+  {
+    this.size = size;
+    int arraysize = ((size-1)/8)+1;
+    this.bitfield = new byte[arraysize];
+    
+    // XXX - More correct would be to check that unused bits are
+    // cleared or clear them explicitly ourselves.
+    System.arraycopy(bitfield, 0, this.bitfield, 0, arraysize);
+  }
+
+  /**
+   * This returns the actual byte array used.  Changes to this array
+   * effect this BitField.  Note that some bits at the end of the byte
+   * array are supposed to be always unset if they represent bits
+   * bigger then the size of the bitfield.
+   */
+  public byte[] getFieldBytes()
+  {
+    return bitfield;
+  }
+
+  /**
+   * Return the size of the BitField. The returned value is one bigger
+   * then the last valid bit number (since bit numbers are counted
+   * from zero).
+   */
+  public int size()
+  {
+    return size;
+  }
+
+  /**
+   * Sets the given bit to true.
+   *
+   * @exception IndexOutOfBoundsException if bit is smaller then zero
+   * bigger then size (inclusive).
+   */
+  public void set(int bit)
+  {
+    if (bit < 0 || bit >= size)
+      throw new IndexOutOfBoundsException(Integer.toString(bit));
+    int index = bit/8;
+    int mask = 128 >> (bit % 8);
+    bitfield[index] |= mask;
+  }
+
+  /**
+   * Return true if the bit is set or false if it is not.
+   *
+   * @exception IndexOutOfBoundsException if bit is smaller then zero
+   * bigger then size (inclusive).
+   */
+  public boolean get(int bit)
+  {
+    if (bit < 0 || bit >= size)
+      throw new IndexOutOfBoundsException(Integer.toString(bit));
+
+    int index = bit/8;
+    int mask = 128 >> (bit % 8);
+    return (bitfield[index] & mask) != 0;
+  }
+
+  public String toString()
+  {
+    // Not very efficient
+    StringBuffer sb = new StringBuffer("BitField[");
+    for (int i = 0; i < size; i++)
+      if (get(i))
+        {
+          sb.append(' ');
+          sb.append(i);
+        }
+    sb.append(" ]");
+
+    return sb.toString();
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java
new file mode 100644
index 0000000000000000000000000000000000000000..c5b21d5c34e829970e21208dd574fd797ec151e9
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java
@@ -0,0 +1,143 @@
+/* ConnectionAcceptor - Accepts connections and routes them to sub-acceptors.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.*;
+import java.net.*;
+
+import net.i2p.I2PException;
+import net.i2p.client.streaming.I2PServerSocket;
+import net.i2p.client.streaming.I2PSocket;
+
+/**
+ * Accepts connections on a TCP port and routes them to sub-acceptors.
+ */
+public class ConnectionAcceptor implements Runnable
+{
+  private final I2PServerSocket serverSocket;
+  private final PeerAcceptor peeracceptor;
+  private Thread thread;
+
+  private boolean stop;
+
+  public ConnectionAcceptor(I2PServerSocket serverSocket,
+                            PeerAcceptor peeracceptor)
+  {
+    this.serverSocket = serverSocket;
+    this.peeracceptor = peeracceptor;
+    
+    stop = false;
+    thread = new Thread(this);
+    thread.start();
+  }
+
+  public void halt()
+  {
+    stop = true;
+
+    I2PServerSocket ss = serverSocket;
+    if (ss != null)
+      try
+        {
+          ss.close();
+        }
+      catch(I2PException ioe) { }
+
+    Thread t = thread;
+    if (t != null)
+      t.interrupt();
+  }
+
+  public int getPort()
+  {
+    return 6881; // serverSocket.getLocalPort();
+  }
+
+  public void run()
+  {
+    while(!stop)
+      {
+        try
+          {
+            final I2PSocket socket = serverSocket.accept();
+            Thread t = new Thread("Connection-" + socket)
+              {
+                public void run()
+                {
+                  try
+                    {
+                      InputStream in = socket.getInputStream();
+                      OutputStream out = socket.getOutputStream();
+                      BufferedInputStream bis = new BufferedInputStream(in);
+                      BufferedOutputStream bos = new BufferedOutputStream(out);
+                      
+                      // See what kind of connection it is.
+                      /*
+                      if (httpacceptor != null)
+                        {
+                          byte[] scratch = new byte[4];
+                          bis.mark(4);
+                          int len = bis.read(scratch);
+                          if (len != 4)
+                            throw new IOException("Need at least 4 bytes");
+                          bis.reset();
+                          if (scratch[0] == 19 && scratch[1] == 'B'
+                              && scratch[2] == 'i' && scratch[3] == 't')
+                            peeracceptor.connection(socket, bis, bos);
+                          else if (scratch[0] == 'G' && scratch[1] == 'E'
+                                   && scratch[2] == 'T' && scratch[3] == ' ')
+                            httpacceptor.connection(socket, bis, bos);
+                        }
+                      else
+                       */
+                        peeracceptor.connection(socket, bis, bos);
+                    }
+                  catch (IOException ioe)
+                    {
+                      try
+                        {
+                          socket.close();
+                        }
+                      catch (IOException ignored) { }
+                    }
+                }
+              };
+            t.start();
+          }
+        catch (I2PException ioe)
+          {
+            Snark.debug("Error while accepting: " + ioe, Snark.ERROR);
+            stop = true;
+          }
+        catch (IOException ioe)
+          {
+            Snark.debug("Error while accepting: " + ioe, Snark.ERROR);
+            stop = true;
+          }
+      }
+
+    try
+      {
+        serverSocket.close();
+      }
+    catch (I2PException ignored) { }
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc2b9b73efb39fecbf4a87e40f96d528efee3b17
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java
@@ -0,0 +1,33 @@
+/* CoordinatorListener.java - Callback when a peer changes state
+
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+/**
+ * Callback used when some peer changes state.
+ */
+public interface CoordinatorListener
+{
+  /**
+   * Called when the PeerCoordinator notices a change in the state of a peer.
+   */
+  void peerChange(PeerCoordinator coordinator, Peer peer);
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..deda083cf5825f5e750cc3c283b1a5ea56ae1cc2
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
@@ -0,0 +1,165 @@
+package org.klomp.snark;
+
+import net.i2p.I2PAppContext;
+import net.i2p.I2PException;
+import net.i2p.util.EepGet;
+import net.i2p.data.Base64;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+import net.i2p.client.streaming.I2PServerSocket;
+import net.i2p.client.streaming.I2PSocket;
+import net.i2p.client.streaming.I2PSocketManager;
+import net.i2p.client.streaming.I2PSocketManagerFactory;
+import net.i2p.util.Log;
+
+import java.io.*;
+import java.util.Properties;
+
+/**
+ * I2P specific helpers for I2PSnark
+ */
+public class I2PSnarkUtil {
+    private I2PAppContext _context;
+    private Log _log;
+    private static I2PSnarkUtil _instance = new I2PSnarkUtil();
+    public static I2PSnarkUtil instance() { return _instance; }
+    
+    private boolean _shouldProxy;
+    private String _proxyHost;
+    private int _proxyPort;
+    private String _i2cpHost;
+    private int _i2cpPort;
+    private Properties _opts;
+    private I2PSocketManager _manager;
+    
+    private I2PSnarkUtil() {
+        _context = I2PAppContext.getGlobalContext();
+        _log = _context.logManager().getLog(Snark.class);
+        setProxy("127.0.0.1", 4444);
+        setI2CPConfig("127.0.0.1", 7654, null);
+    }
+    
+    /**
+     * Specify what HTTP proxy tracker requests should go through (specify a null
+     * host for no proxying)
+     *
+     */
+    public void setProxy(String host, int port) {
+        if ( (host != null) && (port > 0) ) {
+            _shouldProxy = true;
+            _proxyHost = host;
+            _proxyPort = port;
+        } else {
+            _shouldProxy = false;
+            _proxyHost = null;
+            _proxyPort = -1;
+        }
+    }
+    
+    public void setI2CPConfig(String i2cpHost, int i2cpPort, Properties opts) {
+        _i2cpHost = i2cpHost;
+        _i2cpPort = i2cpPort;
+        if (opts != null)
+            _opts = opts;
+    }
+    
+    /**
+     * Connect to the router, if we aren't already
+     */
+    boolean connect() {
+        if (_manager == null) {
+            _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, _opts);
+        }
+        return (_manager != null);
+    }
+    
+    /** connect to the given destination */
+    I2PSocket connect(PeerID peer) throws IOException {
+        try {
+            return _manager.connect(peer.getAddress());
+        } catch (I2PException ie) {
+            throw new IOException("Unable to reach the peer " + peer + ": " + ie.getMessage());
+        }
+    }
+    
+    /**
+     * fetch the given URL, returning the file it is stored in, or null on error
+     */
+    File get(String url) {
+        File out = null;
+        try {
+            out = File.createTempFile("i2psnark", "url");
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+            return null;
+        }
+        EepGet get = new EepGet(_context, _shouldProxy, _proxyHost, _proxyPort, 1, out.getAbsolutePath(), url);
+        if (get.fetch()) {
+            return out;
+        } else {
+            out.delete();
+            return null;
+        }
+    }
+    
+    I2PServerSocket getServerSocket() { 
+        return _manager.getServerSocket();
+    }
+    
+    String getOurIPString() {
+        return _manager.getSession().getMyDestination().toBase64();
+    }
+    Destination getDestination(String ip) {
+        if (ip == null) return null;
+        if (ip.endsWith(".i2p")) {
+            Destination dest = _context.namingService().lookup(ip);
+            if (dest != null) {
+                return dest;
+            } else {
+                try {
+                    return new Destination(ip.substring(0, ip.length()-4)); // sans .i2p
+                } catch (DataFormatException dfe) {
+                    return null;
+                }
+            }
+        } else {
+            try {
+                return new Destination(ip);
+            } catch (DataFormatException dfe) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Given http://blah.i2p/foo/announce turn it into http://i2p/blah/foo/announce
+     */
+    String rewriteAnnounce(String origAnnounce) {
+        int destStart = "http://".length();
+        int destEnd = origAnnounce.indexOf(".i2p");
+        int pathStart = origAnnounce.indexOf('/', destEnd);
+        return "http://i2p/" + origAnnounce.substring(destStart, destEnd) + origAnnounce.substring(pathStart);
+    }
+    
+    /** hook between snark's logger and an i2p log */
+    void debug(String msg, int snarkDebugLevel, Throwable t) {
+        switch (snarkDebugLevel) {
+            case 0:
+            case 1:
+                _log.error(msg, t);
+                break;
+            case 2:
+                _log.warn(msg, t);
+                break;
+            case 3:
+            case 4:
+                _log.info(msg, t);
+                break;
+            case 5:
+            case 6:
+            default:
+                _log.debug(msg, t);
+                break;
+        }
+    }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Message.java b/apps/i2psnark/java/src/org/klomp/snark/Message.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e95fda18e07cf58179a75e604eb582c03ad9b43
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/Message.java
@@ -0,0 +1,137 @@
+/* Message - A protocol message which can be send through a DataOutputStream.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+// Used to queue outgoing connections
+// sendMessage() should be used to translate them to wire format.
+class Message
+{
+  final static byte KEEP_ALIVE   = -1;
+  final static byte CHOKE        = 0;
+  final static byte UNCHOKE      = 1;
+  final static byte INTERESTED   = 2;
+  final static byte UNINTERESTED = 3;
+  final static byte HAVE         = 4;
+  final static byte BITFIELD     = 5;
+  final static byte REQUEST      = 6;
+  final static byte PIECE        = 7;
+  final static byte CANCEL       = 8;
+  
+  // Not all fields are used for every message.
+  // KEEP_ALIVE doesn't have a real wire representation
+  byte type;
+
+  // Used for HAVE, REQUEST, PIECE and CANCEL messages.
+  int piece;
+
+  // Used for REQUEST, PIECE and CANCEL messages.
+  int begin;
+  int length;
+
+  // Used for PIECE and BITFIELD messages
+  byte[] data;
+  int off;
+  int len;
+
+  /** Utility method for sending a message through a DataStream. */
+  void sendMessage(DataOutputStream dos) throws IOException
+  {
+    // KEEP_ALIVE is special.
+    if (type == KEEP_ALIVE)
+      {
+        dos.writeInt(0);
+        return;
+      }
+
+    // Calculate the total length in bytes
+
+    // Type is one byte.
+    int datalen = 1;
+
+    // piece is 4 bytes.
+    if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL)
+      datalen += 4;
+
+    // begin/offset is 4 bytes
+    if (type == REQUEST || type == PIECE || type == CANCEL)
+      datalen += 4;
+
+    // length is 4 bytes
+    if (type == REQUEST || type == CANCEL)
+      datalen += 4;
+
+    // add length of data for piece or bitfield array.
+    if (type == BITFIELD || type == PIECE)
+      datalen += len;
+
+    // Send length
+    dos.writeInt(datalen);
+    dos.writeByte(type & 0xFF);
+
+    // Send additional info (piece number)
+    if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL)
+      dos.writeInt(piece);
+
+    // Send additional info (begin/offset)
+    if (type == REQUEST || type == PIECE || type == CANCEL)
+      dos.writeInt(begin);
+
+    // Send additional info (length); for PIECE this is implicit.
+    if (type == REQUEST || type == CANCEL)
+        dos.writeInt(length);
+
+    // Send actual data
+    if (type == BITFIELD || type == PIECE)
+      dos.write(data, off, len);
+  }
+
+  public String toString()
+  {
+    switch (type)
+      {
+      case KEEP_ALIVE:
+        return "KEEP_ALIVE";
+      case CHOKE:
+        return "CHOKE";
+      case UNCHOKE:
+        return "UNCHOKE";
+      case INTERESTED:
+        return "INTERESTED";
+      case UNINTERESTED:
+        return "UNINTERESTED";
+      case HAVE:
+        return "HAVE(" + piece + ")";
+      case BITFIELD:
+        return "BITFIELD";
+      case REQUEST:
+        return "REQUEST(" + piece + "," + begin + "," + length + ")";
+      case PIECE:
+        return "PIECE(" + piece + "," + begin + "," + length + ")";
+      case CANCEL:
+        return "CANCEL(" + piece + "," + begin + "," + length + ")";
+      default:
+        return "<UNKNOWN>";
+      }
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..e63b7803d4f408bf36b747ed694063f03d49ade8
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
@@ -0,0 +1,382 @@
+/* MetaInfo - Holds all information gotten from a torrent file.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.File;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+
+import org.klomp.snark.bencode.*;
+
+public class MetaInfo
+{
+  private final String announce;
+  private final byte[] info_hash;
+  private final String name;
+  private final List files;
+  private final List lengths;
+  private final int piece_length;
+  private final byte[] piece_hashes;
+  private final long length;
+
+  private byte[] torrentdata;
+
+  MetaInfo(String announce, String name, List files, List lengths,
+           int piece_length, byte[] piece_hashes, long length)
+  {
+    this.announce = announce;
+    this.name = name;
+    this.files = files;
+    this.lengths = lengths;
+    this.piece_length = piece_length;
+    this.piece_hashes = piece_hashes;
+    this.length = length;
+
+    this.info_hash = calculateInfoHash();
+  }
+
+  /**
+   * Creates a new MetaInfo from the given InputStream.  The
+   * InputStream must start with a correctly bencoded dictonary
+   * describing the torrent.
+   */
+  public MetaInfo(InputStream in) throws IOException
+  {
+    this(new BDecoder(in));
+  }
+  
+  /**
+   * Creates a new MetaInfo from the given BDecoder.  The BDecoder
+   * must have a complete dictionary describing the torrent.
+   */
+  public MetaInfo(BDecoder be) throws IOException
+  {
+    // Note that evaluation order matters here...
+    this(be.bdecodeMap().getMap());
+  }
+
+  /**
+   * Creates a new MetaInfo from a Map of BEValues and the SHA1 over
+   * the original bencoded info dictonary (this is a hack, we could
+   * reconstruct the bencoded stream and recalculate the hash). Will
+   * throw a InvalidBEncodingException if the given map does not
+   * contain a valid announce string or info dictonary.
+   */
+  public MetaInfo(Map m) throws InvalidBEncodingException
+  {
+    BEValue val = (BEValue)m.get("announce");
+    if (val == null)
+        throw new InvalidBEncodingException("Missing announce string");
+    this.announce = val.getString();
+
+    val = (BEValue)m.get("info");
+    if (val == null)
+        throw new InvalidBEncodingException("Missing info map");
+    Map info = val.getMap();
+
+    val = (BEValue)info.get("name");
+    if (val == null)
+        throw new InvalidBEncodingException("Missing name string");
+    name = val.getString();
+
+    val = (BEValue)info.get("piece length");
+    if (val == null)
+        throw new InvalidBEncodingException("Missing piece length number");
+    piece_length = val.getInt();
+
+    val = (BEValue)info.get("pieces");
+    if (val == null)
+        throw new InvalidBEncodingException("Missing piece bytes");
+    piece_hashes = val.getBytes();
+
+    val = (BEValue)info.get("length");
+    if (val != null)
+      {
+        // Single file case.
+        length = val.getLong();
+        files = null;
+        lengths = null;
+      }
+    else
+      {
+        // Multi file case.
+        val = (BEValue)info.get("files");
+        if (val == null)
+          throw new InvalidBEncodingException
+            ("Missing length number and/or files list");
+
+        List list = val.getList();
+        int size = list.size();
+        if (size == 0)
+          throw new InvalidBEncodingException("zero size files list");
+
+        files = new ArrayList(size);
+        lengths = new ArrayList(size);
+        long l = 0;
+        for (int i = 0; i < list.size(); i++)
+          {
+            Map desc = ((BEValue)list.get(i)).getMap();
+            val = (BEValue)desc.get("length");
+            if (val == null)
+              throw new InvalidBEncodingException("Missing length number");
+            long len = val.getLong();
+            lengths.add(new Long(len));
+            l += len;
+
+            val = (BEValue)desc.get("path");
+            if (val == null)
+              throw new InvalidBEncodingException("Missing path list");
+            List path_list = val.getList();
+            int path_length = path_list.size();
+            if (path_length == 0)
+              throw new InvalidBEncodingException("zero size file path list");
+
+            List file = new ArrayList(path_length);
+            Iterator it = path_list.iterator();
+            while (it.hasNext())
+              file.add(((BEValue)it.next()).getString());
+
+            files.add(file);
+          }
+        length = l;
+      }
+
+    info_hash = calculateInfoHash();
+  }
+
+  /**
+   * Returns the string representing the URL of the tracker for this torrent.
+   */
+  public String getAnnounce()
+  {
+    return announce;
+  }
+
+  /**
+   * Returns the original 20 byte SHA1 hash over the bencoded info map.
+   */
+  public byte[] getInfoHash()
+  {
+    // XXX - Should we return a clone, just to be sure?
+    return info_hash;
+  }
+
+  /**
+   * Returns the piece hashes. Only used by storage so package local.
+   */
+  byte[] getPieceHashes()
+  {
+    return piece_hashes;
+  }
+
+  /**
+   * Returns the requested name for the file or toplevel directory.
+   * If it is a toplevel directory name getFiles() will return a
+   * non-null List of file name hierarchy name.
+   */
+  public String getName()
+  {
+    return name;
+  }
+
+  /**
+   * Returns a list of lists of file name hierarchies or null if it is
+   * a single name. It has the same size as the list returned by
+   * getLengths().
+   */
+  public List getFiles()
+  {
+    // XXX - Immutable?
+    return files;
+  }
+
+  /**
+   * Returns a list of Longs indication the size of the individual
+   * files, or null if it is a single file. It has the same size as
+   * the list returned by getFiles().
+   */
+  public List getLengths()
+  {
+    // XXX - Immutable?
+    return lengths;
+  }
+
+  /**
+   * Returns the number of pieces.
+   */
+  public int getPieces()
+  {
+    return piece_hashes.length/20;
+  }
+
+  /**
+   * Return the length of a piece. All pieces are of equal length
+   * except for the last one (<code>getPieces()-1</code>).
+   *
+   * @exception IndexOutOfBoundsException when piece is equal to or
+   * greater then the number of pieces in the torrent.
+   */
+  public int getPieceLength(int piece)
+  {
+    int pieces = getPieces();
+    if (piece >= 0 && piece < pieces -1)
+      return piece_length;
+    else if (piece == pieces -1)
+      return (int)(length - piece * piece_length);
+    else
+      throw new IndexOutOfBoundsException("no piece: " + piece);
+  }
+        
+  /**
+   * Checks that the given piece has the same SHA1 hash as the given
+   * byte array. Returns random results or IndexOutOfBoundsExceptions
+   * when the piece number is unknown.
+   */
+  public boolean checkPiece(int piece, byte[] bs, int off, int length)
+  {
+    // Check digest
+    MessageDigest sha1;
+    try
+      {
+        sha1 = MessageDigest.getInstance("SHA");
+      }
+    catch (NoSuchAlgorithmException nsae)
+      {
+        throw new InternalError("No SHA digest available: " + nsae);
+      }
+
+    sha1.update(bs, off, length);
+    byte[] hash = sha1.digest();
+    for (int i = 0; i < 20; i++)
+      if (hash[i] != piece_hashes[20 * piece + i])
+        return false;
+    return true;
+  }
+
+  /**
+   * Returns the total length of the torrent in bytes.
+   */
+  public long getTotalLength()
+  {
+    return length;
+  }
+
+  public String toString()
+  {
+    return "MetaInfo[info_hash='" + hexencode(info_hash)
+      + "', announce='" + announce
+      + "', name='" + name
+      + "', files=" + files
+      + ", #pieces='" + piece_hashes.length/20
+      + "', piece_length='" + piece_length
+      + "', length='" + length
+      + "']";
+  }
+
+  /**
+   * Encode a byte array as a hex encoded string.
+   */
+  private static String hexencode(byte[] bs)
+  {
+    StringBuffer sb = new StringBuffer(bs.length*2);
+    for (int i = 0; i < bs.length; i++)
+      {
+        int c = bs[i] & 0xFF;
+        if (c < 16)
+          sb.append('0');
+        sb.append(Integer.toHexString(c));
+      }
+
+    return sb.toString();
+  }
+
+  /**
+   * Creates a copy of this MetaInfo that shares everything except the
+   * announce URL.
+   */
+  public MetaInfo reannounce(String announce)
+  {
+    return new MetaInfo(announce, name, files,
+                        lengths, piece_length,
+                        piece_hashes, length);
+  }
+
+  public byte[] getTorrentData()
+  {
+    if (torrentdata == null)
+      {
+        Map m = new HashMap();
+        m.put("announce", announce);
+        Map info = createInfoMap();
+        m.put("info", info);
+        torrentdata = BEncoder.bencode(m);
+      }
+    return torrentdata;
+  }
+
+  private Map createInfoMap()
+  {
+    Map info = new HashMap();
+    info.put("name", name);
+    info.put("piece length", new Integer(piece_length));
+    info.put("pieces", piece_hashes);
+    if (files == null)
+      info.put("length", new Long(length));
+    else
+      {
+        List l = new ArrayList();
+        for (int i = 0; i < files.size(); i++)
+          {
+            Map file = new HashMap();
+            file.put("path", files.get(i));
+            file.put("length", lengths.get(i));
+            l.add(file);
+          }
+        info.put("files", l);
+      }
+    return info;
+  }
+
+  private byte[] calculateInfoHash()
+  {
+    Map info = createInfoMap();
+    byte[] infoBytes = BEncoder.bencode(info);
+    try
+      {
+        MessageDigest digest = MessageDigest.getInstance("SHA");
+        return digest.digest(infoBytes);
+      }
+    catch(NoSuchAlgorithmException nsa)
+      {
+        throw new InternalError(nsa.toString());
+      }
+  }
+
+  
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
new file mode 100644
index 0000000000000000000000000000000000000000..e743a9312d0344aa3f80bc1361985a46f3127b03
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
@@ -0,0 +1,388 @@
+/* Peer - All public information concerning a peer.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.*;
+import java.net.*;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.klomp.snark.bencode.*;
+
+import net.i2p.client.streaming.I2PSocket;
+
+public class Peer implements Comparable
+{
+  // Identifying property, the peer id of the other side.
+  private final PeerID peerID;
+
+  private final byte[] my_id;
+  private final MetaInfo metainfo;
+
+  // The data in/output streams set during the handshake and used by
+  // the actual connections.
+  private DataInputStream din;
+  private DataOutputStream dout;
+
+  // Keeps state for in/out connections.  Non-null when the handshake
+  // was successful, the connection setup and runs
+  PeerState state;
+
+  private boolean deregister = true;
+
+  /**
+   * Creates a disconnected peer given a PeerID, your own id and the
+   * relevant MetaInfo.
+   */
+  public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    this.peerID = peerID;
+    this.my_id = my_id;
+    this.metainfo = metainfo;
+  }
+
+  /**
+   * Creates a unconnected peer from the input and output stream got
+   * from the socket. Note that the complete handshake (which can take
+   * some time or block indefinitely) is done in the calling Thread to
+   * get the remote peer id. To completely start the connection call
+   * the connect() method.
+   *
+   * @exception IOException when an error occurred during the handshake.
+   */
+  public Peer(final I2PSocket sock, BufferedInputStream bis,
+              BufferedOutputStream bos, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    this.my_id = my_id;
+    this.metainfo = metainfo;
+
+    byte[] id  = handshake(bis, bos);
+    this.peerID = new PeerID(id, sock.getPeerDestination());
+  }
+
+  /**
+   * Returns the id of the peer.
+   */
+  public PeerID getPeerID()
+  {
+    return peerID;
+  }
+
+  /**
+   * Returns the String representation of the peerID.
+   */
+  public String toString()
+  {
+    return peerID.toString();
+  }
+
+  /**
+   * The hash code of a Peer is the hash code of the peerID.
+   */
+  public int hashCode()
+  {
+    return peerID.hashCode();
+  }
+
+  /**
+   * Two Peers are equal when they have the same PeerID.
+   * All other properties are ignored.
+   */
+  public boolean equals(Object o)
+  {
+    if (o instanceof Peer)
+      {
+        Peer p = (Peer)o;
+        return peerID.equals(p.peerID);
+      }
+    else
+      return false;
+  }
+
+  /**
+   * Compares the PeerIDs.
+   */
+  public int compareTo(Object o)
+  {
+    Peer p = (Peer)o;
+    return peerID.compareTo(p.peerID);
+  }
+
+  /**
+   * Runs the connection to the other peer. This method does not
+   * return until the connection is terminated.
+   *
+   * When the connection is correctly started the connected() method
+   * of the given PeerListener is called. If the connection ends or
+   * the connection could not be setup correctly the disconnected()
+   * method is called.
+   *
+   * If the given BitField is non-null it is send to the peer as first
+   * message.
+   */
+  public void runConnection(PeerListener listener, BitField bitfield)
+  {
+    if (state != null)
+      throw new IllegalStateException("Peer already started");
+
+    try
+      {
+        // Do we need to handshake?
+        if (din == null)
+          {
+            I2PSocket sock = I2PSnarkUtil.instance().connect(peerID);
+            BufferedInputStream bis
+              = new BufferedInputStream(sock.getInputStream());
+            BufferedOutputStream bos
+              = new BufferedOutputStream(sock.getOutputStream());
+            byte [] id = handshake(bis, bos);
+            byte [] expected_id = peerID.getID();
+            if (!Arrays.equals(expected_id, id))
+              throw new IOException("Unexpected peerID '"
+                                    + PeerID.idencode(id)
+                                    + "' expected '"
+                                    + PeerID.idencode(expected_id) + "'");
+          }
+        
+        PeerConnectionIn in = new PeerConnectionIn(this, din);
+        PeerConnectionOut out = new PeerConnectionOut(this, dout);
+        PeerState s = new PeerState(this, listener, metainfo, in, out);
+        
+        // Send our bitmap
+        if (bitfield != null)
+          s.out.sendBitfield(bitfield);
+    
+        // We are up and running!
+        state = s;
+        listener.connected(this);
+  
+        // Use this thread for running the incomming connection.
+        // The outgoing connection has created its own Thread.
+        s.in.run();
+      }
+    catch(IOException eofe)
+      {
+        // Ignore, probably just the other side closing the connection.
+        // Or refusing the connection, timing out, etc.
+      }
+    catch(Throwable t)
+      {
+        Snark.debug(this + ": " + t, Snark.ERROR);
+        t.printStackTrace();
+      }
+    finally
+      {
+        if (deregister) listener.disconnected(this);
+      }
+  }
+
+  /**
+   * Sets DataIn/OutputStreams, does the handshake and returns the id
+   * reported by the other side.
+   */
+  private byte[] handshake(BufferedInputStream bis, BufferedOutputStream bos)
+    throws IOException
+  {
+    din = new DataInputStream(bis);
+    dout = new DataOutputStream(bos);
+    
+    // Handshake write - header
+    dout.write(19);
+    dout.write("BitTorrent protocol".getBytes("UTF-8"));
+    // Handshake write - zeros
+    byte[] zeros = new byte[8];
+    dout.write(zeros);
+    // Handshake write - metainfo hash
+    byte[] shared_hash = metainfo.getInfoHash();
+    dout.write(shared_hash);
+    // Handshake write - peer id
+    dout.write(my_id);
+    dout.flush();
+    
+    // Handshake read - header
+    byte b = din.readByte();
+    if (b != 19)
+      throw new IOException("Handshake failure, expected 19, got "
+                            + (b & 0xff));
+    
+    byte[] bs = new byte[19];
+    din.readFully(bs);
+    String bittorrentProtocol = new String(bs, "UTF-8");
+    if (!"BitTorrent protocol".equals(bittorrentProtocol))
+      throw new IOException("Handshake failure, expected "
+                            + "'Bittorrent protocol', got '"
+                            + bittorrentProtocol + "'");
+    
+    // Handshake read - zeros
+    din.readFully(zeros);
+    
+    // Handshake read - metainfo hash
+    bs = new byte[20];
+    din.readFully(bs);
+    if (!Arrays.equals(shared_hash, bs))
+      throw new IOException("Unexpected MetaInfo hash");
+
+    // Handshake read - peer id
+    din.readFully(bs);
+    return bs;
+  }
+
+  public boolean isConnected()
+  {
+    return state != null;
+  }
+
+  /**
+   * Disconnects this peer if it was connected.  If deregister is
+   * true, PeerListener.disconnected() will be called when the
+   * connection is completely terminated. Otherwise the connection is
+   * silently terminated.
+   */
+  public void disconnect(boolean deregister)
+  {
+    // Both in and out connection will call this.
+    this.deregister = deregister;
+    disconnect();
+  }
+
+  void disconnect()
+  {
+    PeerState s = state;
+    if (s != null)
+      {
+        state = null;
+
+        PeerConnectionIn in = s.in;
+        if (in != null)
+          in.disconnect();
+        PeerConnectionOut out = s.out;
+        if (out != null)
+          out.disconnect();
+      }
+  }
+
+  /**
+   * Tell the peer we have another piece.
+   */
+  public void have(int piece)
+  {
+    PeerState s = state;
+    if (s != null)
+      s.havePiece(piece);
+  }
+
+  /**
+   * Whether or not the peer is interested in pieces we have. Returns
+   * false if not connected.
+   */
+  public boolean isInterested()
+  {
+    PeerState s = state;
+    return (s != null) && s.interested;
+  }
+
+  /**
+   * Sets whether or not we are interested in pieces from this peer.
+   * Defaults to false. When interest is true and this peer unchokes
+   * us then we start downloading from it. Has no effect when not connected.
+   */
+  public void setInteresting(boolean interest)
+  {
+    PeerState s = state;
+    if (s != null)
+      s.setInteresting(interest);
+  }
+
+  /**
+   * Whether or not the peer has pieces we want from it. Returns false
+   * if not connected.
+   */
+  public boolean isInteresting()
+  {
+    PeerState s = state;
+    return (s != null) && s.interesting;
+  }
+
+  /**
+   * Sets whether or not we are choking the peer. Defaults to
+   * true. When choke is false and the peer requests some pieces we
+   * upload them, otherwise requests of this peer are ignored.
+   */
+  public void setChoking(boolean choke)
+  {
+    PeerState s = state;
+    if (s != null)
+      s.setChoking(choke);
+  }
+
+  /**
+   * Whether or not we are choking the peer. Returns true when not connected.
+   */
+  public boolean isChoking()
+  {
+    PeerState s = state;
+    return (s == null) || s.choking;
+  }
+
+  /**
+   * Whether or not the peer choked us. Returns true when not connected.
+   */
+  public boolean isChoked()
+  {
+    PeerState s = state;
+    return (s == null) || s.choked;
+  }
+
+  /**
+   * Returns the number of bytes that have been downloaded.
+   * Can be reset to zero with <code>resetCounters()</code>/
+   */
+  public long getDownloaded()
+  {
+    PeerState s = state;
+    return (s != null) ? s.downloaded : 0;
+  }
+
+  /**
+   * Returns the number of bytes that have been uploaded.
+   * Can be reset to zero with <code>resetCounters()</code>/
+   */
+  public long getUploaded()
+  {
+    PeerState s = state;
+    return (s != null) ? s.uploaded : 0;
+  }
+
+  /**
+   * Resets the downloaded and uploaded counters to zero.
+   */
+  public void resetCounters()
+  {
+    PeerState s = state;
+    if (s != null)
+      {
+        s.downloaded = 0;
+        s.uploaded = 0;
+      }
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
new file mode 100644
index 0000000000000000000000000000000000000000..966a6a455fc23ab46dc301c1b4dade9a17ea5200
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
@@ -0,0 +1,62 @@
+/* PeerAcceptor - Accepts incomming connections from peers.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.*;
+import java.net.*;
+
+import net.i2p.client.streaming.I2PSocket;
+
+/**
+ * Accepts incomming connections from peers. The ConnectionAcceptor
+ * will call the connection() method when it detects an incomming BT
+ * protocol connection. The PeerAcceptor will then create a new peer
+ * if the PeerCoordinator wants more peers.
+ */
+public class PeerAcceptor
+{
+  private final PeerCoordinator coordinator;
+
+  public PeerAcceptor(PeerCoordinator coordinator)
+  {
+    this.coordinator = coordinator;
+  }
+
+  public void connection(I2PSocket socket,
+                         BufferedInputStream bis, BufferedOutputStream bos)
+    throws IOException
+  {
+    if (coordinator.needPeers())
+      {
+        // XXX: inside this Peer constructor's handshake is where you'd deal with the other
+        //      side saying they want to communicate with another torrent - aka multitorrent
+        //      support.  you'd then want to grab the meta info /they/ want, look that up in
+        //      our own list of active torrents, and put it on the right coordinator for it.
+        //      this currently, however, throws an IOException if the metainfo doesn't match
+        //      coodinator.getMetaInfo (Peer.java:242)
+        Peer peer = new Peer(socket, bis, bos, coordinator.getID(),
+                             coordinator.getMetaInfo());
+        coordinator.addPeer(peer);
+      }
+    else
+      socket.close();
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..041387937def4805f64f966f4fb63b2ae3e4e1ad
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java
@@ -0,0 +1,197 @@
+/* PeerCheckTasks - TimerTask that checks for good/bad up/downloaders.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.util.*;
+
+/**
+ * TimerTask that checks for good/bad up/downloader. Works together
+ * with the PeerCoordinator to select which Peers get (un)choked.
+ */
+class PeerCheckerTask extends TimerTask
+{
+  private final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000);
+
+  private final PeerCoordinator coordinator;
+
+  PeerCheckerTask(PeerCoordinator coordinator)
+  {
+    this.coordinator = coordinator;
+  }
+
+  public void run()
+  {
+    synchronized(coordinator.peers)
+      {
+        // Calculate total uploading and worst downloader.
+        long worstdownload = Long.MAX_VALUE;
+        Peer worstDownloader = null;
+
+        int peers = 0;
+        int uploaders = 0;
+        int downloaders = 0;
+        int interested = 0;
+        int interesting = 0;
+        int choking = 0;
+        int choked = 0;
+
+        long uploaded = 0;
+        long downloaded = 0;
+
+        // Keep track of peers we remove now,
+        // we will add them back to the end of the list.
+        List removed = new ArrayList();
+
+        Iterator it = coordinator.peers.iterator();
+        while (it.hasNext())
+          {
+            Peer peer = (Peer)it.next();
+
+            // Remove dying peers
+            if (!peer.isConnected())
+              {
+                it.remove();
+                continue;
+              }
+
+            peers++;
+
+            if (!peer.isChoking())
+              uploaders++;
+            if (!peer.isChoked() && peer.isInteresting())
+              downloaders++;
+            if (peer.isInterested())
+              interested++;
+            if (peer.isInteresting())
+              interesting++;
+            if (peer.isChoking())
+              choking++;
+            if (peer.isChoked())
+              choked++;
+
+            // XXX - We should calculate the up/download rate a bit
+            // more intelligently
+            long upload = peer.getUploaded();
+            uploaded += upload;
+            long download = peer.getDownloaded();
+            downloaded += download;
+            peer.resetCounters();
+
+            if (Snark.debug >= Snark.DEBUG)
+              {
+                Snark.debug(peer + ":", Snark.DEBUG);
+                Snark.debug(" ul: " + upload/KILOPERSECOND
+                            + " dl: " + download/KILOPERSECOND
+                            + " i: " + peer.isInterested()
+                            + " I: " + peer.isInteresting()
+                            + " c: " + peer.isChoking()
+                            + " C: " + peer.isChoked(),
+                            Snark.DEBUG);
+              }
+
+            // If we are at our max uploaders and we have lots of other
+            // interested peers try to make some room.
+            // (Note use of coordinator.uploaders)
+            if (coordinator.uploaders >= PeerCoordinator.MAX_UPLOADERS
+                && interested > PeerCoordinator.MAX_UPLOADERS
+                && !peer.isChoking())
+              {
+                // Check if it still wants pieces from us.
+                if (!peer.isInterested())
+                  {
+                    if (Snark.debug >= Snark.INFO)
+                      Snark.debug("Choke uninterested peer: " + peer,
+                                  Snark.INFO);
+                    peer.setChoking(true);
+                    uploaders--;
+                    coordinator.uploaders--;
+                    
+                    // Put it at the back of the list
+                    it.remove();
+                    removed.add(peer);
+                  }
+                else if (peer.isChoked())
+                  {
+                    // If they are choking us make someone else a downloader
+                    if (Snark.debug >= Snark.DEBUG)
+                      Snark.debug("Choke choking peer: " + peer, Snark.DEBUG);
+                    peer.setChoking(true);
+                    uploaders--;
+                    coordinator.uploaders--;
+                    
+                    // Put it at the back of the list
+                    it.remove();
+                    removed.add(peer);
+                  }
+                else if (peer.isInteresting()
+                         && !peer.isChoked()
+                         && download == 0)
+                  {
+                    // We are downloading but didn't receive anything...
+                    if (Snark.debug >= Snark.DEBUG)
+                      Snark.debug("Choke downloader that doesn't deliver:"
+                                  + peer, Snark.DEBUG);
+                    peer.setChoking(true);
+                    uploaders--;
+                    coordinator.uploaders--;
+                    
+                    // Put it at the back of the list
+                    it.remove();
+                    removed.add(peer);
+                  }
+                else if (!peer.isChoking() && download < worstdownload)
+                  {
+                    // Make sure download is good if we are uploading
+                    worstdownload = download;
+                    worstDownloader = peer;
+                  }
+              }
+          }
+
+        // Resync actual uploaders value
+        // (can shift a bit by disconnecting peers)
+        coordinator.uploaders = uploaders;
+
+        // Remove the worst downloader if needed.
+        if (uploaders >= PeerCoordinator.MAX_UPLOADERS
+            && interested > PeerCoordinator.MAX_UPLOADERS
+            && worstDownloader != null)
+          {
+            if (Snark.debug >= Snark.DEBUG)
+              Snark.debug("Choke worst downloader: " + worstDownloader,
+                          Snark.DEBUG);
+
+            worstDownloader.setChoking(true);
+            coordinator.uploaders--;
+
+            // Put it at the back of the list
+            coordinator.peers.remove(worstDownloader);
+            removed.add(worstDownloader);
+          }
+        
+        // Optimistically unchoke a peer
+        coordinator.unchokePeer();
+
+        // Put peers back at the end of the list that we removed earlier.
+        coordinator.peers.addAll(removed);
+      }
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3d5f95bef0e557af31db3e1e98e013ee0153f69
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
@@ -0,0 +1,156 @@
+/* PeerConnectionIn - Handles incomming messages and hands them to PeerState.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+class PeerConnectionIn implements Runnable
+{
+  private final Peer peer;
+  private final DataInputStream din;
+
+  private Thread thread;
+  private boolean quit;
+
+  public PeerConnectionIn(Peer peer, DataInputStream din)
+  {
+    this.peer = peer;
+    this.din = din;
+    quit = false;
+  }
+
+  void disconnect()
+  {
+    if (quit == true)
+      return;
+
+    quit = true;
+    Thread t = thread;
+    if (t != null)
+      t.interrupt();
+  }
+
+  public void run()
+  {
+    thread = Thread.currentThread();
+    try
+      {
+        PeerState ps = peer.state;
+        while (!quit && ps != null)
+          {
+            // Common variables used for some messages.
+            int piece;
+            int begin;
+            int len;
+        
+            // Wait till we hear something...
+            // The length of a complete message in bytes.
+            int i = din.readInt();
+            if (i < 0)
+              throw new IOException("Unexpected length prefix: " + i);
+
+            if (i == 0)
+              {
+                ps.keepAliveMessage();
+                continue;
+              }
+            
+            byte b = din.readByte();
+            Message m = new Message();
+            m.type = b;
+            switch (b)
+              {
+              case 0:
+                ps.chokeMessage(true);
+                break;
+              case 1:
+                ps.chokeMessage(false);
+                break;
+              case 2:
+                ps.interestedMessage(true);
+                break;
+              case 3:
+                ps.interestedMessage(false);
+                break;
+              case 4:
+                piece = din.readInt();
+                ps.haveMessage(piece);
+                break;
+              case 5:
+                byte[] bitmap = new byte[i-1];
+                din.readFully(bitmap);
+                ps.bitfieldMessage(bitmap);
+                break;
+              case 6:
+                piece = din.readInt();
+                begin = din.readInt();
+                len = din.readInt();
+                ps.requestMessage(piece, begin, len);
+                break;
+              case 7:
+                piece = din.readInt();
+                begin = din.readInt();
+                len = i-9;
+                Request req = ps.getOutstandingRequest(piece, begin, len);
+                byte[] piece_bytes;
+                if (req != null)
+                  {
+                    piece_bytes = req.bs;
+                    din.readFully(piece_bytes, begin, len);
+                    ps.pieceMessage(req);
+                  }
+                else
+                  {
+                    // XXX - Consume but throw away afterwards.
+                    piece_bytes = new byte[len];
+                    din.readFully(piece_bytes);
+                  }
+                break;
+              case 8:
+                piece = din.readInt();
+                begin = din.readInt();
+                len = din.readInt();
+                ps.cancelMessage(piece, begin, len);
+                break;
+              default:
+                byte[] bs = new byte[i-1];
+                din.readFully(bs);
+                ps.unknownMessage(b, bs);
+              }
+          }
+      }
+    catch (IOException ioe)
+      {
+        // Ignore, probably the other side closed connection.
+      }
+    catch (Throwable t)
+      {
+        Snark.debug(peer + ": " + t, Snark.ERROR);
+        t.printStackTrace();
+      }
+    finally
+      {
+        peer.disconnect();
+      }
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d09859a74686a71ded1814caeeca289f2928457
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
@@ -0,0 +1,342 @@
+/* PeerConnectionOut - Keeps a queue of outgoing messages and delivers them.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+class PeerConnectionOut implements Runnable
+{
+  private final Peer peer;
+  private final DataOutputStream dout;
+
+  private Thread thread;
+  private boolean quit;
+
+  // Contains Messages.
+  private List sendQueue = new ArrayList();
+
+  public PeerConnectionOut(Peer peer, DataOutputStream dout)
+  {
+    this.peer = peer;
+    this.dout = dout;
+
+    quit = false;
+    thread = new Thread(this);
+    thread.start();
+  }
+
+  /**
+   * Continuesly monitors for more outgoing messages that have to be send.
+   * Stops if quit is true of an IOException occurs.
+   */
+  public void run()
+  {
+    try
+      {
+        while (!quit)
+          {
+            Message m = null;
+            PeerState state = null;
+            synchronized(sendQueue)
+              {
+                while (!quit && sendQueue.isEmpty())
+                  {
+                    try
+                      {
+                        // Make sure everything will reach the other side.
+                        dout.flush();
+                        
+                        // Wait till more data arrives.
+                        sendQueue.wait();
+                      }
+                    catch (InterruptedException ie)
+                      {
+                        /* ignored */
+                      }
+                  }
+                state = peer.state;
+                if (!quit && state != null)
+                  {
+                    // Piece messages are big. So if there are other
+                    // (control) messages make sure they are send first.
+                    // Also remove request messages from the queue if
+                    // we are currently being choked to prevent them from
+                    // being send even if we get unchoked a little later.
+                    // (Since we will resent them anyway in that case.)
+                    // And remove piece messages if we are choking.
+                    Iterator it = sendQueue.iterator();
+                    while (m == null && it.hasNext())
+                      {
+                        Message nm = (Message)it.next();
+                        if (nm.type == Message.PIECE)
+                          {
+                            if (state.choking)
+                              it.remove();
+                            nm = null;
+                          }
+                        else if (nm.type == Message.REQUEST && state.choked)
+                          {
+                            it.remove();
+                            nm = null;
+                          }
+                          
+                        if (m == null && nm != null)
+                          {
+                            m = nm;
+                            it.remove();
+                          }
+                      }
+                    if (m == null && sendQueue.size() > 0)
+                      m = (Message)sendQueue.remove(0);
+                  }
+              }
+            if (m != null)
+              {
+                if (Snark.debug >= Snark.ALL)
+                  Snark.debug("Send " + peer + ": " + m, Snark.ALL);
+                m.sendMessage(dout);
+
+                // Remove all piece messages after sending a choke message.
+                if (m.type == Message.CHOKE)
+                  removeMessage(Message.PIECE);
+
+                // XXX - Should also register overhead...
+                if (m.type == Message.PIECE)
+                  state.uploaded(m.len);
+
+                m = null;
+              }
+          }
+      }
+    catch (IOException ioe)
+      {
+        // Ignore, probably other side closed connection.
+      }
+    catch (Throwable t)
+      {
+        Snark.debug(peer + ": "  + t, Snark.ERROR);
+        t.printStackTrace();
+      }
+    finally
+      {
+        quit = true;
+        peer.disconnect();
+      }
+  }
+
+  public void disconnect()
+  {
+    synchronized(sendQueue)
+      {
+        if (quit == true)
+          return;
+        
+        quit = true;
+        thread.interrupt();
+        
+        sendQueue.clear();
+        sendQueue.notify();
+      }
+  }
+
+  /**
+   * Adds a message to the sendQueue and notifies the method waiting
+   * on the sendQueue to change.
+   */
+  private void addMessage(Message m)
+  {
+    synchronized(sendQueue)
+      {
+        sendQueue.add(m);
+        sendQueue.notify();
+      }
+  }
+
+  /**
+   * Removes a particular message type from the queue.
+   *
+   * @param type the Message type to remove.
+   * @returns true when a message of the given type was removed, false
+   * otherwise.
+   */
+  private boolean removeMessage(int type)
+  {
+    boolean removed = false;
+    synchronized(sendQueue)
+      {
+        Iterator it = sendQueue.iterator();
+        while (it.hasNext())
+          {
+            Message m = (Message)it.next();
+            if (m.type == type)
+              {
+                it.remove();
+                removed = true;
+              }
+          }
+      }
+    return removed;
+  }
+
+  void sendAlive()
+  {
+    Message m = new Message();
+    m.type = Message.KEEP_ALIVE;
+    addMessage(m);
+  }
+
+  void sendChoke(boolean choke)
+  {
+    // We cancel the (un)choke but keep PIECE messages.
+    // PIECE messages are purged if a choke is actually send.
+    synchronized(sendQueue)
+      {
+        int inverseType  = choke ? Message.UNCHOKE
+                                 : Message.CHOKE;
+        if (!removeMessage(inverseType))
+          {
+            Message m = new Message();
+            if (choke)
+              m.type = Message.CHOKE;
+            else
+              m.type = Message.UNCHOKE;
+            addMessage(m);
+          }
+      }
+  }
+
+  void sendInterest(boolean interest)
+  {
+    synchronized(sendQueue)
+      {
+        int inverseType  = interest ? Message.UNINTERESTED
+                                    : Message.INTERESTED;
+        if (!removeMessage(inverseType))
+          {
+            Message m = new Message();
+            if (interest)
+              m.type = Message.INTERESTED;
+            else
+              m.type = Message.UNINTERESTED;
+            addMessage(m);
+          }
+      }
+  }
+
+  void sendHave(int piece)
+  {
+    Message m = new Message();
+    m.type = Message.HAVE;
+    m.piece = piece;
+    addMessage(m);
+  }
+
+  void sendBitfield(BitField bitfield)
+  {
+    Message m = new Message();
+    m.type = Message.BITFIELD;
+    m.data = bitfield.getFieldBytes();
+    m.off = 0;
+    m.len = m.data.length;
+    addMessage(m);
+  }
+
+  void sendRequests(List requests)
+  {
+    Iterator it = requests.iterator();
+    while (it.hasNext())
+      {
+        Request req = (Request)it.next();
+        sendRequest(req);
+      }
+  }
+
+  void sendRequest(Request req)
+  {
+    Message m = new Message();
+    m.type = Message.REQUEST;
+    m.piece = req.piece;
+    m.begin = req.off;
+    m.length = req.len;
+    addMessage(m);
+  }
+
+  void sendPiece(int piece, int begin, int length, byte[] bytes)
+  {
+    Message m = new Message();
+    m.type = Message.PIECE;
+    m.piece = piece;
+    m.begin = begin;
+    m.length = length;
+    m.data = bytes;
+    m.off = begin;
+    m.len = length;
+    addMessage(m);
+  }
+
+  void sendCancel(Request req)
+  {
+    // See if it is still in our send queue
+    synchronized(sendQueue)
+      {
+        Iterator it = sendQueue.iterator();
+        while (it.hasNext())
+          {
+            Message m = (Message)it.next();
+            if (m.type == Message.REQUEST
+                && m.piece == req.piece
+                && m.begin == req.off
+                && m.length == req.len)
+              it.remove();
+          }
+      }
+
+    // Always send, just to be sure it it is really canceled.
+    Message m = new Message();
+    m.type = Message.CANCEL;
+    m.piece = req.piece;
+    m.begin = req.off;
+    m.length = req.len;
+    addMessage(m);
+  }
+
+  // Called by the PeerState when the other side doesn't want this
+  // request to be handled anymore. Removes any pending Piece Message
+  // from out send queue.
+  void cancelRequest(int piece, int begin, int length)
+  {
+    synchronized (sendQueue)
+      {
+        Iterator it = sendQueue.iterator();
+        while (it.hasNext())
+          {
+            Message m = (Message)it.next();
+            if (m.type == Message.PIECE
+                && m.piece == piece
+                && m.begin == begin
+                && m.length == length)
+              it.remove();
+          }
+      }
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fb39ce96b9f43d6a5270dc8f7b344373fb1083d
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
@@ -0,0 +1,508 @@
+/* PeerCoordinator - Coordinates which peers do what (up and downloading).
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.util.*;
+import java.io.IOException;
+
+/**
+ * Coordinates what peer does what.
+ */
+public class PeerCoordinator implements PeerListener
+{
+  final MetaInfo metainfo;
+  final Storage storage;
+
+  // package local for access by CheckDownLoadersTask
+  final static long CHECK_PERIOD = 20*1000; // 20 seconds
+  final static int MAX_CONNECTIONS = 24;
+  final static int MAX_UPLOADERS = 12; // i2p: might as well balance it out
+
+  // Approximation of the number of current uploaders.
+  // Resynced by PeerChecker once in a while.
+  int uploaders = 0;
+
+  // final static int MAX_DOWNLOADERS = MAX_CONNECTIONS;
+  // int downloaders = 0;
+
+  private long uploaded;
+  private long downloaded;
+
+  // synchronize on this when changing peers or downloaders
+  final List peers = new ArrayList();
+
+  /** Timer to handle all periodical tasks. */
+  private final Timer timer = new Timer(true);
+
+  private final byte[] id;
+
+  // Some random wanted pieces
+  private final List wantedPieces;
+
+  private boolean halted = false;
+
+  private final CoordinatorListener listener;
+
+  public PeerCoordinator(byte[] id, MetaInfo metainfo, Storage storage,
+                         CoordinatorListener listener)
+  {
+    this.id = id;
+    this.metainfo = metainfo;
+    this.storage = storage;
+    this.listener = listener;
+
+    // Make a random list of piece numbers
+    wantedPieces = new ArrayList();
+    BitField bitfield = storage.getBitField();
+    for(int i = 0; i < metainfo.getPieces(); i++)
+      if (!bitfield.get(i))
+        wantedPieces.add(new Integer(i));
+    Collections.shuffle(wantedPieces);
+
+    // Install a timer to check the uploaders.
+    timer.schedule(new PeerCheckerTask(this), CHECK_PERIOD, CHECK_PERIOD);
+  }
+
+  public byte[] getID()
+  {
+    return id;
+  }
+
+  public boolean completed()
+  {
+    return storage.complete();
+  }
+
+
+  public int getPeers()
+  {
+    synchronized(peers)
+      {
+        return peers.size();
+      }
+  }
+
+  /**
+   * Returns how many bytes are still needed to get the complete file.
+   */
+  public long getLeft()
+  {
+    // XXX - Only an approximation.
+    return storage.needed() * metainfo.getPieceLength(0);
+  }
+
+  /**
+   * Returns the total number of uploaded bytes of all peers.
+   */
+  public long getUploaded()
+  {
+    return uploaded;
+  }
+
+  /**
+   * Returns the total number of downloaded bytes of all peers.
+   */
+  public long getDownloaded()
+  {
+    return downloaded;
+  }
+
+  public MetaInfo getMetaInfo()
+  {
+    return metainfo;
+  }
+
+  public boolean needPeers()
+  {
+    synchronized(peers)
+      {
+        return !halted && peers.size() < MAX_CONNECTIONS;
+      }
+  }
+
+  public void halt()
+  {
+    halted = true;
+    synchronized(peers)
+      {
+        // Stop peer checker task.
+        timer.cancel();
+
+        // Stop peers.
+        Iterator it = peers.iterator();
+        while(it.hasNext())
+          {
+            Peer peer = (Peer)it.next();
+            peer.disconnect();
+            it.remove();
+          }
+      }
+  }
+
+  public void connected(Peer peer)
+  {
+    if (halted)
+      {
+        peer.disconnect(false);
+        return;
+      }
+
+    synchronized(peers)
+      {
+        if (peerIDInList(peer.getPeerID(), peers))
+          {
+            if (Snark.debug >= Snark.INFO)
+              Snark.debug("Already connected to: " + peer, Snark.INFO);
+            peer.disconnect(false); // Don't deregister this connection/peer.
+          }
+        else
+          {
+            if (Snark.debug >= Snark.INFO)
+              Snark.debug("New connection to peer: " + peer, Snark.INFO);
+
+            // Add it to the beginning of the list.
+            // And try to optimistically make it a uploader.
+            peers.add(0, peer);
+            unchokePeer();
+
+            if (listener != null)
+              listener.peerChange(this, peer);
+          }
+      }
+  }
+
+  private static boolean peerIDInList(PeerID pid, List peers)
+  {
+    Iterator it = peers.iterator();
+    while (it.hasNext())
+      if (pid.sameID(((Peer)it.next()).getPeerID()))
+        return true;
+    return false;
+  }
+
+  public void addPeer(final Peer peer)
+  {
+    if (halted)
+      {
+        peer.disconnect(false);
+        return;
+      }
+
+    boolean need_more;
+    synchronized(peers)
+      {
+        need_more = !peer.isConnected() && peers.size() < MAX_CONNECTIONS;
+      }
+
+    if (need_more)
+      {
+        // Run the peer with us as listener and the current bitfield.
+        final PeerListener listener = this;
+        final BitField bitfield = storage.getBitField();
+        Runnable r = new Runnable()
+          {
+            public void run()
+            {
+              peer.runConnection(listener, bitfield);
+            }
+          };
+        String threadName = peer.toString();
+        new Thread(r, threadName).start();
+      }
+    else
+      if (Snark.debug >= Snark.INFO)
+        if (peer.isConnected())
+          Snark.debug("Add peer already connected: " + peer, Snark.INFO);
+        else
+          Snark.debug("MAX_CONNECTIONS = " + MAX_CONNECTIONS
+                      + " not accepting extra peer: " + peer, Snark.INFO);
+  }
+
+
+  // (Optimistically) unchoke. Should be called with peers synchronized
+  void unchokePeer()
+  {
+    // linked list will contain all interested peers that we choke.
+    // At the start are the peers that have us unchoked at the end the
+    // other peer that are interested, but are choking us.
+    List interested = new LinkedList();
+    Iterator it = peers.iterator();
+    while (it.hasNext())
+      {
+        Peer peer = (Peer)it.next();
+        boolean remove = false;
+        if (uploaders < MAX_UPLOADERS
+            && peer.isChoking()
+            && peer.isInterested())
+          {
+            if (!peer.isChoked())
+              interested.add(0, peer);
+            else
+              interested.add(peer);
+          }
+      }
+
+    while (uploaders < MAX_UPLOADERS && interested.size() > 0)
+      {
+        Peer peer = (Peer)interested.remove(0);
+        if (Snark.debug >= Snark.INFO)
+          Snark.debug("Unchoke: " + peer, Snark.INFO);
+        peer.setChoking(false);
+        uploaders++;
+        // Put peer back at the end of the list.
+        peers.remove(peer);
+        peers.add(peer);
+      }
+  }
+
+  public byte[] getBitMap()
+  {
+    return storage.getBitField().getFieldBytes();
+  }
+
+  /**
+   * Returns true if we don't have the given piece yet.
+   */
+  public boolean gotHave(Peer peer, int piece)
+  {
+    if (listener != null)
+      listener.peerChange(this, peer);
+
+    synchronized(wantedPieces)
+      {
+        return wantedPieces.contains(new Integer(piece));
+      }
+  }
+
+  /**
+   * Returns true if the given bitfield contains at least one piece we
+   * are interested in.
+   */
+  public boolean gotBitField(Peer peer, BitField bitfield)
+  {
+    if (listener != null)
+      listener.peerChange(this, peer);
+
+    synchronized(wantedPieces)
+      {
+        Iterator it = wantedPieces.iterator();
+        while (it.hasNext())
+          {
+            int i = ((Integer)it.next()).intValue();
+            if (bitfield.get(i))
+              return true;
+          }
+      }
+    return false;
+  }
+
+  /**
+   * Returns one of pieces in the given BitField that is still wanted or
+   * -1 if none of the given pieces are wanted.
+   */
+  public int wantPiece(Peer peer, BitField havePieces)
+  {
+    if (halted)
+      return -1;
+
+    synchronized(wantedPieces)
+      {
+        Integer piece = null;
+        Iterator it = wantedPieces.iterator();
+        while (piece == null && it.hasNext())
+          {
+            Integer i = (Integer)it.next();
+            if (havePieces.get(i.intValue()))
+              {
+                it.remove();
+                piece = i;
+              }
+          }
+
+        if (piece == null)
+          return -1;
+
+        // We add it back at the back of the list. It will be removed
+        // if gotPiece is called later. This means that the last
+        // couple of pieces might very well be asked from multiple
+        // peers but that is OK.
+        wantedPieces.add(piece);
+
+        return piece.intValue();
+      }
+  }
+
+  /**
+   * Returns a byte array containing the requested piece or null of
+   * the piece is unknown.
+   */
+  public byte[] gotRequest(Peer peer, int piece)
+  {
+    if (halted)
+      return null;
+
+    try
+      {
+        return storage.getPiece(piece);
+      }
+    catch (IOException ioe)
+      {
+        Snark.fatal("Error reading storage", ioe);
+        return null; // Never reached.
+      }
+  }
+
+  /**
+   * Called when a peer has uploaded some bytes of a piece.
+   */
+  public void uploaded(Peer peer, int size)
+  {
+    uploaded += size;
+
+    if (listener != null)
+      listener.peerChange(this, peer);
+  }
+
+  /**
+   * Called when a peer has downloaded some bytes of a piece.
+   */
+  public void downloaded(Peer peer, int size)
+  {
+    downloaded += size;
+
+    if (listener != null)
+      listener.peerChange(this, peer);
+  }
+
+  /**
+   * Returns false if the piece is no good (according to the hash).
+   * In that case the peer that supplied the piece should probably be
+   * blacklisted.
+   */
+  public boolean gotPiece(Peer peer, int piece, byte[] bs)
+  {
+    if (halted)
+      return true; // We don't actually care anymore.
+    
+    synchronized(wantedPieces)
+      {
+        Integer p = new Integer(piece);
+        if (!wantedPieces.contains(p))
+          {
+            if (Snark.debug >= Snark.INFO)
+              Snark.debug(peer + " piece " + piece + " no longer needed",
+                          Snark.INFO);
+            
+            // No need to announce have piece to peers.
+            // Assume we got a good piece, we don't really care anymore.
+            return true;
+          }
+        
+        try
+          {
+            if (storage.putPiece(piece, bs))
+              {
+                if (Snark.debug >= Snark.INFO)
+                  Snark.debug("Recv p" + piece + " " + peer, Snark.INFO);
+              }
+            else
+              {
+                // Oops. We didn't actually download this then... :(
+                downloaded -= metainfo.getPieceLength(piece);
+                if (Snark.debug >= Snark.NOTICE)
+                  Snark.debug("Got BAD piece " + piece + " from " + peer,
+                              Snark.NOTICE);
+                return false; // No need to announce BAD piece to peers.
+              }
+          }
+        catch (IOException ioe)
+          {
+            Snark.fatal("Error writing storage", ioe);
+          }
+        wantedPieces.remove(p);
+      }
+
+    // Announce to the world we have it!
+    synchronized(peers)
+      {
+        Iterator it = peers.iterator();
+        while (it.hasNext())
+          {
+            Peer p = (Peer)it.next();
+            if (p.isConnected())
+              p.have(piece);
+          }
+      }
+    
+    return true;
+  }
+
+  public void gotChoke(Peer peer, boolean choke)
+  {
+    if (Snark.debug >= Snark.INFO)
+      Snark.debug("Got choke(" + choke + "): " + peer, Snark.INFO);
+
+    if (listener != null)
+      listener.peerChange(this, peer);
+  }
+
+  public void gotInterest(Peer peer, boolean interest)
+  {
+    if (interest)
+      {
+        synchronized(peers)
+          {
+            if (uploaders < MAX_UPLOADERS)
+              {
+                if(peer.isChoking())
+                  {
+                    uploaders++;
+                    peer.setChoking(false);
+                    if (Snark.debug >= Snark.INFO)
+                      Snark.debug("Unchoke: " + peer, Snark.INFO);
+                  }
+              }
+          }
+      }
+
+    if (listener != null)
+      listener.peerChange(this, peer);
+  }
+
+  public void disconnected(Peer peer)
+  {
+    if (Snark.debug >= Snark.INFO)
+      Snark.debug("Disconnected " + peer, Snark.INFO);
+    
+    synchronized(peers)
+      {
+        // Make sure it is no longer in our lists
+        if (peers.remove(peer))
+          {
+            // Unchoke some random other peer
+            unchokePeer();
+          }
+      }
+
+    if (listener != null)
+      listener.peerChange(this, peer);
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerID.java b/apps/i2psnark/java/src/org/klomp/snark/PeerID.java
new file mode 100644
index 0000000000000000000000000000000000000000..2788e9e5bef3f8c0fb5a61def0109dfcde3d8d54
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerID.java
@@ -0,0 +1,208 @@
+/* PeerID - All public information concerning a peer.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Map;
+
+import org.klomp.snark.bencode.*;
+
+import net.i2p.data.Base64;
+import net.i2p.data.Destination;
+import net.i2p.data.DataFormatException;
+
+public class PeerID implements Comparable
+{
+  private final byte[] id;
+  private final Destination address;
+  private final int port;
+
+  private final int hash;
+
+  public PeerID(byte[] id, Destination address)
+  {
+    this.id = id;
+    this.address = address;
+    this.port = 6881;
+
+    hash = calculateHash();
+  }
+
+  /**
+   * Creates a PeerID from a BDecoder.
+   */
+  public PeerID(BDecoder be)
+    throws IOException
+  {
+    this(be.bdecodeMap().getMap());
+  }
+
+  /**
+   * Creates a PeerID from a Map containing BEncoded peer id, ip and
+   * port.
+   */
+  public PeerID(Map m)
+    throws InvalidBEncodingException, UnknownHostException
+  {
+    BEValue bevalue = (BEValue)m.get("peer id");
+    if (bevalue == null)
+      throw new InvalidBEncodingException("peer id missing");
+    id = bevalue.getBytes();
+
+    bevalue = (BEValue)m.get("ip");
+    if (bevalue == null)
+      throw new InvalidBEncodingException("ip missing");
+    address = I2PSnarkUtil.instance().getDestination(bevalue.getString());
+    if (address == null)
+        throw new InvalidBEncodingException("Invalid destination [" + bevalue.getString() + "]");
+
+    port = 6881;
+
+    hash = calculateHash();
+  }
+
+  public byte[] getID()
+  {
+    return id;
+  }
+
+  public Destination getAddress()
+  {
+    return address;
+  }
+
+  public int getPort()
+  {
+    return port;
+  }
+
+  private int calculateHash()
+  {
+    int b = 0;
+    for (int i = 0; i < id.length; i++)
+      b ^= id[i];
+    return (b ^ address.hashCode()) ^ port;
+  }
+
+  /**
+   * The hash code of a PeerID is the exclusive or of all id bytes.
+   */
+  public int hashCode()
+  {
+    return hash;
+  }
+
+  /**
+   * Returns true if and only if this peerID and the given peerID have
+   * the same 20 bytes as ID.
+   */
+  public boolean sameID(PeerID pid)
+  {
+    boolean equal = true;
+    for (int i = 0; equal && i < id.length; i++)
+      equal = id[i] == pid.id[i];
+    return equal;
+  }
+
+  /**
+   * Two PeerIDs are equal when they have the same id, address and port.
+   */
+  public boolean equals(Object o)
+  {
+    if (o instanceof PeerID)
+      {
+        PeerID pid = (PeerID)o;
+
+        return port == pid.port
+          && address.equals(pid.address)
+          && sameID(pid);
+      }
+    else
+      return false;
+  }
+
+  /**
+   * Compares port, address and id.
+   */
+  public int compareTo(Object o)
+  {
+    PeerID pid = (PeerID)o;
+
+    int result = port - pid.port;
+    if (result != 0)
+      return result;
+
+    result = address.hashCode() - pid.address.hashCode();
+    if (result != 0)
+      return result;
+
+    for (int i = 0; i < id.length; i++)
+      {
+        result = id[i] - pid.id[i];
+        if (result != 0)
+          return result;
+      }
+
+    return 0;
+  }
+
+  /**
+   * Returns the String "id@address" where id is the base64 encoded id.
+   */
+  public String toString()
+  {
+    int nonZero = 0;
+    for (int i = 0; i < id.length; i++) {
+        if (id[i] != 0) {
+            nonZero = i;
+            break;
+        }
+    }
+    return Base64.encode(id, nonZero, id.length-nonZero).substring(0,4) + "@" + address.calculateHash().toBase64().substring(0,6);
+  }
+
+  /**
+   * Encode an id as a hex encoded string and remove leading zeros.
+   */
+  public static String idencode(byte[] bs)
+  {
+    boolean leading_zeros = true;
+
+    StringBuffer sb = new StringBuffer(bs.length*2);
+    for (int i = 0; i < bs.length; i++)
+      {
+        int c = bs[i] & 0xFF;
+        if (leading_zeros && c == 0)
+          continue;
+        else
+          leading_zeros = false;
+
+        if (c < 16)
+          sb.append('0');
+        sb.append(Integer.toHexString(c));
+      }
+
+    return sb.toString();
+  }
+
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..959b42fa1492a230fa6fc531614360e18a92784d
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java
@@ -0,0 +1,145 @@
+/* PeerListener - Interface for listening to peer events.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+/**
+ * Listener for Peer events.
+ */
+public interface PeerListener
+{
+  /**
+   * Called when the connection to the peer has started and the
+   * handshake was successfull.
+   *
+   * @param peer the Peer that just got connected.
+   */
+  void connected(Peer peer);
+
+  /**
+   * Called when the connection to the peer was terminated or the
+   * connection handshake failed.
+   *
+   * @param peer the Peer that just got disconnected.
+   */
+  void disconnected(Peer peer);
+
+  /**
+   * Called when a choke message is received.
+   *
+   * @param peer the Peer that got the message.
+   * @param choke true when the peer got a choke message, false when
+   * the peer got an unchoke message.
+   */
+  void gotChoke(Peer peer, boolean choke);
+
+  /**
+   * Called when an interested message is received.
+   *
+   * @param peer the Peer that got the message.
+   * @param interest true when the peer got a interested message, false when
+   * the peer got an uninterested message.
+   */
+  void gotInterest(Peer peer, boolean interest);
+
+  /**
+   * Called when a have piece message is received. If the method
+   * returns true and the peer has not yet received a interested
+   * message or we indicated earlier to be not interested then an
+   * interested message will be send.
+   *
+   * @param peer the Peer that got the message.
+   * @param piece the piece number that the per just got.
+   *
+   * @return true when it is a piece that we want, false if the piece is
+   * already known.
+   */
+  boolean gotHave(Peer peer, int piece);
+
+  /**
+   * Called when a bitmap message is received. If this method returns
+   * true a interested message will be send back to the peer.
+   *
+   * @param peer the Peer that got the message.
+   * @param bitfield a BitField containing the pieces that the other
+   * side has.
+   *
+   * @return true when the BitField contains pieces we want, false if
+   * the piece is already known.
+   */
+  boolean gotBitField(Peer peer, BitField bitfield);
+
+  /**
+   * Called when a piece is received from the peer. The piece must be
+   * requested by Peer.request() first. If this method returns false
+   * that means the Peer provided a corrupted piece and the connection
+   * will be closed.
+   *
+   * @param peer the Peer that got the piece.
+   * @param piece the piece number received.
+   * @param bs the byte array containing the piece.
+   *
+   * @return true when the bytes represent the piece, false otherwise.
+   */
+  boolean gotPiece(Peer peer, int piece, byte[] bs);
+
+  /**
+   * Called when the peer wants (part of) a piece from us. Only called
+   * when the peer is not choked by us (<code>peer.choke(false)</code>
+   * was called).
+   *
+   * @param peer the Peer that wants the piece.
+   * @param piece the piece number requested.
+   *
+   * @return a byte array containing the piece or null when the piece
+   * is not available (which is a protocol error).
+   */
+  byte[] gotRequest(Peer peer, int piece);
+
+  /**
+   * Called when a (partial) piece has been downloaded from the peer.
+   *
+   * @param peer the Peer from which size bytes where downloaded.
+   * @param size the number of bytes that where downloaded.
+   */
+  void downloaded(Peer peer, int size);
+
+  /**
+   * Called when a (partial) piece has been uploaded to the peer.
+   *
+   * @param peer the Peer to which size bytes where uploaded.
+   * @param size the number of bytes that where uploaded.
+   */
+  void uploaded(Peer peer, int size);
+
+  /**
+   * Called when we are downloading from the peer and need to ask for
+   * a new piece. Might be called multiple times before
+   * <code>gotPiece()</code> is called.
+   *
+   * @param peer the Peer that will be asked to provide the piece.
+   * @param bitfield a BitField containing the pieces that the other
+   * side has.
+   *
+   * @return one of the pieces from the bitfield that we want or -1 if
+   * we are no longer interested in the peer.
+   */
+  int wantPiece(Peer peer, BitField bitfield);
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerMonitorTask.java b/apps/i2psnark/java/src/org/klomp/snark/PeerMonitorTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..b409f58c8e5d3edcc9817ee145a0b8cd8bed76ec
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerMonitorTask.java
@@ -0,0 +1,128 @@
+/* PeerMonitorTasks - TimerTask that monitors the peers and total up/down speed
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.util.*;
+
+/**
+ * TimerTask that monitors the peers and total up/download speeds.
+ * Works together with the main Snark class to report periodical statistics.
+ */
+class PeerMonitorTask extends TimerTask
+{
+  final static long MONITOR_PERIOD = 10 * 1000; // Ten seconds.
+  private final long KILOPERSECOND = 1024 * (MONITOR_PERIOD / 1000);
+
+  private final PeerCoordinator coordinator;
+
+  private long lastDownloaded = 0;
+  private long lastUploaded = 0;
+
+  PeerMonitorTask(PeerCoordinator coordinator)
+  {
+    this.coordinator = coordinator;
+  }
+
+  public void run()
+  {
+    // Get some statistics
+    int peers = 0;
+    int uploaders = 0;
+    int downloaders = 0;
+    int interested = 0;
+    int interesting = 0;
+    int choking = 0;
+    int choked = 0;
+    
+    synchronized(coordinator.peers)
+      {
+        Iterator it = coordinator.peers.iterator();
+        while (it.hasNext())
+          {
+            Peer peer = (Peer)it.next();
+            
+            // Don't list dying peers
+            if (!peer.isConnected())
+              continue;
+            
+            peers++;
+            
+            if (!peer.isChoking())
+              uploaders++;
+            if (!peer.isChoked() && peer.isInteresting())
+              downloaders++;
+            if (peer.isInterested())
+              interested++;
+            if (peer.isInteresting())
+              interesting++;
+            if (peer.isChoking())
+              choking++;
+            if (peer.isChoked())
+              choked++;
+          }
+      }
+
+    // Print some statistics
+    long downloaded = coordinator.getDownloaded();
+    String totalDown;
+    if (downloaded >= 10 * 1024 * 1024)
+      totalDown = (downloaded / (1024 * 1024)) + "MB";
+    else
+      totalDown = (downloaded / 1024 )+ "KB";
+    long uploaded = coordinator.getUploaded();
+    String totalUp;
+    if (uploaded >= 10 * 1024 * 1024)
+      totalUp = (uploaded / (1024 * 1024)) + "MB";
+    else
+      totalUp = (uploaded / 1024) + "KB";
+    
+    int needP = coordinator.storage.needed();
+    long needMB
+      = needP * coordinator.metainfo.getPieceLength(0) / (1024 * 1024);
+    int totalP = coordinator.metainfo.getPieces();
+    long totalMB = coordinator.metainfo.getTotalLength() / (1024 * 1024);
+    
+    System.out.println();
+    System.out.println("Down: "
+                       + (downloaded - lastDownloaded) / KILOPERSECOND
+                       + "KB/s"
+                       + " (" + totalDown + ")"
+                       + " Up: "
+                       + (uploaded - lastUploaded) / KILOPERSECOND
+                       + "KB/s"
+                       + " (" + totalUp + ")"
+                       + " Need " + needP
+                       + " (" + needMB + "MB)"
+                       + " of " + totalP
+                       + " (" + totalMB + "MB)"
+                       + " pieces");
+    System.out.println(peers + ": Download #" + downloaders
+                       + " Upload #" + uploaders
+                       + " Interested #" + interested
+                       + " Interesting #" + interesting
+                       + " Choking #" + choking
+                       + " Choked #" + choked);
+    System.out.println();
+
+    lastDownloaded = downloaded;
+    lastUploaded = uploaded;
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
new file mode 100644
index 0000000000000000000000000000000000000000..75b64e443401ac582107513005fa92707d71161e
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
@@ -0,0 +1,539 @@
+/* PeerState - Keeps track of the Peer state through connection callbacks.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.HashSet;
+
+class PeerState
+{
+  final Peer peer;
+  final PeerListener listener;
+  final MetaInfo metainfo;
+
+  // Interesting and choking describes whether we are interested in or
+  // are choking the other side.
+  boolean interesting = false;
+  boolean choking = true;
+
+  // Interested and choked describes whether the other side is
+  // interested in us or choked us.
+  boolean interested = false;
+  boolean choked = true;
+
+  // Package local for use by Peer.
+  long downloaded;
+  long uploaded;
+
+  BitField bitfield;
+
+  // Package local for use by Peer.
+  final PeerConnectionIn in;
+  final PeerConnectionOut out;
+
+  // Outstanding request
+  private final List outstandingRequests = new ArrayList();
+  private Request lastRequest = null;
+
+  // If we have te resend outstanding requests (true after we got choked).
+  private boolean resend = false;
+
+  private final static int MAX_PIPELINE = 5;
+  private final static int PARTSIZE = 64*1024; // default was 16K, i2p-bt uses 64KB
+
+  PeerState(Peer peer, PeerListener listener, MetaInfo metainfo,
+            PeerConnectionIn in, PeerConnectionOut out)
+  {
+    this.peer = peer;
+    this.listener = listener;
+    this.metainfo = metainfo;
+
+    this.in = in;
+    this.out = out;
+  }
+
+  // NOTE Methods that inspect or change the state synchronize (on this).
+
+  void keepAliveMessage()
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug(peer + " rcv alive", Snark.DEBUG);
+    /* XXX - ignored */
+  }
+
+  void chokeMessage(boolean choke)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug(peer + " rcv " + (choke ? "" : "un") + "choked",
+                  Snark.DEBUG);
+
+    choked = choke;
+    if (choked)
+      resend = true;
+
+    listener.gotChoke(peer, choke);
+
+    if (!choked && interesting)
+      request();
+  }
+
+  void interestedMessage(boolean interest)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug(peer + " rcv " + (interest ? "" : "un")
+                  + "interested", Snark.DEBUG);
+    interested = interest;
+    listener.gotInterest(peer, interest);
+  }
+
+  void haveMessage(int piece)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug(peer + " rcv have(" + piece + ")", Snark.DEBUG);
+    // Sanity check
+    if (piece < 0 || piece >= metainfo.getPieces())
+      {
+        // XXX disconnect?
+        if (Snark.debug >= Snark.INFO)
+          Snark.debug("Got strange 'have: " + piece + "' message from " + peer,
+                      + Snark.INFO);
+        return;
+      }
+
+    synchronized(this)
+      {
+        // Can happen if the other side never send a bitfield message.
+        if (bitfield == null)
+          bitfield = new BitField(metainfo.getPieces());
+
+        bitfield.set(piece);
+      }
+
+    if (listener.gotHave(peer, piece))
+      setInteresting(true);
+  }
+
+  void bitfieldMessage(byte[] bitmap)
+  {
+    synchronized(this)
+      {
+        if (Snark.debug >= Snark.DEBUG)
+          Snark.debug(peer + " rcv bitfield", Snark.DEBUG);
+        if (bitfield != null)
+          {
+            // XXX - Be liberal in what you except?
+            if (Snark.debug >= Snark.INFO)
+              Snark.debug("Got unexpected bitfield message from " + peer,
+                          Snark.INFO);
+            return;
+          }
+        
+        // XXX - Check for weird bitfield and disconnect?
+        bitfield = new BitField(bitmap, metainfo.getPieces());
+      }
+    setInteresting(listener.gotBitField(peer, bitfield));
+  }
+
+  void requestMessage(int piece, int begin, int length)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug(peer + " rcv request("
+                  + piece + ", " + begin + ", " + length + ") ",
+                  Snark.DEBUG);
+    if (choking)
+      {
+        if (Snark.debug >= Snark.INFO)
+          Snark.debug("Request received, but choking " + peer, Snark.INFO);
+        return;
+      }
+
+    // Sanity check
+    if (piece < 0
+        || piece >= metainfo.getPieces()
+        || begin < 0
+        || begin > metainfo.getPieceLength(piece)
+        || length <= 0
+        || length > 4*PARTSIZE)
+      {
+        // XXX - Protocol error -> disconnect?
+        if (Snark.debug >= Snark.INFO)
+          Snark.debug("Got strange 'request: " + piece
+                      + ", " + begin
+                      + ", " + length
+                      + "' message from " + peer,
+                      Snark.INFO);
+        return;
+      }
+
+    byte[] pieceBytes = listener.gotRequest(peer, piece);
+    if (pieceBytes == null)
+      {
+        // XXX - Protocol error-> diconnect?
+        if (Snark.debug >= Snark.INFO)
+          Snark.debug("Got request for unknown piece: " + piece, Snark.INFO);
+        return;
+      }
+
+    // More sanity checks
+    if (begin >= pieceBytes.length || begin + length > pieceBytes.length)
+      {
+        // XXX - Protocol error-> disconnect?
+        if (Snark.debug >= Snark.INFO)
+          Snark.debug("Got out of range 'request: " + piece
+                      + ", " + begin
+                      + ", " + length
+                      + "' message from " + peer,
+                      Snark.INFO);
+        return;
+      }
+
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug("Sending (" + piece + ", " + begin + ", "
+                  + length + ")" + " to " + peer, Snark.DEBUG);
+    out.sendPiece(piece, begin, length, pieceBytes);
+
+    // Tell about last subpiece delivery.
+    if (begin + length == pieceBytes.length)
+      if (Snark.debug >= Snark.DEBUG)
+        Snark.debug("Send p" + piece + " " + peer,
+                    Snark.DEBUG);
+  }
+
+  /**
+   * Called when some bytes have left the outgoing connection.
+   * XXX - Should indicate whether it was a real piece or overhead.
+   */
+  void uploaded(int size)
+  {
+    uploaded += size;
+    listener.uploaded(peer, size);
+  }
+
+  /**
+   * Called when a partial piece request has been handled by
+   * PeerConnectionIn.
+   */
+  void pieceMessage(Request req)
+  {
+    int size = req.len;
+    downloaded += size;
+    listener.downloaded(peer, size);
+
+    // Last chunk needed for this piece?
+    if (getFirstOutstandingRequest(req.piece) == -1)
+      {
+        if (listener.gotPiece(peer, req.piece, req.bs))
+          {
+            if (Snark.debug >= Snark.DEBUG)
+              Snark.debug("Got " + req.piece + ": " + peer, Snark.DEBUG);
+          }
+        else
+          {
+            if (Snark.debug >= Snark.DEBUG)
+              Snark.debug("Got BAD " + req.piece + " from " + peer,
+                          Snark.DEBUG);
+            // XXX ARGH What now !?!
+            downloaded = 0;
+          }
+      }
+  }
+
+  synchronized private int getFirstOutstandingRequest(int piece)
+  {
+    for (int i = 0; i < outstandingRequests.size(); i++)
+      if (((Request)outstandingRequests.get(i)).piece == piece)
+        return i;
+    return -1;
+  }
+
+  /**
+   * Called when a piece message is being processed by the incoming
+   * connection. Returns null when there was no such request. It also
+   * requeues/sends requests when it thinks that they must have been
+   * lost.
+   */
+  Request getOutstandingRequest(int piece, int begin, int length)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug("getChunk("
+                  + piece + "," + begin + "," + length + ") "
+                  + peer, Snark.DEBUG);
+
+    int r = getFirstOutstandingRequest(piece);
+
+    // Unrequested piece number?
+    if (r == -1)
+      {
+        if (Snark.debug >= Snark.INFO)
+          Snark.debug("Unrequested 'piece: " + piece + ", "
+                      + begin + ", " + length + "' received from "
+                      + peer,
+                      Snark.INFO);
+        downloaded = 0; // XXX - punishment?
+        return null;
+      }
+
+    // Lookup the correct piece chunk request from the list.
+    Request req;
+    synchronized(this)
+      {
+        req = (Request)outstandingRequests.get(r);
+        while (req.piece == piece && req.off != begin
+               && r < outstandingRequests.size() - 1)
+          {
+            r++;
+            req = (Request)outstandingRequests.get(r);
+          }
+        
+        // Something wrong?
+        if (req.piece != piece || req.off != begin || req.len != length)
+          {
+            if (Snark.debug >= Snark.INFO)
+              Snark.debug("Unrequested or unneeded 'piece: "
+                          + piece + ", "
+                          + begin + ", "
+                          + length + "' received from "
+                          + peer,
+                          Snark.INFO);
+            downloaded = 0; // XXX - punishment?
+            return null;
+          }
+        
+        // Report missing requests.
+        if (r != 0)
+          {
+            if (Snark.debug >= Snark.INFO)
+              System.err.print("Some requests dropped, got " + req
+                               + ", wanted:");
+            for (int i = 0; i < r; i++)
+              {
+                Request dropReq = (Request)outstandingRequests.remove(0);
+                outstandingRequests.add(dropReq);
+                // We used to rerequest the missing chunks but that mostly
+                // just confuses the other side. So now we just keep
+                // waiting for them. They will be rerequested when we get
+                // choked/unchoked again.
+                /*
+                  if (!choked)
+                  out.sendRequest(dropReq);
+                */
+                if (Snark.debug >= Snark.INFO)
+                  System.err.print(" " + dropReq);
+              }
+            if (Snark.debug >= Snark.INFO)
+              System.err.println(" " + peer);
+          }
+        outstandingRequests.remove(0);
+      }
+
+    // Request more if necessary to keep the pipeline filled.
+    addRequest();
+
+    return req;
+
+  }
+
+  void cancelMessage(int piece, int begin, int length)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug("Got cancel message ("
+                  + piece + ", " + begin + ", " + length + ")",
+                  Snark.DEBUG);
+    out.cancelRequest(piece, begin, length);
+  }
+
+  void unknownMessage(int type, byte[] bs)
+  {
+    if (Snark.debug >= Snark.WARNING)
+      Snark.debug("Warning: Ignoring unknown message type: " + type
+                  + " length: " + bs.length, Snark.WARNING);
+  }
+
+  void havePiece(int piece)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug("Tell " + peer + " havePiece(" + piece + ")", Snark.DEBUG);
+
+    synchronized(this)
+      {
+        // Tell the other side that we are no longer interested in any of
+        // the outstanding requests for this piece.
+        if (lastRequest != null && lastRequest.piece == piece)
+          lastRequest = null;
+        
+        Iterator it = outstandingRequests.iterator();
+        while (it.hasNext())
+          {
+            Request req = (Request)it.next();
+            if (req.piece == piece)
+              {
+                it.remove();
+                // Send cancel even when we are choked to make sure that it is
+                // really never ever send.
+                out.sendCancel(req);
+              }
+          }
+      }
+    
+    // Tell the other side that we really have this piece.
+    out.sendHave(piece);
+    
+    // Request something else if necessary.
+    addRequest();
+    
+    synchronized(this)
+      {
+        // Is the peer still interesting?
+        if (lastRequest == null)
+          setInteresting(false);
+      }
+  }
+
+  // Starts or resumes requesting pieces.
+  private void request()
+  {
+    // Are there outstanding requests that have to be resend?
+    if (resend)
+      {
+        out.sendRequests(outstandingRequests);
+        resend = false;
+      }
+
+    // Add/Send some more requests if necessary.
+    addRequest();
+  }
+
+  /**
+   * Adds a new request to the outstanding requests list.
+   */
+  private void addRequest()
+  {
+    boolean more_pieces = true;
+    while (more_pieces)
+      {
+        synchronized(this)
+          {
+            more_pieces = outstandingRequests.size() < MAX_PIPELINE;
+          }
+        
+        // We want something and we don't have outstanding requests?
+        if (more_pieces && lastRequest == null)
+          more_pieces = requestNextPiece();
+        else if (more_pieces) // We want something
+          {
+            int pieceLength;
+            boolean isLastChunk;
+            synchronized(this)
+              {
+                pieceLength = metainfo.getPieceLength(lastRequest.piece);
+                isLastChunk = lastRequest.off + lastRequest.len == pieceLength;
+              }
+
+            // Last part of a piece?
+            if (isLastChunk)
+              more_pieces = requestNextPiece();
+            else
+              {
+                synchronized(this)
+                  {
+                    int nextPiece = lastRequest.piece;
+                    int nextBegin = lastRequest.off + PARTSIZE;
+                    byte[] bs = lastRequest.bs;
+                    int maxLength = pieceLength - nextBegin;
+                    int nextLength = maxLength > PARTSIZE ? PARTSIZE
+                                                          : maxLength;
+                    Request req
+                      = new Request(nextPiece, bs, nextBegin, nextLength);
+                    outstandingRequests.add(req);
+                    if (!choked)
+                      out.sendRequest(req);
+                    lastRequest = req;
+                  }
+              }
+          }
+      }
+
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug(peer + " requests " + outstandingRequests, Snark.DEBUG);
+  }
+
+  // Starts requesting first chunk of next piece. Returns true if
+  // something has been added to the requests, false otherwise.
+  private boolean requestNextPiece()
+  {
+    // Check that we already know what the other side has.
+    if (bitfield != null)
+      {
+        int nextPiece = listener.wantPiece(peer, bitfield);
+        if (Snark.debug >= Snark.DEBUG)
+          Snark.debug(peer + " want piece " + nextPiece, Snark.DEBUG);
+        synchronized(this)
+          {
+            if (nextPiece != -1
+                && (lastRequest == null || lastRequest.piece != nextPiece))
+              {
+                int piece_length = metainfo.getPieceLength(nextPiece);
+                byte[] bs = new byte[piece_length];
+                
+                int length = Math.min(piece_length, PARTSIZE);
+                Request req = new Request(nextPiece, bs, 0, length);
+                outstandingRequests.add(req);
+                if (!choked)
+                  out.sendRequest(req);
+                lastRequest = req;
+                return true;
+              }
+          }
+      }
+
+    return false;
+  }
+
+  synchronized void setInteresting(boolean interest)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug(peer + " setInteresting(" + interest + ")", Snark.DEBUG);
+
+    if (interest != interesting)
+      {
+        interesting = interest;
+        out.sendInterest(interest);
+
+        if (interesting && !choked)
+          request();
+      }
+  }
+
+  synchronized void setChoking(boolean choke)
+  {
+    if (Snark.debug >= Snark.DEBUG)
+      Snark.debug(peer + " setChoking(" + choke + ")", Snark.DEBUG);
+
+    if (choking != choke)
+      {
+        choking = choke;
+        out.sendChoke(choke);
+      }
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Request.java b/apps/i2psnark/java/src/org/klomp/snark/Request.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a07a4289315e93b4d2464c2be3b84d623b101d4
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/Request.java
@@ -0,0 +1,73 @@
+/* Request - Holds all information needed for a (partial) piece request.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+/**
+ * Holds all information needed for a partial piece request.
+ */
+class Request
+{
+  final int piece;
+  final byte[] bs;
+  final int off;
+  final int len;
+
+  /**
+   * Creates a new Request.
+   *
+   * @param piece Piece number requested.
+   * @param bs byte array where response should be stored.
+   * @param off the offset in the array.
+   * @param len the number of bytes requested.
+   */
+  Request(int piece, byte[] bs, int off, int len)
+  {
+    this.piece = piece;
+    this.bs = bs;
+    this.off = off;
+    this.len = len;
+
+    // Sanity check
+    if (piece < 0 || off < 0 || len <= 0 || off + len > bs.length)
+      throw new IndexOutOfBoundsException("Illegal Request " + toString());
+  }
+
+  public int hashCode()
+  {
+    return piece ^ off ^ len;
+  }
+
+  public boolean equals(Object o)
+  {
+    if (o instanceof Request)
+      {
+        Request req = (Request)o;
+        return req.piece == piece && req.off == off && req.len == len;
+      }
+
+    return false;
+  }
+
+  public String toString()
+  {
+    return "(" + piece + "," + off + "," + len + ")";
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/ShutdownListener.java b/apps/i2psnark/java/src/org/klomp/snark/ShutdownListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..b64f75753849cf79c66d65755c4e74c1b80c2c1b
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/ShutdownListener.java
@@ -0,0 +1,34 @@
+/* ShutdownListener - Callback for end of shutdown sequence
+   
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+/**
+ * Callback for end of shutdown sequence.
+ */
+interface ShutdownListener
+{
+  /**
+   * Called when the SnarkShutdown hook has finished shutting down all
+   * subcomponents.
+   */
+  void shutdown();
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
new file mode 100644
index 0000000000000000000000000000000000000000..b09a0d4eb44281ead1544b39bf9e1cd8a930d584
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
@@ -0,0 +1,598 @@
+/* Snark - Main snark program startup class.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+import org.klomp.snark.bencode.*;
+
+import net.i2p.client.streaming.I2PSocket;
+import net.i2p.client.streaming.I2PServerSocket;
+
+/**
+ * Main Snark program startup class.
+ *
+ * @author Mark Wielaard (mark@klomp.org)
+ */
+public class Snark
+  implements StorageListener, CoordinatorListener, ShutdownListener
+{
+  private final static int MIN_PORT = 6881;
+  private final static int MAX_PORT = 6889;
+
+  // Error messages (non-fatal)
+  public final static int ERROR   = 1;
+
+  // Warning messages
+  public final static int WARNING = 2;
+
+  // Notices (peer level)
+  public final static int NOTICE  = 3;
+
+  // Info messages (protocol policy level)
+  public final static int INFO    = 4;
+
+  // Debug info (protocol level)
+  public final static int DEBUG   = 5;
+
+  // Very low level stuff (network level)
+  public final static int ALL     = 6;
+
+  /**
+   * What level of debug info to show.
+   */
+  public static int debug = NOTICE;
+
+  // Whether or not to ask the user for commands while sharing
+  private static boolean command_interpreter = true;
+
+  private static final String newline = System.getProperty("line.separator");
+
+  private static final String copyright =
+  "The Hunting of the Snark Project - Copyright (C) 2003 Mark J. Wielaard"
+  + newline + newline
+  + "Snark comes with ABSOLUTELY NO WARRANTY.  This is free software, and"
+  + newline
+  + "you are welcome to redistribute it under certain conditions; read the"
+  + newline
+  + "COPYING file for details." + newline + newline
+  + "This is the I2P port, allowing anonymous bittorrent (http://www.i2p.net/)" + newline
+  + "It will not work with normal torrents, so don't even try ;)";
+  
+  private static final String usage =
+  "Press return for help. Type \"quit\" and return to stop.";
+  private static final String help =
+  "Commands: 'info', 'list', 'quit'.";
+
+  // String indicating main activity
+  static String activity = "Not started";
+  
+  public static void main(String[] args)
+  {
+    System.out.println(copyright);
+    System.out.println();
+
+    // Parse debug, share/ip and torrent file options.
+    Snark snark = parseArguments(args);
+
+    SnarkShutdown snarkhook
+      = new SnarkShutdown(snark.storage,
+                          snark.coordinator,
+                          snark.acceptor,
+                          snark.trackerclient,
+                          snark);
+    Runtime.getRuntime().addShutdownHook(snarkhook);
+
+    Timer timer = new Timer(true);
+    TimerTask monitor = new PeerMonitorTask(snark.coordinator);
+    timer.schedule(monitor,
+                   PeerMonitorTask.MONITOR_PERIOD,
+                   PeerMonitorTask.MONITOR_PERIOD);
+
+    // Start command interpreter
+    if (Snark.command_interpreter)
+      {
+        boolean quit = false;
+        
+        System.out.println();
+        System.out.println(usage);
+        System.out.println();
+        
+        try
+          {
+            BufferedReader br = new BufferedReader
+              (new InputStreamReader(System.in));
+            String line = br.readLine();
+            while(!quit && line != null)
+              {
+                line = line.toLowerCase();
+                if ("quit".equals(line))
+                  quit = true;
+                else if ("list".equals(line))
+                  {
+                    synchronized(coordinator.peers)
+                      {
+                        System.out.println(coordinator.peers.size()
+                                           + " peers -"
+                                           + " (i)nterested,"
+                                           + " (I)nteresting,"
+                                           + " (c)hoking,"
+                                           + " (C)hoked:");
+                        Iterator it = coordinator.peers.iterator();
+                        while (it.hasNext())
+                          {
+                            Peer peer = (Peer)it.next();
+                            System.out.println(peer);
+                            System.out.println("\ti: " + peer.isInterested()
+                                               + " I: " + peer.isInteresting()
+                                               + " c: " + peer.isChoking()
+                                               + " C: " + peer.isChoked());
+                          }
+                      }
+                  }
+                else if ("info".equals(line))
+                  {
+                    System.out.println("Name: " + meta.getName());
+                    System.out.println("Torrent: " + torrent);
+                    System.out.println("Tracker: " + meta.getAnnounce());
+                    List files = meta.getFiles();
+                    System.out.println("Files: "
+                                       + ((files == null) ? 1 : files.size()));
+                    System.out.println("Pieces: " + meta.getPieces());
+                    System.out.println("Piece size: "
+                                       + meta.getPieceLength(0) / 1024
+                                       + " KB");
+                    System.out.println("Total size: "
+                                       + meta.getTotalLength() / (1024 * 1024)
+                                       + " MB");
+                  }
+                else if ("".equals(line) || "help".equals(line))
+                  {
+                    System.out.println(usage);
+                    System.out.println(help);
+                  }
+                else
+                  {
+                    System.out.println("Unknown command: " + line);
+                    System.out.println(usage);
+                  }
+                
+                if (!quit)
+                  {
+                    System.out.println();
+                    line = br.readLine();
+                  }
+              }
+          }
+        catch(IOException ioe)
+          {
+            debug("ERROR while reading stdin: " + ioe, ERROR);
+          }
+        
+        // Explicit shutdown.
+        Runtime.getRuntime().removeShutdownHook(snarkhook);
+        snarkhook.start();
+      }
+  }
+
+  static String torrent;
+  static MetaInfo meta;
+  static Storage storage;
+  static PeerCoordinator coordinator;
+  static ConnectionAcceptor acceptor;
+  static TrackerClient trackerclient;
+
+  private Snark(String torrent, String ip, int user_port,
+                StorageListener slistener, CoordinatorListener clistener)
+  {
+    if (slistener == null)
+      slistener = this;
+
+    if (clistener == null)
+      clistener = this;
+
+    this.torrent = torrent;
+
+    activity = "Network setup";
+
+    // "Taking Three as the subject to reason about--
+    // A convenient number to state--
+    // We add Seven, and Ten, and then multiply out
+    // By One Thousand diminished by Eight.
+    //
+    // "The result we proceed to divide, as you see,
+    // By Nine Hundred and Ninety Two:
+    // Then subtract Seventeen, and the answer must be
+    // Exactly and perfectly true.
+
+    // Create a new ID and fill it with something random.  First nine
+    // zeros bytes, then three bytes filled with snark and then
+    // sixteen random bytes.
+    byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17;
+    byte[] id = new byte[20];
+    Random random = new Random();
+    int i;
+    for (i = 0; i < 9; i++)
+      id[i] = 0;
+    id[i++] = snark;
+    id[i++] = snark;
+    id[i++] = snark;
+    while (i < 20)
+      id[i++] = (byte)random.nextInt(256);
+
+    Snark.debug("My peer id: " + PeerID.idencode(id), Snark.INFO);
+
+    int port;
+    IOException lastException = null;
+    boolean ok = I2PSnarkUtil.instance().connect();
+    if (!ok) fatal("Unable to connect to I2P");
+    I2PServerSocket serversocket = I2PSnarkUtil.instance().getServerSocket();
+    if (serversocket == null)
+        fatal("Unable to listen for I2P connections");
+    else
+        debug("Listening on I2P destination " + serversocket.getManager().getSession().getMyDestination().toBase64(), NOTICE);
+
+    // Figure out what the torrent argument represents.
+    meta = null;
+    File f = null;
+    try
+      {
+        InputStream in = null;
+        f = new File(torrent);
+        if (f.exists())
+          in = new FileInputStream(f);
+        else
+          {
+            activity = "Getting torrent";
+            File torrentFile = I2PSnarkUtil.instance().get(torrent);
+            if (torrentFile == null) {
+                fatal("Unable to fetch " + torrent);
+                if (false) return; // never reached - fatal(..) throws
+            } else {
+                torrentFile.deleteOnExit();
+                in = new FileInputStream(torrentFile);
+            }
+          }
+        meta = new MetaInfo(new BDecoder(in));
+      }
+    catch(IOException ioe)
+      {
+        // OK, so it wasn't a torrent metainfo file.
+        if (f != null && f.exists())
+          if (ip == null)
+            fatal("'" + torrent + "' exists,"
+                  + " but is not a valid torrent metainfo file."
+                  + System.getProperty("line.separator"), ioe);
+                 else
+            fatal("I2PSnark does not support creating and tracking a torrent at the moment");
+        /*
+            {
+              // Try to create a new metainfo file
+             Snark.debug
+               ("Trying to create metainfo torrent for '" + torrent + "'",
+                NOTICE);
+             try
+               {
+                 activity = "Creating torrent";
+                 storage = new Storage
+                   (f, "http://" + ip + ":" + port + "/announce", slistener);
+                 storage.create();
+                 meta = storage.getMetaInfo();
+               }
+             catch (IOException ioe2)
+               {
+                 fatal("Could not create torrent for '" + torrent + "'", ioe2);
+               }
+            }
+         */
+        else
+          fatal("Cannot open '" + torrent + "'", ioe);
+      }
+    
+    debug(meta.toString(), INFO);
+
+    // When the metainfo torrent was created from an existing file/dir
+    // it already exists.
+    if (storage == null)
+      {
+        try
+          {
+            activity = "Checking storage";
+            storage = new Storage(meta, slistener);
+            storage.check();
+          }
+        catch (IOException ioe)
+          {
+            fatal("Could not create storage", ioe);
+          }
+      }
+
+    activity = "Collecting pieces";
+    coordinator = new PeerCoordinator(id, meta, storage, clistener);
+    PeerAcceptor peeracceptor = new PeerAcceptor(coordinator);
+    ConnectionAcceptor acceptor = new ConnectionAcceptor(serversocket,
+                                                         peeracceptor);
+
+    trackerclient = new TrackerClient(meta, coordinator);
+    trackerclient.start();
+
+  }
+
+  static Snark parseArguments(String[] args)
+  {
+    return parseArguments(args, null, null);
+  }
+
+  /**
+   * Sets debug, ip and torrent variables then creates a Snark
+   * instance.  Calls usage(), which terminates the program, if
+   * non-valid argument list.  The given listeners will be
+   * passed to all components that take one.
+   */
+  static Snark parseArguments(String[] args,
+                              StorageListener slistener,
+                              CoordinatorListener clistener)
+  {
+    int user_port = -1;
+    String ip = null;
+    String torrent = null;
+
+    int i = 0;
+    while (i < args.length)
+      {
+        if (args[i].equals("--debug"))
+          {
+            debug = INFO;
+            i++;
+
+            // Try if there is an level argument.
+            if (i < args.length)
+              {
+                try
+                  {
+                    int level = Integer.parseInt(args[i]);
+                    if (level >= 0)
+                      {
+                        debug = level;
+                        i++;
+                      }
+                  }
+                catch (NumberFormatException nfe) { }
+              }
+          }
+        else if (args[i].equals("--port"))
+          {
+            if (args.length - 1 < i + 1)
+              usage("--port needs port number to listen on");
+            try
+              {
+                user_port = Integer.parseInt(args[i + 1]);
+              }
+            catch (NumberFormatException nfe)
+              {
+                usage("--port argument must be a number (" + nfe + ")");
+              }
+            i += 2;
+          }
+        else if (args[i].equals("--no-commands"))
+          {
+            command_interpreter = false;
+            i++;
+          }
+        else if (args[i].equals("--eepproxy"))
+          {
+            String proxyHost = args[i+1];
+            String proxyPort = args[i+2];
+            I2PSnarkUtil.instance().setProxy(proxyHost, Integer.parseInt(proxyPort));
+            i += 3;
+          }
+        else if (args[i].equals("--i2cp"))
+          {
+            String i2cpHost = args[i+1];
+            String i2cpPort = args[i+2];
+            Properties opts = null;
+            if (i+3 < args.length) {
+                if (!args[i+3].startsWith("--")) {
+                    opts = new Properties();
+                    StringTokenizer tok = new StringTokenizer(args[i+3], " \t");
+                    while (tok.hasMoreTokens()) {
+                        String str = tok.nextToken();
+                        int split = str.indexOf('=');
+                        if (split > 0) {
+                            opts.setProperty(str.substring(0, split), str.substring(split+1));
+                        }
+                    }
+                }
+            }
+            I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts);
+            i += 3 + (opts != null ? 1 : 0);
+          }
+        else
+          {
+            torrent = args[i];
+            i++;
+            break;
+          }
+      }
+
+    if (torrent == null || i != args.length)
+      if (torrent != null && torrent.startsWith("-"))
+        usage("Unknow option '" + torrent + "'.");
+      else
+        usage("Need exactly one <url>, <file> or <dir>.");
+
+    return new Snark(torrent, ip, user_port, slistener, clistener);
+  }
+  
+  private static void usage(String s)
+  {
+    System.out.println("snark: " + s);
+    usage();
+  }
+
+  private static void usage()
+  {
+    System.out.println
+      ("Usage: snark [--debug [level]] [--no-commands] [--port <port>]");
+    System.out.println
+      ("             [--eepproxy hostname portnum]");
+    System.out.println
+      ("             [--i2cp routerHost routerPort ['name=val name=val name=val']]");
+    System.out.println
+      ("             (<url>|<file>)");
+    System.out.println
+      ("  --debug\tShows some extra info and stacktraces");
+    System.out.println
+      ("    level\tHow much debug details to show");
+    System.out.println
+      ("         \t(defaults to "
+       + NOTICE + ", with --debug to "
+       + INFO + ", highest level is "
+       + ALL + ").");
+    System.out.println
+      ("  --no-commands\tDon't read interactive commands or show usage info.");
+    System.out.println
+      ("  --port\tThe port to listen on for incomming connections");
+    System.out.println
+      ("        \t(if not given defaults to first free port between "
+       + MIN_PORT + "-" + MAX_PORT + ").");
+    System.out.println
+      ("  --share\tStart torrent tracker on <ip> address or <host> name.");
+    System.out.println
+      ("  --eepproxy\thttp proxy to use (default of 127.0.0.1 port 4444)");
+    System.out.println
+      ("  --i2cp\tlocation of your I2P router (default of 127.0.0.1 port 7654)");
+    System.out.println
+      ("        \toptional settings may be included, such as");
+    System.out.println
+      ("        \tinbound.length=2 outbound.length=2 inbound.lengthVariance=-1 ");
+    System.out.println
+      ("  <url>  \tURL pointing to .torrent metainfo file to download/share.");
+    System.out.println
+      ("  <file> \tEither a local .torrent metainfo file to download");
+    System.out.println
+      ("         \tor (with --share) a file to share.");
+    System.exit(-1);
+  }
+
+  /**
+   * Aborts program abnormally.
+   */
+  public static void fatal(String s)
+  {
+    fatal(s, null);
+  }
+
+  /**
+   * Aborts program abnormally.
+   */
+  public static void fatal(String s, Throwable t)
+  {
+    I2PSnarkUtil.instance().debug(s, ERROR, t);
+    //System.err.println("snark: " + s + ((t == null) ? "" : (": " + t)));
+    //if (debug >= INFO && t != null)
+    //  t.printStackTrace();
+    throw new RuntimeException("die bart die");
+  }
+
+  /**
+   * Show debug info if debug is true.
+   */
+  public static void debug(String s, int level)
+  {
+    I2PSnarkUtil.instance().debug(s, level, null);
+    //if (debug >= level)
+    //  System.out.println(s);
+  }
+
+  public void peerChange(PeerCoordinator coordinator, Peer peer)
+  {
+    // System.out.println(peer.toString());
+  }
+  
+  boolean allocating = false;
+  public void storageCreateFile(Storage storage, String name, long length)
+  {
+    if (allocating)
+      System.out.println(); // Done with last file.
+
+    System.out.print("Creating file '" + name
+                     + "' of length " + length + ": ");
+    allocating = true;
+  }
+
+  // How much storage space has been allocated
+  private long allocated = 0;
+
+  public void storageAllocated(Storage storage, long length)
+  {
+    allocating = true;
+    System.out.print(".");
+    allocated += length;
+    if (allocated == meta.getTotalLength())
+      System.out.println(); // We have all the disk space we need.
+  }
+
+  boolean allChecked = false;
+  boolean checking = false;
+  boolean prechecking = true;
+  public void storageChecked(Storage storage, int num, boolean checked)
+  {
+    allocating = false;
+    if (!allChecked && !checking)
+      {
+        // Use the MetaInfo from the storage since our own might not
+        // yet be setup correctly.
+        MetaInfo meta = storage.getMetaInfo();
+        if (meta != null)
+          System.out.print("Checking existing "
+                           + meta.getPieces()
+                           + " pieces: ");
+        checking = true;
+      }
+    if (checking)
+      if (checked)
+        System.out.print("+");
+      else
+        System.out.print("-");
+    else
+      Snark.debug("Got " + (checked ? "" : "BAD ") + "piece: " + num,
+                  Snark.INFO);
+  }
+
+  public void storageAllChecked(Storage storage)
+  {
+    if (checking)
+      System.out.println();
+
+    allChecked = true;
+    checking = false;
+  }
+
+  public void shutdown()
+  {
+    // Should not be necessary since all non-deamon threads should
+    // have died. But in reality this does not always happen.
+    System.exit(0);
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b58628bb7f6999b961fc2762d20d72c712e6c93
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java
@@ -0,0 +1,89 @@
+/* TrackerShutdown - Makes sure everything ends correctly when shutting down.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.IOException;
+
+/**
+ * Makes sure everything ends correctly when shutting down.
+ */
+public class SnarkShutdown extends Thread
+{
+  private final Storage storage;
+  private final PeerCoordinator coordinator;
+  private final ConnectionAcceptor acceptor;
+  private final TrackerClient trackerclient;
+
+  private final ShutdownListener listener;
+
+  public SnarkShutdown(Storage storage,
+                       PeerCoordinator coordinator,
+                       ConnectionAcceptor acceptor,
+                       TrackerClient trackerclient,
+                       ShutdownListener listener)
+  {
+    this.storage = storage;
+    this.coordinator = coordinator;
+    this.acceptor = acceptor;
+    this.trackerclient = trackerclient;
+    this.listener = listener;
+  }
+
+  public void run()
+  {
+    Snark.debug("Shutting down...", Snark.NOTICE);
+
+    Snark.debug("Halting ConnectionAcceptor...", Snark.INFO);
+    if (acceptor != null)
+      acceptor.halt();
+
+    Snark.debug("Halting TrackerClient...", Snark.INFO);
+    if (trackerclient != null)
+      trackerclient.halt();
+
+    Snark.debug("Halting PeerCoordinator...", Snark.INFO);
+    if (coordinator != null)
+      coordinator.halt();
+
+    Snark.debug("Closing Storage...", Snark.INFO);
+    if (storage != null)
+      {
+        try
+          {
+            storage.close();
+          }
+        catch(IOException ioe)
+          {
+            Snark.fatal("Couldn't properly close storage", ioe);
+          }
+      }
+
+    // XXX - Should actually wait till done...
+    try
+      {
+        Snark.debug("Waiting 5 seconds...", Snark.INFO);
+        Thread.sleep(5*1000);
+      }
+    catch (InterruptedException ie) { /* ignored */ }
+
+    listener.shutdown();
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java b/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java
new file mode 100644
index 0000000000000000000000000000000000000000..22ef1c2a456b1f4b8907e254303497b3c0a28e39
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java
@@ -0,0 +1,47 @@
+/* StaticSnark - Main snark startup class for staticly linking with gcj.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.security.Provider;
+import java.security.Security;
+
+import org.klomp.snark.bencode.*;
+
+/**
+ * Main snark startup class for staticly linking with gcj.
+ * It references somee necessary classes that are normally loaded through
+ * reflection.
+ *
+ * @author Mark Wielaard (mark@klomp.org)
+ */
+public class StaticSnark
+{
+  public static void main(String[] args)
+  {
+    // The GNU security provider is needed for SHA-1 MessageDigest checking.
+    // So make sure it is available as a security provider.
+    //Provider gnu = new gnu.java.security.provider.Gnu();
+    //Security.addProvider(gnu);
+
+    // And finally call the normal starting point.
+    Snark.main(args);
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
new file mode 100644
index 0000000000000000000000000000000000000000..287abf216bbb5e1a27b7873cbc7d051439dfcdff
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
@@ -0,0 +1,525 @@
+/* Storage - Class used to store and retrieve pieces.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.*;
+import java.util.*;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Maintains pieces on disk. Can be used to store and retrieve pieces.
+ */
+public class Storage
+{
+  private MetaInfo metainfo;
+  private long[] lengths;
+  private RandomAccessFile[] rafs;
+  private String[] names;
+
+  private final StorageListener listener;
+
+  private final BitField bitfield;
+  private int needed;
+
+  // XXX - Not always set correctly
+  int piece_size;
+  int pieces;
+
+  /** The default piece size. */
+  private static int MIN_PIECE_SIZE = 256*1024;
+  /** The maximum number of pieces in a torrent. */
+  private static long MAX_PIECES = 100*1024/20;
+
+  /**
+   * Creates a new storage based on the supplied MetaInfo.  This will
+   * try to create and/or check all needed files in the MetaInfo.
+   *
+   * @exception IOException when creating and/or checking files fails.
+   */
+  public Storage(MetaInfo metainfo, StorageListener listener)
+    throws IOException
+  {
+    this.metainfo = metainfo;
+    this.listener = listener;
+    needed = metainfo.getPieces();
+    bitfield = new BitField(needed);
+  }
+
+  /**
+   * Creates a storage from the existing file or directory together
+   * with an appropriate MetaInfo file as can be announced on the
+   * given announce String location.
+   */
+  public Storage(File baseFile, String announce, StorageListener listener)
+    throws IOException
+  {
+    this.listener = listener;
+
+    // Create names, rafs and lengths arrays.
+    getFiles(baseFile);
+    
+    long total = 0;
+    ArrayList lengthsList = new ArrayList();
+    for (int i = 0; i < lengths.length; i++)
+      {
+        long length = lengths[i];
+        total += length;
+        lengthsList.add(new Long(length));
+      }
+
+    piece_size = MIN_PIECE_SIZE;
+    pieces = (int) ((total - 1)/piece_size) + 1;
+    while (pieces > MAX_PIECES)
+      {
+        piece_size = piece_size*2;
+        pieces = (int) ((total - 1)/piece_size) +1;
+      }
+
+    // Note that piece_hashes and the bitfield will be filled after
+    // the MetaInfo is created.
+    byte[] piece_hashes = new byte[20*pieces];
+    bitfield = new BitField(pieces);
+    needed = 0;
+
+    List files = new ArrayList();
+    for (int i = 0; i < names.length; i++)
+      {
+        List file = new ArrayList();
+        StringTokenizer st = new StringTokenizer(names[i], File.separator);
+        while (st.hasMoreTokens())
+          {
+            String part = st.nextToken();
+            file.add(part);
+          }
+        files.add(file);
+      }
+
+    String name = baseFile.getName();
+    if (files.size() == 1)
+      {
+        files = null;
+        lengthsList = null;
+      }
+
+    // Note that the piece_hashes are not correctly setup yet.
+    metainfo = new MetaInfo(announce, baseFile.getName(), files,
+                            lengthsList, piece_size, piece_hashes, total);
+
+  }
+
+  // Creates piece hases for a new storage.
+  public void create() throws IOException
+  {
+    // Calculate piece_hashes
+    MessageDigest digest = null;
+    try
+      {
+        digest = MessageDigest.getInstance("SHA");
+      }
+    catch(NoSuchAlgorithmException nsa)
+      {
+        throw new InternalError(nsa.toString());
+      }
+
+    byte[] piece_hashes = metainfo.getPieceHashes();
+
+    byte[] piece = new byte[piece_size];
+    for (int i = 0; i < pieces; i++)
+      {
+        int length = getUncheckedPiece(i, piece, 0);
+        digest.update(piece, 0, length);
+        byte[] hash = digest.digest();
+        for (int j = 0; j < 20; j++)
+          piece_hashes[20 * i + j] = hash[j];
+
+        bitfield.set(i);
+
+        if (listener != null)
+          listener.storageChecked(this, i, true);
+      }
+
+    if (listener != null)
+      listener.storageAllChecked(this);
+
+    // Reannounce to force recalculating the info_hash.
+    metainfo = metainfo.reannounce(metainfo.getAnnounce());
+  }
+
+  private void getFiles(File base) throws IOException
+  {
+    ArrayList files = new ArrayList();
+    addFiles(files, base);
+
+    int size = files.size();
+    names = new String[size];
+    lengths = new long[size];
+    rafs = new RandomAccessFile[size];
+
+    int i = 0;
+    Iterator it = files.iterator();
+    while (it.hasNext())
+      {
+        File f = (File)it.next();
+        names[i] = f.getPath();
+        lengths[i] = f.length();
+        rafs[i] = new RandomAccessFile(f, "r");
+        i++;
+      }
+  }
+
+  private static void addFiles(List l, File f)
+  {
+    if (!f.isDirectory())
+      l.add(f);
+    else
+      {
+        File[] files = f.listFiles();
+        if (files == null)
+          {
+            Snark.debug("WARNING: Skipping '" + f 
+                        + "' not a normal file.", Snark.WARNING);
+            return;
+          }
+        for (int i = 0; i < files.length; i++)
+          addFiles(l, files[i]);
+      }
+  }
+
+  /**
+   * Returns the MetaInfo associated with this Storage.
+   */
+  public MetaInfo getMetaInfo()
+  {
+    return metainfo;
+  }
+
+  /**
+   * How many pieces are still missing from this storage.
+   */
+  public int needed()
+  {
+    return needed;
+  }
+
+  /**
+   * Whether or not this storage contains all pieces if the MetaInfo.
+   */
+  public boolean complete()
+  {
+    return needed == 0;
+  }
+
+  /**
+   * The BitField that tells which pieces this storage contains.
+   * Do not change this since this is the current state of the storage.
+   */
+  public BitField getBitField()
+  {
+    return bitfield;
+  }
+
+  /**
+   * Creates (and/or checks) all files from the metainfo file list.
+   */
+  public void check() throws IOException
+  {
+    File base = new File(filterName(metainfo.getName()));
+
+    List files = metainfo.getFiles();
+    if (files == null)
+      {
+        // Create base as file.
+        Snark.debug("Creating/Checking file: " + base, Snark.NOTICE);
+        if (!base.createNewFile() && !base.exists())
+          throw new IOException("Could not create file " + base);
+
+        lengths = new long[1];
+        rafs = new RandomAccessFile[1];
+        names = new String[1];
+        lengths[0] = metainfo.getTotalLength();
+        rafs[0] = new RandomAccessFile(base, "rw");
+        names[0] = base.getName();
+      }
+    else
+      {
+        // Create base as dir.
+        Snark.debug("Creating/Checking directory: " + base, Snark.NOTICE);
+        if (!base.mkdir() && !base.isDirectory())
+          throw new IOException("Could not create directory " + base);
+
+        List  ls = metainfo.getLengths();
+        int size = files.size();
+        long total = 0;
+        lengths = new long[size];
+        rafs = new RandomAccessFile[size];
+        names = new String[size];
+        for (int i = 0; i < size; i++)
+          {
+            File f = createFileFromNames(base, (List)files.get(i));
+            lengths[i] = ((Long)ls.get(i)).longValue();
+            total += lengths[i];
+            rafs[i] = new RandomAccessFile(f, "rw");
+            names[i] = f.getName();
+          }
+
+        // Sanity check for metainfo file.
+        long metalength = metainfo.getTotalLength();
+        if (total != metalength)
+          throw new IOException("File lengths do not add up "
+                                + total + " != " + metalength);
+      }
+    checkCreateFiles();
+  }
+
+  /**
+   * Removes 'suspicious' characters from the give file name.
+   */
+  private String filterName(String name)
+  {
+    // XXX - Is this enough?
+    return name.replace(File.separatorChar, '_');
+  }
+
+  private File createFileFromNames(File base, List names) throws IOException
+  {
+    File f = null;
+    Iterator it = names.iterator();
+    while (it.hasNext())
+      {
+        String name = filterName((String)it.next());
+        if (it.hasNext())
+          {
+            // Another dir in the hierarchy.
+            f = new File(base, name);
+            if (!f.mkdir() && !f.isDirectory())
+              throw new IOException("Could not create directory " + f);
+            base = f;
+          }
+        else
+          {
+            // The final element (file) in the hierarchy.
+            f = new File(base, name);
+            if (!f.createNewFile() && !f.exists())
+              throw new IOException("Could not create file " + f);
+          }
+      }
+    return f;
+  }
+
+  private void checkCreateFiles() throws IOException
+  {
+    // Whether we are resuming or not,
+    // if any of the files already exists we assume we are resuming.
+    boolean resume = false;
+
+    // Make sure all files are available and of correct length
+    for (int i = 0; i < rafs.length; i++)
+      {
+        long length = rafs[i].length();
+        if(length == lengths[i])
+          {
+            if (listener != null)
+              listener.storageAllocated(this, length);
+            resume = true; // XXX Could dynamicly check
+          }
+        else if (length == 0)
+          allocateFile(i);
+        else
+          throw new IOException("File '" + names[i]
+                                + "' exists, but has wrong length");
+      }
+
+    // Check which pieces match and which don't
+    if (resume)
+      {
+        pieces = metainfo.getPieces();
+        byte[] piece = new byte[metainfo.getPieceLength(0)];
+        for (int i = 0; i < pieces; i++)
+          {
+            int length = getUncheckedPiece(i, piece, 0);
+            boolean correctHash = metainfo.checkPiece(i, piece, 0, length);
+            if (correctHash)
+              {
+                bitfield.set(i);
+                needed--;
+              }
+
+            if (listener != null)
+              listener.storageChecked(this, i, correctHash);
+          }
+      }
+
+    if (listener != null)
+      listener.storageAllChecked(this);
+  }
+
+  private void allocateFile(int nr) throws IOException
+  {
+    // XXX - Is this the best way to make sure we have enough space for
+    // the whole file?
+    listener.storageCreateFile(this, names[nr], lengths[nr]);
+    final int ZEROBLOCKSIZE = metainfo.getPieceLength(0);
+    byte[] zeros = new byte[ZEROBLOCKSIZE];
+    int i;
+    for (i = 0; i < lengths[nr]/ZEROBLOCKSIZE; i++)
+      {
+        rafs[nr].write(zeros);
+        if (listener != null)
+          listener.storageAllocated(this, ZEROBLOCKSIZE);
+      }
+    int size = (int)(lengths[nr] - i*ZEROBLOCKSIZE);
+    rafs[nr].write(zeros, 0, size);
+    if (listener != null)
+      listener.storageAllocated(this, size);
+  }
+
+
+  /**
+   * Closes the Storage and makes sure that all RandomAccessFiles are
+   * closed. The Storage is unusable after this.
+   */
+  public void close() throws IOException
+  {
+    for (int i = 0; i < rafs.length; i++)
+      {
+        synchronized(rafs[i])
+          {
+            rafs[i].close();
+          }
+      }
+  }
+
+  /**
+   * Returns a byte array containing the requested piece or null if
+   * the storage doesn't contain the piece yet.
+   */
+  public byte[] getPiece(int piece) throws IOException
+  {
+    if (!bitfield.get(piece))
+      return null;
+
+    byte[] bs = new byte[metainfo.getPieceLength(piece)];
+    getUncheckedPiece(piece, bs, 0);
+    return bs;
+  }
+
+  /**
+   * Put the piece in the Storage if it is correct.
+   *
+   * @return true if the piece was correct (sha metainfo hash
+   * matches), otherwise false.
+   * @exception IOException when some storage related error occurs.
+   */
+  public boolean putPiece(int piece, byte[] bs) throws IOException
+  {
+    // First check if the piece is correct.
+    // If we were paranoia we could copy the array first.
+    int length = bs.length;
+    boolean correctHash = metainfo.checkPiece(piece, bs, 0, length);
+    if (listener != null)
+      listener.storageChecked(this, piece, correctHash);
+    if (!correctHash)
+      return false;
+
+    boolean complete;
+    synchronized(bitfield)
+      {
+        if (bitfield.get(piece))
+          return true; // No need to store twice.
+        else
+          {
+            bitfield.set(piece);
+            needed--;
+            complete = needed == 0;
+          }
+      }
+
+    long start = piece * metainfo.getPieceLength(0);
+    int i = 0;
+    long raflen = lengths[i];
+    while (start > raflen)
+      {
+        i++;
+        start -= raflen;
+        raflen = lengths[i];
+      }
+    
+    int written = 0;
+    int off = 0;
+    while (written < length)
+      {
+        int need = length - written;
+        int len = (start + need < raflen) ? need : (int)(raflen - start);
+        synchronized(rafs[i])
+          {
+            rafs[i].seek(start);
+            rafs[i].write(bs, off + written, len);
+          }
+        written += len;
+        if (need - len > 0)
+          {
+            i++;
+            raflen = lengths[i];
+            start = 0;
+          }
+      }
+
+    return true;
+  }
+
+  private int getUncheckedPiece(int piece, byte[] bs, int off)
+    throws IOException
+  {
+    // XXX - copy/paste code from putPiece().
+    long start = piece * metainfo.getPieceLength(0);
+    int length = metainfo.getPieceLength(piece);
+    int i = 0;
+    long raflen = lengths[i];
+    while (start > raflen)
+      {
+        i++;
+        start -= raflen;
+        raflen = lengths[i];
+      }
+
+    int read = 0;
+    while (read < length)
+      {
+        int need = length - read;
+        int len = (start + need < raflen) ? need : (int)(raflen - start);
+        synchronized(rafs[i])
+          {
+            rafs[i].seek(start);
+            rafs[i].readFully(bs, off + read, len);
+          }
+        read += len;
+        if (need - len > 0)
+          {
+            i++;
+            raflen = lengths[i];
+            start = 0;
+          }
+      }
+
+    return length;
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java b/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..1cb5c119396983f6123e192b4ffddfee2e9817a0
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java
@@ -0,0 +1,52 @@
+/* StorageListener.java - Interface used as callback when storage changes.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+/**
+ * Callback used when Storage changes.
+ */
+public interface StorageListener
+{
+  /**
+   * Called when the storage creates a new file of a given length.
+   */
+  void storageCreateFile(Storage storage, String name, long length);
+
+  /**
+   * Called to indicate that length bytes have been allocated.
+   */
+  void storageAllocated(Storage storage, long length);
+
+  /**
+   * Called when storage is being checked and the num piece of that
+   * total pieces has been checked. When the piece hash matches the
+   * expected piece hash checked will be true, otherwise it will be
+   * false.
+   */
+  void storageChecked(Storage storage, int num, boolean checked);
+
+  /**
+   * Called when all pieces in the storage have been checked. Does not
+   * mean that the storage is complete, just that the state of the
+   * storage is known.
+   */
+  void storageAllChecked(Storage storage);
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..49939311e1f1d12b7626e8d6fc6b80f956e69528
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
@@ -0,0 +1,254 @@
+/* TrackerClient - Class that informs a tracker and gets new peers.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+import org.klomp.snark.bencode.*;
+
+/**
+ * Informs metainfo tracker of events and gets new peers for peer
+ * coordinator.
+ *
+ * @author Mark Wielaard (mark@klomp.org)
+ */
+public class TrackerClient extends Thread
+{
+  private static final String NO_EVENT = "";
+  private static final String STARTED_EVENT = "started";
+  private static final String COMPLETED_EVENT = "completed";
+  private static final String STOPPED_EVENT = "stopped";
+
+  private final static int SLEEP = 5; // 5 minutes.
+
+  private final MetaInfo meta;
+  private final PeerCoordinator coordinator;
+  private final int port;
+
+  private boolean stop;
+
+  private long interval;
+  private long lastRequestTime;
+
+  public TrackerClient(MetaInfo meta, PeerCoordinator coordinator)
+  {
+    // Set unique name.
+    super("TrackerClient-" + urlencode(coordinator.getID()));
+    this.meta = meta;
+    this.coordinator = coordinator;
+
+    this.port = 6881; //(port == -1) ? 9 : port;
+
+    stop = false;
+  }
+
+  /**
+   * Interrupts this Thread to stop it.
+   */
+  public void halt()
+  {
+    stop = true;
+    this.interrupt();
+  }
+
+  public void run()
+  {
+    // XXX - Support other IPs
+    String announce = I2PSnarkUtil.instance().rewriteAnnounce(meta.getAnnounce());
+    String infoHash = urlencode(meta.getInfoHash());
+    String peerID = urlencode(coordinator.getID());
+
+    long uploaded = coordinator.getUploaded();
+    long downloaded = coordinator.getDownloaded();
+    long left = coordinator.getLeft();
+
+    boolean completed = (left == 0);
+
+    try
+      {
+        boolean started = false;
+        while (!started)
+          {
+            try
+              {
+                // Send start.
+                TrackerInfo info = doRequest(announce, infoHash, peerID,
+                                             uploaded, downloaded, left,
+                                             STARTED_EVENT);
+                Iterator it = info.getPeers().iterator();
+                while (it.hasNext())
+                  coordinator.addPeer((Peer)it.next());
+                started = true;
+              }
+            catch (IOException ioe)
+              {
+                // Probably not fatal (if it doesn't last to long...)
+                Snark.debug
+                  ("WARNING: Could not contact tracker at '"
+                   + announce + "': " + ioe, Snark.WARNING);
+              }
+
+            if (!started && !stop)
+              {
+                Snark.debug("         Retrying in one minute...", Snark.DEBUG);
+                try
+                  {
+                    // Sleep one minutes...
+                    Thread.sleep(60*1000);
+                  }
+                catch(InterruptedException interrupt)
+                  {
+                    // ignore
+                  }
+              }
+          }
+
+        while(!stop)
+          {
+            try
+              {
+                // Sleep some minutes...
+                Thread.sleep(SLEEP*60*1000);
+              }
+            catch(InterruptedException interrupt)
+              {
+                // ignore
+              }
+
+            if (stop)
+              break;
+            
+            uploaded = coordinator.getUploaded();
+            downloaded = coordinator.getDownloaded();
+            left = coordinator.getLeft();
+            
+            // First time we got a complete download?
+            String event;
+            if (!completed && left == 0)
+              {
+                completed = true;
+                event = COMPLETED_EVENT;
+              }
+            else
+              event = NO_EVENT;
+            
+            // Only do a request when necessary.
+            if (event == COMPLETED_EVENT
+                || coordinator.needPeers()
+                || System.currentTimeMillis() > lastRequestTime + interval)
+              {
+                try
+                  {
+                    TrackerInfo info = doRequest(announce, infoHash, peerID,
+                                                 uploaded, downloaded, left,
+                                                 event);
+
+                    Iterator it = info.getPeers().iterator();
+                    while (it.hasNext())
+                      coordinator.addPeer((Peer)it.next());
+                  }
+                catch (IOException ioe)
+                  {
+                    // Probably not fatal (if it doesn't last to long...)
+                    Snark.debug
+                      ("WARNING: Could not contact tracker at '"
+                       + announce + "': " + ioe, Snark.WARNING);
+                  }
+              }
+          }
+      }
+    catch (Throwable t)
+      {
+        Snark.debug("TrackerClient: " + t, Snark.ERROR);
+        t.printStackTrace();
+      }
+    finally
+      {
+        try
+          {
+            TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded,
+                                         downloaded, left, STOPPED_EVENT);
+          }
+        catch(IOException ioe) { /* ignored */ }
+      }
+    
+  }
+  
+  private TrackerInfo doRequest(String announce, String infoHash,
+                                String peerID, long uploaded,
+                                long downloaded, long left, String event)
+    throws IOException
+  {
+    String s = announce
+      + "?info_hash=" + infoHash
+      + "&peer_id=" + peerID
+      + "&port=" + port
+      + "&ip=" + I2PSnarkUtil.instance().getOurIPString()
+      + "&uploaded=" + uploaded
+      + "&downloaded=" + downloaded
+      + "&left=" + left
+      + ((event != NO_EVENT) ? ("&event=" + event) : "");
+    if (Snark.debug >= Snark.INFO)
+      Snark.debug("Sending TrackerClient request: " + s, Snark.INFO);
+      
+    File fetched = I2PSnarkUtil.instance().get(s);
+    if (fetched == null) {
+        throw new IOException("Error fetching " + s);
+    }
+    
+    fetched.deleteOnExit();
+    InputStream in = new FileInputStream(fetched);
+
+    TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
+                                       coordinator.getMetaInfo());
+    if (Snark.debug >= Snark.INFO)
+      Snark.debug("TrackerClient response: " + info, Snark.INFO);
+    lastRequestTime = System.currentTimeMillis();
+    
+    String failure = info.getFailureReason();
+    if (failure != null)
+      throw new IOException(failure);
+    
+    interval = info.getInterval() * 1000;
+    return info;
+  }
+
+  /**
+   * Very lazy byte[] to URL encoder.  Just encodes everything, even
+   * "normal" chars.
+   */
+  static String urlencode(byte[] bs)
+  {
+    StringBuffer sb = new StringBuffer(bs.length*3);
+    for (int i = 0; i < bs.length; i++)
+      {
+        int c = bs[i] & 0xFF;
+        sb.append('%');
+        if (c < 16)
+          sb.append('0');
+        sb.append(Integer.toHexString(c));
+      }
+         
+    return sb.toString();
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..6d464f87b016a8a934783a8bc52b43854421ab66
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
@@ -0,0 +1,128 @@
+/* TrackerInfo - Holds information returned by a tracker, mainly the peer list.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.HashSet;
+
+import org.klomp.snark.bencode.*;
+
+public class TrackerInfo
+{
+  private final String failure_reason;
+  private final int interval;
+  private final Set peers;
+
+  public TrackerInfo(InputStream in, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    this(new BDecoder(in), my_id, metainfo);
+  }
+
+  public TrackerInfo(BDecoder be, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    this(be.bdecodeMap().getMap(), my_id, metainfo);
+  }
+
+  public TrackerInfo(Map m, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    BEValue reason = (BEValue)m.get("failure reason");
+    if (reason != null)
+      {
+        failure_reason = reason.getString();
+        interval = -1;
+        peers = null;
+      }
+    else
+      {
+        failure_reason = null;
+        BEValue beInterval = (BEValue)m.get("interval");
+        if (beInterval == null)
+          throw new InvalidBEncodingException("No interval given");
+        else
+          interval = beInterval.getInt();
+        BEValue bePeers = (BEValue)m.get("peers");
+        if (bePeers == null)
+          throw new InvalidBEncodingException("No peer list");
+        else
+          peers = getPeers(bePeers.getList(), my_id, metainfo);
+      }
+  }
+
+  public static Set getPeers(InputStream in, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    return getPeers(new BDecoder(in), my_id, metainfo);
+  }
+
+  public static Set getPeers(BDecoder be, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    return getPeers(be.bdecodeList().getList(), my_id, metainfo);
+  }
+
+  public static Set getPeers(List l, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    Set peers = new HashSet(l.size());
+
+    Iterator it = l.iterator();
+    while (it.hasNext())
+      {
+        PeerID peerID = new PeerID(((BEValue)it.next()).getMap());
+        peers.add(new Peer(peerID, my_id, metainfo));
+      }
+
+    return peers;
+  }
+
+  public Set getPeers()
+  {
+    return peers;
+  }
+
+  public String getFailureReason()
+  {
+    return failure_reason;
+  }
+
+  public int getInterval()
+  {
+    return interval;
+  }
+
+  public String toString()
+  {
+    if (failure_reason != null)
+      return "TrackerInfo[FAILED: " + failure_reason + "]";
+    else
+      return "TrackerInfo[interval=" + interval
+        + ", peers=" + peers + "]";
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BDecoder.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BDecoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..87b3bf4c547862de8009b70c1835ebb2c2202390
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BDecoder.java
@@ -0,0 +1,355 @@
+/* BDecoder - Converts an InputStream to BEValues.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark.bencode;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.EOFException;
+import java.io.UnsupportedEncodingException;
+
+import java.math.BigInteger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Decodes a bencoded stream to <code>BEValue</code>s.
+ *
+ * A bencoded byte stream can represent byte arrays, numbers, lists and
+ * maps (dictionaries).
+ *
+ * It currently contains a hack to indicate a name of a dictionary of
+ * which a SHA-1 digest hash should be calculated (the hash over the
+ * original bencoded bytes).
+ *
+ * @author Mark Wielaard (mark@klomp.org).
+ */
+public class BDecoder
+{
+  // The InputStream to BDecode.
+  private final InputStream in;
+
+  // The last indicator read.
+  // Zero if unknown.
+  // '0'..'9' indicates a byte[].
+  // 'i' indicates an Number.
+  // 'l' indicates a List.
+  // 'd' indicates a Map.
+  // 'e' indicates end of Number, List or Map (only used internally).
+  // -1 indicates end of stream.
+  // Call getNextIndicator to get the current value (will never return zero).
+  private int indicator = 0;
+
+  // Used for ugly hack to get SHA hash over the metainfo info map
+  private String special_map = "info";
+  private boolean in_special_map = false;
+  private final MessageDigest sha_digest;
+
+  // Ugly hack. Return the SHA has over bytes that make up the special map.
+  public byte[] get_special_map_digest()
+  {
+    byte[] result = sha_digest.digest();
+    return result;
+  }
+
+  // Ugly hack. Name defaults to "info".
+  public void set_special_map_name(String name)
+  {
+    special_map = name;
+  }
+
+  /**
+   * Initalizes a new BDecoder. Nothing is read from the given
+   * <code>InputStream</code> yet.
+   */
+  public BDecoder(InputStream in)
+  {
+    this.in = in;
+    // XXX - Used for ugly hack.
+    try
+      {
+        sha_digest = MessageDigest.getInstance("SHA");
+      }
+    catch(NoSuchAlgorithmException nsa)
+      {
+        throw new InternalError(nsa.toString());
+      }
+  }
+
+  /**
+   * Creates a new BDecoder and immediatly decodes the first value it
+   * sees.
+   *
+   * @return The first BEValue on the stream or null when the stream
+   * has ended.
+   *
+   * @exception InvalidBEncoding when the stream doesn't start with a
+   * bencoded value or the stream isn't a bencoded stream at all.
+   * @exception IOException when somthing bad happens with the stream
+   * to read from.
+   */
+  public static BEValue bdecode(InputStream in) throws IOException
+  {
+    return new BDecoder(in).bdecode();
+  }
+
+  /**
+   * Returns what the next bencoded object will be on the stream or -1
+   * when the end of stream has been reached. Can return something
+   * unexpected (not '0' .. '9', 'i', 'l' or 'd') when the stream
+   * isn't bencoded.
+   *
+   * This might or might not read one extra byte from the stream.
+   */
+  public int getNextIndicator() throws IOException
+  {
+    if (indicator == 0)
+      {
+        indicator = in.read();
+        // XXX - Used for ugly hack
+        if (in_special_map) sha_digest.update((byte)indicator);
+      }
+    return indicator;
+  }
+
+  /**
+   * Gets the next indicator and returns either null when the stream
+   * has ended or bdecodes the rest of the stream and returns the
+   * appropriate BEValue encoded object.
+   */
+  public BEValue bdecode() throws IOException
+  {
+    indicator = getNextIndicator();
+    if (indicator == -1)
+      return null;
+
+    if (indicator >= '0' && indicator <= '9')
+      return bdecodeBytes();
+    else if (indicator == 'i')
+      return bdecodeNumber();
+    else if (indicator == 'l')
+      return bdecodeList();
+    else if (indicator == 'd')
+      return bdecodeMap();
+    else
+      throw new InvalidBEncodingException
+        ("Unknown indicator '" + indicator + "'");
+  }
+
+  /**
+   * Returns the next bencoded value on the stream and makes sure it
+   * is a byte array. If it is not a bencoded byte array it will throw
+   * InvalidBEncodingException.
+   */
+  public BEValue bdecodeBytes() throws IOException
+  {
+    int c = getNextIndicator();
+    int num = c - '0';
+    if (num < 0 || num > 9)
+      throw new InvalidBEncodingException("Number expected, not '"
+                                          + (char)c + "'");
+    indicator = 0;
+
+    c = read();
+    int i = c - '0';
+    while (i >= 0 && i <= 9)
+      {
+        // XXX - This can overflow!
+        num = num*10 + i;
+        c = read();
+        i = c - '0';
+      }
+
+    if (c != ':')
+      throw new InvalidBEncodingException("Colon expected, not '"
+                                          + (char)c + "'");
+
+    return new BEValue(read(num));
+  }
+
+  /**
+   * Returns the next bencoded value on the stream and makes sure it
+   * is a number. If it is not a number it will throw
+   * InvalidBEncodingException.
+   */
+  public BEValue bdecodeNumber() throws IOException
+  {
+    int c = getNextIndicator();
+    if (c != 'i')
+      throw new InvalidBEncodingException("Expected 'i', not '"
+                                          + (char)c + "'");
+    indicator = 0;
+
+    c = read();
+    if (c == '0')
+      {
+        c = read();
+        if (c == 'e')
+          return new BEValue(BigInteger.ZERO);
+        else
+          throw new InvalidBEncodingException("'e' expected after zero,"
+                                              + " not '" + (char)c + "'");
+      }
+
+    // XXX - We don't support more the 255 char big integers
+    char[] chars = new char[256];
+    int off = 0;
+
+    if (c == '-')
+      {
+        c = read();
+        if (c == '0')
+          throw new InvalidBEncodingException("Negative zero not allowed");
+        chars[off] = (char)c;
+        off++;
+      }
+
+    if (c < '1' || c > '9')
+      throw new InvalidBEncodingException("Invalid Integer start '"
+                                          + (char)c + "'");
+    chars[off] = (char)c;
+    off++;
+
+    c = read();
+    int i = c - '0';
+    while(i >= 0 && i <= 9)
+      {
+        chars[off] = (char)c;
+        off++;
+        c = read();
+        i = c - '0';
+      }
+
+    if (c != 'e')
+      throw new InvalidBEncodingException("Integer should end with 'e'");
+
+    String s = new String(chars, 0, off);
+    return new BEValue(new BigInteger(s));
+  }
+
+  /**
+   * Returns the next bencoded value on the stream and makes sure it
+   * is a list. If it is not a list it will throw
+   * InvalidBEncodingException.
+   */
+  public BEValue bdecodeList() throws IOException
+  {
+    int c = getNextIndicator();
+    if (c != 'l')
+      throw new InvalidBEncodingException("Expected 'l', not '"
+                                          + (char)c + "'");
+    indicator = 0;
+
+    List result = new ArrayList();
+    c = getNextIndicator();
+    while (c != 'e')
+      {
+        result.add(bdecode());
+        c = getNextIndicator();
+      }
+    indicator = 0;
+
+    return new BEValue(result);
+  }
+
+  /**
+   * Returns the next bencoded value on the stream and makes sure it
+   * is a map (dictonary). If it is not a map it will throw
+   * InvalidBEncodingException.
+   */
+  public BEValue bdecodeMap() throws IOException
+  {
+    int c = getNextIndicator();
+    if (c != 'd')
+      throw new InvalidBEncodingException("Expected 'd', not '"
+                                          + (char)c + "'");
+    indicator = 0;
+
+    Map result = new HashMap();
+    c = getNextIndicator();
+    while (c != 'e')
+      {
+        // Dictonary keys are always strings.
+        String key = bdecode().getString();
+
+        // XXX ugly hack
+        boolean special = special_map.equals(key);
+        if (special)
+          in_special_map = true;
+
+        BEValue value = bdecode();
+        result.put(key, value);
+
+        // XXX ugly hack continued
+        if (special)
+          in_special_map = false;
+        
+        c = getNextIndicator();
+      }
+    indicator = 0;
+
+    return new BEValue(result);
+  }
+
+  /**
+   * Returns the next byte read from the InputStream (as int).
+   * Throws EOFException if InputStream.read() returned -1.
+   */
+  private int read() throws IOException
+  {
+    int c = in.read();
+    if (c == -1)
+      throw new EOFException();
+    if (in_special_map) sha_digest.update((byte)c);
+    return c;
+  }
+
+  /**
+   * Returns a byte[] containing length valid bytes starting at offset
+   * zero.  Throws EOFException if InputStream.read() returned -1
+   * before all requested bytes could be read.  Note that the byte[]
+   * returned might be bigger then requested but will only contain
+   * length valid bytes.  The returned byte[] will be reused when this
+   * method is called again.
+   */
+  private byte[] read(int length) throws IOException
+  {
+    byte[] result = new byte[length];
+    
+    int read = 0;
+    while (read < length)
+      {
+        int i = in.read(result, read, length - read);
+        if (i == -1)
+          throw new EOFException();
+        read += i;
+      }
+
+    if (in_special_map) sha_digest.update(result, 0, length);
+    return result;
+  }
+
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
new file mode 100644
index 0000000000000000000000000000000000000000..cad99ed70d82df03cc99f1466afb9b17b1f8accd
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
@@ -0,0 +1,190 @@
+/* BEValue - Holds different types that a bencoded byte array can represent.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark.bencode;
+
+import java.io.UnsupportedEncodingException;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Holds different types that a bencoded byte array can represent.
+ * You need to call the correct get method to get the correct java
+ * type object. If the BEValue wasn't actually of the requested type
+ * you will get a InvalidBEncodingException.
+ *
+ * @author Mark Wielaard (mark@klomp.org)
+ */
+public class BEValue
+{
+  // This is either a byte[], Number, List or Map.
+  private final Object value;
+
+  public BEValue(byte[] value)
+  {
+    this.value = value;
+  }
+
+  public BEValue(Number value)
+  {
+    this.value = value;
+  }
+
+  public BEValue(List value)
+  {
+    this.value = value;
+  }
+
+  public BEValue(Map value)
+  {
+    this.value = value;
+  }
+
+  /**
+   * Returns this BEValue as a String. This operation only succeeds
+   * when the BEValue is a byte[], otherwise it will throw a
+   * InvalidBEncodingException. The byte[] will be interpreted as
+   * UTF-8 encoded characters.
+   */
+  public String getString() throws InvalidBEncodingException
+  {
+    try
+      {
+        return new String(getBytes(), "UTF-8");
+      }
+    catch (ClassCastException cce)
+      {
+        throw new InvalidBEncodingException(cce.toString());
+      }
+    catch (UnsupportedEncodingException uee)
+      {
+        throw new InternalError(uee.toString());
+      }
+  }
+
+  /**
+   * Returns this BEValue as a byte[]. This operation only succeeds
+   * when the BEValue is actually a byte[], otherwise it will throw a
+   * InvalidBEncodingException.
+   */
+  public byte[] getBytes() throws InvalidBEncodingException
+  {
+    try
+      {
+        return (byte[])value;
+      }
+    catch (ClassCastException cce)
+      {
+        throw new InvalidBEncodingException(cce.toString());
+      }
+  }
+
+  /**
+   * Returns this BEValue as a Number. This operation only succeeds
+   * when the BEValue is actually a Number, otherwise it will throw a
+   * InvalidBEncodingException.
+   */
+  public Number getNumber() throws InvalidBEncodingException
+  {
+    try
+      {
+        return (Number)value;
+      }
+    catch (ClassCastException cce)
+      {
+        throw new InvalidBEncodingException(cce.toString());
+      }
+  }
+
+  /**
+   * Returns this BEValue as int. This operation only succeeds when
+   * the BEValue is actually a Number, otherwise it will throw a
+   * InvalidBEncodingException. The returned int is the result of
+   * <code>Number.intValue()</code>.
+   */
+  public int getInt() throws InvalidBEncodingException
+  {
+    return getNumber().intValue();
+  }
+
+  /**
+   * Returns this BEValue as long. This operation only succeeds when
+   * the BEValue is actually a Number, otherwise it will throw a
+   * InvalidBEncodingException. The returned long is the result of
+   * <code>Number.longValue()</code>.
+   */
+  public long getLong() throws InvalidBEncodingException
+  {
+    return getNumber().longValue();
+  }
+
+  /**
+   * Returns this BEValue as a List of BEValues. This operation only
+   * succeeds when the BEValue is actually a List, otherwise it will
+   * throw a InvalidBEncodingException.
+   */
+  public List getList() throws InvalidBEncodingException
+  {
+    try
+      {
+        return (List)value;
+      }
+    catch (ClassCastException cce)
+      {
+        throw new InvalidBEncodingException(cce.toString());
+      }
+  }
+
+  /**
+   * Returns this BEValue as a Map of BEValue keys and BEValue
+   * values. This operation only succeeds when the BEValue is actually
+   * a Map, otherwise it will throw a InvalidBEncodingException.
+   */
+  public Map getMap() throws InvalidBEncodingException
+  {
+    try
+      {
+        return (Map)value;
+      }
+    catch (ClassCastException cce)
+      {
+        throw new InvalidBEncodingException(cce.toString());
+      }
+  }
+
+  public String toString()
+  {
+    String valueString;
+    if (value instanceof byte[])
+      {
+        byte[] bs = (byte[])value;
+        // XXX - Stupid heuristic...
+        if (bs.length <= 12)
+          valueString = new String(bs);
+        else
+          valueString = "bytes:" + bs.length;
+      }
+    else
+      valueString = value.toString();
+
+    return "BEValue[" + valueString + "]";
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..e17a36d92ff82b3257d5d3d26e7a29976d165596
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java
@@ -0,0 +1,191 @@
+/* BDecoder - Converts an InputStream to BEValues.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark.bencode;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class BEncoder
+{
+
+  public static byte[] bencode(Object o) throws IllegalArgumentException
+  {
+    try
+      {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        bencode(o, baos);
+        return baos.toByteArray();
+      }
+    catch (IOException ioe)
+      {
+        throw new InternalError(ioe.toString());
+      }
+  }
+
+  public static void bencode(Object o, OutputStream out)
+    throws IOException, IllegalArgumentException
+  {
+    if (o instanceof String)
+      bencode((String)o, out);
+    else if (o instanceof byte[])
+      bencode((byte[])o, out);
+    else if (o instanceof Number)
+      bencode((Number)o, out);
+    else if (o instanceof List)
+      bencode((List)o, out);
+    else if (o instanceof Map)
+      bencode((Map)o, out);
+    else
+      throw new IllegalArgumentException("Cannot bencode: " + o.getClass());
+  }
+
+  public static byte[] bencode(String s)
+  {
+    try
+      {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        bencode(s, baos);
+        return baos.toByteArray();
+      }
+    catch (IOException ioe)
+      {
+        throw new InternalError(ioe.toString());
+      }
+  }
+
+  public static void bencode(String s, OutputStream out) throws IOException
+  {
+    byte[] bs = s.getBytes("UTF-8");
+    bencode(bs, out);
+  }
+
+  public static byte[] bencode(Number n)
+  {
+    try
+      {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        bencode(n, baos);
+        return baos.toByteArray();
+      }
+    catch (IOException ioe)
+      {
+        throw new InternalError(ioe.toString());
+      }
+  }
+
+  public static void bencode(Number n, OutputStream out) throws IOException
+  {
+    out.write('i');
+    String s = n.toString();
+    out.write(s.getBytes("UTF-8"));
+    out.write('e');
+  }
+
+  public static byte[] bencode(List l)
+  {
+    try
+      {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        bencode(l, baos);
+        return baos.toByteArray();
+      }
+    catch (IOException ioe)
+      {
+        throw new InternalError(ioe.toString());
+      }
+  }
+
+  public static void bencode(List l, OutputStream out) throws IOException
+  {
+    out.write('l');
+    Iterator it = l.iterator();
+    while (it.hasNext())
+      bencode(it.next(), out);
+    out.write('e');
+  }
+
+  public static byte[] bencode(byte[] bs)
+  {
+    try
+      {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        bencode(bs, baos);
+        return baos.toByteArray();
+      }
+    catch (IOException ioe)
+      {
+        throw new InternalError(ioe.toString());
+      }
+  }
+
+  public static void bencode(byte[] bs, OutputStream out) throws IOException
+  {
+    String l = Integer.toString(bs.length);
+    out.write(l.getBytes("UTF-8"));
+    out.write(':');
+    out.write(bs);
+  }
+
+  public static byte[] bencode(Map m)
+  {
+    try
+      {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        bencode(m, baos);
+        return baos.toByteArray();
+      }
+    catch (IOException ioe)
+      {
+        throw new InternalError(ioe.toString());
+      }
+  }
+
+  public static void bencode(Map m, OutputStream out) throws IOException
+  {
+    out.write('d');
+
+    // Keys must be sorted. XXX - But is this the correct order?
+    Set s = m.keySet();
+    List l = new ArrayList(s);
+    Collections.sort(l);
+
+    Iterator it = l.iterator();
+    while(it.hasNext())
+      {
+        // Keys must be Strings.
+        String key = (String)it.next();
+        Object value = m.get(key);
+        bencode(key, out);
+        bencode(value, out);
+      }
+
+    out.write('e');
+  }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/InvalidBEncodingException.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/InvalidBEncodingException.java
new file mode 100644
index 0000000000000000000000000000000000000000..1c4552944ce24026cfd6d1c2a7ae164a54feacd6
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/InvalidBEncodingException.java
@@ -0,0 +1,36 @@
+/* InvalidBEncodingException - Thrown when a bencoded stream is corrupted.
+   Copyright (C) 2003 Mark J. Wielaard
+
+   This file is part of Snark.
+   
+   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, 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.
+*/
+
+package org.klomp.snark.bencode;
+
+import java.io.IOException;
+
+/**
+ * Exception thrown when a bencoded stream is corrupted.
+ *
+ * @author Mark Wielaard (mark@klomp.org)
+ */
+public class InvalidBEncodingException extends IOException
+{
+  public InvalidBEncodingException(String message)
+  {
+    super(message);
+  }
+}
diff --git a/apps/i2psnark/readme.txt b/apps/i2psnark/readme.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3970e3cba14d7a58ed94d2a16f0a70e55cf5a131
--- /dev/null
+++ b/apps/i2psnark/readme.txt
@@ -0,0 +1,11 @@
+This is an I2P port of snark [http://klomp.org/snark], a GPL'ed bittorrent client
+
+The build in tracker has been removed for simplicity.
+
+Example usage:
+  java -jar lib/i2psnark.jar myFile.torrent
+
+or, a more verbose setting:
+  java -jar lib/i2psnark.jar --eepproxy 127.0.0.1 4444 \
+       --i2cp 127.0.0.1 7654 "inbound.length=2 outbound.length=2" \
+       --debug 6 myFile.torrent
diff --git a/apps/i2psnark/readme.txt.snark b/apps/i2psnark/readme.txt.snark
new file mode 100644
index 0000000000000000000000000000000000000000..0567e01d469c5a94d6de4387b637aee05496aa73
--- /dev/null
+++ b/apps/i2psnark/readme.txt.snark
@@ -0,0 +1,141 @@
+The Hunting of the Snark Project - BitTorrent Application Suite
+0.5 - The Beaver's Lesson (27 June 2003)
+
+  "It's a Snark!" was the sound that first came to their ears,
+     And seemed almost too good to be true.
+  Then followed a torrent of laughter and cheers:
+     Then the ominous words "It's a Boo-"
+
+  -- from The Hunting Of The Snark by Lewis Carroll
+
+Snark is a client for downloading and sharing files distributed with
+the BitTorrent protocol. It is mainly used for exploring the BitTorrent
+protocol and experimenting with the the GNU Compiler for Java (gcj).
+But it can also be used as a regular BitTorrent Client.
+
+Snark can also act as a torrent creator, micro http server for delivering
+metainfo.torrent files and has an integrated Tracker for making sharing of
+files as easy as possible.
+
+When you give the option --share Snark will automatically
+create a .torrent file, start a very simple webserver to distribute
+the metainfo.torrent file and a local tracker that other BitTorrent
+clients can connect to.
+
+Distribution
+------------
+
+  Copyright (C) 2003 Mark J. Wielaard
+
+  Snark 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
+
+Requirements/Installation
+-------------------------
+
+The GNU Compiler for java (gcj) version 3.3 or later.
+(Earlier versions have a faulty SHA message digest implementation.)
+On Debian GNU/Linux based distributions just install the gcj-3.3 package.
+Edit the GCJ variable in the Makefile if your gcj binary is not gcj-3.3.
+
+Typing 'make' will create the native snark binary and a snark.jar file
+for use with traditional java byte code interpreters.
+
+It is possible to compile the sources with other java compilers
+like jikes or kjc to produce the snark.jar file.  Edit the JAVAC and
+JAVAC_FLAGS variables on top of the Makefile for this.  And type
+'make snark.jar' to create a jar file that can be used by traditional
+java bytecode interpreters like kaffe: 'kaffe -jar snark.jar'.
+You will need at least version 1.1 of kaffe for all functionality to work
+correctly ('--share' does not work with older versions).
+
+When trying out the experimental Gnome frontend you also need the java-gnome
+bindings. On Debian GNU/Linux systems install the package libgnome0-java.
+You can try it out by typing 'make snark-gnome' and then run 'snark-gnome.sh'
+like you would with the normal command line client.
+
+Running
+-------
+
+To use the program start it with:
+
+snark [--debug [level]] [--no-commands] [--port <port>]
+      [--share (<ip>|<host>)] (<url>|<file>|<dir>)
+  --debug       Shows some extra info and stacktraces.
+    level       How much debug details to show
+                (defaults to 3, with --debug to 4, highest level is 6).
+  --no-commands Don't read interactive commands or show usage info.
+  --port        The port to listen on for incomming connections
+                (if not given defaults to first free port between 6881-6889).
+  --share       Start torrent tracker on <ip> address or <host> name.
+  <url>         URL pointing to .torrent metainfo file to download/share.
+  <file>        Either a local .torrent metainfo file to download
+                or (with --share) a file to share.
+  <dir>         A directory with files to share (needs --share).
+
+Since this is an early beta release there are probably still some bugs
+in the program. To help find them run the program with the --debug
+option which shows more information on what it going on. You can also give
+the level of debug output you want. Zero will give (almost) no output at all.
+Everything above debug level 4 is probably to much (only really useful to
+see what goes on on the protocol/network level).
+
+Examples
+
+- To simple start downloading/sharing a file.
+  Either download the .torrent file to disk and start snark with:
+  ./snark somefile.torrent
+
+  Or give it the complete URL:
+  ./snark http://somehost.example.com/cd-images/bbc-lnx.iso.torrent
+
+- To start seeding/sharing a local file:
+  ./snark --share my-host.example.com some-file
+
+  Snark will respond with:
+  Listening on port: 6881
+  Trying to create metainfo torrent for 'some-file'
+  Creating torrent piece hashes: ++++++++++
+  Torrent available on http://my-host.example.com:6881/metainfo.torrent
+
+  You can now point other people to the above URL so they can share
+  the file with their own BitTorrent client.
+
+Commands
+
+While the program is running in text mode you can currently give the
+following commands: 'info', 'list' and 'quit'.
+
+Interactive commands are disabled when the '--no-commands' flag is given.
+This is sometimes desireable for running snark in the background.
+
+More information
+----------------
+
+- The Evolution of Cooperation - Robert Axelrod
+  ISBN 0-465-02121-2
+
+- The BitTorrent protocol description:
+  <http://bitconjurer.org/BitTorrent/protocol.html>
+
+- The GNU Compiler for Java (gcj):
+  <http://gcc.gnu.org/java/>
+
+- java-gnome bindings : <http://java-gnome.sourceforge.net/>
+
+- The Hunting of the Snark - Lewis Carroll
+
+Comments welcome
+
+	- Mark Wielaard <mark@klomp.org>