TemporaryBuffer.LocalFile: use Files.newInputStream()

On Unix, FileInputStream.skip() uses lseek under the hood and allows
skipping beyond EOF, which makes WorkingTreeIterator.computeLength() go
into an endless loop.

Files.newInputStream() returns a sun.nio.ch.ChannelInputStream, which
for files does not skip beyond EOF, avoiding this endless loop.

At the point WorkingTreeIterator.computeLength() is called, the input
stream is either a CR/LF-processing stream, which extend InputStream,
which actually uses reads to implement skipping and thus doesn't skip
beyond EOF, or a BlockInputStream, which also does not skip beyond the
content length, or a stream obtained from TemporaryBuffer.LocalFile.

Bug: jgit-261
Change-Id: I2e1bfe04c308220a1c2d819e6414a6ff7b2f9c34
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java
index 0513da2..f7dab12 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java
@@ -16,8 +16,11 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Files;
 import java.util.HashSet;
+import java.util.Random;
 import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.MergeResult;
@@ -70,6 +73,33 @@ public int run() throws IOException {
 		}
 	}
 
+	private static class NoOpCommandFactory implements FilterCommandFactory {
+
+		@Override
+		public FilterCommand create(Repository db, InputStream in,
+				OutputStream out) throws IOException {
+			return new FilterCommand(in, out) {
+
+				private byte[] buf = new byte[32 * 1024];
+
+				@Override
+				public int run() throws IOException {
+					int n = in.read(buf);
+					if (n < 0) {
+						in.close();
+						out.close();
+						return -1;
+					}
+					if (n > 0) {
+						out.write(buf, 0, n);
+					}
+					return n;
+				}
+
+			};
+		}
+	}
+
 	@Override
 	@Before
 	public void setUp() throws Exception {
@@ -149,6 +179,46 @@ public void testBuiltinCleanFilter() throws Exception {
 	}
 
 	@Test
+	public void testCleanLargeFile() throws Exception {
+		String builtinCommandName = "jgit://builtin/test/clean";
+		FilterCommandRegistry.register(builtinCommandName,
+				new NoOpCommandFactory());
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString("filter", "test", "clean", builtinCommandName);
+		config.save();
+
+		writeTrashFile(".gitattributes", "*.bin filter=test");
+		git.add().addFilepattern(".gitattributes").call();
+		git.commit().setMessage("add filter").call();
+
+		File f = new File(git.getRepository().getWorkTree(), "a.bin");
+		long length = 0;
+		try (OutputStream out = Files.newOutputStream(f.toPath())) {
+			byte[] buf = new byte[8 * 1024];
+			Random r = ThreadLocalRandom.current();
+			while (length < TemporaryBuffer.DEFAULT_IN_CORE_LIMIT) {
+				r.nextBytes(buf);
+				buf[0] = 0;
+				out.write(buf);
+				length += buf.length;
+			}
+			// Write a few bytes extra, just so that we don't have a
+			// multiple of 8k, and we're beyond the in-memory limit.
+			r.nextBytes(buf);
+			out.write(buf, 0, 19);
+			length += 19;
+		}
+
+		fsTick(f);
+		git.add().addFilepattern("a.bin").call();
+		assertEquals(
+				"[.gitattributes, mode:100644, length:17]"
+						+ "[Test.txt, mode:100644, length:11]"
+						+ "[a.bin, mode:100644, length:" + length + "]",
+				indexState(LENGTH));
+	}
+
+	@Test
 	public void testBuiltinSmudgeFilter() throws IOException, GitAPIException {
 		String builtinCommandName = "jgit://builtin/test/smudge";
 		FilterCommandRegistry.register(builtinCommandName,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
index 0f45162..fdb2679 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
@@ -15,10 +15,12 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.UncheckedIOException;
+import java.nio.file.Files;
 import java.util.ArrayList;
 
 import org.eclipse.jgit.internal.JGitText;
@@ -556,9 +558,10 @@ public void writeTo(OutputStream os, ProgressMonitor pm)
 
 		@Override
 		public InputStream openInputStream() throws IOException {
-			if (onDiskFile == null)
+			if (onDiskFile == null) {
 				return super.openInputStream();
-			return new FileInputStream(onDiskFile);
+			}
+			return Files.newInputStream(onDiskFile.toPath());
 		}
 
 		@Override
@@ -566,7 +569,8 @@ public InputStream openInputStreamWithAutoDestroy() throws IOException {
 			if (onDiskFile == null) {
 				return super.openInputStreamWithAutoDestroy();
 			}
-			return new FileInputStream(onDiskFile) {
+			return new FilterInputStream(
+					Files.newInputStream(onDiskFile.toPath())) {
 				@Override
 				public void close() throws IOException {
 					super.close();