// 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(); } }