/* * Copyright (C) 2007 The Android Open Source Project * * 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.android.dx.util; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; /** * Implementation of {@link AnnotatedOutput} which stores the written data * into a {@code byte[]}. * *

Note: As per the {@link Output} interface, multi-byte * writes all use little-endian order.

*/ public final class ByteArrayAnnotatedOutput implements AnnotatedOutput, ByteOutput { /** default size for stretchy instances */ private static final int DEFAULT_SIZE = 1000; /** * whether the instance is stretchy, that is, whether its array * may be resized to increase capacity */ private final boolean stretchy; /** {@code non-null;} the data itself */ private byte[] data; /** {@code >= 0;} current output cursor */ private int cursor; /** whether annotations are to be verbose */ private boolean verbose; /** * {@code null-ok;} list of annotations, or {@code null} if this instance * isn't keeping them */ private ArrayList annotations; /** {@code >= 40 (if used);} the desired maximum annotation width */ private int annotationWidth; /** * {@code >= 8 (if used);} the number of bytes of hex output to use * in annotations */ private int hexCols; /** * Constructs an instance with a fixed maximum size. Note that the * given array is the only one that will be used to store data. In * particular, no reallocation will occur in order to expand the * capacity of the resulting instance. Also, the constructed * instance does not keep annotations by default. * * @param data {@code non-null;} data array to use for output */ public ByteArrayAnnotatedOutput(byte[] data) { this(data, false); } /** * Constructs a "stretchy" instance. The underlying array may be * reallocated. The constructed instance does not keep annotations * by default. */ public ByteArrayAnnotatedOutput() { this(DEFAULT_SIZE); } /** * Constructs a "stretchy" instance with initial size {@code size}. The * underlying array may be reallocated. The constructed instance does not * keep annotations by default. */ public ByteArrayAnnotatedOutput(int size) { this(new byte[size], true); } /** * Internal constructor. * * @param data {@code non-null;} data array to use for output * @param stretchy whether the instance is to be stretchy */ private ByteArrayAnnotatedOutput(byte[] data, boolean stretchy) { if (data == null) { throw new NullPointerException("data == null"); } this.stretchy = stretchy; this.data = data; this.cursor = 0; this.verbose = false; this.annotations = null; this.annotationWidth = 0; this.hexCols = 0; } /** * Gets the underlying {@code byte[]} of this instance, which * may be larger than the number of bytes written * * @see #toByteArray * * @return {@code non-null;} the {@code byte[]} */ public byte[] getArray() { return data; } /** * Constructs and returns a new {@code byte[]} that contains * the written contents exactly (that is, with no extra unwritten * bytes at the end). * * @see #getArray * * @return {@code non-null;} an appropriately-constructed array */ public byte[] toByteArray() { byte[] result = new byte[cursor]; System.arraycopy(data, 0, result, 0, cursor); return result; } /** {@inheritDoc} */ public int getCursor() { return cursor; } /** {@inheritDoc} */ public void assertCursor(int expectedCursor) { if (cursor != expectedCursor) { throw new ExceptionWithContext("expected cursor " + expectedCursor + "; actual value: " + cursor); } } /** {@inheritDoc} */ public void writeByte(int value) { int writeAt = cursor; int end = writeAt + 1; if (stretchy) { ensureCapacity(end); } else if (end > data.length) { throwBounds(); return; } data[writeAt] = (byte) value; cursor = end; } /** {@inheritDoc} */ public void writeShort(int value) { int writeAt = cursor; int end = writeAt + 2; if (stretchy) { ensureCapacity(end); } else if (end > data.length) { throwBounds(); return; } data[writeAt] = (byte) value; data[writeAt + 1] = (byte) (value >> 8); cursor = end; } /** {@inheritDoc} */ public void writeInt(int value) { int writeAt = cursor; int end = writeAt + 4; if (stretchy) { ensureCapacity(end); } else if (end > data.length) { throwBounds(); return; } data[writeAt] = (byte) value; data[writeAt + 1] = (byte) (value >> 8); data[writeAt + 2] = (byte) (value >> 16); data[writeAt + 3] = (byte) (value >> 24); cursor = end; } /** {@inheritDoc} */ public void writeLong(long value) { int writeAt = cursor; int end = writeAt + 8; if (stretchy) { ensureCapacity(end); } else if (end > data.length) { throwBounds(); return; } int half = (int) value; data[writeAt] = (byte) half; data[writeAt + 1] = (byte) (half >> 8); data[writeAt + 2] = (byte) (half >> 16); data[writeAt + 3] = (byte) (half >> 24); half = (int) (value >> 32); data[writeAt + 4] = (byte) half; data[writeAt + 5] = (byte) (half >> 8); data[writeAt + 6] = (byte) (half >> 16); data[writeAt + 7] = (byte) (half >> 24); cursor = end; } /** {@inheritDoc} */ public int writeUleb128(int value) { if (stretchy) { ensureCapacity(cursor + 5); // pessimistic } int cursorBefore = cursor; Leb128Utils.writeUnsignedLeb128(this, value); return (cursor - cursorBefore); } /** {@inheritDoc} */ public int writeSleb128(int value) { if (stretchy) { ensureCapacity(cursor + 5); // pessimistic } int cursorBefore = cursor; Leb128Utils.writeSignedLeb128(this, value); return (cursor - cursorBefore); } /** {@inheritDoc} */ public void write(ByteArray bytes) { int blen = bytes.size(); int writeAt = cursor; int end = writeAt + blen; if (stretchy) { ensureCapacity(end); } else if (end > data.length) { throwBounds(); return; } bytes.getBytes(data, writeAt); cursor = end; } /** {@inheritDoc} */ public void write(byte[] bytes, int offset, int length) { int writeAt = cursor; int end = writeAt + length; int bytesEnd = offset + length; // twos-complement math trick: ((x < 0) || (y < 0)) <=> ((x|y) < 0) if (((offset | length | end) < 0) || (bytesEnd > bytes.length)) { throw new IndexOutOfBoundsException("bytes.length " + bytes.length + "; " + offset + "..!" + end); } if (stretchy) { ensureCapacity(end); } else if (end > data.length) { throwBounds(); return; } System.arraycopy(bytes, offset, data, writeAt, length); cursor = end; } /** {@inheritDoc} */ public void write(byte[] bytes) { write(bytes, 0, bytes.length); } /** {@inheritDoc} */ public void writeZeroes(int count) { if (count < 0) { throw new IllegalArgumentException("count < 0"); } int end = cursor + count; if (stretchy) { ensureCapacity(end); } else if (end > data.length) { throwBounds(); return; } /* * There is no need to actually write zeroes, since the array is * already preinitialized with zeroes. */ cursor = end; } /** {@inheritDoc} */ public void alignTo(int alignment) { int mask = alignment - 1; if ((alignment < 0) || ((mask & alignment) != 0)) { throw new IllegalArgumentException("bogus alignment"); } int end = (cursor + mask) & ~mask; if (stretchy) { ensureCapacity(end); } else if (end > data.length) { throwBounds(); return; } /* * There is no need to actually write zeroes, since the array is * already preinitialized with zeroes. */ cursor = end; } /** {@inheritDoc} */ public boolean annotates() { return (annotations != null); } /** {@inheritDoc} */ public boolean isVerbose() { return verbose; } /** {@inheritDoc} */ public void annotate(String msg) { if (annotations == null) { return; } endAnnotation(); annotations.add(new Annotation(cursor, msg)); } /** {@inheritDoc} */ public void annotate(int amt, String msg) { if (annotations == null) { return; } endAnnotation(); int asz = annotations.size(); int lastEnd = (asz == 0) ? 0 : annotations.get(asz - 1).getEnd(); int startAt; if (lastEnd <= cursor) { startAt = cursor; } else { startAt = lastEnd; } annotations.add(new Annotation(startAt, startAt + amt, msg)); } /** {@inheritDoc} */ public void endAnnotation() { if (annotations == null) { return; } int sz = annotations.size(); if (sz != 0) { annotations.get(sz - 1).setEndIfUnset(cursor); } } /** {@inheritDoc} */ public int getAnnotationWidth() { int leftWidth = 8 + (hexCols * 2) + (hexCols / 2); return annotationWidth - leftWidth; } /** * Indicates that this instance should keep annotations. This method may * be called only once per instance, and only before any data has been * written to the it. * * @param annotationWidth {@code >= 40;} the desired maximum annotation width * @param verbose whether or not to indicate verbose annotations */ public void enableAnnotations(int annotationWidth, boolean verbose) { if ((annotations != null) || (cursor != 0)) { throw new RuntimeException("cannot enable annotations"); } if (annotationWidth < 40) { throw new IllegalArgumentException("annotationWidth < 40"); } int hexCols = (((annotationWidth - 7) / 15) + 1) & ~1; if (hexCols < 6) { hexCols = 6; } else if (hexCols > 10) { hexCols = 10; } this.annotations = new ArrayList(1000); this.annotationWidth = annotationWidth; this.hexCols = hexCols; this.verbose = verbose; } /** * Finishes up annotation processing. This closes off any open * annotations and removes annotations that don't refer to written * data. */ public void finishAnnotating() { // Close off the final annotation, if any. endAnnotation(); // Remove annotations that refer to unwritten data. if (annotations != null) { int asz = annotations.size(); while (asz > 0) { Annotation last = annotations.get(asz - 1); if (last.getStart() > cursor) { annotations.remove(asz - 1); asz--; } else if (last.getEnd() > cursor) { last.setEnd(cursor); break; } else { break; } } } } /** * Writes the annotated content of this instance to the given writer. * * @param out {@code non-null;} where to write to */ public void writeAnnotationsTo(Writer out) throws IOException { int width2 = getAnnotationWidth(); int width1 = annotationWidth - width2 - 1; TwoColumnOutput twoc = new TwoColumnOutput(out, width1, width2, "|"); Writer left = twoc.getLeft(); Writer right = twoc.getRight(); int leftAt = 0; // left-hand byte output cursor int rightAt = 0; // right-hand annotation index int rightSz = annotations.size(); while ((leftAt < cursor) && (rightAt < rightSz)) { Annotation a = annotations.get(rightAt); int start = a.getStart(); int end; String text; if (leftAt < start) { // This is an area with no annotation. end = start; start = leftAt; text = ""; } else { // This is an area with an annotation. end = a.getEnd(); text = a.getText(); rightAt++; } left.write(Hex.dump(data, start, end - start, start, hexCols, 6)); right.write(text); twoc.flush(); leftAt = end; } if (leftAt < cursor) { // There is unannotated output at the end. left.write(Hex.dump(data, leftAt, cursor - leftAt, leftAt, hexCols, 6)); } while (rightAt < rightSz) { // There are zero-byte annotations at the end. right.write(annotations.get(rightAt).getText()); rightAt++; } twoc.flush(); } /** * Throws the excpetion for when an attempt is made to write past the * end of the instance. */ private static void throwBounds() { throw new IndexOutOfBoundsException("attempt to write past the end"); } /** * Reallocates the underlying array if necessary. Calls to this method * should be guarded by a test of {@link #stretchy}. * * @param desiredSize {@code >= 0;} the desired minimum total size of the array */ private void ensureCapacity(int desiredSize) { if (data.length < desiredSize) { byte[] newData = new byte[desiredSize * 2 + 1000]; System.arraycopy(data, 0, newData, 0, cursor); data = newData; } } /** * Annotation on output. */ private static class Annotation { /** {@code >= 0;} start of annotated range (inclusive) */ private final int start; /** * {@code >= 0;} end of annotated range (exclusive); * {@code Integer.MAX_VALUE} if unclosed */ private int end; /** {@code non-null;} annotation text */ private final String text; /** * Constructs an instance. * * @param start {@code >= 0;} start of annotated range * @param end {@code >= start;} end of annotated range (exclusive) or * {@code Integer.MAX_VALUE} if unclosed * @param text {@code non-null;} annotation text */ public Annotation(int start, int end, String text) { this.start = start; this.end = end; this.text = text; } /** * Constructs an instance. It is initally unclosed. * * @param start {@code >= 0;} start of annotated range * @param text {@code non-null;} annotation text */ public Annotation(int start, String text) { this(start, Integer.MAX_VALUE, text); } /** * Sets the end as given, but only if the instance is unclosed; * otherwise, do nothing. * * @param end {@code >= start;} the end */ public void setEndIfUnset(int end) { if (this.end == Integer.MAX_VALUE) { this.end = end; } } /** * Sets the end as given. * * @param end {@code >= start;} the end */ public void setEnd(int end) { this.end = end; } /** * Gets the start. * * @return the start */ public int getStart() { return start; } /** * Gets the end. * * @return the end */ public int getEnd() { return end; } /** * Gets the text. * * @return {@code non-null;} the text */ public String getText() { return text; } } }