From e0509db45d2d7cd665891162d175357141a3910e Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 12 Jul 2011 12:55:46 +0100 Subject: [PATCH] Protocol refactoring. Each bundle now consists of a signed header and zero or more signed batches. There is no overall signature on the bundle, since the bundle's contents may need to be processed before the entire bundle has been read. The protocol does not prevent an adversary from removing batches from a bundle, reordering batches, moving them from one bundle to another, etc. However, since each batch is signed and acknowledged independently, no such guarantees are required. Bundle IDs will go away when the retransmission mechanism is changed. --- .../sf/briar/api/db/DatabaseComponent.java | 6 +- api/net/sf/briar/api/protocol/Batch.java | 9 +- .../sf/briar/api/protocol/BatchBuilder.java | 7 +- api/net/sf/briar/api/protocol/Bundle.java | 33 +-- .../sf/briar/api/protocol/BundleBuilder.java | 18 +- api/net/sf/briar/api/protocol/Header.java | 28 ++ .../sf/briar/api/protocol/HeaderBuilder.java | 24 ++ .../sf/briar/api/protocol/MessageParser.java | 10 + api/net/sf/briar/api/protocol/UniqueId.java | 4 +- .../sf/briar/db/DatabaseComponentImpl.java | 4 + .../db/ReadWriteLockDatabaseComponent.java | 93 ++++--- .../db/SynchronizedDatabaseComponent.java | 90 ++++--- .../net/sf/briar/protocol/BatchImpl.java | 24 +- .../net/sf/briar/protocol/BundleReader.java | 94 +++++++ .../net/sf/briar/protocol/BundleWriter.java | 66 +++++ .../net/sf/briar/protocol/FileBundle.java | 30 +++ .../sf/briar/protocol/FileBundleBuilder.java | 41 +++ .../net/sf/briar/protocol/HeaderImpl.java | 55 ++++ .../net/sf/briar/protocol/MessageImpl.java | 3 +- .../net/sf/briar/protocol/ProtocolModule.java | 7 - test/build.xml | 2 + .../briar/db/DatabaseComponentImplTest.java | 30 ++- .../sf/briar/db/DatabaseComponentTest.java | 212 ++++++++++----- .../ReadWriteLockDatabaseComponentTest.java | 7 +- .../db/SynchronizedDatabaseComponentTest.java | 7 +- .../sf/briar/protocol/BundleReaderTest.java | 251 ++++++++++++++++++ .../sf/briar/protocol/BundleWriterTest.java | 242 +++++++++++++++++ test/net/sf/briar/protocol/TestRaw.java | 29 ++ 28 files changed, 1198 insertions(+), 228 deletions(-) create mode 100644 api/net/sf/briar/api/protocol/Header.java create mode 100644 api/net/sf/briar/api/protocol/HeaderBuilder.java create mode 100644 api/net/sf/briar/api/protocol/MessageParser.java create mode 100644 components/net/sf/briar/protocol/BundleReader.java create mode 100644 components/net/sf/briar/protocol/BundleWriter.java create mode 100644 components/net/sf/briar/protocol/FileBundle.java create mode 100644 components/net/sf/briar/protocol/FileBundleBuilder.java create mode 100644 components/net/sf/briar/protocol/HeaderImpl.java create mode 100644 test/net/sf/briar/protocol/BundleReaderTest.java create mode 100644 test/net/sf/briar/protocol/BundleWriterTest.java create mode 100644 test/net/sf/briar/protocol/TestRaw.java diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java index dd231be4d..f662e85b6 100644 --- a/api/net/sf/briar/api/db/DatabaseComponent.java +++ b/api/net/sf/briar/api/db/DatabaseComponent.java @@ -1,5 +1,7 @@ package net.sf.briar.api.db; +import java.io.IOException; +import java.security.SignatureException; import java.util.Map; import java.util.Set; @@ -49,7 +51,7 @@ public interface DatabaseComponent { * Generates a bundle of acknowledgements, subscriptions, and batches of * messages for the given contact. */ - Bundle generateBundle(ContactId c, BundleBuilder bundleBuilder) throws DbException; + Bundle generateBundle(ContactId c, BundleBuilder bundleBuilder) throws DbException, IOException, SignatureException; /** Returns the IDs of all contacts. */ Set getContacts() throws DbException; @@ -71,7 +73,7 @@ public interface DatabaseComponent { * messages received from the given contact. Some or all of the messages * in the bundle may be stored. */ - void receiveBundle(ContactId c, Bundle b) throws DbException; + void receiveBundle(ContactId c, Bundle b) throws DbException, IOException, SignatureException; /** Removes a contact (and all associated state) from the database. */ void removeContact(ContactId c) throws DbException; diff --git a/api/net/sf/briar/api/protocol/Batch.java b/api/net/sf/briar/api/protocol/Batch.java index eaa0ec261..05dc321a2 100644 --- a/api/net/sf/briar/api/protocol/Batch.java +++ b/api/net/sf/briar/api/protocol/Batch.java @@ -1,16 +1,19 @@ package net.sf.briar.api.protocol; -/** A batch of messages up to CAPACITY bytes in total size. */ +/** A batch of messages up to MAX_SIZE bytes in total size. */ public interface Batch { - public static final long CAPACITY = 1024L * 1024L; + public static final int MAX_SIZE = 1024 * 1024; /** Returns the batch's unique identifier. */ BatchId getId(); - /** Returns the size of the batch in bytes. */ + /** Returns the size of the serialised batch in bytes. */ long getSize(); /** Returns the messages contained in the batch. */ Iterable getMessages(); + + /** Returns the sender's signature over the contents of the batch. */ + byte[] getSignature(); } \ No newline at end of file diff --git a/api/net/sf/briar/api/protocol/BatchBuilder.java b/api/net/sf/briar/api/protocol/BatchBuilder.java index cd5c34551..992657c9a 100644 --- a/api/net/sf/briar/api/protocol/BatchBuilder.java +++ b/api/net/sf/briar/api/protocol/BatchBuilder.java @@ -1,10 +1,15 @@ package net.sf.briar.api.protocol; +import java.security.SignatureException; + public interface BatchBuilder { /** Adds a message to the batch. */ void addMessage(Message m); + /** Sets the sender's signature over the contents of the batch. */ + void setSignature(byte[] sig); + /** Builds and returns the batch. */ - Batch build(); + Batch build() throws SignatureException; } diff --git a/api/net/sf/briar/api/protocol/Bundle.java b/api/net/sf/briar/api/protocol/Bundle.java index 7465c3b56..82ff0f86d 100644 --- a/api/net/sf/briar/api/protocol/Bundle.java +++ b/api/net/sf/briar/api/protocol/Bundle.java @@ -1,28 +1,21 @@ package net.sf.briar.api.protocol; -import java.util.Map; +import java.io.IOException; +import java.security.SignatureException; -/** A bundle of acknowledgements, subscriptions, and batches of messages. */ +/** + * A bundle of acknowledgements, subscriptions, transport details and batches. + */ public interface Bundle { - /** Returns the bundle's unique identifier. */ - BundleId getId(); + /** Returns the size of the serialised bundle in bytes. */ + long getSize() throws IOException; - /** Returns the bundle's capacity in bytes. */ - long getCapacity(); + /** Returns the bundle's header. */ + Header getHeader() throws IOException, SignatureException; - /** Returns the bundle's size in bytes. */ - long getSize(); - - /** Returns the acknowledgements contained in the bundle. */ - Iterable getAcks(); - - /** Returns the subscriptions contained in the bundle. */ - Iterable getSubscriptions(); - - /** Returns the transport details contained in the bundle. */ - Map getTransports(); - - /** Returns the batches of messages contained in the bundle. */ - Iterable getBatches(); + /** + * Returns the next batch of messages, or null if there are no more batches. + */ + Batch getNextBatch() throws IOException, SignatureException; } diff --git a/api/net/sf/briar/api/protocol/BundleBuilder.java b/api/net/sf/briar/api/protocol/BundleBuilder.java index f95ef602d..7f7c83771 100644 --- a/api/net/sf/briar/api/protocol/BundleBuilder.java +++ b/api/net/sf/briar/api/protocol/BundleBuilder.java @@ -1,22 +1,18 @@ package net.sf.briar.api.protocol; +import java.io.IOException; + public interface BundleBuilder { /** Returns the bundle's capacity in bytes. */ - long getCapacity(); + long getCapacity() throws IOException; - /** Adds an acknowledgement to the bundle. */ - void addAck(BatchId b); - - /** Adds a subscription to the bundle. */ - void addSubscription(GroupId g); - - /** Adds a transport detail to the bundle. */ - void addTransport(String key, String value); + /** Adds a header to the bundle. */ + void addHeader(Header h) throws IOException; /** Adds a batch of messages to the bundle. */ - void addBatch(Batch b); + void addBatch(Batch b) throws IOException; /** Builds and returns the bundle. */ - Bundle build(); + Bundle build() throws IOException; } diff --git a/api/net/sf/briar/api/protocol/Header.java b/api/net/sf/briar/api/protocol/Header.java new file mode 100644 index 000000000..ce3c581a0 --- /dev/null +++ b/api/net/sf/briar/api/protocol/Header.java @@ -0,0 +1,28 @@ +package net.sf.briar.api.protocol; + +import java.util.Map; +import java.util.Set; + +/** A bundle header up to MAX_SIZE bytes in total size. */ +public interface Header { + + static final int MAX_SIZE = 1024 * 1024; + + // FIXME: Remove BundleId when refactoring is complete + BundleId getId(); + + /** Returns the size of the serialised header in bytes. */ + long getSize(); + + /** Returns the acknowledgements contained in the header. */ + Set getAcks(); + + /** Returns the subscriptions contained in the header. */ + Set getSubscriptions(); + + /** Returns the transport details contained in the header. */ + Map getTransports(); + + /** Returns the sender's signature over the contents of the header. */ + byte[] getSignature(); +} diff --git a/api/net/sf/briar/api/protocol/HeaderBuilder.java b/api/net/sf/briar/api/protocol/HeaderBuilder.java new file mode 100644 index 000000000..4c00f8218 --- /dev/null +++ b/api/net/sf/briar/api/protocol/HeaderBuilder.java @@ -0,0 +1,24 @@ +package net.sf.briar.api.protocol; + +import java.io.IOException; +import java.security.SignatureException; +import java.util.Map; +import java.util.Set; + +public interface HeaderBuilder { + + /** Adds acknowledgements to the header. */ + void addAcks(Set acks) throws IOException; + + /** Adds subscriptions to the header. */ + void addSubscriptions(Set subs) throws IOException; + + /** Adds transport details to the header. */ + void addTransports(Map transports) throws IOException; + + /** Sets the sender's signature over the contents of the header. */ + void setSignature(byte[] sig) throws IOException; + + /** Builds and returns the header. */ + Header build() throws SignatureException; +} diff --git a/api/net/sf/briar/api/protocol/MessageParser.java b/api/net/sf/briar/api/protocol/MessageParser.java new file mode 100644 index 000000000..95a44e4cd --- /dev/null +++ b/api/net/sf/briar/api/protocol/MessageParser.java @@ -0,0 +1,10 @@ +package net.sf.briar.api.protocol; + +import java.security.SignatureException; + +import net.sf.briar.api.serial.FormatException; + +public interface MessageParser { + + Message parseMessage(byte[] body) throws FormatException, SignatureException; +} diff --git a/api/net/sf/briar/api/protocol/UniqueId.java b/api/net/sf/briar/api/protocol/UniqueId.java index 3eb9cdd15..22f57dfe2 100644 --- a/api/net/sf/briar/api/protocol/UniqueId.java +++ b/api/net/sf/briar/api/protocol/UniqueId.java @@ -2,7 +2,9 @@ package net.sf.briar.api.protocol; import java.util.Arrays; -public abstract class UniqueId { +import net.sf.briar.api.serial.Raw; + +public abstract class UniqueId implements Raw { public static final int LENGTH = 32; diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java index 49427ce33..4fc737634 100644 --- a/components/net/sf/briar/db/DatabaseComponentImpl.java +++ b/components/net/sf/briar/db/DatabaseComponentImpl.java @@ -10,6 +10,7 @@ import net.sf.briar.api.db.DbException; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.BatchBuilder; +import net.sf.briar.api.protocol.HeaderBuilder; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; @@ -27,6 +28,7 @@ DatabaseCleaner.Callback { protected final Database db; protected final DatabaseCleaner cleaner; + protected final Provider headerBuilderProvider; protected final Provider batchBuilderProvider; private final Object spaceLock = new Object(); @@ -36,9 +38,11 @@ DatabaseCleaner.Callback { private volatile boolean writesAllowed = true; DatabaseComponentImpl(Database db, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider) { this.db = db; this.cleaner = cleaner; + this.headerBuilderProvider = headerBuilderProvider; this.batchBuilderProvider = batchBuilderProvider; } diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java index a5ec47798..8ab59f933 100644 --- a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java +++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java @@ -1,9 +1,10 @@ package net.sf.briar.db; +import java.io.IOException; +import java.security.SignatureException; import java.util.HashSet; import java.util.Iterator; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; @@ -20,6 +21,8 @@ import net.sf.briar.api.protocol.BatchId; import net.sf.briar.api.protocol.Bundle; import net.sf.briar.api.protocol.BundleBuilder; import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Header; +import net.sf.briar.api.protocol.HeaderBuilder; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; @@ -55,8 +58,9 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { @Inject ReadWriteLockDatabaseComponent(Database db, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider) { - super(db, cleaner, batchBuilderProvider); + super(db, cleaner, headerBuilderProvider, batchBuilderProvider); } public void close() throws DbException { @@ -187,23 +191,22 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } public Bundle generateBundle(ContactId c, BundleBuilder b) - throws DbException { + throws DbException, IOException, SignatureException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Generating bundle for " + c); - // Ack all batches received from c + HeaderBuilder h; + // Add acks contactLock.readLock().lock(); try { if(!containsContact(c)) throw new NoSuchContactException(); + h = headerBuilderProvider.get(); messageStatusLock.writeLock().lock(); try { Txn txn = db.startTransaction(); try { - int numAcks = 0; - for(BatchId ack : db.removeBatchesToAck(txn, c)) { - b.addAck(ack); - numAcks++; - } + Set acks = db.removeBatchesToAck(txn, c); + h.addAcks(acks); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Added " + numAcks + " acks"); + LOG.fine("Added " + acks.size() + " acks"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -215,7 +218,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } finally { contactLock.readLock().unlock(); } - // Add a list of subscriptions + // Add subscriptions contactLock.readLock().lock(); try { if(!containsContact(c)) throw new NoSuchContactException(); @@ -223,13 +226,10 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { try { Txn txn = db.startTransaction(); try { - int numSubs = 0; - for(GroupId g : db.getSubscriptions(txn)) { - b.addSubscription(g); - numSubs++; - } + Set subs = db.getSubscriptions(txn); + h.addSubscriptions(subs); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Added " + numSubs + " subscriptions"); + LOG.fine("Added " + subs.size() + " subscriptions"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -249,14 +249,10 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { try { Txn txn = db.startTransaction(); try { - int numTransports = 0; Map transports = db.getTransports(txn); - for(Entry e : transports.entrySet()) { - b.addTransport(e.getKey(), e.getValue()); - numTransports++; - } + h.addTransports(transports); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Added " + numTransports + " transports"); + LOG.fine("Added " + transports.size() + " transports"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -268,8 +264,12 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } finally { contactLock.readLock().unlock(); } - // Add as many messages as possible to the bundle + // Sign the header and add it to the bundle + Header header = h.build(); long capacity = b.getCapacity(); + capacity -= header.getSize(); + b.addHeader(header); + // Add as many messages as possible to the bundle while(true) { Batch batch = fillBatch(c, capacity); if(batch == null) break; // No more messages to send @@ -278,7 +278,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { capacity -= size; // If the batch is less than half full, stop trying - there may be // more messages trickling in but we can't wait forever - if(size * 2 < Batch.CAPACITY) break; + if(size * 2 < Batch.MAX_SIZE) break; } Bundle bundle = b.build(); if(LOG.isLoggable(Level.FINE)) @@ -287,20 +287,20 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { return bundle; } - private Batch fillBatch(ContactId c, long capacity) throws DbException { + private Batch fillBatch(ContactId c, long capacity) throws DbException, + SignatureException { contactLock.readLock().lock(); try { if(!containsContact(c)) throw new NoSuchContactException(); messageLock.readLock().lock(); try { Set sent; - BatchBuilder b; Batch batch; messageStatusLock.readLock().lock(); try { Txn txn = db.startTransaction(); try { - capacity = Math.min(capacity, Batch.CAPACITY); + capacity = Math.min(capacity, Batch.MAX_SIZE); Iterator it = db.getSendableMessages(txn, c, capacity).iterator(); if(!it.hasNext()) { @@ -308,7 +308,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { return null; // No more messages to send } sent = new HashSet(); - b = batchBuilderProvider.get(); + BatchBuilder b = batchBuilderProvider.get(); while(it.hasNext()) { MessageId m = it.next(); b.addMessage(db.getMessage(txn, m)); @@ -319,6 +319,9 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } catch(DbException e) { db.abortTransaction(txn); throw e; + } catch(SignatureException e) { + db.abortTransaction(txn); + throw e; } } finally { messageStatusLock.readLock().unlock(); @@ -438,21 +441,23 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } - public void receiveBundle(ContactId c, Bundle b) throws DbException { + public void receiveBundle(ContactId c, Bundle b) throws DbException, + IOException, SignatureException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Received bundle from " + c + ", " + b.getSize() + " bytes"); + Header h; // Mark all messages in acked batches as seen contactLock.readLock().lock(); try { if(!containsContact(c)) throw new NoSuchContactException(); + h = b.getHeader(); messageLock.readLock().lock(); try { messageStatusLock.writeLock().lock(); try { - int acks = 0; - for(BatchId ack : b.getAcks()) { - acks++; + Set acks = h.getAcks(); + for(BatchId ack : acks) { Txn txn = db.startTransaction(); try { db.removeAckedBatch(txn, c, ack); @@ -463,7 +468,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } if(LOG.isLoggable(Level.FINE)) - LOG.fine("Received " + acks + " acks"); + LOG.fine("Received " + acks.size() + " acks"); } finally { messageStatusLock.writeLock().unlock(); } @@ -481,14 +486,12 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { try { Txn txn = db.startTransaction(); try { + // FIXME: Replace clearSubs and addSub with setSubs db.clearSubscriptions(txn, c); - int subs = 0; - for(GroupId g : b.getSubscriptions()) { - subs++; - db.addSubscription(txn, c, g); - } + Set subs = h.getSubscriptions(); + for(GroupId sub : subs) db.addSubscription(txn, c, sub); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Received " + subs + " subscriptions"); + LOG.fine("Received " + subs.size() + " subscriptions"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -508,7 +511,11 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { try { Txn txn = db.startTransaction(); try { - db.setTransports(txn, c, b.getTransports()); + Map transports = h.getTransports(); + db.setTransports(txn, c, transports); + if(LOG.isLoggable(Level.FINE)) + LOG.fine("Received " + transports.size() + + " transports"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -522,7 +529,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } // Store the messages int batches = 0; - for(Batch batch : b.getBatches()) { + for(Batch batch = b.getNextBatch(); batch != null; batch = b.getNextBatch()) { batches++; waitForPermissionToWrite(); contactLock.readLock().lock(); @@ -579,7 +586,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { try { Txn txn = db.startTransaction(); try { - lost = db.addReceivedBundle(txn, c, b.getId()); + lost = db.addReceivedBundle(txn, c, h.getId()); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java index cacf6cfe9..df9472002 100644 --- a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java +++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java @@ -1,9 +1,10 @@ package net.sf.briar.db; +import java.io.IOException; +import java.security.SignatureException; import java.util.HashSet; import java.util.Iterator; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -19,6 +20,8 @@ import net.sf.briar.api.protocol.BatchId; import net.sf.briar.api.protocol.Bundle; import net.sf.briar.api.protocol.BundleBuilder; import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Header; +import net.sf.briar.api.protocol.HeaderBuilder; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; @@ -48,8 +51,9 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { @Inject SynchronizedDatabaseComponent(Database db, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider) { - super(db, cleaner, batchBuilderProvider); + super(db, cleaner, headerBuilderProvider, batchBuilderProvider); } public void close() throws DbException { @@ -140,21 +144,20 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } public Bundle generateBundle(ContactId c, BundleBuilder b) - throws DbException { + throws DbException, IOException, SignatureException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Generating bundle for " + c); - // Ack all batches received from c + HeaderBuilder h; + // Add acks synchronized(contactLock) { if(!containsContact(c)) throw new NoSuchContactException(); + h = headerBuilderProvider.get(); synchronized(messageStatusLock) { Txn txn = db.startTransaction(); try { - int numAcks = 0; - for(BatchId ack : db.removeBatchesToAck(txn, c)) { - b.addAck(ack); - numAcks++; - } + Set acks = db.removeBatchesToAck(txn, c); + h.addAcks(acks); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Added " + numAcks + " acks"); + LOG.fine("Added " + acks.size() + " acks"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -162,19 +165,16 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } } - // Add a list of subscriptions + // Add subscriptions synchronized(contactLock) { if(!containsContact(c)) throw new NoSuchContactException(); synchronized(subscriptionLock) { Txn txn = db.startTransaction(); try { - int numSubs = 0; - for(GroupId g : db.getSubscriptions(txn)) { - b.addSubscription(g); - numSubs++; - } + Set subs = db.getSubscriptions(txn); + h.addSubscriptions(subs); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Added " + numSubs + " subscriptions"); + LOG.fine("Added " + subs.size() + " subscriptions"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -188,14 +188,10 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { synchronized(transportLock) { Txn txn = db.startTransaction(); try { - int numTransports = 0; Map transports = db.getTransports(txn); - for(Entry e : transports.entrySet()) { - b.addTransport(e.getKey(), e.getValue()); - numTransports++; - } + h.addTransports(transports); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Added " + numTransports + " transports"); + LOG.fine("Added " + transports.size() + " transports"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -203,8 +199,12 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } } - // Add as many messages as possible to the bundle + // Sign the header and add it to the bundle + Header header = h.build(); long capacity = b.getCapacity(); + capacity -= header.getSize(); + b.addHeader(header); + // Add as many messages as possible to the bundle while(true) { Batch batch = fillBatch(c, capacity); if(batch == null) break; // No more messages to send @@ -213,7 +213,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { capacity -= size; // If the batch is less than half full, stop trying - there may be // more messages trickling in but we can't wait forever - if(size * 2 < Batch.CAPACITY) break; + if(size * 2 < Batch.MAX_SIZE) break; } Bundle bundle = b.build(); if(LOG.isLoggable(Level.FINE)) @@ -222,14 +222,15 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { return bundle; } - private Batch fillBatch(ContactId c, long capacity) throws DbException { + private Batch fillBatch(ContactId c, long capacity) throws DbException, + SignatureException { synchronized(contactLock) { if(!containsContact(c)) throw new NoSuchContactException(); synchronized(messageLock) { synchronized(messageStatusLock) { Txn txn = db.startTransaction(); try { - capacity = Math.min(capacity, Batch.CAPACITY); + capacity = Math.min(capacity, Batch.MAX_SIZE); Iterator it = db.getSendableMessages(txn, c, capacity).iterator(); if(!it.hasNext()) { @@ -252,6 +253,9 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } catch(DbException e) { db.abortTransaction(txn); throw e; + } catch(SignatureException e) { + db.abortTransaction(txn); + throw e; } } } @@ -331,18 +335,20 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } - public void receiveBundle(ContactId c, Bundle b) throws DbException { + public void receiveBundle(ContactId c, Bundle b) throws DbException, + IOException, SignatureException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Received bundle from " + c + ", " + b.getSize() + " bytes"); + Header h; // Mark all messages in acked batches as seen synchronized(contactLock) { if(!containsContact(c)) throw new NoSuchContactException(); + h = b.getHeader(); synchronized(messageLock) { synchronized(messageStatusLock) { - int acks = 0; - for(BatchId ack : b.getAcks()) { - acks++; + Set acks = h.getAcks(); + for(BatchId ack : acks) { Txn txn = db.startTransaction(); try { db.removeAckedBatch(txn, c, ack); @@ -353,7 +359,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } if(LOG.isLoggable(Level.FINE)) - LOG.fine("Received " + acks + " acks"); + LOG.fine("Received " + acks.size() + " acks"); } } } @@ -363,14 +369,12 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { synchronized(subscriptionLock) { Txn txn = db.startTransaction(); try { + // FIXME: Replace clearSubs and addSub with setSubs db.clearSubscriptions(txn, c); - int subs = 0; - for(GroupId g : b.getSubscriptions()) { - subs++; - db.addSubscription(txn, c, g); - } + Set subs = h.getSubscriptions(); + for(GroupId sub : subs) db.addSubscription(txn, c, sub); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Received " + subs + " subscriptions"); + LOG.fine("Received " + subs.size() + " subscriptions"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -384,7 +388,11 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { synchronized(transportLock) { Txn txn = db.startTransaction(); try { - db.setTransports(txn, c, b.getTransports()); + Map transports = h.getTransports(); + db.setTransports(txn, c, transports); + if(LOG.isLoggable(Level.FINE)) + LOG.fine("Received " + transports.size() + + " transports"); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -394,7 +402,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } // Store the messages int batches = 0; - for(Batch batch : b.getBatches()) { + for(Batch batch = b.getNextBatch(); batch != null; batch = b.getNextBatch()) { batches++; waitForPermissionToWrite(); synchronized(contactLock) { @@ -436,7 +444,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { synchronized(messageStatusLock) { Txn txn = db.startTransaction(); try { - lost = db.addReceivedBundle(txn, c, b.getId()); + lost = db.addReceivedBundle(txn, c, h.getId()); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); diff --git a/components/net/sf/briar/protocol/BatchImpl.java b/components/net/sf/briar/protocol/BatchImpl.java index 68d0e12dd..97ccd5a71 100644 --- a/components/net/sf/briar/protocol/BatchImpl.java +++ b/components/net/sf/briar/protocol/BatchImpl.java @@ -1,8 +1,6 @@ package net.sf.briar.protocol; -import java.util.ArrayList; import java.util.List; -import java.util.Random; import net.sf.briar.api.protocol.Batch; import net.sf.briar.api.protocol.BatchId; @@ -11,15 +9,16 @@ import net.sf.briar.api.protocol.Message; /** A simple in-memory implementation of a batch. */ class BatchImpl implements Batch { - private final List messages = new ArrayList(); - private BatchId id = null; - private long size = 0L; + private final BatchId id; + private final long size; + private final List messages; + private final byte[] signature; - public void seal() { - // FIXME: Calculate batch ID - byte[] b = new byte[BatchId.LENGTH]; - new Random().nextBytes(b); - id = new BatchId(b); + BatchImpl(BatchId id, long size, List messages, byte[] signature) { + this.id = id; + this.size = size; + this.messages = messages; + this.signature = signature; } public BatchId getId() { @@ -34,8 +33,7 @@ class BatchImpl implements Batch { return messages; } - public void addMessage(Message m) { - messages.add(m); - size += m.getSize(); + public byte[] getSignature() { + return signature; } } diff --git a/components/net/sf/briar/protocol/BundleReader.java b/components/net/sf/briar/protocol/BundleReader.java new file mode 100644 index 000000000..c3bcdf0ca --- /dev/null +++ b/components/net/sf/briar/protocol/BundleReader.java @@ -0,0 +1,94 @@ +package net.sf.briar.protocol; + +import java.io.IOException; +import java.security.SignatureException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchBuilder; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Bundle; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Header; +import net.sf.briar.api.protocol.HeaderBuilder; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageParser; +import net.sf.briar.api.protocol.UniqueId; +import net.sf.briar.api.serial.FormatException; +import net.sf.briar.api.serial.Raw; +import net.sf.briar.api.serial.Reader; + +import com.google.inject.Provider; + +/** A bundle that deserialises its contents on demand using a reader. */ +abstract class BundleReader implements Bundle { + + private static enum State { START, FIRST_BATCH, MORE_BATCHES, END }; + + private final Reader r; + private final MessageParser messageParser; + private final Provider headerBuilderProvider; + private final Provider batchBuilderProvider; + private State state = State.START; + + BundleReader(Reader r, MessageParser messageParser, + Provider headerBuilderProvider, + Provider batchBuilderProvider) { + this.r = r; + this.messageParser = messageParser; + this.headerBuilderProvider = headerBuilderProvider; + this.batchBuilderProvider = batchBuilderProvider; + } + + public Header getHeader() throws IOException, SignatureException { + if(state != State.START) throw new IllegalStateException(); + r.setReadLimit(Header.MAX_SIZE); + Set acks = new HashSet(); + for(Raw raw : r.readList(Raw.class)) { + byte[] b = raw.getBytes(); + if(b.length != UniqueId.LENGTH) throw new FormatException(); + acks.add(new BatchId(b)); + } + Set subs = new HashSet(); + for(Raw raw : r.readList(Raw.class)) { + byte[] b = raw.getBytes(); + if(b.length != UniqueId.LENGTH) throw new FormatException(); + subs.add(new GroupId(b)); + } + Map transports = r.readMap(String.class, String.class); + byte[] sig = r.readRaw(); + state = State.FIRST_BATCH; + HeaderBuilder h = headerBuilderProvider.get(); + h.addAcks(acks); + h.addSubscriptions(subs); + h.addTransports(transports); + h.setSignature(sig); + return h.build(); + } + + public Batch getNextBatch() throws IOException, SignatureException { + if(state == State.FIRST_BATCH) { + r.readListStart(); + state = State.MORE_BATCHES; + } + if(state != State.MORE_BATCHES) throw new IllegalStateException(); + if(r.hasListEnd()) { + r.readListEnd(); + state = State.END; + return null; + } + r.setReadLimit(Batch.MAX_SIZE); + List messages = r.readList(Raw.class); + BatchBuilder b = batchBuilderProvider.get(); + for(Raw r : messages) { + Message m = messageParser.parseMessage(r.getBytes()); + b.addMessage(m); + } + byte[] sig = r.readRaw(); + b.setSignature(sig); + return b.build(); + } +} diff --git a/components/net/sf/briar/protocol/BundleWriter.java b/components/net/sf/briar/protocol/BundleWriter.java new file mode 100644 index 000000000..ccd589475 --- /dev/null +++ b/components/net/sf/briar/protocol/BundleWriter.java @@ -0,0 +1,66 @@ +package net.sf.briar.protocol; + +import java.io.IOException; + +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.BundleBuilder; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Header; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.serial.Writer; + +/** A bundle builder that serialises its contents using a writer. */ +abstract class BundleWriter implements BundleBuilder { + + private static enum State { START, FIRST_BATCH, MORE_BATCHES, END }; + + private final Writer w; + private final long capacity; + private State state = State.START; + + BundleWriter(Writer w, long capacity) { + this.w = w; + this.capacity = capacity; + } + + public long getCapacity() { + return capacity; + } + + public void addHeader(Header h) throws IOException { + if(state != State.START) throw new IllegalStateException(); + w.writeListStart(); + for(BatchId ack : h.getAcks()) w.writeRaw(ack); + w.writeListEnd(); + w.writeListStart(); + for(GroupId sub : h.getSubscriptions()) w.writeRaw(sub); + w.writeListEnd(); + w.writeMap(h.getTransports()); + w.writeRaw(h.getSignature()); + state = State.FIRST_BATCH; + } + + public void addBatch(Batch b) throws IOException { + if(state == State.FIRST_BATCH) { + w.writeListStart(); + state = State.MORE_BATCHES; + } + if(state != State.MORE_BATCHES) throw new IllegalStateException(); + w.writeListStart(); + for(Message m : b.getMessages()) w.writeRaw(m.getBody()); + w.writeListEnd(); + w.writeRaw(b.getSignature()); + } + + void close() throws IOException { + if(state == State.FIRST_BATCH) { + w.writeListStart(); + state = State.MORE_BATCHES; + } + if(state != State.MORE_BATCHES) throw new IllegalStateException(); + w.writeListEnd(); + w.close(); + state = State.END; + } +} diff --git a/components/net/sf/briar/protocol/FileBundle.java b/components/net/sf/briar/protocol/FileBundle.java new file mode 100644 index 000000000..930f8699f --- /dev/null +++ b/components/net/sf/briar/protocol/FileBundle.java @@ -0,0 +1,30 @@ +package net.sf.briar.protocol; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import net.sf.briar.api.protocol.BatchBuilder; +import net.sf.briar.api.protocol.HeaderBuilder; +import net.sf.briar.api.protocol.MessageParser; +import net.sf.briar.api.serial.ReaderFactory; + +import com.google.inject.Provider; + +class FileBundle extends BundleReader { + + private final File file; + + FileBundle(File file, ReaderFactory readerFactory, + MessageParser messageParser, + Provider headerBuilderProvider, + Provider batchBuilderProvider) throws IOException { + super(readerFactory.createReader(new FileInputStream(file)), + messageParser, headerBuilderProvider, batchBuilderProvider); + this.file = file; + } + + public long getSize() throws IOException { + return file.length(); + } +} diff --git a/components/net/sf/briar/protocol/FileBundleBuilder.java b/components/net/sf/briar/protocol/FileBundleBuilder.java new file mode 100644 index 000000000..085f637f7 --- /dev/null +++ b/components/net/sf/briar/protocol/FileBundleBuilder.java @@ -0,0 +1,41 @@ +package net.sf.briar.protocol; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import net.sf.briar.api.protocol.BatchBuilder; +import net.sf.briar.api.protocol.Bundle; +import net.sf.briar.api.protocol.HeaderBuilder; +import net.sf.briar.api.protocol.MessageParser; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.WriterFactory; + +import com.google.inject.Provider; + +public class FileBundleBuilder extends BundleWriter { + + private final File file; + private final ReaderFactory readerFactory; + private final MessageParser messageParser; + private final Provider headerBuilderProvider; + private final Provider batchBuilderProvider; + + FileBundleBuilder(File file, long capacity, WriterFactory writerFactory, + ReaderFactory readerFactory, MessageParser messageParser, + Provider headerBuilderProvider, + Provider batchBuilderProvider) throws IOException { + super(writerFactory.createWriter(new FileOutputStream(file)), capacity); + this.file = file; + this.readerFactory = readerFactory; + this.messageParser = messageParser; + this.headerBuilderProvider = headerBuilderProvider; + this.batchBuilderProvider = batchBuilderProvider; + } + + public Bundle build() throws IOException { + super.close(); + return new FileBundle(file, readerFactory, messageParser, + headerBuilderProvider, batchBuilderProvider); + } +} diff --git a/components/net/sf/briar/protocol/HeaderImpl.java b/components/net/sf/briar/protocol/HeaderImpl.java new file mode 100644 index 000000000..763daf0f1 --- /dev/null +++ b/components/net/sf/briar/protocol/HeaderImpl.java @@ -0,0 +1,55 @@ +package net.sf.briar.protocol; + +import java.util.Map; +import java.util.Set; + +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.BundleId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Header; + +/** A simple in-memory implementation of a header. */ +class HeaderImpl implements Header { + + private final BundleId id; + private final long size; + private final Set acks; + private final Set subscriptions; + private final Map transports; + private final byte[] signature; + + HeaderImpl(BundleId id, long size, Set acks, + Set subscriptions, Map transports, + byte[] signature) { + this.id = id; + this.size = size; + this.acks = acks; + this.subscriptions = subscriptions; + this.transports = transports; + this.signature = signature; + } + + public BundleId getId() { + return id; + } + + public long getSize() { + return size; + } + + public Set getAcks() { + return acks; + } + + public Set getSubscriptions() { + return subscriptions; + } + + public Map getTransports() { + return transports; + } + + public byte[] getSignature() { + return signature; + } +} diff --git a/components/net/sf/briar/protocol/MessageImpl.java b/components/net/sf/briar/protocol/MessageImpl.java index 0ae46a822..64306e4e4 100644 --- a/components/net/sf/briar/protocol/MessageImpl.java +++ b/components/net/sf/briar/protocol/MessageImpl.java @@ -5,6 +5,7 @@ import net.sf.briar.api.protocol.GroupId; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; +/** A simple in-memory implementation of a message. */ public class MessageImpl implements Message { private final MessageId id, parent; @@ -53,7 +54,7 @@ public class MessageImpl implements Message { @Override public boolean equals(Object o) { - return o instanceof MessageImpl && id.equals(((MessageImpl)o).id); + return o instanceof Message && id.equals(((Message)o).getId()); } @Override diff --git a/components/net/sf/briar/protocol/ProtocolModule.java b/components/net/sf/briar/protocol/ProtocolModule.java index 30d41ee82..501ad5165 100644 --- a/components/net/sf/briar/protocol/ProtocolModule.java +++ b/components/net/sf/briar/protocol/ProtocolModule.java @@ -1,10 +1,8 @@ package net.sf.briar.protocol; -import net.sf.briar.api.protocol.Batch; import net.sf.briar.api.protocol.Message; import com.google.inject.AbstractModule; -import com.google.inject.Provides; public class ProtocolModule extends AbstractModule { @@ -12,9 +10,4 @@ public class ProtocolModule extends AbstractModule { protected void configure() { bind(Message.class).to(MessageImpl.class); } - - @Provides - Batch createBatch() { - return new BatchImpl(); - } } diff --git a/test/build.xml b/test/build.xml index d908f95f3..f6a2653a3 100644 --- a/test/build.xml +++ b/test/build.xml @@ -20,6 +20,8 @@ + + diff --git a/test/net/sf/briar/db/DatabaseComponentImplTest.java b/test/net/sf/briar/db/DatabaseComponentImplTest.java index 5532d7a56..ba3e7a4e0 100644 --- a/test/net/sf/briar/db/DatabaseComponentImplTest.java +++ b/test/net/sf/briar/db/DatabaseComponentImplTest.java @@ -7,6 +7,7 @@ import java.util.Collections; import net.sf.briar.api.db.DbException; import net.sf.briar.api.protocol.BatchBuilder; +import net.sf.briar.api.protocol.HeaderBuilder; import net.sf.briar.api.protocol.MessageId; import net.sf.briar.db.DatabaseCleaner.Callback; @@ -24,6 +25,7 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest { protected abstract DatabaseComponentImpl createDatabaseComponentImpl( Database database, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider); @Test @@ -33,14 +35,17 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ oneOf(database).getFreeSpace(); will(returnValue(MIN_FREE_SPACE)); }}); Callback db = createDatabaseComponentImpl(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.checkFreeSpaceAndClean(); @@ -54,8 +59,11 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ oneOf(database).getFreeSpace(); will(returnValue(MIN_FREE_SPACE - 1)); @@ -69,7 +77,7 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest { will(returnValue(MIN_FREE_SPACE)); }}); Callback db = createDatabaseComponentImpl(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.checkFreeSpaceAndClean(); @@ -84,8 +92,11 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ oneOf(database).getFreeSpace(); will(returnValue(MIN_FREE_SPACE - 1)); @@ -101,7 +112,7 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest { will(returnValue(MIN_FREE_SPACE)); }}); Callback db = createDatabaseComponentImpl(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.checkFreeSpaceAndClean(); @@ -116,8 +127,11 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ oneOf(database).getFreeSpace(); will(returnValue(MIN_FREE_SPACE - 1)); @@ -135,7 +149,7 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest { will(returnValue(MIN_FREE_SPACE)); }}); Callback db = createDatabaseComponentImpl(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.checkFreeSpaceAndClean(); diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java index 0edcdc4be..174c3a6af 100644 --- a/test/net/sf/briar/db/DatabaseComponentTest.java +++ b/test/net/sf/briar/db/DatabaseComponentTest.java @@ -1,5 +1,7 @@ package net.sf.briar.db; +import java.io.IOException; +import java.security.SignatureException; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -20,6 +22,8 @@ import net.sf.briar.api.protocol.Bundle; import net.sf.briar.api.protocol.BundleBuilder; import net.sf.briar.api.protocol.BundleId; import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Header; +import net.sf.briar.api.protocol.HeaderBuilder; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; import net.sf.briar.protocol.MessageImpl; @@ -32,8 +36,6 @@ import com.google.inject.Provider; public abstract class DatabaseComponentTest extends TestCase { - private static final int ONE_MEGABYTE = 1024 * 1024; - protected final Object txn = new Object(); protected final AuthorId authorId; protected final BatchId batchId; @@ -45,6 +47,11 @@ public abstract class DatabaseComponentTest extends TestCase { private final int size; private final byte[] body; private final Message message; + private final Set contacts; + private final Set acks; + private final Set subs; + private final Map transports; + private final Set messages; public DatabaseComponentTest() { super(); @@ -60,26 +67,32 @@ public abstract class DatabaseComponentTest extends TestCase { body = new byte[size]; message = new MessageImpl(messageId, MessageId.NONE, groupId, authorId, timestamp, body); + contacts = Collections.singleton(contactId); + acks = Collections.singleton(batchId); + subs = Collections.singleton(groupId); + transports = Collections.singletonMap("foo", "bar"); + messages = Collections.singleton(messageId); } protected abstract DatabaseComponent createDatabaseComponent( Database database, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider); @Test public void testSimpleCalls() throws DbException { - final Map transports = - Collections.singletonMap("foo", "bar"); final Map transports1 = Collections.singletonMap("foo", "bar baz"); - final Set subs = Collections.singleton(groupId); Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ allowing(database).startTransaction(); will(returnValue(txn)); @@ -95,7 +108,7 @@ public abstract class DatabaseComponentTest extends TestCase { will(returnValue(contactId)); // getContacts() oneOf(database).getContacts(txn); - will(returnValue(Collections.singleton(contactId))); + will(returnValue(contacts)); // getTransports(contactId) oneOf(database).containsContact(txn, contactId); will(returnValue(true)); @@ -119,16 +132,16 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).close(); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.open(false); assertEquals(Rating.UNRATED, db.getRating(authorId)); assertEquals(contactId, db.addContact(transports)); - assertEquals(Collections.singleton(contactId), db.getContacts()); + assertEquals(contacts, db.getContacts()); assertEquals(transports, db.getTransports(contactId)); db.setTransports(contactId, transports1); db.subscribe(groupId); - assertEquals(Collections.singleton(groupId), db.getSubscriptions()); + assertEquals(subs, db.getSubscriptions()); db.unsubscribe(groupId); db.removeContact(contactId); db.close(); @@ -138,14 +151,16 @@ public abstract class DatabaseComponentTest extends TestCase { @Test public void testNoParentStopsBackwardInclusion() throws DbException { - final Set messages = Collections.singleton(messageId); Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // setRating(Rating.GOOD) allowing(database).startTransaction(); @@ -163,7 +178,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.setRating(authorId, Rating.GOOD); @@ -172,14 +187,16 @@ public abstract class DatabaseComponentTest extends TestCase { @Test public void testMissingParentStopsBackwardInclusion() throws DbException { - final Set messages = Collections.singleton(messageId); Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // setRating(Rating.GOOD) oneOf(database).startTransaction(); @@ -200,7 +217,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.setRating(authorId, Rating.GOOD); @@ -210,14 +227,16 @@ public abstract class DatabaseComponentTest extends TestCase { @Test public void testChangingGroupsStopsBackwardInclusion() throws DbException { final GroupId groupId1 = new GroupId(TestUtils.getRandomId()); - final Set messages = Collections.singleton(messageId); Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // setRating(Rating.GOOD) oneOf(database).startTransaction(); @@ -242,7 +261,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.setRating(authorId, Rating.GOOD); @@ -252,14 +271,16 @@ public abstract class DatabaseComponentTest extends TestCase { @Test public void testUnaffectedParentStopsBackwardInclusion() throws DbException { - final Set messages = Collections.singleton(messageId); Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // setRating(Rating.GOOD) oneOf(database).startTransaction(); @@ -287,7 +308,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.setRating(authorId, Rating.GOOD); @@ -297,14 +318,16 @@ public abstract class DatabaseComponentTest extends TestCase { @Test public void testAffectedParentContinuesBackwardInclusion() throws DbException { - final Set messages = Collections.singleton(messageId); Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // setRating(Rating.GOOD) oneOf(database).startTransaction(); @@ -334,7 +357,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.setRating(authorId, Rating.GOOD); @@ -349,8 +372,11 @@ public abstract class DatabaseComponentTest extends TestCase { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // addLocallyGeneratedMessage(message) oneOf(database).startTransaction(); @@ -360,7 +386,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.addLocallyGeneratedMessage(message); @@ -374,8 +400,11 @@ public abstract class DatabaseComponentTest extends TestCase { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // addLocallyGeneratedMessage(message) oneOf(database).startTransaction(); @@ -387,7 +416,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.addLocallyGeneratedMessage(message); @@ -401,8 +430,11 @@ public abstract class DatabaseComponentTest extends TestCase { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // addLocallyGeneratedMessage(message) oneOf(database).startTransaction(); @@ -412,7 +444,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).addMessage(txn, message); will(returnValue(true)); oneOf(database).getContacts(txn); - will(returnValue(Collections.singleton(contactId))); + will(returnValue(contacts)); oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); // The author is unrated and there are no sendable children oneOf(database).getRating(txn, authorId); @@ -423,7 +455,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.addLocallyGeneratedMessage(message); @@ -438,8 +470,11 @@ public abstract class DatabaseComponentTest extends TestCase { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); context.checking(new Expectations() {{ // addLocallyGeneratedMessage(message) oneOf(database).startTransaction(); @@ -449,7 +484,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).addMessage(txn, message); will(returnValue(true)); oneOf(database).getContacts(txn); - will(returnValue(Collections.singleton(contactId))); + will(returnValue(contacts)); oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); // The author is rated GOOD and there are two sendable children oneOf(database).getRating(txn, authorId); @@ -463,7 +498,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.addLocallyGeneratedMessage(message); @@ -472,14 +507,17 @@ public abstract class DatabaseComponentTest extends TestCase { @Test public void testGenerateBundleThrowsExceptionIfContactIsMissing() - throws DbException { + throws DbException, IOException, SignatureException { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); final BundleBuilder bundleBuilder = context.mock(BundleBuilder.class); context.checking(new Expectations() {{ // Check that the contact is still in the DB @@ -490,7 +528,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); try { db.generateBundle(contactId, bundleBuilder); @@ -501,15 +539,22 @@ public abstract class DatabaseComponentTest extends TestCase { } @Test - public void testGenerateBundle() throws DbException { + public void testGenerateBundle() throws DbException, IOException, + SignatureException { + final long headerSize = 1234L; Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); final BundleBuilder bundleBuilder = context.mock(BundleBuilder.class); + final HeaderBuilder headerBuilder = context.mock(HeaderBuilder.class); + final Header header = context.mock(Header.class); final BatchBuilder batchBuilder = context.mock(BatchBuilder.class); final Batch batch = context.mock(Batch.class); final Bundle bundle = context.mock(Bundle.class); @@ -519,24 +564,33 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).commitTransaction(txn); allowing(database).containsContact(txn, contactId); will(returnValue(true)); - // Add acks to the bundle + // Build the header + oneOf(headerBuilderProvider).get(); + will(returnValue(headerBuilder)); + // Add acks to the header oneOf(database).removeBatchesToAck(txn, contactId); - will(returnValue(Collections.singleton(batchId))); - oneOf(bundleBuilder).addAck(batchId); - // Add subscriptions to the bundle + will(returnValue(acks)); + oneOf(headerBuilder).addAcks(acks); + // Add subscriptions to the header oneOf(database).getSubscriptions(txn); - will(returnValue(Collections.singleton(groupId))); - oneOf(bundleBuilder).addSubscription(groupId); - // Add transports to the bundle + will(returnValue(subs)); + oneOf(headerBuilder).addSubscriptions(subs); + // Add transports to the header oneOf(database).getTransports(txn); - will(returnValue(Collections.singletonMap("foo", "bar"))); - oneOf(bundleBuilder).addTransport("foo", "bar"); - // Prepare to add batches to the bundle + will(returnValue(transports)); + oneOf(headerBuilder).addTransports(transports); + // Build the header + oneOf(headerBuilder).build(); + will(returnValue(header)); oneOf(bundleBuilder).getCapacity(); - will(returnValue((long) ONE_MEGABYTE)); - // Add messages to the batch - oneOf(database).getSendableMessages(txn, contactId, Batch.CAPACITY); - will(returnValue(Collections.singleton(messageId))); + will(returnValue(1024L * 1024L)); + oneOf(header).getSize(); + will(returnValue(headerSize)); + oneOf(bundleBuilder).addHeader(header); + // Add a batch to the bundle + oneOf(database).getSendableMessages(txn, contactId, + Batch.MAX_SIZE - headerSize); + will(returnValue(messages)); oneOf(batchBuilderProvider).get(); will(returnValue(batchBuilder)); oneOf(database).getMessage(txn, messageId); @@ -547,8 +601,8 @@ public abstract class DatabaseComponentTest extends TestCase { // Record the batch as outstanding oneOf(batch).getId(); will(returnValue(batchId)); - oneOf(database).addOutstandingBatch(txn, contactId, batchId, - Collections.singleton(messageId)); + oneOf(database).addOutstandingBatch( + txn, contactId, batchId, messages); // Add the batch to the bundle oneOf(bundleBuilder).addBatch(batch); // Check whether to add another batch @@ -559,7 +613,7 @@ public abstract class DatabaseComponentTest extends TestCase { will(returnValue(bundle)); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.generateBundle(contactId, bundleBuilder); @@ -568,14 +622,17 @@ public abstract class DatabaseComponentTest extends TestCase { @Test public void testReceiveBundleThrowsExceptionIfContactIsMissing() - throws DbException { + throws DbException, IOException, SignatureException { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); final Bundle bundle = context.mock(Bundle.class); context.checking(new Expectations() {{ // Check that the contact is still in the DB @@ -586,7 +643,7 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); try { db.receiveBundle(contactId, bundle); @@ -597,17 +654,20 @@ public abstract class DatabaseComponentTest extends TestCase { } @Test - public void testReceivedBundle() throws DbException { - final Map transports = - Collections.singletonMap("foo", "bar"); + public void testReceivedBundle() throws DbException, IOException, + SignatureException { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); @SuppressWarnings("unchecked") - final Provider batchBuilderProvider = + final Provider headerBuilderProvider = context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); final Bundle bundle = context.mock(Bundle.class); + final Header header = context.mock(Header.class); final Batch batch = context.mock(Batch.class); context.checking(new Expectations() {{ allowing(database).startTransaction(); @@ -615,22 +675,25 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).commitTransaction(txn); allowing(database).containsContact(txn, contactId); will(returnValue(true)); + // Header + oneOf(bundle).getHeader(); + will(returnValue(header)); // Acks - oneOf(bundle).getAcks(); - will(returnValue(Collections.singleton(batchId))); + oneOf(header).getAcks(); + will(returnValue(acks)); oneOf(database).removeAckedBatch(txn, contactId, batchId); // Subscriptions oneOf(database).clearSubscriptions(txn, contactId); - oneOf(bundle).getSubscriptions(); - will(returnValue(Collections.singleton(groupId))); + oneOf(header).getSubscriptions(); + will(returnValue(subs)); oneOf(database).addSubscription(txn, contactId, groupId); // Transports - oneOf(bundle).getTransports(); + oneOf(header).getTransports(); will(returnValue(transports)); oneOf(database).setTransports(txn, contactId, transports); // Batches - oneOf(bundle).getBatches(); - will(returnValue(Collections.singleton(batch))); + oneOf(bundle).getNextBatch(); + will(returnValue(batch)); oneOf(batch).getMessages(); will(returnValue(Collections.singleton(message))); oneOf(database).containsSubscription(txn, groupId); @@ -642,15 +705,18 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(batch).getId(); will(returnValue(batchId)); oneOf(database).addBatchToAck(txn, contactId, batchId); + // Any more batches? Nope + oneOf(bundle).getNextBatch(); + will(returnValue(null)); // Lost batches - oneOf(bundle).getId(); + oneOf(header).getId(); will(returnValue(bundleId)); oneOf(database).addReceivedBundle(txn, contactId, bundleId); will(returnValue(Collections.singleton(batchId))); oneOf(database).removeLostBatch(txn, contactId, batchId); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); db.receiveBundle(contactId, bundle); diff --git a/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java b/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java index 77e32e60f..ccc1a714d 100644 --- a/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java +++ b/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java @@ -2,6 +2,7 @@ package net.sf.briar.db; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.protocol.BatchBuilder; +import net.sf.briar.api.protocol.HeaderBuilder; import com.google.inject.Provider; @@ -11,16 +12,18 @@ extends DatabaseComponentImplTest { @Override protected DatabaseComponent createDatabaseComponent( Database database, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider) { return createDatabaseComponentImpl(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); } @Override protected DatabaseComponentImpl createDatabaseComponentImpl( Database database, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider) { return new ReadWriteLockDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); } } diff --git a/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java b/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java index e562db8ba..5c8f576c3 100644 --- a/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java +++ b/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java @@ -2,6 +2,7 @@ package net.sf.briar.db; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.protocol.BatchBuilder; +import net.sf.briar.api.protocol.HeaderBuilder; import com.google.inject.Provider; @@ -11,16 +12,18 @@ extends DatabaseComponentImplTest { @Override protected DatabaseComponent createDatabaseComponent( Database database, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider) { return createDatabaseComponentImpl(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); } @Override protected DatabaseComponentImpl createDatabaseComponentImpl( Database database, DatabaseCleaner cleaner, + Provider headerBuilderProvider, Provider batchBuilderProvider) { return new SynchronizedDatabaseComponent(database, cleaner, - batchBuilderProvider); + headerBuilderProvider, batchBuilderProvider); } } diff --git a/test/net/sf/briar/protocol/BundleReaderTest.java b/test/net/sf/briar/protocol/BundleReaderTest.java new file mode 100644 index 000000000..b0a5f5d01 --- /dev/null +++ b/test/net/sf/briar/protocol/BundleReaderTest.java @@ -0,0 +1,251 @@ +package net.sf.briar.protocol; + +import java.io.IOException; +import java.security.SignatureException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchBuilder; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Header; +import net.sf.briar.api.protocol.HeaderBuilder; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageParser; +import net.sf.briar.api.serial.Raw; +import net.sf.briar.api.serial.Reader; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Provider; + +public class BundleReaderTest extends TestCase { + + private final long size = 1024L * 1024L; + private final BatchId ack = new BatchId(TestUtils.getRandomId()); + private final List rawAcks = + Collections.singletonList(new TestRaw(ack.getBytes())); + private final Set acks = Collections.singleton(ack); + private final GroupId sub = new GroupId(TestUtils.getRandomId()); + private final List rawSubs = + Collections.singletonList(new TestRaw(sub.getBytes())); + private final Set subs = Collections.singleton(sub); + private final Map transports = + Collections.singletonMap("foo", "bar"); + private final byte[] headerSig = TestUtils.getRandomId(); + private final byte[] messageBody = new byte[123]; + private final List rawMessages = + Collections.singletonList(new TestRaw(messageBody)); + private final byte[] batchSig = TestUtils.getRandomId(); + + @Test + public void testGetHeader() throws IOException, SignatureException { + Mockery context = new Mockery(); + final Reader reader = context.mock(Reader.class); + final MessageParser messageParser = context.mock(MessageParser.class); + @SuppressWarnings("unchecked") + final Provider headerBuilderProvider = + context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); + final HeaderBuilder headerBuilder = context.mock(HeaderBuilder.class); + final Header header = context.mock(Header.class); + context.checking(new Expectations() {{ + oneOf(reader).setReadLimit(Header.MAX_SIZE); + oneOf(headerBuilderProvider).get(); + will(returnValue(headerBuilder)); + // Acks + oneOf(reader).readList(Raw.class); + will(returnValue(rawAcks)); + oneOf(headerBuilder).addAcks(acks); + // Subs + oneOf(reader).readList(Raw.class); + will(returnValue(rawSubs)); + oneOf(headerBuilder).addSubscriptions(subs); + // Transports + oneOf(reader).readMap(String.class, String.class); + will(returnValue(transports)); + oneOf(headerBuilder).addTransports(transports); + // Signature + oneOf(reader).readRaw(); + will(returnValue(headerSig)); + oneOf(headerBuilder).setSignature(headerSig); + // Build the header + oneOf(headerBuilder).build(); + will(returnValue(header)); + }}); + BundleReader r = createBundleReader(reader, messageParser, + headerBuilderProvider, batchBuilderProvider); + + assertEquals(header, r.getHeader()); + + context.assertIsSatisfied(); + } + + @Test + public void testBatchBeforeHeaderThrowsException() throws IOException, + SignatureException { + Mockery context = new Mockery(); + final Reader reader = context.mock(Reader.class); + final MessageParser messageParser = context.mock(MessageParser.class); + @SuppressWarnings("unchecked") + final Provider headerBuilderProvider = + context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); + BundleReader r = createBundleReader(reader, messageParser, + headerBuilderProvider, batchBuilderProvider); + + try { + r.getNextBatch(); + assertTrue(false); + } catch(IllegalStateException expected) {} + + context.assertIsSatisfied(); + } + + @Test + public void testGetHeaderNoBatches() throws IOException, + SignatureException { + Mockery context = new Mockery(); + final Reader reader = context.mock(Reader.class); + final MessageParser messageParser = context.mock(MessageParser.class); + @SuppressWarnings("unchecked") + final Provider headerBuilderProvider = + context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); + final HeaderBuilder headerBuilder = context.mock(HeaderBuilder.class); + final Header header = context.mock(Header.class); + context.checking(new Expectations() {{ + oneOf(reader).setReadLimit(Header.MAX_SIZE); + oneOf(headerBuilderProvider).get(); + will(returnValue(headerBuilder)); + // Acks + oneOf(reader).readList(Raw.class); + will(returnValue(rawAcks)); + oneOf(headerBuilder).addAcks(acks); + // Subs + oneOf(reader).readList(Raw.class); + will(returnValue(rawSubs)); + oneOf(headerBuilder).addSubscriptions(subs); + // Transports + oneOf(reader).readMap(String.class, String.class); + will(returnValue(transports)); + oneOf(headerBuilder).addTransports(transports); + // Signature + oneOf(reader).readRaw(); + will(returnValue(headerSig)); + oneOf(headerBuilder).setSignature(headerSig); + // Build the header + oneOf(headerBuilder).build(); + will(returnValue(header)); + // No batches + oneOf(reader).readListStart(); + oneOf(reader).hasListEnd(); + will(returnValue(true)); + oneOf(reader).readListEnd(); + }}); + BundleReader r = createBundleReader(reader, messageParser, + headerBuilderProvider, batchBuilderProvider); + + assertEquals(header, r.getHeader()); + assertNull(r.getNextBatch()); + + context.assertIsSatisfied(); + } + + @Test + public void testGetHeaderOneBatch() throws IOException, + SignatureException { + Mockery context = new Mockery(); + final Reader reader = context.mock(Reader.class); + final MessageParser messageParser = context.mock(MessageParser.class); + @SuppressWarnings("unchecked") + final Provider headerBuilderProvider = + context.mock(Provider.class); + @SuppressWarnings("unchecked") + final Provider batchBuilderProvider = + context.mock(Provider.class, "batchBuilderProvider"); + final HeaderBuilder headerBuilder = context.mock(HeaderBuilder.class); + final Header header = context.mock(Header.class); + final BatchBuilder batchBuilder = context.mock(BatchBuilder.class); + final Batch batch = context.mock(Batch.class); + final Message message = context.mock(Message.class); + context.checking(new Expectations() {{ + oneOf(reader).setReadLimit(Header.MAX_SIZE); + oneOf(headerBuilderProvider).get(); + will(returnValue(headerBuilder)); + // Acks + oneOf(reader).readList(Raw.class); + will(returnValue(rawAcks)); + oneOf(headerBuilder).addAcks(acks); + // Subs + oneOf(reader).readList(Raw.class); + will(returnValue(rawSubs)); + oneOf(headerBuilder).addSubscriptions(subs); + // Transports + oneOf(reader).readMap(String.class, String.class); + will(returnValue(transports)); + oneOf(headerBuilder).addTransports(transports); + // Signature + oneOf(reader).readRaw(); + will(returnValue(headerSig)); + oneOf(headerBuilder).setSignature(headerSig); + // Build the header + oneOf(headerBuilder).build(); + will(returnValue(header)); + // First batch + oneOf(reader).readListStart(); + oneOf(reader).hasListEnd(); + will(returnValue(false)); + oneOf(reader).setReadLimit(Batch.MAX_SIZE); + oneOf(batchBuilderProvider).get(); + will(returnValue(batchBuilder)); + oneOf(reader).readList(Raw.class); + will(returnValue(rawMessages)); + oneOf(messageParser).parseMessage(messageBody); + will(returnValue(message)); + oneOf(batchBuilder).addMessage(message); + oneOf(reader).readRaw(); + will(returnValue(batchSig)); + oneOf(batchBuilder).setSignature(batchSig); + oneOf(batchBuilder).build(); + will(returnValue(batch)); + // No more batches + oneOf(reader).hasListEnd(); + will(returnValue(true)); + oneOf(reader).readListEnd(); + }}); + BundleReader r = createBundleReader(reader, messageParser, + headerBuilderProvider, batchBuilderProvider); + + assertEquals(header, r.getHeader()); + assertEquals(batch, r.getNextBatch()); + assertNull(r.getNextBatch()); + + context.assertIsSatisfied(); + } + + private BundleReader createBundleReader(Reader reader, + MessageParser messageParser, + Provider headerBuilderProvider, + Provider batchBuilderProvider) { + return new BundleReader(reader, messageParser, headerBuilderProvider, + batchBuilderProvider) { + public long getSize() { + return size; + } + }; + } +} diff --git a/test/net/sf/briar/protocol/BundleWriterTest.java b/test/net/sf/briar/protocol/BundleWriterTest.java new file mode 100644 index 000000000..a2e025300 --- /dev/null +++ b/test/net/sf/briar/protocol/BundleWriterTest.java @@ -0,0 +1,242 @@ +package net.sf.briar.protocol; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Bundle; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Header; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.serial.Writer; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +public class BundleWriterTest extends TestCase { + + private final BatchId ack = new BatchId(TestUtils.getRandomId()); + private final Set acks = Collections.singleton(ack); + private final GroupId sub = new GroupId(TestUtils.getRandomId()); + private final Set subs = Collections.singleton(sub); + private final Map transports = + Collections.singletonMap("foo", "bar"); + private final byte[] headerSig = TestUtils.getRandomId(); + private final long capacity = 1024L * 1024L; + private final byte[] messageBody = new byte[123]; + private final byte[] batchSig = TestUtils.getRandomId(); + + @Test + public void testAddHeader() throws IOException { + Mockery context = new Mockery(); + final Writer writer = context.mock(Writer.class); + final Header header = context.mock(Header.class); + context.checking(new Expectations() {{ + // Acks + oneOf(writer).writeListStart(); + oneOf(header).getAcks(); + will(returnValue(acks)); + oneOf(writer).writeRaw(ack); + oneOf(writer).writeListEnd(); + // Subs + oneOf(writer).writeListStart(); + oneOf(header).getSubscriptions(); + will(returnValue(subs)); + oneOf(writer).writeRaw(sub); + oneOf(writer).writeListEnd(); + // Transports + oneOf(header).getTransports(); + will(returnValue(transports)); + oneOf(writer).writeMap(transports); + // Signature + oneOf(header).getSignature(); + will(returnValue(headerSig)); + oneOf(writer).writeRaw(headerSig); + }}); + BundleWriter w = createBundleWriter(writer); + + w.addHeader(header); + + context.assertIsSatisfied(); + } + + @Test + public void testAddHeaderEmptyLists() throws IOException { + Mockery context = new Mockery(); + final Writer writer = context.mock(Writer.class); + final Header header = context.mock(Header.class); + context.checking(new Expectations() {{ + // Acks + oneOf(writer).writeListStart(); + oneOf(header).getAcks(); + will(returnValue(Collections.emptySet())); + oneOf(writer).writeListEnd(); + // Subs + oneOf(writer).writeListStart(); + oneOf(header).getSubscriptions(); + will(returnValue(Collections.emptySet())); + oneOf(writer).writeListEnd(); + // Transports + oneOf(header).getTransports(); + will(returnValue(Collections.emptyMap())); + oneOf(writer).writeMap(Collections.emptyMap()); + // Signature + oneOf(header).getSignature(); + will(returnValue(headerSig)); + oneOf(writer).writeRaw(headerSig); + }}); + BundleWriter w = createBundleWriter(writer); + + w.addHeader(header); + + context.assertIsSatisfied(); + } + + @Test + public void testBatchBeforeHeaderThrowsException() throws IOException { + Mockery context = new Mockery(); + final Writer writer = context.mock(Writer.class); + final Batch batch = context.mock(Batch.class); + BundleWriter w = createBundleWriter(writer); + + try { + w.addBatch(batch); + assertTrue(false); + } catch(IllegalStateException expected) {} + + context.assertIsSatisfied(); + } + + @Test + public void testCloseBeforeHeaderThrowsException() throws IOException { + Mockery context = new Mockery(); + final Writer writer = context.mock(Writer.class); + BundleWriter w = createBundleWriter(writer); + + try { + w.close(); + assertTrue(false); + } catch(IllegalStateException expected) {} + + context.assertIsSatisfied(); + } + + @Test + public void testCloseWithoutBatchesDoesNotThrowException() + throws IOException { + Mockery context = new Mockery(); + final Writer writer = context.mock(Writer.class); + final Header header = context.mock(Header.class); + context.checking(new Expectations() {{ + // Acks + oneOf(writer).writeListStart(); + oneOf(header).getAcks(); + will(returnValue(acks)); + oneOf(writer).writeRaw(ack); + oneOf(writer).writeListEnd(); + // Subs + oneOf(writer).writeListStart(); + oneOf(header).getSubscriptions(); + will(returnValue(subs)); + oneOf(writer).writeRaw(sub); + oneOf(writer).writeListEnd(); + // Transports + oneOf(header).getTransports(); + will(returnValue(transports)); + oneOf(writer).writeMap(transports); + // Signature + oneOf(header).getSignature(); + will(returnValue(headerSig)); + oneOf(writer).writeRaw(headerSig); + // Close - write an empty list of batches + oneOf(writer).writeListStart(); + oneOf(writer).writeListEnd(); + oneOf(writer).close(); + }}); + BundleWriter w = createBundleWriter(writer); + + w.addHeader(header); + w.close(); + + context.assertIsSatisfied(); + } + + @Test + public void testAddHeaderAndTwoBatches() throws IOException { + Mockery context = new Mockery(); + final Writer writer = context.mock(Writer.class); + final Header header = context.mock(Header.class); + final Batch batch = context.mock(Batch.class); + final Message message = context.mock(Message.class); + context.checking(new Expectations() {{ + // Acks + oneOf(writer).writeListStart(); + oneOf(header).getAcks(); + will(returnValue(acks)); + oneOf(writer).writeRaw(ack); + oneOf(writer).writeListEnd(); + // Subs + oneOf(writer).writeListStart(); + oneOf(header).getSubscriptions(); + will(returnValue(subs)); + oneOf(writer).writeRaw(sub); + oneOf(writer).writeListEnd(); + // Transports + oneOf(header).getTransports(); + will(returnValue(transports)); + oneOf(writer).writeMap(transports); + // Signature + oneOf(header).getSignature(); + will(returnValue(headerSig)); + oneOf(writer).writeRaw(headerSig); + // First batch + oneOf(writer).writeListStart(); + oneOf(writer).writeListStart(); + oneOf(batch).getMessages(); + will(returnValue(Collections.singleton(message))); + oneOf(message).getBody(); + will(returnValue(messageBody)); + oneOf(writer).writeRaw(messageBody); + oneOf(writer).writeListEnd(); + oneOf(batch).getSignature(); + will(returnValue(batchSig)); + oneOf(writer).writeRaw(batchSig); + // Second batch + oneOf(writer).writeListStart(); + oneOf(batch).getMessages(); + will(returnValue(Collections.singleton(message))); + oneOf(message).getBody(); + will(returnValue(messageBody)); + oneOf(writer).writeRaw(messageBody); + oneOf(writer).writeListEnd(); + oneOf(batch).getSignature(); + will(returnValue(batchSig)); + oneOf(writer).writeRaw(batchSig); + // Close + oneOf(writer).writeListEnd(); + oneOf(writer).close(); + }}); + BundleWriter w = createBundleWriter(writer); + + w.addHeader(header); + w.addBatch(batch); + w.addBatch(batch); + w.close(); + + context.assertIsSatisfied(); + } + + private BundleWriter createBundleWriter(Writer writer) { + return new BundleWriter(writer, capacity) { + public Bundle build() throws IOException { + return null; + } + }; + } +} diff --git a/test/net/sf/briar/protocol/TestRaw.java b/test/net/sf/briar/protocol/TestRaw.java new file mode 100644 index 000000000..f9f685b36 --- /dev/null +++ b/test/net/sf/briar/protocol/TestRaw.java @@ -0,0 +1,29 @@ +package net.sf.briar.protocol; + +import java.util.Arrays; + +import net.sf.briar.api.serial.Raw; + +class TestRaw implements Raw { + + private final byte[] bytes; + + TestRaw(byte[] bytes) { + this.bytes = bytes; + } + + public byte[] getBytes() { + return bytes; + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + @Override + public boolean equals(Object o) { + if(o instanceof Raw) return Arrays.equals(bytes, ((Raw) o).getBytes()); + return false; + } +}