blob: 6716f1303bd03c01bfbf83f9b1968ff8fb30ff48 [file] [log] [blame]
// Copyright 2014 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.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();
}
}