forc_debug/debugger/
commands.rs

1use crate::{error::ArgumentError, ContractId};
2use fuel_tx::Receipt;
3use serde::{Deserialize, Serialize};
4
5/// Commands representing all debug operations
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub enum DebugCommand {
8    /// Start a new transaction with optional ABI information
9    StartTransaction {
10        /// Path to the transaction JSON file
11        tx_path: String,
12        /// Optional ABI mappings - either a single ABI path for local dev
13        /// or contract_id:abi_path pairs for contract-specific ABIs
14        abi_mappings: Vec<AbiMapping>,
15    },
16    /// Reset the debugger state
17    Reset,
18    /// Continue execution until next breakpoint or termination
19    Continue,
20    /// Set single stepping mode
21    SetSingleStepping {
22        /// Whether to enable single stepping
23        enable: bool,
24    },
25    /// Set a breakpoint at the specified location
26    SetBreakpoint {
27        /// Contract ID (zeroed for script breakpoints)
28        contract_id: ContractId,
29        /// Instruction offset
30        offset: u64,
31    },
32    /// Get register value(s)
33    GetRegisters {
34        /// Optional specific register indices. If empty, returns all registers
35        indices: Vec<u32>,
36    },
37    /// Get memory contents
38    GetMemory {
39        /// Starting offset in memory
40        offset: u32,
41        /// Number of bytes to read
42        limit: u32,
43    },
44    /// Exit the debugger
45    Quit,
46}
47
48/// ABI mapping for contract debugging
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum AbiMapping {
51    /// Local development ABI (no specific contract ID)
52    Local { abi_path: String },
53    /// Contract-specific ABI mapping
54    Contract {
55        contract_id: ContractId,
56        abi_path: String,
57    },
58}
59
60/// Response types for debug commands
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum DebugResponse {
63    /// Transaction started or continued with execution result
64    RunResult {
65        receipts: Vec<Receipt>,
66        breakpoint: Option<BreakpointHit>,
67    },
68    /// Command completed successfully with no data
69    Success,
70    /// Register values
71    Registers(Vec<RegisterValue>),
72    /// Memory contents
73    Memory(Vec<u8>),
74    /// Error occurred
75    Error(String),
76}
77
78/// Information about a breakpoint hit
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct BreakpointHit {
81    pub contract: ContractId,
82    pub pc: u64,
83}
84
85/// Register value with metadata
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct RegisterValue {
88    pub index: u32,
89    pub value: u64,
90    pub name: String,
91}
92
93impl DebugCommand {
94    /// Parse a command from CLI arguments
95    pub fn from_cli_args(args: &[String]) -> Result<Self, ArgumentError> {
96        if args.is_empty() {
97            return Err(ArgumentError::NotEnough {
98                expected: 1,
99                got: 0,
100            });
101        }
102
103        let cmd = &args[0];
104        let args = &args[1..];
105
106        match cmd.as_str() {
107            "start_tx" | "n" | "tx" | "new_tx" => {
108                Self::parse_start_tx(args).map_err(ArgumentError::Invalid)
109            }
110            "reset" => {
111                if !args.is_empty() {
112                    return Err(ArgumentError::Invalid(
113                        "reset command takes no arguments".to_string(),
114                    ));
115                }
116                Ok(DebugCommand::Reset)
117            }
118            "continue" | "c" => {
119                if !args.is_empty() {
120                    return Err(ArgumentError::Invalid(
121                        "continue command takes no arguments".to_string(),
122                    ));
123                }
124                Ok(DebugCommand::Continue)
125            }
126            "step" | "s" => Self::parse_step(args).map_err(ArgumentError::Invalid),
127            "breakpoint" | "bp" | "b" => {
128                Self::parse_breakpoint(args).map_err(ArgumentError::Invalid)
129            }
130            "register" | "r" | "reg" | "registers" => {
131                Self::parse_registers(args).map_err(ArgumentError::Invalid)
132            }
133            "memory" | "m" | "mem" => Self::parse_memory(args).map_err(ArgumentError::Invalid),
134            "quit" | "q" | "exit" => Ok(DebugCommand::Quit),
135            _ => Err(ArgumentError::UnknownCommand(cmd.to_string())),
136        }
137    }
138
139    /// Parse a start_tx command from CLI arguments
140    ///
141    /// Handles two distinct modes of operation:
142    /// 1. Local Development: `tx transaction.json abi.json`
143    /// 2. Contract-specific: `tx transaction.json --abi <contract_id>:<abi_file.json>`
144    fn parse_start_tx(args: &[String]) -> Result<Self, String> {
145        if args.is_empty() {
146            return Err("Transaction file path required".to_string());
147        }
148
149        let tx_path = args[0].clone();
150        let mut abi_mappings = Vec::new();
151        let mut i = 1;
152
153        while i < args.len() {
154            if args[i] == "--abi" {
155                if i + 1 >= args.len() {
156                    return Err("Missing argument for --abi".to_string());
157                }
158                let abi_arg = &args[i + 1];
159                if let Some((contract_id, abi_path)) = abi_arg.split_once(':') {
160                    let contract_id = contract_id
161                        .parse::<ContractId>()
162                        .map_err(|_| format!("Invalid contract ID: {contract_id}"))?;
163                    abi_mappings.push(AbiMapping::Contract {
164                        contract_id,
165                        abi_path: abi_path.to_string(),
166                    });
167                } else {
168                    return Err(format!("Invalid --abi argument: {abi_arg}"));
169                }
170                i += 2;
171            } else if args[i].ends_with(".json") {
172                // Local development ABI
173                abi_mappings.push(AbiMapping::Local {
174                    abi_path: args[i].clone(),
175                });
176                i += 1;
177            } else {
178                return Err(format!("Unexpected argument: {}", args[i]));
179            }
180        }
181
182        Ok(DebugCommand::StartTransaction {
183            tx_path,
184            abi_mappings,
185        })
186    }
187
188    fn parse_step(args: &[String]) -> Result<Self, String> {
189        let enable = args
190            .first()
191            .is_none_or(|v| !["off", "no", "disable"].contains(&v.as_str()));
192
193        Ok(DebugCommand::SetSingleStepping { enable })
194    }
195
196    fn parse_breakpoint(args: &[String]) -> Result<Self, String> {
197        if args.is_empty() {
198            return Err("Breakpoint offset required".to_string());
199        }
200
201        let (contract_id, offset_str) = if args.len() == 2 {
202            // Contract ID provided
203            let contract_id = args[0]
204                .parse::<ContractId>()
205                .map_err(|_| format!("Invalid contract ID: {}", args[0]))?;
206            (contract_id, &args[1])
207        } else {
208            // No contract ID, use zeroed
209            (ContractId::zeroed(), &args[0])
210        };
211
212        let offset = crate::cli::parse_int(offset_str)
213            .ok_or_else(|| format!("Invalid offset: {offset_str}"))? as u64;
214
215        Ok(DebugCommand::SetBreakpoint {
216            contract_id,
217            offset,
218        })
219    }
220
221    fn parse_registers(args: &[String]) -> Result<Self, String> {
222        let mut indices = Vec::new();
223        for arg in args {
224            if let Some(v) = crate::cli::parse_int(arg) {
225                indices.push(v as u32);
226            } else if let Some(index) = crate::names::register_index(arg) {
227                indices.push(index as u32);
228            } else {
229                return Err(format!("Unknown register: {arg}"));
230            }
231        }
232        Ok(DebugCommand::GetRegisters { indices })
233    }
234
235    fn parse_memory(args: &[String]) -> Result<Self, String> {
236        use fuel_vm::consts::{VM_MAX_RAM, WORD_SIZE};
237
238        let offset = args
239            .first()
240            .map(|a| crate::cli::parse_int(a).ok_or_else(|| format!("Invalid offset: {a}")))
241            .transpose()?
242            .unwrap_or(0) as u32;
243
244        let limit = args
245            .get(1)
246            .map(|a| crate::cli::parse_int(a).ok_or_else(|| format!("Invalid limit: {a}")))
247            .transpose()?
248            .unwrap_or(WORD_SIZE * (VM_MAX_RAM as usize)) as u32;
249
250        Ok(DebugCommand::GetMemory { offset, limit })
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_start_tx_command() {
260        let args = vec!["start_tx".to_string(), "test.json".to_string()];
261        let result = DebugCommand::from_cli_args(&args).unwrap();
262
263        assert!(matches!(
264            result,
265            DebugCommand::StartTransaction { ref tx_path, ref abi_mappings }
266            if tx_path == "test.json" && abi_mappings.is_empty()
267        ));
268
269        // Test alias
270        let args = vec!["n".to_string(), "test.json".to_string()];
271        let result = DebugCommand::from_cli_args(&args).unwrap();
272        assert!(matches!(result, DebugCommand::StartTransaction { .. }));
273    }
274
275    #[test]
276    fn test_reset_command() {
277        let args = vec!["reset".to_string()];
278        let result = DebugCommand::from_cli_args(&args).unwrap();
279        assert!(matches!(result, DebugCommand::Reset));
280    }
281
282    #[test]
283    fn test_continue_command() {
284        let args = vec!["continue".to_string()];
285        let result = DebugCommand::from_cli_args(&args).unwrap();
286        assert!(matches!(result, DebugCommand::Continue));
287
288        // Test alias
289        let args = vec!["c".to_string()];
290        let result = DebugCommand::from_cli_args(&args).unwrap();
291        assert!(matches!(result, DebugCommand::Continue));
292    }
293
294    #[test]
295    fn test_step_command() {
296        let args = vec!["step".to_string()];
297        let result = DebugCommand::from_cli_args(&args).unwrap();
298        assert!(matches!(
299            result,
300            DebugCommand::SetSingleStepping { enable: true }
301        ));
302
303        let args = vec!["step".to_string(), "off".to_string()];
304        let result = DebugCommand::from_cli_args(&args).unwrap();
305        assert!(matches!(
306            result,
307            DebugCommand::SetSingleStepping { enable: false }
308        ));
309
310        // Test alias
311        let args = vec!["s".to_string()];
312        let result = DebugCommand::from_cli_args(&args).unwrap();
313        assert!(matches!(
314            result,
315            DebugCommand::SetSingleStepping { enable: true }
316        ));
317    }
318
319    #[test]
320    fn test_breakpoint_command() {
321        let args = vec!["breakpoint".to_string(), "100".to_string()];
322        let result = DebugCommand::from_cli_args(&args).unwrap();
323        assert!(matches!(
324            result,
325            DebugCommand::SetBreakpoint { contract_id, offset: 100 }
326            if contract_id == ContractId::zeroed()
327        ));
328
329        // Test alias
330        let args = vec!["bp".to_string(), "50".to_string()];
331        let result = DebugCommand::from_cli_args(&args).unwrap();
332        assert!(matches!(
333            result,
334            DebugCommand::SetBreakpoint { offset: 50, .. }
335        ));
336    }
337
338    #[test]
339    fn test_register_command() {
340        let args = vec!["register".to_string()];
341        let result = DebugCommand::from_cli_args(&args).unwrap();
342        assert!(matches!(
343            result,
344            DebugCommand::GetRegisters { ref indices }
345            if indices.is_empty()
346        ));
347
348        let args = vec!["reg".to_string(), "0".to_string()];
349        let result = DebugCommand::from_cli_args(&args).unwrap();
350        assert!(matches!(
351            result,
352            DebugCommand::GetRegisters { ref indices }
353            if indices == &vec![0]
354        ));
355    }
356
357    #[test]
358    fn test_memory_command() {
359        let args = vec!["memory".to_string()];
360        let result = DebugCommand::from_cli_args(&args).unwrap();
361        assert!(matches!(
362            result,
363            DebugCommand::GetMemory {
364                offset: 0,
365                limit: _
366            }
367        ));
368
369        let args = vec!["memory".to_string(), "100".to_string(), "200".to_string()];
370        let result = DebugCommand::from_cli_args(&args).unwrap();
371        assert!(matches!(
372            result,
373            DebugCommand::GetMemory {
374                offset: 100,
375                limit: 200
376            }
377        ));
378
379        // Test alias
380        let args = vec!["m".to_string(), "50".to_string()];
381        let result = DebugCommand::from_cli_args(&args).unwrap();
382        assert!(matches!(result, DebugCommand::GetMemory { offset: 50, .. }));
383    }
384
385    #[test]
386    fn test_quit_command() {
387        let args = vec!["quit".to_string()];
388        let result = DebugCommand::from_cli_args(&args).unwrap();
389        assert!(matches!(result, DebugCommand::Quit));
390
391        // Test aliases
392        let args = vec!["q".to_string()];
393        let result = DebugCommand::from_cli_args(&args).unwrap();
394        assert!(matches!(result, DebugCommand::Quit));
395    }
396
397    #[test]
398    fn test_error_cases() {
399        // Empty args
400        let args = vec![];
401        let result = DebugCommand::from_cli_args(&args);
402        assert!(matches!(
403            result,
404            Err(ArgumentError::NotEnough {
405                expected: 1,
406                got: 0
407            })
408        ));
409
410        // Unknown command
411        let args = vec!["unknown".to_string()];
412        let result = DebugCommand::from_cli_args(&args);
413        assert!(matches!(result, Err(ArgumentError::UnknownCommand(_))));
414
415        // Missing arguments
416        let args = vec!["start_tx".to_string()];
417        let result = DebugCommand::from_cli_args(&args);
418        assert!(result.is_err());
419
420        let args = vec!["breakpoint".to_string()];
421        let result = DebugCommand::from_cli_args(&args);
422        assert!(result.is_err());
423    }
424}