1a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler/*
2a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * Copyright (C) 2016 Google Inc.
3a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler *
4a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * Licensed under the Apache License, Version 2.0 (the "License");
5a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * you may not use this file except in compliance with the License.
6a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * You may obtain a copy of the License at
7a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler *
8a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler *      http://www.apache.org/licenses/LICENSE-2.0
9a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler *
10a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * Unless required by applicable law or agreed to in writing, software
11a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * distributed under the License is distributed on an "AS IS" BASIS,
12a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * See the License for the specific language governing permissions and
14a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler * limitations under the License.
15a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler */
16a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
17a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerpackage com.android.ahat.proguard;
18a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
19a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.io.BufferedReader;
20a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.io.File;
21a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.io.FileNotFoundException;
22a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.io.FileReader;
23a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.io.IOException;
24a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.io.Reader;
25a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.text.ParseException;
26a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.util.HashMap;
27a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerimport java.util.Map;
28a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
29b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler/**
30b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler * A representation of a proguard mapping for deobfuscating class names,
31b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler * field names, and stack frames.
32b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler */
33a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhlerpublic class ProguardMap {
34a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
35a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  private static final String ARRAY_SYMBOL = "[]";
36a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
37a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  private static class FrameData {
38a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public FrameData(String clearMethodName, int lineDelta) {
39a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      this.clearMethodName = clearMethodName;
40a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      this.lineDelta = lineDelta;
41a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
42a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
43a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public final String clearMethodName;
44a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public final int lineDelta;   // lineDelta = obfuscatedLine - clearLine
45a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
46a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
47a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  private static class ClassData {
48a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    private final String mClearName;
49a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
50a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // Mapping from obfuscated field name to clear field name.
51a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    private final Map<String, String> mFields = new HashMap<String, String>();
52a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
53a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // obfuscatedMethodName + clearSignature -> FrameData
54a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    private final Map<String, FrameData> mFrames = new HashMap<String, FrameData>();
55a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
56a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // Constructs a ClassData object for a class with the given clear name.
57a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public ClassData(String clearName) {
58a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      mClearName = clearName;
59a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
60a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
61a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // Returns the clear name of the class.
62a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public String getClearName() {
63a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return mClearName;
64a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
65a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
66a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public void addField(String obfuscatedName, String clearName) {
67a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      mFields.put(obfuscatedName, clearName);
68a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
69a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
70a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // Get the clear name for the field in this class with the given
71a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // obfuscated name. Returns the original obfuscated name if a clear
72a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // name for the field could not be determined.
73a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // TODO: Do we need to take into account the type of the field to
74a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // propery determine the clear name?
75a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public String getField(String obfuscatedName) {
76a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      String clearField = mFields.get(obfuscatedName);
77a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return clearField == null ? obfuscatedName : clearField;
78a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
79a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
80a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // TODO: Does this properly interpret the meaning of line numbers? Is
81a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // it possible to have multiple frame entries for the same method
82a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // name and signature that differ only by line ranges?
83a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public void addFrame(String obfuscatedMethodName, String clearMethodName,
84a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        String clearSignature, int obfuscatedLine, int clearLine) {
85a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      String key = obfuscatedMethodName + clearSignature;
86a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      mFrames.put(key, new FrameData(clearMethodName, obfuscatedLine - clearLine));
87a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
88a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
89a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public Frame getFrame(String clearClassName, String obfuscatedMethodName,
90a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        String clearSignature, String obfuscatedFilename, int obfuscatedLine) {
91a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      String key = obfuscatedMethodName + clearSignature;
92a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      FrameData frame = mFrames.get(key);
93a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      if (frame == null) {
9488aa6900f62421906af0c3f7a0c245b4f2aef50cRichard Uhler        frame = new FrameData(obfuscatedMethodName, 0);
95a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      }
96a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return new Frame(frame.clearMethodName, clearSignature,
9788aa6900f62421906af0c3f7a0c245b4f2aef50cRichard Uhler          getFileName(clearClassName), obfuscatedLine - frame.lineDelta);
98a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
99a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
100a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
101a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  private Map<String, ClassData> mClassesFromClearName = new HashMap<String, ClassData>();
102a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  private Map<String, ClassData> mClassesFromObfuscatedName = new HashMap<String, ClassData>();
103a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
104b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  /**
105b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * Information associated with a stack frame that identifies a particular
106b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * line of source code.
107b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   */
108a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  public static class Frame {
1090a85a95210b3874722a3b36b57fb12307b4b635dRichard Uhler    Frame(String method, String signature, String filename, int line) {
110a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      this.method = method;
111a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      this.signature = signature;
112a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      this.filename = filename;
113a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      this.line = line;
114a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
115a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
116b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler    /**
117b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     * The name of the method the stack frame belongs to.
118b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     * For example, "equals".
119b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     */
120a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public final String method;
121b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler
122b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler    /**
123b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     * The signature of the method the stack frame belongs to.
124b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     * For example, "(Ljava/lang/Object;)Z".
125b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     */
126a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public final String signature;
127b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler
128b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler    /**
129b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     * The name of the file with containing the line of source that the stack
130b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     * frame refers to.
131b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     */
132a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public final String filename;
133b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler
134b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler    /**
135b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     * The line number of the code in the source file that the stack frame
136b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     * refers to.
137b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler     */
138a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    public final int line;
139a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
140a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
141a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  private static void parseException(String msg) throws ParseException {
142a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    throw new ParseException(msg, 0);
143a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
144a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
145b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  /**
146b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * Creates a new empty proguard mapping.
147b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * The {@link #readFromFile readFromFile} and
148b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * {@link #readFromReader readFromReader} methods can be used to populate
149b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * the proguard mapping with proguard mapping information.
150b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   */
151b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  public ProguardMap() {
152b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  }
153b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler
154b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  /**
155b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * Adds the proguard mapping information in <code>mapFile</code> to this
156b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * proguard mapping.
157b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * The <code>mapFile</code> should be a proguard mapping file generated with
158b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * the <code>-printmapping</code> option when proguard was run.
159b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   *
160b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param mapFile the name of a file with proguard mapping information
161b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @throws FileNotFoundException If the <code>mapFile</code> could not be
162b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   *                               found
163b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @throws IOException If an input exception occurred.
164b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @throws ParseException If the <code>mapFile</code> is not a properly
165b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   *                        formatted proguard mapping file.
166b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   */
167a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  public void readFromFile(File mapFile)
168a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    throws FileNotFoundException, IOException, ParseException {
169a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    readFromReader(new FileReader(mapFile));
170a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
171a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
172b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  /**
173b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * Adds the proguard mapping information read from <code>mapReader</code> to
174b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * this proguard mapping.
175b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * <code>mapReader</code> should be a Reader of a proguard mapping file
176b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * generated with the <code>-printmapping</code> option when proguard was run.
177b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   *
178b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param mapReader a Reader for reading the proguard mapping information
179b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @throws IOException If an input exception occurred.
180b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @throws ParseException If the <code>mapFile</code> is not a properly
181b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   *                        formatted proguard mapping file.
182b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   */
183a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  public void readFromReader(Reader mapReader) throws IOException, ParseException {
184a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    BufferedReader reader = new BufferedReader(mapReader);
185a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    String line = reader.readLine();
186a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    while (line != null) {
187a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      // Class lines are of the form:
188a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      //   'clear.class.name -> obfuscated_class_name:'
189a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      int sep = line.indexOf(" -> ");
190a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      if (sep == -1 || sep + 5 >= line.length()) {
191a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        parseException("Error parsing class line: '" + line + "'");
192a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      }
193a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      String clearClassName = line.substring(0, sep);
194a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      String obfuscatedClassName = line.substring(sep + 4, line.length() - 1);
195a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
196a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      ClassData classData = new ClassData(clearClassName);
197a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      mClassesFromClearName.put(clearClassName, classData);
198a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      mClassesFromObfuscatedName.put(obfuscatedClassName, classData);
199a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
200a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      // After the class line comes zero or more field/method lines of the form:
201a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      //   '    type clearName -> obfuscatedName'
202a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      line = reader.readLine();
203a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      while (line != null && line.startsWith("    ")) {
204a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        String trimmed = line.trim();
205a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        int ws = trimmed.indexOf(' ');
206a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        sep = trimmed.indexOf(" -> ");
207a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        if (ws == -1 || sep == -1) {
208a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          parseException("Error parse field/method line: '" + line + "'");
209a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        }
210a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
211a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        String type = trimmed.substring(0, ws);
212a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        String clearName = trimmed.substring(ws + 1, sep);
213a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        String obfuscatedName = trimmed.substring(sep + 4, trimmed.length());
214a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
215a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        // If the clearName contains '(', then this is for a method instead of a
216a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        // field.
217a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        if (clearName.indexOf('(') == -1) {
218a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          classData.addField(obfuscatedName, clearName);
219a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        } else {
220a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          // For methods, the type is of the form: [#:[#:]]<returnType>
221a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          int obfuscatedLine = 0;
222a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          int colon = type.indexOf(':');
223a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          if (colon != -1) {
224a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler            obfuscatedLine = Integer.parseInt(type.substring(0, colon));
225a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler            type = type.substring(colon + 1);
226a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          }
227a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          colon = type.indexOf(':');
228a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          if (colon != -1) {
229a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler            type = type.substring(colon + 1);
230a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          }
231a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
232a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          // For methods, the clearName is of the form: <clearName><sig>[:#[:#]]
233a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          int op = clearName.indexOf('(');
234a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          int cp = clearName.indexOf(')');
235a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          if (op == -1 || cp == -1) {
236a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler            parseException("Error parse method line: '" + line + "'");
237a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          }
238a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
239a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          String sig = clearName.substring(op, cp + 1);
240a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
241a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          int clearLine = obfuscatedLine;
242a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          colon = clearName.lastIndexOf(':');
243a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          if (colon != -1) {
244a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler            clearLine = Integer.parseInt(clearName.substring(colon + 1));
245a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler            clearName = clearName.substring(0, colon);
246a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          }
247a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
248a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          colon = clearName.lastIndexOf(':');
249a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          if (colon != -1) {
250a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler            clearLine = Integer.parseInt(clearName.substring(colon + 1));
251a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler            clearName = clearName.substring(0, colon);
252a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          }
253a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
254a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          clearName = clearName.substring(0, op);
255a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
256a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          String clearSig = fromProguardSignature(sig + type);
257a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          classData.addFrame(obfuscatedName, clearName, clearSig,
258a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler              obfuscatedLine, clearLine);
259a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        }
260a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
261a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        line = reader.readLine();
262a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      }
263a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
264a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    reader.close();
265a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
266a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
267b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  /**
268b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * Returns the deobfuscated version of the given obfuscated class name.
269b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * If this proguard mapping does not include information about how to
270b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * deobfuscate the obfuscated class name, the obfuscated class name
271b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * is returned.
272b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   *
273b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param obfuscatedClassName the obfuscated class name to deobfuscate
274b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @return the deobfuscated class name.
275b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   */
276a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  public String getClassName(String obfuscatedClassName) {
277a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // Class names for arrays may have trailing [] that need to be
278a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    // stripped before doing the lookup.
279a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    String baseName = obfuscatedClassName;
280a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    String arraySuffix = "";
281a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    while (baseName.endsWith(ARRAY_SYMBOL)) {
282a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      arraySuffix += ARRAY_SYMBOL;
283a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      baseName = baseName.substring(0, baseName.length() - ARRAY_SYMBOL.length());
284a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
285a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
286a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    ClassData classData = mClassesFromObfuscatedName.get(baseName);
287a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    String clearBaseName = classData == null ? baseName : classData.getClearName();
288a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    return clearBaseName + arraySuffix;
289a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
290a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
291b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  /**
292b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * Returns the deobfuscated version of the obfuscated field name for the
293b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * given deobfuscated class name.
294b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * If this proguard mapping does not include information about how to
295b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * deobfuscate the obfuscated field name, the obfuscated field name is
296b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * returned.
297b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   *
298b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param clearClass the deobfuscated name of the class the field belongs to
299b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param obfuscatedField the obfuscated field name to deobfuscate
300b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @return the deobfuscated field name.
301b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   */
302a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  public String getFieldName(String clearClass, String obfuscatedField) {
303a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    ClassData classData = mClassesFromClearName.get(clearClass);
304a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    if (classData == null) {
305a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return obfuscatedField;
306a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
307a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    return classData.getField(obfuscatedField);
308a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
309a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
310b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler  /**
311b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * Returns the deobfuscated version of the obfuscated stack frame
312b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * information for the given deobfuscated class name.
313b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * If this proguard mapping does not include information about how to
314b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * deobfuscate the obfuscated stack frame information, the obfuscated stack
315b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * frame information is returned.
316b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   *
317b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param clearClassName the deobfuscated name of the class the stack frame's
318b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * method belongs to
319b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param obfuscatedMethodName the obfuscated method name to deobfuscate
320b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param obfuscatedSignature the obfuscated method signature to deobfuscate
321b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param obfuscatedFilename the obfuscated file name to deobfuscate.
322b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @param obfuscatedLine the obfuscated line number to deobfuscate.
323b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   * @return the deobfuscated stack frame information.
324b7732a3fcf06a55075a601dbc593b23a6fc71dbfRichard Uhler   */
325a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  public Frame getFrame(String clearClassName, String obfuscatedMethodName,
326a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine) {
327a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    String clearSignature = getSignature(obfuscatedSignature);
328a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    ClassData classData = mClassesFromClearName.get(clearClassName);
329a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    if (classData == null) {
330a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return new Frame(obfuscatedMethodName, clearSignature,
331a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          obfuscatedFilename, obfuscatedLine);
332a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
333a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    return classData.getFrame(clearClassName, obfuscatedMethodName, clearSignature,
334a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        obfuscatedFilename, obfuscatedLine);
335a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
336a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
337a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  // Converts a proguard-formatted method signature into a Java formatted
338a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  // method signature.
339a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  private static String fromProguardSignature(String sig) throws ParseException {
340a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    if (sig.startsWith("(")) {
341a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      int end = sig.indexOf(')');
342a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      if (end == -1) {
343a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        parseException("Error parsing signature: " + sig);
344a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      }
345a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
346a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      StringBuilder converted = new StringBuilder();
347a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      converted.append('(');
348a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      if (end > 1) {
349a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        for (String arg : sig.substring(1, end).split(",")) {
350a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler          converted.append(fromProguardSignature(arg));
351a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        }
352a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      }
353a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      converted.append(')');
354a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      converted.append(fromProguardSignature(sig.substring(end + 1)));
355a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return converted.toString();
356a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.endsWith(ARRAY_SYMBOL)) {
357a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "[" + fromProguardSignature(sig.substring(0, sig.length() - 2));
358a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("boolean")) {
359a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "Z";
360a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("byte")) {
361a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "B";
362a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("char")) {
363a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "C";
364a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("short")) {
365a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "S";
366a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("int")) {
367a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "I";
368a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("long")) {
369a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "J";
370a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("float")) {
371a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "F";
372a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("double")) {
373a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "D";
374a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else if (sig.equals("void")) {
375a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "V";
376a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    } else {
377a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      return "L" + sig.replace('.', '/') + ";";
378a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
379a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
380a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
381a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  // Return a clear signature for the given obfuscated signature.
382a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  private String getSignature(String obfuscatedSig) {
383a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    StringBuilder builder = new StringBuilder();
384a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    for (int i = 0; i < obfuscatedSig.length(); i++) {
385a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      if (obfuscatedSig.charAt(i) == 'L') {
386a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        int e = obfuscatedSig.indexOf(';', i);
387a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        builder.append('L');
388a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        String cls = obfuscatedSig.substring(i + 1, e).replace('/', '.');
389a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        builder.append(getClassName(cls).replace('.', '/'));
390a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        builder.append(';');
391a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        i = e;
392a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      } else {
393a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler        builder.append(obfuscatedSig.charAt(i));
394a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      }
395a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
396a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    return builder.toString();
397a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
398a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
39988aa6900f62421906af0c3f7a0c245b4f2aef50cRichard Uhler  // Return a file name for the given clear class name.
40088aa6900f62421906af0c3f7a0c245b4f2aef50cRichard Uhler  private static String getFileName(String clearClass) {
401a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    String filename = clearClass;
40288aa6900f62421906af0c3f7a0c245b4f2aef50cRichard Uhler    int dot = filename.lastIndexOf('.');
403a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    if (dot != -1) {
404a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      filename = filename.substring(dot + 1);
405a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
406a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler
407a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    int dollar = filename.indexOf('$');
408a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    if (dollar != -1) {
409a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler      filename = filename.substring(0, dollar);
410a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    }
411a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler    return filename + ".java";
412a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler  }
413a3b7cf0f12da448664536f8b7c793e73da0a4c7fRichard Uhler}
414