xref: /aosp_15_r20/external/leakcanary2/shark-hprof/src/main/java/shark/HprofDeobfuscator.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)

<lambda>null1 package shark
2 
3 import shark.HprofHeader.Companion.parseHeaderOf
4 import shark.HprofRecord.HeapDumpEndRecord
5 import shark.HprofRecord.HeapDumpRecord.ObjectRecord
6 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
7 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
8 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.StaticFieldRecord
9 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
10 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
11 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
12 import shark.HprofRecord.LoadClassRecord
13 import shark.HprofRecord.StackFrameRecord
14 import shark.HprofRecord.StringRecord
15 import shark.StreamingRecordReaderAdapter.Companion.asStreamingRecordReader
16 import java.io.File
17 
18 /**
19  * Converts a Hprof file to another file with deobfuscated class and field names.
20  */
21 class HprofDeobfuscator {
22 
23   /**
24    * @see HprofDeobfuscator
25    */
26   fun deobfuscate(
27     proguardMapping: ProguardMapping,
28     inputHprofFile: File,
29     /**
30      * Optional output file. Defaults to a file in the same directory as [inputHprofFile], with
31      * the same name and "-deobfuscated" prepended before the ".hprof" extension. If the file extension
32      * is not ".hprof", then "-deobfuscated" is added at the end of the file.
33      */
34     outputHprofFile: File = File(
35       inputHprofFile.parent, inputHprofFile.name.replace(
36       ".hprof", "-deobfuscated.hprof"
37     ).let { if (it != inputHprofFile.name) it else inputHprofFile.name + "-deobfuscated" })
38   ): File {
39     val (hprofStringCache, classNames, maxId) = readHprofRecords(inputHprofFile)
40 
41     return writeHprofRecords(
42       inputHprofFile,
43       outputHprofFile,
44       proguardMapping,
45       hprofStringCache,
46       classNames,
47       maxId + 1
48     )
49   }
50 
51   /**
52    * Reads StringRecords and LoadClassRecord from an Hprof file and tracks maximum HprofRecord id
53    * value.
54    *
55    * @return a Triple of: hprofStringCache map, classNames map and maxId value
56    */
57   private fun readHprofRecords(
58     inputHprofFile: File
59   ): Triple<Map<Long, String>, Map<Long, Long>, Long> {
60     val hprofStringCache = mutableMapOf<Long, String>()
61     val classNames = mutableMapOf<Long, Long>()
62 
63     var maxId: Long = 0
64 
65     val reader = StreamingHprofReader.readerFor(inputHprofFile).asStreamingRecordReader()
66     reader.readRecords(setOf(HprofRecord::class)
67     ) { _, record ->
68       when (record) {
69         is StringRecord -> {
70           maxId = maxId.coerceAtLeast(record.id)
71           hprofStringCache[record.id] = record.string
72         }
73         is LoadClassRecord -> {
74           maxId = maxId.coerceAtLeast(record.id)
75           classNames[record.id] = record.classNameStringId
76         }
77         is StackFrameRecord -> maxId = maxId.coerceAtLeast(record.id)
78         is ObjectRecord -> {
79           maxId = when (record) {
80             is ClassDumpRecord -> maxId.coerceAtLeast(record.id)
81             is InstanceDumpRecord -> maxId.coerceAtLeast(record.id)
82             is ObjectArrayDumpRecord -> maxId.coerceAtLeast(record.id)
83             is PrimitiveArrayDumpRecord -> maxId.coerceAtLeast(record.id)
84           }
85         }
86       }
87     }
88     return Triple(hprofStringCache, classNames, maxId)
89   }
90 
91   @Suppress("LongParameterList")
92   private fun writeHprofRecords(
93     inputHprofFile: File,
94     outputHprofFile: File,
95     proguardMapping: ProguardMapping,
96     hprofStringCache: Map<Long, String>,
97     classNames: Map<Long, Long>,
98     firstId: Long
99   ): File {
100     var id = firstId
101 
102     val hprofHeader = parseHeaderOf(inputHprofFile)
103     val reader =
104       StreamingHprofReader.readerFor(inputHprofFile, hprofHeader).asStreamingRecordReader()
105     HprofWriter.openWriterFor(
106       outputHprofFile,
107       hprofHeader = hprofHeader
108     ).use { writer ->
109       reader.readRecords(setOf(HprofRecord::class),
110         OnHprofRecordListener { _,
111           record ->
112           // HprofWriter automatically emits HeapDumpEndRecord, because it flushes
113           // all continuous heap dump sub records as one heap dump record.
114           if (record is HeapDumpEndRecord) {
115             return@OnHprofRecordListener
116           }
117 
118           when (record) {
119             is StringRecord -> {
120               writer.write(
121                 createDeobfuscatedStringRecord(record, proguardMapping, hprofStringCache)
122               )
123             }
124             is ClassDumpRecord -> {
125               val (recordsToWrite, maxId) = createDeobfuscatedClassDumpRecord(
126                 record, proguardMapping, hprofStringCache, classNames, id
127               )
128               id = maxId
129               recordsToWrite.forEach {
130                 writer.write(it)
131               }
132             }
133             else -> writer.write(record)
134           }
135         })
136     }
137 
138     return outputHprofFile
139   }
140 
141   private fun createDeobfuscatedStringRecord(
142     record: StringRecord,
143     proguardMapping: ProguardMapping,
144     hprofStringCache: Map<Long, String>
145   ): StringRecord {
146     val obfuscatedName = hprofStringCache[record.id]!!
147     return StringRecord(
148       record.id, proguardMapping.deobfuscateClassName(obfuscatedName)
149     )
150   }
151 
152   /**
153    * Deobfuscated ClassDumpRecord's field names. Different classes can have fields with the same
154    * names. We need to generate new StringRecords in such cases.
155    *
156    * @return a Pair of: list of HprofRecords to write and new maxId value
157    */
158   private fun createDeobfuscatedClassDumpRecord(
159     record: ClassDumpRecord,
160     proguardMapping: ProguardMapping,
161     hprofStringCache: Map<Long, String>,
162     classNames: Map<Long, Long>,
163     maxId: Long
164   ): Pair<List<HprofRecord>, Long> {
165     val recordsToWrite = mutableListOf<HprofRecord>()
166 
167     var id = maxId
168 
169     val newFields = record.fields.map { field ->
170       val className = hprofStringCache[classNames[record.id]]!!
171       val fieldName = hprofStringCache[field.nameStringId]!!
172       val deobfuscatedName =
173         proguardMapping.deobfuscateFieldName(className, fieldName)
174 
175       val newStringRecord = StringRecord(id++, deobfuscatedName)
176       recordsToWrite.add(newStringRecord)
177 
178       FieldRecord(newStringRecord.id, field.type)
179     }
180     val newStaticFields = record.staticFields.map { field ->
181       val className = hprofStringCache[classNames[record.id]]!!
182       val fieldName = hprofStringCache[field.nameStringId]!!
183       val deobfuscatedName =
184         proguardMapping.deobfuscateFieldName(className, fieldName)
185 
186       val newStringRecord = StringRecord(id++, deobfuscatedName)
187       recordsToWrite.add(newStringRecord)
188 
189       StaticFieldRecord(newStringRecord.id, field.type, field.value)
190     }
191 
192     recordsToWrite.add(
193       ClassDumpRecord(
194         record.id,
195         record.stackTraceSerialNumber,
196         record.superclassId,
197         record.classLoaderId,
198         record.signersId,
199         record.protectionDomainId,
200         record.instanceSize,
201         newStaticFields,
202         newFields
203       )
204     )
205 
206     return Pair(recordsToWrite, id)
207   }
208 }
209