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
39fn largest_le(sorted: &[usize], target: usize) -> Option<usize> {
42 let pos = sorted.partition_point(|&x| x <= target);
43 if pos > 0 {
44 Some(sorted[pos - 1])
45 } else {
46 None
47 }
48}
49
50#[derive(Debug, Clone)]
51pub(super) struct MethodSpan {
52 pub(super) start: usize,
53 pub(super) end: usize,
54 pub(super) method: MethodRef,
55}
56
57#[derive(Debug, Clone)]
59pub struct MethodTable {
60 spans: Vec<MethodSpan>,
61 manifest_index_by_start: BTreeMap<usize, usize>,
62}
63
64impl MethodTable {
65 #[must_use]
72 pub fn new(instructions: &[Instruction], manifest: Option<&ContractManifest>) -> Self {
73 let script_start = instructions.first().map(|ins| ins.offset).unwrap_or(0);
74 let script_end = instructions
75 .last()
76 .map(|ins| ins.offset.saturating_add(1))
77 .unwrap_or(script_start);
78
79 let known_offsets: BTreeSet<usize> = instructions.iter().map(|ins| ins.offset).collect();
84
85 let mut manifest_index_by_start = BTreeMap::new();
86 let entry_manifest = manifest.and_then(|manifest| {
87 let entry_method = find_manifest_entry_method(manifest, script_start)?;
88 let index = manifest
89 .abi
90 .methods
91 .iter()
92 .position(|candidate| std::ptr::eq(candidate, entry_method.0))?;
93 Some((entry_method.0, index))
94 });
95
96 let mut starts = BTreeMap::new();
97 starts.insert(script_start, ());
98 for start in collect_initslot_offsets(instructions) {
99 starts.insert(start, ());
100 }
101 for start in collect_call_targets(instructions) {
102 starts.insert(start, ());
103 }
104 let mut callers_by_target: BTreeMap<usize, BTreeSet<usize>> = BTreeMap::new();
105 for (index, instruction) in instructions.iter().enumerate() {
106 if instruction.opcode == OpCode::CallA {
107 if let Some(start) = calla_target_from_pusha(instructions, index)
108 .filter(|start| known_offsets.contains(start))
109 {
110 starts.insert(start, ());
111 callers_by_target.entry(start).or_default().insert(index);
112 }
113 continue;
114 }
115 if matches!(instruction.opcode, OpCode::Call | OpCode::Call_L) {
116 if let Some(target) = Self::direct_call_target(instruction) {
117 callers_by_target.entry(target).or_default().insert(index);
118 }
119 }
120 }
121
122 loop {
123 let method_starts: Vec<usize> = starts.keys().copied().collect();
126 let mut progress = false;
127
128 for (index, instruction) in instructions.iter().enumerate() {
129 if instruction.opcode != OpCode::CallA {
130 continue;
131 }
132 let Some(arg_index) = calla_ldarg_index(instructions, index) else {
133 continue;
134 };
135 let Some(method_offset) = largest_le(&method_starts, instruction.offset) else {
136 continue;
137 };
138 let mut visited = BTreeSet::new();
139 if let Some(start) = Self::resolve_argument_target_for_method(
140 instructions,
141 &callers_by_target,
142 &method_starts,
143 method_offset,
144 arg_index,
145 &mut visited,
146 ) {
147 let mut changed = starts.insert(start, ()).is_none();
148 let callers = callers_by_target.entry(start).or_default();
149 if callers.insert(index) {
150 changed = true;
151 }
152 if changed {
153 progress = true;
154 }
155 }
156 }
157
158 if !progress {
159 break;
160 }
161 }
162
163 if let Some(manifest) = manifest {
164 for (idx, method) in manifest.abi.methods.iter().enumerate() {
165 if let Some(start) = offset_as_usize(method.offset) {
166 manifest_index_by_start.insert(start, idx);
167 starts.insert(start, ());
168 }
169 }
170 if let Some((_, index)) = entry_manifest {
171 manifest_index_by_start.entry(script_start).or_insert(index);
172 }
173 }
174
175 let ordered_starts: Vec<usize> = starts.into_keys().collect();
176 let mut spans = Vec::new();
177 for (position, start) in ordered_starts.iter().copied().enumerate() {
178 let end = ordered_starts
179 .get(position + 1)
180 .copied()
181 .unwrap_or(script_end);
182 let method = if let Some(manifest) = manifest {
183 if let Some(index) = manifest_index_by_start.get(&start).copied() {
184 let manifest_method = &manifest.abi.methods[index];
185 MethodRef {
186 offset: start,
187 name: sanitize_identifier(&manifest_method.name),
188 }
189 } else if start == script_start {
190 MethodRef {
191 offset: start,
192 name: entry_manifest
193 .as_ref()
194 .map(|(method, _)| sanitize_identifier(&method.name))
195 .unwrap_or_else(|| "script_entry".to_string()),
196 }
197 } else {
198 MethodRef::synthetic(start)
199 }
200 } else if start == script_start {
201 MethodRef {
202 offset: start,
203 name: "script_entry".to_string(),
204 }
205 } else {
206 MethodRef::synthetic(start)
207 };
208
209 spans.push(MethodSpan { start, end, method });
210 }
211
212 spans.sort_by_key(|span| span.start);
213
214 Self {
215 spans,
216 manifest_index_by_start,
217 }
218 }
219
220 fn resolve_argument_target_for_method(
221 instructions: &[Instruction],
222 callers_by_target: &BTreeMap<usize, BTreeSet<usize>>,
223 method_starts: &[usize],
224 method_offset: usize,
225 arg_index: u8,
226 visited: &mut BTreeSet<(usize, u8)>,
227 ) -> Option<usize> {
228 if !visited.insert((method_offset, arg_index)) {
229 return None;
230 }
231
232 let call_sites = callers_by_target.get(&method_offset)?;
233 let callee_arg_count =
234 initslot_arg_count_at(instructions, method_offset).unwrap_or(arg_index as usize + 1);
235
236 for &call_index in call_sites {
237 let call_offset = instructions.get(call_index)?.offset;
238 match trace_call_arg_source(instructions, call_index, arg_index, callee_arg_count) {
239 Some(CallArgSource::Target(target)) => return Some(target),
240 Some(CallArgSource::PassThrough(next_arg)) => {
241 let caller_method_offset =
242 largest_le(method_starts, call_offset).unwrap_or(call_offset);
243 if let Some(target) = Self::resolve_argument_target_for_method(
244 instructions,
245 callers_by_target,
246 method_starts,
247 caller_method_offset,
248 next_arg,
249 visited,
250 ) {
251 return Some(target);
252 }
253 }
254 None => {}
255 }
256 }
257
258 None
259 }
260
261 pub(super) fn spans(&self) -> &[MethodSpan] {
263 &self.spans
264 }
265
266 #[must_use]
268 pub fn method_for_offset(&self, offset: usize) -> MethodRef {
269 match self.spans.binary_search_by_key(&offset, |span| span.start) {
270 Ok(index) => self.spans[index].method.clone(),
271 Err(0) => self
272 .spans
273 .first()
274 .map(|span| span.method.clone())
275 .unwrap_or_else(|| MethodRef::synthetic(offset)),
276 Err(index) => {
277 let span = &self.spans[index - 1];
278 span.method.clone()
279 }
280 }
281 }
282
283 #[must_use]
286 pub fn resolve_internal_target(&self, target_offset: usize) -> MethodRef {
287 match self
288 .spans
289 .binary_search_by_key(&target_offset, |span| span.start)
290 {
291 Ok(index) => self.spans[index].method.clone(),
292 Err(_) => MethodRef::synthetic(target_offset),
293 }
294 }
295
296 fn direct_call_target(instruction: &Instruction) -> Option<usize> {
297 let delta = match instruction.operand {
298 Some(Operand::Jump(value)) => value as isize,
299 Some(Operand::Jump32(value)) => value as isize,
300 _ => return None,
301 };
302 instruction.offset.checked_add_signed(delta)
303 }
304
305 #[must_use]
307 pub fn manifest_index_for_start(&self, offset: usize) -> Option<usize> {
308 self.manifest_index_by_start.get(&offset).copied()
309 }
310}