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/// Returns the largest element in `sorted` that is `<= target`.
40/// `sorted` MUST be sorted ascending. O(log n).
41fn 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/// Helper for mapping bytecode offsets to method ranges.
58#[derive(Debug, Clone)]
59pub struct MethodTable {
60    spans: Vec<MethodSpan>,
61    manifest_index_by_start: BTreeMap<usize, usize>,
62}
63
64impl MethodTable {
65    /// Build a method table using stable method starts plus manifest metadata.
66    ///
67    /// Stable starts include the script entry, manifest ABI offsets, compiler
68    /// `INITSLOT` prologues, and direct internal call targets. This keeps
69    /// analysis aligned with inferred helpers without over-splitting detached
70    /// tails that are only useful for presentation-time rendering.
71    #[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        // Valid instruction start offsets. A CALLA pointer target (from PUSHA)
80        // must land on one to be a real method start; an out-of-range target
81        // would otherwise fabricate a phantom span/method. `collect_call_targets`
82        // already applies the same filter to CALL/CALL_L targets.
83        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            // Sorted Vec of method-start offsets; rebuilt per fixpoint iter so
124            // newly-inserted starts from the previous round are visible.
125            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    /// Return all known method spans ordered by start offset.
262    pub(super) fn spans(&self) -> &[MethodSpan] {
263        &self.spans
264    }
265
266    /// Resolve the method that contains the given bytecode offset.
267    #[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    /// Resolve an internal call target to a method reference.
284    /// `spans` is kept sorted by start offset, so we use binary search.
285    #[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    /// Return the manifest ABI method index for a method starting at `offset`, if any.
306    #[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}