/*
 * Copyright 2013, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jf.dexlib2.util;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import org.jf.util.ExceptionWithContext;
import org.jf.util.Hex;
import org.jf.util.TwoColumnOutput;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

Collects/presents a set of textual annotations, each associated with a range of bytes or a specific point between bytes. Point annotations cannot occur within the middle of a range annotation, only at the endpoints, or some other area with no range annotation. Multiple point annotations can be defined for a given point. They will be printed in insertion order. Only a single range annotation may exist for any given range of bytes. Range annotations may not overlap.
/** * Collects/presents a set of textual annotations, each associated with a range of bytes or a specific point * between bytes. * * Point annotations cannot occur within the middle of a range annotation, only at the endpoints, or some other area * with no range annotation. * * Multiple point annotations can be defined for a given point. They will be printed in insertion order. * * Only a single range annotation may exist for any given range of bytes. Range annotations may not overlap. */
public class AnnotatedBytes {
This defines the bytes ranges and their associated range and point annotations. A range is defined by 2 consecutive keys in the map. The first key is the inclusive start point, the second key is the exclusive end point. The range annotation for a range is associated with the first key for that range. The point annotations for a point are associated with the key at that point.
/** * This defines the bytes ranges and their associated range and point annotations. * * A range is defined by 2 consecutive keys in the map. The first key is the inclusive start point, the second key * is the exclusive end point. The range annotation for a range is associated with the first key for that range. * The point annotations for a point are associated with the key at that point. */
@Nonnull private TreeMap<Integer, AnnotationEndpoint> annotatations = Maps.newTreeMap(); private int cursor; private int indentLevel;
>= 40 (if used); the desired maximum output width
/** &gt;= 40 (if used); the desired maximum output width */
private int outputWidth;
>= 8 (if used); the number of bytes of hex output to use in annotations
/** * &gt;= 8 (if used); the number of bytes of hex output to use * in annotations */
private int hexCols = 8; private int startLimit = -1; private int endLimit = -1; public AnnotatedBytes(int width) { this.outputWidth = width; }
Moves the cursor to a new location
Params:
  • offset – The offset to move to
/** * Moves the cursor to a new location * * @param offset The offset to move to */
public void moveTo(int offset) { cursor = offset; }
Moves the cursor forward or backward by some amount
Params:
  • offset – The amount to move the cursor
/** * Moves the cursor forward or backward by some amount * * @param offset The amount to move the cursor */
public void moveBy(int offset) { cursor += offset; } public void annotateTo(int offset, @Nonnull String msg, Object... formatArgs) { annotate(offset - cursor, msg, formatArgs); }
Add an annotation of the given length at the current location. The location
Params:
  • length – the length of data being annotated
  • msg – the annotation message
  • formatArgs – format arguments to pass to String.format
/** * Add an annotation of the given length at the current location. * * The location * * * @param length the length of data being annotated * @param msg the annotation message * @param formatArgs format arguments to pass to String.format */
public void annotate(int length, @Nonnull String msg, Object... formatArgs) { if (startLimit != -1 && endLimit != -1 && (cursor < startLimit || cursor >= endLimit)) { throw new ExceptionWithContext("Annotating outside the parent bounds"); } String formattedMsg; if (formatArgs != null && formatArgs.length > 0) { formattedMsg = String.format(msg, formatArgs); } else { formattedMsg = msg; } int exclusiveEndOffset = cursor + length; AnnotationEndpoint endPoint = null; // Do we have an endpoint at the beginning of this annotation already? AnnotationEndpoint startPoint = annotatations.get(cursor); if (startPoint == null) { // Nope. We need to check that we're not in the middle of an existing range annotation. Map.Entry<Integer, AnnotationEndpoint> previousEntry = annotatations.lowerEntry(cursor); if (previousEntry != null) { AnnotationEndpoint previousAnnotations = previousEntry.getValue(); AnnotationItem previousRangeAnnotation = previousAnnotations.rangeAnnotation; if (previousRangeAnnotation != null) { throw new ExceptionWithContext( "Cannot add annotation %s, due to existing annotation %s", formatAnnotation(cursor, cursor + length, formattedMsg), formatAnnotation(previousEntry.getKey(), previousRangeAnnotation.annotation)); } } } else if (length > 0) { AnnotationItem existingRangeAnnotation = startPoint.rangeAnnotation; if (existingRangeAnnotation != null) { throw new ExceptionWithContext( "Cannot add annotation %s, due to existing annotation %s", formatAnnotation(cursor, cursor + length, formattedMsg), formatAnnotation(cursor, existingRangeAnnotation.annotation)); } } if (length > 0) { // Ensure that there is no later annotation that would intersect with this one Map.Entry<Integer, AnnotationEndpoint> nextEntry = annotatations.higherEntry(cursor); if (nextEntry != null) { int nextKey = nextEntry.getKey(); if (nextKey < exclusiveEndOffset) { // there is an endpoint that would intersect with this annotation. Find one of the annotations // associated with the endpoint, to print in the error message AnnotationEndpoint nextEndpoint = nextEntry.getValue(); AnnotationItem nextRangeAnnotation = nextEndpoint.rangeAnnotation; if (nextRangeAnnotation != null) { throw new ExceptionWithContext( "Cannot add annotation %s, due to existing annotation %s", formatAnnotation(cursor, cursor + length, formattedMsg), formatAnnotation(nextKey, nextRangeAnnotation.annotation)); } if (nextEndpoint.pointAnnotations.size() > 0) { throw new ExceptionWithContext( "Cannot add annotation %s, due to existing annotation %s", formatAnnotation(cursor, cursor + length, formattedMsg), formatAnnotation(nextKey, nextKey, nextEndpoint.pointAnnotations.get(0).annotation)); } // There are no annotations on this endpoint. This "shouldn't" happen. We can still throw an exception. throw new ExceptionWithContext( "Cannot add annotation %s, due to existing annotation endpoint at %d", formatAnnotation(cursor, cursor + length, formattedMsg), nextKey); } if (nextKey == exclusiveEndOffset) { // the next endpoint matches the end of the annotation we are adding endPoint = nextEntry.getValue(); } } } // Now, actually add the annotation // If startPoint is null, we need to create a new one and add it to annotations. Otherwise, we just need to add // the annotation to the existing AnnotationEndpoint // the range annotation if (startPoint == null) { startPoint = new AnnotationEndpoint(); annotatations.put(cursor, startPoint); } if (length == 0) { startPoint.pointAnnotations.add(new AnnotationItem(indentLevel, formattedMsg)); } else { startPoint.rangeAnnotation = new AnnotationItem(indentLevel, formattedMsg); // If endPoint is null, we need to create a new, empty one and add it to annotations if (endPoint == null) { endPoint = new AnnotationEndpoint(); annotatations.put(exclusiveEndOffset, endPoint); } } cursor += length; } private String formatAnnotation(int offset, String annotationMsg) { Integer endOffset = annotatations.higherKey(offset); return formatAnnotation(offset, endOffset, annotationMsg); } private String formatAnnotation(int offset, Integer endOffset, String annotationMsg) { if (endOffset != null) { return String.format("[0x%x, 0x%x) \"%s\"", offset, endOffset, annotationMsg); } else { return String.format("[0x%x, ) \"%s\"", offset, annotationMsg); } } public void indent() { indentLevel++; } public void deindent() { indentLevel--; if (indentLevel < 0) { indentLevel = 0; } } public int getCursor() { return cursor; } private static class AnnotationEndpoint {
Annotations that are associated with a specific point between bytes
/** Annotations that are associated with a specific point between bytes */
@Nonnull public final List<AnnotationItem> pointAnnotations = Lists.newArrayList();
Annotations that are associated with a range of bytes
/** Annotations that are associated with a range of bytes */
@Nullable public AnnotationItem rangeAnnotation = null; } private static class AnnotationItem { public final int indentLevel; public final String annotation; public AnnotationItem(int indentLevel, String annotation) { this.indentLevel = indentLevel; this.annotation = annotation; } }
Returns:The width of the right side containing the annotations
/** * @return The width of the right side containing the annotations */
public int getAnnotationWidth() { int leftWidth = 8 + (hexCols * 2) + (hexCols / 2); return outputWidth - leftWidth; }
Writes the annotated content of this instance to the given writer.
Params:
  • out – non-null; where to write to
/** * Writes the annotated content of this instance to the given writer. * * @param out non-null; where to write to */
public void writeAnnotations(Writer out, byte[] data, int offset) throws IOException { int rightWidth = getAnnotationWidth(); int leftWidth = outputWidth - rightWidth - 1; String padding = Strings.repeat(" ", 1000); TwoColumnOutput twoc = new TwoColumnOutput(out, leftWidth, rightWidth, "|"); Integer[] keys = new Integer[annotatations.size()]; keys = annotatations.keySet().toArray(keys); AnnotationEndpoint[] values = new AnnotationEndpoint[annotatations.size()]; values = annotatations.values().toArray(values); for (int i=0; i<keys.length-1; i++) { int rangeStart = keys[i]; int rangeEnd = keys[i+1]; AnnotationEndpoint annotations = values[i]; for (AnnotationItem pointAnnotation: annotations.pointAnnotations) { String paddingSub = padding.substring(0, pointAnnotation.indentLevel*2); twoc.write("", paddingSub + pointAnnotation.annotation); } String right; AnnotationItem rangeAnnotation = annotations.rangeAnnotation; if (rangeAnnotation != null) { right = padding.substring(0, rangeAnnotation.indentLevel*2); right += rangeAnnotation.annotation; } else { right = ""; } String left = Hex.dump(data, rangeStart + offset, rangeEnd - rangeStart, rangeStart + offset, hexCols, 6); twoc.write(left, right); } int lastKey = keys[keys.length-1]; if (lastKey < data.length) { String left = Hex.dump(data, lastKey + offset, (data.length - offset) - lastKey, lastKey + offset, hexCols, 6); twoc.write(left, ""); } } public void setLimit(int start, int end) { this.startLimit = start; this.endLimit = end; } public void clearLimit() { this.startLimit = -1; this.endLimit = -1; } }