From 3d549ea6ac9a4a91ad210371b36093781be964bb Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Tue, 12 Jul 2011 16:50:20 +0100
Subject: [PATCH] Builders for incoming and outgoing headers and batches. The
 protocol and serial components can now be used to serialise, sign,
 deserialise and verify real bundles (except for message parsing).

---
 .../sf/briar/api/db/DatabaseComponent.java    |  10 +-
 .../sf/briar/api/protocol/BatchBuilder.java   |   4 +-
 api/net/sf/briar/api/protocol/Bundle.java     |  21 ---
 .../sf/briar/api/protocol/BundleReader.java   |  25 +++
 .../{BundleBuilder.java => BundleWriter.java} |  10 +-
 .../sf/briar/api/protocol/HeaderBuilder.java  |  12 +-
 api/net/sf/briar/api/protocol/Message.java    |   7 +-
 api/net/sf/briar/api/serial/Reader.java       |   1 +
 api/net/sf/briar/api/serial/Writer.java       |   4 +-
 components/net/sf/briar/db/JdbcDatabase.java  |   2 +-
 .../db/ReadWriteLockDatabaseComponent.java    |  99 ++++++-----
 .../db/SynchronizedDatabaseComponent.java     |  89 +++++-----
 .../sf/briar/protocol/BatchBuilderImpl.java   |  44 +++++
 ...undleReader.java => BundleReaderImpl.java} |  22 ++-
 ...undleWriter.java => BundleWriterImpl.java} |  10 +-
 .../net/sf/briar/protocol/FileBundle.java     |  30 ----
 .../sf/briar/protocol/FileBundleBuilder.java  |  41 -----
 .../sf/briar/protocol/HeaderBuilderImpl.java  |  63 +++++++
 .../briar/protocol/IncomingBatchBuilder.java  |  40 +++++
 .../briar/protocol/IncomingHeaderBuilder.java |  47 ++++++
 .../net/sf/briar/protocol/MessageImpl.java    |   2 +-
 .../briar/protocol/OutgoingBatchBuilder.java  |  37 +++++
 .../briar/protocol/OutgoingHeaderBuilder.java |  44 +++++
 .../sf/briar/serial/ReaderFactoryImpl.java    |   2 +-
 .../net/sf/briar/serial/ReaderImpl.java       |   4 +
 .../sf/briar/serial/WriterFactoryImpl.java    |   2 +-
 .../net/sf/briar/serial/WriterImpl.java       |  10 +-
 test/build.xml                                |   1 +
 .../sf/briar/db/DatabaseComponentTest.java    |  47 +++---
 test/net/sf/briar/db/H2DatabaseTest.java      |   2 +-
 .../briar/protocol/BundleReadWriteTest.java   | 156 ++++++++++++++++++
 .../sf/briar/protocol/BundleReaderTest.java   |  59 ++++---
 .../sf/briar/protocol/BundleWriterTest.java   |  28 ++--
 33 files changed, 692 insertions(+), 283 deletions(-)
 delete mode 100644 api/net/sf/briar/api/protocol/Bundle.java
 create mode 100644 api/net/sf/briar/api/protocol/BundleReader.java
 rename api/net/sf/briar/api/protocol/{BundleBuilder.java => BundleWriter.java} (60%)
 create mode 100644 components/net/sf/briar/protocol/BatchBuilderImpl.java
 rename components/net/sf/briar/protocol/{BundleReader.java => BundleReaderImpl.java} (84%)
 rename components/net/sf/briar/protocol/{BundleWriter.java => BundleWriterImpl.java} (86%)
 delete mode 100644 components/net/sf/briar/protocol/FileBundle.java
 delete mode 100644 components/net/sf/briar/protocol/FileBundleBuilder.java
 create mode 100644 components/net/sf/briar/protocol/HeaderBuilderImpl.java
 create mode 100644 components/net/sf/briar/protocol/IncomingBatchBuilder.java
 create mode 100644 components/net/sf/briar/protocol/IncomingHeaderBuilder.java
 create mode 100644 components/net/sf/briar/protocol/OutgoingBatchBuilder.java
 create mode 100644 components/net/sf/briar/protocol/OutgoingHeaderBuilder.java
 create mode 100644 test/net/sf/briar/protocol/BundleReadWriteTest.java

diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java
index f662e85b60..bd7075d4a3 100644
--- a/api/net/sf/briar/api/db/DatabaseComponent.java
+++ b/api/net/sf/briar/api/db/DatabaseComponent.java
@@ -1,15 +1,15 @@
 package net.sf.briar.api.db;
 
 import java.io.IOException;
-import java.security.SignatureException;
+import java.security.GeneralSecurityException;
 import java.util.Map;
 import java.util.Set;
 
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.Bundle;
-import net.sf.briar.api.protocol.BundleBuilder;
+import net.sf.briar.api.protocol.BundleReader;
+import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 
@@ -51,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, IOException, SignatureException;
+	void generateBundle(ContactId c, BundleWriter bundleBuilder) throws DbException, IOException, GeneralSecurityException;
 
 	/** Returns the IDs of all contacts. */
 	Set<ContactId> getContacts() throws DbException;
@@ -73,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, IOException, SignatureException;
+	void receiveBundle(ContactId c, BundleReader b) throws DbException, IOException, GeneralSecurityException;
 
 	/** 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/BatchBuilder.java b/api/net/sf/briar/api/protocol/BatchBuilder.java
index 992657c9a9..fd17670c0d 100644
--- a/api/net/sf/briar/api/protocol/BatchBuilder.java
+++ b/api/net/sf/briar/api/protocol/BatchBuilder.java
@@ -1,5 +1,7 @@
 package net.sf.briar.api.protocol;
 
+import java.io.IOException;
+import java.security.InvalidKeyException;
 import java.security.SignatureException;
 
 public interface BatchBuilder {
@@ -11,5 +13,5 @@ public interface BatchBuilder {
 	void setSignature(byte[] sig);
 
 	/** Builds and returns the batch. */
-	Batch build() throws SignatureException;
+	Batch build() throws IOException, SignatureException, InvalidKeyException;
 }
diff --git a/api/net/sf/briar/api/protocol/Bundle.java b/api/net/sf/briar/api/protocol/Bundle.java
deleted file mode 100644
index 82ff0f86d9..0000000000
--- a/api/net/sf/briar/api/protocol/Bundle.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package net.sf.briar.api.protocol;
-
-import java.io.IOException;
-import java.security.SignatureException;
-
-/**
- * A bundle of acknowledgements, subscriptions, transport details and batches.
- */
-public interface Bundle {
-
-	/** Returns the size of the serialised bundle in bytes. */
-	long getSize() throws IOException;
-
-	/** Returns the bundle's header. */
-	Header getHeader() throws IOException, SignatureException;
-
-	/**
-	 * 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/BundleReader.java b/api/net/sf/briar/api/protocol/BundleReader.java
new file mode 100644
index 0000000000..b5e6390437
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/BundleReader.java
@@ -0,0 +1,25 @@
+package net.sf.briar.api.protocol;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/**
+ * An interface for reading a bundle of acknowledgements, subscriptions,
+ * transport details and batches.
+ */
+public interface BundleReader {
+
+	/** Returns the size of the serialised bundle in bytes. */
+	long getSize() throws IOException;
+
+	/** Returns the bundle's header. */
+	Header getHeader() throws IOException, GeneralSecurityException;
+
+	/**
+	 * Returns the next batch of messages, or null if there are no more batches.
+	 */
+	Batch getNextBatch() throws IOException, GeneralSecurityException;
+
+	/** Finishes reading the bundle. */
+	void close() throws IOException;
+}
diff --git a/api/net/sf/briar/api/protocol/BundleBuilder.java b/api/net/sf/briar/api/protocol/BundleWriter.java
similarity index 60%
rename from api/net/sf/briar/api/protocol/BundleBuilder.java
rename to api/net/sf/briar/api/protocol/BundleWriter.java
index 7f7c837719..730a133ae6 100644
--- a/api/net/sf/briar/api/protocol/BundleBuilder.java
+++ b/api/net/sf/briar/api/protocol/BundleWriter.java
@@ -2,7 +2,11 @@ package net.sf.briar.api.protocol;
 
 import java.io.IOException;
 
-public interface BundleBuilder {
+/**
+ * An interface for writing a bundle of acknowledgements, subscriptions,
+ * transport details and batches.
+ */
+public interface BundleWriter {
 
 	/** Returns the bundle's capacity in bytes. */
 	long getCapacity() throws IOException;
@@ -13,6 +17,6 @@ public interface BundleBuilder {
 	/** Adds a batch of messages to the bundle. */
 	void addBatch(Batch b) throws IOException;
 
-	/** Builds and returns the bundle. */
-	Bundle build() throws IOException;
+	/** Finishes writing the bundle. */
+	void close() throws IOException;
 }
diff --git a/api/net/sf/briar/api/protocol/HeaderBuilder.java b/api/net/sf/briar/api/protocol/HeaderBuilder.java
index 4c00f8218f..9c13b889f3 100644
--- a/api/net/sf/briar/api/protocol/HeaderBuilder.java
+++ b/api/net/sf/briar/api/protocol/HeaderBuilder.java
@@ -1,24 +1,24 @@
 package net.sf.briar.api.protocol;
 
 import java.io.IOException;
+import java.security.InvalidKeyException;
 import java.security.SignatureException;
 import java.util.Map;
-import java.util.Set;
 
 public interface HeaderBuilder {
 
 	/** Adds acknowledgements to the header. */
-	void addAcks(Set<BatchId> acks) throws IOException;
+	void addAcks(Iterable<BatchId> acks);
 
 	/** Adds subscriptions to the header. */
-	void addSubscriptions(Set<GroupId> subs) throws IOException;
+	void addSubscriptions(Iterable<GroupId> subs);
 
 	/** Adds transport details to the header. */
-	void addTransports(Map<String, String> transports) throws IOException;
+	void addTransports(Map<String, String> transports);
 
 	/** Sets the sender's signature over the contents of the header. */
-	void setSignature(byte[] sig) throws IOException;
+	void setSignature(byte[] sig);
 
 	/** Builds and returns the header. */
-	Header build() throws SignatureException;
+	Header build() throws IOException, SignatureException, InvalidKeyException;
 }
diff --git a/api/net/sf/briar/api/protocol/Message.java b/api/net/sf/briar/api/protocol/Message.java
index 8ed42d12d2..715f1115ae 100644
--- a/api/net/sf/briar/api/protocol/Message.java
+++ b/api/net/sf/briar/api/protocol/Message.java
@@ -1,6 +1,8 @@
 package net.sf.briar.api.protocol;
 
-public interface Message {
+import net.sf.briar.api.serial.Raw;
+
+public interface Message extends Raw {
 
 	/** Returns the message's unique identifier. */
 	MessageId getId();
@@ -22,7 +24,4 @@ public interface Message {
 
 	/** Returns the size of the message in bytes. */
 	int getSize();
-
-	/** Returns the message in wire format. */
-	byte[] getBody();
 }
\ No newline at end of file
diff --git a/api/net/sf/briar/api/serial/Reader.java b/api/net/sf/briar/api/serial/Reader.java
index 3a92c90a16..47bc385919 100644
--- a/api/net/sf/briar/api/serial/Reader.java
+++ b/api/net/sf/briar/api/serial/Reader.java
@@ -9,6 +9,7 @@ public interface Reader {
 	boolean eof() throws IOException;
 	void setReadLimit(long limit);
 	void resetReadLimit();
+	void close() throws IOException;
 
 	boolean hasBoolean() throws IOException;
 	boolean readBoolean() throws IOException;
diff --git a/api/net/sf/briar/api/serial/Writer.java b/api/net/sf/briar/api/serial/Writer.java
index 103b8a5f0c..41f273333b 100644
--- a/api/net/sf/briar/api/serial/Writer.java
+++ b/api/net/sf/briar/api/serial/Writer.java
@@ -6,6 +6,8 @@ import java.util.Map;
 
 public interface Writer {
 
+	void close() throws IOException;
+
 	void writeBoolean(boolean b) throws IOException;
 
 	void writeUint7(byte b) throws IOException;
@@ -31,6 +33,4 @@ public interface Writer {
 	void writeMapEnd() throws IOException;
 
 	void writeNull() throws IOException;
-
-	void close() throws IOException;
 }
diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java
index 950547b50d..2c6c555263 100644
--- a/components/net/sf/briar/db/JdbcDatabase.java
+++ b/components/net/sf/briar/db/JdbcDatabase.java
@@ -467,7 +467,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.setBytes(4, m.getAuthor().getBytes());
 			ps.setLong(5, m.getTimestamp());
 			ps.setInt(6, m.getSize());
-			ps.setBlob(7, new ByteArrayInputStream(m.getBody()));
+			ps.setBlob(7, new ByteArrayInputStream(m.getBytes()));
 			ps.setInt(8, 0);
 			int rowsAffected = ps.executeUpdate();
 			assert rowsAffected == 1;
diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
index 8ab59f9335..f9971cfc06 100644
--- a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
+++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
@@ -1,6 +1,7 @@
 package net.sf.briar.db;
 
 import java.io.IOException;
+import java.security.GeneralSecurityException;
 import java.security.SignatureException;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -18,8 +19,9 @@ import net.sf.briar.api.protocol.AuthorId;
 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.BundleBuilder;
+import net.sf.briar.api.protocol.BundleId;
+import net.sf.briar.api.protocol.BundleReader;
+import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.HeaderBuilder;
@@ -190,8 +192,8 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		}
 	}
 
-	public Bundle generateBundle(ContactId c, BundleBuilder b)
-	throws DbException, IOException, SignatureException {
+	public void generateBundle(ContactId c, BundleWriter b) throws DbException,
+	IOException, GeneralSecurityException {
 		if(LOG.isLoggable(Level.FINE)) LOG.fine("Generating bundle for " + c);
 		HeaderBuilder h;
 		// Add acks
@@ -280,15 +282,13 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 			// more messages trickling in but we can't wait forever
 			if(size * 2 < Batch.MAX_SIZE) break;
 		}
-		Bundle bundle = b.build();
-		if(LOG.isLoggable(Level.FINE))
-			LOG.fine("Bundle generated, " + bundle.getSize() + " bytes");
+		b.close();
+		if(LOG.isLoggable(Level.FINE)) LOG.fine("Bundle generated");
 		System.gc();
-		return bundle;
 	}
 
 	private Batch fillBatch(ContactId c, long capacity) throws DbException,
-	SignatureException {
+	IOException, GeneralSecurityException {
 		contactLock.readLock().lock();
 		try {
 			if(!containsContact(c)) throw new NoSuchContactException();
@@ -441,8 +441,8 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		}
 	}
 
-	public void receiveBundle(ContactId c, Bundle b) throws DbException,
-	IOException, SignatureException {
+	public void receiveBundle(ContactId c, BundleReader b) throws DbException,
+	IOException, GeneralSecurityException {
 		if(LOG.isLoggable(Level.FINE))
 			LOG.fine("Received bundle from " + c + ", "
 					+ b.getSize() + " bytes");
@@ -529,52 +529,64 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		}
 		// Store the messages
 		int batches = 0;
-		for(Batch batch = b.getNextBatch(); batch != null; batch = b.getNextBatch()) {
+		Batch batch = null;
+		while((batch = b.getNextBatch()) != null) {
+			storeBatch(c, batch);
 			batches++;
-			waitForPermissionToWrite();
-			contactLock.readLock().lock();
+		}
+		if(LOG.isLoggable(Level.FINE))
+			LOG.fine("Received " + batches + " batches");
+		b.close();
+		retransmitLostBatches(c, h.getId());
+		System.gc();
+	}
+
+	private void storeBatch(ContactId c, Batch b) throws DbException {
+		waitForPermissionToWrite();
+		contactLock.readLock().lock();
+		try {
+			if(!containsContact(c)) throw new NoSuchContactException();
+			messageLock.writeLock().lock();
 			try {
-				if(!containsContact(c)) throw new NoSuchContactException();
-				messageLock.writeLock().lock();
+				messageStatusLock.writeLock().lock();
 				try {
-					messageStatusLock.writeLock().lock();
+					subscriptionLock.readLock().lock();
 					try {
-						subscriptionLock.readLock().lock();
+						Txn txn = db.startTransaction();
 						try {
-							Txn txn = db.startTransaction();
-							try {
-								int received = 0, stored = 0;
-								for(Message m : batch.getMessages()) {
-									received++;
-									GroupId g = m.getGroup();
-									if(db.containsSubscription(txn, g)) {
-										if(storeMessage(txn, m, c)) stored++;
-									}
+							int received = 0, stored = 0;
+							for(Message m : b.getMessages()) {
+								received++;
+								GroupId g = m.getGroup();
+								if(db.containsSubscription(txn, g)) {
+									if(storeMessage(txn, m, c)) stored++;
 								}
-								if(LOG.isLoggable(Level.FINE))
-									LOG.fine("Received " + received
-											+ " messages, stored " + stored);
-								db.addBatchToAck(txn, c, batch.getId());
-								db.commitTransaction(txn);
-							} catch(DbException e) {
-								db.abortTransaction(txn);
-								throw e;
 							}
-						} finally {
-							subscriptionLock.readLock().unlock();
+							if(LOG.isLoggable(Level.FINE))
+								LOG.fine("Received " + received
+										+ " messages, stored " + stored);
+							db.addBatchToAck(txn, c, b.getId());
+							db.commitTransaction(txn);
+						} catch(DbException e) {
+							db.abortTransaction(txn);
+							throw e;
 						}
 					} finally {
-						messageStatusLock.writeLock().unlock();
+						subscriptionLock.readLock().unlock();
 					}
 				} finally {
-					messageLock.writeLock().unlock();
+					messageStatusLock.writeLock().unlock();
 				}
 			} finally {
-				contactLock.readLock().unlock();
+				messageLock.writeLock().unlock();
 			}
+		} finally {
+			contactLock.readLock().unlock();
 		}
-		if(LOG.isLoggable(Level.FINE))
-			LOG.fine("Received " + batches + " batches");
+	}
+
+	private void retransmitLostBatches(ContactId c, BundleId b)
+	throws DbException {
 		// Find any lost batches that need to be retransmitted
 		Set<BatchId> lost;
 		contactLock.readLock().lock();
@@ -586,7 +598,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 				try {
 					Txn txn = db.startTransaction();
 					try {
-						lost = db.addReceivedBundle(txn, c, h.getId());
+						lost = db.addReceivedBundle(txn, c, b);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
@@ -629,7 +641,6 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 				contactLock.readLock().unlock();
 			}
 		}
-		System.gc();
 	}
 
 	public void removeContact(ContactId c) throws DbException {
diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
index df94720028..f7caf09107 100644
--- a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
+++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
@@ -1,6 +1,7 @@
 package net.sf.briar.db;
 
 import java.io.IOException;
+import java.security.GeneralSecurityException;
 import java.security.SignatureException;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -17,8 +18,9 @@ import net.sf.briar.api.protocol.AuthorId;
 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.BundleBuilder;
+import net.sf.briar.api.protocol.BundleId;
+import net.sf.briar.api.protocol.BundleReader;
+import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.HeaderBuilder;
@@ -143,8 +145,8 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		}
 	}
 
-	public Bundle generateBundle(ContactId c, BundleBuilder b)
-	throws DbException, IOException, SignatureException {
+	public void generateBundle(ContactId c, BundleWriter b) throws DbException,
+	IOException, GeneralSecurityException {
 		if(LOG.isLoggable(Level.FINE)) LOG.fine("Generating bundle for " + c);
 		HeaderBuilder h;
 		// Add acks
@@ -215,15 +217,13 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 			// more messages trickling in but we can't wait forever
 			if(size * 2 < Batch.MAX_SIZE) break;
 		}
-		Bundle bundle = b.build();
-		if(LOG.isLoggable(Level.FINE))
-			LOG.fine("Bundle generated, " + bundle.getSize() + " bytes");
+		b.close();
+		if(LOG.isLoggable(Level.FINE)) LOG.fine("Bundle generated");
 		System.gc();
-		return bundle;
 	}
 
 	private Batch fillBatch(ContactId c, long capacity) throws DbException,
-	SignatureException {
+	IOException, GeneralSecurityException {
 		synchronized(contactLock) {
 			if(!containsContact(c)) throw new NoSuchContactException();
 			synchronized(messageLock) {
@@ -335,8 +335,8 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		}
 	}
 
-	public void receiveBundle(ContactId c, Bundle b) throws DbException,
-	IOException, SignatureException {
+	public void receiveBundle(ContactId c, BundleReader b) throws DbException,
+	IOException, GeneralSecurityException {
 		if(LOG.isLoggable(Level.FINE))
 			LOG.fine("Received bundle from " + c + ", "
 					+ b.getSize() + " bytes");
@@ -402,40 +402,52 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		}
 		// Store the messages
 		int batches = 0;
-		for(Batch batch = b.getNextBatch(); batch != null; batch = b.getNextBatch()) {
+		Batch batch = null;
+		while((batch = b.getNextBatch()) != null) {
+			storeBatch(c, batch);
 			batches++;
-			waitForPermissionToWrite();
-			synchronized(contactLock) {
-				if(!containsContact(c)) throw new NoSuchContactException();
-				synchronized(messageLock) {
-					synchronized(messageStatusLock) {
-						synchronized(subscriptionLock) {
-							Txn txn = db.startTransaction();
-							try {
-								int received = 0, stored = 0;
-								for(Message m : batch.getMessages()) {
-									received++;
-									GroupId g = m.getGroup();
-									if(db.containsSubscription(txn, g)) {
-										if(storeMessage(txn, m, c)) stored++;
-									}
+		}
+		if(LOG.isLoggable(Level.FINE))
+			LOG.fine("Received " + batches + " batches");
+		b.close();
+		retransmitLostBatches(c, h.getId());
+		System.gc();
+	}
+
+	private void storeBatch(ContactId c, Batch b) throws DbException {
+		waitForPermissionToWrite();
+		synchronized(contactLock) {
+			if(!containsContact(c)) throw new NoSuchContactException();
+			synchronized(messageLock) {
+				synchronized(messageStatusLock) {
+					synchronized(subscriptionLock) {
+						Txn txn = db.startTransaction();
+						try {
+							int received = 0, stored = 0;
+							for(Message m : b.getMessages()) {
+								received++;
+								GroupId g = m.getGroup();
+								if(db.containsSubscription(txn, g)) {
+									if(storeMessage(txn, m, c)) stored++;
 								}
-								if(LOG.isLoggable(Level.FINE))
-									LOG.fine("Received " + received
-											+ " messages, stored " + stored);
-								db.addBatchToAck(txn, c, batch.getId());
-								db.commitTransaction(txn);
-							} catch(DbException e) {
-								db.abortTransaction(txn);
-								throw e;
 							}
+							if(LOG.isLoggable(Level.FINE))
+								LOG.fine("Received " + received
+										+ " messages, stored " + stored);
+							db.addBatchToAck(txn, c, b.getId());
+							db.commitTransaction(txn);
+						} catch(DbException e) {
+							db.abortTransaction(txn);
+							throw e;
 						}
 					}
 				}
 			}
 		}
-		if(LOG.isLoggable(Level.FINE))
-			LOG.fine("Received " + batches + " batches");
+	}
+
+	private void retransmitLostBatches(ContactId c, BundleId b)
+	throws DbException {
 		// Find any lost batches that need to be retransmitted
 		Set<BatchId> lost;
 		synchronized(contactLock) {
@@ -444,7 +456,7 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 				synchronized(messageStatusLock) {
 					Txn txn = db.startTransaction();
 					try {
-						lost = db.addReceivedBundle(txn, c, h.getId());
+						lost = db.addReceivedBundle(txn, c, b);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
@@ -472,7 +484,6 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 				}
 			}
 		}
-		System.gc();
 	}
 
 	public void removeContact(ContactId c) throws DbException {
diff --git a/components/net/sf/briar/protocol/BatchBuilderImpl.java b/components/net/sf/briar/protocol/BatchBuilderImpl.java
new file mode 100644
index 0000000000..d1fabbeed4
--- /dev/null
+++ b/components/net/sf/briar/protocol/BatchBuilderImpl.java
@@ -0,0 +1,44 @@
+package net.sf.briar.protocol;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.briar.api.protocol.BatchBuilder;
+import net.sf.briar.api.protocol.Message;
+import net.sf.briar.api.serial.Writer;
+import net.sf.briar.api.serial.WriterFactory;
+
+abstract class BatchBuilderImpl implements BatchBuilder {
+
+	protected final List<Message> messages = new ArrayList<Message>();
+	protected final KeyPair keyPair;
+	protected final Signature signature;
+	protected final MessageDigest messageDigest;
+
+	private final WriterFactory writerFactory;
+
+	protected BatchBuilderImpl(KeyPair keyPair, Signature signature,
+			MessageDigest messageDigest, WriterFactory writerFactory) {
+		this.keyPair = keyPair;
+		this.signature = signature;
+		this.messageDigest = messageDigest;
+		this.writerFactory = writerFactory;
+	}
+
+	public void addMessage(Message m) {
+		messages.add(m);
+	}
+
+	protected byte[] getSignableRepresentation() throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		Writer w = writerFactory.createWriter(out);
+		w.writeList(messages);
+		w.close();
+		return out.toByteArray();
+	}
+}
diff --git a/components/net/sf/briar/protocol/BundleReader.java b/components/net/sf/briar/protocol/BundleReaderImpl.java
similarity index 84%
rename from components/net/sf/briar/protocol/BundleReader.java
rename to components/net/sf/briar/protocol/BundleReaderImpl.java
index c3bcdf0ca5..ea397abdf2 100644
--- a/components/net/sf/briar/protocol/BundleReader.java
+++ b/components/net/sf/briar/protocol/BundleReaderImpl.java
@@ -1,7 +1,7 @@
 package net.sf.briar.protocol;
 
 import java.io.IOException;
-import java.security.SignatureException;
+import java.security.GeneralSecurityException;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -10,7 +10,7 @@ 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.BundleReader;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.HeaderBuilder;
@@ -24,26 +24,32 @@ 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 {
+class BundleReaderImpl implements BundleReader {
 
 	private static enum State { START, FIRST_BATCH, MORE_BATCHES, END };
 
 	private final Reader r;
+	private final long size;
 	private final MessageParser messageParser;
 	private final Provider<HeaderBuilder> headerBuilderProvider;
 	private final Provider<BatchBuilder> batchBuilderProvider;
 	private State state = State.START;
 
-	BundleReader(Reader r, MessageParser messageParser,
+	BundleReaderImpl(Reader r, long size, MessageParser messageParser,
 			Provider<HeaderBuilder> headerBuilderProvider,
 			Provider<BatchBuilder> batchBuilderProvider) {
 		this.r = r;
+		this.size = size;
 		this.messageParser = messageParser;
 		this.headerBuilderProvider = headerBuilderProvider;
 		this.batchBuilderProvider = batchBuilderProvider;
 	}
 
-	public Header getHeader() throws IOException, SignatureException {
+	public long getSize() {
+		return size;
+	}
+
+	public Header getHeader() throws IOException, GeneralSecurityException {
 		if(state != State.START) throw new IllegalStateException();
 		r.setReadLimit(Header.MAX_SIZE);
 		Set<BatchId> acks = new HashSet<BatchId>();
@@ -69,7 +75,7 @@ abstract class BundleReader implements Bundle {
 		return h.build();
 	}
 
-	public Batch getNextBatch() throws IOException, SignatureException {
+	public Batch getNextBatch() throws IOException, GeneralSecurityException {
 		if(state == State.FIRST_BATCH) {
 			r.readListStart();
 			state = State.MORE_BATCHES;
@@ -91,4 +97,8 @@ abstract class BundleReader implements Bundle {
 		b.setSignature(sig);
 		return b.build();
 	}
+
+	public void close() throws IOException {
+		r.close();
+	}
 }
diff --git a/components/net/sf/briar/protocol/BundleWriter.java b/components/net/sf/briar/protocol/BundleWriterImpl.java
similarity index 86%
rename from components/net/sf/briar/protocol/BundleWriter.java
rename to components/net/sf/briar/protocol/BundleWriterImpl.java
index ccd5894752..ba3f3f8306 100644
--- a/components/net/sf/briar/protocol/BundleWriter.java
+++ b/components/net/sf/briar/protocol/BundleWriterImpl.java
@@ -4,14 +4,14 @@ 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.BundleWriter;
 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 {
+class BundleWriterImpl implements BundleWriter {
 
 	private static enum State { START, FIRST_BATCH, MORE_BATCHES, END };
 
@@ -19,7 +19,7 @@ abstract class BundleWriter implements BundleBuilder {
 	private final long capacity;
 	private State state = State.START;
 
-	BundleWriter(Writer w, long capacity) {
+	BundleWriterImpl(Writer w, long capacity) {
 		this.w = w;
 		this.capacity = capacity;
 	}
@@ -48,12 +48,12 @@ abstract class BundleWriter implements BundleBuilder {
 		}
 		if(state != State.MORE_BATCHES) throw new IllegalStateException();
 		w.writeListStart();
-		for(Message m : b.getMessages()) w.writeRaw(m.getBody());
+		for(Message m : b.getMessages()) w.writeRaw(m.getBytes());
 		w.writeListEnd();
 		w.writeRaw(b.getSignature());
 	}
 
-	void close() throws IOException {
+	public void close() throws IOException {
 		if(state == State.FIRST_BATCH) {
 			w.writeListStart();
 			state = State.MORE_BATCHES;
diff --git a/components/net/sf/briar/protocol/FileBundle.java b/components/net/sf/briar/protocol/FileBundle.java
deleted file mode 100644
index 930f8699fe..0000000000
--- a/components/net/sf/briar/protocol/FileBundle.java
+++ /dev/null
@@ -1,30 +0,0 @@
-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<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> 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
deleted file mode 100644
index 085f637f74..0000000000
--- a/components/net/sf/briar/protocol/FileBundleBuilder.java
+++ /dev/null
@@ -1,41 +0,0 @@
-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<HeaderBuilder> headerBuilderProvider;
-	private final Provider<BatchBuilder> batchBuilderProvider;
-
-	FileBundleBuilder(File file, long capacity, WriterFactory writerFactory,
-			ReaderFactory readerFactory, MessageParser messageParser,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> 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/HeaderBuilderImpl.java b/components/net/sf/briar/protocol/HeaderBuilderImpl.java
new file mode 100644
index 0000000000..78f19923a3
--- /dev/null
+++ b/components/net/sf/briar/protocol/HeaderBuilderImpl.java
@@ -0,0 +1,63 @@
+package net.sf.briar.protocol;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.HeaderBuilder;
+import net.sf.briar.api.serial.Writer;
+import net.sf.briar.api.serial.WriterFactory;
+
+abstract class HeaderBuilderImpl implements HeaderBuilder {
+
+	protected final List<BatchId> acks = new ArrayList<BatchId>();
+	protected final List<GroupId> subs = new ArrayList<GroupId>();
+	protected final Map<String, String> transports =
+		new LinkedHashMap<String, String>();
+
+	protected final KeyPair keyPair;
+	protected final Signature signature;
+	protected final MessageDigest messageDigest;
+
+	private final WriterFactory writerFactory;
+
+	protected HeaderBuilderImpl(KeyPair keyPair, Signature signature,
+			MessageDigest messageDigest, WriterFactory writerFactory) {
+		this.keyPair = keyPair;
+		this.signature = signature;
+		this.messageDigest = messageDigest;
+		this.writerFactory = writerFactory;
+	}
+
+	public void addAcks(Iterable<BatchId> acks) {
+		for(BatchId ack : acks) this.acks.add(ack);
+	}
+
+	public void addSubscriptions(Iterable<GroupId> subs) {
+		for(GroupId sub : subs) this.subs.add(sub);
+	}
+
+	public void addTransports(Map<String, String> transports) {
+		for(String key : transports.keySet()) {
+			this.transports.put(key, transports.get(key));
+		}
+	}
+
+	protected byte[] getSignableRepresentation() throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		Writer w = writerFactory.createWriter(out);
+		w.writeList(acks);
+		w.writeList(subs);
+		w.writeMap(transports);
+		w.close();
+		return out.toByteArray();
+	}
+}
diff --git a/components/net/sf/briar/protocol/IncomingBatchBuilder.java b/components/net/sf/briar/protocol/IncomingBatchBuilder.java
new file mode 100644
index 0000000000..5cce8fa658
--- /dev/null
+++ b/components/net/sf/briar/protocol/IncomingBatchBuilder.java
@@ -0,0 +1,40 @@
+package net.sf.briar.protocol;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.security.SignatureException;
+
+import net.sf.briar.api.protocol.Batch;
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.serial.WriterFactory;
+
+public class IncomingBatchBuilder extends BatchBuilderImpl {
+
+	IncomingBatchBuilder(KeyPair keyPair, Signature signature,
+			MessageDigest messageDigest, WriterFactory writerFactory) {
+		super(keyPair, signature, messageDigest, writerFactory);
+	}
+
+	private byte[] sig = null;
+
+	public void setSignature(byte[] sig) {
+		this.sig = sig;
+	}
+
+	public Batch build() throws IOException, SignatureException,
+	InvalidKeyException {
+		if(sig == null) throw new IllegalStateException();
+		byte[] raw = getSignableRepresentation();
+		signature.initVerify(keyPair.getPublic());
+		signature.update(raw);
+		signature.verify(sig);
+		messageDigest.reset();
+		messageDigest.update(raw);
+		messageDigest.update(sig);
+		byte[] hash = messageDigest.digest();
+		return new BatchImpl(new BatchId(hash), raw.length, messages, sig);
+	}
+}
diff --git a/components/net/sf/briar/protocol/IncomingHeaderBuilder.java b/components/net/sf/briar/protocol/IncomingHeaderBuilder.java
new file mode 100644
index 0000000000..04371f8497
--- /dev/null
+++ b/components/net/sf/briar/protocol/IncomingHeaderBuilder.java
@@ -0,0 +1,47 @@
+package net.sf.briar.protocol;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.HashSet;
+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;
+import net.sf.briar.api.serial.WriterFactory;
+
+class IncomingHeaderBuilder extends HeaderBuilderImpl {
+
+	private byte[] sig = null;
+
+	IncomingHeaderBuilder(KeyPair keyPair, Signature signature,
+			MessageDigest messageDigest, WriterFactory writerFactory) {
+		super(keyPair, signature, messageDigest, writerFactory);
+	}
+
+	public void setSignature(byte[] sig) {
+		this.sig = sig;
+	}
+
+	public Header build() throws IOException, SignatureException,
+	InvalidKeyException {
+		if(sig == null) throw new IllegalStateException();
+		byte[] raw = getSignableRepresentation();
+		signature.initVerify(keyPair.getPublic());
+		signature.update(raw);
+		signature.verify(sig);
+		messageDigest.reset();
+		messageDigest.update(raw);
+		messageDigest.update(sig);
+		byte[] hash = messageDigest.digest();
+		Set<BatchId> ackSet = new HashSet<BatchId>(acks);
+		Set<GroupId> subSet = new HashSet<GroupId>(subs);
+		return new HeaderImpl(new BundleId(hash), raw.length, ackSet, subSet,
+				transports, sig);
+	}
+}
diff --git a/components/net/sf/briar/protocol/MessageImpl.java b/components/net/sf/briar/protocol/MessageImpl.java
index 64306e4e44..3fcbc273b9 100644
--- a/components/net/sf/briar/protocol/MessageImpl.java
+++ b/components/net/sf/briar/protocol/MessageImpl.java
@@ -48,7 +48,7 @@ public class MessageImpl implements Message {
 		return body.length;
 	}
 
-	public byte[] getBody() {
+	public byte[] getBytes() {
 		return body;
 	}
 
diff --git a/components/net/sf/briar/protocol/OutgoingBatchBuilder.java b/components/net/sf/briar/protocol/OutgoingBatchBuilder.java
new file mode 100644
index 0000000000..1f06d42539
--- /dev/null
+++ b/components/net/sf/briar/protocol/OutgoingBatchBuilder.java
@@ -0,0 +1,37 @@
+package net.sf.briar.protocol;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.security.SignatureException;
+
+import net.sf.briar.api.protocol.Batch;
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.serial.WriterFactory;
+
+public class OutgoingBatchBuilder extends BatchBuilderImpl {
+
+	OutgoingBatchBuilder(KeyPair keyPair, Signature signature,
+			MessageDigest messageDigest, WriterFactory writerFactory) {
+		super(keyPair, signature, messageDigest, writerFactory);
+	}
+
+	public void setSignature(byte[] sig) {
+		throw new UnsupportedOperationException();
+	}
+
+	public Batch build() throws IOException, SignatureException,
+	InvalidKeyException {
+		byte[] raw = getSignableRepresentation();
+		signature.initSign(keyPair.getPrivate());
+		signature.update(raw);
+		byte[] sig = signature.sign();
+		messageDigest.reset();
+		messageDigest.update(raw);
+		messageDigest.update(sig);
+		byte[] hash = messageDigest.digest();
+		return new BatchImpl(new BatchId(hash), raw.length, messages, sig);
+	}
+}
diff --git a/components/net/sf/briar/protocol/OutgoingHeaderBuilder.java b/components/net/sf/briar/protocol/OutgoingHeaderBuilder.java
new file mode 100644
index 0000000000..268ec5cc69
--- /dev/null
+++ b/components/net/sf/briar/protocol/OutgoingHeaderBuilder.java
@@ -0,0 +1,44 @@
+package net.sf.briar.protocol;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.HashSet;
+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;
+import net.sf.briar.api.serial.WriterFactory;
+
+public class OutgoingHeaderBuilder extends HeaderBuilderImpl {
+
+	OutgoingHeaderBuilder(KeyPair keyPair, Signature signature,
+			MessageDigest messageDigest, WriterFactory writerFactory) {
+		super(keyPair, signature, messageDigest, writerFactory);
+	}
+
+	public void setSignature(byte[] sig) {
+		throw new UnsupportedOperationException();
+	}
+
+	public Header build() throws IOException, SignatureException,
+	InvalidKeyException {
+		byte[] raw = getSignableRepresentation();
+		signature.initSign(keyPair.getPrivate());
+		signature.update(raw);
+		byte[] sig = signature.sign();
+		messageDigest.reset();
+		messageDigest.update(raw);
+		messageDigest.update(sig);
+		byte[] hash = messageDigest.digest();
+		Set<BatchId> ackSet = new HashSet<BatchId>(acks);
+		Set<GroupId> subSet = new HashSet<GroupId>(subs);
+		return new HeaderImpl(new BundleId(hash), raw.length, ackSet, subSet,
+				transports, sig);
+	}
+}
diff --git a/components/net/sf/briar/serial/ReaderFactoryImpl.java b/components/net/sf/briar/serial/ReaderFactoryImpl.java
index dd2a1a4da6..2f52c03e2e 100644
--- a/components/net/sf/briar/serial/ReaderFactoryImpl.java
+++ b/components/net/sf/briar/serial/ReaderFactoryImpl.java
@@ -5,7 +5,7 @@ import java.io.InputStream;
 import net.sf.briar.api.serial.Reader;
 import net.sf.briar.api.serial.ReaderFactory;
 
-class ReaderFactoryImpl implements ReaderFactory {
+public class ReaderFactoryImpl implements ReaderFactory {
 
 	public Reader createReader(InputStream in) {
 		return new ReaderImpl(in);
diff --git a/components/net/sf/briar/serial/ReaderImpl.java b/components/net/sf/briar/serial/ReaderImpl.java
index a704334158..b98383e0e6 100644
--- a/components/net/sf/briar/serial/ReaderImpl.java
+++ b/components/net/sf/briar/serial/ReaderImpl.java
@@ -53,6 +53,10 @@ class ReaderImpl implements Reader {
 		readLimit = 0L;
 	}
 
+	public void close() throws IOException {
+		in.close();
+	}
+
 	public boolean hasBoolean() throws IOException {
 		if(!started) readNext(true);
 		if(eof) return false;
diff --git a/components/net/sf/briar/serial/WriterFactoryImpl.java b/components/net/sf/briar/serial/WriterFactoryImpl.java
index 63563fdf34..de01ca18f4 100644
--- a/components/net/sf/briar/serial/WriterFactoryImpl.java
+++ b/components/net/sf/briar/serial/WriterFactoryImpl.java
@@ -5,7 +5,7 @@ import java.io.OutputStream;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 
-class WriterFactoryImpl implements WriterFactory {
+public class WriterFactoryImpl implements WriterFactory {
 
 	public Writer createWriter(OutputStream out) {
 		return new WriterImpl(out);
diff --git a/components/net/sf/briar/serial/WriterImpl.java b/components/net/sf/briar/serial/WriterImpl.java
index eb20d6b029..18f1760780 100644
--- a/components/net/sf/briar/serial/WriterImpl.java
+++ b/components/net/sf/briar/serial/WriterImpl.java
@@ -18,6 +18,11 @@ class WriterImpl implements Writer {
 		this.out = out;
 	}
 
+	public void close() throws IOException {
+		out.flush();
+		out.close();
+	}
+
 	public void writeBoolean(boolean b) throws IOException {
 		if(b) out.write(Tag.TRUE);
 		else out.write(Tag.FALSE);
@@ -156,9 +161,4 @@ class WriterImpl implements Writer {
 	public void writeNull() throws IOException {
 		out.write(Tag.NULL);
 	}
-
-	public void close() throws IOException {
-		out.flush();
-		out.close();
-	}
 }
diff --git a/test/build.xml b/test/build.xml
index f6a2653a30..00bd9fb3ef 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -21,6 +21,7 @@
 			<test name='net.sf.briar.i18n.I18nTest'/>
 			<test name='net.sf.briar.invitation.InvitationWorkerTest'/>
 			<test name='net.sf.briar.protocol.BundleReaderTest'/>
+			<test name='net.sf.briar.protocol.BundleReadWriteTest'/>
 			<test name='net.sf.briar.protocol.BundleWriterTest'/>
 			<test name='net.sf.briar.serial.ReaderImplTest'/>
 			<test name='net.sf.briar.serial.WriterImplTest'/>
diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java
index 174c3a6afd..fc8fa86343 100644
--- a/test/net/sf/briar/db/DatabaseComponentTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentTest.java
@@ -1,7 +1,5 @@
 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;
@@ -18,9 +16,9 @@ import net.sf.briar.api.protocol.AuthorId;
 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.BundleBuilder;
 import net.sf.briar.api.protocol.BundleId;
+import net.sf.briar.api.protocol.BundleReader;
+import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.HeaderBuilder;
@@ -507,7 +505,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 
 	@Test
 	public void testGenerateBundleThrowsExceptionIfContactIsMissing()
-	throws DbException, IOException, SignatureException {
+	throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -518,7 +516,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Provider<BatchBuilder> batchBuilderProvider =
 			context.mock(Provider.class, "batchBuilderProvider");
-		final BundleBuilder bundleBuilder = context.mock(BundleBuilder.class);
+		final BundleWriter bundleBuilder = context.mock(BundleWriter.class);
 		context.checking(new Expectations() {{
 			// Check that the contact is still in the DB
 			oneOf(database).startTransaction();
@@ -539,8 +537,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 	}
 
 	@Test
-	public void testGenerateBundle() throws DbException, IOException,
-	SignatureException {
+	public void testGenerateBundle() throws Exception {
 		final long headerSize = 1234L;
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
@@ -552,12 +549,11 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Provider<BatchBuilder> batchBuilderProvider =
 			context.mock(Provider.class, "batchBuilderProvider");
-		final BundleBuilder bundleBuilder = context.mock(BundleBuilder.class);
+		final BundleWriter bundleWriter = context.mock(BundleWriter.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);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -582,11 +578,11 @@ public abstract class DatabaseComponentTest extends TestCase {
 			// Build the header
 			oneOf(headerBuilder).build();
 			will(returnValue(header));
-			oneOf(bundleBuilder).getCapacity();
+			oneOf(bundleWriter).getCapacity();
 			will(returnValue(1024L * 1024L));
 			oneOf(header).getSize();
 			will(returnValue(headerSize));
-			oneOf(bundleBuilder).addHeader(header);
+			oneOf(bundleWriter).addHeader(header);
 			// Add a batch to the bundle
 			oneOf(database).getSendableMessages(txn, contactId,
 					Batch.MAX_SIZE - headerSize);
@@ -604,25 +600,24 @@ public abstract class DatabaseComponentTest extends TestCase {
 			oneOf(database).addOutstandingBatch(
 					txn, contactId, batchId, messages);
 			// Add the batch to the bundle
-			oneOf(bundleBuilder).addBatch(batch);
+			oneOf(bundleWriter).addBatch(batch);
 			// Check whether to add another batch
 			oneOf(batch).getSize();
 			will(returnValue((long) message.getSize()));
 			// No, just send the bundle
-			oneOf(bundleBuilder).build();
-			will(returnValue(bundle));
+			oneOf(bundleWriter).close();
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				headerBuilderProvider, batchBuilderProvider);
 
-		db.generateBundle(contactId, bundleBuilder);
+		db.generateBundle(contactId, bundleWriter);
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testReceiveBundleThrowsExceptionIfContactIsMissing()
-	throws DbException, IOException, SignatureException {
+	throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -633,7 +628,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Provider<BatchBuilder> batchBuilderProvider =
 			context.mock(Provider.class, "batchBuilderProvider");
-		final Bundle bundle = context.mock(Bundle.class);
+		final BundleReader bundleReader = context.mock(BundleReader.class);
 		context.checking(new Expectations() {{
 			// Check that the contact is still in the DB
 			oneOf(database).startTransaction();
@@ -646,7 +641,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 				headerBuilderProvider, batchBuilderProvider);
 
 		try {
-			db.receiveBundle(contactId, bundle);
+			db.receiveBundle(contactId, bundleReader);
 			assertTrue(false);
 		} catch(NoSuchContactException expected) {}
 
@@ -654,8 +649,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 	}
 
 	@Test
-	public void testReceivedBundle() throws DbException, IOException,
-	SignatureException {
+	public void testReceiveBundle() throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -666,7 +660,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Provider<BatchBuilder> batchBuilderProvider =
 			context.mock(Provider.class, "batchBuilderProvider");
-		final Bundle bundle = context.mock(Bundle.class);
+		final BundleReader bundleReader = context.mock(BundleReader.class);
 		final Header header = context.mock(Header.class);
 		final Batch batch = context.mock(Batch.class);
 		context.checking(new Expectations() {{
@@ -676,7 +670,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// Header
-			oneOf(bundle).getHeader();
+			oneOf(bundleReader).getHeader();
 			will(returnValue(header));
 			// Acks
 			oneOf(header).getAcks();
@@ -692,7 +686,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(transports));
 			oneOf(database).setTransports(txn, contactId, transports);
 			// Batches
-			oneOf(bundle).getNextBatch();
+			oneOf(bundleReader).getNextBatch();
 			will(returnValue(batch));
 			oneOf(batch).getMessages();
 			will(returnValue(Collections.singleton(message)));
@@ -706,8 +700,9 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(batchId));
 			oneOf(database).addBatchToAck(txn, contactId, batchId);
 			// Any more batches? Nope
-			oneOf(bundle).getNextBatch();
+			oneOf(bundleReader).getNextBatch();
 			will(returnValue(null));
+			oneOf(bundleReader).close();
 			// Lost batches
 			oneOf(header).getId();
 			will(returnValue(bundleId));
@@ -718,7 +713,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				headerBuilderProvider, batchBuilderProvider);
 
-		db.receiveBundle(contactId, bundle);
+		db.receiveBundle(contactId, bundleReader);
 
 		context.assertIsSatisfied();
 	}
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
index f89dbc974f..9596a1930d 100644
--- a/test/net/sf/briar/db/H2DatabaseTest.java
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -110,7 +110,7 @@ public class H2DatabaseTest extends TestCase {
 		assertEquals(authorId, m1.getAuthor());
 		assertEquals(timestamp, m1.getTimestamp());
 		assertEquals(size, m1.getSize());
-		assertTrue(Arrays.equals(body, m1.getBody()));
+		assertTrue(Arrays.equals(body, m1.getBytes()));
 		// Delete the records
 		db.removeContact(txn, contactId);
 		db.removeMessage(txn, messageId);
diff --git a/test/net/sf/briar/protocol/BundleReadWriteTest.java b/test/net/sf/briar/protocol/BundleReadWriteTest.java
new file mode 100644
index 0000000000..52b4824133
--- /dev/null
+++ b/test/net/sf/briar/protocol/BundleReadWriteTest.java
@@ -0,0 +1,156 @@
+package net.sf.briar.protocol;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.SignatureException;
+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.AuthorId;
+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.BundleReader;
+import net.sf.briar.api.protocol.BundleWriter;
+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.api.protocol.MessageParser;
+import net.sf.briar.api.protocol.UniqueId;
+import net.sf.briar.api.serial.FormatException;
+import net.sf.briar.api.serial.Reader;
+import net.sf.briar.api.serial.Writer;
+import net.sf.briar.api.serial.WriterFactory;
+import net.sf.briar.serial.ReaderFactoryImpl;
+import net.sf.briar.serial.WriterFactoryImpl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Provider;
+
+public class BundleReadWriteTest extends TestCase {
+
+	private static final String SIGNATURE_ALGO = "SHA256withRSA";
+	private static final String KEY_PAIR_ALGO = "RSA";
+	private static final String DIGEST_ALGO = "SHA-256";
+
+	private final File testDir = TestUtils.getTestDirectory();
+	private final File bundle = new File(testDir, "bundle");
+
+	private final long capacity = 1024L;
+	private final BatchId ack = new BatchId(TestUtils.getRandomId());
+	private final Set<BatchId> acks = Collections.singleton(ack);
+	private final GroupId sub = new GroupId(TestUtils.getRandomId());
+	private final Set<GroupId> subs = Collections.singleton(sub);
+	private final Map<String, String> transports =
+		Collections.singletonMap("foo", "bar");
+
+	private final MessageId messageId = new MessageId(TestUtils.getRandomId());
+	private final AuthorId authorId = new AuthorId(TestUtils.getRandomId());
+	private final long timestamp = System.currentTimeMillis();
+	private final byte[] messageBody = new byte[123];
+	private final Message message = new MessageImpl(messageId, MessageId.NONE,
+			sub, authorId, timestamp, messageBody);
+
+	// FIXME: This test should not depend on an impl in another component
+	private final WriterFactory wf = new WriterFactoryImpl();
+
+	private final KeyPair keyPair;
+	private final Signature sig;
+	private final MessageDigest digest;
+
+	public BundleReadWriteTest() throws NoSuchAlgorithmException {
+		super();
+		keyPair = KeyPairGenerator.getInstance(KEY_PAIR_ALGO).generateKeyPair();
+		sig = Signature.getInstance(SIGNATURE_ALGO);
+		digest = MessageDigest.getInstance(DIGEST_ALGO);
+		assertEquals(digest.getDigestLength(), UniqueId.LENGTH);
+	}
+
+	@Before
+	public void setUp() {
+		testDir.mkdirs();
+	}
+
+	@Test
+	public void testWriteBundle() throws Exception {
+		HeaderBuilder h = new OutgoingHeaderBuilder(keyPair, sig, digest, wf);
+		h.addAcks(acks);
+		h.addSubscriptions(subs);
+		h.addTransports(transports);
+		Header header = h.build();
+
+		BatchBuilder b = new OutgoingBatchBuilder(keyPair, sig, digest, wf);
+		b.addMessage(message);
+		Batch batch = b.build();
+
+		FileOutputStream out = new FileOutputStream(bundle);
+		Writer writer = new WriterFactoryImpl().createWriter(out);
+		BundleWriter w = new BundleWriterImpl(writer, capacity);
+
+		w.addHeader(header);
+		w.addBatch(batch);
+		w.close();
+
+		assertTrue(bundle.exists());
+		assertTrue(bundle.length() > messageBody.length);
+	}
+
+	@Test
+	public void testWriteAndReadBundle() throws Exception {
+
+		testWriteBundle();
+
+		MessageParser messageParser = new MessageParser() {
+			public Message parseMessage(byte[] body) throws FormatException,
+			SignatureException {
+				// FIXME: Really parse the message
+				return message;
+			}
+		};
+		Provider<HeaderBuilder> headerBuilderProvider =
+			new Provider<HeaderBuilder>() {
+			public HeaderBuilder get() {
+				return new IncomingHeaderBuilder(keyPair, sig, digest, wf);
+			}
+		};
+		Provider<BatchBuilder> batchBuilderProvider =
+			new Provider<BatchBuilder>() {
+			public BatchBuilder get() {
+				return new IncomingBatchBuilder(keyPair, sig, digest, wf);
+			}
+		};
+
+		FileInputStream in = new FileInputStream(bundle);
+		Reader reader = new ReaderFactoryImpl().createReader(in);
+		BundleReader r = new BundleReaderImpl(reader, bundle.length(),
+				messageParser, headerBuilderProvider, batchBuilderProvider);
+
+		Header h = r.getHeader();
+		assertEquals(acks, h.getAcks());
+		assertEquals(subs, h.getSubscriptions());
+		assertEquals(transports, h.getTransports());
+		Batch b = r.getNextBatch();
+		assertEquals(Collections.singletonList(message), b.getMessages());
+		assertNull(r.getNextBatch());
+		r.close();
+	}
+
+	@After
+	public void tearDown() {
+		TestUtils.deleteTestDirectory(testDir);
+	}
+}
diff --git a/test/net/sf/briar/protocol/BundleReaderTest.java b/test/net/sf/briar/protocol/BundleReaderTest.java
index b0a5f5d012..1923416f4b 100644
--- a/test/net/sf/briar/protocol/BundleReaderTest.java
+++ b/test/net/sf/briar/protocol/BundleReaderTest.java
@@ -12,6 +12,7 @@ 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.BundleReader;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.HeaderBuilder;
@@ -46,7 +47,7 @@ public class BundleReaderTest extends TestCase {
 	private final byte[] batchSig = TestUtils.getRandomId();
 
 	@Test
-	public void testGetHeader() throws IOException, SignatureException {
+	public void testGetHeader() throws Exception {
 		Mockery context = new Mockery();
 		final Reader reader = context.mock(Reader.class);
 		final MessageParser messageParser = context.mock(MessageParser.class);
@@ -82,7 +83,7 @@ public class BundleReaderTest extends TestCase {
 			oneOf(headerBuilder).build();
 			will(returnValue(header));
 		}});
-		BundleReader r = createBundleReader(reader, messageParser,
+		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
 				headerBuilderProvider, batchBuilderProvider);
 
 		assertEquals(header, r.getHeader());
@@ -91,8 +92,7 @@ public class BundleReaderTest extends TestCase {
 	}
 
 	@Test
-	public void testBatchBeforeHeaderThrowsException() throws IOException,
-	SignatureException {
+	public void testBatchBeforeHeaderThrowsException() throws Exception {
 		Mockery context = new Mockery();
 		final Reader reader = context.mock(Reader.class);
 		final MessageParser messageParser = context.mock(MessageParser.class);
@@ -102,7 +102,7 @@ public class BundleReaderTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Provider<BatchBuilder> batchBuilderProvider =
 			context.mock(Provider.class, "batchBuilderProvider");
-		BundleReader r = createBundleReader(reader, messageParser,
+		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
 				headerBuilderProvider, batchBuilderProvider);
 
 		try {
@@ -114,8 +114,30 @@ public class BundleReaderTest extends TestCase {
 	}
 
 	@Test
-	public void testGetHeaderNoBatches() throws IOException,
+	public void testCloseBeforeHeaderDoesNotThrowException() 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<HeaderBuilder> headerBuilderProvider =
+			context.mock(Provider.class);
+		@SuppressWarnings("unchecked")
+		final Provider<BatchBuilder> batchBuilderProvider =
+			context.mock(Provider.class, "batchBuilderProvider");
+		context.checking(new Expectations() {{
+			oneOf(reader).close();
+		}});
+		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
+				headerBuilderProvider, batchBuilderProvider);
+
+		r.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testGetHeaderNoBatches() throws Exception {
 		Mockery context = new Mockery();
 		final Reader reader = context.mock(Reader.class);
 		final MessageParser messageParser = context.mock(MessageParser.class);
@@ -155,19 +177,21 @@ public class BundleReaderTest extends TestCase {
 			oneOf(reader).hasListEnd();
 			will(returnValue(true));
 			oneOf(reader).readListEnd();
+			// Close
+			oneOf(reader).close();
 		}});
-		BundleReader r = createBundleReader(reader, messageParser,
+		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
 				headerBuilderProvider, batchBuilderProvider);
 
 		assertEquals(header, r.getHeader());
 		assertNull(r.getNextBatch());
+		r.close();
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testGetHeaderOneBatch() throws IOException,
-	SignatureException {
+	public void testGetHeaderOneBatch() throws Exception {
 		Mockery context = new Mockery();
 		final Reader reader = context.mock(Reader.class);
 		final MessageParser messageParser = context.mock(MessageParser.class);
@@ -226,26 +250,17 @@ public class BundleReaderTest extends TestCase {
 			oneOf(reader).hasListEnd();
 			will(returnValue(true));
 			oneOf(reader).readListEnd();
+			// Close
+			oneOf(reader).close();
 		}});
-		BundleReader r = createBundleReader(reader, messageParser,
+		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
 				headerBuilderProvider, batchBuilderProvider);
 
 		assertEquals(header, r.getHeader());
 		assertEquals(batch, r.getNextBatch());
 		assertNull(r.getNextBatch());
+		r.close();
 
 		context.assertIsSatisfied();
 	}
-
-	private BundleReader createBundleReader(Reader reader,
-			MessageParser messageParser,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> 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
index a2e0253003..2ba6fedd38 100644
--- a/test/net/sf/briar/protocol/BundleWriterTest.java
+++ b/test/net/sf/briar/protocol/BundleWriterTest.java
@@ -9,7 +9,7 @@ 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.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.Message;
@@ -21,6 +21,7 @@ import org.junit.Test;
 
 public class BundleWriterTest extends TestCase {
 
+	private final long capacity = 1024L * 1024L;
 	private final BatchId ack = new BatchId(TestUtils.getRandomId());
 	private final Set<BatchId> acks = Collections.singleton(ack);
 	private final GroupId sub = new GroupId(TestUtils.getRandomId());
@@ -28,7 +29,6 @@ public class BundleWriterTest extends TestCase {
 	private final Map<String, String> 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();
 
@@ -59,7 +59,7 @@ public class BundleWriterTest extends TestCase {
 			will(returnValue(headerSig));
 			oneOf(writer).writeRaw(headerSig);
 		}});
-		BundleWriter w = createBundleWriter(writer);
+		BundleWriter w = new BundleWriterImpl(writer, capacity);
 
 		w.addHeader(header);
 
@@ -91,7 +91,7 @@ public class BundleWriterTest extends TestCase {
 			will(returnValue(headerSig));
 			oneOf(writer).writeRaw(headerSig);
 		}});
-		BundleWriter w = createBundleWriter(writer);
+		BundleWriter w = new BundleWriterImpl(writer, capacity);
 
 		w.addHeader(header);
 
@@ -103,7 +103,7 @@ public class BundleWriterTest extends TestCase {
 		Mockery context = new Mockery();
 		final Writer writer = context.mock(Writer.class);
 		final Batch batch = context.mock(Batch.class);
-		BundleWriter w = createBundleWriter(writer);
+		BundleWriter w = new BundleWriterImpl(writer, capacity);
 
 		try {
 			w.addBatch(batch);
@@ -117,7 +117,7 @@ public class BundleWriterTest extends TestCase {
 	public void testCloseBeforeHeaderThrowsException() throws IOException {
 		Mockery context = new Mockery();
 		final Writer writer = context.mock(Writer.class);
-		BundleWriter w = createBundleWriter(writer);
+		BundleWriter w = new BundleWriterImpl(writer, capacity);
 
 		try {
 			w.close();
@@ -159,7 +159,7 @@ public class BundleWriterTest extends TestCase {
 			oneOf(writer).writeListEnd();
 			oneOf(writer).close();
 		}});
-		BundleWriter w = createBundleWriter(writer);
+		BundleWriter w = new BundleWriterImpl(writer, capacity);
 
 		w.addHeader(header);
 		w.close();
@@ -200,7 +200,7 @@ public class BundleWriterTest extends TestCase {
 			oneOf(writer).writeListStart();
 			oneOf(batch).getMessages();
 			will(returnValue(Collections.singleton(message)));
-			oneOf(message).getBody();
+			oneOf(message).getBytes();
 			will(returnValue(messageBody));
 			oneOf(writer).writeRaw(messageBody);
 			oneOf(writer).writeListEnd();
@@ -211,7 +211,7 @@ public class BundleWriterTest extends TestCase {
 			oneOf(writer).writeListStart();
 			oneOf(batch).getMessages();
 			will(returnValue(Collections.singleton(message)));
-			oneOf(message).getBody();
+			oneOf(message).getBytes();
 			will(returnValue(messageBody));
 			oneOf(writer).writeRaw(messageBody);
 			oneOf(writer).writeListEnd();
@@ -222,7 +222,7 @@ public class BundleWriterTest extends TestCase {
 			oneOf(writer).writeListEnd();
 			oneOf(writer).close();
 		}});
-		BundleWriter w = createBundleWriter(writer);
+		BundleWriter w = new BundleWriterImpl(writer, capacity);
 
 		w.addHeader(header);
 		w.addBatch(batch);
@@ -231,12 +231,4 @@ public class BundleWriterTest extends TestCase {
 
 		context.assertIsSatisfied();
 	}
-
-	private BundleWriter createBundleWriter(Writer writer) {
-		return new BundleWriter(writer, capacity) {
-			public Bundle build() throws IOException {
-				return null;
-			}
-		};
-	}
 }
-- 
GitLab