Skip to main content

neo_decompiler/decompiler/analysis/
methods.rs

1use 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/// Reference to a (possibly inferred) method within a script.
18///
19/// When a manifest is present, `name` typically matches the ABI method name.
20/// For internal helper routines without ABI metadata, `name` will be a
21/// synthetic `sub_0x....` label.
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
23pub struct MethodRef {
24    /// Method entry offset in bytecode.
25    pub offset: usize,
26    /// Human-readable method name.
27    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/// Helper for mapping bytecode offsets to method ranges.
47#[derive(Debug, Clone)]
48pub struct MethodTable {
49    spans: Vec<MethodSpan>,
50    manifest_index_by_start: BTreeMap<usize, usize>,
51}
52
53impl MethodTable {
54    /// Build a method table using stable method starts plus manifest metadata.
55    ///
56    /// Stable starts include the script entry, manifest ABI offsets, compiler
57    /// `INITSLOT` prologues, and direct internal call targets. This keeps
58    /// analysis aligned with inferred helpers without over-splitting detached
59    /// tails that are only useful for presentation-time rendering.
60    #[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    /// Return all known method spans ordered by start offset.
251    pub(super) fn spans(&self) -> &[MethodSpan] {
252        &self.spans
253    }
254
255    /// Resolve the method that contains the given bytecode offset.
256    #[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    /// Resolve an internal call target to a method reference.
273    #[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    /// Return the manifest ABI method index for a method starting at `offset`, if any.
292    #[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}