Skip to main content

r2smt_pcode/
parse.rs

1//! Pure parser for r2ghidra `pdgsd` P-code text.
2//!
3//! Independent of any live r2 process so it can be exercised with
4//! fixed fixtures and without an r2ghidra install. The grammar
5//! (observed from `pdgsd N`, stable SLEIGH textual P-code):
6//!
7//! ```text
8//! 0x<hex>: <asm text>                 # instruction header (not indented)
9//!     <out> = <OPCODE> <in0>[, <in1>] # data-flow op (indented)
10//!     <out> = LOAD ram[<addr>]        # memory load
11//!     STORE ram[<addr>] = <value>     # memory store
12//!     BRANCH qword_ptr(0x<hex>)       # control flow
13//!     CBRANCH qword_ptr(0x<hex>), <cond>
14//! ```
15//!
16//! Varnode tokens: `(unique,0x<hex>,<size>)`, a bare register name
17//! (`sp`, `w0`, `x8`, `ZR`, `tmpCY`, …), a hex constant (`0x10`),
18//! `ram[<vn>]`, or `qword_ptr(0x<hex>)`.
19
20/// Structural parse failure. Open-domain (free-form reason) per the
21/// project error policy — parser reasons are inherently open.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ParseError(pub String);
24
25/// A P-code varnode operand.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Varnode {
28    /// A named register / flag varnode (`sp`, `w8`, `ZR`, `tmpCY`, …).
29    Register(String),
30    /// A SLEIGH `unique` temporary: `(unique,offset,size)`.
31    Unique {
32        /// Byte offset within the unique space.
33        offset: u64,
34        /// Width in bytes.
35        size: u8,
36    },
37    /// An integer constant. Width is often implicit in the textual
38    /// form, so it is inferred at lift time from the operation.
39    Const {
40        /// The literal value.
41        value: u64,
42        /// Width in bytes when the form carried one.
43        size: Option<u8>,
44    },
45    /// A memory reference `ram[<addr>]` — the inner varnode is the
46    /// address expression.
47    Ram(Box<Varnode>),
48    /// A code address branch target (`qword_ptr(0x..)`).
49    CodeAddr(u64),
50}
51
52/// One P-code micro-operation.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct PcodeOp {
55    /// Output varnode, or `None` for `STORE` / control-flow ops.
56    pub out: Option<Varnode>,
57    /// SLEIGH opcode mnemonic, verbatim (`INT_ADD`, `COPY`, …).
58    pub opcode: String,
59    /// Input varnodes in source order.
60    pub inputs: Vec<Varnode>,
61}
62
63/// All P-code ops emitted for one machine instruction.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct PcodeInsn {
66    /// Machine address of the instruction.
67    pub address: u64,
68    /// P-code ops, in emission order.
69    pub ops: Vec<PcodeOp>,
70}
71
72/// Parse a `pdgsd` dump into structured instructions.
73///
74/// # Errors
75///
76/// Returns [`ParseError`] when an indented op line cannot be
77/// structurally decoded — dropping a data-flow op silently would be
78/// unsound, so the caller must fall back to another IR source.
79pub fn parse_pcode(text: &str) -> Result<Vec<PcodeInsn>, ParseError> {
80    let mut out: Vec<PcodeInsn> = Vec::new();
81    for raw in text.lines() {
82        let line = raw.trim_end();
83        if line.trim().is_empty() {
84            continue;
85        }
86        if line.starts_with(char::is_whitespace) {
87            let op = parse_op_line(line.trim())?;
88            let Some(current) = out.last_mut() else {
89                return Err(ParseError(format!(
90                    "op line before any instruction header: {line:?}"
91                )));
92            };
93            current.ops.push(op);
94        } else {
95            // Instruction header: `0x<hex>: <asm>`.
96            let Some(addr) = parse_header_address(line) else {
97                // Non-header, non-indented line (e.g. a stray r2 log
98                // that slipped through). Skip — only op lines are
99                // load-bearing.
100                continue;
101            };
102            out.push(PcodeInsn {
103                address: addr,
104                ops: Vec::new(),
105            });
106        }
107    }
108    Ok(out)
109}
110
111fn parse_header_address(line: &str) -> Option<u64> {
112    let (addr_tok, _rest) = line.split_once(':')?;
113    let hex = addr_tok.trim().strip_prefix("0x")?;
114    u64::from_str_radix(hex, 16).ok()
115}
116
117fn parse_op_line(line: &str) -> Result<PcodeOp, ParseError> {
118    // `STORE ram[<addr>] = <value>` — no varnode on the left of `=`.
119    if let Some(rest) = line.strip_prefix("STORE ") {
120        let (addr_part, value_part) = rest
121            .split_once(" = ")
122            .ok_or_else(|| ParseError(format!("malformed STORE: {line:?}")))?;
123        return Ok(PcodeOp {
124            out: None,
125            opcode: "STORE".to_string(),
126            inputs: vec![
127                parse_varnode(addr_part.trim())?,
128                parse_varnode(value_part.trim())?,
129            ],
130        });
131    }
132
133    // `OUT = OPCODE [IN, IN, ...]`  or  `OUT = LOAD ram[ADDR]`.
134    if let Some((lhs, rhs)) = split_assignment(line) {
135        let out = parse_varnode(lhs.trim())?;
136        let mut toks = rhs.trim().splitn(2, char::is_whitespace);
137        let opcode = toks
138            .next()
139            .ok_or_else(|| ParseError(format!("missing opcode: {line:?}")))?
140            .to_string();
141        let inputs = match toks.next() {
142            Some(args) => parse_input_list(args.trim())?,
143            None => Vec::new(),
144        };
145        return Ok(PcodeOp {
146            out: Some(out),
147            opcode,
148            inputs,
149        });
150    }
151
152    // Control-flow ops with no output: `BRANCH ...`, `CBRANCH ...`,
153    // `RETURN ...`, `CALL ...`, `BRANCHIND ...`, `CALLIND ...`.
154    let mut toks = line.splitn(2, char::is_whitespace);
155    let opcode = toks
156        .next()
157        .ok_or_else(|| ParseError(format!("empty op line: {line:?}")))?
158        .to_string();
159    if !opcode.chars().all(|c| c.is_ascii_uppercase() || c == '_') {
160        return Err(ParseError(format!("unrecognised op line: {line:?}")));
161    }
162    let inputs = match toks.next() {
163        Some(args) => parse_input_list(args.trim())?,
164        None => Vec::new(),
165    };
166    Ok(PcodeOp {
167        out: None,
168        opcode,
169        inputs,
170    })
171}
172
173/// Split `A = B` on the *first* ` = ` that is not inside parentheses
174/// or brackets (so `(unique,0x6000,8) = INT_ADD sp, 0x8` splits at the
175/// right place and `ram[..]` indices are not mis-split).
176fn split_assignment(line: &str) -> Option<(&str, &str)> {
177    let bytes = line.as_bytes();
178    let mut depth: i32 = 0;
179    let mut i = 0;
180    while i + 2 < bytes.len() {
181        match bytes[i] {
182            b'(' | b'[' => depth += 1,
183            b')' | b']' => depth -= 1,
184            b' ' if depth == 0 && bytes[i + 1] == b'=' && bytes[i + 2] == b' ' => {
185                return Some((&line[..i], &line[i + 3..]));
186            }
187            _ => {}
188        }
189        i += 1;
190    }
191    None
192}
193
194/// Split a top-level comma list, respecting `(...)` / `[...]` nesting.
195fn parse_input_list(s: &str) -> Result<Vec<Varnode>, ParseError> {
196    let mut parts: Vec<&str> = Vec::new();
197    let bytes = s.as_bytes();
198    let mut depth: i32 = 0;
199    let mut start = 0;
200    for (i, &b) in bytes.iter().enumerate() {
201        match b {
202            b'(' | b'[' => depth += 1,
203            b')' | b']' => depth -= 1,
204            b',' if depth == 0 => {
205                parts.push(&s[start..i]);
206                start = i + 1;
207            }
208            _ => {}
209        }
210    }
211    parts.push(&s[start..]);
212    parts
213        .into_iter()
214        .map(str::trim)
215        .filter(|p| !p.is_empty())
216        .map(parse_varnode)
217        .collect()
218}
219
220fn parse_varnode(tok: &str) -> Result<Varnode, ParseError> {
221    let tok = tok.trim();
222    if let Some(inner) = tok.strip_prefix("ram[").and_then(|s| s.strip_suffix(']')) {
223        return Ok(Varnode::Ram(Box::new(parse_varnode(inner)?)));
224    }
225    if let Some(inner) = tok
226        .strip_prefix("qword_ptr(")
227        .and_then(|s| s.strip_suffix(')'))
228    {
229        let hex = inner.trim().strip_prefix("0x").unwrap_or(inner.trim());
230        let v = u64::from_str_radix(hex, 16)
231            .map_err(|_| ParseError(format!("bad code addr: {tok:?}")))?;
232        return Ok(Varnode::CodeAddr(v));
233    }
234    if let Some(inner) = tok.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
235        // (space,0xoffset,size)
236        let mut f = inner.split(',');
237        let space = f.next().unwrap_or("").trim();
238        let off_tok = f.next().unwrap_or("").trim();
239        let size_tok = f.next().unwrap_or("").trim();
240        let offset =
241            parse_u64(off_tok).ok_or_else(|| ParseError(format!("bad varnode offset: {tok:?}")))?;
242        let size: u8 = size_tok
243            .parse()
244            .map_err(|_| ParseError(format!("bad varnode size: {tok:?}")))?;
245        return match space {
246            "unique" => Ok(Varnode::Unique { offset, size }),
247            "const" => Ok(Varnode::Const {
248                value: offset,
249                size: Some(size),
250            }),
251            _ => Ok(Varnode::Register(format!("{space}_{offset:#x}"))),
252        };
253    }
254    if let Some(v) = parse_u64(tok) {
255        return Ok(Varnode::Const {
256            value: v,
257            size: None,
258        });
259    }
260    if !tok.is_empty()
261        && tok
262            .chars()
263            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
264    {
265        return Ok(Varnode::Register(tok.to_string()));
266    }
267    Err(ParseError(format!("unrecognised varnode: {tok:?}")))
268}
269
270fn parse_u64(tok: &str) -> Option<u64> {
271    if let Some(hex) = tok.strip_prefix("0x") {
272        u64::from_str_radix(hex, 16).ok()
273    } else {
274        tok.parse::<u64>().ok()
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
281
282    use super::*;
283
284    const FIXTURE: &str = "\
2850x00000328: sub sp, sp, #0x10
286    sp = INT_SUB sp, 0x10
2870x00000338: mul w8, w8, w9
288    (unique,0x2ae80,4) = INT_MULT w8, w9
289    x8 = INT_ZEXT (unique,0x2ae80,4)
2900x00000344: and w8, w8, #0x1
291    (unique,0x12180,4) = INT_AND w8, 0x1
292    x8 = INT_ZEXT (unique,0x12180,4)
2930x0000034c: b.ne 0x360
294    (unique,0xa00,1) = BOOL_NEGATE ZR
295    CBRANCH qword_ptr(0x00000360), (unique,0xa00,1)
2960x00000360: ldr w8, [sp, #0x8]
297    (unique,0x6000,8) = INT_ADD sp, 0x8
298    (unique,0x24700,4) = LOAD ram[(unique,0x6000,8)]
299    x8 = INT_ZEXT (unique,0x24700,4)
3000x00000370: str w8, [sp, #0xc]
301    STORE ram[(unique,0x6000,8)] = w8";
302
303    #[test]
304    fn test_parse_groups_ops_under_instruction_headers() {
305        let p = parse_pcode(FIXTURE).unwrap();
306        assert_eq!(p.len(), 6);
307        assert_eq!(p[0].address, 0x328);
308        assert_eq!(p[0].ops.len(), 1);
309        assert_eq!(p[2].address, 0x344);
310        assert_eq!(p[2].ops.len(), 2);
311    }
312
313    #[test]
314    fn test_parse_assignment_op_varnodes() {
315        let p = parse_pcode(FIXTURE).unwrap();
316        let op = &p[1].ops[0]; // (unique,0x2ae80,4) = INT_MULT w8, w9
317        assert_eq!(
318            op.out,
319            Some(Varnode::Unique {
320                offset: 0x2ae80,
321                size: 4
322            })
323        );
324        assert_eq!(op.opcode, "INT_MULT");
325        assert_eq!(
326            op.inputs,
327            vec![
328                Varnode::Register("w8".into()),
329                Varnode::Register("w9".into())
330            ]
331        );
332    }
333
334    #[test]
335    fn test_parse_const_input() {
336        let p = parse_pcode(FIXTURE).unwrap();
337        let op = &p[2].ops[0]; // (unique,0x12180,4) = INT_AND w8, 0x1
338        assert_eq!(
339            op.inputs[1],
340            Varnode::Const {
341                value: 1,
342                size: None
343            }
344        );
345    }
346
347    #[test]
348    fn test_parse_cbranch_and_store_have_no_output() {
349        let p = parse_pcode(FIXTURE).unwrap();
350        let cbr = &p[3].ops[1];
351        assert_eq!(cbr.opcode, "CBRANCH");
352        assert_eq!(cbr.out, None);
353        assert_eq!(cbr.inputs[0], Varnode::CodeAddr(0x360));
354
355        let store = p[5].ops.last().unwrap();
356        assert_eq!(store.opcode, "STORE");
357        assert_eq!(store.out, None);
358        assert!(matches!(store.inputs[0], Varnode::Ram(_)));
359        assert_eq!(store.inputs[1], Varnode::Register("w8".into()));
360    }
361
362    #[test]
363    fn test_parse_load_ram_address() {
364        let p = parse_pcode(FIXTURE).unwrap();
365        let ld = &p[4].ops[1]; // (unique,0x24700,4) = LOAD ram[(unique,0x6000,8)]
366        assert_eq!(ld.opcode, "LOAD");
367        assert_eq!(
368            ld.inputs[0],
369            Varnode::Ram(Box::new(Varnode::Unique {
370                offset: 0x6000,
371                size: 8
372            }))
373        );
374    }
375
376    #[test]
377    fn test_parse_rejects_orphan_op_line() {
378        let err = parse_pcode("    sp = INT_SUB sp, 0x10").unwrap_err();
379        assert!(err.0.contains("before any instruction header"));
380    }
381}