1use alloc::string::String;
5
6#[derive(Debug, PartialEq, Eq)]
9pub enum Command<'a> {
10 Help(Option<&'a str>),
11 Quit,
12 WhoAmI,
13
14 PeerAdd {
15 pubkey: &'a str,
16 alias: Option<&'a str>,
17 },
18 PeerAlias {
19 peer: &'a str,
20 alias: &'a str,
21 },
22 PeerRm {
23 peer: &'a str,
24 },
25 Peers,
26
27 Query {
28 peer: &'a str,
29 },
30 Msg {
31 peer: &'a str,
32 text: &'a str,
33 },
34 Text {
35 body: &'a str,
36 }, Me {
38 action: &'a str,
39 },
40
41 Ping {
42 peer: &'a str,
43 bytes: Option<u16>,
44 },
45
46 PfsStart {
47 peer: &'a str,
48 minutes: Option<u16>,
49 },
50 PfsEnd {
51 peer: &'a str,
52 },
53 PfsStatus {
54 peer: Option<&'a str>,
55 },
56
57 Beacon,
58
59 ChannelJoin {
60 name: &'a str,
61 key: &'a str,
62 },
63 ChannelLeave {
64 name: &'a str,
65 },
66 ChannelSend {
67 name: &'a str,
68 text: &'a str,
69 },
70 Channels,
71
72 Stats,
73 Counters,
74 Log {
75 level: &'a str,
76 },
77 SetShow, Set {
79 var: &'a str,
80 val: &'a str,
81 },
82 Raw {
83 peer: &'a str,
84 hex: &'a str,
85 },
86
87 PowerOff,
88 Reboot,
89}
90
91#[derive(Debug, PartialEq, Eq)]
92pub enum ParseError {
93 Empty,
94 UnknownCommand(String),
95 MissingArg(&'static str),
96 BadNumber,
97}
98
99pub fn parse(line: &str) -> Result<Command<'_>, ParseError> {
101 let line = line.trim();
102 if line.is_empty() {
103 return Err(ParseError::Empty);
104 }
105 if !line.starts_with('/') {
106 return Ok(Command::Text { body: line });
107 }
108 let (head, rest) = split_one(&line[1..]);
109 match head {
110 "help" | "?" => Ok(Command::Help(if rest.is_empty() {
111 None
112 } else {
113 Some(rest)
114 })),
115 "quit" | "exit" => Ok(Command::Quit),
116 "whoami" => Ok(Command::WhoAmI),
117
118 "peer" => parse_peer(rest),
119 "peers" => Ok(Command::Peers),
120 "query" => {
121 let (peer, _) = split_one(rest);
122 require(peer, "peer-ref")?;
123 Ok(Command::Query { peer })
124 }
125 "msg" => {
126 let (peer, tail) = split_one(rest);
127 require(peer, "peer-ref")?;
128 require(tail, "text")?;
129 Ok(Command::Msg { peer, text: tail })
130 }
131 "me" => {
132 require(rest, "action")?;
133 Ok(Command::Me { action: rest })
134 }
135 "ping" => {
136 let (peer, tail) = split_one(rest);
137 require(peer, "peer-ref")?;
138 let bytes = if tail.is_empty() {
139 None
140 } else {
141 Some(tail.parse::<u16>().map_err(|_| ParseError::BadNumber)?)
142 };
143 Ok(Command::Ping { peer, bytes })
144 }
145 "pfs" => parse_pfs(rest),
146 "beacon" => Ok(Command::Beacon),
147 "channel" => parse_channel(rest),
148 "channels" => Ok(Command::Channels),
149 "stats" => Ok(Command::Stats),
150 "counters" => Ok(Command::Counters),
151 "log" => {
152 require(rest, "level")?;
153 Ok(Command::Log { level: rest })
154 }
155 "set" => {
156 if rest.is_empty() {
157 return Ok(Command::SetShow);
158 }
159 let (var, val) = split_one(rest);
160 require(var, "var")?;
161 require(val, "value")?;
162 Ok(Command::Set { var, val })
163 }
164 "raw" => {
165 let (peer, hex) = split_one(rest);
166 require(peer, "peer-ref")?;
167 require(hex, "hex")?;
168 Ok(Command::Raw { peer, hex })
169 }
170 "poweroff" | "off" => Ok(Command::PowerOff),
171 "reboot" => Ok(Command::Reboot),
172
173 other => {
174 let mut s = String::new();
175 s.push_str(other);
176 Err(ParseError::UnknownCommand(s))
177 }
178 }
179}
180
181fn parse_peer(rest: &str) -> Result<Command<'_>, ParseError> {
182 let (sub, tail) = split_one(rest);
183 match sub {
184 "add" => {
185 let (pubkey, alias_part) = split_one(tail);
186 require(pubkey, "pubkey")?;
187 let alias = if alias_part.is_empty() {
188 None
189 } else {
190 Some(alias_part)
191 };
192 Ok(Command::PeerAdd { pubkey, alias })
193 }
194 "alias" => {
195 let (peer, alias) = split_one(tail);
196 require(peer, "peer-ref")?;
197 require(alias, "alias")?;
198 Ok(Command::PeerAlias { peer, alias })
199 }
200 "rm" | "remove" => {
201 let (peer, _) = split_one(tail);
202 require(peer, "peer-ref")?;
203 Ok(Command::PeerRm { peer })
204 }
205 _ => Err(ParseError::UnknownCommand(format_str("peer ", sub))),
206 }
207}
208
209fn parse_pfs(rest: &str) -> Result<Command<'_>, ParseError> {
210 let (sub, tail) = split_one(rest);
211 match sub {
212 "start" => {
213 let (peer, mins_part) = split_one(tail);
214 require(peer, "peer-ref")?;
215 let minutes = if mins_part.is_empty() {
216 None
217 } else {
218 Some(
219 mins_part
220 .parse::<u16>()
221 .map_err(|_| ParseError::BadNumber)?,
222 )
223 };
224 Ok(Command::PfsStart { peer, minutes })
225 }
226 "end" => {
227 let (peer, _) = split_one(tail);
228 require(peer, "peer-ref")?;
229 Ok(Command::PfsEnd { peer })
230 }
231 "status" => {
232 let (peer, _) = split_one(tail);
233 Ok(Command::PfsStatus {
234 peer: if peer.is_empty() { None } else { Some(peer) },
235 })
236 }
237 _ => Err(ParseError::UnknownCommand(format_str("pfs ", sub))),
238 }
239}
240
241fn parse_channel(rest: &str) -> Result<Command<'_>, ParseError> {
242 let (sub, tail) = split_one(rest);
243 match sub {
244 "join" => {
245 let (name, key) = split_one(tail);
246 require(name, "channel name")?;
247 require(key, "key")?;
248 Ok(Command::ChannelJoin { name, key })
249 }
250 "leave" => {
251 let (name, _) = split_one(tail);
252 require(name, "channel name")?;
253 Ok(Command::ChannelLeave { name })
254 }
255 "send" => {
256 let (name, text) = split_one(tail);
257 require(name, "channel name")?;
258 require(text, "text")?;
259 Ok(Command::ChannelSend { name, text })
260 }
261 _ => Err(ParseError::UnknownCommand(format_str("channel ", sub))),
262 }
263}
264
265fn require(s: &str, name: &'static str) -> Result<(), ParseError> {
266 if s.is_empty() {
267 Err(ParseError::MissingArg(name))
268 } else {
269 Ok(())
270 }
271}
272
273fn split_one(s: &str) -> (&str, &str) {
274 let s = s.trim_start();
275 match s.find(|c: char| c.is_whitespace()) {
276 Some(idx) => (&s[..idx], s[idx..].trim_start()),
277 None => (s, ""),
278 }
279}
280
281fn format_str(prefix: &str, s: &str) -> String {
282 let mut out = String::new();
283 out.push_str(prefix);
284 out.push_str(s);
285 out
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn bare_text() {
294 assert_eq!(
295 parse("hello world"),
296 Ok(Command::Text {
297 body: "hello world"
298 })
299 );
300 }
301
302 #[test]
303 fn quit_and_aliases() {
304 assert_eq!(parse("/quit"), Ok(Command::Quit));
305 assert_eq!(parse("/exit"), Ok(Command::Quit));
306 }
307
308 #[test]
309 fn peer_add_with_alias() {
310 assert_eq!(
311 parse("/peer add ABCDEF bob"),
312 Ok(Command::PeerAdd {
313 pubkey: "ABCDEF",
314 alias: Some("bob")
315 })
316 );
317 }
318
319 #[test]
320 fn peer_alias() {
321 assert_eq!(
322 parse("/peer alias bob alice"),
323 Ok(Command::PeerAlias {
324 peer: "bob",
325 alias: "alice"
326 })
327 );
328 }
329
330 #[test]
331 fn ping_with_bytes() {
332 assert_eq!(
333 parse("/ping bob 64"),
334 Ok(Command::Ping {
335 peer: "bob",
336 bytes: Some(64)
337 })
338 );
339 }
340
341 #[test]
342 fn set_show_vs_set() {
343 assert_eq!(parse("/set"), Ok(Command::SetShow));
344 assert_eq!(
345 parse("/set flood_hops 3"),
346 Ok(Command::Set {
347 var: "flood_hops",
348 val: "3"
349 })
350 );
351 }
352
353 #[test]
354 fn counters() {
355 assert_eq!(parse("/counters"), Ok(Command::Counters));
356 }
357
358 #[test]
359 fn reboot() {
360 assert_eq!(parse("/reboot"), Ok(Command::Reboot));
361 }
362
363 #[test]
364 fn unknown_cmd() {
365 assert!(matches!(parse("/wut"), Err(ParseError::UnknownCommand(_))));
366 }
367}