Support '\r\n' separators when splitting Ninja files into separately ?

?parsed fragments.

Closes #10210.

PiperOrigin-RevId: 280170780
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/BufferSplitter.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/BufferSplitter.java
index 9372bf8..2e1db30 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/BufferSplitter.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/BufferSplitter.java
@@ -30,23 +30,23 @@
 public class BufferSplitter implements Callable<List<ByteFragmentAtOffset>> {
   private final ByteBufferFragment bufferFragment;
   private final DeclarationConsumer consumer;
-  private final SeparatorPredicate separatorPredicate;
+  private final SeparatorFinder separatorFinder;
   private final int offset;
 
   /**
    * @param bufferFragment {@link ByteBufferFragment}, fragment of which should be splitted
    * @param consumer declaration consumer
-   * @param separatorPredicate predicate for separating declarations
+   * @param separatorFinder finds declaration separators
    * @param offset start offset of <code>buffer</code> from the beginning of the file
    */
   public BufferSplitter(
       ByteBufferFragment bufferFragment,
       DeclarationConsumer consumer,
-      SeparatorPredicate separatorPredicate,
+      SeparatorFinder separatorFinder,
       int offset) {
     this.bufferFragment = bufferFragment;
     this.consumer = consumer;
-    this.separatorPredicate = separatorPredicate;
+    this.separatorFinder = separatorFinder;
     this.offset = offset;
   }
 
@@ -61,22 +61,19 @@
   public List<ByteFragmentAtOffset> call() throws Exception {
     List<ByteFragmentAtOffset> fragments = Lists.newArrayList();
     int start = 0;
-    for (int i = 0; i < bufferFragment.length() - 2; i++) {
-      byte previous = bufferFragment.byteAt(i);
-      byte current = bufferFragment.byteAt(i + 1);
-      byte next = bufferFragment.byteAt(i + 2);
-
-      if (!separatorPredicate.test(previous, current, next)) {
-        continue;
+    while (true) {
+      int end = separatorFinder.findNextSeparator(bufferFragment, start, -1);
+      if (end < 0) {
+        break;
       }
-      ByteBufferFragment fragment = bufferFragment.subFragment(start, i + 2);
+      ByteBufferFragment fragment = bufferFragment.subFragment(start, end + 1);
       ByteFragmentAtOffset fragmentAtOffset = new ByteFragmentAtOffset(offset, fragment);
       if (start > 0) {
         consumer.declaration(fragmentAtOffset);
       } else {
         fragments.add(fragmentAtOffset);
       }
-      start = i + 2;
+      start = end + 1;
     }
     // There is always at least one byte at the bounds of the fragment.
     ByteBufferFragment lastFragment = bufferFragment.subFragment(start, bufferFragment.length());
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/DeclarationAssembler.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/DeclarationAssembler.java
index 383335a..97667d1 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/DeclarationAssembler.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/DeclarationAssembler.java
@@ -18,6 +18,8 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Range;
+import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 
@@ -28,18 +30,18 @@
  */
 public class DeclarationAssembler {
   private final DeclarationConsumer declarationConsumer;
-  private final SeparatorPredicate separatorPredicate;
+  private final SeparatorFinder separatorFinder;
 
   /**
    * @param declarationConsumer delegate declaration consumer for actual processing / parsing
-   * @param separatorPredicate predicate used to determine if two fragments should be separate
+   * @param separatorFinder callback used to determine if two fragments should be separate
    *     declarations (in the Ninja case, if the new line starts with a space, it should be treated
    *     as a part of the previous declaration, i.e. the separator is longer then one symbol).
    */
   public DeclarationAssembler(
-      DeclarationConsumer declarationConsumer, SeparatorPredicate separatorPredicate) {
+      DeclarationConsumer declarationConsumer, SeparatorFinder separatorFinder) {
     this.declarationConsumer = declarationConsumer;
-    this.separatorPredicate = separatorPredicate;
+    this.separatorFinder = separatorFinder;
   }
 
   /**
@@ -69,56 +71,60 @@
   }
 
   private void sendMerged(List<ByteFragmentAtOffset> list) throws GenericParsingException {
-    int offset = -1;
-    List<ByteBufferFragment> leftPart = Lists.newArrayList();
+    Preconditions.checkArgument(!list.isEmpty());
+    ByteFragmentAtOffset first = list.get(0);
+    if (list.size() == 1) {
+      declarationConsumer.declaration(first);
+      return;
+    }
 
-    for (ByteFragmentAtOffset edge : list) {
-      ByteBufferFragment sequence = edge.getFragment();
-      // If the new sequence is separate from already collected parts,
-      // merge them and feed to consumer.
-      if (!leftPart.isEmpty()) {
-        ByteBufferFragment lastPart = Iterables.getLast(leftPart);
-        // The order of symbols: previousInOld, lastInOld, currentInNew, nextInNew.
-        byte previousInOld = lastPart.length() == 1 ? 0 : lastPart.byteAt(lastPart.length() - 2);
-        byte lastInOld = lastPart.byteAt(lastPart.length() - 1);
-        byte currentInNew = sequence.byteAt(0);
-        byte nextInNew = sequence.length() == 1 ? 0 : sequence.byteAt(1);
-
-        // <symbol> | \n<non-space>
-        if (separatorPredicate.test(lastInOld, currentInNew, nextInNew)) {
-          // Add separator to the end of the accumulated sequence
-          leftPart.add(sequence.subFragment(0, 1));
-          ByteFragmentAtOffset byteFragmentAtOffset =
-              new ByteFragmentAtOffset(edge.getOffset(), ByteBufferFragment.merge(leftPart));
-          declarationConsumer.declaration(byteFragmentAtOffset);
-          leftPart.clear();
-          // Cutting out the separator in the beginning
-          if (sequence.length() > 1) {
-            leftPart.add(sequence.subFragment(1, sequence.length()));
-            offset = edge.getOffset();
-          }
-          continue;
-        }
-
-        // <symbol>\n | <non-space>
-        if (separatorPredicate.test(previousInOld, lastInOld, currentInNew)) {
-          ByteFragmentAtOffset byteFragmentAtOffset =
-              new ByteFragmentAtOffset(edge.getOffset(), ByteBufferFragment.merge(leftPart));
-          declarationConsumer.declaration(byteFragmentAtOffset);
-          leftPart.clear();
-        }
+    // 1. We merge all the passed fragments into one fragment.
+    // 2. We check 6 bytes at the connection of two fragments, 3 bytes in each part:
+    // separator can consist of 4 bytes (<escape>/r/n<indent>),
+    // so in case only a part of the separator is in one of the fragments,
+    // we get 3 bytes in one part and one byte in the other.
+    // 3. We record the ranges of at most 6 bytes at the connections of the fragments into
+    // interestingRanges.
+    // 4. Later we will check only interestingRanges for separators, and create corresponding
+    // fragments; the underlying common ByteBuffer will be reused, so we are not performing
+    // extensive copying.
+    int firstOffset = first.getOffset();
+    List<ByteBufferFragment> fragments = new ArrayList<>();
+    List<Range<Integer>> interestingRanges = Lists.newArrayList();
+    int fragmentShift = 0;
+    for (ByteFragmentAtOffset byteFragmentAtOffset : list) {
+      ByteBufferFragment fragment = byteFragmentAtOffset.getFragment();
+      fragments.add(fragment);
+      if (fragmentShift > 0) {
+        // We are only looking for the separators between fragments.
+        int start = Math.max(0, fragmentShift - 3);
+        int end = fragmentShift + Math.min(4, fragment.length());
+        // Assert that the ranges are not intersecting, otherwise the code that iterates ranges
+        // will work incorrectly.
+        Preconditions.checkState(
+            interestingRanges.isEmpty()
+                || Iterables.getLast(interestingRanges).upperEndpoint() < start);
+        interestingRanges.add(Range.openClosed(start, end));
       }
+      fragmentShift += fragment.length();
+    }
 
-      leftPart.add(sequence);
-      if (offset == -1) {
-        offset = edge.getOffset();
+    ByteBufferFragment merged = ByteBufferFragment.merge(fragments);
+
+    int previousEnd = 0;
+    for (Range<Integer> range : interestingRanges) {
+      int idx =
+          separatorFinder.findNextSeparator(merged, range.lowerEndpoint(), range.upperEndpoint());
+      if (idx >= 0) {
+        // There should always be a previous fragment, as we are checking non-intersecting ranges,
+        // starting from the connection point between first and second fragments.
+        Preconditions.checkState(idx > previousEnd);
+        declarationConsumer.declaration(
+            new ByteFragmentAtOffset(firstOffset, merged.subFragment(previousEnd, idx + 1)));
+        previousEnd = idx + 1;
       }
     }
-    if (!leftPart.isEmpty()) {
-      Preconditions.checkState(offset >= 0);
-      ByteFragmentAtOffset byteFragmentAtOffset =
-          new ByteFragmentAtOffset(offset, ByteBufferFragment.merge(leftPart));
-      declarationConsumer.declaration(byteFragmentAtOffset);
-    }
+    declarationConsumer.declaration(
+        new ByteFragmentAtOffset(firstOffset, merged.subFragment(previousEnd, merged.length())));
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/SeparatorPredicate.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/IncorrectSeparatorException.java
similarity index 62%
rename from src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/SeparatorPredicate.java
rename to src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/IncorrectSeparatorException.java
index fbb2d3a..4d0c6c1 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/SeparatorPredicate.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/IncorrectSeparatorException.java
@@ -15,15 +15,9 @@
 
 package com.google.devtools.build.lib.bazel.rules.ninja.file;
 
-/** Interface for determining where the byte sequence should be split into parts. */
-public interface SeparatorPredicate {
-
-  /**
-   * Returns true if the sequence should be split after <code>current</code> byte.
-   *
-   * @param previous previous byte (before current)
-   * @param current current byte
-   * @param next next byte (after current)
-   */
-  boolean test(byte previous, byte current, byte next);
+/** Thrown by {@link BufferSplitter} when incorrect file separators are used ('\r'). */
+public class IncorrectSeparatorException extends GenericParsingException {
+  public IncorrectSeparatorException(String message) {
+    super(message);
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/NinjaSeparatorFinder.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/NinjaSeparatorFinder.java
new file mode 100644
index 0000000..eb6121a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/NinjaSeparatorFinder.java
@@ -0,0 +1,78 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package com.google.devtools.build.lib.bazel.rules.ninja.file;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Implementation of {@link SeparatorFinder} for Ninja files.
+ *
+ * <p>The Ninja declaration consists of several text lines; if the line is a part of the previous
+ * declaration, it starts with some amount of spaces or tabs. If the line is the beginning of the
+ * new declaration, it starts with non-space symbol. Dollar symbol '$' escapes the newline, i.e.
+ * "$\nsomething" does not contain a separator.
+ *
+ * <p>We support '\r\n' separators in Ninja files and throw {@link IncorrectSeparatorException} in
+ * case an incorrect separator '\r' is used.
+ */
+public class NinjaSeparatorFinder implements SeparatorFinder {
+  public static final NinjaSeparatorFinder INSTANCE = new NinjaSeparatorFinder();
+
+  private static final byte DOLLAR_BYTE = '$';
+  private static final byte LINEFEED_BYTE = '\r';
+  private static final byte NEWLINE_BYTE = '\n';
+  private static final byte SPACE_BYTE = ' ';
+  private static final byte TAB_BYTE = '\t';
+
+  private NinjaSeparatorFinder() {}
+
+  @Override
+  public int findNextSeparator(ByteBufferFragment fragment, int startingFrom, int untilExcluded)
+      throws IncorrectSeparatorException {
+    Preconditions.checkState(startingFrom < fragment.length());
+    Preconditions.checkState(untilExcluded < 0 || untilExcluded <= fragment.length());
+
+    boolean escaped = DOLLAR_BYTE == fragment.byteAt(startingFrom);
+    int endExcl = untilExcluded > 0 ? untilExcluded : fragment.length();
+    for (int i = startingFrom + 1; i < endExcl - 1; i++) {
+      byte current = fragment.byteAt(i);
+      byte next = fragment.byteAt(i + 1);
+      byte afterNextOrSpace = i < (endExcl - 2) ? fragment.byteAt(i + 2) : SPACE_BYTE;
+      if (LINEFEED_BYTE == current && NEWLINE_BYTE != next) {
+        throw new IncorrectSeparatorException(
+            "Wrong newline separators: \\r should be followed by \\n.");
+      }
+      if (!escaped
+          && SPACE_BYTE != afterNextOrSpace
+          && TAB_BYTE != afterNextOrSpace
+          && LINEFEED_BYTE == current) {
+        // To do not introduce the length of the separator, let us point to the last symbol of it.
+        return i + 1;
+      }
+      if (!escaped && SPACE_BYTE != next && TAB_BYTE != next && NEWLINE_BYTE == current) {
+        return i;
+      }
+      if (escaped && LINEFEED_BYTE == current) {
+        // Jump over the whole escaped linefeed + newline.
+        ++i;
+        escaped = false;
+      } else {
+        escaped = DOLLAR_BYTE == current;
+      }
+    }
+    return -1;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/NinjaSeparatorPredicate.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/NinjaSeparatorPredicate.java
deleted file mode 100644
index 5884ac9..0000000
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/NinjaSeparatorPredicate.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2019 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package com.google.devtools.build.lib.bazel.rules.ninja.file;
-
-/**
- * Implementation of {@link SeparatorPredicate} for Ninja files.
- *
- * <p>The Ninja declaration consists of several text lines; if the line is a part of the previous
- * declaration, it starts with some amount of spaces or tabs. If the line is the beginning of the
- * new declaration, it starts with non-space symbol. Dollar symbol '$' escapes the newline, i.e.
- * "$\nsomething" does not contain a separator.
- */
-public class NinjaSeparatorPredicate implements SeparatorPredicate {
-  public static final NinjaSeparatorPredicate INSTANCE = new NinjaSeparatorPredicate();
-
-  private static final byte DOLLAR_BYTE = '$';
-  private static final byte NEWLINE_BYTE = '\n';
-  private static final byte SPACE_BYTE = ' ';
-  private static final byte TAB_BYTE = '\t';
-
-  private NinjaSeparatorPredicate() {}
-
-  @Override
-  public boolean test(byte previous, byte current, byte next) {
-    return NEWLINE_BYTE == current
-        && DOLLAR_BYTE != previous
-        && SPACE_BYTE != next
-        && TAB_BYTE != next;
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/ParallelFileProcessing.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/ParallelFileProcessing.java
index ce3ab26..51ebafe 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/ParallelFileProcessing.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/ParallelFileProcessing.java
@@ -25,21 +25,21 @@
 
 /**
  * Parallel file processing implementation. See comment to {@link #processFile(ReadableByteChannel,
- * BlockParameters, Supplier, ListeningExecutorService, SeparatorPredicate)}.
+ * BlockParameters, Supplier, ListeningExecutorService, SeparatorFinder)}.
  */
 public class ParallelFileProcessing {
   private final ReadableByteChannel channel;
   private final BlockParameters parameters;
   private final Supplier<DeclarationConsumer> tokenConsumerFactory;
   private final ListeningExecutorService executorService;
-  private final SeparatorPredicate predicate;
+  private final SeparatorFinder predicate;
 
   private ParallelFileProcessing(
       ReadableByteChannel channel,
       BlockParameters parameters,
       Supplier<DeclarationConsumer> tokenConsumerFactory,
       ListeningExecutorService executorService,
-      SeparatorPredicate predicate) {
+      SeparatorFinder predicate) {
     this.channel = channel;
     this.parameters = parameters;
     this.tokenConsumerFactory = tokenConsumerFactory;
@@ -90,7 +90,7 @@
       BlockParameters parameters,
       Supplier<DeclarationConsumer> tokenConsumerFactory,
       ListeningExecutorService executorService,
-      SeparatorPredicate predicate)
+      SeparatorFinder predicate)
       throws GenericParsingException, IOException, InterruptedException {
     new ParallelFileProcessing(
             channel, parameters, tokenConsumerFactory, executorService, predicate)
@@ -141,19 +141,25 @@
     int blockSize = parameters.getTokenizeBlockSize();
     while (from < bb.limit()) {
       int to = Math.min(bb.limit(), from + blockSize);
+      if (bb.limit() - to < BlockParameters.MIN_TOKENIZE_BLOCK_SIZE) {
+        // Do not create the last block too small, rather join it with the previous block.
+        to = bb.limit();
+      }
       DeclarationConsumer consumer = tokenConsumerFactory.get();
       ByteBufferFragment fragment = new ByteBufferFragment(bb, from, to);
       BufferSplitter tokenizer = new BufferSplitter(fragment, consumer, predicate, offset);
       future.add(executorService.submit(tokenizer));
-      from += blockSize;
+      from = to;
     }
   }
 
   /** Sizes of blocks for reading from file and parsing for {@link ParallelFileProcessing}. */
   public static class BlockParameters {
+
     private static final int READ_BLOCK_SIZE = 25 * 1024 * 1024;
     private static final int MIN_READ_BLOCK_SIZE = 10 * 1024 * 1024;
     private static final int TOKENIZE_BLOCK_SIZE = 1024 * 1024;
+    private static final int MIN_TOKENIZE_BLOCK_SIZE = 100;
 
     /** Size of the reading buffer. */
     private int readBlockSize;
@@ -173,7 +179,8 @@
     public BlockParameters(long fileSize) {
       readBlockSize = (int) Math.min(READ_BLOCK_SIZE, fileSize);
       minReadBlockSize = Math.min(MIN_READ_BLOCK_SIZE, (int) Math.ceil((double) fileSize / 2));
-      tokenizeBlockSize = Math.min(TOKENIZE_BLOCK_SIZE, minReadBlockSize / 4);
+      tokenizeBlockSize =
+          Math.max(MIN_TOKENIZE_BLOCK_SIZE, Math.min(TOKENIZE_BLOCK_SIZE, minReadBlockSize / 4));
     }
 
     public int getReadBlockSize() {
@@ -188,7 +195,8 @@
       if (readBlockSize > 0) {
         this.readBlockSize = readBlockSize;
         minReadBlockSize = Math.min(minReadBlockSize, (int) Math.ceil((double) readBlockSize / 2));
-        tokenizeBlockSize = Math.min(tokenizeBlockSize, minReadBlockSize / 4);
+        tokenizeBlockSize =
+            Math.max(MIN_TOKENIZE_BLOCK_SIZE, Math.min(tokenizeBlockSize, minReadBlockSize / 4));
       }
       return this;
     }
@@ -197,14 +205,6 @@
       return tokenizeBlockSize;
     }
 
-    /** Sets tokenizeBlockSize, if it is less than readBlockSize. */
-    public BlockParameters setTokenizeBlockSize(int tokenizeBlockSize) {
-      if (tokenizeBlockSize > 0 && tokenizeBlockSize <= readBlockSize) {
-        this.tokenizeBlockSize = tokenizeBlockSize;
-      }
-      return this;
-    }
-
     public int getMinReadBlockSize() {
       return minReadBlockSize;
     }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/SeparatorFinder.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/SeparatorFinder.java
new file mode 100644
index 0000000..c879646
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/file/SeparatorFinder.java
@@ -0,0 +1,32 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package com.google.devtools.build.lib.bazel.rules.ninja.file;
+
+/** Interface for determining where the byte sequence should be split into parts. */
+public interface SeparatorFinder {
+
+  /**
+   * Returns the index of the end of the next separator (separator can be of two symbols, \r\n), or
+   * -1 if the fragment does not contain any separators.
+   *
+   * @param fragment fragment to search in
+   * @param startingFrom index to start search from
+   * @param untilExcluded index to stop search before (excluded from search).
+   * @throws IncorrectSeparatorException if the incorrect separator value (\r) is used
+   */
+  int findNextSeparator(ByteBufferFragment fragment, int startingFrom, int untilExcluded)
+      throws IncorrectSeparatorException;
+}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/BufferSplitterTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/BufferSplitterTest.java
index 5a21ea3..fbe00f8 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/BufferSplitterTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/BufferSplitterTest.java
@@ -24,7 +24,7 @@
 import com.google.devtools.build.lib.bazel.rules.ninja.file.ByteBufferFragment;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.ByteFragmentAtOffset;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.DeclarationConsumer;
-import com.google.devtools.build.lib.bazel.rules.ninja.file.NinjaSeparatorPredicate;
+import com.google.devtools.build.lib.bazel.rules.ninja.file.NinjaSeparatorFinder;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
@@ -51,7 +51,7 @@
     byte[] chars = String.join("\n", list).getBytes(StandardCharsets.ISO_8859_1);
     ByteBufferFragment fragment = new ByteBufferFragment(ByteBuffer.wrap(chars), 0, chars.length);
     BufferSplitter tokenizer =
-        new BufferSplitter(fragment, consumer, NinjaSeparatorPredicate.INSTANCE, offsetValue);
+        new BufferSplitter(fragment, consumer, NinjaSeparatorFinder.INSTANCE, offsetValue);
     List<ByteFragmentAtOffset> edges = tokenizer.call();
     assertThat(result).containsExactly("two\n");
     assertThat(
@@ -74,7 +74,7 @@
 
     ByteBufferFragment fragment = new ByteBufferFragment(ByteBuffer.wrap(chars), 0, chars.length);
     BufferSplitter tokenizer =
-        new BufferSplitter(fragment, consumer, NinjaSeparatorPredicate.INSTANCE, 0);
+        new BufferSplitter(fragment, consumer, NinjaSeparatorFinder.INSTANCE, 0);
     List<ByteFragmentAtOffset> edges = tokenizer.call();
     assertThat(result).containsExactly("two\n\ttwo-detail\n");
     assertThat(
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/DeclarationAssemblerTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/DeclarationAssemblerTest.java
index 5192ea8..ff52c74 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/DeclarationAssemblerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/DeclarationAssemblerTest.java
@@ -22,7 +22,7 @@
 import com.google.devtools.build.lib.bazel.rules.ninja.file.ByteFragmentAtOffset;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.DeclarationAssembler;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
-import com.google.devtools.build.lib.bazel.rules.ninja.file.NinjaSeparatorPredicate;
+import com.google.devtools.build.lib.bazel.rules.ninja.file.NinjaSeparatorFinder;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -65,7 +65,7 @@
               list.add(byteFragmentAtOffset.getFragment().toString());
               assertThat(byteFragmentAtOffset.getOffset()).isAnyOf(0, chars1.length);
             },
-            NinjaSeparatorPredicate.INSTANCE);
+            NinjaSeparatorFinder.INSTANCE);
 
     assembler.wrapUp(
         Lists.newArrayList(
@@ -87,7 +87,7 @@
               list.add(byteFragmentAtOffset.getFragment().toString());
               assertThat(byteFragmentAtOffset.getOffset()).isEqualTo(0);
             },
-            NinjaSeparatorPredicate.INSTANCE);
+            NinjaSeparatorFinder.INSTANCE);
 
     final byte[] chars = s.getBytes(StandardCharsets.ISO_8859_1);
     assembler.wrapUp(
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaSeparatorFinderTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaSeparatorFinderTest.java
new file mode 100644
index 0000000..9e9f722
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaSeparatorFinderTest.java
@@ -0,0 +1,77 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package com.google.devtools.build.lib.bazel.rules.ninja;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+
+import com.google.devtools.build.lib.bazel.rules.ninja.file.ByteBufferFragment;
+import com.google.devtools.build.lib.bazel.rules.ninja.file.IncorrectSeparatorException;
+import com.google.devtools.build.lib.bazel.rules.ninja.file.NinjaSeparatorFinder;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link NinjaSeparatorFinder}. */
+@RunWith(JUnit4.class)
+public class NinjaSeparatorFinderTest {
+  @Test
+  public void testIsSeparator() throws IncorrectSeparatorException {
+    doTestIsSeparator(" \na", 1);
+    doTestIsSeparator("b\na", 1);
+    doTestIsSeparator(" \na", 1);
+    doTestIsSeparator("b\n$", 1);
+    doTestIsSeparator(" \n\n", 1);
+    doTestIsSeparator("a\n\n", 1);
+    // We are pointing to the last symbol of separator.
+    doTestIsSeparator("a\r\n\n", 2);
+    doTestIsSeparator(" \r\n\n", 2);
+    doTestIsSeparator("a\r\na", 2);
+    doTestIsSeparator("\r\na", 1);
+
+    doTestIsSeparator(" \n ", -1);
+    doTestIsSeparator(" \r\n ", -1);
+    doTestIsSeparator("$\n ", -1);
+    doTestIsSeparator("$\r\n ", -1);
+    doTestIsSeparator("$\r\n\n ", -1);
+    doTestIsSeparator("$\r\n\r\n ", -1);
+    doTestIsSeparator("$\n\n", -1);
+    doTestIsSeparator("$\na", -1);
+    doTestIsSeparator("$\r\na", -1);
+    doTestIsSeparator("a\n ", -1);
+    doTestIsSeparator("a\n\t", -1);
+    // Not enough information.
+    doTestIsSeparator("\r\n", -1);
+    doTestIsSeparator("\n", -1);
+    // Test for incorrect separators.
+    byte[] bytes = "a\rb".getBytes(StandardCharsets.ISO_8859_1);
+    ByteBuffer buffer = ByteBuffer.wrap(bytes);
+    ByteBufferFragment fragment = new ByteBufferFragment(buffer, 0, buffer.limit());
+    assertThrows(
+        IncorrectSeparatorException.class,
+        () -> NinjaSeparatorFinder.INSTANCE.findNextSeparator(fragment, 0, -1));
+  }
+
+  private static void doTestIsSeparator(String s, int expected) throws IncorrectSeparatorException {
+    byte[] bytes = s.getBytes(StandardCharsets.ISO_8859_1);
+    ByteBuffer buffer = ByteBuffer.wrap(bytes);
+    ByteBufferFragment fragment = new ByteBufferFragment(buffer, 0, buffer.limit());
+    int result = NinjaSeparatorFinder.INSTANCE.findNextSeparator(fragment, 0, -1);
+    assertThat(result).isEqualTo(expected);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaSeparatorPredicateTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaSeparatorPredicateTest.java
deleted file mode 100644
index d9fa2b3..0000000
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaSeparatorPredicateTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2019 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package com.google.devtools.build.lib.bazel.rules.ninja;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.devtools.build.lib.bazel.rules.ninja.file.NinjaSeparatorPredicate;
-import java.nio.charset.StandardCharsets;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link NinjaSeparatorPredicate}. */
-@RunWith(JUnit4.class)
-public class NinjaSeparatorPredicateTest {
-  @Test
-  public void testIsSeparator() {
-    doTestIsSeparator(" \n ", false);
-    doTestIsSeparator(" \na", true);
-    doTestIsSeparator("$\n ", false);
-    doTestIsSeparator("$\n\n", false);
-    doTestIsSeparator("$\na", false);
-    doTestIsSeparator("b\na", true);
-    doTestIsSeparator(" \na", true);
-    doTestIsSeparator("b\n$", true);
-    doTestIsSeparator(" \n\n", true);
-    doTestIsSeparator("a\n\n", true);
-    doTestIsSeparator("a\n ", false);
-    doTestIsSeparator("a\n\t", false);
-  }
-
-  private static void doTestIsSeparator(String s, Boolean expected) {
-    assertThat(s).hasLength(3);
-    byte[] bytes = s.getBytes(StandardCharsets.ISO_8859_1);
-    boolean result = NinjaSeparatorPredicate.INSTANCE.test(bytes[0], bytes[1], bytes[2]);
-    assertThat(result).isEqualTo(expected);
-  }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/ParallelFileProcessingTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/ParallelFileProcessingTest.java
index f016edb..cfe5a49 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/ParallelFileProcessingTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/ParallelFileProcessingTest.java
@@ -27,7 +27,7 @@
 import com.google.devtools.build.lib.bazel.rules.ninja.file.ByteBufferFragment;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.DeclarationConsumer;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
-import com.google.devtools.build.lib.bazel.rules.ninja.file.NinjaSeparatorPredicate;
+import com.google.devtools.build.lib.bazel.rules.ninja.file.NinjaSeparatorFinder;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.ParallelFileProcessing;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.ParallelFileProcessing.BlockParameters;
 import com.google.devtools.build.lib.concurrent.ExecutorUtil;
@@ -144,7 +144,7 @@
           parameters != null ? parameters : new BlockParameters(file.length()),
           factory,
           service,
-          NinjaSeparatorPredicate.INSTANCE);
+          NinjaSeparatorFinder.INSTANCE);
     } finally {
       ExecutorUtil.interruptibleShutdown(service);
     }