neo_decompiler/decompiler/analysis/
methods.rs1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::Serialize;
4
5use crate::instruction::{Instruction, OpCode, Operand};
6use crate::manifest::ContractManifest;
7
8use super::super::helpers::{
9 collect_call_targets, collect_initslot_offsets, find_manifest_entry_method, offset_as_usize,
10 sanitize_identifier,
11};
12use super::call_graph::{
13 calla_ldarg_index, calla_target_from_pusha, initslot_arg_count_at, trace_call_arg_source,
14 CallArgSource,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
23pub struct MethodRef {
24 pub offset: usize,
26 pub name: String,
28}
29
30impl MethodRef {
31 pub(super) fn synthetic(offset: usize) -> Self {
32 Self {
33 offset,
34 name: format!("sub_0x{offset:04X}"),
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
40pub(super) struct MethodSpan {
41 pub(super) start: usize,
42 pub(super) end: usize,
43 pub(super) method: MethodRef,
44}
45
46#[derive(Debug, Clone)]
48pub struct MethodTable {
49 spans: Vec<MethodSpan>,
50 manifest_index_by_start: BTreeMap<usize, usize>,
51}
52
53impl MethodTable {
54 #[must_use]
61 pub fn new(instructions: &[Instruction], manifest: Option<&ContractManifest>) -> Self {
62 let script_start = instructions.first().map(|ins| ins.offset).unwrap_or(0);
63 let script_end = instructions
64 .last()
65 .map(|ins| ins.offset.saturating_add(1))
66 .unwrap_or(script_start);
67
68 let mut manifest_index_by_start = BTreeMap::new();
69 let entry_manifest = manifest.and_then(|manifest| {
70 let entry_method = find_manifest_entry_method(manifest, script_start)?;
71 let index = manifest
72 .abi
73 .methods
74 .iter()
75 .position(|candidate| std::ptr::eq(candidate, entry_method.0))?;
76 Some((entry_method.0, index))
77 });
78
79 let mut starts = BTreeMap::new();
80 starts.insert(script_start, ());
81 for start in collect_initslot_offsets(instructions) {
82 starts.insert(start, ());
83 }
84 for start in collect_call_targets(instructions) {
85 starts.insert(start, ());
86 }
87 let mut callers_by_target: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
88 for (index, instruction) in instructions.iter().enumerate() {
89 if instruction.opcode == OpCode::CallA {
90 if let Some(start) = calla_target_from_pusha(instructions, index) {
91 starts.insert(start, ());
92 callers_by_target.entry(start).or_default().push(index);
93 }
94 continue;
95 }
96 if matches!(instruction.opcode, OpCode::Call | OpCode::Call_L) {
97 if let Some(target) = Self::direct_call_target(instruction) {
98 callers_by_target.entry(target).or_default().push(index);
99 }
100 }
101 }
102
103 loop {
104 let method_starts: Vec<usize> = starts.keys().copied().collect();
105 let mut progress = false;
106
107 for (index, instruction) in instructions.iter().enumerate() {
108 if instruction.opcode != OpCode::CallA {
109 continue;
110 }
111 let Some(arg_index) = calla_ldarg_index(instructions, index) else {
112 continue;
113 };
114 let Some(method_offset) = method_starts
115 .iter()
116 .copied()
117 .filter(|start| *start <= instruction.offset)
118 .max()
119 else {
120 continue;
121 };
122 let mut visited = BTreeSet::new();
123 if let Some(start) = Self::resolve_argument_target_for_method(
124 instructions,
125 &callers_by_target,
126 &method_starts,
127 method_offset,
128 arg_index,
129 &mut visited,
130 ) {
131 let mut changed = starts.insert(start, ()).is_none();
132 let callers = callers_by_target.entry(start).or_default();
133 if !callers.contains(&index) {
134 callers.push(index);
135 changed = true;
136 }
137 if changed {
138 progress = true;
139 }
140 }
141 }
142
143 if !progress {
144 break;
145 }
146 }
147
148 if let Some(manifest) = manifest {
149 for (idx, method) in manifest.abi.methods.iter().enumerate() {
150 if let Some(start) = offset_as_usize(method.offset) {
151 manifest_index_by_start.insert(start, idx);
152 starts.insert(start, ());
153 }
154 }
155 if let Some((_, index)) = entry_manifest {
156 manifest_index_by_start.entry(script_start).or_insert(index);
157 }
158 }
159
160 let ordered_starts: Vec<usize> = starts.into_keys().collect();
161 let mut spans = Vec::new();
162 for (position, start) in ordered_starts.iter().copied().enumerate() {
163 let end = ordered_starts
164 .get(position + 1)
165 .copied()
166 .unwrap_or(script_end);
167 let method = if let Some(manifest) = manifest {
168 if let Some(index) = manifest_index_by_start.get(&start).copied() {
169 let manifest_method = &manifest.abi.methods[index];
170 MethodRef {
171 offset: start,
172 name: sanitize_identifier(&manifest_method.name),
173 }
174 } else if start == script_start {
175 MethodRef {
176 offset: start,
177 name: entry_manifest
178 .as_ref()
179 .map(|(method, _)| sanitize_identifier(&method.name))
180 .unwrap_or_else(|| "script_entry".to_string()),
181 }
182 } else {
183 MethodRef::synthetic(start)
184 }
185 } else if start == script_start {
186 MethodRef {
187 offset: start,
188 name: "script_entry".to_string(),
189 }
190 } else {
191 MethodRef::synthetic(start)
192 };
193
194 spans.push(MethodSpan { start, end, method });
195 }
196
197 spans.sort_by_key(|span| span.start);
198
199 Self {
200 spans,
201 manifest_index_by_start,
202 }
203 }
204
205 fn resolve_argument_target_for_method(
206 instructions: &[Instruction],
207 callers_by_target: &BTreeMap<usize, Vec<usize>>,
208 method_starts: &[usize],
209 method_offset: usize,
210 arg_index: u8,
211 visited: &mut BTreeSet<(usize, u8)>,
212 ) -> Option<usize> {
213 if !visited.insert((method_offset, arg_index)) {
214 return None;
215 }
216
217 let call_sites = callers_by_target.get(&method_offset)?;
218 let callee_arg_count =
219 initslot_arg_count_at(instructions, method_offset).unwrap_or(arg_index as usize + 1);
220
221 for &call_index in call_sites {
222 let call_offset = instructions.get(call_index)?.offset;
223 match trace_call_arg_source(instructions, call_index, arg_index, callee_arg_count) {
224 Some(CallArgSource::Target(target)) => return Some(target),
225 Some(CallArgSource::PassThrough(next_arg)) => {
226 let caller_method_offset = method_starts
227 .iter()
228 .copied()
229 .filter(|start| *start <= call_offset)
230 .max()
231 .unwrap_or(call_offset);
232 if let Some(target) = Self::resolve_argument_target_for_method(
233 instructions,
234 callers_by_target,
235 method_starts,
236 caller_method_offset,
237 next_arg,
238 visited,
239 ) {
240 return Some(target);
241 }
242 }
243 None => {}
244 }
245 }
246
247 None
248 }
249
250 pub(super) fn spans(&self) -> &[MethodSpan] {
252 &self.spans
253 }
254
255 #[must_use]
257 pub fn method_for_offset(&self, offset: usize) -> MethodRef {
258 match self.spans.binary_search_by_key(&offset, |span| span.start) {
259 Ok(index) => self.spans[index].method.clone(),
260 Err(0) => self
261 .spans
262 .first()
263 .map(|span| span.method.clone())
264 .unwrap_or_else(|| MethodRef::synthetic(offset)),
265 Err(index) => {
266 let span = &self.spans[index - 1];
267 span.method.clone()
268 }
269 }
270 }
271
272 #[must_use]
274 pub fn resolve_internal_target(&self, target_offset: usize) -> MethodRef {
275 self.spans
276 .iter()
277 .find(|span| span.start == target_offset)
278 .map(|span| span.method.clone())
279 .unwrap_or_else(|| MethodRef::synthetic(target_offset))
280 }
281
282 fn direct_call_target(instruction: &Instruction) -> Option<usize> {
283 let delta = match instruction.operand {
284 Some(Operand::Jump(value)) => value as isize,
285 Some(Operand::Jump32(value)) => value as isize,
286 _ => return None,
287 };
288 instruction.offset.checked_add_signed(delta)
289 }
290
291 #[must_use]
293 pub fn manifest_index_for_start(&self, offset: usize) -> Option<usize> {
294 self.manifest_index_by_start.get(&offset).copied()
295 }
296}