From 6764ade4750e43643a0d1038e62c7aef9c3b915a Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Tue, 19 Nov 2013 18:05:44 +0000
Subject: [PATCH] Delimited structs - this will allow us to skip unrecognised
 structs.

---
 .../src/net/sf/briar/api/serial/Reader.java   |  12 +-
 .../sf/briar/api/serial/SerialComponent.java  |   6 +-
 .../src/net/sf/briar/api/serial/Writer.java   |   5 +-
 .../sf/briar/messaging/AuthorFactoryImpl.java |   3 +-
 .../net/sf/briar/messaging/AuthorReader.java  |   7 +-
 .../sf/briar/messaging/GroupFactoryImpl.java  |   3 +-
 .../net/sf/briar/messaging/GroupReader.java   |   3 +-
 .../briar/messaging/MessageFactoryImpl.java   |  11 +-
 .../net/sf/briar/messaging/MessageReader.java |   8 +-
 .../sf/briar/messaging/PacketReaderImpl.java  |  52 +++++--
 .../sf/briar/messaging/PacketWriterImpl.java  |  54 ++++---
 .../messaging/SubscriptionUpdateReader.java   |   9 +-
 .../src/net/sf/briar/serial/ReaderImpl.java   | 136 ++++++++----------
 .../sf/briar/serial/SerialComponentImpl.java  |  24 ++--
 briar-core/src/net/sf/briar/serial/Tag.java   |  30 ++--
 .../src/net/sf/briar/serial/WriterImpl.java   |  14 +-
 .../briar/messaging/PacketReaderImplTest.java |  29 ++--
 .../briar/messaging/PacketWriterImplTest.java |   8 +-
 .../net/sf/briar/serial/ReaderImplTest.java   |  78 ++++++----
 .../net/sf/briar/serial/WriterImplTest.java   |  30 ++--
 20 files changed, 304 insertions(+), 218 deletions(-)

diff --git a/briar-api/src/net/sf/briar/api/serial/Reader.java b/briar-api/src/net/sf/briar/api/serial/Reader.java
index bdb0c3eff6..c81826f15a 100644
--- a/briar-api/src/net/sf/briar/api/serial/Reader.java
+++ b/briar-api/src/net/sf/briar/api/serial/Reader.java
@@ -40,30 +40,28 @@ public interface Reader {
 	double readFloat64() throws IOException;
 
 	boolean hasString() throws IOException;
-	String readString() throws IOException;
 	String readString(int maxLength) throws IOException;
 
 	boolean hasBytes() throws IOException;
-	byte[] readBytes() throws IOException;
 	byte[] readBytes(int maxLength) throws IOException;
 
 	boolean hasList() throws IOException;
 	<E> List<E> readList(Class<E> e) throws IOException;
-	boolean hasListStart() throws IOException;
 	void readListStart() throws IOException;
 	boolean hasListEnd() throws IOException;
 	void readListEnd() throws IOException;
 
 	boolean hasMap() throws IOException;
 	<K, V> Map<K, V> readMap(Class<K> k, Class<V> v) throws IOException;
-	boolean hasMapStart() throws IOException;
 	void readMapStart() throws IOException;
 	boolean hasMapEnd() throws IOException;
 	void readMapEnd() throws IOException;
 
+	boolean hasStruct(int id) throws IOException;
+	void readStructStart(int id) throws IOException;
+	boolean hasStructEnd() throws IOException;
+	void readStructEnd() throws IOException;
+
 	boolean hasNull() throws IOException;
 	void readNull() throws IOException;
-
-	boolean hasStruct(int id) throws IOException;
-	void readStructId(int id) throws IOException;
 }
diff --git a/briar-api/src/net/sf/briar/api/serial/SerialComponent.java b/briar-api/src/net/sf/briar/api/serial/SerialComponent.java
index ae0c2f106a..0666a88db6 100644
--- a/briar-api/src/net/sf/briar/api/serial/SerialComponent.java
+++ b/briar-api/src/net/sf/briar/api/serial/SerialComponent.java
@@ -2,11 +2,13 @@ package net.sf.briar.api.serial;
 
 public interface SerialComponent {
 
+	int getSerialisedListStartLength();
+
 	int getSerialisedListEndLength();
 
-	int getSerialisedListStartLength();
+	int getSerialisedStructStartLength(int id);
 
-	int getSerialisedStructIdLength(int id);
+	int getSerialisedStructEndLength();
 
 	int getSerialisedUniqueIdLength();
 }
diff --git a/briar-api/src/net/sf/briar/api/serial/Writer.java b/briar-api/src/net/sf/briar/api/serial/Writer.java
index d13b711c61..ad2d338a43 100644
--- a/briar-api/src/net/sf/briar/api/serial/Writer.java
+++ b/briar-api/src/net/sf/briar/api/serial/Writer.java
@@ -35,7 +35,8 @@ public interface Writer {
 	void writeMapStart() throws IOException;
 	void writeMapEnd() throws IOException;
 
-	void writeNull() throws IOException;
+	void writeStructStart(int id) throws IOException;
+	void writeStructEnd() throws IOException;
 
-	void writeStructId(int id) throws IOException;
+	void writeNull() throws IOException;
 }
diff --git a/briar-core/src/net/sf/briar/messaging/AuthorFactoryImpl.java b/briar-core/src/net/sf/briar/messaging/AuthorFactoryImpl.java
index 67cfde1a17..8dc4ba94c9 100644
--- a/briar-core/src/net/sf/briar/messaging/AuthorFactoryImpl.java
+++ b/briar-core/src/net/sf/briar/messaging/AuthorFactoryImpl.java
@@ -41,9 +41,10 @@ class AuthorFactoryImpl implements AuthorFactory {
 	private AuthorId getId(String name, byte[] publicKey) throws IOException {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(AUTHOR);
+		w.writeStructStart(AUTHOR);
 		w.writeString(name);
 		w.writeBytes(publicKey);
+		w.writeStructEnd();
 		MessageDigest messageDigest = crypto.getMessageDigest();
 		messageDigest.update(out.toByteArray());
 		return new AuthorId(messageDigest.digest());
diff --git a/briar-core/src/net/sf/briar/messaging/AuthorReader.java b/briar-core/src/net/sf/briar/messaging/AuthorReader.java
index a2123b4378..3414b8aea6 100644
--- a/briar-core/src/net/sf/briar/messaging/AuthorReader.java
+++ b/briar-core/src/net/sf/briar/messaging/AuthorReader.java
@@ -23,12 +23,15 @@ class AuthorReader implements StructReader<Author> {
 	}
 
 	public Author readStruct(Reader r) throws IOException {
+		// Set up the reader
 		DigestingConsumer digesting = new DigestingConsumer(messageDigest);
-		// Read and digest the data
 		r.addConsumer(digesting);
-		r.readStructId(AUTHOR);
+		// Read and digest the data
+		r.readStructStart(AUTHOR);
 		String name = r.readString(MAX_AUTHOR_NAME_LENGTH);
 		byte[] publicKey = r.readBytes(MAX_PUBLIC_KEY_LENGTH);
+		r.readStructEnd();
+		// Reset the reader
 		r.removeConsumer(digesting);
 		// Build and return the author
 		AuthorId id = new AuthorId(messageDigest.digest());
diff --git a/briar-core/src/net/sf/briar/messaging/GroupFactoryImpl.java b/briar-core/src/net/sf/briar/messaging/GroupFactoryImpl.java
index 81c0d53752..7bbc712005 100644
--- a/briar-core/src/net/sf/briar/messaging/GroupFactoryImpl.java
+++ b/briar-core/src/net/sf/briar/messaging/GroupFactoryImpl.java
@@ -36,9 +36,10 @@ class GroupFactoryImpl implements GroupFactory {
 	public Group createGroup(String name, byte[] salt) throws IOException {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(GROUP);
+		w.writeStructStart(GROUP);
 		w.writeString(name);
 		w.writeBytes(salt);
+		w.writeStructEnd();
 		MessageDigest messageDigest = crypto.getMessageDigest();
 		messageDigest.update(out.toByteArray());
 		GroupId id = new GroupId(messageDigest.digest());
diff --git a/briar-core/src/net/sf/briar/messaging/GroupReader.java b/briar-core/src/net/sf/briar/messaging/GroupReader.java
index ac3b20583d..20402c5ae2 100644
--- a/briar-core/src/net/sf/briar/messaging/GroupReader.java
+++ b/briar-core/src/net/sf/briar/messaging/GroupReader.java
@@ -26,11 +26,12 @@ class GroupReader implements StructReader<Group> {
 		DigestingConsumer digesting = new DigestingConsumer(messageDigest);
 		// Read and digest the data
 		r.addConsumer(digesting);
-		r.readStructId(GROUP);
+		r.readStructStart(GROUP);
 		String name = r.readString(MAX_GROUP_NAME_LENGTH);
 		byte[] publicKey = null;
 		if(r.hasNull()) r.readNull();
 		else publicKey = r.readBytes(MAX_PUBLIC_KEY_LENGTH);
+		r.readStructEnd();
 		r.removeConsumer(digesting);
 		// Build and return the group
 		GroupId id = new GroupId(messageDigest.digest());
diff --git a/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java b/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java
index b8e9f09f1c..810c8fd210 100644
--- a/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java
+++ b/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java
@@ -100,7 +100,7 @@ class MessageFactoryImpl implements MessageFactory {
 			w.addConsumer(signingConsumer);
 		}
 		// Write the message
-		w.writeStructId(MESSAGE);
+		w.writeStructStart(MESSAGE);
 		if(parent == null) w.writeNull();
 		else w.writeBytes(parent.getBytes());
 		if(group == null) w.writeNull();
@@ -109,7 +109,7 @@ class MessageFactoryImpl implements MessageFactory {
 		else writeAuthor(w, author);
 		w.writeString(contentType);
 		long timestamp = clock.currentTimeMillis();
-		w.writeInt64(timestamp);
+		w.writeIntAny(timestamp);
 		byte[] salt = new byte[MESSAGE_SALT_LENGTH];
 		random.nextBytes(salt);
 		w.writeBytes(salt);
@@ -125,6 +125,7 @@ class MessageFactoryImpl implements MessageFactory {
 				throw new IllegalArgumentException();
 			w.writeBytes(sig);
 		}
+		w.writeStructEnd();
 		// Hash the message, including the signature, to get the message ID
 		w.removeConsumer(digestingConsumer);
 		MessageId id = new MessageId(messageDigest.digest());
@@ -143,14 +144,16 @@ class MessageFactoryImpl implements MessageFactory {
 	}
 
 	private void writeGroup(Writer w, Group g) throws IOException {
-		w.writeStructId(GROUP);
+		w.writeStructStart(GROUP);
 		w.writeString(g.getName());
 		w.writeBytes(g.getSalt());
+		w.writeStructEnd();
 	}
 
 	private void writeAuthor(Writer w, Author a) throws IOException {
-		w.writeStructId(AUTHOR);
+		w.writeStructStart(AUTHOR);
 		w.writeString(a.getName());
 		w.writeBytes(a.getPublicKey());
+		w.writeStructEnd();
 	}
 }
diff --git a/briar-core/src/net/sf/briar/messaging/MessageReader.java b/briar-core/src/net/sf/briar/messaging/MessageReader.java
index db0e9598ca..c3a6978865 100644
--- a/briar-core/src/net/sf/briar/messaging/MessageReader.java
+++ b/briar-core/src/net/sf/briar/messaging/MessageReader.java
@@ -42,8 +42,8 @@ class MessageReader implements StructReader<UnverifiedMessage> {
 		CountingConsumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
 		r.addConsumer(copying);
 		r.addConsumer(counting);
-		// Read the initial tag
-		r.readStructId(MESSAGE);
+		// Read the start of the struct
+		r.readStructStart(MESSAGE);
 		// Read the parent's message ID, if there is one
 		MessageId parent = null;
 		if(r.hasNull()) {
@@ -64,7 +64,7 @@ class MessageReader implements StructReader<UnverifiedMessage> {
 		// Read the content type
 		String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
 		// Read the timestamp
-		long timestamp = r.readInt64();
+		long timestamp = r.readIntAny();
 		if(timestamp < 0) throw new FormatException();
 		// Read the salt
 		byte[] salt = r.readBytes(MESSAGE_SALT_LENGTH);
@@ -89,6 +89,8 @@ class MessageReader implements StructReader<UnverifiedMessage> {
 		byte[] signature = null;
 		if(author == null) r.readNull();
 		else signature = r.readBytes(MAX_SIGNATURE_LENGTH);
+		// Read the end of the struct
+		r.readStructEnd();
 		// The signature will be verified later
 		r.removeConsumer(counting);
 		r.removeConsumer(copying);
diff --git a/briar-core/src/net/sf/briar/messaging/PacketReaderImpl.java b/briar-core/src/net/sf/briar/messaging/PacketReaderImpl.java
index 682f1824d5..07b4bd8688 100644
--- a/briar-core/src/net/sf/briar/messaging/PacketReaderImpl.java
+++ b/briar-core/src/net/sf/briar/messaging/PacketReaderImpl.java
@@ -70,12 +70,17 @@ class PacketReaderImpl implements PacketReader {
 	}
 
 	public Ack readAck() throws IOException {
+		// Set up the reader
 		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
 		r.addConsumer(counting);
-		r.readStructId(ACK);
+		// Read the start of the struct
+		r.readStructStart(ACK);
 		// Read the message IDs as byte arrays
 		r.setMaxBytesLength(UniqueId.LENGTH);
 		List<Bytes> raw = r.readList(Bytes.class);
+		// Read the end of the struct
+		r.readStructEnd();
+		// Reset the reader
 		r.resetMaxBytesLength();
 		r.removeConsumer(counting);
 		if(raw.isEmpty()) throw new FormatException();
@@ -103,12 +108,17 @@ class PacketReaderImpl implements PacketReader {
 	}
 
 	public Offer readOffer() throws IOException {
+		// Set up the reader
 		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
 		r.addConsumer(counting);
-		r.readStructId(OFFER);
+		// Read the start of the struct
+		r.readStructStart(OFFER);
 		// Read the message IDs as byte arrays
 		r.setMaxBytesLength(UniqueId.LENGTH);
 		List<Bytes> raw = r.readList(Bytes.class);
+		// Read the end of the struct
+		r.readStructEnd();
+		// Reset the reader
 		r.resetMaxBytesLength();
 		r.removeConsumer(counting);
 		if(raw.isEmpty()) throw new FormatException();
@@ -128,14 +138,19 @@ class PacketReaderImpl implements PacketReader {
 	}
 
 	public Request readRequest() throws IOException {
+		// Set up the reader
 		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
 		r.addConsumer(counting);
-		r.readStructId(REQUEST);
+		// Read the start of the struct
+		r.readStructStart(REQUEST);
 		// There may be up to 7 bits of padding at the end of the bitmap
 		int padding = r.readUint7();
 		if(padding > 7) throw new FormatException();
 		// Read the bitmap
 		byte[] bitmap = r.readBytes(MAX_PACKET_LENGTH);
+		// Read the end of the struct
+		r.readStructEnd();
+		// Reset the reader
 		r.removeConsumer(counting);
 		// Convert the bitmap into a BitSet
 		int length = bitmap.length * 8 - padding;
@@ -154,9 +169,10 @@ class PacketReaderImpl implements PacketReader {
 	}
 
 	public RetentionAck readRetentionAck() throws IOException {
-		r.readStructId(RETENTION_ACK);
-		long version = r.readInt64();
+		r.readStructStart(RETENTION_ACK);
+		long version = r.readIntAny();
 		if(version < 0) throw new FormatException();
+		r.readStructEnd();
 		return new RetentionAck(version);
 	}
 
@@ -165,11 +181,12 @@ class PacketReaderImpl implements PacketReader {
 	}
 
 	public RetentionUpdate readRetentionUpdate() throws IOException {
-		r.readStructId(RETENTION_UPDATE);
-		long retention = r.readInt64();
+		r.readStructStart(RETENTION_UPDATE);
+		long retention = r.readIntAny();
 		if(retention < 0) throw new FormatException();
-		long version = r.readInt64();
+		long version = r.readIntAny();
 		if(version < 0) throw new FormatException();
+		r.readStructEnd();
 		return new RetentionUpdate(retention, version);
 	}
 
@@ -178,9 +195,10 @@ class PacketReaderImpl implements PacketReader {
 	}
 
 	public SubscriptionAck readSubscriptionAck() throws IOException {
-		r.readStructId(SUBSCRIPTION_ACK);
-		long version = r.readInt64();
+		r.readStructStart(SUBSCRIPTION_ACK);
+		long version = r.readIntAny();
 		if(version < 0) throw new FormatException();
+		r.readStructEnd();
 		return new SubscriptionAck(version);
 	}
 
@@ -197,11 +215,12 @@ class PacketReaderImpl implements PacketReader {
 	}
 
 	public TransportAck readTransportAck() throws IOException {
-		r.readStructId(TRANSPORT_ACK);
+		r.readStructStart(TRANSPORT_ACK);
 		byte[] b = r.readBytes(UniqueId.LENGTH);
 		if(b.length < UniqueId.LENGTH) throw new FormatException();
-		long version = r.readInt64();
+		long version = r.readIntAny();
 		if(version < 0) throw new FormatException();
+		r.readStructEnd();
 		return new TransportAck(new TransportId(b), version);
 	}
 
@@ -210,9 +229,11 @@ class PacketReaderImpl implements PacketReader {
 	}
 
 	public TransportUpdate readTransportUpdate() throws IOException {
+		// Set up the reader
 		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
 		r.addConsumer(counting);
-		r.readStructId(TRANSPORT_UPDATE);
+		// Read the start of the struct
+		r.readStructStart(TRANSPORT_UPDATE);
 		// Read the transport ID
 		byte[] b = r.readBytes(UniqueId.LENGTH);
 		if(b.length < UniqueId.LENGTH) throw new FormatException();
@@ -223,8 +244,11 @@ class PacketReaderImpl implements PacketReader {
 		r.resetMaxStringLength();
 		if(m.size() > MAX_PROPERTIES_PER_TRANSPORT) throw new FormatException();
 		// Read the version number
-		long version = r.readInt64();
+		long version = r.readIntAny();
 		if(version < 0) throw new FormatException();
+		// Read the end of the struct
+		r.readStructEnd();
+		// Reset the reader
 		r.removeConsumer(counting);
 		// Build and return the transport update
 		return new TransportUpdate(id, new TransportProperties(m), version);
diff --git a/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java b/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java
index 9cdc897b48..9b2607f8dd 100644
--- a/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java
+++ b/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java
@@ -50,27 +50,30 @@ class PacketWriterImpl implements PacketWriter {
 
 	public int getMaxMessagesForAck(long capacity) {
 		int packet = (int) Math.min(capacity, MAX_PACKET_LENGTH);
-		int overhead = serial.getSerialisedStructIdLength(ACK)
+		int overhead = serial.getSerialisedStructStartLength(ACK)
 				+ serial.getSerialisedListStartLength()
-				+ serial.getSerialisedListEndLength();
+				+ serial.getSerialisedListEndLength()
+				+ serial.getSerialisedStructEndLength();
 		int idLength = serial.getSerialisedUniqueIdLength();
 		return (packet - overhead) / idLength;
 	}
 
 	public int getMaxMessagesForOffer(long capacity) {
 		int packet = (int) Math.min(capacity, MAX_PACKET_LENGTH);
-		int overhead = serial.getSerialisedStructIdLength(OFFER)
+		int overhead = serial.getSerialisedStructStartLength(OFFER)
 				+ serial.getSerialisedListStartLength()
-				+ serial.getSerialisedListEndLength();
+				+ serial.getSerialisedListEndLength()
+				+ serial.getSerialisedStructEndLength();
 		int idLength = serial.getSerialisedUniqueIdLength();
 		return (packet - overhead) / idLength;
 	}
 
 	public void writeAck(Ack a) throws IOException {
-		w.writeStructId(ACK);
+		w.writeStructStart(ACK);
 		w.writeListStart();
 		for(MessageId m : a.getMessageIds()) w.writeBytes(m.getBytes());
 		w.writeListEnd();
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
@@ -80,10 +83,11 @@ class PacketWriterImpl implements PacketWriter {
 	}
 
 	public void writeOffer(Offer o) throws IOException {
-		w.writeStructId(OFFER);
+		w.writeStructStart(OFFER);
 		w.writeListStart();
 		for(MessageId m : o.getMessageIds()) w.writeBytes(m.getBytes());
 		w.writeListEnd();
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
@@ -101,57 +105,65 @@ class PacketWriterImpl implements PacketWriter {
 				bitmap[offset] |= bit;
 			}
 		}
-		w.writeStructId(REQUEST);
+		w.writeStructStart(REQUEST);
 		w.writeUint7((byte) (bytes * 8 - length));
 		w.writeBytes(bitmap);
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
 	public void writeRetentionAck(RetentionAck a) throws IOException {
-		w.writeStructId(RETENTION_ACK);
-		w.writeInt64(a.getVersion());
+		w.writeStructStart(RETENTION_ACK);
+		w.writeIntAny(a.getVersion());
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
 	public void writeRetentionUpdate(RetentionUpdate u) throws IOException {
-		w.writeStructId(RETENTION_UPDATE);
-		w.writeInt64(u.getRetentionTime());
-		w.writeInt64(u.getVersion());
+		w.writeStructStart(RETENTION_UPDATE);
+		w.writeIntAny(u.getRetentionTime());
+		w.writeIntAny(u.getVersion());
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
 	public void writeSubscriptionAck(SubscriptionAck a) throws IOException {
-		w.writeStructId(SUBSCRIPTION_ACK);
-		w.writeInt64(a.getVersion());
+		w.writeStructStart(SUBSCRIPTION_ACK);
+		w.writeIntAny(a.getVersion());
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
 	public void writeSubscriptionUpdate(SubscriptionUpdate u)
 			throws IOException {
-		w.writeStructId(SUBSCRIPTION_UPDATE);
+		w.writeStructStart(SUBSCRIPTION_UPDATE);
 		w.writeListStart();
 		for(Group g : u.getGroups()) {
-			w.writeStructId(GROUP);
+			w.writeStructStart(GROUP);
 			w.writeString(g.getName());
 			w.writeBytes(g.getSalt());
+			w.writeStructEnd();
 		}
 		w.writeListEnd();
-		w.writeInt64(u.getVersion());
+		w.writeIntAny(u.getVersion());
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
 	public void writeTransportAck(TransportAck a) throws IOException {
-		w.writeStructId(TRANSPORT_ACK);
+		w.writeStructStart(TRANSPORT_ACK);
 		w.writeBytes(a.getId().getBytes());
-		w.writeInt64(a.getVersion());
+		w.writeIntAny(a.getVersion());
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
 	public void writeTransportUpdate(TransportUpdate u) throws IOException {
-		w.writeStructId(TRANSPORT_UPDATE);
+		w.writeStructStart(TRANSPORT_UPDATE);
 		w.writeBytes(u.getId().getBytes());
 		w.writeMap(u.getProperties());
-		w.writeInt64(u.getVersion());
+		w.writeIntAny(u.getVersion());
+		w.writeStructEnd();
 		if(flush) out.flush();
 	}
 
diff --git a/briar-core/src/net/sf/briar/messaging/SubscriptionUpdateReader.java b/briar-core/src/net/sf/briar/messaging/SubscriptionUpdateReader.java
index 6312e4ea31..1ed20b5eda 100644
--- a/briar-core/src/net/sf/briar/messaging/SubscriptionUpdateReader.java
+++ b/briar-core/src/net/sf/briar/messaging/SubscriptionUpdateReader.java
@@ -26,9 +26,11 @@ class SubscriptionUpdateReader implements StructReader<SubscriptionUpdate> {
 	}
 
 	public SubscriptionUpdate readStruct(Reader r) throws IOException {
+		// Set up the reader
 		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
 		r.addConsumer(counting);
-		r.readStructId(SUBSCRIPTION_UPDATE);
+		// Read the start of the struct
+		r.readStructStart(SUBSCRIPTION_UPDATE);
 		// Read the subscriptions
 		List<Group> subs = new ArrayList<Group>();
 		r.readListStart();
@@ -36,8 +38,11 @@ class SubscriptionUpdateReader implements StructReader<SubscriptionUpdate> {
 			subs.add(groupReader.readStruct(r));
 		r.readListEnd();
 		// Read the version number
-		long version = r.readInt64();
+		long version = r.readIntAny();
 		if(version < 0) throw new FormatException();
+		// Read the end of the struct
+		r.readStructEnd();
+		// Reset the reader
 		r.removeConsumer(counting);
 		// Build and return the subscription update
 		subs = Collections.unmodifiableList(subs);
diff --git a/briar-core/src/net/sf/briar/serial/ReaderImpl.java b/briar-core/src/net/sf/briar/serial/ReaderImpl.java
index 440940d6dc..6ab7a5b1a8 100644
--- a/briar-core/src/net/sf/briar/serial/ReaderImpl.java
+++ b/briar-core/src/net/sf/briar/serial/ReaderImpl.java
@@ -23,8 +23,8 @@ class ReaderImpl implements Reader {
 	private final Collection<Consumer> consumers = new ArrayList<Consumer>(0);
 
 	private boolean hasLookahead = false, eof = false;
-	private byte next, nextNext;
-	private byte[] buf = null;
+	private byte next, nextStructId;
+	private byte[] buf = new byte[8];
 	private int maxStringLength = Integer.MAX_VALUE;
 	private int maxBytesLength = Integer.MAX_VALUE;
 
@@ -33,42 +33,40 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean eof() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		return eof;
 	}
 
-	private byte readLookahead(boolean eofAcceptable) throws IOException {
+	private void readLookahead() throws IOException {
 		assert !eof;
 		// If one or two lookahead bytes have been read, feed the consumers
 		if(hasLookahead) consumeLookahead();
 		// Read a lookahead byte
 		int i = in.read();
 		if(i == -1) {
-			if(!eofAcceptable) throw new FormatException();
 			eof = true;
+			return;
 		}
 		next = (byte) i;
 		// If necessary, read another lookahead byte
 		if(next == Tag.STRUCT) {
 			i = in.read();
 			if(i == -1) throw new FormatException();
-			nextNext = (byte) i;
+			nextStructId = (byte) i;
 		}
 		hasLookahead = true;
-		return next;
 	}
 
 	private void consumeLookahead() throws IOException {
 		assert hasLookahead;
 		for(Consumer c : consumers) {
 			c.write(next);
-			if(next == Tag.STRUCT) c.write(nextNext);
+			if(next == Tag.STRUCT) c.write(nextStructId);
 		}
 		hasLookahead = false;
 	}
 
 	public void close() throws IOException {
-		buf = null;
 		in.close();
 	}
 
@@ -97,7 +95,7 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasBoolean() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.FALSE || next == Tag.TRUE;
 	}
@@ -109,7 +107,7 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasUint7() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next >= 0;
 	}
@@ -121,34 +119,37 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasInt8() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.INT8;
 	}
 
 	public byte readInt8() throws IOException {
 		if(!hasInt8()) throw new FormatException();
-		readLookahead(false);
 		consumeLookahead();
-		return next;
+		int i = in.read();
+		if(i == -1) {
+			eof = true;
+			throw new FormatException();
+		}
+		return (byte) i;
 	}
 
 	public boolean hasInt16() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.INT16;
 	}
 
 	public short readInt16() throws IOException {
 		if(!hasInt16()) throw new FormatException();
-		byte b1 = readLookahead(false);
-		byte b2 = readLookahead(false);
 		consumeLookahead();
-		return (short) (((b1 & 0xFF) << 8) | (b2 & 0xFF));
+		readIntoBuffer(2);
+		return (short) (((buf[0] & 0xFF) << 8) | (buf[1] & 0xFF));
 	}
 
 	public boolean hasInt32() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.INT32;
 	}
@@ -166,7 +167,7 @@ class ReaderImpl implements Reader {
 	}
 
 	private void readIntoBuffer(int length) throws IOException {
-		if(buf == null || buf.length < length) buf = new byte[length];
+		if(buf.length < length) buf = new byte[length];
 		readIntoBuffer(buf, length);
 	}
 
@@ -177,17 +178,16 @@ class ReaderImpl implements Reader {
 			int read = in.read(b, offset, length - offset);
 			if(read == -1) {
 				eof = true;
-				break;
+				throw new FormatException();
 			}
 			offset += read;
 		}
-		if(offset < length) throw new FormatException();
 		// Feed the hungry mouths
 		for(Consumer c : consumers) c.write(b, 0, length);
 	}
 
 	public boolean hasInt64() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.INT64;
 	}
@@ -207,7 +207,7 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasIntAny() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next >= 0 || next == Tag.INT8 || next == Tag.INT16
 				|| next == Tag.INT32 || next == Tag.INT64;
@@ -224,7 +224,7 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasFloat32() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.FLOAT32;
 	}
@@ -236,7 +236,7 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasFloat64() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.FLOAT64;
 	}
@@ -248,15 +248,11 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasString() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.STRING;
 	}
 
-	public String readString() throws IOException {
-		return readString(maxStringLength);
-	}
-
 	public String readString(int maxLength) throws IOException {
 		if(!hasString()) throw new FormatException();
 		consumeLookahead();
@@ -269,30 +265,29 @@ class ReaderImpl implements Reader {
 
 	private int readLength() throws IOException {
 		if(!hasLength()) throw new FormatException();
-		if(next >= 0) return readUint7();
-		if(next == Tag.INT8) return readInt8();
-		if(next == Tag.INT16) return readInt16();
-		if(next == Tag.INT32) return readInt32();
-		throw new IllegalStateException();
+		int length;
+		if(next >= 0) length = readUint7();
+		else if(next == Tag.INT8) length = readInt8();
+		else if(next == Tag.INT16) length = readInt16();
+		else if(next == Tag.INT32) length = readInt32();
+		else throw new IllegalStateException();
+		if(length < 0) throw new FormatException();
+		return length;
 	}
 
 	private boolean hasLength() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next >= 0 || next == Tag.INT8 || next == Tag.INT16
 				|| next == Tag.INT32;
 	}
 
 	public boolean hasBytes() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.BYTES;
 	}
 
-	public byte[] readBytes() throws IOException {
-		return readBytes(maxBytesLength);
-	}
-
 	public byte[] readBytes(int maxLength) throws IOException {
 		if(!hasBytes()) throw new FormatException();
 		consumeLookahead();
@@ -305,7 +300,7 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasList() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.LIST;
 	}
@@ -320,13 +315,12 @@ class ReaderImpl implements Reader {
 	}
 
 	private boolean hasEnd() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.END;
 	}
 
 	private void readEnd() throws IOException {
-		if(!hasLookahead) throw new IllegalStateException();
 		if(!hasEnd()) throw new FormatException();
 		consumeLookahead();
 	}
@@ -340,8 +334,8 @@ class ReaderImpl implements Reader {
 		if(hasInt64()) return Long.valueOf(readInt64());
 		if(hasFloat32()) return Float.valueOf(readFloat32());
 		if(hasFloat64()) return Double.valueOf(readFloat64());
-		if(hasString()) return readString();
-		if(hasBytes()) return new Bytes(readBytes());
+		if(hasString()) return readString(maxStringLength);
+		if(hasBytes()) return new Bytes(readBytes(maxBytesLength));
 		if(hasList()) return readList(Object.class);
 		if(hasMap()) return readMap(Object.class, Object.class);
 		if(hasNull()) {
@@ -378,14 +372,8 @@ class ReaderImpl implements Reader {
 		}
 	}
 
-	public boolean hasListStart() throws IOException {
-		if(!hasLookahead) readLookahead(true);
-		if(eof) return false;
-		return next == Tag.LIST;
-	}
-
 	public void readListStart() throws IOException {
-		if(!hasListStart()) throw new FormatException();
+		if(!hasList()) throw new FormatException();
 		consumeLookahead();
 	}
 
@@ -398,7 +386,7 @@ class ReaderImpl implements Reader {
 	}
 
 	public boolean hasMap() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
 		return next == Tag.MAP;
 	}
@@ -415,14 +403,8 @@ class ReaderImpl implements Reader {
 		return Collections.unmodifiableMap(m);
 	}
 
-	public boolean hasMapStart() throws IOException {
-		if(!hasLookahead) readLookahead(true);
-		if(eof) return false;
-		return next == Tag.MAP;
-	}
-
 	public void readMapStart() throws IOException {
-		if(!hasMapStart()) throw new FormatException();
+		if(!hasMap()) throw new FormatException();
 		consumeLookahead();
 	}
 
@@ -434,26 +416,34 @@ class ReaderImpl implements Reader {
 		readEnd();
 	}
 
-	public boolean hasNull() throws IOException {
-		if(!hasLookahead) readLookahead(true);
+	public boolean hasStruct(int id) throws IOException {
+		if(id < 0 || id > 255) throw new IllegalArgumentException();
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
-		return next == Tag.NULL;
+		return (nextStructId & 0xFF) == id;
 	}
 
-	public void readNull() throws IOException {
-		if(!hasNull()) throw new FormatException();
+	public void readStructStart(int id) throws IOException {
+		if(!hasStruct(id)) throw new FormatException();
 		consumeLookahead();
 	}
 
-	public boolean hasStruct(int id) throws IOException {
-		if(id < 0 || id > 255) throw new IllegalArgumentException();
-		if(!hasLookahead) readLookahead(true);
+	public boolean hasStructEnd() throws IOException {
+		return hasEnd();
+	}
+
+	public void readStructEnd() throws IOException {
+		readEnd();
+	}
+
+	public boolean hasNull() throws IOException {
+		if(!hasLookahead) readLookahead();
 		if(eof) return false;
-		return id == (0xFF & nextNext);
+		return next == Tag.NULL;
 	}
 
-	public void readStructId(int id) throws IOException {
-		if(!hasStruct(id)) throw new FormatException();
+	public void readNull() throws IOException {
+		if(!hasNull()) throw new FormatException();
 		consumeLookahead();
 	}
 }
diff --git a/briar-core/src/net/sf/briar/serial/SerialComponentImpl.java b/briar-core/src/net/sf/briar/serial/SerialComponentImpl.java
index e55e94db0e..b3c5ba798a 100644
--- a/briar-core/src/net/sf/briar/serial/SerialComponentImpl.java
+++ b/briar-core/src/net/sf/briar/serial/SerialComponentImpl.java
@@ -5,31 +5,35 @@ import net.sf.briar.api.serial.SerialComponent;
 
 class SerialComponentImpl implements SerialComponent {
 
+	public int getSerialisedListStartLength() {
+		// LIST tag
+		return 1;
+	}
+
 	public int getSerialisedListEndLength() {
 		// END tag
 		return 1;
 	}
 
-	public int getSerialisedListStartLength() {
-		// LIST tag
-		return 1;
+	public int getSerialisedStructStartLength(int id) {
+		// STRUCT tag, ID
+		return 2;
 	}
 
-	public int getSerialisedStructIdLength(int id) {
-		if(id < 0 || id > 255) throw new IllegalArgumentException();
-		return id < 32 ? 1 : 2;
+	public int getSerialisedStructEndLength() {
+		// END tag
+		return 1;
 	}
 
 	public int getSerialisedUniqueIdLength() {
 		// BYTES tag, length spec, bytes
 		return 1 + getSerialisedLengthSpecLength(UniqueId.LENGTH)
-		+ UniqueId.LENGTH;
+				+ UniqueId.LENGTH;
 	}
 
 	private int getSerialisedLengthSpecLength(int length) {
 		if(length < 0) throw new IllegalArgumentException();
-		if(length < 128) return 1; // Uint7
-		if(length < Short.MAX_VALUE) return 3; // Int16
-		return 5; // Int32
+		// Uint7, int16 or int32
+		return length <= Byte.MAX_VALUE ? 1 : length <= Short.MAX_VALUE ? 3 : 5;
 	}
 }
diff --git a/briar-core/src/net/sf/briar/serial/Tag.java b/briar-core/src/net/sf/briar/serial/Tag.java
index 04b0e45478..67b6802d3d 100644
--- a/briar-core/src/net/sf/briar/serial/Tag.java
+++ b/briar-core/src/net/sf/briar/serial/Tag.java
@@ -2,19 +2,19 @@ package net.sf.briar.serial;
 
 interface Tag {
 
-	byte FALSE = (byte) 0xFF; // 1111 1111
-	byte TRUE = (byte) 0xFE; // 1111 1110
-	byte INT8 = (byte) 0xFD; // 1111 1101
-	byte INT16 = (byte) 0xFC; // 1111 1100
-	byte INT32 = (byte) 0xFB; // 1111 1011
-	byte INT64 = (byte) 0xFA; // 1111 1010
-	byte FLOAT32 = (byte) 0xF9; // 1111 1001
-	byte FLOAT64 = (byte) 0xF8; // 1111 1000
-	byte STRING = (byte) 0xF7; // 1111 0111
-	byte BYTES = (byte) 0xF6; // 1111 0110
-	byte LIST = (byte) 0xF5; // 1111 0111
-	byte MAP = (byte) 0xF4; // 1111 0100
-	byte END = (byte) 0xF3; // 1111 0011
-	byte NULL = (byte) 0xF2; // 1111 0010
-	byte STRUCT = (byte) 0xF1; // 1111 0001
+	byte FALSE = (byte) 0xFF;
+	byte TRUE = (byte) 0xFE;
+	byte INT8 = (byte) 0xFD;
+	byte INT16 = (byte) 0xFC;
+	byte INT32 = (byte) 0xFB;
+	byte INT64 = (byte) 0xFA;
+	byte FLOAT32 = (byte) 0xF9;
+	byte FLOAT64 = (byte) 0xF8;
+	byte STRING = (byte) 0xF7;
+	byte BYTES = (byte) 0xF6;
+	byte LIST = (byte) 0xF5;
+	byte MAP = (byte) 0xF4;
+	byte STRUCT = (byte) 0xF3;
+	byte END = (byte) 0xF2;
+	byte NULL = (byte) 0xF1;
 }
diff --git a/briar-core/src/net/sf/briar/serial/WriterImpl.java b/briar-core/src/net/sf/briar/serial/WriterImpl.java
index aee13708f7..97635a5abc 100644
--- a/briar-core/src/net/sf/briar/serial/WriterImpl.java
+++ b/briar-core/src/net/sf/briar/serial/WriterImpl.java
@@ -177,16 +177,20 @@ class WriterImpl implements Writer {
 		write(Tag.END);
 	}
 
-	public void writeNull() throws IOException {
-		write(Tag.NULL);
-	}
-
-	public void writeStructId(int id) throws IOException {
+	public void writeStructStart(int id) throws IOException {
 		if(id < 0 || id > 255) throw new IllegalArgumentException();
 		write(Tag.STRUCT);
 		write((byte) id);
 	}
 
+	public void writeStructEnd() throws IOException {
+		write(Tag.END);
+	}
+
+	public void writeNull() throws IOException {
+		write(Tag.NULL);
+	}
+
 	private void write(byte b) throws IOException {
 		out.write(b);
 		for(Consumer c : consumers) c.write(b);
diff --git a/briar-tests/src/net/sf/briar/messaging/PacketReaderImplTest.java b/briar-tests/src/net/sf/briar/messaging/PacketReaderImplTest.java
index a50b364485..43a2f4427c 100644
--- a/briar-tests/src/net/sf/briar/messaging/PacketReaderImplTest.java
+++ b/briar-tests/src/net/sf/briar/messaging/PacketReaderImplTest.java
@@ -165,14 +165,17 @@ public class PacketReaderImplTest extends BriarTestCase {
 	private byte[] createAck(boolean tooBig) throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(ACK);
+		w.writeStructStart(ACK);
 		w.writeListStart();
 		while(out.size() + serial.getSerialisedUniqueIdLength()
+				+ serial.getSerialisedListEndLength()
+				+ serial.getSerialisedStructEndLength()
 				< MAX_PACKET_LENGTH) {
 			w.writeBytes(TestUtils.getRandomId());
 		}
 		if(tooBig) w.writeBytes(TestUtils.getRandomId());
 		w.writeListEnd();
+		w.writeStructEnd();
 		assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH);
 		return out.toByteArray();
 	}
@@ -180,23 +183,27 @@ public class PacketReaderImplTest extends BriarTestCase {
 	private byte[] createEmptyAck() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(ACK);
+		w.writeStructStart(ACK);
 		w.writeListStart();
 		w.writeListEnd();
+		w.writeStructEnd();
 		return out.toByteArray();
 	}
 
 	private byte[] createOffer(boolean tooBig) throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(OFFER);
+		w.writeStructStart(OFFER);
 		w.writeListStart();
 		while(out.size() + serial.getSerialisedUniqueIdLength()
+				+ serial.getSerialisedListEndLength()
+				+ serial.getSerialisedStructEndLength()
 				< MAX_PACKET_LENGTH) {
 			w.writeBytes(TestUtils.getRandomId());
 		}
 		if(tooBig) w.writeBytes(TestUtils.getRandomId());
 		w.writeListEnd();
+		w.writeStructEnd();
 		assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH);
 		return out.toByteArray();
 	}
@@ -204,24 +211,27 @@ public class PacketReaderImplTest extends BriarTestCase {
 	private byte[] createEmptyOffer() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(OFFER);
+		w.writeStructStart(OFFER);
 		w.writeListStart();
 		w.writeListEnd();
+		w.writeStructEnd();
 		return out.toByteArray();
 	}
 
 	private byte[] createRequest(boolean tooBig) throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(REQUEST);
-		// Allow one byte for the STRUCT tag, one byte for the REQUEST tag,
+		w.writeStructStart(REQUEST);
+		// Allow one byte for the STRUCT tag, one byte for the struct ID,
 		// one byte for the padding length as a uint7, one byte for the BYTES
-		// tag, and five bytes for the length of the byte array as an int32
-		int size = MAX_PACKET_LENGTH - 9;
+		// tag, five bytes for the length of the byte array as an int32, and
+		// one byte for the END tag
+		int size = MAX_PACKET_LENGTH - 10;
 		if(tooBig) size++;
 		assertTrue(size > Short.MAX_VALUE);
 		w.writeUint7((byte) 0);
 		w.writeBytes(new byte[size]);
+		w.writeStructEnd();
 		assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH);
 		return out.toByteArray();
 	}
@@ -229,9 +239,10 @@ public class PacketReaderImplTest extends BriarTestCase {
 	private byte[] createRequest(byte[] bitmap) throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(REQUEST);
+		w.writeStructStart(REQUEST);
 		w.writeUint7((byte) 0);
 		w.writeBytes(bitmap);
+		w.writeStructEnd();
 		return out.toByteArray();
 	}
 }
diff --git a/briar-tests/src/net/sf/briar/messaging/PacketWriterImplTest.java b/briar-tests/src/net/sf/briar/messaging/PacketWriterImplTest.java
index 2ef9af2e3b..197bed53e7 100644
--- a/briar-tests/src/net/sf/briar/messaging/PacketWriterImplTest.java
+++ b/briar-tests/src/net/sf/briar/messaging/PacketWriterImplTest.java
@@ -61,9 +61,9 @@ public class PacketWriterImplTest extends BriarTestCase {
 		b.set(15);
 		w.writeRequest(new Request(b, 16));
 		// STRUCT tag, struct ID 5, 0 as uint7, BYTES tag, length 2 as uint7,
-		// 0xD959
+		// bitmap 0xD959, END tag
 		byte[] output = out.toByteArray();
-		assertEquals("F1" + "05" + "00" + "F6" + "02" + "D959",
+		assertEquals("F3" + "05" + "00" + "F6" + "02" + "D959" + "F2",
 				StringUtils.toHexString(output));
 	}
 
@@ -85,9 +85,9 @@ public class PacketWriterImplTest extends BriarTestCase {
 		b.set(12);
 		w.writeRequest(new Request(b, 13));
 		// STRUCT tag, struct ID 5, 3 as uint7, BYTES tag, length 2 as uint7,
-		// 0xD959
+		// bitmap 0x59D8, END tag
 		byte[] output = out.toByteArray();
-		assertEquals("F1" + "05" + "03" + "F6" + "02" + "59D8",
+		assertEquals("F3" + "05" + "03" + "F6" + "02" + "59D8" + "F2",
 				StringUtils.toHexString(output));
 	}
 }
diff --git a/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java b/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java
index ec8651d6a8..d2ef31cddb 100644
--- a/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java
+++ b/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java
@@ -122,8 +122,8 @@ public class ReaderImplTest extends BriarTestCase {
 	@Test
 	public void testReadString() throws Exception {
 		setContents("F703666F6F" + "F700");
-		assertEquals("foo", r.readString());
-		assertEquals("", r.readString());
+		assertEquals("foo", r.readString(1000));
+		assertEquals("", r.readString(1000));
 		assertTrue(r.eof());
 	}
 
@@ -140,8 +140,8 @@ public class ReaderImplTest extends BriarTestCase {
 	@Test
 	public void testReadBytes() throws Exception {
 		setContents("F603010203" + "F600");
-		assertArrayEquals(new byte[] {1, 2, 3}, r.readBytes());
-		assertArrayEquals(new byte[] {}, r.readBytes());
+		assertArrayEquals(new byte[] {1, 2, 3}, r.readBytes(1000));
+		assertArrayEquals(new byte[] {}, r.readBytes(1000));
 		assertTrue(r.eof());
 	}
 
@@ -157,7 +157,7 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadList() throws Exception {
-		setContents("F5" + "01" + "F703666F6F" + "FC0080" + "F3");
+		setContents("F5" + "01" + "F703666F6F" + "FC0080" + "F2");
 		List<Object> l = r.readList(Object.class);
 		assertNotNull(l);
 		assertEquals(3, l.size());
@@ -169,7 +169,7 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadListTypeSafe() throws Exception {
-		setContents("F5" + "01" + "02" + "03" + "F3");
+		setContents("F5" + "01" + "02" + "03" + "F2");
 		List<Byte> l = r.readList(Byte.class);
 		assertNotNull(l);
 		assertEquals(3, l.size());
@@ -181,7 +181,7 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadListTypeSafeThrowsFormatException() throws Exception {
-		setContents("F5" + "01" + "F703666F6F" + "03" + "F3");
+		setContents("F5" + "01" + "F703666F6F" + "03" + "F2");
 		// Trying to read a mixed list as a list of bytes should throw a
 		// FormatException
 		try {
@@ -192,7 +192,7 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadMap() throws Exception {
-		setContents("F4" + "F703666F6F" + "7B" + "F600" + "F2" + "F3");
+		setContents("F4" + "F703666F6F" + "7B" + "F600" + "F1" + "F2");
 		Map<Object, Object> m = r.readMap(Object.class, Object.class);
 		assertNotNull(m);
 		assertEquals(2, m.size());
@@ -205,7 +205,7 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadMapTypeSafe() throws Exception {
-		setContents("F4" + "F703666F6F" + "7B" + "F700" + "F2" + "F3");
+		setContents("F4" + "F703666F6F" + "7B" + "F700" + "F1" + "F2");
 		Map<String, Byte> m = r.readMap(String.class, Byte.class);
 		assertNotNull(m);
 		assertEquals(2, m.size());
@@ -217,8 +217,8 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testMapKeysMustBeUnique() throws Exception {
-		setContents("F4" + "F703666F6F" + "01" + "F703626172" + "02" + "F3"
-				+ "F4" + "F703666F6F" + "01" + "F703666F6F" + "02" + "F3");
+		setContents("F4" + "F703666F6F" + "01" + "F703626172" + "02" + "F2"
+				+ "F4" + "F703666F6F" + "01" + "F703666F6F" + "02" + "F2");
 		// The first map has unique keys
 		Map<String, Byte> m = r.readMap(String.class, Byte.class);
 		assertNotNull(m);
@@ -234,13 +234,13 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadDelimitedListElements() throws Exception {
-		setContents("F5" + "01" + "F703666F6F" + "FC0080" + "F3");
-		assertTrue(r.hasListStart());
+		setContents("F5" + "01" + "F703666F6F" + "FC0080" + "F2");
+		assertTrue(r.hasList());
 		r.readListStart();
 		assertFalse(r.hasListEnd());
 		assertEquals((byte) 1, r.readIntAny());
 		assertFalse(r.hasListEnd());
-		assertEquals("foo", r.readString());
+		assertEquals("foo", r.readString(1000));
 		assertFalse(r.hasListEnd());
 		assertEquals((short) 128, r.readIntAny());
 		assertTrue(r.hasListEnd());
@@ -250,7 +250,7 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadDelimitedListTypeSafe() throws Exception {
-		setContents("F5" + "01" + "02" + "03" + "F3");
+		setContents("F5" + "01" + "02" + "03" + "F2");
 		List<Byte> l = r.readList(Byte.class);
 		assertNotNull(l);
 		assertEquals(3, l.size());
@@ -262,15 +262,15 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadDelimitedMapEntries() throws Exception {
-		setContents("F4" + "F703666F6F" + "7B" + "F600" + "F2" + "F3");
-		assertTrue(r.hasMapStart());
+		setContents("F4" + "F703666F6F" + "7B" + "F600" + "F1" + "F2");
+		assertTrue(r.hasMap());
 		r.readMapStart();
 		assertFalse(r.hasMapEnd());
-		assertEquals("foo", r.readString());
+		assertEquals("foo", r.readString(1000));
 		assertFalse(r.hasMapEnd());
 		assertEquals((byte) 123, r.readIntAny());
 		assertFalse(r.hasMapEnd());
-		assertArrayEquals(new byte[] {}, r.readBytes());
+		assertArrayEquals(new byte[] {}, r.readBytes(1000));
 		assertFalse(r.hasMapEnd());
 		assertTrue(r.hasNull());
 		r.readNull();
@@ -281,7 +281,7 @@ public class ReaderImplTest extends BriarTestCase {
 
 	@Test
 	public void testReadDelimitedMapTypeSafe() throws Exception {
-		setContents("F4" + "F703666F6F" + "7B" + "F700" + "F2" + "F3");
+		setContents("F4" + "F703666F6F" + "7B" + "F700" + "F1" + "F2");
 		Map<String, Byte> m = r.readMap(String.class, Byte.class);
 		assertNotNull(m);
 		assertEquals(2, m.size());
@@ -294,8 +294,8 @@ public class ReaderImplTest extends BriarTestCase {
 	@Test
 	@SuppressWarnings("unchecked")
 	public void testReadNestedMapsAndLists() throws Exception {
-		setContents("F4" + "F4" + "F703666F6F" + "7B" + "F3"
-				+ "F5" + "01" + "F3" + "F3");
+		setContents("F4" + "F4" + "F703666F6F" + "7B" + "F2"
+				+ "F5" + "01" + "F2" + "F2");
 		Map<Object, Object> m = r.readMap(Object.class, Object.class);
 		assertNotNull(m);
 		assertEquals(1, m.size());
@@ -313,23 +313,34 @@ public class ReaderImplTest extends BriarTestCase {
 
 
 	@Test
-	public void testMaxLengthAppliesInsideMap() throws Exception {
-		setContents("F4" + "F703666F6F" + "F603010203" + "F3");
+	public void testMaxStringLengthAppliesInsideMap() throws Exception {
+		setContents("F4" + "F703666F6F" + "F603010203" + "F2");
 		r.setMaxStringLength(3);
-		r.setMaxBytesLength(3);
 		Map<String, Bytes> m = r.readMap(String.class, Bytes.class);
+		assertTrue(r.eof());
 		String key = "foo";
 		Bytes value = new Bytes(new byte[] {1, 2, 3});
 		assertEquals(Collections.singletonMap(key, value), m);
 		// The max string length should be applied inside the map
-		setContents("F4" + "F703666F6F" + "F603010203" + "F3");
+		setContents("F4" + "F703666F6F" + "F603010203" + "F2");
 		r.setMaxStringLength(2);
 		try {
 			r.readMap(String.class, Bytes.class);
 			fail();
 		} catch(FormatException expected) {}
+	}
+
+	@Test
+	public void testMaxBytesLengthAppliesInsideMap() throws Exception {
+		setContents("F4" + "F703666F6F" + "F603010203" + "F2");
+		r.setMaxBytesLength(3);
+		Map<String, Bytes> m = r.readMap(String.class, Bytes.class);
+		assertTrue(r.eof());
+		String key = "foo";
+		Bytes value = new Bytes(new byte[] {1, 2, 3});
+		assertEquals(Collections.singletonMap(key, value), m);
 		// The max bytes length should be applied inside the map
-		setContents("F4" + "F703666F6F" + "F603010203" + "F3");
+		setContents("F4" + "F703666F6F" + "F603010203" + "F2");
 		r.setMaxBytesLength(2);
 		try {
 			r.readMap(String.class, Bytes.class);
@@ -337,6 +348,19 @@ public class ReaderImplTest extends BriarTestCase {
 		} catch(FormatException expected) {}
 	}
 
+	@Test
+	public void testReadStruct() throws Exception {
+		// Two structs with IDs 0 and 255, each containing 1 as a uint7
+		setContents("F300" + "01" + "F2" + "F3FF" + "01" + "F2");
+		r.readStructStart(0);
+		assertEquals(1, r.readUint7());
+		r.readStructEnd();
+		r.readStructStart(255);
+		assertEquals(1, r.readUint7());
+		r.readStructEnd();
+		assertTrue(r.eof());
+	}
+
 	@Test
 	public void testReadEmptyInput() throws Exception {
 		setContents("");
diff --git a/briar-tests/src/net/sf/briar/serial/WriterImplTest.java b/briar-tests/src/net/sf/briar/serial/WriterImplTest.java
index 398c818411..502c5216a8 100644
--- a/briar-tests/src/net/sf/briar/serial/WriterImplTest.java
+++ b/briar-tests/src/net/sf/briar/serial/WriterImplTest.java
@@ -155,7 +155,7 @@ public class WriterImplTest extends BriarTestCase {
 		for(int i = 0; i < 16; i++) l.add(i);
 		w.writeList(l);
 		// LIST tag, elements as uint7, END tag
-		checkContents("F5" + "000102030405060708090A0B0C0D0E0F" + "F3");
+		checkContents("F5" + "000102030405060708090A0B0C0D0E0F" + "F2");
 	}
 
 	@Test
@@ -166,7 +166,7 @@ public class WriterImplTest extends BriarTestCase {
 		l.add(2);
 		w.writeList(l);
 		// LIST tag, 1 as uint7, null, 2 as uint7, END tag
-		checkContents("F5" + "01" + "F2" + "02" + "F3");
+		checkContents("F5" + "01" + "F1" + "02" + "F2");
 	}
 
 	@Test
@@ -178,7 +178,7 @@ public class WriterImplTest extends BriarTestCase {
 		// MAP tag, entries as uint7, END tag
 		checkContents("F4" + "0001" + "0102" + "0203" + "0304" + "0405"
 				+ "0506" + "0607" + "0708" + "0809" + "090A" + "0A0B" + "0B0C"
-				+ "0C0D" + "0D0E" + "0E0F" + "0F10" + "F3");
+				+ "0C0D" + "0D0E" + "0E0F" + "0F10" + "F2");
 	}
 
 	@Test
@@ -189,7 +189,7 @@ public class WriterImplTest extends BriarTestCase {
 		w.writeIntAny(128L); // Written as int16
 		w.writeListEnd();
 		// LIST tag, 1 as uint7, "foo" as string, 128 as int16, END tag
-		checkContents("F5" + "01" + "F703666F6F" + "FC0080" + "F3");
+		checkContents("F5" + "01" + "F703666F6F" + "FC0080" + "F2");
 	}
 
 	@Test
@@ -202,7 +202,7 @@ public class WriterImplTest extends BriarTestCase {
 		w.writeMapEnd();
 		// MAP tag, "foo" as string, 123 as uint7, byte[0] as bytes,
 		// NULL tag, END tag
-		checkContents("F4" + "F703666F6F" + "7B" + "F600" + "F2" + "F3");
+		checkContents("F4" + "F703666F6F" + "7B" + "F600" + "F1" + "F2");
 	}
 
 	@Test
@@ -216,22 +216,22 @@ public class WriterImplTest extends BriarTestCase {
 		w.writeMap(m1);
 		// MAP tag, MAP tag, "foo" as string, 123 as uint7, END tag,
 		// LIST tag, 1 as uint7, END tag, END tag
-		checkContents("F4" + "F4" + "F703666F6F" + "7B" + "F3"
-				+ "F5" + "01" + "F3" + "F3");
+		checkContents("F4" + "F4" + "F703666F6F" + "7B" + "F2"
+				+ "F5" + "01" + "F2" + "F2");
 	}
 
 	@Test
-	public void testWriteNull() throws IOException {
-		w.writeNull();
-		checkContents("F2");
+	public void testWriteStruct() throws IOException {
+		w.writeStructStart(123);
+		w.writeStructEnd();
+		// STRUCT tag, 123 as struct ID, END tag
+		checkContents("F3" + "7B" + "F2");
 	}
 
 	@Test
-	public void testWriteStructId() throws IOException {
-		w.writeStructId(32);
-		w.writeStructId(255);
-		// STRUCT tag, 32 as uint8, STRUCT tag, 255 as uint8
-		checkContents("F1" + "20" + "F1" + "FF");
+	public void testWriteNull() throws IOException {
+		w.writeNull();
+		checkContents("F1");
 	}
 
 	private void checkContents(String hex) throws IOException {
-- 
GitLab