1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package dexfuzz.rawdex;
18
19import dexfuzz.Log;
20
21import java.io.IOException;
22import java.util.ArrayList;
23import java.util.HashMap;
24import java.util.List;
25import java.util.Map;
26
27/**
28 * This class allows the recording of both Offsettable items (that is, items that can be
29 * referred to by an offset somewhere else in the file - RawDexObjects) and Offsets.
30 * The idea in a nutshell is that for every Offsettable item we read, we remember
31 * its original position in the file using a map, and the order in which the Offsettables were
32 * written out. We also remember every Offset we read in, and its value. Then, after reading
33 * the whole file, we use the map to find the Offsettable it pointed at.
34 * Then, as we write out the file, for every Offsettable we write out, we record its new position,
35 * using the order we collected earlier. For every Offset we write out, we look at its Offsettable
36 * to see where it was written. If it hasn't been written yet, then we write out a blank value
37 * for the time being, remember where that blank value was written, and put the Offset into a
38 * table for patching once everything has been written out.
39 * There are some variables (index_after_map_list, restore_point) used for remembering certain
40 * points to jump forward and back to, because we cannot read and write the file out in exactly
41 * the same order.
42 * TODO: Perhaps it makes more sense to just reorder the offsettable_table once it's been read,
43 * in preparation for the order in which the file is written out?
44 * Finally, we provide methods for adding new Offsettable items into the right place in the order
45 * table.
46 */
47public class OffsetTracker {
48  /**
49   * A map from the original offset in the input DEX file to
50   * the Offsettable it points to. (That Offsettable will contain
51   * the actual item, and later on the new offset for the item when
52   * the item is written out.
53   */
54  private Map<Integer, Offsettable> offsettableMap;
55
56  /**
57   * A table of all Offsettables. We need to ensure we write out
58   * all items in the same order we read them in, to make sure we update
59   * the Offsettable.new_position field with the correct value wrt to
60   * the original_position field.
61   */
62  private List<Offsettable> offsettableTable;
63
64  /**
65   * A table of all offsets that is populated as we read in the DEX file.
66   * As the end, we find the correct Offsettable for the Offset in the above
67   * map, and associate them.
68   */
69  private List<Offset> needsAssociationTable;
70
71  /**
72   * A table of all offsets that we could not write out an updated offset for
73   * as we write out a DEX file. Will be checked after writing is complete,
74   * to allow specific patching of each offset's location as at that point
75   * all Offsettables will have been updated with their new position.
76   */
77  private List<Offset> needsUpdateTable;
78
79  /**
80   * Tracks how far we are through the offsettable_table as we write out the file.
81   */
82  private int offsettableTableIdx;
83
84  /**
85   * Because we write in a slightly different order to how we read
86   * (why? to read, we read the header, then the map list, and then use the map
87   *  list to read everything else.
88   *  when we write, we write the header, and then we cannot write the map list
89   *  because we don't know where it will go yet, so we write everything else first)
90   * so: we remember the position in the offsettable_table after we read the map list,
91   * so we can skip there after we write out the header.
92   */
93  private int indexAfterMapList;
94
95  /**
96   * Related to index_after_map_list, this is the index we save when we're jumping back to
97   * write the map list.
98   */
99  private int restorePoint;
100
101  /**
102   * Create a new OffsetTracker. Should persist between parsing a DEX file, and outputting
103   * the mutated DEX file.
104   */
105  public OffsetTracker() {
106    offsettableMap = new HashMap<Integer,Offsettable>();
107    offsettableTable = new ArrayList<Offsettable>();
108    needsAssociationTable = new ArrayList<Offset>();
109    needsUpdateTable = new ArrayList<Offset>();
110  }
111
112  /**
113   * Lookup an Item by the offset it had in the input DEX file.
114   * @param offset The offset in the input DEX file.
115   * @return The corresponding Item.
116   */
117  public RawDexObject getItemByOffset(int offset) {
118    return offsettableMap.get(offset).getItem();
119  }
120
121  /**
122   * As Items are read in, they call this function once they have word-aligned the file pointer,
123   * to record their position and themselves into an Offsettable object, that will be tracked.
124   * @param file Used for recording position into the new Offsettable.
125   * @param item Used for recording the relevant Item into the new Offsettable.
126   */
127  public void getNewOffsettable(DexRandomAccessFile file, RawDexObject item) throws IOException {
128    Offsettable offsettable = new Offsettable(item, false);
129    offsettable.setOriginalPosition((int) file.getFilePointer());
130    offsettableMap.put(offsettable.getOriginalPosition(), offsettable);
131    offsettableTable.add(offsettable);
132  }
133
134  /**
135   * As Items read in Offsets, they call this function with the offset they originally
136   * read from the file, to allow later association with an Offsettable.
137   * @param originalOffset The original offset read from the input DEX file.
138   * @return An Offset that will later be associated with an Offsettable.
139   */
140  public Offset getNewOffset(int originalOffset) throws IOException {
141    Offset offset = new Offset(false);
142    offset.setOriginalOffset(originalOffset);
143    needsAssociationTable.add(offset);
144    return offset;
145  }
146
147  /**
148   * Only MapItem should call this method, when the MapItem that points to the header
149   * is read.
150   */
151  public Offset getNewHeaderOffset(int originalOffset) throws IOException {
152    Offset offset = new Offset(true);
153    offset.setOriginalOffset(originalOffset);
154    needsAssociationTable.add(offset);
155    return offset;
156  }
157
158  /**
159   * Call this after reading, to associate Offsets with Offsettables.
160   */
161  public void associateOffsets() {
162    for (Offset offset : needsAssociationTable) {
163      if (offset.getOriginalOffset() == 0 && !(offset.pointsAtHeader())) {
164        offset.setPointsAtNull();
165      } else {
166        offset.pointTo(offsettableMap.get(offset.getOriginalOffset()));
167        if (!offset.pointsToSomething()) {
168          Log.error(String.format("Couldn't find original offset 0x%x!",
169              offset.getOriginalOffset()));
170        }
171      }
172    }
173    needsAssociationTable.clear();
174  }
175
176  /**
177   * As Items are written out into the output DEX file, this function is called
178   * to update the next Offsettable with the file pointer's current position.
179   * This should allow the tracking of new offset locations.
180   * This also requires that reading and writing of all items happens in the same order
181   * (with the exception of the map list, see above)
182   * @param file Used for recording the new position.
183   */
184  public void updatePositionOfNextOffsettable(DexRandomAccessFile file) throws IOException {
185    if (offsettableTableIdx == offsettableTable.size()) {
186      Log.errorAndQuit("Not all created Offsettable items have been added to the "
187          + "Offsettable Table!");
188    }
189    Offsettable offsettable = offsettableTable.get(offsettableTableIdx);
190    offsettable.setNewPosition((int) file.getFilePointer());
191    offsettableTableIdx++;
192  }
193
194  /**
195   * As Items are written out, any writing out of an offset must call this function, passing
196   * in the relevant offset. This function will write out the offset, if the associated
197   * Offsettable has been updated with its new position, or else will write out a null value, and
198   * the Offset will be stored for writing after all Items have been written, and all
199   * Offsettables MUST have been updated.
200   * @param offset The offset received from getNewOffset().
201   * @param file Used for writing out to the file.
202   * @param useUleb128 Whether or not the offset should be written in UINT or ULEB128 form.
203   */
204  public void tryToWriteOffset(Offset offset, DexRandomAccessFile file, boolean useUleb128)
205      throws IOException {
206    if (!offset.isNewOffset() && (!offset.pointsToSomething())) {
207      if (useUleb128) {
208        file.writeUleb128(0);
209      } else {
210        file.writeUInt(0);
211      }
212      return;
213    }
214
215    if (offset.readyForWriting()) {
216      if (useUleb128) {
217        file.writeUleb128(offset.getNewPositionOfItem());
218      } else {
219        file.writeUInt(offset.getNewPositionOfItem());
220      }
221    } else {
222      offset.setOutputLocation((int) file.getFilePointer());
223      if (useUleb128) {
224        file.writeLargestUleb128(offset.getOriginalOffset());
225        offset.setUsesUleb128();
226      } else {
227        file.writeUInt(offset.getOriginalOffset());
228      }
229      needsUpdateTable.add(offset);
230    }
231  }
232
233  /**
234   * This is called after all writing has finished, to write out any Offsets
235   * that could not be written out during the original writing phase, because their
236   * associated Offsettables hadn't had their new positions calculated yet.
237   * @param file Used for writing out to the file.
238   */
239  public void updateOffsets(DexRandomAccessFile file) throws IOException {
240    if (offsettableTableIdx != offsettableTable.size()) {
241      Log.errorAndQuit("Being asked to update dangling offsets but the "
242          + "correct number of offsettables has not been written out!");
243    }
244    for (Offset offset : needsUpdateTable) {
245      file.seek(offset.getOutputLocation());
246      if (offset.usesUleb128()) {
247        file.writeLargestUleb128(offset.getNewPositionOfItem());
248      } else {
249        file.writeUInt(offset.getNewPositionOfItem());
250      }
251    }
252    needsUpdateTable.clear();
253  }
254
255  /**
256   * Called after writing out the header, to skip to after the map list.
257   */
258  public void skipToAfterMapList() {
259    offsettableTableIdx = indexAfterMapList;
260  }
261
262  /**
263   * Called once the map list needs to be written out, to set the
264   * offsettable table index back to the right place.
265   */
266  public void goBackToMapList() {
267    restorePoint = offsettableTableIdx;
268    offsettableTableIdx = (indexAfterMapList - 1);
269  }
270
271  /**
272   * Called once the map list has been written out, to set the
273   * offsettable table index back to where it was before.
274   */
275  public void goBackToPreviousPoint() {
276    if (offsettableTableIdx != indexAfterMapList) {
277      Log.errorAndQuit("Being asked to go to the point before the MapList was written out,"
278          + " but we're not in the right place.");
279    }
280    offsettableTableIdx = restorePoint;
281  }
282
283  /**
284   * Called after reading in the map list, to remember the point to be skipped
285   * to later.
286   */
287  public void rememberPointAfterMapList() {
288    indexAfterMapList = offsettableTable.size();
289  }
290
291  private void updateHeaderOffsetIfValid(Offset offset, Offsettable previousFirst,
292      Offsettable newFirst, String offsetName) {
293    if (offset.pointsToThisOffsettable(previousFirst)) {
294      offset.pointToNew(newFirst);
295    } else {
296      Log.errorAndQuit("Header " + offsetName + " offset not pointing at first element?");
297    }
298  }
299
300  private void addTypeListsToMapFile(RawDexFile rawDexFile, Offsettable typeListOffsettable) {
301    // Create a MapItem for the TypeLists
302    MapItem typeListMapItem = new MapItem();
303    typeListMapItem.offset = new Offset(false);
304    typeListMapItem.offset.pointToNew(typeListOffsettable);
305    typeListMapItem.type = MapItem.TYPE_TYPE_LIST;
306    typeListMapItem.size = 1;
307
308    // Insert into the MapList.
309    // (first, find the MapItem that points to StringDataItems...)
310    int idx = 0;
311    for (MapItem mapItem : rawDexFile.mapList.mapItems) {
312      if (mapItem.type == MapItem.TYPE_STRING_DATA_ITEM) {
313        break;
314      }
315      idx++;
316    }
317    // (now insert the TypeList MapItem just before the StringDataItem one...)
318    rawDexFile.mapList.mapItems.add(idx, typeListMapItem);
319  }
320
321  private void addFieldIdsToHeaderAndMapFile(RawDexFile rawDexFile,
322      Offsettable fieldOffsettable) {
323    // Add the field IDs to the header.
324    rawDexFile.header.fieldIdsOff.unsetNullAndPointTo(fieldOffsettable);
325    rawDexFile.header.fieldIdsSize = 1;
326
327    // Create a MapItem for the field IDs.
328    MapItem fieldMapItem = new MapItem();
329    fieldMapItem.offset = new Offset(false);
330    fieldMapItem.offset.pointToNew(fieldOffsettable);
331    fieldMapItem.type = MapItem.TYPE_FIELD_ID_ITEM;
332    fieldMapItem.size = 1;
333
334    // Insert into the MapList.
335    // (first, find the MapItem that points to MethodIdItems...)
336    int idx = 0;
337    for (MapItem mapItem : rawDexFile.mapList.mapItems) {
338      if (mapItem.type == MapItem.TYPE_METHOD_ID_ITEM) {
339        break;
340      }
341      idx++;
342    }
343    // (now insert the FieldIdItem MapItem just before the MethodIdItem one...)
344    rawDexFile.mapList.mapItems.add(idx, fieldMapItem);
345  }
346
347
348  private void updateOffsetsInHeaderAndMapFile(RawDexFile rawDexFile,
349      Offsettable newFirstOffsettable) {
350    Offsettable prevFirstOffsettable = null;
351    for (int i = 0; i < offsettableTable.size(); i++) {
352      if (offsettableTable.get(i) == newFirstOffsettable) {
353        prevFirstOffsettable = offsettableTable.get(i + 1);
354        break;
355      }
356    }
357    if (prevFirstOffsettable == null) {
358      Log.errorAndQuit("When calling updateMapListOffsets, could not find new "
359          + "first offsettable?");
360    }
361
362    // Based on the type of the item we just added, check the relevant Offset in the header
363    // and if it pointed at the prev_first_offsettable, make it point at the new one.
364    // NB: if it isn't pointing at the prev one, something is wrong.
365    HeaderItem header = rawDexFile.header;
366    if (newFirstOffsettable.getItem() instanceof StringIdItem) {
367      updateHeaderOffsetIfValid(header.stringIdsOff, prevFirstOffsettable,
368          newFirstOffsettable, "StringID");
369    } else if (newFirstOffsettable.getItem() instanceof TypeIdItem) {
370      updateHeaderOffsetIfValid(header.typeIdsOff, prevFirstOffsettable,
371          newFirstOffsettable, "TypeID");
372    } else if (newFirstOffsettable.getItem() instanceof ProtoIdItem) {
373      updateHeaderOffsetIfValid(header.protoIdsOff, prevFirstOffsettable,
374          newFirstOffsettable, "ProtoID");
375    } else if (newFirstOffsettable.getItem() instanceof FieldIdItem) {
376      updateHeaderOffsetIfValid(header.fieldIdsOff, prevFirstOffsettable,
377          newFirstOffsettable, "FieldID");
378    } else if (newFirstOffsettable.getItem() instanceof MethodIdItem) {
379      updateHeaderOffsetIfValid(header.methodIdsOff, prevFirstOffsettable,
380          newFirstOffsettable, "MethodID");
381    } else if (newFirstOffsettable.getItem() instanceof ClassDefItem) {
382      updateHeaderOffsetIfValid(header.classDefsOff, prevFirstOffsettable,
383          newFirstOffsettable, "ClassDef");
384    }
385
386    // Now iterate through the MapList's MapItems, and see if their Offsets pointed at the
387    // prev_first_offsettable, and if so, make them now point at the new_first_offsettable.
388    for (MapItem mapItem : rawDexFile.mapList.mapItems) {
389      if (mapItem.offset.pointsToThisOffsettable(prevFirstOffsettable)) {
390        Log.info("Updating offset in MapItem (type: " + mapItem.type + ") after "
391            + "we called insertNewOffsettableAsFirstOfType()");
392        mapItem.offset.pointToNew(newFirstOffsettable);
393      }
394    }
395  }
396
397  private void insertOffsettableAt(int idx, Offsettable offsettable) {
398    offsettableTable.add(idx, offsettable);
399    if (indexAfterMapList > idx) {
400      indexAfterMapList++;
401    }
402    if (restorePoint > idx) {
403      restorePoint++;
404    }
405  }
406
407  /**
408   * If we're creating our first TypeList, then IdCreator has to call this method to
409   * ensure it gets put into the correct place in the offsettable table.
410   * This assumes TypeLists always come before StringDatas.
411   */
412  public Offsettable insertNewOffsettableAsFirstEverTypeList(RawDexObject item,
413      RawDexFile rawDexFile) {
414    // We find the first StringDataItem, the type lists will come before this.
415    Log.info("Calling insertNewOffsettableAsFirstEverTypeList()");
416    for (int i = 0; i < offsettableTable.size(); i++) {
417      if (offsettableTable.get(i).getItem() instanceof StringDataItem) {
418        Offsettable offsettable = new Offsettable(item, true);
419        insertOffsettableAt(i, offsettable);
420        addTypeListsToMapFile(rawDexFile, offsettable);
421        return offsettable;
422      }
423    }
424    Log.errorAndQuit("Could not find any StringDataItems to insert the type list before.");
425    return null;
426  }
427
428  /**
429   * If we're creating our first FieldId, then IdCreator has to call this method to
430   * ensure it gets put into the correct place in the offsettable table.
431   * This assumes FieldIds always come before MethodIds.
432   */
433  public Offsettable insertNewOffsettableAsFirstEverField(RawDexObject item,
434      RawDexFile rawDexFile) {
435    // We find the first MethodIdItem, the fields will come before this.
436    Log.info("Calling insertNewOffsettableAsFirstEverField()");
437    for (int i = 0; i < offsettableTable.size(); i++) {
438      if (offsettableTable.get(i).getItem() instanceof MethodIdItem) {
439        Offsettable offsettable = new Offsettable(item, true);
440        insertOffsettableAt(i, offsettable);
441        addFieldIdsToHeaderAndMapFile(rawDexFile, offsettable);
442        return offsettable;
443      }
444    }
445    Log.errorAndQuit("Could not find any MethodIdItems to insert the field before.");
446    return null;
447  }
448
449  /**
450   * If we're creating a new Item (such as FieldId, MethodId) that is placed into the
451   * first position of the relevant ID table, then IdCreator has to call this method to
452   * ensure it gets put into the correct place in the offsettable table.
453   */
454  public Offsettable insertNewOffsettableAsFirstOfType(RawDexObject item,
455      RawDexFile rawDexFile) {
456    Log.debug("Calling insertNewOffsettableAsFirstOfType()");
457    int index = getOffsettableIndexForFirstItemType(item);
458    if (index == -1) {
459      Log.errorAndQuit("Could not find any object of class: " + item.getClass());
460    }
461    Offsettable offsettable = new Offsettable(item, true);
462    insertOffsettableAt(index, offsettable);
463    updateOffsetsInHeaderAndMapFile(rawDexFile, offsettable);
464    return offsettable;
465  }
466
467  /**
468   * IdCreator has to call this method when it creates a new IdItem, to make sure it
469   * gets put into the correct place in the offsettable table. IdCreator should
470   * provide the IdItem that should come before this new IdItem.
471   */
472  public Offsettable insertNewOffsettableAfter(RawDexObject item, RawDexObject itemBefore) {
473    Log.debug("Calling insertNewOffsettableAfter()");
474    int index = getOffsettableIndexForItem(itemBefore);
475    if (index == -1) {
476      Log.errorAndQuit("Did not find specified 'after' object in offsettable table.");
477    }
478    Offsettable offsettable = new Offsettable(item, true);
479    insertOffsettableAt(index + 1, offsettable);
480    return offsettable;
481  }
482
483  private int getOffsettableIndexForFirstItemType(RawDexObject item) {
484    Class<?> itemClass = item.getClass();
485    for (int i = 0; i < offsettableTable.size(); i++) {
486      if (offsettableTable.get(i).getItem().getClass().equals(itemClass)) {
487        return i;
488      }
489    }
490    return -1;
491  }
492
493  private int getOffsettableIndexForItem(RawDexObject item) {
494    for (int i = 0; i < offsettableTable.size(); i++) {
495      if (offsettableTable.get(i).getItem() == item) {
496        return i;
497      }
498    }
499    return -1;
500  }
501
502  /**
503   * Given a RawDexObject, get the Offsettable that contains it.
504   */
505  public Offsettable getOffsettableForItem(RawDexObject item) {
506    for (int i = 0; i < offsettableTable.size(); i++) {
507      if (offsettableTable.get(i).getItem() == item) {
508        return offsettableTable.get(i);
509      }
510    }
511    return null;
512  }
513}
514