1use 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#[derive(Clone)]
22pub struct AddressCanonicalizer {
23 default_space: String,
24 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 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 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 pub fn from_u64(&self, offset: u64) -> Address {
68 Address {
69 space: self.default_space.clone(),
70 offset,
71 }
72 }
73
74 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 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 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 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 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}"), format!(" 0x{offset:x} "), format!("0x{offset:x}L"), format!("ram:0x{offset:x}"), ];
188 for f in forms {
189 proptest::prop_assert_eq!(c.canonicalize(&f).unwrap(), canonical.clone());
190 }
191 proptest::prop_assert_eq!(c.canonicalize(&canonical).unwrap(), canonical);
193 }
194
195 #[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}