diff --git a/apps/i2psnark/_icons/magnet.png b/apps/i2psnark/_icons/magnet.png
new file mode 100644
index 0000000000000000000000000000000000000000..3430f2e5a9c08050cb90248f42315876fd04cb6a
Binary files /dev/null and b/apps/i2psnark/_icons/magnet.png differ
diff --git a/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java
index 52099d1d2e67bc5e657260ccb598a55369bf35a0..b4ba0f2996acd956d728a8e2d792d7a47dd16024 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java
@@ -137,6 +137,11 @@ public class ConnectionAcceptor implements Runnable
                     }
                 }
             } else {
+                if (socket.getPeerDestination().equals(_util.getMyDestination())) {
+                    _util.debug("Incoming connection from myself", Snark.ERROR);
+                    try { socket.close(); } catch (IOException ioe) {}
+                    continue;
+                }
                 Thread t = new I2PAppThread(new Handler(socket), "I2PSnark incoming connection");
                 t.start();
             }
@@ -174,11 +179,8 @@ public class ConnectionAcceptor implements Runnable
           try {
               InputStream in = _socket.getInputStream();
               OutputStream out = _socket.getOutputStream();
-
-              if (true) {
-                  in = new BufferedInputStream(in);
-                  //out = new BufferedOutputStream(out);
-              }
+              // this is for the readahead in PeerAcceptor.connection()
+              in = new BufferedInputStream(in);
               if (_log.shouldLog(Log.DEBUG))
                   _log.debug("Handling socket from " + _socket.getPeerDestination().calculateHash().toBase64());
               peeracceptor.connection(_socket, in, out);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java
index a86afa781b6c32d7cd64bbd4d1d6920153c8aaa0..478c17bb507832aa2bdcd12a799513e870714380 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java
@@ -31,6 +31,12 @@ public interface CoordinatorListener
    */
   void peerChange(PeerCoordinator coordinator, Peer peer);
 
+  /**
+   * Called when the PeerCoordinator got the MetaInfo via magnet.
+   * @since 0.8.4
+   */
+  void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo);
+
   public boolean overUploadLimit(int uploaders);
   public boolean overUpBWLimit();
   public boolean overUpBWLimit(long total);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..b3770070f4e99436f747a1ca8aee559b64bc51f5
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java
@@ -0,0 +1,361 @@
+package org.klomp.snark;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataHelper;
+import net.i2p.util.Log;
+
+import org.klomp.snark.bencode.BDecoder;
+import org.klomp.snark.bencode.BEncoder;
+import org.klomp.snark.bencode.BEValue;
+import org.klomp.snark.bencode.InvalidBEncodingException;
+
+/**
+ * REF: BEP 10 Extension Protocol
+ * @since 0.8.2
+ * @author zzz
+ */
+abstract class ExtensionHandler {
+
+    private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(ExtensionHandler.class);
+
+    public static final int ID_HANDSHAKE = 0;
+    public static final int ID_METADATA = 1;
+    public static final String TYPE_METADATA = "ut_metadata";
+    public static final int ID_PEX = 2;
+    /** not ut_pex since the compact format is different */
+    public static final String TYPE_PEX = "i2p_pex";
+    /** Pieces * SHA1 Hash length, + 25% extra for file names, benconding overhead, etc */
+    private static final int MAX_METADATA_SIZE = Storage.MAX_PIECES * 20 * 5 / 4;
+    private static final int PARALLEL_REQUESTS = 3;
+
+
+  /**
+   *  @param metasize -1 if unknown
+   *  @return bencoded outgoing handshake message
+   */
+    public static byte[] getHandshake(int metasize) {
+        Map<String, Object> handshake = new HashMap();
+        Map<String, Integer> m = new HashMap();
+        m.put(TYPE_METADATA, Integer.valueOf(ID_METADATA));
+        m.put(TYPE_PEX, Integer.valueOf(ID_PEX));
+        if (metasize >= 0)
+            handshake.put("metadata_size", Integer.valueOf(metasize));
+        handshake.put("m", m);
+        handshake.put("p", Integer.valueOf(6881));
+        handshake.put("v", "I2PSnark");
+        handshake.put("reqq", Integer.valueOf(5));
+        return BEncoder.bencode(handshake);
+    }
+
+    public static void handleMessage(Peer peer, PeerListener listener, int id, byte[] bs) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Got extension msg " + id + " length " + bs.length + " from " + peer);
+        if (id == ID_HANDSHAKE)
+            handleHandshake(peer, listener, bs);
+        else if (id == ID_METADATA)
+            handleMetadata(peer, listener, bs);
+        else if (id == ID_PEX)
+            handlePEX(peer, listener, bs);
+        else if (_log.shouldLog(Log.INFO))
+            _log.info("Unknown extension msg " + id + " from " + peer);
+    }
+
+    private static void handleHandshake(Peer peer, PeerListener listener, byte[] bs) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Got handshake msg from " + peer);
+        try {
+            // this throws NPE on missing keys
+            InputStream is = new ByteArrayInputStream(bs);
+            BDecoder dec = new BDecoder(is);
+            BEValue bev = dec.bdecodeMap();
+            Map<String, BEValue> map = bev.getMap();
+            peer.setHandshakeMap(map);
+            Map<String, BEValue> msgmap = map.get("m").getMap();
+
+            if (msgmap.get(TYPE_PEX) != null) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.debug("Peer supports PEX extension: " + peer);
+                // peer state calls peer listener calls sendPEX()
+            }
+
+            MagnetState state = peer.getMagnetState();
+
+            if (msgmap.get(TYPE_METADATA) == null) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.debug("Peer does not support metadata extension: " + peer);
+                // drop if we need metainfo and we haven't found anybody yet
+                synchronized(state) {
+                    if (!state.isInitialized()) {
+                        _log.debug("Dropping peer, we need metadata! " + peer);
+                        peer.disconnect();
+                    }
+                }
+                return;
+            }
+
+            BEValue msize = map.get("metadata_size");
+            if (msize == null) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.debug("Peer does not have the metainfo size yet: " + peer);
+                // drop if we need metainfo and we haven't found anybody yet
+                synchronized(state) {
+                    if (!state.isInitialized()) {
+                        _log.debug("Dropping peer, we need metadata! " + peer);
+                        peer.disconnect();
+                    }
+                }
+                return;
+            }
+            int metaSize = msize.getInt();
+            if (_log.shouldLog(Log.WARN))
+                _log.debug("Got the metainfo size: " + metaSize);
+
+            int remaining;
+            synchronized(state) {
+                if (state.isComplete())
+                    return;
+
+                if (state.isInitialized()) {
+                    if (state.getSize() != metaSize) {
+                        if (_log.shouldLog(Log.WARN))
+                            _log.debug("Wrong metainfo size " + metaSize + " from: " + peer);
+                        peer.disconnect();
+                        return;
+                    }
+                } else {
+                    // initialize it
+                    if (metaSize > MAX_METADATA_SIZE) {
+                        if (_log.shouldLog(Log.WARN))
+                            _log.debug("Huge metainfo size " + metaSize + " from: " + peer);
+                        peer.disconnect(false);
+                        return;
+                    }
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info("Initialized state, metadata size = " + metaSize + " from " + peer);
+                    state.initialize(metaSize);
+                }
+                remaining = state.chunksRemaining();
+            }
+
+            // send requests for chunks
+            int count = Math.min(remaining, PARALLEL_REQUESTS);
+            for (int i = 0; i < count; i++) {
+                int chk;
+                synchronized(state) {
+                    chk = state.getNextRequest();
+                }
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Request chunk " + chk + " from " + peer);
+                sendRequest(peer, chk);
+            }
+        } catch (Exception e) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Handshake exception from " + peer, e);
+        }
+    }
+
+    private static final int TYPE_REQUEST = 0;
+    private static final int TYPE_DATA = 1;
+    private static final int TYPE_REJECT = 2;
+
+    private static final int CHUNK_SIZE = 16*1024;
+
+    /**
+     * REF: BEP 9
+     * @since 0.8.4
+     */
+    private static void handleMetadata(Peer peer, PeerListener listener, byte[] bs) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Got metadata msg from " + peer);
+        try {
+            InputStream is = new ByteArrayInputStream(bs);
+            BDecoder dec = new BDecoder(is);
+            BEValue bev = dec.bdecodeMap();
+            Map<String, BEValue> map = bev.getMap();
+            int type = map.get("msg_type").getInt();
+            int piece = map.get("piece").getInt();
+
+            MagnetState state = peer.getMagnetState();
+            if (type == TYPE_REQUEST) {
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Got request for " + piece + " from: " + peer);
+                byte[] pc;
+                synchronized(state) {
+                    pc = state.getChunk(piece);
+                }
+                sendPiece(peer, piece, pc);
+                // Do this here because PeerConnectionOut only reports for PIECE messages
+                peer.uploaded(pc.length);
+                listener.uploaded(peer, pc.length);
+            } else if (type == TYPE_DATA) {
+                int size = map.get("total_size").getInt();
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Got data for " + piece + " length " + size + " from: " + peer);
+                boolean done;
+                int chk = -1;
+                synchronized(state) {
+                    if (state.isComplete())
+                        return;
+                    int len = is.available();
+                    if (len != size) {
+                        // probably fatal
+                        if (_log.shouldLog(Log.WARN))
+                            _log.warn("total_size " + size + " but avail data " + len);
+                    }
+                    peer.downloaded(len);
+                    listener.downloaded(peer, len);
+                    done = state.saveChunk(piece, bs, bs.length - len, len);
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info("Got chunk " + piece + " from " + peer);
+                    if (!done)
+                        chk = state.getNextRequest();
+                }
+                // out of the lock
+                if (done) {
+                    // Done!
+                    // PeerState will call the listener (peer coord), who will
+                    // check to see if the MagnetState has it
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Got last chunk from " + peer);
+                } else {
+                    // get the next chunk
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info("Request chunk " + chk + " from " + peer);
+                    sendRequest(peer, chk);
+                }
+            } else if (type == TYPE_REJECT) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Got reject msg from " + peer);
+                peer.disconnect(false);
+            } else {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Got unknown metadata msg from " + peer);
+                peer.disconnect(false);
+            }
+        } catch (Exception e) {
+            if (_log.shouldLog(Log.WARN))
+                _log.info("Metadata ext. msg. exception from " + peer, e);
+            // fatal ?
+            peer.disconnect(false);
+        }
+    }
+
+    private static void sendRequest(Peer peer, int piece) {
+        sendMessage(peer, TYPE_REQUEST, piece);
+    }
+
+    private static void sendReject(Peer peer, int piece) {
+        sendMessage(peer, TYPE_REJECT, piece);
+    }
+
+    /** REQUEST and REJECT are the same except for message type */
+    private static void sendMessage(Peer peer, int type, int piece) {
+        Map<String, Object> map = new HashMap();
+        map.put("msg_type", Integer.valueOf(type));
+        map.put("piece", Integer.valueOf(piece));
+        byte[] payload = BEncoder.bencode(map);
+        try {
+            int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_METADATA).getInt();
+            peer.sendExtension(hisMsgCode, payload);
+        } catch (Exception e) {
+            // NPE, no metadata capability
+            if (_log.shouldLog(Log.WARN))
+                _log.info("Metadata send req msg exception to " + peer, e);
+        }
+    }
+
+    private static void sendPiece(Peer peer, int piece, byte[] data) {
+        Map<String, Object> map = new HashMap();
+        map.put("msg_type", Integer.valueOf(TYPE_DATA));
+        map.put("piece", Integer.valueOf(piece));
+        map.put("total_size", Integer.valueOf(data.length));
+        byte[] dict = BEncoder.bencode(map);
+        byte[] payload = new byte[dict.length + data.length];
+        System.arraycopy(dict, 0, payload, 0, dict.length);
+        System.arraycopy(data, 0, payload, dict.length, data.length);
+        try {
+            int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_METADATA).getInt();
+            peer.sendExtension(hisMsgCode, payload);
+        } catch (Exception e) {
+            // NPE, no metadata caps
+            if (_log.shouldLog(Log.WARN))
+                _log.info("Metadata send piece msg exception to " + peer, e);
+        }
+    }
+
+    private static final int HASH_LENGTH = 32;
+
+    /**
+     * Can't find a published standard for this anywhere.
+     * See the libtorrent code.
+     * Here we use the "added" key as a single string of concatenated
+     * 32-byte peer hashes.
+     * added.f and dropped unsupported
+     * @since 0.8.4
+     */
+    private static void handlePEX(Peer peer, PeerListener listener, byte[] bs) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Got PEX msg from " + peer);
+        try {
+            InputStream is = new ByteArrayInputStream(bs);
+            BDecoder dec = new BDecoder(is);
+            BEValue bev = dec.bdecodeMap();
+            Map<String, BEValue> map = bev.getMap();
+            byte[] ids = map.get("added").getBytes();
+            if (ids.length < HASH_LENGTH)
+                return;
+            int len = Math.min(ids.length, (I2PSnarkUtil.MAX_CONNECTIONS - 1) * HASH_LENGTH);
+            List<PeerID> peers = new ArrayList(len / HASH_LENGTH);
+            for (int off = 0; off < len; off += HASH_LENGTH) {
+                byte[] hash = new byte[HASH_LENGTH];
+                System.arraycopy(ids, off, hash, 0, HASH_LENGTH);
+                if (DataHelper.eq(hash, peer.getPeerID().getDestHash()))
+                    continue;
+                PeerID pID = new PeerID(hash);
+                peers.add(pID);
+            }
+            // could include ourselves, listener must remove
+            listener.gotPeers(peer, peers);
+        } catch (Exception e) {
+            if (_log.shouldLog(Log.WARN))
+                _log.info("PEX msg exception from " + peer, e);
+            //peer.disconnect(false);
+        }
+    }
+
+    /**
+     * added.f and dropped unsupported
+     * @param pList non-null
+     * @since 0.8.4
+     */
+    public static void sendPEX(Peer peer, List<Peer> pList) {
+        if (pList.isEmpty())
+            return;
+        Map<String, Object> map = new HashMap();
+        byte[] peers = new byte[HASH_LENGTH * pList.size()];
+        int off = 0;
+        for (Peer p : pList) {
+            System.arraycopy(p.getPeerID().getDestHash(), 0, peers, off, HASH_LENGTH);
+            off += HASH_LENGTH;
+        }
+        map.put("added", peers);
+        byte[] payload = BEncoder.bencode(map);
+        try {
+            int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_PEX).getInt();
+            peer.sendExtension(hisMsgCode, payload);
+        } catch (Exception e) {
+            // NPE, no PEX caps
+            if (_log.shouldLog(Log.WARN))
+                _log.info("PEX msg exception to " + peer, e);
+        }
+    }
+
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandshake.java b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandshake.java
deleted file mode 100644
index fb69b044d284a7414f74b3e58a0b5773fd1c8419..0000000000000000000000000000000000000000
--- a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandshake.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.klomp.snark;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.klomp.snark.bencode.BEncoder;
-import org.klomp.snark.bencode.BEValue;
-
-/**
- * REF: BEP 10 Extension Protocol
- * @since 0.8.2
- */
-class ExtensionHandshake {
-
-    private static final byte[] _payload = buildPayload();
-
-  /**
-   *  @return bencoded data
-   */
-    static byte[] getPayload() {
-        return _payload;
-    }
-
-    /** just a test for now */
-    private static byte[] buildPayload() {
-        Map<String, Object> handshake = new HashMap();
-        Map<String, Integer> m = new HashMap();
-        m.put("foo", Integer.valueOf(99));
-        m.put("bar", Integer.valueOf(101));
-        handshake.put("m", m);
-        handshake.put("p", Integer.valueOf(6881));
-        handshake.put("v", "I2PSnark");
-        handshake.put("reqq", Integer.valueOf(5));
-        return BEncoder.bencode(handshake);
-    }
-}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
index b36c0fdcbf493cb682fb3bc28d5d56f7b744027f..13bda2e5575013d204b5e9f88fd856c51abfdb62 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
@@ -34,6 +34,9 @@ import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 import net.i2p.util.Translate;
 
+import org.klomp.snark.dht.DHT;
+//import org.klomp.snark.dht.KRPC;
+
 /**
  * I2P specific helpers for I2PSnark
  * We use this class as a sort of context for i2psnark
@@ -58,6 +61,8 @@ public class I2PSnarkUtil {
     private int _maxConnections;
     private File _tmpDir;
     private int _startupDelay;
+    private boolean _shouldUseOT;
+    private DHT _dht;
 
     public static final int DEFAULT_STARTUP_DELAY = 3;
     public static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers";
@@ -66,6 +71,9 @@ public class I2PSnarkUtil {
     public static final String DEFAULT_OPENTRACKERS = "http://tracker.welterde.i2p/a";
     public static final int DEFAULT_MAX_UP_BW = 8;  //KBps
     public static final int MAX_CONNECTIONS = 16; // per torrent
+    private static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond";
+    //private static final boolean ENABLE_DHT = true;
+
     public I2PSnarkUtil(I2PAppContext ctx) {
         _context = ctx;
         _log = _context.logManager().getLog(Snark.class);
@@ -78,6 +86,7 @@ public class I2PSnarkUtil {
         _maxUpBW = DEFAULT_MAX_UP_BW;
         _maxConnections = MAX_CONNECTIONS;
         _startupDelay = DEFAULT_STARTUP_DELAY;
+        _shouldUseOT = DEFAULT_USE_OPENTRACKERS;
         // This is used for both announce replies and .torrent file downloads,
         // so it must be available even if not connected to I2CP.
         // so much for multiple instances
@@ -124,9 +133,21 @@ public class I2PSnarkUtil {
         _configured = true;
     }
     
+    /**
+     *  @param KBps
+     */
     public void setMaxUpBW(int limit) {
         _maxUpBW = limit;
+        _opts.put(PROP_MAX_BW, Integer.toString(limit * (1024 * 6 / 5)));   // add a little for overhead
         _configured = true;
+        if (_manager != null) {
+            I2PSession sess = _manager.getSession();
+            if (sess != null) {
+                Properties newProps = new Properties();
+                newProps.putAll(_opts);
+                sess.updateOptions(newProps);
+            }
+        }
     }
     
     public void setMaxConnections(int limit) {
@@ -146,6 +167,10 @@ public class I2PSnarkUtil {
     public int getEepProxyPort() { return _proxyPort; }
     public boolean getEepProxySet() { return _shouldProxy; }
     public int getMaxUploaders() { return _maxUploaders; }
+
+    /**
+     *  @return KBps
+     */
     public int getMaxUpBW() { return _maxUpBW; }
     public int getMaxConnections() { return _maxConnections; }
     public int getStartupDelay() { return _startupDelay; }  
@@ -158,7 +183,7 @@ public class I2PSnarkUtil {
             // try to find why reconnecting after stop
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Connecting to I2P", new Exception("I did it"));
-            Properties opts = new Properties();
+            Properties opts = _context.getProperties();
             if (_opts != null) {
                 for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) {
                     String key = (String)iter.next();
@@ -187,10 +212,20 @@ public class I2PSnarkUtil {
             //    opts.setProperty("i2p.streaming.readTimeout", "120000");
             _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts);
         }
+        // FIXME this only instantiates krpc once, left stuck with old manager
+        //if (ENABLE_DHT && _manager != null && _dht == null)
+        //    _dht = new KRPC(_context, _manager.getSession());
         return (_manager != null);
     }
     
+    /**
+     * @return null if disabled or not started
+     * @since 0.8.4
+     */
+    public DHT getDHT() { return _dht; }
+
     public boolean connected() { return _manager != null; }
+
     /**
      * Destroy the destination itself
      */
@@ -214,6 +249,8 @@ public class I2PSnarkUtil {
         Destination addr = peer.getAddress();
         if (addr == null)
             throw new IOException("Null address");
+        if (addr.equals(getMyDestination()))
+            throw new IOException("Attempt to connect to myself");
         Hash dest = addr.calculateHash();
         if (_shitlist.contains(dest))
             throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted");
@@ -287,15 +324,23 @@ public class I2PSnarkUtil {
     }
     
     String getOurIPString() {
+        Destination dest = getMyDestination();
+        if (dest != null)
+            return dest.toBase64();
+        return "unknown";
+    }
+
+    /**
+     *  @return dest or null
+     *  @since 0.8.4
+     */
+    Destination getMyDestination() {
         if (_manager == null)
-            return "unknown";
+            return null;
         I2PSession sess = _manager.getSession();
-        if (sess != null) {
-            Destination dest = sess.getMyDestination();
-            if (dest != null)
-                return dest.toBase64();
-        }
-        return "unknown";
+        if (sess != null)
+            return sess.getMyDestination();
+        return null;
     }
 
     /** Base64 only - static (no naming service) */
@@ -400,10 +445,10 @@ public class I2PSnarkUtil {
 
     /** comma delimited list open trackers to use as backups */
     /** sorted map of name to announceURL=baseURL */
-    public List getOpenTrackers() { 
+    public List<String> getOpenTrackers() { 
         if (!shouldUseOpenTrackers())
             return null;
-        List rv = new ArrayList(1);
+        List<String> rv = new ArrayList(1);
         String trackers = getOpenTrackerString();
         StringTokenizer tok = new StringTokenizer(trackers, ", ");
         while (tok.hasMoreTokens())
@@ -414,11 +459,27 @@ public class I2PSnarkUtil {
         return rv;
     }
     
+    public void setUseOpenTrackers(boolean yes) {
+        _shouldUseOT = yes;
+    }
+
     public boolean shouldUseOpenTrackers() {
-        String rv = (String) _opts.get(PROP_USE_OPENTRACKERS);
-        if (rv == null)
-            return DEFAULT_USE_OPENTRACKERS;
-        return Boolean.valueOf(rv).booleanValue();
+        return _shouldUseOT;
+    }
+
+    /**
+     *  Like DataHelper.toHexString but ensures no loss of leading zero bytes
+     *  @since 0.8.4
+     */
+    public static String toHex(byte[] b) {
+        StringBuilder buf = new StringBuilder(40);
+        for (int i = 0; i < b.length; i++) {
+            int bi = b[i] & 0xff;
+            if (bi < 16)
+                buf.append('0');
+            buf.append(Integer.toHexString(bi));
+        }
+        return buf.toString();
     }
 
     /** hook between snark's logger and an i2p log */
diff --git a/apps/i2psnark/java/src/org/klomp/snark/MagnetState.java b/apps/i2psnark/java/src/org/klomp/snark/MagnetState.java
new file mode 100644
index 0000000000000000000000000000000000000000..e9eb5638963a309ccf12c123341bd1abee19c5e1
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/MagnetState.java
@@ -0,0 +1,204 @@
+package org.klomp.snark;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataHelper;
+
+import org.klomp.snark.bencode.BDecoder;
+import org.klomp.snark.bencode.BEValue;
+
+/**
+ * Simple state for the download of the metainfo, shared between
+ * Peer and ExtensionHandler.
+ *
+ * Nothing is synchronized here!
+ * Caller must synchronize on this for everything!
+ *
+ * Reference: BEP 9
+ *
+ * @since 0.8.4
+ * author zzz
+ */
+class MagnetState {
+    public static final int CHUNK_SIZE = 16*1024;
+    private static final Random random = I2PAppContext.getGlobalContext().random();
+
+    private final byte[] infohash;
+    private boolean complete;
+    /** if false, nothing below is valid */
+    private boolean isInitialized;
+
+    private int metaSize;
+    private int totalChunks;
+    /** bitfield for the metainfo chunks - will remain null if we start out complete */
+    private BitField requested;
+    private BitField have;
+    /** bitfield for the metainfo */
+    private byte[] metainfoBytes;
+    /** only valid when finished */
+    private MetaInfo metainfo;
+
+    /**
+     *  @param meta null for new magnet
+     */
+    public MagnetState(byte[] iHash, MetaInfo meta) {
+        infohash = iHash;
+        if (meta != null) {
+            metainfo = meta;
+            initialize(meta.getInfoBytes().length);
+            complete = true;
+        }
+    }
+
+    /**
+     *  @param call this for a new magnet when you have the size
+     *  @throws IllegalArgumentException
+     */
+    public void initialize(int size) {
+        if (isInitialized)
+            throw new IllegalArgumentException("already set");
+        isInitialized = true;
+        metaSize = size;
+        totalChunks = (size + (CHUNK_SIZE - 1)) / CHUNK_SIZE;
+        if (metainfo != null) {
+            metainfoBytes = metainfo.getInfoBytes();
+        } else {
+            // we don't need these if complete
+            have = new BitField(totalChunks);
+            requested = new BitField(totalChunks);
+            metainfoBytes = new byte[metaSize];
+        }
+    }
+
+    /**
+     *  @param Call this for a new magnet when the download is complete.
+     *  @throws IllegalArgumentException
+     */
+    public void setMetaInfo(MetaInfo meta) {
+        metainfo = meta;
+    }
+
+    /**
+     *  @throws IllegalArgumentException
+     */
+    public MetaInfo getMetaInfo() {
+        if (!complete)
+            throw new IllegalArgumentException("not complete");
+        return metainfo;
+    }
+
+    /**
+     *  @throws IllegalArgumentException
+     */
+    public int getSize() {
+        if (!isInitialized)
+            throw new IllegalArgumentException("not initialized");
+        return metaSize;
+    }
+
+    public boolean isInitialized() {
+        return isInitialized;
+    }
+
+    public boolean isComplete() {
+        return complete;
+    }
+
+    public int chunkSize(int chunk) {
+        return Math.min(CHUNK_SIZE, metaSize - (chunk * CHUNK_SIZE));
+    }
+
+    /** @return chunk count */
+    public int chunksRemaining() {
+        if (!isInitialized)
+            throw new IllegalArgumentException("not initialized");
+        if (complete)
+            return 0;
+        return totalChunks - have.count();
+    }
+
+    /** @return chunk number */
+    public int getNextRequest() {
+        if (!isInitialized)
+            throw new IllegalArgumentException("not initialized");
+        if (complete)
+            throw new IllegalArgumentException("complete");
+        int rand = random.nextInt(totalChunks);
+        for (int i = 0; i < totalChunks; i++) {
+            int chk = (i + rand) % totalChunks; 
+            if (!(have.get(chk) || requested.get(chk))) {
+                requested.set(chk);
+                return chk; 
+            }
+        }
+        // all requested - end game
+        for (int i = 0; i < totalChunks; i++) {
+            int chk = (i + rand) % totalChunks; 
+            if (!have.get(chk))
+                return chk; 
+        }
+        throw new IllegalArgumentException("complete");
+    }
+
+    /**
+     *  @throws IllegalArgumentException
+     */
+    public byte[] getChunk(int chunk) {
+        if (!complete)
+            throw new IllegalArgumentException("not complete");
+        if (chunk < 0 || chunk >= totalChunks)
+            throw new IllegalArgumentException("bad chunk number");
+        int size = chunkSize(chunk);
+        byte[] rv = new byte[size];
+        System.arraycopy(metainfoBytes, chunk * CHUNK_SIZE, rv, 0, size);
+        // use meta.getInfoBytes() so we don't save it in memory
+        return rv;
+    }
+
+    /**
+     *  @return true if this was the last piece
+     *  @throws NPE, IllegalArgumentException, IOException, ...
+     */
+    public boolean saveChunk(int chunk, byte[] data, int off, int length) throws Exception {
+        if (!isInitialized)
+            throw new IllegalArgumentException("not initialized");
+        if (chunk < 0 || chunk >= totalChunks)
+            throw new IllegalArgumentException("bad chunk number");
+        if (have.get(chunk))
+            return false;  // shouldn't happen if synced
+        int size = chunkSize(chunk);
+        if (size != length)
+            throw new IllegalArgumentException("bad chunk length");
+        System.arraycopy(data, off, metainfoBytes, chunk * CHUNK_SIZE, size);
+        have.set(chunk);
+        boolean done = have.complete();
+        if (done) {
+            metainfo = buildMetaInfo();
+            complete = true;
+        }
+        return done;
+    }
+
+    /**
+     *  @return true if this was the last piece
+     *  @throws NPE, IllegalArgumentException, IOException, ...
+     */
+    public MetaInfo buildMetaInfo() throws Exception {
+        // top map has nothing in it but the info map (no announce)
+        Map<String, Object> map = new HashMap();
+        InputStream is = new ByteArrayInputStream(metainfoBytes);
+        BDecoder dec = new BDecoder(is);
+        BEValue bev = dec.bdecodeMap();
+        map.put("info", bev);
+        MetaInfo newmeta = new MetaInfo(map);
+        if (!DataHelper.eq(newmeta.getInfoHash(), infohash))
+            throw new IOException("info hash mismatch");
+        return newmeta;
+    }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Message.java b/apps/i2psnark/java/src/org/klomp/snark/Message.java
index a9d1e23f2532508d0f5a904d14f45253328e4497..b4344f3ba588a68049fd08aa52bda99da4d3e3b6 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Message.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Message.java
@@ -53,6 +53,7 @@ class Message
 
   // Used for HAVE, REQUEST, PIECE and CANCEL messages.
   // low byte used for EXTENSION message
+  // low two bytes used for PORT message
   int piece;
 
   // Used for REQUEST, PIECE and CANCEL messages.
@@ -67,7 +68,8 @@ class Message
   // Used to do deferred fetch of data
   DataLoader dataLoader;
 
-  SimpleTimer.TimedEvent expireEvent;
+  // now unused
+  //SimpleTimer.TimedEvent expireEvent;
   
   /** Utility method for sending a message through a DataStream. */
   void sendMessage(DataOutputStream dos) throws IOException
@@ -103,10 +105,13 @@ class Message
     if (type == REQUEST || type == CANCEL)
       datalen += 4;
 
-    // length is 1 byte
+    // msg type is 1 byte
     if (type == EXTENSION)
       datalen += 1;
 
+    if (type == PORT)
+      datalen += 2;
+
     // add length of data for piece or bitfield array.
     if (type == BITFIELD || type == PIECE || type == EXTENSION)
       datalen += len;
@@ -130,6 +135,9 @@ class Message
     if (type == EXTENSION)
         dos.writeByte((byte) piece & 0xff);
 
+    if (type == PORT)
+        dos.writeShort(piece & 0xffff);
+
     // Send actual data
     if (type == BITFIELD || type == PIECE || type == EXTENSION)
       dos.write(data, off, len);
@@ -160,6 +168,8 @@ class Message
         return "PIECE(" + piece + "," + begin + "," + length + ")";
       case CANCEL:
         return "CANCEL(" + piece + "," + begin + "," + length + ")";
+      case PORT:
+        return "PORT(" + piece + ")";
       case EXTENSION:
         return "EXTENSION(" + piece + ',' + data.length + ')';
       default:
diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
index ad2680045f19256d440de1db1860d482c925892c..3e13c7a421ee81606da5a67181ec3c3494a707b7 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
@@ -25,6 +25,7 @@ import java.io.InputStream;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -53,37 +54,43 @@ public class MetaInfo
   private final byte[] info_hash;
   private final String name;
   private final String name_utf8;
-  private final List files;
-  private final List files_utf8;
-  private final List lengths;
+  private final List<List<String>> files;
+  private final List<List<String>> files_utf8;
+  private final List<Long> lengths;
   private final int piece_length;
   private final byte[] piece_hashes;
   private final long length;
-  private final Map infoMap;
+  private Map<String, BEValue> infoMap;
 
-  private byte[] torrentdata;
-
-  MetaInfo(String announce, String name, String name_utf8, List files, List lengths,
+  /**
+   *  Called by Storage when creating a new torrent from local data
+   *
+   *  @param announce may be null
+   *  @param files null for single-file torrent
+   *  @param lengths null for single-file torrent
+   */
+  MetaInfo(String announce, String name, String name_utf8, List<List<String>> files, List<Long> lengths,
            int piece_length, byte[] piece_hashes, long length)
   {
     this.announce = announce;
     this.name = name;
     this.name_utf8 = name_utf8;
-    this.files = files;
+    this.files = files == null ? null : Collections.unmodifiableList(files);
     this.files_utf8 = null;
-    this.lengths = lengths;
+    this.lengths = lengths == null ? null : Collections.unmodifiableList(lengths);
     this.piece_length = piece_length;
     this.piece_hashes = piece_hashes;
     this.length = length;
 
     this.info_hash = calculateInfoHash();
-    infoMap = null;
+    //infoMap = null;
   }
 
   /**
    * Creates a new MetaInfo from the given InputStream.  The
    * InputStream must start with a correctly bencoded dictonary
    * describing the torrent.
+   * Caller must close the stream.
    */
   public MetaInfo(InputStream in) throws IOException
   {
@@ -104,23 +111,29 @@ public class MetaInfo
    * 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.
+   * NOT throw a InvalidBEncodingException if the given map does not
+   * contain a valid announce string.
+   * WILL throw a InvalidBEncodingException if the given map does not
+   * contain a valid info dictionary.
    */
   public MetaInfo(Map m) throws InvalidBEncodingException
   {
     if (_log.shouldLog(Log.DEBUG))
         _log.debug("Creating a metaInfo: " + m, new Exception("source"));
     BEValue val = (BEValue)m.get("announce");
-    if (val == null)
-        throw new InvalidBEncodingException("Missing announce string");
-    this.announce = val.getString();
+    // Disabled check, we can get info from a magnet now
+    if (val == null) {
+        //throw new InvalidBEncodingException("Missing announce string");
+        this.announce = null;
+    } else {
+        this.announce = val.getString();
+    }
 
     val = (BEValue)m.get("info");
     if (val == null)
         throw new InvalidBEncodingException("Missing info map");
     Map info = val.getMap();
-    infoMap = info;
+    infoMap = Collections.unmodifiableMap(info);
 
     val = (BEValue)info.get("name");
     if (val == null)
@@ -160,39 +173,39 @@ public class MetaInfo
           throw new InvalidBEncodingException
             ("Missing length number and/or files list");
 
-        List list = val.getList();
+        List<BEValue> list = val.getList();
         int size = list.size();
         if (size == 0)
           throw new InvalidBEncodingException("zero size files list");
 
-        files = new ArrayList(size);
-        files_utf8 = new ArrayList(size);
-        lengths = new ArrayList(size);
+        List<List<String>> m_files = new ArrayList(size);
+        List<List<String>> m_files_utf8 = new ArrayList(size);
+        List<Long> m_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");
+            Map<String, BEValue> desc = list.get(i).getMap();
+            val = desc.get("length");
             if (val == null)
               throw new InvalidBEncodingException("Missing length number");
             long len = val.getLong();
-            lengths.add(new Long(len));
+            m_lengths.add(Long.valueOf(len));
             l += len;
 
             val = (BEValue)desc.get("path");
             if (val == null)
               throw new InvalidBEncodingException("Missing path list");
-            List path_list = val.getList();
+            List<BEValue> 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();
+            List<String> file = new ArrayList(path_length);
+            Iterator<BEValue> it = path_list.iterator();
             while (it.hasNext())
-              file.add(((BEValue)it.next()).getString());
+              file.add(it.next().getString());
 
-            files.add(file);
+            m_files.add(Collections.unmodifiableList(file));
             
             val = (BEValue)desc.get("path.utf-8");
             if (val != null) {
@@ -202,11 +215,14 @@ public class MetaInfo
                     file = new ArrayList(path_length);
                     it = path_list.iterator();
                     while (it.hasNext())
-                        file.add(((BEValue)it.next()).getString());
-                    files_utf8.add(file);
+                        file.add(it.next().getString());
+                    m_files_utf8.add(Collections.unmodifiableList(file));
                 }
             }
           }
+        files = Collections.unmodifiableList(m_files);
+        files_utf8 = Collections.unmodifiableList(m_files_utf8);
+        lengths = Collections.unmodifiableList(m_lengths);
         length = l;
       }
 
@@ -215,6 +231,7 @@ public class MetaInfo
 
   /**
    * Returns the string representing the URL of the tracker for this torrent.
+   * @return may be null!
    */
   public String getAnnounce()
   {
@@ -253,9 +270,8 @@ public class MetaInfo
    * a single name. It has the same size as the list returned by
    * getLengths().
    */
-  public List getFiles()
+  public List<List<String>> getFiles()
   {
-    // XXX - Immutable?
     return files;
   }
 
@@ -264,9 +280,8 @@ public class MetaInfo
    * files, or null if it is a single file. It has the same size as
    * the list returned by getFiles().
    */
-  public List getLengths()
+  public List<Long> getLengths()
   {
-    // XXX - Immutable?
     return lengths;
   }
 
@@ -388,33 +403,42 @@ public class MetaInfo
                         piece_hashes, length);
   }
 
-  public byte[] getTorrentData()
+  /**
+   *  Called by servlet to save a new torrent file generated from local data
+   */
+  public synchronized byte[] getTorrentData()
   {
-    if (torrentdata == null)
-      {
         Map m = new HashMap();
-        m.put("announce", announce);
+        if (announce != null)
+            m.put("announce", announce);
         Map info = createInfoMap();
         m.put("info", info);
-        torrentdata = BEncoder.bencode(m);
-      }
-    return torrentdata;
+        // don't save this locally, we should only do this once
+        return BEncoder.bencode(m);
   }
 
-  private Map createInfoMap()
+  /** @since 0.8.4 */
+  public synchronized byte[] getInfoBytes() {
+    if (infoMap == null)
+        createInfoMap();
+    return BEncoder.bencode(infoMap);
+  }
+
+  /** @return an unmodifiable view of the Map */
+  private Map<String, BEValue> createInfoMap()
   {
+    // if we loaded this metainfo from a file, we have the map
+    if (infoMap != null)
+        return Collections.unmodifiableMap(infoMap);
+    // otherwise we must create it
     Map info = new HashMap();
-    if (infoMap != null) {
-        info.putAll(infoMap);
-        return info;
-    }
     info.put("name", name);
     if (name_utf8 != null)
         info.put("name.utf-8", name_utf8);
     info.put("piece length", Integer.valueOf(piece_length));
     info.put("pieces", piece_hashes);
     if (files == null)
-      info.put("length", new Long(length));
+      info.put("length", Long.valueOf(length));
     else
       {
         List l = new ArrayList();
@@ -429,7 +453,8 @@ public class MetaInfo
           }
         info.put("files", l);
       }
-    return info;
+    infoMap = info;
+    return Collections.unmodifiableMap(infoMap);
   }
 
   private byte[] calculateInfoHash()
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
index cd46bbf8b6a4b26da32515e6335700bf6fdaeef7..5489148121228bab903e2c4ac79ef7c1c03b979b 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
@@ -20,7 +20,6 @@
 
 package org.klomp.snark;
 
-import java.io.BufferedInputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
@@ -28,28 +27,44 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
+import net.i2p.I2PAppContext;
 import net.i2p.client.streaming.I2PSocket;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
 import net.i2p.util.Log;
 
+import org.klomp.snark.bencode.BEValue;
+
 public class Peer implements Comparable
 {
-  private Log _log = new Log(Peer.class);
+  private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(Peer.class);
   // Identifying property, the peer id of the other side.
   private final PeerID peerID;
 
   private final byte[] my_id;
-  final MetaInfo metainfo;
+  private final byte[] infohash;
+  /** will start out null in magnet mode */
+  private MetaInfo metainfo;
+  private Map<String, BEValue> handshakeMap;
 
   // The data in/output streams set during the handshake and used by
   // the actual connections.
   private DataInputStream din;
   private DataOutputStream dout;
 
+  /** running counters */
+  private long downloaded;
+  private long uploaded;
+
   // Keeps state for in/out connections.  Non-null when the handshake
   // was successful, the connection setup and runs
   PeerState state;
 
+  /** shared across all peers on this torrent */
+  MagnetState magnetState;
+
   private I2PSocket sock;
   
   private boolean deregister = true;
@@ -64,18 +79,22 @@ public class Peer implements Comparable
   static final long OPTION_EXTENSION = 0x0000000000100000l;
   static final long OPTION_FAST      = 0x0000000000000004l;
   static final long OPTION_DHT       = 0x0000000000000001l;
+  /** we use a different bit since the compact format is different */
+  static final long OPTION_I2P_DHT   = 0x0000000040000000l;
+  static final long OPTION_AZMP      = 0x1000000000000000l;
   private long options;
 
   /**
    * Outgoing connection.
    * Creates a disconnected peer given a PeerID, your own id and the
    * relevant MetaInfo.
+   * @param metainfo null if in magnet mode
    */
-  public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo)
-    throws IOException
+  public Peer(PeerID peerID, byte[] my_id, byte[] infohash, MetaInfo metainfo)
   {
     this.peerID = peerID;
     this.my_id = my_id;
+    this.infohash = infohash;
     this.metainfo = metainfo;
     _id = ++__id;
     //_log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating"));
@@ -89,12 +108,14 @@ public class Peer implements Comparable
    * get the remote peer id. To completely start the connection call
    * the connect() method.
    *
+   * @param metainfo null if in magnet mode
    * @exception IOException when an error occurred during the handshake.
    */
-  public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, MetaInfo metainfo)
+  public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, byte[] infohash, MetaInfo metainfo)
     throws IOException
   {
     this.my_id = my_id;
+    this.infohash = infohash;
     this.metainfo = metainfo;
     this.sock = sock;
 
@@ -102,7 +123,7 @@ public class Peer implements Comparable
     this.peerID = new PeerID(id, sock.getPeerDestination());
     _id = ++__id;
     if (_log.shouldLog(Log.DEBUG))
-        _log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating " + _id));
+        _log.debug("Creating a new peer " + peerID.toString(), new Exception("creating " + _id));
   }
 
   /**
@@ -192,7 +213,7 @@ public class Peer implements Comparable
    * If the given BitField is non-null it is send to the peer as first
    * message.
    */
-  public void runConnection(I2PSnarkUtil util, PeerListener listener, BitField bitfield)
+  public void runConnection(I2PSnarkUtil util, PeerListener listener, BitField bitfield, MagnetState mState)
   {
     if (state != null)
       throw new IllegalStateException("Peer already started");
@@ -212,19 +233,8 @@ public class Peer implements Comparable
                 throw new IOException("Unable to reach " + peerID);
             }
             InputStream in = sock.getInputStream();
-            OutputStream out = sock.getOutputStream(); //new BufferedOutputStream(sock.getOutputStream());
-            if (true) {
-                // buffered output streams are internally synchronized, so we can't get through to the underlying
-                // I2PSocket's MessageOutputStream to close() it if we are blocking on a write(...).  Oh, and the
-                // buffer is unnecessary anyway, as unbuffered access lets the streaming lib do the 'right thing'.
-                //out = new BufferedOutputStream(out);
-                in = new BufferedInputStream(sock.getInputStream());
-            }
-            //BufferedInputStream bis
-            //  = new BufferedInputStream(sock.getInputStream());
-            //BufferedOutputStream bos
-            //  = new BufferedOutputStream(sock.getOutputStream());
-            byte [] id = handshake(in, out); //handshake(bis, bos);
+            OutputStream out = sock.getOutputStream();
+            byte [] id = handshake(in, out);
             byte [] expected_id = peerID.getID();
             if (expected_id == null) {
                 peerID.setID(id);
@@ -243,14 +253,29 @@ public class Peer implements Comparable
                 _log.debug("Already have din [" + sock + "] with " + toString());
           }
         
+        // bad idea?
+        if (metainfo == null && (options & OPTION_EXTENSION) == 0) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Peer does not support extensions and we need metainfo, dropping");
+            throw new IOException("Peer does not support extensions and we need metainfo, dropping");
+        }
+
         PeerConnectionIn in = new PeerConnectionIn(this, din);
         PeerConnectionOut out = new PeerConnectionOut(this, dout);
         PeerState s = new PeerState(this, listener, metainfo, in, out);
         
         if ((options & OPTION_EXTENSION) != 0) {
             if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Peer supports extensions, sending test message");
-            out.sendExtension(0, ExtensionHandshake.getPayload());
+                _log.debug("Peer supports extensions, sending reply message");
+            int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1;
+            out.sendExtension(0, ExtensionHandler.getHandshake(metasize));
+        }
+
+        if ((options & OPTION_I2P_DHT) != 0 && util.getDHT() != null) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Peer supports DHT, sending PORT message");
+            int port = util.getDHT().getPort();
+            out.sendPort(port);
         }
 
         // Send our bitmap
@@ -259,6 +284,7 @@ public class Peer implements Comparable
     
         // We are up and running!
         state = s;
+        magnetState = mState;
         listener.connected(this);
   
         if (_log.shouldLog(Log.DEBUG))
@@ -293,7 +319,7 @@ public class Peer implements Comparable
    * Sets DataIn/OutputStreams, does the handshake and returns the id
    * reported by the other side.
    */
-  private byte[] handshake(InputStream in, OutputStream out) //BufferedInputStream bis, BufferedOutputStream bos)
+  private byte[] handshake(InputStream in, OutputStream out)
     throws IOException
   {
     din = new DataInputStream(in);
@@ -303,10 +329,13 @@ public class Peer implements Comparable
     dout.write(19);
     dout.write("BitTorrent protocol".getBytes("UTF-8"));
     // Handshake write - options
-    dout.writeLong(OPTION_EXTENSION);
+    long myOptions = OPTION_EXTENSION;
+    // FIXME get util here somehow
+    //if (util.getDHT() != null)
+    //    myOptions |= OPTION_I2P_DHT;
+    dout.writeLong(myOptions);
     // Handshake write - metainfo hash
-    byte[] shared_hash = metainfo.getInfoHash();
-    dout.write(shared_hash);
+    dout.write(infohash);
     // Handshake write - peer id
     dout.write(my_id);
     dout.flush();
@@ -334,7 +363,7 @@ public class Peer implements Comparable
     // Handshake read - metainfo hash
     bs = new byte[20];
     din.readFully(bs);
-    if (!Arrays.equals(shared_hash, bs))
+    if (!Arrays.equals(infohash, bs))
       throw new IOException("Unexpected MetaInfo hash");
 
     // Handshake read - peer id
@@ -342,8 +371,11 @@ public class Peer implements Comparable
     if (_log.shouldLog(Log.DEBUG))
         _log.debug("Read the remote side's hash and peerID fully from " + toString());
 
+    if (DataHelper.eq(my_id, bs))
+        throw new IOException("Connected to myself");
+
     if (options != 0) {
-        // send them something
+        // send them something in runConnection() above
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Peer supports options 0x" + Long.toString(options, 16) + ": " + toString());
     }
@@ -351,6 +383,55 @@ public class Peer implements Comparable
     return bs;
   }
 
+  /** @since 0.8.4 */
+  public long getOptions() {
+      return options;
+  }
+
+  /** @since 0.8.4 */
+  public Destination getDestination() {
+      if (sock == null)
+          return null;
+      return sock.getPeerDestination();
+  }
+
+  /**
+   *  Shared state across all peers, callers must sync on returned object
+   *  @return non-null
+   *  @since 0.8.4
+   */
+  public MagnetState getMagnetState() {
+      return magnetState;
+  }
+
+  /** @return could be null @since 0.8.4 */
+  public Map<String, BEValue> getHandshakeMap() {
+      return handshakeMap;
+  }
+
+  /** @since 0.8.4 */
+  public void setHandshakeMap(Map<String, BEValue> map) {
+      handshakeMap = map;
+  }
+
+  /** @since 0.8.4 */
+  public void sendExtension(int type, byte[] payload) {
+    PeerState s = state;
+    if (s != null)
+        s.out.sendExtension(type, payload);
+  }
+
+  /**
+   *  Switch from magnet mode to normal mode
+   *  @since 0.8.4
+   */
+  public void setMetaInfo(MetaInfo meta) {
+    metainfo = meta;
+    PeerState s = state;
+    if (s != null)
+        s.setMetaInfo(meta);
+  }
+
   public boolean isConnected()
   {
     return state != null;
@@ -513,14 +594,29 @@ public class Peer implements Comparable
     return (s == null) || s.choked;
   }
 
+  /**
+   * Increment the counter.
+   * @since 0.8.4
+   */
+  public void downloaded(int size) {
+      downloaded += size;
+  }
+
+  /**
+   * Increment the counter.
+   * @since 0.8.4
+   */
+  public void uploaded(int size) {
+      uploaded += size;
+  }
+
   /**
    * 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;
+      return downloaded;
   }
 
   /**
@@ -529,8 +625,7 @@ public class Peer implements Comparable
    */
   public long getUploaded()
   {
-    PeerState s = state;
-    return (s != null) ? s.uploaded : 0;
+      return uploaded;
   }
 
   /**
@@ -538,12 +633,8 @@ public class Peer implements Comparable
    */
   public void resetCounters()
   {
-    PeerState s = state;
-    if (s != null)
-      {
-        s.downloaded = 0;
-        s.uploaded = 0;
-      }
+      downloaded = 0;
+      uploaded = 0;
   }
   
   public long getInactiveTime() {
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
index 58ef3ae2db01c25582f858fcc26a74a095a46e41..0adb13d9b78f41a7735de614f8333fa0374f14c1 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
@@ -88,12 +88,11 @@ public class PeerAcceptor
     }
     if (coordinator != null) {
         // single torrent capability
-        MetaInfo meta = coordinator.getMetaInfo();
-        if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
+        if (DataHelper.eq(coordinator.getInfoHash(), peerInfoHash)) {
             if (coordinator.needPeers())
               {
                 Peer peer = new Peer(socket, in, out, coordinator.getID(),
-                                     coordinator.getMetaInfo());
+                                     coordinator.getInfoHash(), coordinator.getMetaInfo());
                 coordinator.addPeer(peer);
               }
             else
@@ -101,26 +100,25 @@ public class PeerAcceptor
         } else {
           // its for another infohash, but we are only single torrent capable.  b0rk.
             throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash) 
-                                  + ") while we only support (" + Base64.encode(meta.getInfoHash()) + ")");
+                                  + ") while we only support (" + Base64.encode(coordinator.getInfoHash()) + ")");
         }
     } else {
         // multitorrent capable, so lets see what we can handle
         for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) {
             PeerCoordinator cur = (PeerCoordinator)iter.next();
-            MetaInfo meta = cur.getMetaInfo();
-            
-            if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
+
+            if (DataHelper.eq(cur.getInfoHash(), peerInfoHash)) {
                 if (cur.needPeers())
                   {
                     Peer peer = new Peer(socket, in, out, cur.getID(),
-                                         cur.getMetaInfo());
+                                         cur.getInfoHash(), cur.getMetaInfo());
                     cur.addPeer(peer);
                     return;
                   }
                 else 
                   {
                     if (_log.shouldLog(Log.DEBUG))
-                      _log.debug("Rejecting new peer for " + cur.snark.torrent);
+                      _log.debug("Rejecting new peer for " + cur.getName());
                     socket.close();
                     return;
                   }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java
index aa9cf2187db0d855dd306199125d939f710ef64d..5d7b6d66db947808fde63b67e859c85dbec3191d 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java
@@ -37,7 +37,8 @@ class PeerCheckerTask extends TimerTask
   private static final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000);
 
   private final PeerCoordinator coordinator;
-  public I2PSnarkUtil _util;
+  private final I2PSnarkUtil _util;
+  private int _runCount;
 
   PeerCheckerTask(I2PSnarkUtil util, PeerCoordinator coordinator)
   {
@@ -49,12 +50,10 @@ class PeerCheckerTask extends TimerTask
 
   public void run()
   {
+        _runCount++;
         List<Peer> peerList = coordinator.peerList();
         if (peerList.isEmpty() || coordinator.halted()) {
-          coordinator.peerCount = 0;
-          coordinator.interestedAndChoking = 0;
           coordinator.setRateHistory(0, 0);
-          coordinator.uploaders = 0;
           if (coordinator.halted())
             cancel();
           return;
@@ -206,7 +205,14 @@ class PeerCheckerTask extends TimerTask
                   }
               }
             peer.retransmitRequests();
+            // send PEX
+            if ((_runCount % 17) == 0 && !peer.isCompleted())
+                coordinator.sendPeers(peer);
             peer.keepAlive();
+            // announce them to local tracker (TrackerClient does this too)
+            if (_util.getDHT() != null && (_runCount % 5) == 0) {
+                _util.getDHT().announce(coordinator.getInfoHash(), peer.getPeerID().getDestHash());
+            }
           }
 
         // Resync actual uploaders value
@@ -247,8 +253,14 @@ class PeerCheckerTask extends TimerTask
 	coordinator.setRateHistory(uploaded, downloaded);
 
         // close out unused files, but we don't need to do it every time
-        if (random.nextInt(4) == 0)
-            coordinator.getStorage().cleanRAFs();
+        Storage storage = coordinator.getStorage();
+        if (storage != null && (_runCount % 4) == 0) {
+                storage.cleanRAFs();
+        }
 
+        // announce ourselves to local tracker (TrackerClient does this too)
+        if (_util.getDHT() != null && (_runCount % 16) == 0) {
+            _util.getDHT().announce(coordinator.getInfoHash());
+        }
   }
 }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
index 43bb9a7122ea121615b0646a2c096e1a20931e43..33da75263e9f20e69e3c9f2d3f932e9b9354b6c1 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
@@ -32,6 +32,13 @@ class PeerConnectionIn implements Runnable
   private final Peer peer;
   private final DataInputStream din;
 
+  // The max length of a complete message in bytes.
+  // The biggest is the piece message, for which the length is the
+  // request size (32K) plus 9. (we could also check if Storage.MAX_PIECES / 8
+  // in the bitfield message is bigger but it's currently 5000/8 = 625 so don't bother)
+  private static final int MAX_MSG_SIZE = Math.max(PeerState.PARTSIZE + 9,
+                                                   MagnetState.CHUNK_SIZE + 100);  // 100 for the ext msg dictionary
+
   private Thread thread;
   private volatile boolean quit;
 
@@ -77,20 +84,16 @@ class PeerConnectionIn implements Runnable
             int len;
         
             // Wait till we hear something...
-            // The length of a complete message in bytes.
-            // The biggest is the piece message, for which the length is the
-            // request size (32K) plus 9. (we could also check if Storage.MAX_PIECES / 8
-            // in the bitfield message is bigger but it's currently 5000/8 = 625 so don't bother)
             int i = din.readInt();
             lastRcvd = System.currentTimeMillis();
-            if (i < 0 || i > PeerState.PARTSIZE + 9)
+            if (i < 0 || i > MAX_MSG_SIZE)
               throw new IOException("Unexpected length prefix: " + i);
 
             if (i == 0)
               {
                 ps.keepAliveMessage();
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received keepalive from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received keepalive from " + peer);
                 continue;
               }
             
@@ -102,35 +105,35 @@ class PeerConnectionIn implements Runnable
               case 0:
                 ps.chokeMessage(true);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received choke from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received choke from " + peer);
                 break;
               case 1:
                 ps.chokeMessage(false);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received unchoke from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received unchoke from " + peer);
                 break;
               case 2:
                 ps.interestedMessage(true);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received interested from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received interested from " + peer);
                 break;
               case 3:
                 ps.interestedMessage(false);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received not interested from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received not interested from " + peer);
                 break;
               case 4:
                 piece = din.readInt();
                 ps.haveMessage(piece);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received havePiece(" + piece + ") from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received havePiece(" + piece + ") from " + peer);
                 break;
               case 5:
                 byte[] bitmap = new byte[i-1];
                 din.readFully(bitmap);
                 ps.bitfieldMessage(bitmap);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received bitmap from " + peer + " on " + peer.metainfo.getName() + ": size=" + (i-1) /* + ": " + ps.bitfield */ );
+                    _log.debug("Received bitmap from " + peer  + ": size=" + (i-1) /* + ": " + ps.bitfield */ );
                 break;
               case 6:
                 piece = din.readInt();
@@ -138,7 +141,7 @@ class PeerConnectionIn implements Runnable
                 len = din.readInt();
                 ps.requestMessage(piece, begin, len);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received request(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received request(" + piece + "," + begin + ") from " + peer);
                 break;
               case 7:
                 piece = din.readInt();
@@ -152,7 +155,7 @@ class PeerConnectionIn implements Runnable
                     din.readFully(piece_bytes, begin, len);
                     ps.pieceMessage(req);
                     if (_log.shouldLog(Log.DEBUG)) 
-                        _log.debug("Received data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
+                        _log.debug("Received data(" + piece + "," + begin + ") from " + peer);
                   }
                 else
                   {
@@ -160,7 +163,7 @@ class PeerConnectionIn implements Runnable
                     piece_bytes = new byte[len];
                     din.readFully(piece_bytes);
                     if (_log.shouldLog(Log.DEBUG)) 
-                        _log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
+                        _log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer);
                   }
                 break;
               case 8:
@@ -169,22 +172,28 @@ class PeerConnectionIn implements Runnable
                 len = din.readInt();
                 ps.cancelMessage(piece, begin, len);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received cancel(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received cancel(" + piece + "," + begin + ") from " + peer);
+                break;
+              case 9:  // PORT message
+                int port = din.readUnsignedShort();
+                ps.portMessage(port);
+                if (_log.shouldLog(Log.DEBUG)) 
+                    _log.debug("Received port message from " + peer);
                 break;
               case 20:  // Extension message
                 int id = din.readUnsignedByte();
                 byte[] payload = new byte[i-2];
                 din.readFully(payload);
-                ps.extensionMessage(id, payload);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received extension message from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received extension message from " + peer);
+                ps.extensionMessage(id, payload);
                 break;
               default:
                 byte[] bs = new byte[i-1];
                 din.readFully(bs);
                 ps.unknownMessage(b, bs);
                 if (_log.shouldLog(Log.DEBUG)) 
-                    _log.debug("Received unknown message from " + peer + " on " + peer.metainfo.getName());
+                    _log.debug("Received unknown message from " + peer);
               }
           }
       }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
index 38cb29e3b445dcae98e8589a59690af494b02c88..225edd4923a3c73fcd83633edfc9e3bda06cfe8d 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
@@ -29,8 +29,8 @@ import java.util.List;
 import net.i2p.I2PAppContext;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
-import net.i2p.util.SimpleScheduler;
-import net.i2p.util.SimpleTimer;
+//import net.i2p.util.SimpleScheduler;
+//import net.i2p.util.SimpleTimer;
 
 class PeerConnectionOut implements Runnable
 {
@@ -124,34 +124,34 @@ class PeerConnectionOut implements Runnable
                           {
                             if (state.choking) {
                               it.remove();
-                              SimpleTimer.getInstance().removeEvent(nm.expireEvent);
+                              //SimpleTimer.getInstance().removeEvent(nm.expireEvent);
                             }
                             nm = null;
                           }
                         else if (nm.type == Message.REQUEST && state.choked)
                           {
                             it.remove();
-                            SimpleTimer.getInstance().removeEvent(nm.expireEvent);
+                            //SimpleTimer.getInstance().removeEvent(nm.expireEvent);
                             nm = null;
                           }
                           
                         if (m == null && nm != null)
                           {
                             m = nm;
-                            SimpleTimer.getInstance().removeEvent(nm.expireEvent);
+                            //SimpleTimer.getInstance().removeEvent(nm.expireEvent);
                             it.remove();
                           }
                       }
                     if (m == null && !sendQueue.isEmpty()) {
                       m = (Message)sendQueue.remove(0);
-                      SimpleTimer.getInstance().removeEvent(m.expireEvent);
+                      //SimpleTimer.getInstance().removeEvent(m.expireEvent);
                     }
                   }
               }
             if (m != null)
               {
                 if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("Send " + peer + ": " + m + " on " + peer.metainfo.getName());
+                    _log.debug("Send " + peer + ": " + m);
 
                 // This can block for quite a while.
                 // To help get slow peers going, and track the bandwidth better,
@@ -241,6 +241,8 @@ class PeerConnectionOut implements Runnable
   
   /** remove messages not sent in 3m */
   private static final int SEND_TIMEOUT = 3*60*1000;
+
+/*****
   private class RemoveTooSlow implements SimpleTimer.TimedEvent {
       private Message _m;
       public RemoveTooSlow(Message m) {
@@ -258,6 +260,7 @@ class PeerConnectionOut implements Runnable
               _log.info("Took too long to send " + _m + " to " + peer);
       }
   }
+*****/
 
   /**
    * Removes a particular message type from the queue.
@@ -474,7 +477,8 @@ class PeerConnectionOut implements Runnable
     m.off = 0;
     m.len = length;
     // since we have the data already loaded, queue a timeout to remove it
-    SimpleScheduler.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT);
+    // no longer prefetched
+    //SimpleScheduler.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT);
     addMessage(m);
   }
 
@@ -547,4 +551,12 @@ class PeerConnectionOut implements Runnable
     m.len = bytes.length;
     addMessage(m);
   }
+
+  /** @since 0.8.4 */
+  void sendPort(int port) {
+    Message m = new Message();
+    m.type = Message.PORT;
+    m.piece = port;
+    addMessage(m);
+  }
 }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
index 8026f34f6826b51c7e703465642e847f54655fe8..cdf1e58b5e70cd3e4cd8adbb947ac76e66fa43ce 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
@@ -27,33 +27,59 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Queue;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.LinkedBlockingQueue;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 import net.i2p.util.SimpleTimer2;
 
+import org.klomp.snark.bencode.BEValue;
+import org.klomp.snark.bencode.InvalidBEncodingException;
+import org.klomp.snark.dht.DHT;
+
 /**
  * Coordinates what peer does what.
  */
 public class PeerCoordinator implements PeerListener
 {
   private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerCoordinator.class);
-  final MetaInfo metainfo;
-  final Storage storage;
-  final Snark snark;
+
+  /**
+   * External use by PeerMonitorTask only.
+   * Will be null when in magnet mode.
+   */
+  MetaInfo metainfo;
+
+  /**
+   * External use by PeerMonitorTask only.
+   * Will be null when in magnet mode.
+   */
+  Storage storage;
+  private final Snark snark;
 
   // package local for access by CheckDownLoadersTask
   final static long CHECK_PERIOD = 40*1000; // 40 seconds
   final static int MAX_UPLOADERS = 6;
 
-  // Approximation of the number of current uploaders.
-  // Resynced by PeerChecker once in a while.
-  int uploaders = 0;
-  int interestedAndChoking = 0;
+  /**
+   * Approximation of the number of current uploaders.
+   * Resynced by PeerChecker once in a while.
+   * External use by PeerCheckerTask only.
+   */
+  int uploaders;
+
+  /**
+   * External use by PeerCheckerTask only.
+   */
+  int interestedAndChoking;
 
   // final static int MAX_DOWNLOADERS = MAX_CONNECTIONS;
   // int downloaders = 0;
@@ -61,19 +87,29 @@ public class PeerCoordinator implements PeerListener
   private long uploaded;
   private long downloaded;
   final static int RATE_DEPTH = 3; // make following arrays RATE_DEPTH long
-  private long uploaded_old[] = {-1,-1,-1};
-  private long downloaded_old[] = {-1,-1,-1};
+  private final long uploaded_old[] = {-1,-1,-1};
+  private final long downloaded_old[] = {-1,-1,-1};
 
-  // synchronize on this when changing peers or downloaders
-  // This is a Queue, not a Set, because PeerCheckerTask keeps things in order for choking/unchoking
+  /**
+   * synchronize on this when changing peers or downloaders.
+   * This is a Queue, not a Set, because PeerCheckerTask keeps things in order for choking/unchoking.
+   * External use by PeerMonitorTask only.
+   */
   final Queue<Peer> peers;
+
+  /**
+   * Peers we heard about via PEX
+   */
+  private final Set<PeerID> pexPeers;
+
   /** estimate of the peers, without requiring any synchronization */
-  volatile int peerCount;
+  private volatile int peerCount;
 
   /** Timer to handle all periodical tasks. */
   private final CheckEvent timer;
 
   private final byte[] id;
+  private final byte[] infohash;
 
   /** The wanted pieces. We could use a TreeSet but we'd have to clear and re-add everything
    *  when priorities change.
@@ -85,18 +121,21 @@ public class PeerCoordinator implements PeerListener
 
   private boolean halted = false;
 
+  private final MagnetState magnetState;
   private final CoordinatorListener listener;
-  public I2PSnarkUtil _util;
+  private final I2PSnarkUtil _util;
   private static final Random _random = I2PAppContext.getGlobalContext().random();
   
-  public String trackerProblems = null;
-  public int trackerSeenPeers = 0;
-
-  public PeerCoordinator(I2PSnarkUtil util, byte[] id, MetaInfo metainfo, Storage storage,
+  /**
+   *  @param metainfo null if in magnet mode
+   *  @param storage null if in magnet mode
+   */
+  public PeerCoordinator(I2PSnarkUtil util, byte[] id, byte[] infohash, MetaInfo metainfo, Storage storage,
                          CoordinatorListener listener, Snark torrent)
   {
     _util = util;
     this.id = id;
+    this.infohash = infohash;
     this.metainfo = metainfo;
     this.storage = storage;
     this.listener = listener;
@@ -106,6 +145,8 @@ public class PeerCoordinator implements PeerListener
     setWantedPieces();
     partialPieces = new ArrayList(getMaxConnections() + 1);
     peers = new LinkedBlockingQueue();
+    magnetState = new MagnetState(infohash, metainfo);
+    pexPeers = new ConcurrentHashSet();
 
     // Install a timer to check the uploaders.
     // Randomize the first start time so multiple tasks are spread out,
@@ -133,6 +174,8 @@ public class PeerCoordinator implements PeerListener
   // only called externally from Storage after the double-check fails
   public void setWantedPieces()
   {
+    if (metainfo == null || storage == null)
+        return;
     // Make a list of pieces
       synchronized(wantedPieces) {
           wantedPieces.clear();
@@ -153,7 +196,6 @@ public class PeerCoordinator implements PeerListener
   }
 
   public Storage getStorage() { return storage; }
-  public CoordinatorListener getListener() { return listener; }
 
   // for web page detailed stats
   public List<Peer> peerList()
@@ -166,8 +208,16 @@ public class PeerCoordinator implements PeerListener
     return id;
   }
 
+  public String getName()
+  {
+    return snark.getName();
+  }
+
   public boolean completed()
   {
+    // FIXME return metainfo complete status
+    if (storage == null)
+        return false;
     return storage.complete();
   }
 
@@ -184,9 +234,12 @@ public class PeerCoordinator implements PeerListener
 
   /**
    * Returns how many bytes are still needed to get the complete file.
+   * @return -1 if in magnet mode
    */
   public long getLeft()
   {
+    if (metainfo == null | storage == null)
+        return -1;
     // XXX - Only an approximation.
     return ((long) storage.needed()) * metainfo.getPieceLength(0);
   }
@@ -271,6 +324,12 @@ public class PeerCoordinator implements PeerListener
     return metainfo;
   }
 
+  /** @since 0.8.4 */
+  public byte[] getInfoHash()
+  {
+    return infohash;
+  }
+
   public boolean needPeers()
   {
         return !halted && peers.size() < getMaxConnections();
@@ -281,6 +340,8 @@ public class PeerCoordinator implements PeerListener
    *  @return 512K: 16; 1M: 11; 2M: 6
    */
   private int getMaxConnections() {
+    if (metainfo == null)
+        return 6;
     int size = metainfo.getPieceLength(0);
     int max = _util.getMaxConnections();
     if (size <= 512*1024 || completed())
@@ -355,8 +416,15 @@ public class PeerCoordinator implements PeerListener
           }
         else
           {
-            if (_log.shouldLog(Log.INFO))
-              _log.info("New connection to peer: " + peer + " for " + metainfo.getName());
+            if (_log.shouldLog(Log.INFO)) {
+                // just for logging
+                String name;
+                if (metainfo == null)
+                    name = "Magnet";
+                else
+                    name = metainfo.getName();
+               _log.info("New connection to peer: " + peer + " for " + name);
+            }
 
             // Add it to the beginning of the list.
             // And try to optimistically make it a uploader.
@@ -415,17 +483,27 @@ public class PeerCoordinator implements PeerListener
 
     if (need_more)
       {
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + metainfo.getName(), new Exception("add/run"));
-
+        if (_log.shouldLog(Log.DEBUG)) {
+            // just for logging
+            String name;
+            if (metainfo == null)
+                name = "Magnet";
+            else
+                name = metainfo.getName();
+            _log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + name, new Exception("add/run"));
+        }
         // Run the peer with us as listener and the current bitfield.
         final PeerListener listener = this;
-        final BitField bitfield = storage.getBitField();
+        final BitField bitfield;
+        if (storage != null)
+            bitfield = storage.getBitField();
+        else
+            bitfield = null;
         Runnable r = new Runnable()
           {
             public void run()
             {
-              peer.runConnection(_util, listener, bitfield);
+              peer.runConnection(_util, listener, bitfield, magnetState);
             }
           };
         String threadName = "Snark peer " + peer.toString();
@@ -486,11 +564,6 @@ public class PeerCoordinator implements PeerListener
         interestedAndChoking = count;
   }
 
-  public byte[] getBitMap()
-  {
-    return storage.getBitField().getFieldBytes();
-  }
-
   /**
    * @return true if we still want the given piece
    */
@@ -647,6 +720,8 @@ public class PeerCoordinator implements PeerListener
    *  @since 0.8.1
    */
   public void updatePiecePriorities() {
+      if (storage == null)
+          return;
       int[] pri = storage.getPiecePriorities();
       if (pri == null) {
           _log.debug("Updated piece priorities called but no priorities to set?");
@@ -713,6 +788,8 @@ public class PeerCoordinator implements PeerListener
   {
     if (halted)
       return null;
+    if (metainfo == null || storage == null)
+        return null;
 
     try
       {
@@ -755,6 +832,8 @@ public class PeerCoordinator implements PeerListener
    */
   public boolean gotPiece(Peer peer, int piece, byte[] bs)
   {
+    if (metainfo == null || storage == null)
+        return true;
     if (halted) {
       _log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
       return true; // We don't actually care anymore.
@@ -951,6 +1030,8 @@ public class PeerCoordinator implements PeerListener
    *  @since 0.8.2
    */
   public PartialPiece getPartialPiece(Peer peer, BitField havePieces) {
+      if (metainfo == null)
+          return null;
       synchronized(wantedPieces) {
           // sorts by remaining bytes, least first
           Collections.sort(partialPieces);
@@ -1057,6 +1138,107 @@ public class PeerCoordinator implements PeerListener
       }
   }
 
+  /**
+   *  PeerListener callback
+   *  @since 0.8.4
+   */
+  public void gotExtension(Peer peer, int id, byte[] bs) {
+      if (_log.shouldLog(Log.DEBUG))
+          _log.debug("Got extension message " + id + " from " + peer);
+      // basic handling done in PeerState... here we just check if we are done
+      if (metainfo == null && id == ExtensionHandler.ID_METADATA) {
+          synchronized (magnetState) {
+              if (magnetState.isComplete()) {
+                  if (_log.shouldLog(Log.WARN))
+                      _log.warn("Got completed metainfo via extension");
+                  metainfo = magnetState.getMetaInfo();
+                  listener.gotMetaInfo(this, metainfo);
+              }
+          }
+      } else if (id == ExtensionHandler.ID_HANDSHAKE) {
+          sendPeers(peer);
+      }
+  }
+
+  /**
+   *  Send a PEX message to the peer, if he supports PEX.
+   *  This just sends everybody we are connected to, we don't
+   *  track new vs. old peers yet.
+   *  @since 0.8.4
+   */
+  void sendPeers(Peer peer) {
+      Map<String, BEValue> handshake = peer.getHandshakeMap();
+      if (handshake == null)
+          return;
+      BEValue bev = handshake.get("m");
+      if (bev == null)
+          return;
+      try {
+          if (bev.getMap().get(ExtensionHandler.TYPE_PEX) != null) {
+              List<Peer> pList = peerList();
+              pList.remove(peer);
+              if (!pList.isEmpty())
+                  ExtensionHandler.sendPEX(peer, pList);
+          }
+      } catch (InvalidBEncodingException ibee) {}
+  }
+
+  /**
+   *  Sets the storage after transition out of magnet mode
+   *  Snark calls this after we call gotMetaInfo()
+   *  @since 0.8.4
+   */
+  public void setStorage(Storage stg) {
+      storage = stg;
+      setWantedPieces();
+      // ok we should be in business
+      for (Peer p : peers) {
+          p.setMetaInfo(metainfo);
+      }
+  }
+
+  /**
+   *  PeerListener callback
+   *  Tell the DHT to ping it, this will get back the node info
+   *  @since 0.8.4
+   */
+  public void gotPort(Peer peer, int port) {
+      DHT dht = _util.getDHT();
+      if (dht != null)
+          dht.ping(peer.getDestination(), port);
+  }
+
+  /**
+   *  PeerListener callback
+   *  @since 0.8.4
+   */
+  public void gotPeers(Peer peer, List<PeerID> peers) {
+      if (completed() || !needPeers())
+          return;
+      Destination myDest = _util.getMyDestination();
+      if (myDest == null)
+          return;
+      byte[] myHash = myDest.calculateHash().getData();
+      List<Peer> pList = peerList();
+      for (PeerID id : peers) {
+           if (peerIDInList(id, pList) != null)
+               continue;
+           if (DataHelper.eq(myHash, id.getDestHash()))
+               continue;
+           pexPeers.add(id);
+      }
+      // TrackerClient will poll for pexPeers and do the add in its thread,
+      // rather than running another thread here.
+  }
+
+  /**
+   *  Called by TrackerClient
+   *  @since 0.8.4
+   */
+  Set<PeerID> getPEXPeers() {
+      return pexPeers;
+  }
+
   /** Return number of allowed uploaders for this torrent.
    ** Check with Snark to see if we are over the total upload limit.
    */
@@ -1072,6 +1254,14 @@ public class PeerCoordinator implements PeerListener
         return MAX_UPLOADERS;
   }
 
+  /**
+   *  @return current
+   *  @since 0.8.4
+   */
+  public int getUploaders() {
+      return uploaders;
+  }
+
   public boolean overUpBWLimit()
   {
     if (listener != null)
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java
index 975c12c1062c7096c51d5da5e0c2b4290838a74e..c7650a55249d4cf485598a97ccc968080fede24a 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java
@@ -179,4 +179,32 @@ interface PeerListener
    * @since 0.8.2
    */
   PartialPiece getPartialPiece(Peer peer, BitField havePieces);
+
+  /**
+   * Called when an extension message is received.
+   *
+   * @param peer the Peer that got the message.
+   * @param id the message ID
+   * @param bs the message payload
+   * @since 0.8.4
+   */
+  void gotExtension(Peer peer, int id, byte[] bs);
+
+  /**
+   * Called when a port message is received.
+   *
+   * @param peer the Peer that got the message.
+   * @param port the port
+   * @since 0.8.4
+   */
+  void gotPort(Peer peer, int port);
+
+  /**
+   * Called when peers are received via PEX
+   *
+   * @param peer the Peer that got the message.
+   * @param pIDList the peer IDs (dest hashes)
+   * @since 0.8.4
+   */
+  void gotPeers(Peer peer, List<PeerID> pIDList);
 }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
index 5551bc90eb3284ccbf3c84c629d391bb15860d7c..aadc5cba25b35829c275a55b6a7ffb948462b942 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
@@ -32,15 +32,13 @@ import java.util.Set;
 import net.i2p.I2PAppContext;
 import net.i2p.util.Log;
 
-import org.klomp.snark.bencode.BDecoder;
-import org.klomp.snark.bencode.BEValue;
-
 class PeerState implements DataLoader
 {
   private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerState.class);
   private final Peer peer;
+  /** Fixme, used by Peer.disconnect() to get to the coordinator */
   final PeerListener listener;
-  private final MetaInfo metainfo;
+  private MetaInfo metainfo;
 
   // Interesting and choking describes whether we are interested in or
   // are choking the other side.
@@ -52,10 +50,6 @@ class PeerState implements DataLoader
   boolean interested = false;
   boolean choked = true;
 
-  // Package local for use by Peer.
-  long downloaded;
-  long uploaded;
-
   /** the pieces the peer has */
   BitField bitfield;
 
@@ -74,6 +68,9 @@ class PeerState implements DataLoader
   public final static int PARTSIZE = 16*1024; // outbound request
   private final static int MAX_PARTSIZE = 64*1024; // Don't let anybody request more than this
 
+  /**
+   * @param metainfo null if in magnet mode
+   */
   PeerState(Peer peer, PeerListener listener, MetaInfo metainfo,
             PeerConnectionIn in, PeerConnectionOut out)
   {
@@ -135,6 +132,9 @@ class PeerState implements DataLoader
   {
     if (_log.shouldLog(Log.DEBUG))
       _log.debug(peer + " rcv have(" + piece + ")");
+    // FIXME we will lose these until we get the metainfo
+    if (metainfo == null)
+        return;
     // Sanity check
     if (piece < 0 || piece >= metainfo.getPieces())
       {
@@ -172,8 +172,15 @@ class PeerState implements DataLoader
           }
         
         // XXX - Check for weird bitfield and disconnect?
-        bitfield = new BitField(bitmap, metainfo.getPieces());
+        // FIXME will have to regenerate the bitfield after we know exactly
+        // how many pieces there are, as we don't know how many spare bits there are.
+        if (metainfo == null)
+            bitfield = new BitField(bitmap, bitmap.length * 8);
+        else
+            bitfield = new BitField(bitmap, metainfo.getPieces());
       }
+    if (metainfo == null)
+        return;
     boolean interest = listener.gotBitField(peer, bitfield);
     setInteresting(interest);
     if (bitfield.complete() && !interest) {
@@ -191,6 +198,8 @@ class PeerState implements DataLoader
     if (_log.shouldLog(Log.DEBUG))
       _log.debug(peer + " rcv request("
                   + piece + ", " + begin + ", " + length + ") ");
+    if (metainfo == null)
+        return;
     if (choking)
       {
         if (_log.shouldLog(Log.INFO))
@@ -273,7 +282,7 @@ class PeerState implements DataLoader
    */
   void uploaded(int size)
   {
-    uploaded += size;
+    peer.uploaded(size);
     listener.uploaded(peer, size);
   }
 
@@ -293,7 +302,7 @@ class PeerState implements DataLoader
   void pieceMessage(Request req)
   {
     int size = req.len;
-    downloaded += size;
+    peer.downloaded(size);
     listener.downloaded(peer, size);
 
     if (_log.shouldLog(Log.DEBUG))
@@ -314,9 +323,6 @@ class PeerState implements DataLoader
           {
             if (_log.shouldLog(Log.WARN))
               _log.warn("Got BAD " + req.piece + " from " + peer);
-            // XXX ARGH What now !?!
-            // FIXME Why would we set downloaded to 0?
-            downloaded = 0;
           }
       }
 
@@ -360,7 +366,6 @@ class PeerState implements DataLoader
           _log.info("Unrequested 'piece: " + piece + ", "
                       + begin + ", " + length + "' received from "
                       + peer);
-        downloaded = 0; // XXX - punishment?
         return null;
       }
 
@@ -385,7 +390,6 @@ class PeerState implements DataLoader
                           + begin + ", "
                           + length + "' received from "
                           + peer);
-            downloaded = 0; // XXX - punishment?
             return null;
           }
 
@@ -485,22 +489,36 @@ class PeerState implements DataLoader
   /** @since 0.8.2 */
   void extensionMessage(int id, byte[] bs)
   {
-      if (id == 0) {
-          InputStream is = new ByteArrayInputStream(bs);
-          try {
-              BDecoder dec = new BDecoder(is);
-              BEValue bev = dec.bdecodeMap();
-              Map map = bev.getMap();
-              if (_log.shouldLog(Log.DEBUG))
-                  _log.debug("Got extension handshake message " + bev.toString());
-          } catch (Exception e) {
-              if (_log.shouldLog(Log.DEBUG))
-                  _log.debug("Failed extension decode", e);
-          }
+      ExtensionHandler.handleMessage(peer, listener, id, bs);
+      // Peer coord will get metadata from MagnetState,
+      // verify, and then call gotMetaInfo()
+      listener.gotExtension(peer, id, bs);
+  }
+
+  /**
+   *  Switch from magnet mode to normal mode
+   *  @since 0.8.4
+   */
+  public void setMetaInfo(MetaInfo meta) {
+      BitField oldBF = bitfield;
+      if (oldBF != null) {
+          if (oldBF.size() != meta.getPieces())
+              // fix bitfield, it was too big by 1-7 bits
+              bitfield = new BitField(oldBF.getFieldBytes(), meta.getPieces());
+          // else no extra
       } else {
-          if (_log.shouldLog(Log.DEBUG))
-              _log.debug("Got extended message type: " + id + " length: " + bs.length);
+          // it will be initialized later
+          //bitfield = new BitField(meta.getPieces());
       }
+      metainfo = meta;
+      if (bitfield.count() > 0)
+          setInteresting(true);
+  }
+
+  /** @since 0.8.4 */
+  void portMessage(int port)
+  {
+      listener.gotPort(peer, port);
   }
 
   void unknownMessage(int type, byte[] bs)
@@ -619,6 +637,8 @@ class PeerState implements DataLoader
     // no bitfield yet? nothing to request then.
     if (bitfield == null)
         return;
+    if (metainfo == null)
+        return;
     boolean more_pieces = true;
     while (more_pieces)
       {
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Piece.java b/apps/i2psnark/java/src/org/klomp/snark/Piece.java
index 6855d36a0ed293069ec7d3cdd1d5b4141e45a20c..dd48508a9a3ba761e22fb122c5c8d0d4197e23f9 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Piece.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Piece.java
@@ -35,8 +35,8 @@ class Piece implements Comparable {
     
     @Override
     public boolean equals(Object o) {
+        if (o == null) return false;
         if (o instanceof Piece) {
-            if (o == null) return false;
             return this.id == ((Piece)o).id;
         }
         return false;
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
index 31105a2693a6906d84114b8bc8a6af5bd2126115..98bcd4eed470b086e45401ed183558c7eeb3a68e 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
@@ -26,6 +26,7 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Properties;
@@ -107,11 +108,13 @@ public class Snark
           } catch (Throwable t) {
               System.out.println("OOM in the OOM");
           }
-          System.exit(0);
+          //System.exit(0);
       }
       
   }
   
+/******** No, not maintaining a command-line client
+
   public static void main(String[] args)
   {
     System.out.println(copyright);
@@ -235,19 +238,27 @@ public class Snark
       }
   }
 
+***********/
+
   public static final String PROP_MAX_CONNECTIONS = "i2psnark.maxConnections";
-  public String torrent;
-  public MetaInfo meta;
-  public Storage storage;
-  public PeerCoordinator coordinator;
-  public ConnectionAcceptor acceptor;
-  public TrackerClient trackerclient;
-  public String rootDataDir = ".";
-  public CompleteListener completeListener;
-  public boolean stopped;
-  byte[] id;
-  public I2PSnarkUtil _util;
-  private PeerCoordinatorSet _peerCoordinatorSet;
+
+  /** most of these used to be public, use accessors below instead */
+  private String torrent;
+  private MetaInfo meta;
+  private Storage storage;
+  private PeerCoordinator coordinator;
+  private ConnectionAcceptor acceptor;
+  private TrackerClient trackerclient;
+  private String rootDataDir = ".";
+  private final CompleteListener completeListener;
+  private boolean stopped;
+  private byte[] id;
+  private byte[] infoHash;
+  private final I2PSnarkUtil _util;
+  private final PeerCoordinatorSet _peerCoordinatorSet;
+  private String trackerProblems;
+  private int trackerSeenPeers;
+
 
   /** from main() via parseArguments() single torrent */
   Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
@@ -306,31 +317,7 @@ public class Snark
     stopped = true;
     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;
-    id = new byte[20];
-    Random random = I2PAppContext.getGlobalContext().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);
-
+    id = generateID();
     debug("My peer id: " + PeerID.idencode(id), Snark.INFO);
 
     int port;
@@ -373,6 +360,7 @@ public class Snark
             }
           }
         meta = new MetaInfo(new BDecoder(in));
+        infoHash = meta.getInfoHash();
       }
     catch(IOException ioe)
       {
@@ -406,6 +394,8 @@ public class Snark
          */
         else
           fatal("Cannot open '" + torrent + "'", ioe);
+      } catch (OutOfMemoryError oom) {
+          fatal("ERROR - Out of memory, cannot create torrent " + torrent + ": " + oom.getMessage());
       } finally {
           if (in != null)
               try { in.close(); } catch (IOException ioe) {}
@@ -457,6 +447,64 @@ public class Snark
     if (start)
         startTorrent();
   }
+
+  /**
+   *  multitorrent, magnet
+   *
+   *  @param torrent a fake name for now (not a file name)
+   *  @param ih 20-byte info hash
+   *  @since 0.8.4
+   */
+  public Snark(I2PSnarkUtil util, String torrent, byte[] ih,
+        CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
+        ConnectionAcceptor connectionAcceptor, boolean start, String rootDir)
+  {
+    completeListener = complistener;
+    _util = util;
+    _peerCoordinatorSet = peerCoordinatorSet;
+    acceptor = connectionAcceptor;
+    this.torrent = torrent;
+    this.infoHash = ih;
+    this.rootDataDir = rootDir;
+    stopped = true;
+    id = generateID();
+
+    // All we have is an infoHash
+    // meta remains null
+    // storage remains null
+
+    if (start)
+        startTorrent();
+  }
+
+  private static byte[] generateID() {
+    // "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[] rv = new byte[20];
+    Random random = I2PAppContext.getGlobalContext().random();
+    int i;
+    for (i = 0; i < 9; i++)
+      rv[i] = 0;
+    rv[i++] = snark;
+    rv[i++] = snark;
+    rv[i++] = snark;
+    while (i < 20)
+      rv[i++] = (byte)random.nextInt(256);
+    return rv;
+  }
+
   /**
    * Start up contacting peers and querying the tracker
    */
@@ -473,7 +521,7 @@ public class Snark
         }
         debug("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient", NOTICE);
         activity = "Collecting pieces";
-        coordinator = new PeerCoordinator(_util, id, meta, storage, this, this);
+        coordinator = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this);
         if (_peerCoordinatorSet != null) {
             // multitorrent
             _peerCoordinatorSet.add(coordinator);
@@ -486,7 +534,8 @@ public class Snark
             // single torrent
             acceptor = new ConnectionAcceptor(_util, serversocket, new PeerAcceptor(coordinator));
         }
-        trackerclient = new TrackerClient(_util, meta, coordinator);
+        // TODO pass saved closest DHT nodes to the tracker? or direct to the coordinator?
+        trackerclient = new TrackerClient(_util, meta, coordinator, this);
     }
 
     stopped = false;
@@ -496,8 +545,7 @@ public class Snark
         // restart safely, so lets build a new one to replace the old
         if (_peerCoordinatorSet != null)
             _peerCoordinatorSet.remove(coordinator);
-        PeerCoordinator newCoord = new PeerCoordinator(_util, coordinator.getID(), coordinator.getMetaInfo(), 
-                                                       coordinator.getStorage(), coordinator.getListener(), this);
+        PeerCoordinator newCoord = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this);
         if (_peerCoordinatorSet != null)
             _peerCoordinatorSet.add(newCoord);
         coordinator = newCoord;
@@ -506,18 +554,17 @@ public class Snark
     if (!trackerclient.started() && !coordinatorChanged) {
         trackerclient.start();
     } else if (trackerclient.halted() || coordinatorChanged) {
-        try
-          {
-            storage.reopen(rootDataDir);
-          }
-        catch (IOException ioe)
-          {
-            try { storage.close(); } catch (IOException ioee) {
-                ioee.printStackTrace();
-            }
-            fatal("Could not reopen storage", ioe);
-          }
-        TrackerClient newClient = new TrackerClient(_util, coordinator.getMetaInfo(), coordinator);
+        if (storage != null) {
+            try {
+                 storage.reopen(rootDataDir);
+             }   catch (IOException ioe) {
+                 try { storage.close(); } catch (IOException ioee) {
+                     ioee.printStackTrace();
+                 }
+                 fatal("Could not reopen storage", ioe);
+             }
+        }
+        TrackerClient newClient = new TrackerClient(_util, meta, coordinator, this);
         if (!trackerclient.halted())
             trackerclient.halt();
         trackerclient = newClient;
@@ -553,18 +600,238 @@ public class Snark
         _util.disconnect();
   }
 
-  static Snark parseArguments(String[] args)
+  private static Snark parseArguments(String[] args)
   {
     return parseArguments(args, null, null);
   }
 
+    // Accessors
+
+    /**
+     *  @return file name of .torrent file (should be full absolute path), or a fake name if in magnet mode.
+     *  @since 0.8.4
+     */
+    public String getName() {
+        return torrent;
+    }
+
+    /**
+     *  @return base name of torrent [filtered version of getMetaInfo.getName()], or a fake name if in magnet mode
+     *  @since 0.8.4
+     */
+    public String getBaseName() {
+        if (storage != null)
+            return storage.getBaseName();
+        return torrent;
+    }
+
+    /**
+     *  @return always will be valid even in magnet mode
+     *  @since 0.8.4
+     */
+    public byte[] getID() {
+        return id;
+    }
+
+    /**
+     *  @return always will be valid even in magnet mode
+     *  @since 0.8.4
+     */
+    public byte[] getInfoHash() {
+        // should always be the same
+        if (meta != null)
+            return meta.getInfoHash();
+        return infoHash;
+    }
+
+    /**
+     *  @return may be null if in magnet mode
+     *  @since 0.8.4
+     */
+    public MetaInfo getMetaInfo() {
+        return meta;
+    }
+
+    /**
+     *  @return may be null if in magnet mode
+     *  @since 0.8.4
+     */
+    public Storage getStorage() {
+        return storage;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public boolean isStopped() {
+        return stopped;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public long getDownloadRate() {
+        PeerCoordinator coord = coordinator;
+        if (coord != null)
+            return coord.getDownloadRate();
+        return 0;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public long getUploadRate() {
+        PeerCoordinator coord = coordinator;
+        if (coord != null)
+            return coord.getUploadRate();
+        return 0;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public long getDownloaded() {
+        PeerCoordinator coord = coordinator;
+        if (coord != null)
+            return coord.getDownloaded();
+        return 0;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public long getUploaded() {
+        PeerCoordinator coord = coordinator;
+        if (coord != null)
+            return coord.getUploaded();
+        return 0;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public int getPeerCount() {
+        PeerCoordinator coord = coordinator;
+        if (coord != null)
+            return coord.getPeerCount();
+        return 0;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public List<Peer> getPeerList() {
+        PeerCoordinator coord = coordinator;
+        if (coord != null)
+            return coord.peerList();
+        return Collections.EMPTY_LIST;
+    }
+
+    /**
+     *  @return String returned from tracker, or null if no error
+     *  @since 0.8.4
+     */
+    public String getTrackerProblems() {
+        return trackerProblems;
+    }
+
+    /**
+     *  @param p tracker error string or null
+     *  @since 0.8.4
+     */
+    public void setTrackerProblems(String p) {
+        trackerProblems = p;
+    }
+
+    /**
+     *  @return count returned from tracker
+     *  @since 0.8.4
+     */
+    public int getTrackerSeenPeers() {
+        return trackerSeenPeers;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public void setTrackerSeenPeers(int p) {
+        trackerSeenPeers = p;
+    }
+
+    /**
+     *  @since 0.8.4
+     */
+    public void updatePiecePriorities() {
+        PeerCoordinator coord = coordinator;
+        if (coord != null)
+            coord.updatePiecePriorities();
+    }
+
+    /**
+     *  @return total of all torrent files, or total of metainfo file if fetching magnet, or -1
+     *  @since 0.8.4
+     */
+    public long getTotalLength() {
+        if (meta != null)
+            return meta.getTotalLength();
+        // FIXME else return metainfo length if available
+        return -1;
+    }
+
+    /**
+     *  @return number of pieces still needed (magnet mode or not), or -1 if unknown
+     *  @since 0.8.4
+     */
+    public long getNeeded() {
+        if (storage != null)
+            return storage.needed();
+        if (meta != null)
+            // FIXME subtract chunks we have
+            return meta.getTotalLength();
+        // FIXME fake
+        return -1;
+    }
+
+    /**
+     *  @param p the piece number
+     *  @return metainfo piece length or 16K if fetching magnet
+     *  @since 0.8.4
+     */
+    public int getPieceLength(int p) {
+        if (meta != null)
+            return meta.getPieceLength(p);
+        return 16*1024;
+    }
+
+    /**
+     *  @return number of pieces
+     *  @since 0.8.4
+     */
+    public int getPieces() {
+        if (meta != null)
+            return meta.getPieces();
+        // FIXME else return metainfo pieces if available
+        return -1;
+    }
+
+    /**
+     *  @return true if restarted
+     *  @since 0.8.4
+     */
+    public boolean restartAcceptor() {
+        if (acceptor == null)
+            return false;
+        acceptor.restart();
+        return true;
+    }
+
   /**
    * 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,
+  private static Snark parseArguments(String[] args,
                               StorageListener slistener,
                               CoordinatorListener clistener)
   {
@@ -713,13 +980,12 @@ public class Snark
       ("  <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 void fatal(String s)
+  private void fatal(String s)
   {
     fatal(s, null);
   }
@@ -727,7 +993,7 @@ public class Snark
   /**
    * Aborts program abnormally.
    */
-  public void fatal(String s, Throwable t)
+  private void fatal(String s, Throwable t)
   {
     _util.debug(s, ERROR, t);
     //System.err.println("snark: " + s + ((t == null) ? "" : (": " + t)));
@@ -751,7 +1017,36 @@ public class Snark
     // System.out.println(peer.toString());
   }
   
-  boolean allocating = false;
+  /**
+   * Called when the PeerCoordinator got the MetaInfo via magnet.
+   * CoordinatorListener.
+   * Create the storage, tell SnarkManager, and give the storage
+   * back to the coordinator.
+   *
+   * @throws RuntimeException via fatal()
+   * @since 0.8.4
+   */
+  public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) {
+      meta = metainfo;
+      try {
+          storage = new Storage(_util, meta, this);
+          storage.check(rootDataDir);
+          if (completeListener != null) {
+              String newName = completeListener.gotMetaInfo(this);
+              if (newName != null)
+                  torrent = newName;
+              // else some horrible problem
+          }
+          coordinator.setStorage(storage);
+      } catch (IOException ioe) {
+          if (storage != null) {
+              try { storage.close(); } catch (IOException ioee) {}
+          }
+          fatal("Could not check or create storage", ioe);
+      }
+  }
+
+  private boolean allocating = false;
   public void storageCreateFile(Storage storage, String name, long length)
   {
     //if (allocating)
@@ -774,9 +1069,9 @@ public class Snark
     //  System.out.println(); // We have all the disk space we need.
   }
 
-  boolean allChecked = false;
-  boolean checking = false;
-  boolean prechecking = true;
+  private boolean allChecked = false;
+  private boolean checking = false;
+  private boolean prechecking = true;
   public void storageChecked(Storage storage, int num, boolean checked)
   {
     allocating = false;
@@ -821,16 +1116,28 @@ public class Snark
     coordinator.setWantedPieces();
   }
 
+  /** SnarkSnutdown callback unused */
   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);
+    //System.exit(0);
   }
   
   public interface CompleteListener {
     public void torrentComplete(Snark snark);
     public void updateStatus(Snark snark);
+
+    /**
+     * We transitioned from magnet mode, we have now initialized our
+     * metainfo and storage. The listener should now call getMetaInfo()
+     * and save the data to disk.
+     *
+     * @return the new name for the torrent or null on error
+     * @since 0.8.4
+     */
+    public String gotMetaInfo(Snark snark);
+
     // not really listeners but the easiest way to get back to an optional SnarkManager
     public long getSavedTorrentTime(Snark snark);
     public BitField getSavedTorrentBitField(Snark snark);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 82d1d80f4f53789bb8a06c21e709fd57ad261a44..3f9f90edfbbc8f0fcd7045337b29317cb6389386 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -5,6 +5,7 @@ import java.io.FileFilter;
 import java.io.FileInputStream;
 import java.io.FilenameFilter;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -16,14 +17,18 @@ import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.TreeMap;
 import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
+import net.i2p.util.ConcurrentHashSet;
+import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 import net.i2p.util.OrderedProperties;
 import net.i2p.util.SecureDirectory;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * Manage multiple snarks
@@ -32,8 +37,14 @@ public class SnarkManager implements Snark.CompleteListener {
     private static SnarkManager _instance = new SnarkManager();
     public static SnarkManager instance() { return _instance; }
     
-    /** map of (canonical) filename of the .torrent file to Snark instance (unsynchronized) */
+    /**
+     *  Map of (canonical) filename of the .torrent file to Snark instance.
+     *  This is a CHM so listTorrentFiles() need not be synced, but
+     *  all adds, deletes, and the DirMonitor should sync on it.
+     */
     private final Map<String, Snark> _snarks;
+    /** used to prevent DirMonitor from deleting torrents that don't have a torrent file yet */
+    private final Set<String> _magnets;
     private final Object _addSnarkLock;
     private /* FIXME final FIXME */ File _configFile;
     private Properties _config;
@@ -57,6 +68,7 @@ public class SnarkManager implements Snark.CompleteListener {
     public static final String PROP_META_PREFIX = "i2psnark.zmeta.";
     public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
     public static final String PROP_META_PRIORITY_SUFFIX = ".priority";
+    public static final String PROP_META_MAGNET_PREFIX = "i2psnark.magnet.";
 
     private static final String CONFIG_FILE = "i2psnark.config";
     public static final String PROP_AUTO_START = "i2snark.autoStart";   // oops
@@ -71,7 +83,8 @@ public class SnarkManager implements Snark.CompleteListener {
     public static final int DEFAULT_MAX_UP_BW = 10;
     public static final int DEFAULT_STARTUP_DELAY = 3; 
     private SnarkManager() {
-        _snarks = new HashMap();
+        _snarks = new ConcurrentHashMap();
+        _magnets = new ConcurrentHashSet();
         _addSnarkLock = new Object();
         _context = I2PAppContext.getGlobalContext();
         _log = _context.logManager().getLog(SnarkManager.class);
@@ -90,8 +103,6 @@ public class SnarkManager implements Snark.CompleteListener {
         _running = true;
         _peerCoordinatorSet = new PeerCoordinatorSet();
         _connectionAcceptor = new ConnectionAcceptor(_util);
-        int minutes = getStartupDelayMinutes();
-        _messages.add(_("Adding torrents in {0} minutes", minutes));
         _monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true);
         _monitor.start();
         _context.addShutdownTask(new SnarkManagerShutdown());
@@ -236,11 +247,9 @@ public class SnarkManager implements Snark.CompleteListener {
                     i2cpOpts.put(pair.substring(0, split), pair.substring(split+1));
             }
         }
-        if (i2cpHost != null) {
-            _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts);
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Configuring with I2CP options " + i2cpOpts);
-        }
+        _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Configuring with I2CP options " + i2cpOpts);
         //I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties());
         //String eepHost = _config.getProperty(PROP_EEP_HOST);
         //int eepPort = getInt(PROP_EEP_PORT, 4444);
@@ -252,7 +261,9 @@ public class SnarkManager implements Snark.CompleteListener {
         String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS);
         if (ot != null)
             _util.setOpenTrackerString(ot);
-        // FIXME set util use open trackers property somehow
+        String useOT = _config.getProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS);
+        boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue();
+        _util.setUseOpenTrackers(bOT);
         getDataDir().mkdirs();
     }
     
@@ -321,15 +332,18 @@ public class SnarkManager implements Snark.CompleteListener {
                 	    _util.setStartupDelay(minutes);
 	                    changed = true;
         	            _config.setProperty(PROP_STARTUP_DELAY, "" + minutes);
-                	    addMessage(_("Startup delay limit changed to {0} minutes", minutes));
+                	    addMessage(_("Startup delay changed to {0}", DataHelper.formatDuration2(minutes * 60 * 1000)));
                 	}
 
 	}
+        // FIXME do this even if == null
 	if (i2cpHost != null) {
             int oldI2CPPort = _util.getI2CPPort();
             String oldI2CPHost = _util.getI2CPHost();
             int port = oldI2CPPort;
-            try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
+            if (i2cpPort != null) {
+                try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
+            }
             String host = oldI2CPHost;
             Map opts = new HashMap();
             if (i2cpOpts == null) i2cpOpts = "";
@@ -359,7 +373,7 @@ public class SnarkManager implements Snark.CompleteListener {
                 Set names = listTorrentFiles();
                 for (Iterator iter = names.iterator(); iter.hasNext(); ) {
                     Snark snark = getTorrent((String)iter.next());
-                    if ( (snark != null) && (!snark.stopped) ) {
+                    if ( (snark != null) && (!snark.isStopped()) ) {
                         snarksActive = true;
                         break;
                     }
@@ -368,6 +382,7 @@ public class SnarkManager implements Snark.CompleteListener {
                     Properties p = new Properties();
                     p.putAll(opts);
                     _util.setI2CPConfig(i2cpHost, port, p);
+                    _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW));
                     addMessage(_("I2CP and tunnel changes will take effect after stopping all torrents"));
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts 
@@ -381,6 +396,7 @@ public class SnarkManager implements Snark.CompleteListener {
                     p.putAll(opts);
                     addMessage(_("I2CP settings changed to {0}", i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")"));
                     _util.setI2CPConfig(i2cpHost, port, p);
+                    _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW));
                     boolean ok = _util.connect();
                     if (!ok) {
                         addMessage(_("Unable to connect with the new settings, reverting to the old I2CP settings"));
@@ -398,9 +414,8 @@ public class SnarkManager implements Snark.CompleteListener {
                         for (Iterator iter = names.iterator(); iter.hasNext(); ) {
                             String name = (String)iter.next();
                             Snark snark = getTorrent(name);
-                            if ( (snark != null) && (snark.acceptor != null) ) {
-                                snark.acceptor.restart();
-                                addMessage(_("I2CP listener restarted for \"{0}\"", snark.meta.getName()));
+                            if (snark != null && snark.restartAcceptor()) {
+                                addMessage(_("I2CP listener restarted for \"{0}\"", snark.getBaseName()));
                             }
                         }
                     }
@@ -422,6 +437,7 @@ public class SnarkManager implements Snark.CompleteListener {
                 addMessage(_("Enabled open trackers - torrent restart required to take effect."));
             else
                 addMessage(_("Disabled open trackers - torrent restart required to take effect."));
+            _util.setUseOpenTrackers(useOpenTrackers);
             changed = true;
         }
         if (openTrackers != null) {
@@ -461,8 +477,13 @@ public class SnarkManager implements Snark.CompleteListener {
     /** hardcoded for sanity.  perhaps this should be customizable, for people who increase their ulimit, etc. */
     private static final int MAX_FILES_PER_TORRENT = 512;
     
-    /** set of canonical .torrent filenames that we are dealing with */
-    public Set<String> listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } }
+    /**
+     *  Set of canonical .torrent filenames that we are dealing with.
+     *  An unsynchronized copy.
+     */
+    public Set<String> listTorrentFiles() {
+        return new HashSet(_snarks.keySet());
+    }
 
     /**
      * Grab the torrent given the (canonical) filename of the .torrent file
@@ -478,17 +499,38 @@ public class SnarkManager implements Snark.CompleteListener {
     public Snark getTorrentByBaseName(String filename) {
         synchronized (_snarks) {
             for (Snark s : _snarks.values()) {
-                if (s.storage.getBaseName().equals(filename))
+                if (s.getBaseName().equals(filename))
                     return s;
             }
         }
         return null;
     }
 
-    /** @throws RuntimeException via Snark.fatal() */
+    /**
+     * Grab the torrent given the info hash
+     * @return Snark or null
+     * @since 0.8.4
+     */
+    public Snark getTorrentByInfoHash(byte[] infohash) {
+        synchronized (_snarks) {
+            for (Snark s : _snarks.values()) {
+                if (DataHelper.eq(infohash, s.getInfoHash()))
+                    return s;
+            }
+        }
+        return null;
+    }
+
+    /**
+     *  Caller must verify this torrent is not already added.
+     *  @throws RuntimeException via Snark.fatal()
+     */
     public void addTorrent(String filename) { addTorrent(filename, false); }
 
-    /** @throws RuntimeException via Snark.fatal() */
+    /**
+     *  Caller must verify this torrent is not already added.
+     *  @throws RuntimeException via Snark.fatal()
+     */
     public void addTorrent(String filename, boolean dontAutoStart) {
         if ((!dontAutoStart) && !_util.connected()) {
             addMessage(_("Connecting to I2P"));
@@ -538,23 +580,26 @@ public class SnarkManager implements Snark.CompleteListener {
                     
                     if (!TrackerClient.isValidAnnounce(info.getAnnounce())) {
                         if (_util.shouldUseOpenTrackers() && _util.getOpenTrackers() != null) {
-                            addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", will announce to i2p open trackers only", info.getName()));
+                            addMessage(_("Warning - No I2P trackers in \"{0}\", will announce to I2P open trackers and DHT only.", info.getName()));
+                        } else if (_util.getDHT() != null) {
+                            addMessage(_("Warning - No I2P trackers in \"{0}\", and open trackers are disabled, will announce to DHT only.", info.getName()));
                         } else {
-                            addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", and open trackers are disabled, you must enable open trackers before starting the torrent!", info.getName()));
+                            addMessage(_("Warning - No I2P trackers in \"{0}\", and DHT and open trackers are disabled, you should enable open trackers or DHT before starting the torrent.", info.getName()));
                             dontAutoStart = true;
                         }
                     }
-                    String rejectMessage = locked_validateTorrent(info);
+                    String rejectMessage = validateTorrent(info);
                     if (rejectMessage != null) {
                         sfile.delete();
                         addMessage(rejectMessage);
                         return;
                     } else {
+                        // TODO load saved closest DHT nodes and pass to the Snark ?
+                        // This may take a LONG time
                         torrent = new Snark(_util, filename, null, -1, null, null, this,
                                             _peerCoordinatorSet, _connectionAcceptor,
                                             false, dataDir.getPath());
                         loadSavedFilePriorities(torrent);
-                        torrent.completeListener = this;
                         synchronized (_snarks) {
                             _snarks.put(filename, torrent);
                         }
@@ -564,6 +609,8 @@ public class SnarkManager implements Snark.CompleteListener {
                     if (sfile.exists())
                         sfile.delete();
                     return;
+                } catch (OutOfMemoryError oom) {
+                    addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", sfile.getName()) + ": " + oom.getMessage());
                 } finally {
                     if (fis != null) try { fis.close(); } catch (IOException ioe) {}
                 }
@@ -572,21 +619,163 @@ public class SnarkManager implements Snark.CompleteListener {
             return;
         }
         // ok, snark created, now lets start it up or configure it further
-        File f = new File(filename);
         if (!dontAutoStart && shouldAutoStart()) {
             torrent.startTorrent();
-            addMessage(_("Torrent added and started: \"{0}\"", torrent.storage.getBaseName()));
+            addMessage(_("Torrent added and started: \"{0}\"", torrent.getBaseName()));
         } else {
-            addMessage(_("Torrent added: \"{0}\"", torrent.storage.getBaseName()));
+            addMessage(_("Torrent added: \"{0}\"", torrent.getBaseName()));
         }
     }
     
     /**
-     * Get the timestamp for a torrent from the config file
+     * Add a torrent with the info hash alone (magnet / maggot)
+     *
+     * @param name hex or b32 name from the magnet link
+     * @param ih 20 byte info hash
+     * @throws RuntimeException via Snark.fatal()
+     * @since 0.8.4
+     */
+    public void addMagnet(String name, byte[] ih, boolean updateStatus) {
+        Snark torrent = new Snark(_util, name, ih, this,
+                                  _peerCoordinatorSet, _connectionAcceptor,
+                                  false, getDataDir().getPath());
+
+        synchronized (_snarks) {
+            Snark snark = getTorrentByInfoHash(ih);
+            if (snark != null) {
+                addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName()));
+                return;
+            }
+            // Tell the dir monitor not to delete us
+            _magnets.add(name);
+            if (updateStatus)
+                saveMagnetStatus(ih);
+            _snarks.put(name, torrent);
+        }
+        if (shouldAutoStart()) {
+            torrent.startTorrent();
+            addMessage(_("Fetching {0}", name));
+            boolean haveSavedPeers = false;
+            if ((!util().connected()) && !haveSavedPeers) {
+                addMessage(_("We have no saved peers and no other torrents are running. " +
+                             "Fetch of {0} will not succeed until you start another torrent.", name));
+            }
+        } else {
+            addMessage(_("Adding {0}", name));
+      }
+    }
+
+    /**
+     * Stop and delete a torrent running in magnet mode
+     *
+     * @param snark a torrent with a fake file name ("Magnet xxxx")
+     * @since 0.8.4
+     */
+    public void deleteMagnet(Snark snark) {
+        synchronized (_snarks) {
+            _snarks.remove(snark.getName());
+        }
+        snark.stopTorrent();
+        _magnets.remove(snark.getName());
+        removeMagnetStatus(snark.getInfoHash());
+    }
+
+    /**
+     * Add a torrent from a MetaInfo. Save the MetaInfo data to filename.
+     * Holds the snarks lock to prevent interference from the DirMonitor.
+     * This verifies that a torrent with this infohash is not already added.
+     * This may take a LONG time to create or check the storage.
+     *
+     * @param metainfo the metainfo for the torrent
+     * @param bitfield the current completion status of the torrent
+     * @param filename the absolute path to save the metainfo to, generally ending in ".torrent", which is also the name of the torrent
+     *                 Must be a filesystem-safe name.
+     * @throws RuntimeException via Snark.fatal()
+     * @since 0.8.4
+     */
+    public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, boolean dontAutoStart) throws IOException {
+        // prevent interference by DirMonitor
+        synchronized (_snarks) {
+            Snark snark = getTorrentByInfoHash(metainfo.getInfoHash());
+            if (snark != null) {
+                addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName()));
+                return;
+            }
+            // so addTorrent won't recheck
+            saveTorrentStatus(metainfo, bitfield, null); // no file priorities
+            try {
+                locked_writeMetaInfo(metainfo, filename);
+                // hold the lock for a long time
+                addTorrent(filename, dontAutoStart);
+            } catch (IOException ioe) {
+                addMessage(_("Failed to copy torrent file to {0}", filename));
+                _log.error("Failed to write torrent file", ioe);
+            }
+        }
+    }
+
+    /**
+     * Add a torrent from a file not in the torrent directory. Copy the file to filename.
+     * Holds the snarks lock to prevent interference from the DirMonitor.
+     * Caller must verify this torrent is not already added.
+     * This may take a LONG time to create or check the storage.
+     *
+     * @param fromfile where the file is now, presumably in a temp directory somewhere
+     * @param filename the absolute path to save the metainfo to, generally ending in ".torrent", which is also the name of the torrent
+     *                 Must be a filesystem-safe name.
+     * @throws RuntimeException via Snark.fatal()
+     * @since 0.8.4
+     */
+    public void copyAndAddTorrent(File fromfile, String filename) throws IOException {
+        // prevent interference by DirMonitor
+        synchronized (_snarks) {
+            boolean success = FileUtil.copy(fromfile.getAbsolutePath(), filename, false);
+            if (!success) {
+                addMessage(_("Failed to copy torrent file to {0}", filename));
+                _log.error("Failed to write torrent file to " + filename);
+                return;
+            }
+            SecureFileOutputStream.setPerms(new File(filename));
+            // hold the lock for a long time
+            addTorrent(filename);
+         }
+    }
+
+    /**
+     * Write the metainfo to the file, caller must hold the snarks lock
+     * to prevent interference from the DirMonitor.
+     *
+     * @param metainfo The metainfo for the torrent
+     * @param filename The absolute path to save the metainfo to, generally ending in ".torrent".
+     *                 Must be a filesystem-safe name.
+     * @since 0.8.4
+     */
+    private static void locked_writeMetaInfo(MetaInfo metainfo, String filename) throws IOException {
+        File file = new File(filename);
+        if (file.exists())
+            throw new IOException("Cannot overwrite an existing .torrent file: " + file.getPath());
+        OutputStream out = null;
+        try {
+            out = new SecureFileOutputStream(filename);
+            out.write(metainfo.getTorrentData());
+        } catch (IOException ioe) {
+            // remove any partial
+            file.delete();
+            throw ioe;
+        } finally {
+            try {
+                if (out == null)
+                    out.close();
+            } catch (IOException ioe) {}
+        }
+    }
+
+    /**
+     * Get the timestamp for a torrent from the config file.
+     * A Snark.CompleteListener method.
      */
     public long getSavedTorrentTime(Snark snark) {
-        MetaInfo metainfo = snark.meta;
-        byte[] ih = metainfo.getInfoHash();
+        byte[] ih = snark.getInfoHash();
         String infohash = Base64.encode(ih);
         infohash = infohash.replace('=', '$');
         String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
@@ -603,10 +792,13 @@ public class SnarkManager implements Snark.CompleteListener {
     /**
      * Get the saved bitfield for a torrent from the config file.
      * Convert "." to a full bitfield.
+     * A Snark.CompleteListener method.
      */
     public BitField getSavedTorrentBitField(Snark snark) {
-        MetaInfo metainfo = snark.meta;
-        byte[] ih = metainfo.getInfoHash();
+        MetaInfo metainfo = snark.getMetaInfo();
+        if (metainfo == null)
+            return null;
+        byte[] ih = snark.getInfoHash();
         String infohash = Base64.encode(ih);
         infohash = infohash.replace('=', '$');
         String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
@@ -636,10 +828,13 @@ public class SnarkManager implements Snark.CompleteListener {
      * @since 0.8.1
      */
     public void loadSavedFilePriorities(Snark snark) {
-        MetaInfo metainfo = snark.meta;
+        MetaInfo metainfo = snark.getMetaInfo();
+        Storage storage = snark.getStorage();
+        if (metainfo == null || storage == null)
+            return;
         if (metainfo.getFiles() == null)
             return;
-        byte[] ih = metainfo.getInfoHash();
+        byte[] ih = snark.getInfoHash();
         String infohash = Base64.encode(ih);
         infohash = infohash.replace('=', '$');
         String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX);
@@ -655,7 +850,7 @@ public class SnarkManager implements Snark.CompleteListener {
                 } catch (Throwable t) {}
             }
         }
-        snark.storage.setFilePriorities(rv);
+        storage.setFilePriorities(rv);
     }
     
     /**
@@ -666,6 +861,8 @@ public class SnarkManager implements Snark.CompleteListener {
      * The time is a standard long converted to string.
      * The status is either a bitfield converted to Base64 or "." for a completed
      * torrent to save space in the config file and in memory.
+     *
+     * @param bitfield non-null
      * @param priorities may be null
      */
     public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
@@ -709,6 +906,8 @@ public class SnarkManager implements Snark.CompleteListener {
             _config.remove(prop);
         }
 
+        // TODO save closest DHT nodes too
+
         saveConfig();
     }
     
@@ -726,9 +925,33 @@ public class SnarkManager implements Snark.CompleteListener {
     }
     
     /**
+     *  Just remember we have it
+     *  @since 0.8.4
+     */
+    public void saveMagnetStatus(byte[] ih) {
+        String infohash = Base64.encode(ih);
+        infohash = infohash.replace('=', '$');
+        _config.setProperty(PROP_META_MAGNET_PREFIX + infohash, ".");
+        saveConfig();
+    }
+    
+    /**
+     *  Remove the magnet marker from the config file.
+     *  @since 0.8.4
+     */
+    public void removeMagnetStatus(byte[] ih) {
+        String infohash = Base64.encode(ih);
+        infohash = infohash.replace('=', '$');
+        _config.remove(PROP_META_MAGNET_PREFIX + infohash);
+        saveConfig();
+    }
+    
+    /**
+     *  Does not really delete on failure, that's the caller's responsibility.
      *  Warning - does not validate announce URL - use TrackerClient.isValidAnnounce()
+     *  @return failure message or null on success
      */
-    private String locked_validateTorrent(MetaInfo info) throws IOException {
+    private String validateTorrent(MetaInfo info) {
         List files = info.getFiles();
         if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
             return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size());
@@ -777,86 +1000,186 @@ public class SnarkManager implements Snark.CompleteListener {
             remaining = _snarks.size();
         }
         if (torrent != null) {
-            boolean wasStopped = torrent.stopped;
+            boolean wasStopped = torrent.isStopped();
             torrent.stopTorrent();
             if (remaining == 0) {
                 // should we disconnect/reconnect here (taking care to deal with the other thread's
                 // I2PServerSocket.accept() call properly?)
                 ////_util.
             }
-            String name;
-            if (torrent.storage != null) {
-                name = torrent.storage.getBaseName();
-            } else {
-                name = sfile.getName();
-            }
             if (!wasStopped)
-                addMessage(_("Torrent stopped: \"{0}\"", name));
+                addMessage(_("Torrent stopped: \"{0}\"", torrent.getBaseName()));
         }
         return torrent;
     }
+
+    /**
+     * Stop the torrent, leaving it on the list of torrents unless told to remove it
+     * @since 0.8.4
+     */
+    public void stopTorrent(Snark torrent, boolean shouldRemove) {
+        if (shouldRemove) {
+            synchronized (_snarks) {
+                _snarks.remove(torrent.getName());
+            }
+        }
+        boolean wasStopped = torrent.isStopped();
+        torrent.stopTorrent();
+        if (!wasStopped)
+            addMessage(_("Torrent stopped: \"{0}\"", torrent.getBaseName()));
+    }
+
     /**
      * Stop the torrent and delete the torrent file itself, but leaving the data
      * behind.
+     * Holds the snarks lock to prevent interference from the DirMonitor.
      */
     public void removeTorrent(String filename) {
-        Snark torrent = stopTorrent(filename, true);
-        if (torrent != null) {
+        Snark torrent;
+        // prevent interference by DirMonitor
+        synchronized (_snarks) {
+            torrent = stopTorrent(filename, true);
+            if (torrent == null)
+                return;
             File torrentFile = new File(filename);
             torrentFile.delete();
-            String name;
-            if (torrent.storage != null) {
-                removeTorrentStatus(torrent.storage.getMetaInfo());
-                name = torrent.storage.getBaseName();
-            } else {
-                name = torrentFile.getName();
-            }
-            addMessage(_("Torrent removed: \"{0}\"", name));
         }
+        Storage storage = torrent.getStorage();
+        if (storage != null)
+            removeTorrentStatus(storage.getMetaInfo());
+        addMessage(_("Torrent removed: \"{0}\"", torrent.getBaseName()));
     }
     
     private class DirMonitor implements Runnable {
         public void run() {
-            try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {}
-            // the first message was a "We are starting up in 1m" 
-            synchronized (_messages) { 
-                if (_messages.size() == 1)
-                    _messages.remove(0);
+            // don't bother delaying if auto start is false
+            long delay = 60 * 1000 * getStartupDelayMinutes();
+            if (delay > 0 && shouldAutoStart()) {
+                _messages.add(_("Adding torrents in {0}", DataHelper.formatDuration2(delay)));
+                try { Thread.sleep(delay); } catch (InterruptedException ie) {}
+                // the first message was a "We are starting up in 1m" 
+                synchronized (_messages) { 
+                    if (_messages.size() == 1)
+                        _messages.remove(0);
+                }
             }
 
             // here because we need to delay until I2CP is up
             // although the user will see the default until then
             getBWLimit();
+            boolean doMagnets = true;
             while (true) {
                 File dir = getDataDir();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
                 try {
-                    monitorTorrents(dir);
+                    // Don't let this interfere with .torrent files being added or deleted
+                    synchronized (_snarks) {
+                        monitorTorrents(dir);
+                    }
                 } catch (Exception e) {
                     _log.error("Error in the DirectoryMonitor", e);
                 }
+                if (doMagnets) {
+                    addMagnets();
+                    doMagnets = false;
+                }
                 try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
             }
         }
     }
     
-    /** two listeners */
+    // Begin Snark.CompleteListeners
+
+    /**
+     * A Snark.CompleteListener method.
+     */
     public void torrentComplete(Snark snark) {
+        MetaInfo meta = snark.getMetaInfo();
+        Storage storage = snark.getStorage();
+        if (meta == null || storage == null)
+            return;
         StringBuilder buf = new StringBuilder(256);
-        buf.append("<a href=\"/i2psnark/").append(snark.storage.getBaseName());
-        if (snark.meta.getFiles() != null)
+        buf.append("<a href=\"/i2psnark/").append(storage.getBaseName());
+        if (meta.getFiles() != null)
             buf.append('/');
-        buf.append("\">").append(snark.storage.getBaseName()).append("</a>");
-        long len = snark.meta.getTotalLength();
+        buf.append("\">").append(storage.getBaseName()).append("</a>");
         addMessage(_("Download finished: {0}", buf.toString())); //  + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')');
         updateStatus(snark);
     }
     
+    /**
+     * A Snark.CompleteListener method.
+     */
     public void updateStatus(Snark snark) {
-        saveTorrentStatus(snark.meta, snark.storage.getBitField(), snark.storage.getFilePriorities());
+        MetaInfo meta = snark.getMetaInfo();
+        Storage storage = snark.getStorage();
+        if (meta != null && storage != null)
+            saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities());
     }
     
+    /**
+     * We transitioned from magnet mode, we have now initialized our
+     * metainfo and storage. The listener should now call getMetaInfo()
+     * and save the data to disk.
+     * A Snark.CompleteListener method.
+     *
+     * @return the new name for the torrent or null on error
+     * @since 0.8.4
+     */
+    public String gotMetaInfo(Snark snark) {
+        MetaInfo meta = snark.getMetaInfo();
+        Storage storage = snark.getStorage();
+        if (meta != null && storage != null) {
+            String rejectMessage = validateTorrent(meta);
+            if (rejectMessage != null) {
+                addMessage(rejectMessage);
+                snark.stopTorrent();
+                return null;
+            }
+            saveTorrentStatus(meta, storage.getBitField(), null); // no file priorities
+            String name = (new File(getDataDir(), storage.getBaseName() + ".torrent")).getAbsolutePath();
+            try {
+                synchronized (_snarks) {
+                    locked_writeMetaInfo(meta, name);
+                    // put it in the list under the new name
+                    _snarks.remove(snark.getName());
+                    _snarks.put(name, snark);
+                }
+                _magnets.remove(snark.getName());
+                removeMagnetStatus(snark.getInfoHash());
+                addMessage(_("Metainfo received for {0}", snark.getName()));
+                addMessage(_("Starting up torrent {0}", storage.getBaseName()));
+                return name;
+            } catch (IOException ioe) {
+                addMessage(_("Failed to copy torrent file to {0}", name));
+                _log.error("Failed to write torrent file", ioe);
+            }
+        }
+        return null;
+    }
+
+    // End Snark.CompleteListeners
+
+    /**
+     * Add all magnets from the config file
+     * @since 0.8.4
+     */
+    private void addMagnets() {
+        for (Object o : _config.keySet()) {
+            String k = (String) o;
+            if (k.startsWith(PROP_META_MAGNET_PREFIX)) {
+                String b64 = k.substring(PROP_META_MAGNET_PREFIX.length());
+                b64 = b64.replace('$', '=');
+                byte[] ih = Base64.decode(b64);
+                // ignore value
+                if (ih != null && ih.length == 20)
+                    addMagnet("Magnet: " + I2PSnarkUtil.toHex(ih), ih, false);
+                // else remove from config?
+            }
+        }
+    }
+
     private void monitorTorrents(File dir) {
         String fileNames[] = dir.list(TorrentFilenameFilter.instance());
         List<String> foundNames = new ArrayList(0);
@@ -887,6 +1210,8 @@ public class SnarkManager implements Snark.CompleteListener {
                 }
             }
         }
+        // Don't remove magnet torrents that don't have a torrent file yet
+        existingNames.removeAll(_magnets);
         // now lets see which ones have been removed...
         for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) {
             String name = (String)iter.next();
@@ -940,12 +1265,12 @@ public class SnarkManager implements Snark.CompleteListener {
     
     /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */
     public static final String PROP_TRACKERS = "i2psnark.trackers";
-    private static Map trackerMap = null;
+    private static Map<String, String> trackerMap = null;
     /** sorted map of name to announceURL=baseURL */
-    public Map getTrackers() { 
+    public Map<String, String> getTrackers() { 
         if (trackerMap != null) // only do this once, can't be updated while running
             return trackerMap;
-        Map rv = new TreeMap();
+        Map<String, String> rv = new TreeMap();
         String trackers = _config.getProperty(PROP_TRACKERS);
         if ( (trackers == null) || (trackers.trim().length() <= 0) )
             trackers = _context.getProperty(PROP_TRACKERS);
@@ -984,7 +1309,7 @@ public class SnarkManager implements Snark.CompleteListener {
             Set names = listTorrentFiles();
             for (Iterator iter = names.iterator(); iter.hasNext(); ) {
                 Snark snark = getTorrent((String)iter.next());
-                if ( (snark != null) && (!snark.stopped) )
+                if ( (snark != null) && (!snark.isStopped()) )
                     snark.stopTorrent();
             }
         }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java b/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java
index 38b470a7c9cf61b07ac1eee30df59a4872721a76..52bef12b4719d7b3d05e1785cfb5bed00f2b5665 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java
@@ -38,6 +38,7 @@ public class StaticSnark
     //Security.addProvider(gnu);
 
     // And finally call the normal starting point.
-    Snark.main(args);
+    //Snark.main(args);
+    System.err.println("unsupported");
   }
 }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
index 17fae523525630ec0abcc345796893791028fcda..dfc229dce201818fe7b9b2e1eec2891b2b9525d8 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
@@ -87,6 +87,9 @@ public class Storage
    * 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.
+   *
+   * @param announce may be null
+   * @param listener may be null
    */
   public Storage(I2PSnarkUtil util, File baseFile, String announce, StorageListener listener)
     throws IOException
@@ -97,12 +100,12 @@ public class Storage
     getFiles(baseFile);
     
     long total = 0;
-    ArrayList lengthsList = new ArrayList();
+    ArrayList<Long> lengthsList = new ArrayList();
     for (int i = 0; i < lengths.length; i++)
       {
         long length = lengths[i];
         total += length;
-        lengthsList.add(new Long(length));
+        lengthsList.add(Long.valueOf(length));
       }
 
     piece_size = MIN_PIECE_SIZE;
@@ -119,10 +122,10 @@ public class Storage
     bitfield = new BitField(pieces);
     needed = 0;
 
-    List files = new ArrayList();
+    List<List<String>> files = new ArrayList();
     for (int i = 0; i < names.length; i++)
       {
-        List file = new ArrayList();
+        List<String> file = new ArrayList();
         StringTokenizer st = new StringTokenizer(names[i], File.separator);
         while (st.hasMoreTokens())
           {
@@ -535,7 +538,7 @@ public class Storage
     } else {
       // the following sets the needed variable
       changed = true;
-      checkCreateFiles();
+      checkCreateFiles(false);
     }
     if (complete()) {
         _util.debug("Torrent is complete", Snark.NOTICE);
@@ -590,7 +593,7 @@ public class Storage
    * Removes 'suspicious' characters from the given file name.
    * http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx
    */
-  private static String filterName(String name)
+  public static String filterName(String name)
   {
     if (name.equals(".") || name.equals(" "))
         return "_";
@@ -646,15 +649,26 @@ public class Storage
   /**
    * This is called at the beginning, and at presumed completion,
    * so we have to be careful about locking.
+   *
+   * @param recheck if true, this is a check after we downloaded the
+   *        last piece, and we don't modify the global bitfield unless
+   *        the check fails.
    */
-  private void checkCreateFiles() throws IOException
+  private void checkCreateFiles(boolean recheck) throws IOException
   {
     // Whether we are resuming or not,
     // if any of the files already exists we assume we are resuming.
     boolean resume = false;
 
     _probablyComplete = true;
-    needed = metainfo.getPieces();
+    // use local variables during the check
+    int need = metainfo.getPieces();
+    BitField bfield;
+    if (recheck) {
+        bfield = new BitField(need);
+    } else {
+        bfield = bitfield;
+    }
 
     // Make sure all files are available and of correct length
     for (int i = 0; i < rafs.length; i++)
@@ -715,8 +729,8 @@ public class Storage
             }
             if (correctHash)
               {
-                bitfield.set(i);
-                needed--;
+                bfield.set(i);
+                need--;
               }
 
             if (listener != null)
@@ -736,6 +750,15 @@ public class Storage
     //  }
     //}
 
+    // do this here so we don't confuse the user during checking
+    needed = need;
+    if (recheck && need > 0) {
+        // whoops, recheck failed
+        synchronized(bitfield) {
+            bitfield = bfield;
+        }
+    }
+
     if (listener != null) {
       listener.storageAllChecked(this);
       if (needed <= 0)
@@ -750,7 +773,8 @@ public class Storage
     openRAF(nr, false);  // RW
     // XXX - Is this the best way to make sure we have enough space for
     // the whole file?
-    listener.storageCreateFile(this, names[nr], lengths[nr]);
+    if (listener != null)
+        listener.storageCreateFile(this, names[nr], lengths[nr]);
     final int ZEROBLOCKSIZE = metainfo.getPieceLength(0);
     byte[] zeros;
     try {
@@ -899,11 +923,7 @@ public class Storage
       // checkCreateFiles() which will set 'needed' and 'bitfield'
       // and also call listener.storageCompleted() if the double-check
       // was successful.
-      // Todo: set a listener variable so the web shows "checking" and don't
-      // have the user panic when completed amount goes to zero temporarily?
-      needed = metainfo.getPieces();
-      bitfield = new BitField(needed);
-      checkCreateFiles();
+      checkCreateFiles(true);
       if (needed > 0) {
         if (listener != null)
             listener.setWantedPieces(this);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
index 89815035b4e6d1a4fa15cd9f7e8098d6877c93d2..c8f1cd6c96d28fdb99734176fc889fe2d1c8fb61 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
@@ -34,9 +34,12 @@ import java.util.Random;
 import java.util.Set;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.Hash;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 
+import org.klomp.snark.dht.DHT;
+
 /**
  * Informs metainfo tracker of events and gets new peers for peer
  * coordinator.
@@ -63,6 +66,7 @@ public class TrackerClient extends I2PAppThread
   private I2PSnarkUtil _util;
   private final MetaInfo meta;
   private final PeerCoordinator coordinator;
+  private final Snark snark;
   private final int port;
 
   private boolean stop;
@@ -70,15 +74,19 @@ public class TrackerClient extends I2PAppThread
 
   private List trackers;
 
-  public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator)
+  /**
+   * @param meta null if in magnet mode
+   */
+  public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator, Snark snark)
   {
     super();
     // Set unique name.
-    String id = urlencode(coordinator.getID());
+    String id = urlencode(snark.getID());
     setName("TrackerClient " + id.substring(id.length() - 12));
     _util = util;
     this.meta = meta;
     this.coordinator = coordinator;
+    this.snark = snark;
 
     this.port = 6881; //(port == -1) ? 9 : port;
 
@@ -118,11 +126,10 @@ public class TrackerClient extends I2PAppThread
     @Override
   public void run()
   {
-    String infoHash = urlencode(meta.getInfoHash());
-    String peerID = urlencode(coordinator.getID());
+    String infoHash = urlencode(snark.getInfoHash());
+    String peerID = urlencode(snark.getID());
+
 
-    _log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash);
-    
     // Construct the list of trackers for this torrent,
     // starting with the primary one listed in the metainfo,
     // followed by the secondary open trackers
@@ -130,12 +137,18 @@ public class TrackerClient extends I2PAppThread
     // the primary tracker, that we don't add it twice.
     // todo: check for b32 matches as well
     trackers = new ArrayList(2);
-    String primary = meta.getAnnounce();
-    if (isValidAnnounce(primary)) {
-        trackers.add(new Tracker(meta.getAnnounce(), true));
-    } else {
-        _log.warn("Skipping invalid or non-i2p announce: " + primary);
+    String primary = null;
+    if (meta != null) {
+        primary = meta.getAnnounce();
+        if (isValidAnnounce(primary)) {
+            trackers.add(new Tracker(meta.getAnnounce(), true));
+            _log.debug("Announce: [" + primary + "] infoHash: " + infoHash);
+        } else {
+            _log.warn("Skipping invalid or non-i2p announce: " + primary);
+        }
     }
+    if (primary == null)
+        primary = "";
     List tlist = _util.getOpenTrackers();
     if (tlist != null) {
         for (int i = 0; i < tlist.size(); i++) {
@@ -160,15 +173,17 @@ public class TrackerClient extends I2PAppThread
                 continue;
              if (primary.startsWith("http://i2p/" + dest))
                 continue;
-             trackers.add(new Tracker(url, false));
+             // opentrackers are primary if we don't have primary
+             trackers.add(new Tracker(url, primary.equals("")));
              _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
         }
     }
 
-    if (tlist.isEmpty()) {
+    if (trackers.isEmpty()) {
         // FIXME really need to get this message to the gui
         stop = true;
         _log.error("No valid trackers for infoHash: " + infoHash);
+        // FIXME keep going if DHT enabled
         return;
     }
 
@@ -188,6 +203,9 @@ public class TrackerClient extends I2PAppThread
         Random r = I2PAppContext.getGlobalContext().random();
         while(!stop)
           {
+            // Local DHT tracker announce
+            if (_util.getDHT() != null)
+                _util.getDHT().announce(snark.getInfoHash());
             try
               {
                 // Sleep some minutes...
@@ -200,7 +218,7 @@ public class TrackerClient extends I2PAppThread
                   firstTime = false;
                 } else if (completed && runStarted)
                   delay = 3*SLEEP*60*1000 + random;
-                else if (coordinator.trackerProblems != null && ++consecutiveFails < MAX_CONSEC_FAILS)
+                else if (snark.getTrackerProblems() != null && ++consecutiveFails < MAX_CONSEC_FAILS)
                   delay = INITIAL_SLEEP;
                 else
                   // sleep a while, when we wake up we will contact only the trackers whose intervals have passed
@@ -221,7 +239,7 @@ public class TrackerClient extends I2PAppThread
             
             uploaded = coordinator.getUploaded();
             downloaded = coordinator.getDownloaded();
-            left = coordinator.getLeft();
+            left = coordinator.getLeft();   // -1 in magnet mode
             
             // First time we got a complete download?
             String event;
@@ -251,7 +269,7 @@ public class TrackerClient extends I2PAppThread
                                                  uploaded, downloaded, left,
                                                  event);
 
-                    coordinator.trackerProblems = null;
+                    snark.setTrackerProblems(null);
                     tr.trackerProblems = null;
                     tr.registerFails = 0;
                     tr.consecutiveFails = 0;
@@ -260,24 +278,30 @@ public class TrackerClient extends I2PAppThread
                     runStarted = true;
                     tr.started = true;
 
-                    Set peers = info.getPeers();
+                    Set<Peer> peers = info.getPeers();
                     tr.seenPeers = info.getPeerCount();
-                    if (coordinator.trackerSeenPeers < tr.seenPeers) // update rising number quickly
-                        coordinator.trackerSeenPeers = tr.seenPeers;
-                    if ( (left > 0) && (!completed) ) {
+                    if (snark.getTrackerSeenPeers() < tr.seenPeers) // update rising number quickly
+                        snark.setTrackerSeenPeers(tr.seenPeers);
+
+                    // pass everybody over to our tracker
+                    if (_util.getDHT() != null) {
+                        for (Peer peer : peers) {
+                            _util.getDHT().announce(snark.getInfoHash(), peer.getPeerID().getDestHash());
+                        }
+                    }
+
+                    if ( (left != 0) && (!completed) ) {
                         // we only want to talk to new people if we need things
                         // from them (duh)
-                        List ordered = new ArrayList(peers);
+                        List<Peer> ordered = new ArrayList(peers);
                         Collections.shuffle(ordered, r);
-                        Iterator it = ordered.iterator();
+                        Iterator<Peer> it = ordered.iterator();
                         while ((!stop) && it.hasNext()) {
-                          Peer cur = (Peer)it.next();
+                          Peer cur = it.next();
                           // FIXME if id == us || dest == us continue;
                           // only delay if we actually make an attempt to add peer
-                          if(coordinator.addPeer(cur)) {
-                            int delay = DELAY_MUL;
-                            delay *= r.nextInt(10);
-                            delay += DELAY_MIN;
+                          if(coordinator.addPeer(cur) && it.hasNext()) {
+                            int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN;
                             sleptTime += delay;
                             try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                           }
@@ -293,12 +317,12 @@ public class TrackerClient extends I2PAppThread
                     tr.trackerProblems = ioe.getMessage();
                     // don't show secondary tracker problems to the user
                     if (tr.isPrimary)
-                      coordinator.trackerProblems = tr.trackerProblems;
+                      snark.setTrackerProblems(tr.trackerProblems);
                     if (tr.trackerProblems.toLowerCase().startsWith(NOT_REGISTERED)) {
                       // Give a guy some time to register it if using opentrackers too
                       if (trackers.size() == 1) {
                         stop = true;
-                        coordinator.snark.stopTorrent();
+                        snark.stopTorrent();
                       } else { // hopefully each on the opentrackers list is really open
                         if (tr.registerFails++ > MAX_REGISTER_FAILS)
                           tr.stop = true;
@@ -315,8 +339,66 @@ public class TrackerClient extends I2PAppThread
                   maxSeenPeers = tr.seenPeers;
             }  // *** end of trackers loop here
 
+            // Get peers from PEX
+            if (left > 0 && coordinator.needPeers() && !stop) {
+                Set<PeerID> pids = coordinator.getPEXPeers();
+                if (!pids.isEmpty()) {
+                    _util.debug("Got " + pids.size() + " from PEX", Snark.INFO);
+                    List<Peer> peers = new ArrayList(pids.size());
+                    for (PeerID pID : pids) {
+                        peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
+                    }
+                    Collections.shuffle(peers, r);
+                    Iterator<Peer> it = peers.iterator();
+                    while ((!stop) && it.hasNext()) {
+                        Peer cur = it.next();
+                        if (coordinator.addPeer(cur) && it.hasNext()) {
+                            int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN;
+                            try { Thread.sleep(delay); } catch (InterruptedException ie) {}
+                         }
+                    }
+                }
+            }
+
+            // Get peers from DHT
+            // FIXME this needs to be in its own thread
+            if (_util.getDHT() != null && !stop) {
+                int numwant;
+                if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
+                    numwant = 1;
+                else
+                    numwant = _util.getMaxConnections();
+                List<Hash> hashes = _util.getDHT().getPeers(snark.getInfoHash(), numwant, 2*60*1000);
+                _util.debug("Got " + hashes + " from DHT", Snark.INFO);
+                // announce  ourselves while the token is still good
+                // FIXME this needs to be in its own thread
+                if (!stop) {
+                    int good = _util.getDHT().announce(snark.getInfoHash(), 8, 5*60*1000);
+                    _util.debug("Sent " + good + " good announces to DHT", Snark.INFO);
+                }
+
+                // now try these peers
+                if ((!stop) && !hashes.isEmpty()) {
+                    List<Peer> peers = new ArrayList(hashes.size());
+                    for (Hash h : hashes) {
+                        PeerID pID = new PeerID(h.getData());
+                        peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
+                    }
+                    Collections.shuffle(peers, r);
+                    Iterator<Peer> it = peers.iterator();
+                    while ((!stop) && it.hasNext()) {
+                        Peer cur = it.next();
+                        if (coordinator.addPeer(cur) && it.hasNext()) {
+                            int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN;
+                            try { Thread.sleep(delay); } catch (InterruptedException ie) {}
+                         }
+                    }
+                }
+            }
+
+
             // we could try and total the unique peers but that's too hard for now
-            coordinator.trackerSeenPeers = maxSeenPeers;
+            snark.setTrackerSeenPeers(maxSeenPeers);
             if (!runStarted)
                 _util.debug("         Retrying in one minute...", Snark.DEBUG);
           } // *** end of while loop
@@ -329,6 +411,9 @@ public class TrackerClient extends I2PAppThread
       }
     finally
       {
+        // Local DHT tracker unannounce
+        if (_util.getDHT() != null)
+            _util.getDHT().unannounce(snark.getInfoHash());
         try
           {
             // try to contact everybody we can
@@ -351,6 +436,8 @@ public class TrackerClient extends I2PAppThread
                                 long downloaded, long left, String event)
     throws IOException
   {
+    // What do we send for left in magnet mode? Can we omit it?
+    long tleft = left >= 0 ? left : 1;
     String s = tr.announce
       + "?info_hash=" + infoHash
       + "&peer_id=" + peerID
@@ -358,10 +445,10 @@ public class TrackerClient extends I2PAppThread
       + "&ip=" + _util.getOurIPString() + ".i2p"
       + "&uploaded=" + uploaded
       + "&downloaded=" + downloaded
-      + "&left=" + left
+      + "&left=" + tleft
       + "&compact=1"   // NOTE: opentracker will return 400 for &compact alone
       + ((! event.equals(NO_EVENT)) ? ("&event=" + event) : "");
-    if (left <= 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
+    if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
         s += "&numwant=0";
     else
         s += "&numwant=" + _util.getMaxConnections();
@@ -377,8 +464,8 @@ public class TrackerClient extends I2PAppThread
     try {
         in = new FileInputStream(fetched);
 
-        TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
-                                           coordinator.getMetaInfo());
+        TrackerInfo info = new TrackerInfo(in, snark.getID(),
+                                           snark.getInfoHash(), snark.getMetaInfo());
         _util.debug("TrackerClient response: " + info, Snark.INFO);
 
         String failure = info.getFailureReason();
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
index 360a4f47e416c3ed728523cfcfbd4e5a09ca9b55..1b829d0eef00d674fdbbf5225381014c931beb8d 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
@@ -46,19 +46,20 @@ public class TrackerInfo
   private int complete;
   private int incomplete;
 
-  public TrackerInfo(InputStream in, byte[] my_id, MetaInfo metainfo)
+  /** @param metainfo may be null */
+  public TrackerInfo(InputStream in, byte[] my_id, byte[] infohash, MetaInfo metainfo)
     throws IOException
   {
-    this(new BDecoder(in), my_id, metainfo);
+    this(new BDecoder(in), my_id, infohash, metainfo);
   }
 
-  public TrackerInfo(BDecoder be, byte[] my_id, MetaInfo metainfo)
+  private TrackerInfo(BDecoder be, byte[] my_id, byte[] infohash, MetaInfo metainfo)
     throws IOException
   {
-    this(be.bdecodeMap().getMap(), my_id, metainfo);
+    this(be.bdecodeMap().getMap(), my_id, infohash, metainfo);
   }
 
-  public TrackerInfo(Map m, byte[] my_id, MetaInfo metainfo)
+  private TrackerInfo(Map m, byte[] my_id, byte[] infohash, MetaInfo metainfo)
     throws IOException
   {
     BEValue reason = (BEValue)m.get("failure reason");
@@ -84,10 +85,10 @@ public class TrackerInfo
             Set<Peer> p;
             try {
               // One big string (the official compact format)
-              p = getPeers(bePeers.getBytes(), my_id, metainfo);
+              p = getPeers(bePeers.getBytes(), my_id, infohash, metainfo);
             } catch (InvalidBEncodingException ibe) {
               // List of Dictionaries or List of Strings
-              p = getPeers(bePeers.getList(), my_id, metainfo);
+              p = getPeers(bePeers.getList(), my_id, infohash, metainfo);
             }
             peers = p;
         }
@@ -123,7 +124,7 @@ public class TrackerInfo
 ******/
 
   /** List of Dictionaries or List of Strings */
-  private static Set<Peer> getPeers(List<BEValue> l, byte[] my_id, MetaInfo metainfo)
+  private static Set<Peer> getPeers(List<BEValue> l, byte[] my_id, byte[] infohash, MetaInfo metainfo)
     throws IOException
   {
     Set<Peer> peers = new HashSet(l.size());
@@ -144,7 +145,7 @@ public class TrackerInfo
                 continue;
             }
         }
-        peers.add(new Peer(peerID, my_id, metainfo));
+        peers.add(new Peer(peerID, my_id, infohash, metainfo));
       }
 
     return peers;
@@ -156,7 +157,7 @@ public class TrackerInfo
    *  One big string of concatenated 32-byte hashes
    *  @since 0.8.1
    */
-  private static Set<Peer> getPeers(byte[] l, byte[] my_id, MetaInfo metainfo)
+  private static Set<Peer> getPeers(byte[] l, byte[] my_id, byte[] infohash, MetaInfo metainfo)
     throws IOException
   {
     int count = l.length / HASH_LENGTH;
@@ -172,7 +173,7 @@ public class TrackerInfo
             // won't happen
             continue;
         }
-        peers.add(new Peer(peerID, my_id, metainfo));
+        peers.add(new Peer(peerID, my_id, infohash, metainfo));
       }
 
     return peers;
diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
index 986e456437a7e57770e442d207f12b3377019c13..4cae2881ae6c7d70a5e809c7187348b2d6484cd8 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
@@ -24,6 +24,8 @@ import java.io.UnsupportedEncodingException;
 import java.util.List;
 import java.util.Map;
 
+import net.i2p.data.Base64;
+
 /**
  * Holds different types that a bencoded byte array can represent.
  * You need to call the correct get method to get the correct java
@@ -178,12 +180,37 @@ public class BEValue
     String valueString;
     if (value instanceof byte[])
       {
+        // try to do a nice job for debugging
         byte[] bs = (byte[])value;
-        // XXX - Stupid heuristic... and not UTF-8
-        if (bs.length <= 12)
-          valueString = new String(bs);
-        else
-          valueString = "bytes:" + bs.length;
+        if (bs.length == 0)
+          valueString =  "0 bytes";
+        else if (bs.length <= 32) {
+          StringBuilder buf = new StringBuilder(32);
+          boolean bin = false;
+          for (int i = 0; i < bs.length; i++) {
+              int b = bs[i] & 0xff;
+              // no UTF-8
+              if (b < ' ' || b > 0x7e) {
+                  bin = true;
+                  break;
+              }
+          }
+          if (bin && bs.length <= 8) {
+              buf.append(bs.length).append(" bytes: 0x");
+              for (int i = 0; i < bs.length; i++) {
+                  int b = bs[i] & 0xff;
+                  if (b < 16)
+                      buf.append('0');
+                  buf.append(Integer.toHexString(b));
+              }
+          } else if (bin) {
+              buf.append(bs.length).append(" bytes: ").append(Base64.encode(bs));
+          } else {
+              buf.append('"').append(new String(bs)).append('"');
+          }
+          valueString = buf.toString();
+        } else
+          valueString =  bs.length + " bytes";
       }
     else
       valueString = value.toString();
diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java
index b8129f47722de6595fd470b162a4ad155af726a2..9584b0d9db322e8fa1276be415cc190e46bee4a3 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java
@@ -50,6 +50,8 @@ public class BEncoder
   public static void bencode(Object o, OutputStream out)
     throws IOException, IllegalArgumentException
   {
+    if (o == null)
+      throw new NullPointerException("Cannot bencode null");
     if (o instanceof String)
       bencode((String)o, out);
     else if (o instanceof byte[])
@@ -59,7 +61,7 @@ public class BEncoder
     else if (o instanceof List)
       bencode((List)o, out);
     else if (o instanceof Map)
-      bencode((Map)o, out);
+      bencode((Map<String, Object>)o, out);
     else if (o instanceof BEValue)
       bencode(((BEValue)o).getValue(), out);
     else
@@ -153,7 +155,7 @@ public class BEncoder
     out.write(bs);
   }
 
-  public static byte[] bencode(Map m)
+  public static byte[] bencode(Map<String, Object> m)
   {
     try
       {
@@ -167,20 +169,20 @@ public class BEncoder
       }
   }
 
-  public static void bencode(Map m, OutputStream out) throws IOException
+  public static void bencode(Map<String, Object> 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);
+    Set<String> s = m.keySet();
+    List<String> l = new ArrayList(s);
     Collections.sort(l);
 
-    Iterator it = l.iterator();
+    Iterator<String> it = l.iterator();
     while(it.hasNext())
       {
         // Keys must be Strings.
-        String key = (String)it.next();
+        String key = it.next();
         Object value = m.get(key);
         bencode(key, out);
         bencode(value, out);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java
new file mode 100644
index 0000000000000000000000000000000000000000..a074890b93ff555c753fdb1cc0959a8eab67d45d
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java
@@ -0,0 +1,82 @@
+package org.klomp.snark.dht;
+
+/*
+ *  GPLv2
+ */
+
+import java.util.List;
+
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+
+
+/**
+ * Stub for KRPC
+ */
+public interface DHT {
+
+
+    /**
+     *  @return The UDP port that should be included in a PORT message.
+     */
+    public int getPort();
+
+    /**
+     *  Ping. We don't have a NID yet so the node is presumed
+     *  to be absent from our DHT.
+     *  Non-blocking, does not wait for pong.
+     *  If and when the pong is received the node will be inserted in our DHT.
+     */
+    public void ping(Destination dest, int port);
+
+    /**
+     *  Get peers for a torrent.
+     *  Blocking!
+     *  Caller should run in a thread.
+     *
+     *  @param ih the Info Hash (torrent)
+     *  @param max maximum number of peers to return
+     *  @param maxWait the maximum time to wait (ms) must be > 0
+     *  @return list or empty list (never null)
+     */
+    public List<Hash> getPeers(byte[] ih, int max, long maxWait);
+
+    /**
+     *  Announce to ourselves.
+     *  Non-blocking.
+     *
+     *  @param ih the Info Hash (torrent)
+     */
+    public void announce(byte[] ih);
+
+    /**
+     *  Announce somebody else we know about.
+     *  Non-blocking.
+     *
+     *  @param ih the Info Hash (torrent)
+     *  @param peer the peer's Hash
+     */
+    public void announce(byte[] ih, byte[] peerHash);
+
+    /**
+     *  Remove reference to ourselves in the local tracker.
+     *  Use when shutting down the torrent locally.
+     *  Non-blocking.
+     *
+     *  @param ih the Info Hash (torrent)
+     */
+    public void unannounce(byte[] ih);
+
+    /**
+     *  Announce to the closest DHT peers.
+     *  Blocking unless maxWait <= 0
+     *  Caller should run in a thread.
+     *  This also automatically announces ourself to our local tracker.
+     *  For best results do a getPeers() first so we have tokens.
+     *
+     *  @param ih the Info Hash (torrent)
+     *  @param maxWait the maximum total time to wait (ms) or 0 to do all in parallel and return immediately.
+     *  @return the number of successful announces, not counting ourselves.
+     */
+    public int announce(byte[] ih, int max, long maxWait);
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index cfe95df13b7e91b1fb457414a63eab796e31812e..9bbfec8fee5dfb299952bd9db6c677140e949d50 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -6,6 +6,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.text.Collator;
+import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -26,6 +27,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.Base32;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 import net.i2p.util.FileUtil;
@@ -33,6 +35,7 @@ import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 import net.i2p.util.SecureFileOutputStream;
 
+import org.klomp.snark.I2PSnarkUtil;
 import org.klomp.snark.MetaInfo;
 import org.klomp.snark.Peer;
 import org.klomp.snark.Snark;
@@ -58,8 +61,13 @@ public class I2PSnarkServlet extends Default {
     private Resource _resourceBase;
     private String _themePath;
     private String _imgPath;
+    private String _lastAnnounceURL = "";
     
     public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
+    /** BEP 9 */
+    private static final String MAGNET = "magnet:?xt=urn:btih:";
+    /** http://sponge.i2p/files/maggotspec.txt */
+    private static final String MAGGOT = "maggot://";
  
     @Override
     public void init(ServletConfig cfg) throws ServletException {
@@ -153,7 +161,7 @@ public class I2PSnarkServlet extends Default {
                 resp.setCharacterEncoding("UTF-8");
                 resp.setContentType("text/html; charset=UTF-8");
                 Resource resource = getResource(pathInContext);
-                if (resource == null || (!resource.exists()) || !resource.isDirectory()) {
+                if (resource == null || (!resource.exists())) {
                     resp.sendError(HttpResponse.__404_Not_Found);
                 } else {
                     String base = URI.addPaths(req.getRequestURI(), "/");
@@ -376,7 +384,7 @@ public class I2PSnarkServlet extends Default {
         for (int i = 0; i < snarks.size(); i++) {
             Snark snark = (Snark)snarks.get(i);
             boolean showDebug = "2".equals(peerParam);
-            boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.meta.getInfoHash()).equals(peerParam);
+            boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.getInfoHash()).equals(peerParam);
             displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug);
         }
 
@@ -478,10 +486,12 @@ public class I2PSnarkServlet extends Default {
             if (newURL != null) {
                 if (newURL.startsWith("http://")) {
                     _manager.addMessage(_("Fetching {0}", urlify(newURL)));
-                    I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add");
+                    I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add", true);
                     fetch.start();
+                } else if (newURL.startsWith(MAGNET) || newURL.startsWith(MAGGOT)) {
+                    addMagnet(newURL);
                 } else {
-                    _manager.addMessage(_("Invalid URL - must start with http://"));
+                    _manager.addMessage(_("Invalid URL: Must start with \"http://\", \"{0}\", or \"{1}\"", MAGNET, MAGGOT));
                 }
             } else {
                 // no file or URL specified
@@ -494,8 +504,8 @@ public class I2PSnarkServlet extends Default {
                     for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                         String name = (String)iter.next();
                         Snark snark = _manager.getTorrent(name);
-                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
-                            _manager.stopTorrent(name, false);
+                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
+                            _manager.stopTorrent(snark, false);
                             break;
                         }
                     }
@@ -508,11 +518,9 @@ public class I2PSnarkServlet extends Default {
                 if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                     for (String name : _manager.listTorrentFiles()) {
                         Snark snark = _manager.getTorrent(name);
-                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
+                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
                             snark.startTorrent();
-                            if (snark.storage != null)
-                                name = snark.storage.getBaseName();
-                            _manager.addMessage(_("Starting up torrent {0}", name));
+                            _manager.addMessage(_("Starting up torrent {0}", snark.getBaseName()));
                             break;
                         }
                     }
@@ -526,8 +534,15 @@ public class I2PSnarkServlet extends Default {
                     for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                         String name = (String)iter.next();
                         Snark snark = _manager.getTorrent(name);
-                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
-                            _manager.stopTorrent(name, true);
+                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
+                            MetaInfo meta = snark.getMetaInfo();
+                            if (meta == null) {
+                                // magnet - remove and delete are the same thing
+                                _manager.deleteMagnet(snark);
+                                _manager.addMessage(_("Magnet deleted: {0}", name));
+                                return;
+                            }
+                            _manager.stopTorrent(snark, true);
                             // should we delete the torrent file?
                             // yeah, need to, otherwise it'll get autoadded again (at the moment
                             File f = new File(name);
@@ -546,13 +561,20 @@ public class I2PSnarkServlet extends Default {
                     for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                         String name = (String)iter.next();
                         Snark snark = _manager.getTorrent(name);
-                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
-                            _manager.stopTorrent(name, true);
+                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
+                            MetaInfo meta = snark.getMetaInfo();
+                            if (meta == null) {
+                                // magnet - remove and delete are the same thing
+                                _manager.deleteMagnet(snark);
+                                _manager.addMessage(_("Magnet deleted: {0}", name));
+                                return;
+                            }
+                            _manager.stopTorrent(snark, true);
                             File f = new File(name);
                             f.delete();
                             _manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath()));
-                            List files = snark.meta.getFiles();
-                            String dataFile = snark.meta.getName();
+                            List files = meta.getFiles();
+                            String dataFile = snark.getBaseName();
                             f = new File(_manager.getDataDir(), dataFile);
                             if (files == null) { // single file torrent
                                 if (f.delete())
@@ -612,23 +634,23 @@ public class I2PSnarkServlet extends Default {
                 if (announceURL == null || announceURL.length() <= 0)
                     _manager.addMessage(_("Error creating torrent - you must select a tracker"));
                 else if (baseFile.exists()) {
+                    _lastAnnounceURL = announceURL;
+                    if (announceURL.equals("none"))
+                        announceURL = null;
                     try {
+                        // This may take a long time to check the storage, but since it already exists,
+                        // it shouldn't be THAT bad, so keep it in this thread.
                         Storage s = new Storage(_manager.util(), baseFile, announceURL, null);
                         s.create();
                         s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
                         MetaInfo info = s.getMetaInfo();
-                        File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
-                        if (torrentFile.exists())
-                            throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
-                        _manager.saveTorrentStatus(info, s.getBitField(), null); // so addTorrent won't recheck
-                        // DirMonitor could grab this first, maybe hold _snarks lock?
-                        FileOutputStream out = new FileOutputStream(torrentFile);
-                        out.write(info.getTorrentData());
-                        out.close();
+                        File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
+                        // FIXME is the storage going to stay around thanks to the info reference?
+                        // now add it, but don't automatically start it
+                        _manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), true);
                         _manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
-                        // now fire it up, but don't automatically seed it
-                        _manager.addTorrent(torrentFile.getCanonicalPath(), true);
-                        _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
+                        if (announceURL != null)
+                            _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
                     } catch (IOException ioe) {
                         _manager.addMessage(_("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe.getMessage());
                     }
@@ -643,8 +665,8 @@ public class I2PSnarkServlet extends Default {
             List snarks = getSortedSnarks(req);
             for (int i = 0; i < snarks.size(); i++) {
                 Snark snark = (Snark)snarks.get(i);
-                if (!snark.stopped)
-                    _manager.stopTorrent(snark.torrent, false);
+                if (!snark.isStopped())
+                    _manager.stopTorrent(snark, false);
             }
             if (_manager.util().connected()) {
                 // Give the stopped announces time to get out
@@ -657,7 +679,7 @@ public class I2PSnarkServlet extends Default {
             List snarks = getSortedSnarks(req);
             for (int i = 0; i < snarks.size(); i++) {
                 Snark snark = (Snark)snarks.get(i);
-                if (snark.stopped)
+                if (snark.isStopped())
                     snark.startTorrent();
             }
         } else {
@@ -725,7 +747,7 @@ public class I2PSnarkServlet extends Default {
     private static final int MAX_DISPLAYED_ERROR_LENGTH = 43;
     private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers,
                               boolean isDegraded, boolean noThinsp, boolean showDebug) throws IOException {
-        String filename = snark.torrent;
+        String filename = snark.getName();
         File f = new File(filename);
         filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
         int i = filename.lastIndexOf(".torrent");
@@ -733,31 +755,28 @@ public class I2PSnarkServlet extends Default {
             filename = filename.substring(0, i);
         String fullFilename = filename;
         if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH) {
-            fullFilename = new String(filename);
-            filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "&hellip;";
+            String start = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH);
+            if (start.indexOf(" ") < 0 && start.indexOf("-") < 0) {
+                // browser has nowhere to break it
+                fullFilename = filename;
+                filename = start + "&hellip;";
+            }
         }
-        long total = snark.meta.getTotalLength();
+        long total = snark.getTotalLength();
         // Early typecast, avoid possibly overflowing a temp integer
-        long remaining = (long) snark.storage.needed() * (long) snark.meta.getPieceLength(0); 
+        long remaining = (long) snark.getNeeded() * (long) snark.getPieceLength(0); 
         if (remaining > total)
             remaining = total;
-        long downBps = 0;
-        long upBps = 0;
-        if (snark.coordinator != null) {
-            downBps = snark.coordinator.getDownloadRate();
-            upBps = snark.coordinator.getUploadRate();
-        }
+        long downBps = snark.getDownloadRate();
+        long upBps = snark.getUploadRate();
         long remainingSeconds;
         if (downBps > 0)
             remainingSeconds = remaining / downBps;
         else
             remainingSeconds = -1;
-        boolean isRunning = !snark.stopped;
-        long uploaded = 0;
-        if (snark.coordinator != null) {
-            uploaded = snark.coordinator.getUploaded();
-            stats[0] += snark.coordinator.getDownloaded();
-        }
+        boolean isRunning = !snark.isStopped();
+        long uploaded = snark.getUploaded();
+        stats[0] += snark.getDownloaded();
         stats[1] += uploaded;
         if (isRunning) {
             stats[2] += downBps;
@@ -765,25 +784,22 @@ public class I2PSnarkServlet extends Default {
         }
         stats[5] += total;
         
-        boolean isValid = snark.meta != null;
-        boolean singleFile = snark.meta.getFiles() == null;
+        MetaInfo meta = snark.getMetaInfo();
+        // isValid means isNotMagnet
+        boolean isValid = meta != null;
+        boolean isMultiFile = isValid && meta.getFiles() != null;
         
-        String err = null;
-        int curPeers = 0;
-        int knownPeers = 0;
-        if (snark.coordinator != null) {
-            err = snark.coordinator.trackerProblems;
-            curPeers = snark.coordinator.getPeerCount();
-            stats[4] += curPeers;
-            knownPeers = Math.max(curPeers, snark.coordinator.trackerSeenPeers);
-        }
+        String err = snark.getTrackerProblems();
+        int curPeers = snark.getPeerCount();
+        stats[4] += curPeers;
+        int knownPeers = Math.max(curPeers, snark.getTrackerSeenPeers());
         
         String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
         String statusString;
         if (err != null) {
             if (isRunning && curPeers > 0 && !showPeers)
                 statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") +
-                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
+                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
                                curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
             else if (isRunning)
@@ -796,10 +812,10 @@ public class I2PSnarkServlet extends Default {
                 statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") +
                 "<br>" + err;
             }
-        } else if (remaining <= 0) {
+        } else if (remaining == 0) {  // < 0 means no meta size yet
             if (isRunning && curPeers > 0 && !showPeers)
                 statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") +
-                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
+                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
                                curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
             else if (isRunning)
@@ -811,7 +827,7 @@ public class I2PSnarkServlet extends Default {
         } else {
             if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
                 statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("OK") +
-                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
+                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
                                curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
             else if (isRunning && curPeers > 0 && downBps > 0)
@@ -820,7 +836,7 @@ public class I2PSnarkServlet extends Default {
                                ngettext("1 peer", "{0} peers", knownPeers);
             else if (isRunning && curPeers > 0 && !showPeers)
                 statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Stalled") +
-                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
+                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
                                curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
             else if (isRunning && curPeers > 0)
@@ -841,41 +857,24 @@ public class I2PSnarkServlet extends Default {
         out.write(statusString + "</td>\n\t");
 
         out.write("<td class=\"" + rowClass + "\">");
-        // temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash
-        String announce = snark.meta.getAnnounce();
-        if (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") ||
-            announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/") || announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/")) {
-            Map trackers = _manager.getTrackers();
-            for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
-                Map.Entry entry = (Map.Entry)iter.next();
-                String name = (String)entry.getKey();
-                String baseURL = (String)entry.getValue();
-                if (!(baseURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv
-                      (announce.startsWith("http://lnQ6yoBT") && baseURL.startsWith("http://tracker2.postman.i2p/")) ||
-                      (announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/") && baseURL.startsWith("http://tracker2.postman.i2p/"))))
-                    continue;
-                int e = baseURL.indexOf('=');
-                if (e < 0)
-                    continue;
-                baseURL = baseURL.substring(e + 1);
-                out.write("<a href=\"" + baseURL + "details.php?dllist=1&amp;filelist=1&amp;info_hash=");
-                out.write(TrackerClient.urlencode(snark.meta.getInfoHash()));
-                out.write("\" title=\"" + _("Details at {0} tracker", name) + "\" target=\"_blank\">");
-                out.write("<img alt=\"" + _("Info") + "\" border=\"0\" src=\"" + _imgPath + "details.png\">");
-                out.write("</a>");
-                break;
-            }
+        if (isValid) {
+            StringBuilder buf = new StringBuilder(128);
+            buf.append("<a href=\"").append(snark.getBaseName())
+               .append("/\" title=\"").append(_("Torrent details"))
+               .append("\"><img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"")
+               .append(_imgPath).append("details.png\"></a>");
+             out.write(buf.toString());
         }
 
         out.write("</td>\n<td class=\"" + rowClass + "\">");
         StringBuilder buf = null;
-        if (remaining == 0 || snark.meta.getFiles() != null) {
+        if (remaining == 0 || isMultiFile) {
             buf = new StringBuilder(128);
-            buf.append("<a href=\"").append(snark.storage.getBaseName());
-            if (snark.meta.getFiles() != null)
+            buf.append("<a href=\"").append(snark.getBaseName());
+            if (isMultiFile)
                 buf.append('/');
             buf.append("\" title=\"");
-            if (snark.meta.getFiles() != null)
+            if (isMultiFile)
                 buf.append(_("View files"));
             else
                 buf.append(_("Open file"));
@@ -883,21 +882,23 @@ public class I2PSnarkServlet extends Default {
             out.write(buf.toString());
         }
         String icon;
-        if (snark.meta.getFiles() != null)
+        if (isMultiFile)
             icon = "folder";
+        else if (isValid)
+            icon = toIcon(meta.getName());
         else
-            icon = toIcon(snark.meta.getName());
-        if (remaining == 0 || snark.meta.getFiles() != null) {
+            icon = "magnet";
+        if (remaining == 0 || isMultiFile) {
             out.write(toImg(icon, _("Open")));
             out.write("</a>");
         } else {
             out.write(toImg(icon));
         }
         out.write("</td><td class=\"snarkTorrentName " + rowClass + "\">");
-        if (remaining == 0 || snark.meta.getFiles() != null)
+        if (remaining == 0 || isMultiFile)
             out.write(buf.toString());
         out.write(filename);
-        if (remaining == 0 || snark.meta.getFiles() != null)
+        if (remaining == 0 || isMultiFile)
             out.write("</a>");
 
         out.write("<td align=\"right\" class=\"snarkTorrentETA " + rowClass + "\">");
@@ -907,24 +908,26 @@ public class I2PSnarkServlet extends Default {
         out.write("<td align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
         if (remaining > 0)
             out.write(formatSize(total-remaining) + thinsp(noThinsp) + formatSize(total));
-        else
+        else if (remaining == 0)
             out.write(formatSize(total)); // 3GB
+        //else
+        //    out.write("??");  // no meta size yet
         out.write("</td>\n\t");
         out.write("<td align=\"right\" class=\"snarkTorrentUploaded " + rowClass + "\">");
-        if(isRunning)
+        if(isRunning && isValid)
            out.write(formatSize(uploaded));
         out.write("</td>\n\t");
         out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">");
-        if(isRunning && remaining > 0)
+        if(isRunning && remaining != 0)
             out.write(formatSize(downBps) + "ps");
         out.write("</td>\n\t");
         out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">");
-        if(isRunning)
+        if(isRunning && isValid)
             out.write(formatSize(upBps) + "ps");
         out.write("</td>\n\t");
         out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">");
-        String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash());
-        String b64 = Base64.encode(snark.meta.getInfoHash());
+        String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.getInfoHash());
+        String b64 = Base64.encode(snark.getInfoHash());
         if (showPeers)
             parameters = parameters + "&p=1";
         if (isRunning) {
@@ -939,7 +942,6 @@ public class I2PSnarkServlet extends Default {
             if (isDegraded)
                 out.write("</a>");
         } else {
-            if (isValid) {
                 if (isDegraded)
                     out.write("<a href=\"/i2psnark/?action=Start_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\"");
                 else
@@ -950,24 +952,25 @@ public class I2PSnarkServlet extends Default {
                 out.write("\">");
                 if (isDegraded)
                     out.write("</a>");
-            }
 
-            if (isDegraded)
-                out.write("<a href=\"/i2psnark/?action=Remove_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\"");
-            else
-                out.write("<input type=\"image\" name=\"action_Remove_" + b64 + "\" value=\"foo\" title=\"");
-            out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
-            out.write("\" onclick=\"if (!confirm('");
-            // Can't figure out how to escape double quotes inside the onclick string.
-            // Single quotes in translate strings with parameters must be doubled.
-            // Then the remaining single quite must be escaped
-            out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
-            out.write("')) { return false; }\"");
-            out.write(" src=\"" + _imgPath + "remove.png\" alt=\"");
-            out.write(_("Remove"));
-            out.write("\">");
-            if (isDegraded)
-                out.write("</a>");
+            if (isValid) {
+                if (isDegraded)
+                    out.write("<a href=\"/i2psnark/?action=Remove_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\"");
+                else
+                    out.write("<input type=\"image\" name=\"action\" value=\"Remove_" + b64 + "\" title=\"");
+                out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
+                out.write("\" onclick=\"if (!confirm('");
+                // Can't figure out how to escape double quotes inside the onclick string.
+                // Single quotes in translate strings with parameters must be doubled.
+                // Then the remaining single quite must be escaped
+                out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
+                out.write("')) { return false; }\"");
+                out.write(" src=\"" + _imgPath + "remove.png\" alt=\"");
+                out.write(_("Remove"));
+                out.write("\">");
+                if (isDegraded)
+                    out.write("</a>");
+            }
 
             if (isDegraded)
                 out.write("<a href=\"/i2psnark/?action=Delete_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\"");
@@ -989,7 +992,7 @@ public class I2PSnarkServlet extends Default {
         out.write("</td>\n</tr>\n");
 
         if(showPeers && isRunning && curPeers > 0) {
-            List<Peer> peers = snark.coordinator.peerList();
+            List<Peer> peers = snark.getPeerList();
             if (!showDebug)
                 Collections.sort(peers, new PeerComparator());
             for (Peer peer : peers) {
@@ -1022,14 +1025,21 @@ public class I2PSnarkServlet extends Default {
                 out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
                 out.write("</td>\n\t");
                 out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
-                float pct = (float) (100.0 * (float) peer.completed() / snark.meta.getPieces());
-                if (pct == 100.0)
-                    out.write(_("Seed"));
-                else {
-                    String ps = String.valueOf(pct);
-                    if (ps.length() > 5)
-                        ps = ps.substring(0, 5);
-                    out.write(ps + "%");
+                float pct;
+                if (isValid) {
+                    pct = (float) (100.0 * (float) peer.completed() / meta.getPieces());
+                    if (pct == 100.0)
+                        out.write(_("Seed"));
+                    else {
+                        String ps = String.valueOf(pct);
+                        if (ps.length() > 5)
+                            ps = ps.substring(0, 5);
+                        out.write(ps + "%");
+                    }
+                } else {
+                    pct = (float) 101.0;
+                    // until we get the metainfo we don't know how many pieces there are
+                    //out.write("??");
                 }
                 out.write("</td>\n\t");
                 out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
@@ -1048,10 +1058,16 @@ public class I2PSnarkServlet extends Default {
                         out.write("\">");
                         out.write(formatSize(peer.getDownloadRate()) + "ps</a></span>");
                     }
+                } else if (!isValid) {
+                    //if (peer supports metadata extension) {
+                        out.write("<span class=\"unchoked\">");
+                        out.write(formatSize(peer.getDownloadRate()) + "ps</span>");
+                    //} else {
+                    //}
                 }
                 out.write("</td>\n\t");
                 out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
-                if (pct != 100.0) {
+                if (isValid && pct < 100.0) {
                     if (peer.isInterested() && !peer.isChoking()) {
                         out.write("<span class=\"unchoked\">");
                         out.write(formatSize(peer.getUploadRate()) + "ps</span>");
@@ -1094,8 +1110,40 @@ public class I2PSnarkServlet extends Default {
         }
     }
 
+    /**
+     *  @return string or null
+     *  @since 0.8.4
+     */
+    private String getTrackerLink(String announce, byte[] infohash) {
+        // temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash
+        if (announce != null && (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") ||
+              announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/") ||
+              announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/"))) {
+            Map<String, String> trackers = _manager.getTrackers();
+            for (Map.Entry<String, String> entry : trackers.entrySet()) {
+                String baseURL = entry.getValue();
+                if (!(baseURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv
+                      (announce.startsWith("http://lnQ6yoBT") && baseURL.startsWith("http://tracker2.postman.i2p/")) ||
+                      (announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/") && baseURL.startsWith("http://tracker2.postman.i2p/"))))
+                    continue;
+                int e = baseURL.indexOf('=');
+                if (e < 0)
+                    continue;
+                baseURL = baseURL.substring(e + 1);
+                String name = entry.getKey();
+                StringBuilder buf = new StringBuilder(128);
+                buf.append("<a href=\"").append(baseURL).append("details.php?dllist=1&amp;filelist=1&amp;info_hash=")
+                   .append(TrackerClient.urlencode(infohash))
+                   .append("\" title=\"").append(_("Details at {0} tracker", name)).append("\" target=\"_blank\">" +
+                          "<img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"")
+                   .append(_imgPath).append("details.png\"></a>");
+                return buf.toString();
+            }
+        }
+        return null;
+    }
+
     private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
-        String uri = req.getRequestURI();
         String newURL = req.getParameter("newURL");
         if ( (newURL == null) || (newURL.trim().length() <= 0) )
             newURL = "";
@@ -1136,7 +1184,6 @@ public class I2PSnarkServlet extends Default {
     }
     
     private void writeSeedForm(PrintWriter out, HttpServletRequest req) throws IOException {
-        String uri = req.getRequestURI();
         String baseFile = req.getParameter("baseFile");
         if (baseFile == null || baseFile.trim().length() <= 0)
             baseFile = "";
@@ -1167,6 +1214,10 @@ public class I2PSnarkServlet extends Default {
         out.write(":<td><select name=\"announceURL\"><option value=\"\">");
         out.write(_("Select a tracker"));
         out.write("</option>\n");
+        // todo remember this one with _lastAnnounceURL also
+        out.write("<option value=\"none\">");
+        out.write(_("Open trackers and DHT only"));
+        out.write("</option>\n");
         Map trackers = _manager.getTrackers();
         for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
             Map.Entry entry = (Map.Entry)iter.next();
@@ -1175,6 +1226,8 @@ public class I2PSnarkServlet extends Default {
             int e = announceURL.indexOf('=');
             if (e > 0)
                 announceURL = announceURL.substring(0, e);
+            if (announceURL.equals(_lastAnnounceURL))
+                announceURL += "\" selected=\"selected";
             out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
         }
         out.write("</select>\n");
@@ -1190,7 +1243,6 @@ public class I2PSnarkServlet extends Default {
     }
     
     private void writeConfigForm(PrintWriter out, HttpServletRequest req) throws IOException {
-        String uri = req.getRequestURI();
         String dataDir = _manager.getDataDir().getAbsolutePath();
         boolean autoStart = _manager.shouldAutoStart();
         boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers();
@@ -1308,15 +1360,17 @@ public class I2PSnarkServlet extends Default {
         out.write("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
         out.write(renderOptions(0, 4, options.remove("outbound.length"), "outbound.length", HOP));
 
-        out.write("<tr><td>");
-        out.write(_("I2CP host"));
-        out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\"" 
-                  + _manager.util().getI2CPHost() + "\" size=\"15\" > ");
+        if (!_context.isRouterContext()) {
+            out.write("<tr><td>");
+            out.write(_("I2CP host"));
+            out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\"" 
+                      + _manager.util().getI2CPHost() + "\" size=\"15\" > ");
 
-        out.write("<tr><td>");
-        out.write(_("I2CP port"));
-        out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" +
-                  + _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n");
+            out.write("<tr><td>");
+            out.write(_("I2CP port"));
+            out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" +
+                      + _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n");
+        }
 
         StringBuilder opts = new StringBuilder(64);
         for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) {
@@ -1344,6 +1398,49 @@ public class I2PSnarkServlet extends Default {
         out.write("</a></span></span></div>\n");
     }
 
+    /**
+     *  @param url in base32 or hex, xt must be first magnet param
+     *  @since 0.8.4
+     */
+    private void addMagnet(String url) {
+        String ihash;
+        String name;
+        if (url.startsWith(MAGNET)) {
+            ihash = url.substring(MAGNET.length()).trim();
+            int amp = ihash.indexOf('&');
+            if (amp >= 0)
+                ihash = url.substring(0, amp);
+            name = "Magnet " + ihash;
+        } else if (url.startsWith(MAGGOT)) {
+            ihash = url.substring(MAGGOT.length()).trim();
+            int col = ihash.indexOf(':');
+            if (col >= 0)
+                ihash = url.substring(0, col);
+            name = "Maggot " + ihash;
+        } else {
+            return;
+        }
+        byte[] ih = null;
+        if (ihash.length() == 32) {
+            ih = Base32.decode(ihash);
+        } else if (ihash.length() == 40) {
+            //  Like DataHelper.fromHexString() but ensures no loss of leading zero bytes
+            ih = new byte[20];
+            try {
+                for (int i = 0; i < 20; i++) {
+                    ih[i] = (byte) (Integer.parseInt(ihash.substring(i*2, (i*2) + 2), 16) & 0xff);
+                }
+            } catch (NumberFormatException nfe) {
+                ih = null;
+            }
+        }
+        if (ih == null || ih.length != 20) {
+            _manager.addMessage(_("Invalid info hash in magnet URL {0}", url));
+            return;
+        }
+        _manager.addMagnet(name, ih, true);
+    }
+
     /** copied from ConfigTunnelsHelper */
     private static final String HOP = "hop";
     private static final String TUNNEL = "tunnel";
@@ -1384,6 +1481,11 @@ public class I2PSnarkServlet extends Default {
         return _manager.util().getString(s, o);
     }
 
+    /** translate */
+    private String _(String s, Object o, Object o2) {
+        return _manager.util().getString(s, o, o2);
+    }
+
     /** translate (ngettext) @since 0.7.14 */
     private String ngettext(String s, String p, int n) {
         return _manager.util().getString(n, s, p);
@@ -1459,13 +1561,11 @@ public class I2PSnarkServlet extends Default {
     private String getListHTML(Resource r, String base, boolean parent, Map postParams)
         throws IOException
     {
-        if (!r.isDirectory())
-            return null;
-        
-        String[] ls = r.list();
-        if (ls==null)
-            return null;
-        Arrays.sort(ls, Collator.getInstance());
+        String[] ls = null;
+        if (r.isDirectory()) {
+            ls = r.list();
+            Arrays.sort(ls, Collator.getInstance());
+        }  // if r is not a directory, we are only showing torrent info section
         
         StringBuilder buf=new StringBuilder(4096);
         buf.append(DOCTYPE + "<HTML><HEAD><TITLE>");
@@ -1487,6 +1587,7 @@ public class I2PSnarkServlet extends Default {
 
         if (title.endsWith("/"))
             title = title.substring(0, title.length() - 1);
+        String directory = title;
         title = _("Torrent") + ": " + title;
         buf.append(title);
         buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" +
@@ -1495,13 +1596,70 @@ public class I2PSnarkServlet extends Default {
         
         if (parent)  // always true
             buf.append("<div class=\"page\"><div class=\"mainsection\">");
-        boolean showPriority = snark != null && !snark.storage.complete();
+        boolean showPriority = ls != null && snark != null && snark.getStorage() != null && !snark.getStorage().complete();
         if (showPriority)
             buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
-        buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" >" +
-            "<thead><tr><th>")
+        buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" ><thead>");
+        if (snark != null) {
+            // first row - torrent info
+            // FIXME center
+            buf.append("<tr><th colspan=\"" + (showPriority ? '4' : '3') + "\"><div>")
+                .append(_("Torrent")).append(": ").append(snark.getBaseName());
+            int pieces = snark.getPieces();
+            double completion = (pieces - snark.getNeeded()) / (double) pieces;
+            if (completion < 1.0)
+                buf.append("<br>").append(_("Completion")).append(": ").append((new DecimalFormat("0.00%")).format(completion));
+            else
+                buf.append("<br>").append(_("Complete"));
+            // else unknown
+            buf.append("<br>").append(_("Size")).append(": ").append(formatSize(snark.getTotalLength()));
+            MetaInfo meta = snark.getMetaInfo();
+            if (meta != null) {
+                List files = meta.getFiles();
+                int fileCount = files != null ? files.size() : 1;
+                buf.append("<br>").append(_("Files")).append(": ").append(fileCount);
+            }
+            buf.append("<br>").append(_("Pieces")).append(": ").append(pieces);
+            buf.append("<br>").append(_("Piece size")).append(": ").append(formatSize(snark.getPieceLength(0)));
+
+            if (meta != null) {
+                String announce = meta.getAnnounce();
+                if (announce != null) {
+                    buf.append("<br>");
+                    String trackerLink = getTrackerLink(announce, snark.getInfoHash());
+                    if (trackerLink != null)
+                        buf.append(trackerLink).append(' ');
+                    buf.append(_("Tracker")).append(": ");
+                    if (announce.startsWith("http://"))
+                        announce = announce.substring(7);
+                    int slsh = announce.indexOf('/');
+                    if (slsh > 0)
+                        announce = announce.substring(0, slsh);
+                    if (announce.length() > 67)
+                        announce = announce.substring(0, 40) + "&hellip;" + announce.substring(announce.length() - 8);
+                    buf.append(announce);
+                }
+            }
+
+            String hex = I2PSnarkUtil.toHex(snark.getInfoHash());
+            buf.append("<br>").append(toImg("magnet", _("Magnet link"))).append(" <a href=\"")
+               .append(MAGNET).append(hex).append("\">")
+               .append(MAGNET).append(hex).append("</a>");
+            // We don't have the hash of the torrent file
+            //buf.append("<br>").append(_("Maggot link")).append(": <a href=\"").append(MAGGOT).append(hex).append(':').append(hex).append("\">")
+            //   .append(MAGGOT).append(hex).append(':').append(hex).append("</a>");
+            buf.append("</div></th></tr>");
+        }
+        if (ls == null) {
+            // We are only showing the torrent info section
+            buf.append("</thead></table></div></div></BODY></HTML>");
+            return buf.toString();
+        }
+
+        // second row - dir info
+        buf.append("<tr><th>")
             .append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" >&nbsp;")
-            .append(title).append("</th><th align=\"right\">")
+            .append(_("Directory")).append(": ").append(directory).append("</th><th align=\"right\">")
             .append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "size.png\" >&nbsp;")
             .append(_("Size"));
         buf.append("</th><th class=\"headerstatus\">")
@@ -1542,15 +1700,16 @@ public class I2PSnarkServlet extends Default {
                 complete = true;
                 status = toImg("tick") + ' ' + _("Directory");
             } else {
-                if (snark == null) {
+                if (snark == null || snark.getStorage() == null) {
                     // Assume complete, perhaps he removed a completed torrent but kept a bookmark
                     complete = true;
                     status = toImg("cancel") + ' ' + _("Torrent not found?");
                 } else {
+                    Storage storage = snark.getStorage();
                     try {
                         File f = item.getFile();
                         if (f != null) {
-                            long remaining = snark.storage.remaining(f.getCanonicalPath());
+                            long remaining = storage.remaining(f.getCanonicalPath());
                             if (remaining < 0) {
                                 complete = true;
                                 status = toImg("cancel") + ' ' + _("File not found in torrent?");
@@ -1558,7 +1717,7 @@ public class I2PSnarkServlet extends Default {
                                 complete = true;
                                 status = toImg("tick") + ' ' + _("Complete");
                             } else {
-                                int priority = snark.storage.getPriority(f.getCanonicalPath());
+                                int priority = storage.getPriority(f.getCanonicalPath());
                                 if (priority < 0)
                                     status = toImg("cancel");
                                 else if (priority == 0)
@@ -1614,7 +1773,7 @@ public class I2PSnarkServlet extends Default {
                 buf.append("<td class=\"priority\">");
                 File f = item.getFile();
                 if ((!complete) && (!item.isDirectory()) && f != null) {
-                    int pri = snark.storage.getPriority(f.getCanonicalPath());
+                    int pri = snark.getStorage().getPriority(f.getCanonicalPath());
                     buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
                     if (pri > 0)
                         buf.append("checked=\"true\"");
@@ -1716,6 +1875,9 @@ public class I2PSnarkServlet extends Default {
 
     /** @since 0.8.1 */
     private void savePriorities(Snark snark, Map postParams) {
+        Storage storage = snark.getStorage();
+        if (storage == null)
+            return;
         Set<Map.Entry> entries = postParams.entrySet();
         for (Map.Entry entry : entries) {
             String key = (String)entry.getKey();
@@ -1724,14 +1886,13 @@ public class I2PSnarkServlet extends Default {
                     String file = key.substring(4);
                     String val = ((String[])entry.getValue())[0];   // jetty arrays
                     int pri = Integer.parseInt(val);
-                    snark.storage.setPriority(file, pri);
+                    storage.setPriority(file, pri);
                     //System.err.println("Priority now " + pri + " for " + file);
                 } catch (Throwable t) { t.printStackTrace(); }
             }
         }
-        if (snark.coordinator != null)
-            snark.coordinator.updatePiecePriorities();
-        _manager.saveTorrentStatus(snark.storage.getMetaInfo(), snark.storage.getBitField(), snark.storage.getFilePriorities());
+         snark.updatePiecePriorities();
+        _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities());
     }
 
 
@@ -1753,15 +1914,17 @@ private static class FetchAndAdd implements Runnable {
                 FileInputStream in = null;
                 try {
                     in = new FileInputStream(file);
+                    // we do not retain this MetaInfo object, hopefully it will go away quickly
                     MetaInfo info = new MetaInfo(in);
+                    try { in.close(); } catch (IOException ioe) {}
+                    Snark snark = _manager.getTorrentByInfoHash(info.getInfoHash());
+                    if (snark != null) {
+                        _manager.addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName()));
+                        return;
+                    }
+
                     String name = info.getName();
-                    name = DataHelper.stripHTML(name);  // XSS
-                    name = name.replace('/', '_');
-                    name = name.replace('\\', '_');
-                    name = name.replace('&', '+');
-                    name = name.replace('\'', '_');
-                    name = name.replace('"', '_');
-                    name = name.replace('`', '_');
+                    name = Storage.filterName(name);
                     name = name + ".torrent";
                     File torrentFile = new File(_manager.getDataDir(), name);
 
@@ -1773,18 +1936,15 @@ private static class FetchAndAdd implements Runnable {
                         else
                             _manager.addMessage(_("Torrent already in the queue: {0}", name));
                     } else {
-                        boolean success = FileUtil.copy(file.getAbsolutePath(), canonical, false);
-                        if (success) {
-                            SecureFileOutputStream.setPerms(torrentFile);
-                            _manager.addTorrent(canonical);
-                        } else {
-                            _manager.addMessage(_("Failed to copy torrent file to {0}", canonical));
-                        }
+                        // This may take a LONG time to create the storage.
+                        _manager.copyAndAddTorrent(file, canonical);
                     }
                 } catch (IOException ioe) {
                     _manager.addMessage(_("Torrent at {0} was not valid", urlify(_url)) + ": " + ioe.getMessage());
+                } catch (OutOfMemoryError oom) {
+                    _manager.addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", urlify(_url)) + ": " + oom.getMessage());
                 } finally {
-                    try { in.close(); } catch (IOException ioe) {}
+                    try { if (in != null) in.close(); } catch (IOException ioe) {}
                 }
             } else {
                 _manager.addMessage(_("Torrent was not retrieved from {0}", urlify(_url)));
diff --git a/core/java/src/net/i2p/data/ByteArray.java b/core/java/src/net/i2p/data/ByteArray.java
index 7369a27d191c01e63c2d9ca9b4f10807b4f7f344..9f460fe4753f8eb41d4a7f399ee5c54bf166619d 100644
--- a/core/java/src/net/i2p/data/ByteArray.java
+++ b/core/java/src/net/i2p/data/ByteArray.java
@@ -24,6 +24,7 @@ public class ByteArray implements Serializable, Comparable {
     public ByteArray() {
     }
 
+    /** Sets valid */
     public ByteArray(byte[] data) {
         _data = data;
         _valid = (data != null ? data.length : 0);
@@ -38,6 +39,7 @@ public class ByteArray implements Serializable, Comparable {
         return _data;
     }
 
+    /** Warning, does not set valid */
     public void setData(byte[] data) {
         _data = data;
     }