Author: toad
Date: 2007-06-09 17:12:22 +0000 (Sat, 09 Jun 2007)
New Revision: 13498

Modified:
   trunk/freenet/src/freenet/clients/http/DarknetConnectionsToadlet.java
   trunk/freenet/src/freenet/clients/http/SimpleToadletServer.java
   trunk/freenet/src/freenet/clients/http/ToadletContainer.java
   trunk/freenet/src/freenet/clients/http/ToadletContextImpl.java
   trunk/freenet/src/freenet/io/comm/RetrievalException.java
   trunk/freenet/src/freenet/io/xfer/PartiallyReceivedBulk.java
   trunk/freenet/src/freenet/l10n/freenet.l10n.en.properties
   trunk/freenet/src/freenet/node/Node.java
   trunk/freenet/src/freenet/node/PeerNode.java
   trunk/freenet/src/freenet/support/io/RandomAccessFileWrapper.java
   trunk/freenet/src/freenet/support/io/RandomAccessThing.java
Log:
Fix F2F file transfer to not be a DoS risk: Ask the user whether they want the 
file, let them either accept or reject it (new support for rejection).
Also fix the incoming related messages being persisted as N2NTMs - they 
shouldn't be. In future we may persist the transfers themselves, but not today.

Modified: trunk/freenet/src/freenet/clients/http/DarknetConnectionsToadlet.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/DarknetConnectionsToadlet.java       
2007-06-08 18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/clients/http/DarknetConnectionsToadlet.java       
2007-06-09 17:12:22 UTC (rev 13498)
@@ -919,6 +919,34 @@
                        headers.put("Location", "/friends/");
                        ctx.sendReplyHeaders(302, "Found", headers, null, 0);
                        return;
+               } else if (request.isPartSet("acceptTransfer")) {
+                       // FIXME this is ugly, should probably move both this 
code and the PeerNode code somewhere.
+                       PeerNode[] peerNodes = node.getDarknetConnections();
+                       for(int i = 0; i < peerNodes.length; i++) {
+                               if 
(request.isPartSet("node_"+peerNodes[i].hashCode())) {
+                                       long id = 
Long.parseLong(request.getPartAsString("id", 32)); // FIXME handle 
NumberFormatException
+                                       peerNodes[i].acceptTransfer(id);
+                                       break;
+                               }
+                       }
+                       MultiValueTable headers = new MultiValueTable();
+                       headers.put("Location", "/friends/");
+                       ctx.sendReplyHeaders(302, "Found", headers, null, 0);
+                       return;
+               } else if (request.isPartSet("rejectTransfer")) {
+                       // FIXME this is ugly, should probably move both this 
code and the PeerNode code somewhere.
+                       PeerNode[] peerNodes = node.getDarknetConnections();
+                       for(int i = 0; i < peerNodes.length; i++) {
+                               if 
(request.isPartSet("node_"+peerNodes[i].hashCode())) {
+                                       long id = 
Long.parseLong(request.getPartAsString("id", 32)); // FIXME handle 
NumberFormatException
+                                       peerNodes[i].rejectTransfer(id);
+                                       break;
+                               }
+                       }
+                       MultiValueTable headers = new MultiValueTable();
+                       headers.put("Location", "/friends/");
+                       ctx.sendReplyHeaders(302, "Found", headers, null, 0);
+                       return;
                } else {
                        this.handleGet(uri, new HTTPRequestImpl(uri), ctx);
                }

Modified: trunk/freenet/src/freenet/clients/http/SimpleToadletServer.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/SimpleToadletServer.java     
2007-06-08 18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/clients/http/SimpleToadletServer.java     
2007-06-09 17:12:22 UTC (rev 13498)
@@ -29,6 +29,7 @@
 import freenet.io.NetworkInterface;
 import freenet.l10n.L10n;
 import freenet.node.NodeClientCore;
+import freenet.support.HTMLNode;
 import freenet.support.Logger;
 import freenet.support.OOMHandler;
 import freenet.support.StringArray;
@@ -419,7 +420,7 @@
                }

                void start() {
-                       Thread t = new Thread(this, 
"SimpleToadletServer$SocketHandler");
+                       Thread t = new Thread(this, 
"SimpleToadletServer$SocketHandler@"+hashCode());
                        t.setDaemon(true);
                        t.start();
                }
@@ -482,4 +483,14 @@
                return L10n.getString("SimpleToadletServer."+key);
        }

+       public HTMLNode addFormChild(HTMLNode parentNode, String target, String 
name) {
+               HTMLNode formNode =
+                       parentNode.addChild("form", new String[] { "action", 
"method", "enctype", "id", "name", "accept-charset" }, 
+                                       new String[] { target, "post", 
"multipart/form-data", name, name, "utf-8"} );
+               formNode.addChild("input", new String[] { "type", "name", 
"value" }, 
+                               new String[] { "hidden", "formPassword", 
getFormPassword() });
+               
+               return formNode;
+       }
+
 }

Modified: trunk/freenet/src/freenet/clients/http/ToadletContainer.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/ToadletContainer.java        
2007-06-08 18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/clients/http/ToadletContainer.java        
2007-06-09 17:12:22 UTC (rev 13498)
@@ -6,6 +6,8 @@
 import java.net.InetAddress;
 import java.net.URI;

+import freenet.support.HTMLNode;
+
 /** Interface for toadlet containers. Toadlets should register here. */
 public interface ToadletContainer {

@@ -36,4 +38,6 @@

        /** Whether to tell spiders to go away */
        public boolean doRobots();
+
+       public HTMLNode addFormChild(HTMLNode parentNode, String target, String 
name);
 }

Modified: trunk/freenet/src/freenet/clients/http/ToadletContextImpl.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/ToadletContextImpl.java      
2007-06-08 18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/clients/http/ToadletContextImpl.java      
2007-06-09 17:12:22 UTC (rev 13498)
@@ -372,6 +372,8 @@
                } catch (ToadletContextClosedException e) {
                        Logger.error(ToadletContextImpl.class, 
"ToadletContextClosedException while handling connection!");
                        return;
+               } catch (Throwable t) {
+                       Logger.error(ToadletContextImpl.class, "Caught error: 
"+t+" handling socket", t);
                }
        }

@@ -425,13 +427,7 @@
        }

        public HTMLNode addFormChild(HTMLNode parentNode, String target, String 
name) {
-               HTMLNode formNode =
-                       parentNode.addChild("form", new String[] { "action", 
"method", "enctype", "id", "name", "accept-charset" }, 
-                                       new String[] { target, "post", 
"multipart/form-data", name, name, "utf-8"} );
-               formNode.addChild("input", new String[] { "type", "name", 
"value" }, 
-                               new String[] { "hidden", "formPassword", 
container.getFormPassword() });
-               
-               return formNode;
+               return container.addFormChild(parentNode, target, name);
        }

        public boolean isAllowedFullAccess() {

Modified: trunk/freenet/src/freenet/io/comm/RetrievalException.java
===================================================================
--- trunk/freenet/src/freenet/io/comm/RetrievalException.java   2007-06-08 
18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/io/comm/RetrievalException.java   2007-06-09 
17:12:22 UTC (rev 13498)
@@ -39,6 +39,7 @@
     public static final int ALREADY_CACHED = 6;
     public static final int SENDER_DISCONNECTED = 7;
     public static final int NO_DATAINSERT = 8;
+    public static final int CANCELLED_BY_RECEIVER = 9;

        int _reason;
        String _cause;

Modified: trunk/freenet/src/freenet/io/xfer/PartiallyReceivedBulk.java
===================================================================
--- trunk/freenet/src/freenet/io/xfer/PartiallyReceivedBulk.java        
2007-06-08 18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/io/xfer/PartiallyReceivedBulk.java        
2007-06-09 17:12:22 UTC (rev 13498)
@@ -129,7 +129,7 @@
                }
        }

-       void abort(int errCode, String why) {
+       public void abort(int errCode, String why) {
                BulkTransmitter[] notifyBTs;
                BulkReceiver notifyBR;
                synchronized(this) {
@@ -146,6 +146,7 @@
                }
                if(notifyBR != null)
                        notifyBR.onAborted();
+               raf.close();
        }

        public synchronized boolean isAborted() {

Modified: trunk/freenet/src/freenet/l10n/freenet.l10n.en.properties
===================================================================
--- trunk/freenet/src/freenet/l10n/freenet.l10n.en.properties   2007-06-08 
18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/l10n/freenet.l10n.en.properties   2007-06-09 
17:12:22 UTC (rev 13498)
@@ -325,6 +325,15 @@
 FetchException.shortError.7=Too many metadata levels
 FetchException.shortError.8=Too many archive restarts
 FetchException.shortError.9=Too much recursion
+FileOfferUserAlert.title=Direct file transfer
+FileOfferUserAlert.offeredFileHeader=The node ${name} has offered a file:
+FileOfferUserAlert.fileLabel=File:
+FileOfferUserAlert.senderLabel=Sender:
+FileOfferUserAlert.commentLabel=Comment:
+FileOfferUserAlert.mimeLabel=MIME Type:
+FileOfferUserAlert.sizeLabel=Size:
+FileOfferUserAlert.acceptTransferButton=Accept Transfer
+FileOfferUserAlert.rejectTransferButton=Reject Transfer
 GIFFilter.invalidHeader=The file does not contain a valid GIF header.
 GIFFilter.invalidHeaderTitle=Invalid header
 GIFFilter.notGif=The file you tried to fetch is not a GIF. It might be some 
other file format, and your browser may do something dangerous with it, 
therefore we have blocked it.

Modified: trunk/freenet/src/freenet/node/Node.java
===================================================================
--- trunk/freenet/src/freenet/node/Node.java    2007-06-08 18:42:22 UTC (rev 
13497)
+++ trunk/freenet/src/freenet/node/Node.java    2007-06-09 17:12:22 UTC (rev 
13498)
@@ -424,6 +424,8 @@
        public static final int N2N_TEXT_MESSAGE_TYPE_FILE_OFFER = 2;
        /** Identifier within fproxy messages for accepting an offer to 
transfer a file */
        public static final int N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_ACCEPTED = 3;
+       /** Identifier within fproxy messages for rejecting an offer to 
transfer a file */
+       public static final int N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_REJECTED = 4;
        public static final int EXTRA_PEER_DATA_TYPE_N2NTM = 1;
        public static final int EXTRA_PEER_DATA_TYPE_PEER_NOTE = 2;
        public static final int EXTRA_PEER_DATA_TYPE_QUEUED_TO_SEND_N2NTM = 3;
@@ -2681,6 +2683,8 @@
                        source.handleFproxyFileOffer(fs, fileNumber);
                } else if(type == 
Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_ACCEPTED) {
                        source.handleFproxyFileOfferAccepted(fs, fileNumber);
+               } else if(type == 
Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_REJECTED) {
+                       source.handleFproxyFileOfferRejected(fs, fileNumber);
                } else {
                        Logger.error(this, "Received unknown fproxy node to 
node message sub-type '"+type+"' from "+source.getPeer());
                }

Modified: trunk/freenet/src/freenet/node/PeerNode.java
===================================================================
--- trunk/freenet/src/freenet/node/PeerNode.java        2007-06-08 18:42:22 UTC 
(rev 13497)
+++ trunk/freenet/src/freenet/node/PeerNode.java        2007-06-09 17:12:22 UTC 
(rev 13498)
@@ -53,6 +53,7 @@
 import freenet.io.comm.PeerContext;
 import freenet.io.comm.PeerParseException;
 import freenet.io.comm.ReferenceSignatureVerificationException;
+import freenet.io.comm.RetrievalException;
 import freenet.io.xfer.BulkReceiver;
 import freenet.io.xfer.BulkTransmitter;
 import freenet.io.xfer.PacketThrottle;
@@ -60,14 +61,18 @@
 import freenet.keys.ClientSSK;
 import freenet.keys.FreenetURI;
 import freenet.keys.USK;
+import freenet.l10n.L10n;
 import freenet.node.useralerts.N2NTMUserAlert;
+import freenet.node.useralerts.UserAlert;
 import freenet.support.Base64;
 import freenet.support.Fields;
+import freenet.support.HTMLNode;
 import freenet.support.HexUtil;
 import freenet.support.IllegalBase64Exception;
 import freenet.support.LRUHashtable;
 import freenet.support.Logger;
 import freenet.support.SimpleFieldSet;
+import freenet.support.SizeUtil;
 import freenet.support.WouldBlockException;
 import freenet.support.io.FileUtil;
 import freenet.support.io.RandomAccessFileWrapper;
@@ -3208,6 +3213,8 @@
                private PartiallyReceivedBulk prb;
                private BulkTransmitter transmitter;
                private BulkReceiver receiver;
+               /** True if the offer has either been accepted or rejected */
+               private boolean acceptedOrRejected;

                FileOffer(long uid, RandomAccessThing data, String filename, 
String mimeType, String comment) throws IOException {
                        this.uid = uid;
@@ -3237,6 +3244,7 @@
                }

                public void accept() {
+                       acceptedOrRejected = true;
                        File dest = new File(node.clientCore.downloadDir, 
"direct-"+FileUtil.sanitize(getName())+"-"+filename);
                        try {
                                data = new RandomAccessFileWrapper(dest, "rw");
@@ -3266,6 +3274,7 @@
                        }, "Receiver for bulk transfer "+uid+":"+filename);
                        t.setDaemon(true);
                        t.start();
+                       if(logMINOR) Logger.minor(this, "Receiving on "+t);
                        sendFileOfferAccepted(uid);
                }

@@ -3294,6 +3303,17 @@
                        t.setDaemon(true);
                        t.start();
                }
+
+               public void reject() {
+                       acceptedOrRejected = true;
+                       sendFileOfferRejected(uid);
+               }
+
+               public void onRejected() {
+                       transmitter.cancel();
+                       // FIXME prb's can't be shared, right? Well they aren't 
here...
+                       prb.abort(RetrievalException.CANCELLED_BY_RECEIVER, 
"Cancelled by receiver");
+               }
        }

        public int sendTextMessage(String message) {
@@ -3359,6 +3379,39 @@
                }
        }

+       public int sendFileOfferRejected(long uid) {
+               storeOffers();
+               long now = System.currentTimeMillis();
+               SimpleFieldSet fs = new SimpleFieldSet(true);
+               fs.put("n2nType", Node.N2N_MESSAGE_TYPE_FPROXY);
+               fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_REJECTED);
+               try {
+                       fs.putSingle("source_nodename", 
Base64.encode(node.getMyName().getBytes("UTF-8")));
+                       fs.putSingle("target_nodename", 
Base64.encode(getName().getBytes("UTF-8")));
+                       fs.put("composedTime", now);
+                       fs.put("sentTime", now);
+                       fs.put("uid", uid);
+                       if(logMINOR)
+                               Logger.minor(this, "Sending node to node 
message (file offer accepted):\n"+fs);
+                       Message n2ntm;
+                       n2ntm = DMT.createNodeToNodeMessage(
+                                       Node.N2N_MESSAGE_TYPE_FPROXY, fs
+                                                       
.toString().getBytes("UTF-8"));
+                       try {
+                               sendAsync(n2ntm, null, 0, null);
+                       } catch (NotConnectedException e) {
+                               fs.removeValue("sentTime");
+                               queueN2NTM(fs);
+                               setPeerNodeStatus(System.currentTimeMillis());
+                               return getPeerNodeStatus();
+                       }
+                       this.setPeerNodeStatus(System.currentTimeMillis());
+                       return getPeerNodeStatus();
+               } catch (UnsupportedEncodingException e) {
+                       throw new Error("Impossible: "+e, e);
+               }
+       }
+
        public int sendFileOffer(File filename, String message) throws 
IOException {
                String fnam = filename.getName();
                String mime = DefaultMIMETypes.guessMIMEType(fnam, false);
@@ -3423,7 +3476,7 @@
        }

        public void handleFproxyFileOffer(SimpleFieldSet fs, int fileNumber) {
-               FileOffer offer;
+               final FileOffer offer;
                try {
                        offer = new FileOffer(fs, false);
                } catch (FSParseException e) {
@@ -3435,11 +3488,149 @@
                        if(hisFileOffersByUID.containsKey(u)) return; // Ignore 
re-advertisement
                        hisFileOffersByUID.put(u, offer);
                }
-               // Auto-accept : FIXME
-               offer.accept();
+               
+               // Don't persist for now - FIXME
+               this.deleteExtraPeerDataFile(fileNumber);
+               
+               UserAlert alert = new UserAlert() {
+                       public String dismissButtonText() {
+                               return null; // Cannot hide, but can reject
+                       }
+                       public HTMLNode getHTMLText() {
+                               HTMLNode div = new HTMLNode("div");
+                               
+                               // FIXME localise!!!
+                               
+                               div.addChild("p", l10n("offeredFileHeader", 
"name", getName()));
+                               
+                               // Descriptive table
+                               
+                               HTMLNode table = div.addChild("table", 
"border", "0");
+                               HTMLNode row = table.addChild("tr");
+                               row.addChild("td").addChild("#", 
l10n("fileLabel"));
+                               row.addChild("td").addChild("#", 
offer.filename);
+                               row = table.addChild("tr");
+                               row.addChild("td").addChild("#", 
l10n("sizeLabel"));
+                               row.addChild("td").addChild("#", 
SizeUtil.formatSize(offer.size));
+                               row = table.addChild("tr");
+                               row.addChild("td").addChild("#", 
l10n("mimeLabel"));
+                               row.addChild("td").addChild("#", 
offer.mimeType);
+                               row = table.addChild("tr");
+                               row.addChild("td").addChild("#", 
l10n("senderLabel"));
+                               row.addChild("td").addChild("#", getName());
+                               row = table.addChild("tr");
+                               if(offer.comment != null && 
offer.comment.length() > 0) {
+                                       row.addChild("td").addChild("#", 
l10n("commentLabel"));
+                                       row.addChild("td").addChild("#", 
offer.comment);
+                               }
+                               
+                               // Accept/reject form
+                               
+                               // Hopefully we will have a container when this 
function is called!
+                               HTMLNode form = 
node.clientCore.getToadletContainer().addFormChild(div, "/friends/", 
"f2fFileOfferAcceptForm");
+                               
+                               // FIXME node_ is inefficient
+                               form.addChild("input", new String[] { "type", 
"name" },
+                                               new String[] { "hidden", 
"node_"+PeerNode.this.hashCode() });
+
+                               form.addChild("input", new String[] { "type", 
"name", "value" },
+                                               new String[] { "hidden", "id", 
Long.toString(offer.uid) });
+                               
+                               form.addChild("input", new String[] { "type", 
"name", "value" }, 
+                                               new String[] { "submit", 
"acceptTransfer", l10n("acceptTransferButton") });
+
+                               form.addChild("input", new String[] { "type", 
"name", "value" }, 
+                                               new String[] { "submit", 
"rejectTransfer", l10n("rejectTransferButton") });
+                               
+                               return div;
+                       }
+                       public short getPriorityClass() {
+                               return UserAlert.MINOR;
+                       }
+                       public String getText() {
+                               StringBuffer sb = new StringBuffer();
+                               sb.append(l10n("offeredFileHeader", "name", 
getName()));
+                               sb.append('\n');
+                               sb.append(l10n("fileLabel"));
+                               sb.append(' ');
+                               sb.append(offer.filename);
+                               sb.append('\n');
+                               sb.append(l10n("sizeLabel"));
+                               sb.append(' ');
+                               sb.append(SizeUtil.formatSize(offer.size));
+                               sb.append('\n');
+                               sb.append(l10n("mimeLabel"));
+                               sb.append(' ');
+                               sb.append(offer.mimeType);
+                               sb.append('\n');
+                               sb.append(l10n("senderLabel"));
+                               sb.append(' ');
+                               sb.append(getName());
+                               sb.append('\n');
+                               if(offer.comment != null && 
offer.comment.length() > 0) {
+                                       sb.append(l10n("commentLabel"));
+                                       sb.append(' ');
+                                       sb.append(offer.comment);
+                               }
+                               return sb.toString();
+                       }
+                       public String getTitle() {
+                               return l10n("title");
+                       }
+
+                       private String l10n(String key) {
+                               return 
L10n.getString("FileOfferUserAlert."+key);
+                       }
+                       private String l10n(String key, String pattern, String 
value) {
+                               return 
L10n.getString("FileOfferUserAlert."+key, pattern, value);
+                       }
+                       public boolean isValid() {
+                               if(offer.acceptedOrRejected) {
+                                       node.clientCore.alerts.unregister(this);
+                                       return false;
+                               }
+                               return true;
+                       }
+                       public void isValid(boolean validity) {
+                               // Ignore
+                       }
+                       public void onDismiss() {
+                               // Ignore
+                       }
+                       public boolean shouldUnregisterOnDismiss() {
+                               return false;
+                       }
+
+                       public boolean userCanDismiss() {
+                               return false; // should accept or reject
+                       }
+               };
+               
+               node.clientCore.alerts.register(alert);
        }

+       public void acceptTransfer(long id) {
+               if(logMINOR)
+                       Logger.minor(this, "Accepting transfer "+id+" on 
"+this);
+               FileOffer fo;
+               synchronized(this) {
+                       fo = (FileOffer) hisFileOffersByUID.get(new Long(id));
+               }
+               fo.accept();
+       }
+       
+       public void rejectTransfer(long id) {
+               FileOffer fo;
+               synchronized(this) {
+                       fo = (FileOffer) hisFileOffersByUID.remove(new 
Long(id));
+               }
+               fo.reject();
+       }
+       
        public void handleFproxyFileOfferAccepted(SimpleFieldSet fs, int 
fileNumber) {
+               // Don't persist for now - FIXME
+               this.deleteExtraPeerDataFile(fileNumber);
+               
                long uid;
                try {
                        uid = fs.getLong("uid");
@@ -3469,4 +3660,23 @@
                        Logger.error(this, "Cannot send because node 
disconnected: "+e+" for "+uid+":"+fo.filename, e);
                }
        }
+
+       public void handleFproxyFileOfferRejected(SimpleFieldSet fs, int 
fileNumber) {
+               // Don't persist for now - FIXME
+               this.deleteExtraPeerDataFile(fileNumber);
+               
+               long uid;
+               try {
+                       uid = fs.getLong("uid");
+               } catch (FSParseException e) {
+                       Logger.error(this, "Could not parse offer rejected: 
"+e+" on "+this+" :\n"+fs, e);
+                       return;
+               }
+               
+               FileOffer fo;
+               synchronized(this) {
+                       fo = (FileOffer) myFileOffersByUID.remove(new 
Long(uid));
+               }
+               fo.onRejected();
+       }
 }

Modified: trunk/freenet/src/freenet/support/io/RandomAccessFileWrapper.java
===================================================================
--- trunk/freenet/src/freenet/support/io/RandomAccessFileWrapper.java   
2007-06-08 18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/support/io/RandomAccessFileWrapper.java   
2007-06-09 17:12:22 UTC (rev 13498)
@@ -5,6 +5,8 @@
 import java.io.IOException;
 import java.io.RandomAccessFile;

+import freenet.support.Logger;
+
 public class RandomAccessFileWrapper implements RandomAccessThing {

        // FIXME maybe we should avoid opening these until we are ready to use 
them
@@ -38,4 +40,12 @@
                return raf.length();
        }

+       public void close() {
+               try {
+                       raf.close();
+               } catch (IOException e) {
+                       Logger.error(this, "Could not close "+raf+" : "+e+" for 
"+this, e);
+               }
+       }
+
 }

Modified: trunk/freenet/src/freenet/support/io/RandomAccessThing.java
===================================================================
--- trunk/freenet/src/freenet/support/io/RandomAccessThing.java 2007-06-08 
18:42:22 UTC (rev 13497)
+++ trunk/freenet/src/freenet/support/io/RandomAccessThing.java 2007-06-09 
17:12:22 UTC (rev 13498)
@@ -16,5 +16,7 @@
        public void pread(long fileOffset, byte[] buf, int bufOffset, int 
length) throws IOException;

        public void pwrite(long fileOffset, byte[] buf, int bufOffset, int 
length) throws IOException;
+
+       public void close();

 }


Reply via email to