• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     /** &gt;= 40 (if used); the desired maximum output width */
72     private int outputWidth;
73 
74     /**
75      * &gt;= 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 }