View Javadoc
1   /*
2    * Copyright (C) 2012 Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.pgm;
11  
12  import static java.nio.charset.StandardCharsets.UTF_8;
13  import static org.junit.Assert.assertArrayEquals;
14  import static org.junit.Assert.assertEquals;
15  import static org.junit.Assert.fail;
16  import static org.junit.Assume.assumeNoException;
17  
18  import java.io.BufferedInputStream;
19  import java.io.BufferedReader;
20  import java.io.ByteArrayInputStream;
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.InputStreamReader;
26  import java.io.OutputStream;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.List;
30  import java.util.concurrent.ExecutorService;
31  import java.util.concurrent.Executors;
32  import java.util.concurrent.Future;
33  import java.util.zip.ZipEntry;
34  import java.util.zip.ZipInputStream;
35  
36  import org.eclipse.jgit.api.Git;
37  import org.eclipse.jgit.dircache.DirCache;
38  import org.eclipse.jgit.lib.CLIRepositoryTestCase;
39  import org.eclipse.jgit.lib.FileMode;
40  import org.junit.Before;
41  import org.junit.Test;
42  
43  public class ArchiveTest extends CLIRepositoryTestCase {
44  	private Git git;
45  	private String emptyTree;
46  
47  	@Override
48  	@Before
49  	public void setUp() throws Exception {
50  		super.setUp();
51  		git = new Git(db);
52  		git.commit().setMessage("initial commit").call();
53  		emptyTree = db.resolve("HEAD^{tree}").abbreviate(12).name();
54  	}
55  
56  	@Test
57  	public void testEmptyArchive() throws Exception {
58  		byte[] result = CLIGitCommand.executeRaw(
59  				"git archive --format=zip " + emptyTree, db).outBytes();
60  		assertArrayEquals(new String[0], listZipEntries(result));
61  	}
62  
63  	@Test
64  	public void testEmptyTar() throws Exception {
65  		byte[] result = CLIGitCommand.executeRaw(
66  				"git archive --format=tar " + emptyTree, db).outBytes();
67  		assertArrayEquals(new String[0], listTarEntries(result));
68  	}
69  
70  	@Test
71  	public void testUnrecognizedFormat() throws Exception {
72  		String[] expect = new String[] {
73  				"fatal: Unknown archive format 'nonsense'", "" };
74  		String[] actual = executeUnchecked(
75  				"git archive --format=nonsense " + emptyTree);
76  		assertArrayEquals(expect, actual);
77  	}
78  
79  	@Test
80  	public void testArchiveWithFiles() throws Exception {
81  		writeTrashFile("a", "a file with content!");
82  		writeTrashFile("c", ""); // empty file
83  		writeTrashFile("unrelated", "another file, just for kicks");
84  		git.add().addFilepattern("a").call();
85  		git.add().addFilepattern("c").call();
86  		git.commit().setMessage("populate toplevel").call();
87  
88  		byte[] result = CLIGitCommand.executeRaw(
89  				"git archive --format=zip HEAD", db).outBytes();
90  		assertArrayEquals(new String[] { "a", "c" },
91  				listZipEntries(result));
92  	}
93  
94  	private void commitGreeting() throws Exception {
95  		writeTrashFile("greeting", "hello, world!");
96  		git.add().addFilepattern("greeting").call();
97  		git.commit().setMessage("a commit with a file").call();
98  	}
99  
100 	@Test
101 	public void testDefaultFormatIsTar() throws Exception {
102 		commitGreeting();
103 		byte[] result = CLIGitCommand.executeRaw(
104 				"git archive HEAD", db).outBytes();
105 		assertArrayEquals(new String[] { "greeting" },
106 				listTarEntries(result));
107 	}
108 
109 	@Test
110 	public void testFormatOverridesFilename() throws Exception {
111 		File archive = new File(db.getWorkTree(), "format-overrides-name.tar");
112 		String path = archive.getAbsolutePath();
113 
114 		commitGreeting();
115 		assertArrayEquals(new String[] { "" },
116 				execute("git archive " +
117 					"--format=zip " +
118 					shellQuote("--output=" + path) + " " +
119 					"HEAD"));
120 		assertContainsEntryWithMode(path, "", "greeting");
121 		assertIsZip(archive);
122 	}
123 
124 	@Test
125 	public void testUnrecognizedExtensionMeansTar() throws Exception {
126 		File archive = new File(db.getWorkTree(), "example.txt");
127 		String path = archive.getAbsolutePath();
128 
129 		commitGreeting();
130 		assertArrayEquals(new String[] { "" },
131 				execute("git archive " +
132 					shellQuote("--output=" + path) + " " +
133 					"HEAD"));
134 		assertTarContainsEntry(path, "", "greeting");
135 		assertIsTar(archive);
136 	}
137 
138 	@Test
139 	public void testNoExtensionMeansTar() throws Exception {
140 		File archive = new File(db.getWorkTree(), "example");
141 		String path = archive.getAbsolutePath();
142 
143 		commitGreeting();
144 		assertArrayEquals(new String[] { "" },
145 				execute("git archive " +
146 					shellQuote("--output=" + path) + " " +
147 					"HEAD"));
148 		assertIsTar(archive);
149 	}
150 
151 	@Test
152 	public void testExtensionMatchIsAnchored() throws Exception {
153 		File archive = new File(db.getWorkTree(), "two-extensions.zip.bak");
154 		String path = archive.getAbsolutePath();
155 
156 		commitGreeting();
157 		assertArrayEquals(new String[] { "" },
158 				execute("git archive " +
159 					shellQuote("--output=" + path) + " " +
160 					"HEAD"));
161 		assertIsTar(archive);
162 	}
163 
164 	@Test
165 	public void testZipExtension() throws Exception {
166 		File archiveWithDot = new File(db.getWorkTree(), "greeting.zip");
167 		File archiveNoDot = new File(db.getWorkTree(), "greetingzip");
168 
169 		commitGreeting();
170 		execute("git archive " +
171 			shellQuote("--output=" + archiveWithDot.getAbsolutePath()) + " " +
172 			"HEAD");
173 		execute("git archive " +
174 			shellQuote("--output=" + archiveNoDot.getAbsolutePath()) + " " +
175 			"HEAD");
176 		assertIsZip(archiveWithDot);
177 		assertIsTar(archiveNoDot);
178 	}
179 
180 	@Test
181 	public void testTarExtension() throws Exception {
182 		File archive = new File(db.getWorkTree(), "tarball.tar");
183 		String path = archive.getAbsolutePath();
184 
185 		commitGreeting();
186 		assertArrayEquals(new String[] { "" },
187 				execute("git archive " +
188 					shellQuote("--output=" + path) + " " +
189 					"HEAD"));
190 		assertIsTar(archive);
191 	}
192 
193 	@Test
194 	public void testTgzExtensions() throws Exception {
195 		commitGreeting();
196 
197 		for (String ext : Arrays.asList("tar.gz", "tgz")) {
198 			File archiveWithDot = new File(db.getWorkTree(), "tarball." + ext);
199 			File archiveNoDot = new File(db.getWorkTree(), "tarball" + ext);
200 
201 			execute("git archive " +
202 				shellQuote("--output=" + archiveWithDot.getAbsolutePath()) + " " +
203 				"HEAD");
204 			execute("git archive " +
205 				shellQuote("--output=" + archiveNoDot.getAbsolutePath()) + " " +
206 				"HEAD");
207 			assertIsGzip(archiveWithDot);
208 			assertIsTar(archiveNoDot);
209 		}
210 	}
211 
212 	@Test
213 	public void testTbz2Extension() throws Exception {
214 		commitGreeting();
215 
216 		for (String ext : Arrays.asList("tar.bz2", "tbz", "tbz2")) {
217 			File archiveWithDot = new File(db.getWorkTree(), "tarball." + ext);
218 			File archiveNoDot = new File(db.getWorkTree(), "tarball" + ext);
219 
220 			execute("git archive " +
221 				shellQuote("--output=" + archiveWithDot.getAbsolutePath()) + " " +
222 				"HEAD");
223 			execute("git archive " +
224 				shellQuote("--output=" + archiveNoDot.getAbsolutePath()) + " " +
225 				"HEAD");
226 			assertIsBzip2(archiveWithDot);
227 			assertIsTar(archiveNoDot);
228 		}
229 	}
230 
231 	@Test
232 	public void testTxzExtension() throws Exception {
233 		commitGreeting();
234 
235 		for (String ext : Arrays.asList("tar.xz", "txz")) {
236 			File archiveWithDot = new File(db.getWorkTree(), "tarball." + ext);
237 			File archiveNoDot = new File(db.getWorkTree(), "tarball" + ext);
238 
239 			execute("git archive " +
240 				shellQuote("--output=" + archiveWithDot.getAbsolutePath()) + " " +
241 				"HEAD");
242 			execute("git archive " +
243 				shellQuote("--output=" + archiveNoDot.getAbsolutePath()) + " " +
244 				"HEAD");
245 			assertIsXz(archiveWithDot);
246 			assertIsTar(archiveNoDot);
247 		}
248 	}
249 
250 	@Test
251 	public void testArchiveWithSubdir() throws Exception {
252 		writeTrashFile("a", "a file with content!");
253 		writeTrashFile("b.c", "before subdir in git sort order");
254 		writeTrashFile("b0c", "after subdir in git sort order");
255 		writeTrashFile("c", "");
256 		git.add().addFilepattern("a").call();
257 		git.add().addFilepattern("b.c").call();
258 		git.add().addFilepattern("b0c").call();
259 		git.add().addFilepattern("c").call();
260 		git.commit().setMessage("populate toplevel").call();
261 		writeTrashFile("b/b", "file in subdirectory");
262 		writeTrashFile("b/a", "another file in subdirectory");
263 		git.add().addFilepattern("b").call();
264 		git.commit().setMessage("add subdir").call();
265 
266 		byte[] result = CLIGitCommand.executeRaw(
267 				"git archive --format=zip master", db).outBytes();
268 		String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" };
269 		String[] actual = listZipEntries(result);
270 
271 		Arrays.sort(expect);
272 		Arrays.sort(actual);
273 		assertArrayEquals(expect, actual);
274 	}
275 
276 	@Test
277 	public void testTarWithSubdir() throws Exception {
278 		writeTrashFile("a", "a file with content!");
279 		writeTrashFile("b.c", "before subdir in git sort order");
280 		writeTrashFile("b0c", "after subdir in git sort order");
281 		writeTrashFile("c", "");
282 		git.add().addFilepattern("a").call();
283 		git.add().addFilepattern("b.c").call();
284 		git.add().addFilepattern("b0c").call();
285 		git.add().addFilepattern("c").call();
286 		git.commit().setMessage("populate toplevel").call();
287 		writeTrashFile("b/b", "file in subdirectory");
288 		writeTrashFile("b/a", "another file in subdirectory");
289 		git.add().addFilepattern("b").call();
290 		git.commit().setMessage("add subdir").call();
291 
292 		byte[] result = CLIGitCommand.executeRaw(
293 				"git archive --format=tar master", db).outBytes();
294 		String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" };
295 		String[] actual = listTarEntries(result);
296 
297 		Arrays.sort(expect);
298 		Arrays.sort(actual);
299 		assertArrayEquals(expect, actual);
300 	}
301 
302 	private void commitBazAndFooSlashBar() throws Exception {
303 		writeTrashFile("baz", "a file");
304 		writeTrashFile("foo/bar", "another file");
305 		git.add().addFilepattern("baz").call();
306 		git.add().addFilepattern("foo").call();
307 		git.commit().setMessage("sample commit").call();
308 	}
309 
310 	@Test
311 	public void testArchivePrefixOption() throws Exception {
312 		commitBazAndFooSlashBar();
313 		byte[] result = CLIGitCommand.executeRaw(
314 				"git archive --prefix=x/ --format=zip master", db).outBytes();
315 		String[] expect = { "x/", "x/baz", "x/foo/", "x/foo/bar" };
316 		String[] actual = listZipEntries(result);
317 
318 		Arrays.sort(expect);
319 		Arrays.sort(actual);
320 		assertArrayEquals(expect, actual);
321 	}
322 
323 	@Test
324 	public void testTarPrefixOption() throws Exception {
325 		commitBazAndFooSlashBar();
326 		byte[] result = CLIGitCommand.executeRaw(
327 				"git archive --prefix=x/ --format=tar master", db).outBytes();
328 		String[] expect = { "x/", "x/baz", "x/foo/", "x/foo/bar" };
329 		String[] actual = listTarEntries(result);
330 
331 		Arrays.sort(expect);
332 		Arrays.sort(actual);
333 		assertArrayEquals(expect, actual);
334 	}
335 
336 	private void commitFoo() throws Exception {
337 		writeTrashFile("foo", "a file");
338 		git.add().addFilepattern("foo").call();
339 		git.commit().setMessage("boring commit").call();
340 	}
341 
342 	@Test
343 	public void testPrefixDoesNotNormalizeDoubleSlash() throws Exception {
344 		commitFoo();
345 		byte[] result = CLIGitCommand.executeRaw(
346 				"git archive --prefix=x// --format=zip master", db).outBytes();
347 		String[] expect = { "x/", "x//foo" };
348 		assertArrayEquals(expect, listZipEntries(result));
349 	}
350 
351 	@Test
352 	public void testPrefixDoesNotNormalizeDoubleSlashInTar() throws Exception {
353 		commitFoo();
354 		byte[] result = CLIGitCommand.executeRaw(
355 				"git archive --prefix=x// --format=tar master", db).outBytes();
356 		String[] expect = { "x/", "x//foo" };
357 		assertArrayEquals(expect, listTarEntries(result));
358 	}
359 
360 	/**
361 	 * The prefix passed to "git archive" need not end with '/'.
362 	 * In practice it is not very common to have a nonempty prefix
363 	 * that does not name a directory (and hence end with /), but
364 	 * since git has historically supported other prefixes, we do,
365 	 * too.
366 	 *
367 	 * @throws Exception
368 	 */
369 	@Test
370 	public void testPrefixWithoutTrailingSlash() throws Exception {
371 		commitBazAndFooSlashBar();
372 		byte[] result = CLIGitCommand.executeRaw(
373 				"git archive --prefix=my- --format=zip master", db).outBytes();
374 		String[] expect = { "my-baz", "my-foo/", "my-foo/bar" };
375 		String[] actual = listZipEntries(result);
376 
377 		Arrays.sort(expect);
378 		Arrays.sort(actual);
379 		assertArrayEquals(expect, actual);
380 	}
381 
382 	@Test
383 	public void testTarPrefixWithoutTrailingSlash() throws Exception {
384 		commitBazAndFooSlashBar();
385 		byte[] result = CLIGitCommand.executeRaw(
386 				"git archive --prefix=my- --format=tar master", db).outBytes();
387 		String[] expect = { "my-baz", "my-foo/", "my-foo/bar" };
388 		String[] actual = listTarEntries(result);
389 
390 		Arrays.sort(expect);
391 		Arrays.sort(actual);
392 		assertArrayEquals(expect, actual);
393 	}
394 
395 	@Test
396 	public void testArchiveIncludesSubmoduleDirectory() throws Exception {
397 		writeTrashFile("a", "a file with content!");
398 		writeTrashFile("c", "after submodule");
399 		git.add().addFilepattern("a").call();
400 		git.add().addFilepattern("c").call();
401 		git.commit().setMessage("initial commit").call();
402 		git.submoduleAdd().setURI("./.").setPath("b").call().close();
403 		git.commit().setMessage("add submodule").call();
404 
405 		byte[] result = CLIGitCommand.executeRaw(
406 				"git archive --format=zip master", db).outBytes();
407 		String[] expect = { ".gitmodules", "a", "b/", "c" };
408 		String[] actual = listZipEntries(result);
409 
410 		Arrays.sort(expect);
411 		Arrays.sort(actual);
412 		assertArrayEquals(expect, actual);
413 	}
414 
415 	@Test
416 	public void testTarIncludesSubmoduleDirectory() throws Exception {
417 		writeTrashFile("a", "a file with content!");
418 		writeTrashFile("c", "after submodule");
419 		git.add().addFilepattern("a").call();
420 		git.add().addFilepattern("c").call();
421 		git.commit().setMessage("initial commit").call();
422 		git.submoduleAdd().setURI("./.").setPath("b").call().close();
423 		git.commit().setMessage("add submodule").call();
424 
425 		byte[] result = CLIGitCommand.executeRaw(
426 				"git archive --format=tar master", db).outBytes();
427 		String[] expect = { ".gitmodules", "a", "b/", "c" };
428 		String[] actual = listTarEntries(result);
429 
430 		Arrays.sort(expect);
431 		Arrays.sort(actual);
432 		assertArrayEquals(expect, actual);
433 	}
434 
435 	@Test
436 	public void testArchivePreservesMode() throws Exception {
437 		writeTrashFile("plain", "a file with content");
438 		writeTrashFile("executable", "an executable file");
439 		writeTrashFile("symlink", "plain");
440 		writeTrashFile("dir/content", "clutter in a subdir");
441 		git.add().addFilepattern("plain").call();
442 		git.add().addFilepattern("executable").call();
443 		git.add().addFilepattern("symlink").call();
444 		git.add().addFilepattern("dir").call();
445 
446 		DirCache cache = db.lockDirCache();
447 		cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE);
448 		cache.getEntry("symlink").setFileMode(FileMode.SYMLINK);
449 		cache.write();
450 		cache.commit();
451 		cache.unlock();
452 
453 		git.commit().setMessage("three files with different modes").call();
454 
455 		byte[] zipData = CLIGitCommand.executeRaw(
456 				"git archive --format=zip master", db).outBytes();
457 		writeRaw("zip-with-modes.zip", zipData);
458 		assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "plain");
459 		assertContainsEntryWithMode("zip-with-modes.zip", "-rwx", "executable");
460 		assertContainsEntryWithMode("zip-with-modes.zip", "l", "symlink");
461 		assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "dir/");
462 	}
463 
464 	@Test
465 	public void testTarPreservesMode() throws Exception {
466 		writeTrashFile("plain", "a file with content");
467 		writeTrashFile("executable", "an executable file");
468 		writeTrashFile("symlink", "plain");
469 		writeTrashFile("dir/content", "clutter in a subdir");
470 		git.add().addFilepattern("plain").call();
471 		git.add().addFilepattern("executable").call();
472 		git.add().addFilepattern("symlink").call();
473 		git.add().addFilepattern("dir").call();
474 
475 		DirCache cache = db.lockDirCache();
476 		cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE);
477 		cache.getEntry("symlink").setFileMode(FileMode.SYMLINK);
478 		cache.write();
479 		cache.commit();
480 		cache.unlock();
481 
482 		git.commit().setMessage("three files with different modes").call();
483 
484 		byte[] archive = CLIGitCommand.executeRaw(
485 				"git archive --format=tar master", db).outBytes();
486 		writeRaw("with-modes.tar", archive);
487 		assertTarContainsEntry("with-modes.tar", "-rw-r--r--", "plain");
488 		assertTarContainsEntry("with-modes.tar", "-rwxr-xr-x", "executable");
489 		assertTarContainsEntry("with-modes.tar", "l", "symlink -> plain");
490 		assertTarContainsEntry("with-modes.tar", "drwxr-xr-x", "dir/");
491 	}
492 
493 	@Test
494 	public void testArchiveWithLongFilename() throws Exception {
495 		StringBuilder filename = new StringBuilder();
496 		List<String> l = new ArrayList<>();
497 		for (int i = 0; i < 20; i++) {
498 			filename.append("1234567890/");
499 			l.add(filename.toString());
500 		}
501 		filename.append("1234567890");
502 		l.add(filename.toString());
503 		writeTrashFile(filename.toString(), "file with long path");
504 		git.add().addFilepattern("1234567890").call();
505 		git.commit().setMessage("file with long name").call();
506 
507 		byte[] result = CLIGitCommand.executeRaw(
508 				"git archive --format=zip HEAD", db).outBytes();
509 		assertArrayEquals(l.toArray(new String[0]),
510 				listZipEntries(result));
511 	}
512 
513 	@Test
514 	public void testTarWithLongFilename() throws Exception {
515 		StringBuilder filename = new StringBuilder();
516 		List<String> l = new ArrayList<>();
517 		for (int i = 0; i < 20; i++) {
518 			filename.append("1234567890/");
519 			l.add(filename.toString());
520 		}
521 		filename.append("1234567890");
522 		l.add(filename.toString());
523 		writeTrashFile(filename.toString(), "file with long path");
524 		git.add().addFilepattern("1234567890").call();
525 		git.commit().setMessage("file with long name").call();
526 
527 		byte[] result = CLIGitCommand.executeRaw(
528 				"git archive --format=tar HEAD", db).outBytes();
529 		assertArrayEquals(l.toArray(new String[0]),
530 				listTarEntries(result));
531 	}
532 
533 	@Test
534 	public void testArchivePreservesContent() throws Exception {
535 		String payload = "“The quick brown fox jumps over the lazy dog!”";
536 		writeTrashFile("xyzzy", payload);
537 		git.add().addFilepattern("xyzzy").call();
538 		git.commit().setMessage("add file with content").call();
539 
540 		byte[] result = CLIGitCommand.executeRaw(
541 				"git archive --format=zip HEAD", db).outBytes();
542 		assertArrayEquals(new String[] { payload },
543 				zipEntryContent(result, "xyzzy"));
544 	}
545 
546 	@Test
547 	public void testTarPreservesContent() throws Exception {
548 		String payload = "“The quick brown fox jumps over the lazy dog!”";
549 		writeTrashFile("xyzzy", payload);
550 		git.add().addFilepattern("xyzzy").call();
551 		git.commit().setMessage("add file with content").call();
552 
553 		byte[] result = CLIGitCommand.executeRaw(
554 				"git archive --format=tar HEAD", db).outBytes();
555 		assertArrayEquals(new String[] { payload },
556 				tarEntryContent(result, "xyzzy"));
557 	}
558 
559 	private Process spawnAssumingCommandPresent(String... cmdline) {
560 		File cwd = db.getWorkTree();
561 		ProcessBuilder procBuilder = new ProcessBuilder(cmdline)
562 				.directory(cwd)
563 				.redirectErrorStream(true);
564 		Process proc = null;
565 		try {
566 			proc = procBuilder.start();
567 		} catch (IOException e) {
568 			// On machines without `cmdline[0]`, let the test pass.
569 			assumeNoException(e);
570 		}
571 
572 		return proc;
573 	}
574 
575 	private BufferedReader readFromProcess(Process proc) throws Exception {
576 		return new BufferedReader(
577 				new InputStreamReader(proc.getInputStream(), UTF_8));
578 	}
579 
580 	private void grepForEntry(String name, String mode, String... cmdline)
581 			throws Exception {
582 		Process proc = spawnAssumingCommandPresent(cmdline);
583 		proc.getOutputStream().close();
584 		BufferedReader reader = readFromProcess(proc);
585 		try {
586 			String line;
587 			while ((line = reader.readLine()) != null)
588 				if (line.startsWith(mode) && line.endsWith(name))
589 					// found it!
590 					return;
591 			fail("expected entry " + name + " with mode " + mode + " but found none");
592 		} finally {
593 			proc.getOutputStream().close();
594 			proc.destroy();
595 		}
596 	}
597 
598 	private void assertMagic(long offset, byte[] magicBytes, File file) throws Exception {
599 		try (BufferedInputStream in = new BufferedInputStream(
600 				new FileInputStream(file))) {
601 			if (offset > 0) {
602 				long skipped = in.skip(offset);
603 				assertEquals(offset, skipped);
604 			}
605 
606 			byte[] actual = new byte[magicBytes.length];
607 			in.read(actual);
608 			assertArrayEquals(magicBytes, actual);
609 		}
610 	}
611 
612 	private void assertMagic(byte[] magicBytes, File file) throws Exception {
613 		assertMagic(0, magicBytes, file);
614 	}
615 
616 	private void assertIsTar(File file) throws Exception {
617 		assertMagic(257, new byte[] { 'u', 's', 't', 'a', 'r', 0 }, file);
618 	}
619 
620 	private void assertIsZip(File file) throws Exception {
621 		assertMagic(new byte[] { 'P', 'K', 3, 4 }, file);
622 	}
623 
624 	private void assertIsGzip(File file) throws Exception {
625 		assertMagic(new byte[] { 037, (byte) 0213 }, file);
626 	}
627 
628 	private void assertIsBzip2(File file) throws Exception {
629 		assertMagic(new byte[] { 'B', 'Z', 'h' }, file);
630 	}
631 
632 	private void assertIsXz(File file) throws Exception {
633 		assertMagic(new byte[] { (byte) 0xfd, '7', 'z', 'X', 'Z', 0 }, file);
634 	}
635 
636 	private void assertContainsEntryWithMode(String zipFilename, String mode, String name)
637 			throws Exception {
638 		grepForEntry(name, mode, "zipinfo", zipFilename);
639 	}
640 
641 	private void assertTarContainsEntry(String tarfile, String mode, String name)
642 			throws Exception {
643 		grepForEntry(name, mode, "tar", "tvf", tarfile);
644 	}
645 
646 	private void writeRaw(String filename, byte[] data)
647 			throws IOException {
648 		File path = new File(db.getWorkTree(), filename);
649 		try (OutputStream out = new FileOutputStream(path)) {
650 			out.write(data);
651 		}
652 	}
653 
654 	private static String[] listZipEntries(byte[] zipData) throws IOException {
655 		List<String> l = new ArrayList<>();
656 		try (ZipInputStream in = new ZipInputStream(
657 				new ByteArrayInputStream(zipData))) {
658 			ZipEntry e;
659 			while ((e = in.getNextEntry()) != null)
660 				l.add(e.getName());
661 		}
662 		return l.toArray(new String[0]);
663 	}
664 
665 	private static Future<Object> writeAsync(OutputStream stream, byte[] data) {
666 		ExecutorService executor = Executors.newSingleThreadExecutor();
667 
668 		return executor.submit(() -> {
669 			try {
670 				stream.write(data);
671 				return null;
672 			} finally {
673 				stream.close();
674 			}
675 		});
676 	}
677 
678 	private String[] listTarEntries(byte[] tarData) throws Exception {
679 		List<String> l = new ArrayList<>();
680 		Process proc = spawnAssumingCommandPresent("tar", "tf", "-");
681 		try (BufferedReader reader = readFromProcess(proc)) {
682 			OutputStream out = proc.getOutputStream();
683 
684 			// Dump tarball to tar stdin in background
685 			Future<?> writing = writeAsync(out, tarData);
686 
687 			try {
688 				String line;
689 				while ((line = reader.readLine()) != null)
690 					l.add(line);
691 
692 				return l.toArray(new String[0]);
693 			} finally {
694 				writing.get();
695 				proc.destroy();
696 			}
697 		}
698 	}
699 
700 	private static String[] zipEntryContent(byte[] zipData, String path)
701 			throws IOException {
702 		ZipInputStream in = new ZipInputStream(
703 				new ByteArrayInputStream(zipData));
704 		ZipEntry e;
705 		while ((e = in.getNextEntry()) != null) {
706 			if (!e.getName().equals(path))
707 				continue;
708 
709 			// found!
710 			List<String> l = new ArrayList<>();
711 			BufferedReader reader = new BufferedReader(
712 					new InputStreamReader(in, UTF_8));
713 			String line;
714 			while ((line = reader.readLine()) != null)
715 				l.add(line);
716 			return l.toArray(new String[0]);
717 		}
718 
719 		// not found
720 		return null;
721 	}
722 
723 	private String[] tarEntryContent(byte[] tarData, String path)
724 			throws Exception {
725 		List<String> l = new ArrayList<>();
726 		Process proc = spawnAssumingCommandPresent("tar", "Oxf", "-", path);
727 		try (BufferedReader reader = readFromProcess(proc)) {
728 			OutputStream out = proc.getOutputStream();
729 			Future<?> writing = writeAsync(out, tarData);
730 
731 			try {
732 				String line;
733 				while ((line = reader.readLine()) != null)
734 					l.add(line);
735 
736 				return l.toArray(new String[0]);
737 			} finally {
738 				writing.get();
739 				proc.destroy();
740 			}
741 		}
742 	}
743 }