diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/util/AnsiStrippingOutputStream.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/lib/util/AnsiStrippingOutputStream.java | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/util/AnsiStrippingOutputStream.java b/src/main/java/com/google/devtools/build/lib/util/AnsiStrippingOutputStream.java new file mode 100644 index 0000000000..6c6b878543 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/util/AnsiStrippingOutputStream.java @@ -0,0 +1,176 @@ +// Copyright 2014 Google Inc. 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.util; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A pass-thru {@link OutputStream} that strips ANSI control codes. + */ +public class AnsiStrippingOutputStream extends OutputStream { + // The idea is straightforward: the regexp for ANSI control codes is + // \x1b\[[;0-9]*[a-zA-Z] . Implementing it as a stream is a little ugly, + // though. + + private enum State { + NORMAL, + AFTER_ESCAPE, + PARAMETER, + } + + private byte[] outputBuffer; + private int outputBufferPos; + + private static final int ESCAPE_BUFFER_LENGTH = 128; + private byte[] escapeCodeBuffer; + private int escapeCodeBufferPos; + private OutputStream output; + private State state; + + public AnsiStrippingOutputStream(OutputStream output) { + this.output = output; + escapeCodeBuffer = new byte[ESCAPE_BUFFER_LENGTH]; + escapeCodeBufferPos = 0; + state = State.NORMAL; + } + + @Override + public synchronized void write(int b) throws IOException { + // As per the contract of OutputStream.write(int) + byte[] array = { (byte) (b & 0xff) }; + write(array, 0, 1); + } + + @Override + public synchronized void write(byte b[], int off, int len) throws IOException { + int i = 0; + if (state == State.NORMAL) { + + // Avoid outputBuffer allocation entirely if that's possible + while ((i < len) && (b[off + i] != 0x1b)) { + i++; + } + if (i == len) { + output.write(b, off, len); + return; + } + } + + // In the worst case, the contents of the escape buffer and the contents + // of the input buffer are both copied to the output, so the length of the + // output buffer should be the sum of the length of both these buffers. + outputBuffer = new byte[len + ESCAPE_BUFFER_LENGTH]; + System.arraycopy(b, off, outputBuffer, 0, i); + outputBufferPos = i; + + for (; i < len; i++) { + processByte(b[off + i]); + } + + try { + output.write(outputBuffer, 0, outputBufferPos); + } finally { + outputBuffer = null; // Make it possible to garbage collect the array + } + } + + private void processByte(byte b) { + switch (state) { + case NORMAL: + if (escapeCodeBufferPos != 0) { + throw new IllegalStateException(); + } + if (b == 0x1b) { + state = State.AFTER_ESCAPE; + addByteToEscapeBuffer(b); + } else { + dumpByte(b); + } + break; + + case AFTER_ESCAPE: + if (b == '[') { + state = State.PARAMETER; + addByteToEscapeBuffer(b); + } else if (b == 0x1b) { + dumpEscapeBuffer(); + state = State.AFTER_ESCAPE; + addByteToEscapeBuffer(b); + } else { + dumpEscapeBuffer(); + dumpByte(b); + state = State.NORMAL; + } + break; + + case PARAMETER: + if ((b >= '0' && b <= '9') || b == ';') { + // Parameter continues + addByteToEscapeBuffer(b); + } else if ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) { + // Found a control sequence, discard it and revert to normal state + discardEscapeBuffer(); + state = State.NORMAL; + } else if (b == 0x1b) { + // Another escape sequence begins immediately after, and this is + // an illegal escape sequence + dumpEscapeBuffer(); + state = State.AFTER_ESCAPE; + addByteToEscapeBuffer(b); + } else { + // Illegal control sequence, output it + dumpEscapeBuffer(); + state = State.NORMAL; + } + break; + } + } + + private void addByteToEscapeBuffer(byte b) { + escapeCodeBuffer[escapeCodeBufferPos++] = b; + if (escapeCodeBufferPos == ESCAPE_BUFFER_LENGTH) { + // Buffer full. Assume that no sane code emits an ANSI control code this + // long and revert to normal state. + dumpEscapeBuffer(); + state = State.NORMAL; + } + } + + private void discardEscapeBuffer() { + escapeCodeBufferPos = 0; + } + + private void dumpByte(byte b) { + outputBuffer[outputBufferPos++] = b; + } + + private void dumpEscapeBuffer() { + System.arraycopy(escapeCodeBuffer, 0, + outputBuffer, outputBufferPos, escapeCodeBufferPos); + outputBufferPos += escapeCodeBufferPos; + escapeCodeBufferPos = 0; + } + + @Override + public void flush() throws IOException { + output.flush(); + } + + @Override + public void close() throws IOException { + output.close(); + } +} |