diff --git a/api/net/sf/briar/api/setup/SetupParameters.java b/api/net/sf/briar/api/setup/SetupParameters.java
index 9408e9b82f2abc3c4f997ffb4ba7700c441ffea0..be90063496b962ddada6c2181fff438009d20417 100644
--- a/api/net/sf/briar/api/setup/SetupParameters.java
+++ b/api/net/sf/briar/api/setup/SetupParameters.java
@@ -7,4 +7,6 @@ public interface SetupParameters {
 	File getChosenLocation();
 
 	String[] getBundledFontFilenames();
+
+	long getExeHeaderSize();
 }
diff --git a/components/net/sf/briar/setup/SetupModule.java b/components/net/sf/briar/setup/SetupModule.java
deleted file mode 100644
index 71dd5b0ea93d18feebc5fa2c017fbac18245cceb..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/setup/SetupModule.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.sf.briar.setup;
-
-import net.sf.briar.api.setup.SetupWorkerFactory;
-
-import com.google.inject.AbstractModule;
-
-public class SetupModule extends AbstractModule {
-
-	@Override
-	protected void configure() {
-		bind(SetupWorkerFactory.class).to(SetupWorkerFactoryImpl.class);
-	}
-}
diff --git a/components/net/sf/briar/setup/SetupWorker.java b/components/net/sf/briar/setup/SetupWorker.java
index ccfa490acec9b987b4129292729ab9b6f71b6136..389a246b96ffb1d29074628959c7639c6c24f439 100644
--- a/components/net/sf/briar/setup/SetupWorker.java
+++ b/components/net/sf/briar/setup/SetupWorker.java
@@ -5,7 +5,6 @@ import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
-import java.security.CodeSource;
 
 import net.sf.briar.api.i18n.I18n;
 import net.sf.briar.api.setup.SetupCallback;
@@ -18,18 +17,19 @@ class SetupWorker implements Runnable {
 
 	private static final String MAIN_CLASS =
 		"net.sf.briar.ui.invitation.InvitationMain";
-	private static final int EXE_HEADER_SIZE = 62976;
 
 	private final SetupCallback callback;
 	private final SetupParameters parameters;
 	private final I18n i18n;
+	private final File jar;
 	private final ZipUtils.Callback unzipCallback;
 
 	SetupWorker(final SetupCallback callback, SetupParameters parameters,
-			I18n i18n) {
+			I18n i18n, File jar) {
 		this.parameters = parameters;
 		this.callback = callback;
 		this.i18n = i18n;
+		this.jar = jar;
 		unzipCallback = new ZipUtils.Callback() {
 			public void processingFile(File f) {
 				callback.extractingFile(f);
@@ -38,6 +38,10 @@ class SetupWorker implements Runnable {
 	}
 
 	public void run() {
+		if(!jar.isFile()) {
+			callback.error("Not running from jar");
+			return;
+		}
 		File dir = parameters.getChosenLocation();
 		assert dir != null;
 		if(!dir.exists()) {
@@ -66,8 +70,6 @@ class SetupWorker implements Runnable {
 			return;
 		}
 		try {
-			if(callback.isCancelled()) return;
-			File jar = getJar();
 			if(callback.isCancelled()) return;
 			copyInstaller(jar, data);
 			if(callback.isCancelled()) return;
@@ -86,14 +88,6 @@ class SetupWorker implements Runnable {
 		callback.installed(dir);
 	}
 
-	private File getJar() throws IOException {
-		CodeSource c = FileUtils.class.getProtectionDomain().getCodeSource();
-		File jar = new File(c.getLocation().getPath());
-		assert jar.exists();
-		if(!jar.isFile()) throw new IOException("Not running from a jar");
-		return jar;
-	}
-
 	private void copyInstaller(File jar, File dir) throws IOException {
 		File dest = new File(dir, "setup.dat");
 		callback.copyingFile(dest);
@@ -103,7 +97,7 @@ class SetupWorker implements Runnable {
 	private void extractFiles(File jar, File dir, String regex)
 	throws IOException {
 		FileInputStream in = new FileInputStream(jar);
-		in.skip(EXE_HEADER_SIZE);
+		in.skip(parameters.getExeHeaderSize());
 		ZipUtils.unzipStream(in, dir, regex, unzipCallback);
 	}
 
diff --git a/components/net/sf/briar/setup/SetupWorkerFactoryImpl.java b/components/net/sf/briar/setup/SetupWorkerFactoryImpl.java
index 51425399cedd2d6f167018601ca84bc8a90cb4c1..0df98b59ad0b39389db646b940f1ce11ca8b6b6d 100644
--- a/components/net/sf/briar/setup/SetupWorkerFactoryImpl.java
+++ b/components/net/sf/briar/setup/SetupWorkerFactoryImpl.java
@@ -1,23 +1,27 @@
 package net.sf.briar.setup;
 
+import java.io.File;
+import java.security.CodeSource;
+
 import net.sf.briar.api.i18n.I18n;
 import net.sf.briar.api.setup.SetupCallback;
 import net.sf.briar.api.setup.SetupParameters;
 import net.sf.briar.api.setup.SetupWorkerFactory;
-
-import com.google.inject.Inject;
+import net.sf.briar.util.FileUtils;
 
 public class SetupWorkerFactoryImpl implements SetupWorkerFactory {
 
 	private final I18n i18n;
 
-	@Inject
 	public SetupWorkerFactoryImpl(I18n i18n) {
 		this.i18n = i18n;
 	}
 
 	public Runnable createWorker(SetupCallback callback,
 			SetupParameters parameters) {
-		return new SetupWorker(callback, parameters, i18n);
+		CodeSource c = FileUtils.class.getProtectionDomain().getCodeSource();
+		File jar = new File(c.getLocation().getPath());
+		assert jar.exists();
+		return new SetupWorker(callback, parameters, i18n, jar);
 	}
 }
diff --git a/test/build.xml b/test/build.xml
index dfc757721352458e13b4f5ebb0b04406e0d31610..9c90d42b1299ec8fe1954b8264ed9e8068abcbc8 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -1,7 +1,7 @@
 <project name='test' default='test'>
 	<import file='../build-common.xml'/>
 	<target name='test' depends='depend'>
-		<junit haltonfailure='true' printsummary='on' showoutput='true'>
+		<junit printsummary='on'>
 			<classpath>
 				<fileset refid='bundled-jars'/>
 				<fileset refid='test-jars'/>
@@ -10,7 +10,10 @@
 				<path refid='test-classes'/>
 				<path refid='util-classes'/>
 			</classpath>
+			<test name='net.sf.briar.setup.SetupWorkerTest'/>
 			<test name='net.sf.briar.util.FileUtilsTest'/>
+			<test name='net.sf.briar.util.StringUtilsTest'/>
+			<test name='net.sf.briar.util.ZipUtilsTest'/>
 		</junit>
 	</target>
 </project>
diff --git a/test/net/sf/briar/util/TestUtils.java b/test/net/sf/briar/TestUtils.java
similarity index 66%
rename from test/net/sf/briar/util/TestUtils.java
rename to test/net/sf/briar/TestUtils.java
index 5bfe71fbb8a8a1d17ca8ca10a414ad15732a9ba0..8e3e8daf30e270f1e9b1162bea1eef317f655d33 100644
--- a/test/net/sf/briar/util/TestUtils.java
+++ b/test/net/sf/briar/TestUtils.java
@@ -1,18 +1,18 @@
-package net.sf.briar.util;
+package net.sf.briar;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
 
-class TestUtils {
+public class TestUtils {
 
-	static void delete(File f) throws IOException {
+	public static void delete(File f) throws IOException {
 		if(f.isDirectory()) for(File child : f.listFiles()) delete(child);
 		f.delete();
 	}
 
-	static void createFile(File f, String s) throws IOException {
+	public static void createFile(File f, String s) throws IOException {
 		f.getParentFile().mkdirs();
 		PrintStream out = new PrintStream(new FileOutputStream(f));
 		out.print(s);
diff --git a/test/net/sf/briar/setup/SetupWorkerTest.java b/test/net/sf/briar/setup/SetupWorkerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca88539be1416a03b94a0706af74bb34498c8a3c
--- /dev/null
+++ b/test/net/sf/briar/setup/SetupWorkerTest.java
@@ -0,0 +1,168 @@
+package net.sf.briar.setup;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.ZipOutputStream;
+
+import junit.framework.TestCase;
+import net.sf.briar.TestUtils;
+import net.sf.briar.api.i18n.I18n;
+import net.sf.briar.api.setup.SetupCallback;
+import net.sf.briar.api.setup.SetupParameters;
+import net.sf.briar.util.ZipUtils;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SetupWorkerTest extends TestCase {
+
+	private static final int HEADER_SIZE = 1234;
+
+	private final File testDir = new File("test.tmp");
+	private final File jar = new File(testDir, "test.jar");
+
+	@Before
+	public void setUp() throws IOException {
+		testDir.mkdirs();
+		jar.createNewFile();
+	}
+
+	@Test
+	public void testHaltsIfNotRunningFromJar() {
+		Mockery context = new Mockery();
+		final SetupCallback callback = context.mock(SetupCallback.class);
+		SetupParameters params = context.mock(SetupParameters.class);
+		I18n i18n = context.mock(I18n.class);
+		context.checking(new Expectations() {{
+			oneOf(callback).error("Not running from jar");
+		}});
+
+		new SetupWorker(callback, params, i18n, testDir).run();
+
+		context.assertIsSatisfied();
+		File[] children = testDir.listFiles();
+		assertNotNull(children);
+		assertEquals(1, children.length);
+		assertEquals(jar, children[0]);
+	}
+
+	@Test
+	public void testHaltsIfDestinationDoesNotExist() {
+		final File nonExistent = new File(testDir, "does.not.exist");
+		Mockery context = new Mockery();
+		final SetupCallback callback = context.mock(SetupCallback.class);
+		final SetupParameters params = context.mock(SetupParameters.class);
+		I18n i18n = context.mock(I18n.class);
+		context.checking(new Expectations() {{
+			oneOf(params).getChosenLocation();
+			will(returnValue(nonExistent));
+			oneOf(callback).notFound(nonExistent);
+		}});
+
+		new SetupWorker(callback, params, i18n, jar).run();
+
+		context.assertIsSatisfied();
+		File[] children = testDir.listFiles();
+		assertNotNull(children);
+		assertEquals(1, children.length);
+		assertEquals(jar, children[0]);
+	}
+
+	@Test
+	public void testHaltsIfDestinationIsNotADirectory() {
+		Mockery context = new Mockery();
+		final SetupCallback callback = context.mock(SetupCallback.class);
+		final SetupParameters params = context.mock(SetupParameters.class);
+		I18n i18n = context.mock(I18n.class);
+		context.checking(new Expectations() {{
+			oneOf(params).getChosenLocation();
+			will(returnValue(jar));
+			oneOf(callback).notDirectory(jar);
+		}});
+
+		new SetupWorker(callback, params, i18n, jar).run();
+
+		context.assertIsSatisfied();
+		File[] children = testDir.listFiles();
+		assertNotNull(children);
+		assertEquals(1, children.length);
+		assertEquals(jar, children[0]);
+	}
+
+	@Test
+	public void testCreatesExpectedFiles() throws IOException {
+		final File setupDat = new File(testDir, "Briar/Data/setup.dat");
+		final File jreFoo = new File(testDir, "Briar/Data/jre/foo");
+		final File fooJar = new File(testDir, "Briar/Data/foo.jar");
+		final File fooTtf = new File(testDir, "Briar/Data/foo.ttf");
+		final File fooXyz = new File(testDir, "Briar/Data/foo.xyz");
+		createJar();
+
+		Mockery context = new Mockery();
+		final SetupCallback callback = context.mock(SetupCallback.class);
+		final SetupParameters params = context.mock(SetupParameters.class);
+		final I18n i18n = context.mock(I18n.class);
+		context.checking(new Expectations() {{
+			oneOf(params).getChosenLocation();
+			will(returnValue(testDir));
+			allowing(callback).isCancelled();
+			will(returnValue(false));
+			oneOf(callback).copyingFile(setupDat);
+			oneOf(params).getExeHeaderSize();
+			will(returnValue((long) HEADER_SIZE));
+			oneOf(callback).extractingFile(jreFoo);
+			oneOf(callback).extractingFile(fooJar);
+			oneOf(callback).extractingFile(fooTtf);
+			oneOf(i18n).saveLocale(new File(testDir, "Briar/Data"));
+			oneOf(callback).installed(new File(testDir, "Briar"));
+		}});
+
+		new SetupWorker(callback, params, i18n, jar).run();
+
+		context.assertIsSatisfied();
+		assertTrue(setupDat.exists());
+		assertTrue(setupDat.isFile());
+		assertEquals(jar.length(), setupDat.length());
+		assertTrue(jreFoo.exists());
+		assertTrue(jreFoo.isFile());
+		assertEquals("one one one".length(), jreFoo.length());
+		assertTrue(fooJar.exists());
+		assertTrue(fooJar.isFile());
+		assertEquals("two two two".length(), fooJar.length());
+		assertTrue(fooTtf.exists());
+		assertTrue(fooTtf.isFile());
+		assertEquals("three three three".length(), fooTtf.length());
+		assertFalse(fooXyz.exists());
+	}
+
+	private void createJar() throws IOException {
+		FileOutputStream out = new FileOutputStream(jar);
+		byte[] header = new byte[HEADER_SIZE];
+		out.write(header);
+		ZipOutputStream zip = new ZipOutputStream(out);
+		File temp = new File(testDir, "temp");
+		TestUtils.createFile(temp, "one one one");
+		ZipUtils.copyToZip("jre/foo", temp, zip);
+		temp.delete();
+		TestUtils.createFile(temp, "two two two");
+		ZipUtils.copyToZip("foo.jar", temp, zip);
+		temp.delete();
+		TestUtils.createFile(temp, "three three three");
+		ZipUtils.copyToZip("foo.ttf", temp, zip);
+		temp.delete();
+		TestUtils.createFile(temp, "four four four");
+		ZipUtils.copyToZip("foo.xyz", temp, zip);
+		temp.delete();
+		zip.flush();
+		zip.close();
+	}
+
+	@After
+	public void tearDown() throws IOException {
+		TestUtils.delete(testDir);
+	}
+}
diff --git a/test/net/sf/briar/util/FileUtilsTest.java b/test/net/sf/briar/util/FileUtilsTest.java
index 1b76d85705a1ed6b1bad16b045cae92ae869db4b..94fe9476e33fb6e0e1aae977da557a63daab6d76 100644
--- a/test/net/sf/briar/util/FileUtilsTest.java
+++ b/test/net/sf/briar/util/FileUtilsTest.java
@@ -7,6 +7,7 @@ import java.io.InputStream;
 import java.util.Scanner;
 
 import junit.framework.TestCase;
+import net.sf.briar.TestUtils;
 import net.sf.briar.util.FileUtils.Callback;
 
 import org.jmock.Expectations;
diff --git a/test/net/sf/briar/util/ZipUtilsTest.java b/test/net/sf/briar/util/ZipUtilsTest.java
index f6c1d91e04f23e73a4b0b23f53b35695ebe21383..4e95a437fac193b6ae16aff4e4034196c1befb8c 100644
--- a/test/net/sf/briar/util/ZipUtilsTest.java
+++ b/test/net/sf/briar/util/ZipUtilsTest.java
@@ -4,6 +4,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.Map;
 import java.util.Scanner;
 import java.util.TreeMap;
@@ -12,6 +13,7 @@ import java.util.zip.ZipInputStream;
 import java.util.zip.ZipOutputStream;
 
 import junit.framework.TestCase;
+import net.sf.briar.TestUtils;
 import net.sf.briar.util.ZipUtils.Callback;
 
 import org.jmock.Expectations;
@@ -24,6 +26,10 @@ public class ZipUtilsTest extends TestCase {
 
 	private final File testDir = new File("test.tmp");
 
+	private final File f1 = new File(testDir, "abc/def/1");
+	private final File f2 = new File(testDir, "abc/def/2");
+	private final File f3 = new File(testDir, "abc/3");
+
 	@Before
 	public void setUp() {
 		testDir.mkdirs();
@@ -72,15 +78,12 @@ public class ZipUtilsTest extends TestCase {
 
 	@Test
 	public void testCopyToZipRecursively() throws IOException {
-		final File src1 = new File(testDir, "abc/def/1");
-		final File src2 = new File(testDir, "abc/def/2");
-		final File src3 = new File(testDir, "abc/3");
 		Mockery context = new Mockery();
 		final Callback callback = context.mock(Callback.class);
 		context.checking(new Expectations() {{
-			oneOf(callback).processingFile(src1);
-			oneOf(callback).processingFile(src2);
-			oneOf(callback).processingFile(src3);
+			oneOf(callback).processingFile(f1);
+			oneOf(callback).processingFile(f2);
+			oneOf(callback).processingFile(f3);
 		}});
 
 		copyRecursively(callback);
@@ -94,10 +97,9 @@ public class ZipUtilsTest extends TestCase {
 	}
 
 	private void copyRecursively(Callback callback) throws IOException {
-		TestUtils.createFile(new File(testDir, "abc/def/1"), "one one one");
-		TestUtils.createFile(new File(testDir, "abc/def/2"), "two two two");
-		TestUtils.createFile(new File(testDir, "abc/3"), "three three three");
-
+		TestUtils.createFile(f1, "one one one");
+		TestUtils.createFile(f2, "two two two");
+		TestUtils.createFile(f3, "three three three");
 		File src = new File(testDir, "abc");
 		File dest = new File(testDir, "dest");
 		ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest));
@@ -113,6 +115,85 @@ public class ZipUtilsTest extends TestCase {
 		checkZipEntries(dest, expected);
 	}
 
+	@Test
+	public void testUnzipStream() throws IOException {
+		Mockery context = new Mockery();
+		final Callback callback = context.mock(Callback.class);
+		context.checking(new Expectations() {{
+			oneOf(callback).processingFile(f1);
+			oneOf(callback).processingFile(f2);
+			oneOf(callback).processingFile(f3);
+		}});
+
+		unzipStream(null, callback);
+
+		context.assertIsSatisfied();
+
+		assertTrue(f1.exists());
+		assertTrue(f1.isFile());
+		assertEquals("one one one".length(), f1.length());
+		assertTrue(f2.exists());
+		assertTrue(f2.isFile());
+		assertEquals("two two two".length(), f2.length());
+		assertTrue(f3.exists());
+		assertTrue(f3.isFile());
+		assertEquals("three three three".length(), f3.length());
+	}
+
+	@Test
+	public void testUnzipStreamWithRegex() throws IOException {
+		Mockery context = new Mockery();
+		final Callback callback = context.mock(Callback.class);
+		context.checking(new Expectations() {{
+			oneOf(callback).processingFile(f1);
+			oneOf(callback).processingFile(f2);
+		}});
+
+		unzipStream("^abc/def/.*", callback);
+
+		context.assertIsSatisfied();
+
+		assertTrue(f1.exists());
+		assertTrue(f1.isFile());
+		assertEquals("one one one".length(), f1.length());
+		assertTrue(f2.exists());
+		assertTrue(f2.isFile());
+		assertEquals("two two two".length(), f2.length());
+		assertFalse(f3.exists());
+	}
+
+	@Test
+	public void testUnzipStreamNoCallback() throws IOException {
+		unzipStream(null, null);
+
+		assertTrue(f1.exists());
+		assertTrue(f1.isFile());
+		assertEquals("one one one".length(), f1.length());
+		assertTrue(f2.exists());
+		assertTrue(f2.isFile());
+		assertEquals("two two two".length(), f2.length());
+		assertTrue(f3.exists());
+		assertTrue(f3.isFile());
+		assertEquals("three three three".length(), f3.length());
+	}
+
+	private void unzipStream(String regex, Callback callback)
+	throws IOException {
+		TestUtils.createFile(f1, "one one one");
+		TestUtils.createFile(f2, "two two two");
+		TestUtils.createFile(f3, "three three three");
+		File src = new File(testDir, "abc");
+		File dest = new File(testDir, "dest");
+		ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest));
+		ZipUtils.copyToZipRecursively(src.getName(), src, zip, null);
+		zip.flush();
+		zip.close();
+		TestUtils.delete(src);
+
+		InputStream in = new FileInputStream(dest);
+		ZipUtils.unzipStream(in, testDir, regex, callback);
+	}
+
 	@After
 	public void tearDown() throws IOException {
 		TestUtils.delete(testDir);
diff --git a/ui/net/sf/briar/ui/setup/SetupParametersImpl.java b/ui/net/sf/briar/ui/setup/SetupParametersImpl.java
index 1aca3ec08bee2b1a70cc401a53d627227ee7be13..0fb1b72a483afd3759da7f59d371cd914beca892 100644
--- a/ui/net/sf/briar/ui/setup/SetupParametersImpl.java
+++ b/ui/net/sf/briar/ui/setup/SetupParametersImpl.java
@@ -7,6 +7,8 @@ import net.sf.briar.api.setup.SetupParameters;
 
 class SetupParametersImpl implements SetupParameters {
 
+	private static final int EXE_HEADER_SIZE = 62976;
+
 	private final LocationPanel locationPanel;
 	private final FontManager fontManager;
 
@@ -22,4 +24,8 @@ class SetupParametersImpl implements SetupParameters {
 	public String[] getBundledFontFilenames() {
 		return fontManager.getBundledFontFilenames();
 	}
+
+	public long getExeHeaderSize() {
+		return EXE_HEADER_SIZE;
+	}
 }
diff --git a/util/net/sf/briar/util/ZipUtils.java b/util/net/sf/briar/util/ZipUtils.java
index 202a9b086e930853f29910212a4b893b6dd898ca..71777b5f47ee68bfd278c65e1b59d72844eb275d 100644
--- a/util/net/sf/briar/util/ZipUtils.java
+++ b/util/net/sf/briar/util/ZipUtils.java
@@ -54,18 +54,18 @@ public class ZipUtils {
 
 	/**
 	 * Unzips the given stream to the given directory, skipping any zip entries
-	 * that don't match the given regex. If the callback is not null it's
-	 * called once for each file extracted.
+	 * that don't match the given regex (a null regex matches all entries). If
+	 * the callback is not null it's called once for each file extracted.
 	 */
 	public static void unzipStream(InputStream in, File dir, String regex,
 			Callback callback) throws IOException {
-		String path = dir.getCanonicalPath();
+		String path = dir.getPath();
 		ZipInputStream zip = new ZipInputStream(in);
 		byte[] buf = new byte[1024];
 		ZipEntry entry;
 		while((entry = zip.getNextEntry()) != null) {
 			String name = entry.getName();
-			if(name.matches(regex)) {
+			if(regex == null || name.matches(regex)) {
 				File file = new File(path + "/" + name);
 				if(callback != null) callback.processingFile(file);
 				if(entry.isDirectory()) {