Skip to main content

ghidra_ipc/
address.rs

1//! Strict-total address canonicalization (spec §9). Lenient input, one canonical
2//! output form. This is the single code path; no tool formats addresses itself.
3
4use thiserror::Error;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Address {
8    pub space: String,
9    pub offset: u64,
10}
11
12#[derive(Debug, Error, PartialEq, Eq)]
13pub enum AddressParseError {
14    #[error("empty address")]
15    Empty,
16    #[error("invalid address offset: {0:?}")]
17    InvalidOffset(String),
18}
19
20/// Parses lenient address forms and renders the strict-total canonical form.
21#[derive(Clone)]
22pub struct AddressCanonicalizer {
23    default_space: String,
24    /// Pointer size in bytes; offsets zero-pad to `pointer_size * 2` hex digits.
25    pointer_size: u8,
26}
27
28impl AddressCanonicalizer {
29    pub fn new(default_space: impl Into<String>, pointer_size: u8) -> Self {
30        Self {
31            default_space: default_space.into(),
32            pointer_size,
33        }
34    }
35
36    /// Parse a lenient string: `0x401230`, `0X401230`, bare hex `401230`, or
37    /// `space:offset`; trailing `L`/`l` and surrounding whitespace tolerated.
38    /// Bare and `0x`-prefixed offsets are HEX (Ghidra convention).
39    pub fn parse_str(&self, input: &str) -> Result<Address, AddressParseError> {
40        let mut s = input.trim();
41        if s.is_empty() {
42            return Err(AddressParseError::Empty);
43        }
44        // Strip a single trailing L/l (e.g. a Java-style long literal).
45        if let Some(stripped) = s.strip_suffix(['L', 'l']) {
46            s = stripped.trim_end();
47        }
48        let (space, offset_tok) = match s.split_once(':') {
49            Some((sp, off)) => (sp.trim().to_string(), off.trim()),
50            None => (self.default_space.clone(), s),
51        };
52        let hex = offset_tok
53            .strip_prefix("0x")
54            .or_else(|| offset_tok.strip_prefix("0X"))
55            .unwrap_or(offset_tok);
56        if hex.is_empty() {
57            return Err(AddressParseError::InvalidOffset(input.to_string()));
58        }
59        let offset = u64::from_str_radix(hex, 16)
60            .map_err(|_| AddressParseError::InvalidOffset(input.to_string()))?;
61        Ok(Address { space, offset })
62    }
63
64    /// Build an address from a raw integer offset in the default space. This is
65    /// the JSON-integer input form; the MCP boundary dispatches a JSON number to
66    /// here and a JSON string to `parse_str`.
67    pub fn from_u64(&self, offset: u64) -> Address {
68        Address {
69            space: self.default_space.clone(),
70            offset,
71        }
72    }
73
74    /// Render an address to its strict-total canonical string.
75    ///
76    /// The zero-pad below is a MIN width, not a cap: an offset wider than the
77    /// space's nominal pointer width (Ghidra never emits one) still renders in
78    /// full. This is deliberate — `render` must stay TOTAL over the whole `u64`
79    /// offset space per spec §13 (see the `prop_totality_all_lenient_forms_agree`
80    /// proptest). Hence no width assertion here: a total function must not panic
81    /// on a valid input, even in debug builds (M0-review finding #2, doc-note form).
82    pub fn render(&self, addr: &Address) -> String {
83        let width = (self.pointer_size as usize) * 2;
84        let hex = format!("{:0width$x}", addr.offset, width = width);
85        if addr.space == self.default_space {
86            format!("0x{hex}")
87        } else {
88            format!("{}:0x{hex}", addr.space)
89        }
90    }
91
92    /// Convenience: parse a lenient string and render the canonical form.
93    pub fn canonicalize(&self, input: &str) -> Result<String, AddressParseError> {
94        self.parse_str(input).map(|a| self.render(&a))
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn canon32() -> AddressCanonicalizer {
103        AddressCanonicalizer::new("ram", 4)
104    }
105
106    #[test]
107    fn renders_default_space_bare_padded_to_pointer_size() {
108        let c = canon32();
109        let a = c.parse_str("0x401230").unwrap();
110        assert_eq!(c.render(&a), "0x00401230");
111    }
112
113    #[test]
114    fn renders_non_default_space_with_prefix() {
115        let c = canon32();
116        let a = c.parse_str("overlay:0x10").unwrap();
117        assert_eq!(c.render(&a), "overlay:0x00000010");
118    }
119
120    #[test]
121    fn ram_prefix_equal_to_default_renders_bare() {
122        // Explicit "ram:" (the default) must render identically to the bare form.
123        let c = canon32();
124        assert_eq!(c.canonicalize("ram:0x401230").unwrap(), "0x00401230");
125    }
126
127    #[test]
128    fn totality_all_input_forms_of_same_address_are_byte_identical() {
129        let c = canon32();
130        let forms = [
131            "0x401230",
132            "0X401230",
133            "401230",
134            "  0x401230  ",
135            "0x401230L",
136            "ram:0x401230",
137        ];
138        let canon: Vec<String> = forms.iter().map(|f| c.canonicalize(f).unwrap()).collect();
139        assert!(canon.iter().all(|s| s == "0x00401230"), "got {canon:?}");
140    }
141
142    #[test]
143    fn integer_offset_matches_hex_string_form() {
144        let c = canon32();
145        // from_u64 takes a DECIMAL integer offset; 0x401230 == 4198960.
146        assert_eq!(
147            c.render(&c.from_u64(4_198_960)),
148            c.canonicalize("0x401230").unwrap()
149        );
150    }
151
152    #[test]
153    fn sixty_four_bit_padding() {
154        let c = AddressCanonicalizer::new("ram", 8);
155        assert_eq!(c.canonicalize("0x401230").unwrap(), "0x0000000000401230");
156    }
157
158    #[test]
159    fn rejects_empty_and_nonhex() {
160        let c = canon32();
161        assert_eq!(c.parse_str(""), Err(AddressParseError::Empty));
162        assert_eq!(c.parse_str("   "), Err(AddressParseError::Empty));
163        assert!(matches!(
164            c.parse_str("0x"),
165            Err(AddressParseError::InvalidOffset(_))
166        ));
167        assert!(matches!(
168            c.parse_str("0xzz"),
169            Err(AddressParseError::InvalidOffset(_))
170        ));
171    }
172
173    // The hardcoded cases above are regression examples; THIS is the real §13 totality
174    // property test — over the whole u64 offset space, not six strings.
175    proptest::proptest! {
176        #[test]
177        fn prop_totality_all_lenient_forms_agree(offset in proptest::prelude::any::<u64>()) {
178            let c = canon32();
179            let canonical = c.render(&Address { space: "ram".to_string(), offset });
180            let forms = [
181                format!("0x{offset:x}"),
182                format!("0X{offset:X}"),
183                format!("{offset:x}"),       // bare hex
184                format!("  0x{offset:x}  "), // surrounding whitespace
185                format!("0x{offset:x}L"),    // trailing L
186                format!("ram:0x{offset:x}"), // explicit default space
187            ];
188            for f in forms {
189                proptest::prop_assert_eq!(c.canonicalize(&f).unwrap(), canonical.clone());
190            }
191            // Canonical form is a fixpoint (idempotent).
192            proptest::prop_assert_eq!(c.canonicalize(&canonical).unwrap(), canonical);
193        }
194
195        // Totality must also hold for a NON-default space: the same `space:offset` always
196        // canonicalizes identically AND keeps its space prefix (never collapses to bare `0x`).
197        #[test]
198        fn prop_non_default_space_totality(
199            offset in proptest::prelude::any::<u64>(),
200            space in "[A-Za-z][A-Za-z0-9_]{0,7}",
201        ) {
202            proptest::prop_assume!(space != "ram");
203            let c = canon32();
204            let canonical = c.render(&Address { space: space.clone(), offset });
205            let expected_prefix = format!("{}:0x", space);
206            proptest::prop_assert!(canonical.starts_with(&expected_prefix));
207            let forms = [
208                format!("{space}:0x{offset:x}"),
209                format!("{space}:0X{offset:X}"),
210                format!("{space}:{offset:x}"),
211                format!("  {space}:0x{offset:x}  "),
212                format!("{space}:0x{offset:x}L"),
213            ];
214            for f in forms {
215                proptest::prop_assert_eq!(c.canonicalize(&f).unwrap(), canonical.clone());
216            }
217        }
218    }
219}