Check BLOB content size before trying to render it

Make sure that the content returned by JGit is below the
maximum size allowed for formatting/rendering with Soy.

The method loader.getCachedBytes(MAX_FILE_SIZE) would
return an in-memory content larger than MAX_FILE_SIZE if
the the BLOB is smaller than streamFileThreshold.

Parsing 100s megabytes of in-memory content using a
prettyfier regex and trying to render it in HTML would
result in a massive allocation of strings in the JVM heap.
The overload of memory allocation may eventually result in
triggering continuous 'stop-the-world' GC cycles, blocking
the process for several minutes.

Bug: https://github.com/google/gitiles/issues/192
Change-Id: I6ab5f367e731d67d4a5816a0beae5551106bb72b
diff --git a/java/com/google/gitiles/BlobSoyData.java b/java/com/google/gitiles/BlobSoyData.java
index c505ed4..c906b4a 100644
--- a/java/com/google/gitiles/BlobSoyData.java
+++ b/java/com/google/gitiles/BlobSoyData.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
@@ -47,7 +48,7 @@
    * will be displayed as binary files, even if the contents was text. For example really big XML
    * files may be above this limit and will get displayed as binary.
    */
-  private static final int MAX_FILE_SIZE = 10 << 20;
+  @VisibleForTesting static final int MAX_FILE_SIZE = 10 << 20;
 
   private final GitilesView view;
   private final ObjectReader reader;
@@ -70,7 +71,8 @@
     String content;
     try {
       byte[] raw = loader.getCachedBytes(MAX_FILE_SIZE);
-      content = !RawText.isBinary(raw) ? RawParseUtils.decode(raw) : null;
+      content =
+          (raw.length < MAX_FILE_SIZE && !RawText.isBinary(raw)) ? RawParseUtils.decode(raw) : null;
     } catch (LargeObjectException.OutOfMemory e) {
       throw e;
     } catch (LargeObjectException e) {
diff --git a/javatests/com/google/gitiles/PathServletTest.java b/javatests/com/google/gitiles/PathServletTest.java
index c0f98b1..998ede1 100644
--- a/javatests/com/google/gitiles/PathServletTest.java
+++ b/javatests/com/google/gitiles/PathServletTest.java
@@ -102,6 +102,24 @@
   }
 
   @Test
+  public void largeFileHtml() throws Exception {
+    int largeContentSize = BlobSoyData.MAX_FILE_SIZE + 1;
+    repo.branch("master").commit().add("foo", generateContent(largeContentSize)).create();
+
+    Map<String, ?> data = (Map<String, ?>) buildData("/repo/+/master/foo").get("data");
+    assertThat(data).containsEntry("lines", null);
+    assertThat(data).containsEntry("size", "" + largeContentSize);
+  }
+
+  private static String generateContent(int contentSize) {
+    char[] str = new char[contentSize];
+    for (int i = 0; i < contentSize; i++) {
+      str[i] = (char) ('0' + (i % 78));
+    }
+    return new String(str);
+  }
+
+  @Test
   public void symlinkHtml() throws Exception {
     final RevBlob link = repo.blob("foo");
     repo.branch("master")