1 /* 2 * Copyright 2013, Google LLC 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google LLC nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 package com.android.tools.smali.dexlib2.util; 32 33 import com.android.tools.smali.util.ExceptionWithContext; 34 import com.android.tools.smali.util.Hex; 35 import com.android.tools.smali.util.StringUtils; 36 import com.android.tools.smali.util.TwoColumnOutput; 37 38 import javax.annotation.Nonnull; 39 import javax.annotation.Nullable; 40 import java.io.IOException; 41 import java.io.Writer; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.TreeMap; 46 47 /** 48 * Collects/presents a set of textual annotations, each associated with a range of bytes or a specific point 49 * between bytes. 50 * 51 * Point annotations cannot occur within the middle of a range annotation, only at the endpoints, or some other area 52 * with no range annotation. 53 * 54 * Multiple point annotations can be defined for a given point. They will be printed in insertion order. 55 * 56 * Only a single range annotation may exist for any given range of bytes. Range annotations may not overlap. 57 */ 58 public class AnnotatedBytes { 59 /** 60 * This defines the bytes ranges and their associated range and point annotations. 61 * 62 * A range is defined by 2 consecutive keys in the map. The first key is the inclusive start point, the second key 63 * is the exclusive end point. The range annotation for a range is associated with the first key for that range. 64 * The point annotations for a point are associated with the key at that point. 65 */ 66 @Nonnull private TreeMap<Integer, AnnotationEndpoint> annotatations = new TreeMap<>(); 67 68 private int cursor; 69 private int indentLevel; 70 71 /** >= 40 (if used); the desired maximum output width */ 72 private int outputWidth; 73 74 /** 75 * >= 8 (if used); the number of bytes of hex output to use 76 * in annotations 77 */ 78 private int hexCols = 8; 79 80 private int startLimit = -1; 81 private int endLimit = -1; 82 AnnotatedBytes(int width)83 public AnnotatedBytes(int width) { 84 this.outputWidth = width; 85 } 86 87 /** 88 * Moves the cursor to a new location 89 * 90 * @param offset The offset to move to 91 */ moveTo(int offset)92 public void moveTo(int offset) { 93 cursor = offset; 94 } 95 96 /** 97 * Moves the cursor forward or backward by some amount 98 * 99 * @param offset The amount to move the cursor 100 */ moveBy(int offset)101 public void moveBy(int offset) { 102 cursor += offset; 103 } 104 annotateTo(int offset, @Nonnull String msg, Object... formatArgs)105 public void annotateTo(int offset, @Nonnull String msg, Object... formatArgs) { 106 annotate(offset - cursor, msg, formatArgs); 107 } 108 109 /** 110 * Add an annotation of the given length at the current location. 111 * 112 * The location 113 * 114 * 115 * @param length the length of data being annotated 116 * @param msg the annotation message 117 * @param formatArgs format arguments to pass to String.format 118 */ annotate(int length, @Nonnull String msg, Object... formatArgs)119 public void annotate(int length, @Nonnull String msg, Object... formatArgs) { 120 if (startLimit != -1 && endLimit != -1 && (cursor < startLimit || cursor >= endLimit)) { 121 throw new ExceptionWithContext("Annotating outside the parent bounds"); 122 } 123 124 String formattedMsg; 125 if (formatArgs != null && formatArgs.length > 0) { 126 formattedMsg = String.format(msg, formatArgs); 127 } else { 128 formattedMsg = msg; 129 } 130 int exclusiveEndOffset = cursor + length; 131 132 AnnotationEndpoint endPoint = null; 133 134 // Do we have an endpoint at the beginning of this annotation already? 135 AnnotationEndpoint startPoint = annotatations.get(cursor); 136 if (startPoint == null) { 137 // Nope. We need to check that we're not in the middle of an existing range annotation. 138 Map.Entry<Integer, AnnotationEndpoint> previousEntry = annotatations.lowerEntry(cursor); 139 if (previousEntry != null) { 140 AnnotationEndpoint previousAnnotations = previousEntry.getValue(); 141 AnnotationItem previousRangeAnnotation = previousAnnotations.rangeAnnotation; 142 if (previousRangeAnnotation != null) { 143 throw new ExceptionWithContext( 144 "Cannot add annotation %s, due to existing annotation %s", 145 formatAnnotation(cursor, cursor + length, formattedMsg), 146 formatAnnotation(previousEntry.getKey(), 147 previousRangeAnnotation.annotation)); 148 } 149 } 150 } else if (length > 0) { 151 AnnotationItem existingRangeAnnotation = startPoint.rangeAnnotation; 152 if (existingRangeAnnotation != null) { 153 throw new ExceptionWithContext( 154 "Cannot add annotation %s, due to existing annotation %s", 155 formatAnnotation(cursor, cursor + length, formattedMsg), 156 formatAnnotation(cursor, existingRangeAnnotation.annotation)); 157 } 158 } 159 160 if (length > 0) { 161 // Ensure that there is no later annotation that would intersect with this one 162 Map.Entry<Integer, AnnotationEndpoint> nextEntry = annotatations.higherEntry(cursor); 163 if (nextEntry != null) { 164 int nextKey = nextEntry.getKey(); 165 if (nextKey < exclusiveEndOffset) { 166 // there is an endpoint that would intersect with this annotation. Find one of the annotations 167 // associated with the endpoint, to print in the error message 168 AnnotationEndpoint nextEndpoint = nextEntry.getValue(); 169 AnnotationItem nextRangeAnnotation = nextEndpoint.rangeAnnotation; 170 if (nextRangeAnnotation != null) { 171 throw new ExceptionWithContext( 172 "Cannot add annotation %s, due to existing annotation %s", 173 formatAnnotation(cursor, cursor + length, formattedMsg), 174 formatAnnotation(nextKey, nextRangeAnnotation.annotation)); 175 } 176 if (nextEndpoint.pointAnnotations.size() > 0) { 177 throw new ExceptionWithContext( 178 "Cannot add annotation %s, due to existing annotation %s", 179 formatAnnotation(cursor, cursor + length, formattedMsg), 180 formatAnnotation(nextKey, nextKey, 181 nextEndpoint.pointAnnotations.get(0).annotation)); 182 } 183 // There are no annotations on this endpoint. This "shouldn't" happen. We can still throw an exception. 184 throw new ExceptionWithContext( 185 "Cannot add annotation %s, due to existing annotation endpoint at %d", 186 formatAnnotation(cursor, cursor + length, formattedMsg), 187 nextKey); 188 } 189 190 if (nextKey == exclusiveEndOffset) { 191 // the next endpoint matches the end of the annotation we are adding 192 endPoint = nextEntry.getValue(); 193 } 194 } 195 } 196 197 // Now, actually add the annotation 198 // If startPoint is null, we need to create a new one and add it to annotations. Otherwise, we just need to add 199 // the annotation to the existing AnnotationEndpoint 200 // the range annotation 201 if (startPoint == null) { 202 startPoint = new AnnotationEndpoint(); 203 annotatations.put(cursor, startPoint); 204 } 205 if (length == 0) { 206 startPoint.pointAnnotations.add(new AnnotationItem(indentLevel, formattedMsg)); 207 } else { 208 startPoint.rangeAnnotation = new AnnotationItem(indentLevel, formattedMsg); 209 210 // If endPoint is null, we need to create a new, empty one and add it to annotations 211 if (endPoint == null) { 212 endPoint = new AnnotationEndpoint(); 213 annotatations.put(exclusiveEndOffset, endPoint); 214 } 215 } 216 217 cursor += length; 218 } 219 formatAnnotation(int offset, String annotationMsg)220 private String formatAnnotation(int offset, String annotationMsg) { 221 Integer endOffset = annotatations.higherKey(offset); 222 return formatAnnotation(offset, endOffset, annotationMsg); 223 } 224 formatAnnotation(int offset, Integer endOffset, String annotationMsg)225 private String formatAnnotation(int offset, Integer endOffset, String annotationMsg) { 226 if (endOffset != null) { 227 return String.format("[0x%x, 0x%x) \"%s\"", offset, endOffset, annotationMsg); 228 } else { 229 return String.format("[0x%x, ) \"%s\"", offset, annotationMsg); 230 } 231 } 232 indent()233 public void indent() { 234 indentLevel++; 235 } 236 deindent()237 public void deindent() { 238 indentLevel--; 239 if (indentLevel < 0) { 240 indentLevel = 0; 241 } 242 } 243 getCursor()244 public int getCursor() { 245 return cursor; 246 } 247 248 private static class AnnotationEndpoint { 249 /** Annotations that are associated with a specific point between bytes */ 250 @Nonnull 251 public final List<AnnotationItem> pointAnnotations = new ArrayList<>(); 252 /** Annotations that are associated with a range of bytes */ 253 @Nullable 254 public AnnotationItem rangeAnnotation = null; 255 } 256 257 private static class AnnotationItem { 258 public final int indentLevel; 259 public final String annotation; 260 AnnotationItem(int indentLevel, String annotation)261 public AnnotationItem(int indentLevel, String annotation) { 262 this.indentLevel = indentLevel; 263 this.annotation = annotation; 264 } 265 } 266 267 /** 268 * @return The width of the right side containing the annotations 269 */ getAnnotationWidth()270 public int getAnnotationWidth() { 271 int leftWidth = 8 + (hexCols * 2) + (hexCols / 2); 272 273 return outputWidth - leftWidth; 274 } 275 276 /** 277 * Writes the annotated content of this instance to the given writer. 278 * 279 * @param out non-null; where to write to 280 */ writeAnnotations(Writer out, byte[] data, int offset)281 public void writeAnnotations(Writer out, byte[] data, int offset) throws IOException { 282 int rightWidth = getAnnotationWidth(); 283 int leftWidth = outputWidth - rightWidth - 1; 284 285 String padding = StringUtils.repeat(" ", 1000); 286 287 TwoColumnOutput twoc = new TwoColumnOutput(out, leftWidth, rightWidth, "|"); 288 289 Integer[] keys = new Integer[annotatations.size()]; 290 keys = annotatations.keySet().toArray(keys); 291 292 AnnotationEndpoint[] values = new AnnotationEndpoint[annotatations.size()]; 293 values = annotatations.values().toArray(values); 294 295 for (int i=0; i<keys.length-1; i++) { 296 int rangeStart = keys[i]; 297 int rangeEnd = keys[i+1]; 298 299 AnnotationEndpoint annotations = values[i]; 300 301 for (AnnotationItem pointAnnotation: annotations.pointAnnotations) { 302 String paddingSub = padding.substring(0, pointAnnotation.indentLevel*2); 303 twoc.write("", paddingSub + pointAnnotation.annotation); 304 } 305 306 String right; 307 AnnotationItem rangeAnnotation = annotations.rangeAnnotation; 308 if (rangeAnnotation != null) { 309 right = padding.substring(0, rangeAnnotation.indentLevel*2); 310 right += rangeAnnotation.annotation; 311 } else { 312 right = ""; 313 } 314 315 String left = Hex.dump(data, rangeStart + offset, rangeEnd - rangeStart, rangeStart + offset, hexCols, 6); 316 317 twoc.write(left, right); 318 } 319 320 int lastKey = keys[keys.length-1]; 321 if (lastKey < data.length) { 322 String left = Hex.dump(data, lastKey + offset, (data.length - offset) - lastKey, lastKey + offset, hexCols, 6); 323 twoc.write(left, ""); 324 } 325 } 326 setLimit(int start, int end)327 public void setLimit(int start, int end) { 328 this.startLimit = start; 329 this.endLimit = end; 330 } 331 clearLimit()332 public void clearLimit() { 333 this.startLimit = -1; 334 this.endLimit = -1; 335 } 336 }