paavohtl Claude Opus 4.6 commited on
Commit
33fa915
·
1 Parent(s): 5018b6b

feat: add openwa-debug-cli binary for debug server access

Browse files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

crates/openwa-debug-cli/Cargo.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "openwa-debug-cli"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "CLI tool for querying the OpenWA debug server"
6
+
7
+ [[bin]]
8
+ name = "openwa-debug"
9
+ path = "src/main.rs"
10
+
11
+ [dependencies]
12
+ openwa-debug-proto = { path = "../openwa-debug-proto" }
crates/openwa-debug-cli/src/main.rs ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! CLI tool for the OpenWA debug server.
2
+ //!
3
+ //! Stateless: connects, sends one request, prints the response, disconnects.
4
+ //!
5
+ //! Usage:
6
+ //! openwa-debug ping
7
+ //! openwa-debug help
8
+ //! openwa-debug read <ghidra_addr> [len] [--format hex|raw] [--port N]
9
+
10
+ use std::io::{self, BufReader, BufWriter, Write};
11
+ use std::net::TcpStream;
12
+ use std::process;
13
+ use std::time::Duration;
14
+
15
+ use openwa_debug_proto::*;
16
+
17
+ fn main() {
18
+ let args: Vec<String> = std::env::args().skip(1).collect();
19
+ if args.is_empty() {
20
+ print_usage();
21
+ process::exit(1);
22
+ }
23
+
24
+ let mut port = DEFAULT_PORT;
25
+ let mut format = Format::Hex;
26
+ let mut positional = Vec::new();
27
+
28
+ // Parse args
29
+ let mut i = 0;
30
+ while i < args.len() {
31
+ match args[i].as_str() {
32
+ "--port" => {
33
+ i += 1;
34
+ port = args.get(i)
35
+ .and_then(|s| s.parse().ok())
36
+ .unwrap_or_else(|| { eprintln!("Error: --port requires a number"); process::exit(1); });
37
+ }
38
+ "--format" => {
39
+ i += 1;
40
+ format = match args.get(i).map(|s| s.as_str()) {
41
+ Some("hex") => Format::Hex,
42
+ Some("raw") => Format::Raw,
43
+ _ => { eprintln!("Error: --format must be 'hex' or 'raw'"); process::exit(1); }
44
+ };
45
+ }
46
+ other => positional.push(other.to_string()),
47
+ }
48
+ i += 1;
49
+ }
50
+
51
+ let request = match positional.first().map(|s| s.as_str()) {
52
+ Some("ping") => Request::Ping,
53
+ Some("help") => Request::Help,
54
+ Some("read") => {
55
+ let addr_str = positional.get(1).unwrap_or_else(|| {
56
+ eprintln!("Error: read requires an address");
57
+ process::exit(1);
58
+ });
59
+ let addr = parse_addr(addr_str);
60
+ let len = positional.get(2)
61
+ .map(|s| parse_u32(s))
62
+ .unwrap_or(256);
63
+ Request::Read { addr, len }
64
+ }
65
+ Some(cmd) => {
66
+ eprintln!("Unknown command: {cmd}");
67
+ print_usage();
68
+ process::exit(1);
69
+ }
70
+ None => {
71
+ print_usage();
72
+ process::exit(1);
73
+ }
74
+ };
75
+
76
+ match send_request(port, &request) {
77
+ Ok(response) => print_response(&response, format),
78
+ Err(e) => {
79
+ eprintln!("Error: {e}");
80
+ process::exit(1);
81
+ }
82
+ }
83
+ }
84
+
85
+ fn send_request(port: u16, request: &Request) -> io::Result<Response> {
86
+ let stream = TcpStream::connect_timeout(
87
+ &format!("127.0.0.1:{port}").parse().unwrap(),
88
+ Duration::from_secs(5),
89
+ )?;
90
+ stream.set_read_timeout(Some(Duration::from_secs(10)))?;
91
+ stream.set_nodelay(true)?;
92
+
93
+ let mut reader = BufReader::new(stream.try_clone()?);
94
+ let mut writer = BufWriter::new(stream);
95
+
96
+ write_frame(&mut writer, request)?;
97
+ read_frame(&mut reader)
98
+ }
99
+
100
+ fn print_response(response: &Response, format: Format) {
101
+ match response {
102
+ Response::Pong => println!("pong"),
103
+ Response::Help { commands } => {
104
+ println!("Available commands:");
105
+ for cmd in commands {
106
+ println!(" {} — {}", cmd.usage, cmd.description);
107
+ }
108
+ }
109
+ Response::ReadResult { ghidra_addr, runtime_addr, data, pointers } => {
110
+ match format {
111
+ Format::Hex => print_hex_read(*ghidra_addr, *runtime_addr, data, pointers),
112
+ Format::Raw => {
113
+ let stdout = io::stdout();
114
+ let mut out = stdout.lock();
115
+ let _ = out.write_all(data);
116
+ }
117
+ }
118
+ }
119
+ Response::Error { message } => {
120
+ eprintln!("Server error: {message}");
121
+ process::exit(1);
122
+ }
123
+ }
124
+ }
125
+
126
+ fn print_hex_read(ghidra_addr: u32, runtime_addr: u32, data: &[u8], pointers: &[PointerInfo]) {
127
+ println!(
128
+ "Reading 0x{:X} bytes at ghidra:0x{:08X} (runtime:0x{:08X})\n",
129
+ data.len(), ghidra_addr, runtime_addr
130
+ );
131
+
132
+ // Hex dump with ASCII sidebar
133
+ for (chunk_idx, chunk) in data.chunks(16).enumerate() {
134
+ let offset = chunk_idx * 16;
135
+ // Offset column
136
+ print!("{:08X} ", offset);
137
+ // Hex bytes
138
+ for (i, byte) in chunk.iter().enumerate() {
139
+ print!("{:02X} ", byte);
140
+ if i == 7 { print!(" "); }
141
+ }
142
+ // Padding for short last line
143
+ if chunk.len() < 16 {
144
+ for i in chunk.len()..16 {
145
+ print!(" ");
146
+ if i == 7 { print!(" "); }
147
+ }
148
+ }
149
+ // ASCII sidebar
150
+ print!(" |");
151
+ for byte in chunk {
152
+ if *byte >= 0x20 && *byte <= 0x7E {
153
+ print!("{}", *byte as char);
154
+ } else {
155
+ print!(".");
156
+ }
157
+ }
158
+ println!("|");
159
+ }
160
+
161
+ // Pointer annotations
162
+ if !pointers.is_empty() {
163
+ println!("\nPointers detected:");
164
+ for p in pointers {
165
+ let kind_str = match p.kind {
166
+ PointerKind::Vtable => "VTABLE",
167
+ PointerKind::Code => "CODE",
168
+ PointerKind::Data => "DATA",
169
+ PointerKind::Object => "OBJECT",
170
+ PointerKind::Heap => "HEAP",
171
+ };
172
+ let detail_str = p.detail.as_deref().map(|d| format!(" {d}")).unwrap_or_default();
173
+ println!(
174
+ " +0x{:03X} 0x{:08X} {:<7} ghidra:0x{:08X}{}",
175
+ p.offset, p.raw_value, kind_str, p.ghidra_value, detail_str
176
+ );
177
+ }
178
+ }
179
+ }
180
+
181
+ #[derive(Clone, Copy)]
182
+ enum Format {
183
+ Hex,
184
+ Raw,
185
+ }
186
+
187
+ fn parse_addr(s: &str) -> u32 {
188
+ let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
189
+ u32::from_str_radix(s, 16).unwrap_or_else(|_| {
190
+ eprintln!("Error: invalid hex address '{s}'");
191
+ process::exit(1);
192
+ })
193
+ }
194
+
195
+ fn parse_u32(s: &str) -> u32 {
196
+ // Support both hex (0x...) and decimal
197
+ if s.starts_with("0x") || s.starts_with("0X") {
198
+ let hex = &s[2..];
199
+ u32::from_str_radix(hex, 16).unwrap_or_else(|_| {
200
+ eprintln!("Error: invalid hex number '{s}'");
201
+ process::exit(1);
202
+ })
203
+ } else {
204
+ s.parse().unwrap_or_else(|_| {
205
+ eprintln!("Error: invalid number '{s}'");
206
+ process::exit(1);
207
+ })
208
+ }
209
+ }
210
+
211
+ fn print_usage() {
212
+ eprintln!("Usage: openwa-debug <command> [args...] [--port N] [--format hex|raw]");
213
+ eprintln!();
214
+ eprintln!("Commands:");
215
+ eprintln!(" ping Check if server is running");
216
+ eprintln!(" help List server commands");
217
+ eprintln!(" read <addr> [len] Read memory (addr is Ghidra VA, len default 256)");
218
+ eprintln!();
219
+ eprintln!("Options:");
220
+ eprintln!(" --port <N> Server port (default: 19840)");
221
+ eprintln!(" --format hex|raw Output format (default: hex)");
222
+ }