1 /** The parsers for the chat and multiplayer protocol. The main parser is `message`. |
|
2 * # Protocol |
|
3 * All messages consist of `\n`-separated strings. The end of a message is |
|
4 * indicated by a double newline - `\n\n`. |
|
5 * |
|
6 * For example, a nullary command like PING will be actually sent as `PING\n\n`. |
|
7 * A unary command, such as `START_GAME nick` will be actually sent as `START_GAME\nnick\n\n`. |
|
8 */ |
|
9 use nom::{ |
|
10 branch::alt, |
|
11 bytes::complete::{tag, tag_no_case, take_until, take_while}, |
|
12 character::complete::{newline, not_line_ending}, |
|
13 combinator::{map, peek}, |
|
14 error::{ErrorKind, ParseError}, |
|
15 multi::separated_list0, |
|
16 sequence::{delimited, pair, preceded, terminated, tuple}, |
|
17 Err, IResult, |
|
18 }; |
|
19 |
|
20 use std::{ |
|
21 num::ParseIntError, |
|
22 ops::Range, |
|
23 str, |
|
24 str::{FromStr, Utf8Error}, |
|
25 }; |
|
26 |
|
27 use super::messages::{HwProtocolMessage, HwProtocolMessage::*}; |
|
28 use crate::core::types::{ |
|
29 GameCfg, HedgehogInfo, ServerVar, TeamInfo, VoteType, MAX_HEDGEHOGS_PER_TEAM, |
|
30 }; |
|
31 |
|
32 #[derive(Debug, PartialEq)] |
|
33 pub struct HwProtocolError {} |
|
34 |
|
35 impl HwProtocolError { |
|
36 fn new() -> Self { |
|
37 HwProtocolError {} |
|
38 } |
|
39 } |
|
40 |
|
41 impl<I> ParseError<I> for HwProtocolError { |
|
42 fn from_error_kind(input: I, kind: ErrorKind) -> Self { |
|
43 HwProtocolError::new() |
|
44 } |
|
45 |
|
46 fn append(input: I, kind: ErrorKind, other: Self) -> Self { |
|
47 HwProtocolError::new() |
|
48 } |
|
49 } |
|
50 |
|
51 impl From<Utf8Error> for HwProtocolError { |
|
52 fn from(_: Utf8Error) -> Self { |
|
53 HwProtocolError::new() |
|
54 } |
|
55 } |
|
56 |
|
57 impl From<ParseIntError> for HwProtocolError { |
|
58 fn from(_: ParseIntError) -> Self { |
|
59 HwProtocolError::new() |
|
60 } |
|
61 } |
|
62 |
|
63 pub type HwResult<'a, O> = IResult<&'a [u8], O, HwProtocolError>; |
|
64 |
|
65 fn end_of_message(input: &[u8]) -> HwResult<&[u8]> { |
|
66 tag("\n\n")(input) |
|
67 } |
|
68 |
|
69 fn convert_utf8(input: &[u8]) -> HwResult<&str> { |
|
70 match str::from_utf8(input) { |
|
71 Ok(str) => Ok((b"", str)), |
|
72 Err(utf_err) => Result::Err(Err::Failure(utf_err.into())), |
|
73 } |
|
74 } |
|
75 |
|
76 fn convert_from_str<T>(str: &str) -> HwResult<T> |
|
77 where |
|
78 T: FromStr<Err = ParseIntError>, |
|
79 { |
|
80 match T::from_str(str) { |
|
81 Ok(x) => Ok((b"", x)), |
|
82 Err(format_err) => Result::Err(Err::Failure(format_err.into())), |
|
83 } |
|
84 } |
|
85 |
|
86 fn str_line(input: &[u8]) -> HwResult<&str> { |
|
87 let (i, text) = not_line_ending(input.clone())?; |
|
88 if i != input { |
|
89 Ok((i, convert_utf8(text)?.1)) |
|
90 } else { |
|
91 Err(Err::Error(HwProtocolError::new())) |
|
92 } |
|
93 } |
|
94 |
|
95 fn a_line(input: &[u8]) -> HwResult<String> { |
|
96 map(str_line, String::from)(input) |
|
97 } |
|
98 |
|
99 fn cmd_arg(input: &[u8]) -> HwResult<String> { |
|
100 let delimiters = b" \n"; |
|
101 let (i, str) = take_while(move |c| !delimiters.contains(&c))(input.clone())?; |
|
102 if i != input { |
|
103 Ok((i, convert_utf8(str)?.1.to_string())) |
|
104 } else { |
|
105 Err(Err::Error(HwProtocolError::new())) |
|
106 } |
|
107 } |
|
108 |
|
109 fn u8_line(input: &[u8]) -> HwResult<u8> { |
|
110 let (i, str) = str_line(input)?; |
|
111 Ok((i, convert_from_str(str)?.1)) |
|
112 } |
|
113 |
|
114 fn u16_line(input: &[u8]) -> HwResult<u16> { |
|
115 let (i, str) = str_line(input)?; |
|
116 Ok((i, convert_from_str(str)?.1)) |
|
117 } |
|
118 |
|
119 fn u32_line(input: &[u8]) -> HwResult<u32> { |
|
120 let (i, str) = str_line(input)?; |
|
121 Ok((i, convert_from_str(str)?.1)) |
|
122 } |
|
123 |
|
124 fn yes_no_line(input: &[u8]) -> HwResult<bool> { |
|
125 alt(( |
|
126 map(tag_no_case(b"YES"), |_| true), |
|
127 map(tag_no_case(b"NO"), |_| false), |
|
128 ))(input) |
|
129 } |
|
130 |
|
131 fn opt_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option<String>> { |
|
132 alt(( |
|
133 map(peek(end_of_message), |_| None), |
|
134 map(preceded(tag("\n"), a_line), Some), |
|
135 ))(input) |
|
136 } |
|
137 |
|
138 fn spaces(input: &[u8]) -> HwResult<&[u8]> { |
|
139 preceded(tag(" "), take_while(|c| c == b' '))(input) |
|
140 } |
|
141 |
|
142 fn opt_space_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option<String>> { |
|
143 alt(( |
|
144 map(peek(end_of_message), |_| None), |
|
145 map(preceded(spaces, a_line), Some), |
|
146 ))(input) |
|
147 } |
|
148 |
|
149 fn hedgehog_array(input: &[u8]) -> HwResult<[HedgehogInfo; 8]> { |
|
150 fn hedgehog_line(input: &[u8]) -> HwResult<HedgehogInfo> { |
|
151 map( |
|
152 tuple((terminated(a_line, newline), a_line)), |
|
153 |(name, hat)| HedgehogInfo { name, hat }, |
|
154 )(input) |
|
155 } |
|
156 |
|
157 let (i, (h1, h2, h3, h4, h5, h6, h7, h8)) = tuple(( |
|
158 terminated(hedgehog_line, newline), |
|
159 terminated(hedgehog_line, newline), |
|
160 terminated(hedgehog_line, newline), |
|
161 terminated(hedgehog_line, newline), |
|
162 terminated(hedgehog_line, newline), |
|
163 terminated(hedgehog_line, newline), |
|
164 terminated(hedgehog_line, newline), |
|
165 hedgehog_line, |
|
166 ))(input)?; |
|
167 |
|
168 Ok((i, [h1, h2, h3, h4, h5, h6, h7, h8])) |
|
169 } |
|
170 |
|
171 fn voting(input: &[u8]) -> HwResult<VoteType> { |
|
172 alt(( |
|
173 map(tag_no_case("PAUSE"), |_| VoteType::Pause), |
|
174 map(tag_no_case("NEWSEED"), |_| VoteType::NewSeed), |
|
175 map( |
|
176 preceded(pair(tag_no_case("KICK"), spaces), a_line), |
|
177 VoteType::Kick, |
|
178 ), |
|
179 map( |
|
180 preceded(pair(tag_no_case("HEDGEHOGS"), spaces), u8_line), |
|
181 VoteType::HedgehogsPerTeam, |
|
182 ), |
|
183 map(preceded(tag_no_case("MAP"), opt_space_arg), VoteType::Map), |
|
184 ))(input) |
|
185 } |
|
186 |
|
187 fn no_arg_message(input: &[u8]) -> HwResult<HwProtocolMessage> { |
|
188 fn message<'a>( |
|
189 name: &'a str, |
|
190 msg: HwProtocolMessage, |
|
191 ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { |
|
192 move |i| map(tag(name), |_| msg.clone())(i) |
|
193 } |
|
194 |
|
195 alt(( |
|
196 message("PING", Ping), |
|
197 message("PONG", Pong), |
|
198 message("LIST", List), |
|
199 message("BANLIST", BanList), |
|
200 message("GET_SERVER_VAR", GetServerVar), |
|
201 message("TOGGLE_READY", ToggleReady), |
|
202 message("START_GAME", StartGame), |
|
203 message("TOGGLE_RESTRICT_JOINS", ToggleRestrictJoin), |
|
204 message("TOGGLE_RESTRICT_TEAMS", ToggleRestrictTeams), |
|
205 message("TOGGLE_REGISTERED_ONLY", ToggleRegisteredOnly), |
|
206 ))(input) |
|
207 } |
|
208 |
|
209 fn single_arg_message(input: &[u8]) -> HwResult<HwProtocolMessage> { |
|
210 fn message<'a, T, F, G>( |
|
211 name: &'a str, |
|
212 parser: F, |
|
213 constructor: G, |
|
214 ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwProtocolMessage> |
|
215 where |
|
216 F: Fn(&[u8]) -> HwResult<T>, |
|
217 G: Fn(T) -> HwProtocolMessage, |
|
218 { |
|
219 map(preceded(tag(name), parser), constructor) |
|
220 } |
|
221 |
|
222 alt(( |
|
223 message("NICK\n", a_line, Nick), |
|
224 message("INFO\n", a_line, Info), |
|
225 message("CHAT\n", a_line, Chat), |
|
226 message("PART", opt_arg, Part), |
|
227 message("FOLLOW\n", a_line, Follow), |
|
228 message("KICK\n", a_line, Kick), |
|
229 message("UNBAN\n", a_line, Unban), |
|
230 message("EM\n", a_line, EngineMessage), |
|
231 message("TEAMCHAT\n", a_line, TeamChat), |
|
232 message("ROOM_NAME\n", a_line, RoomName), |
|
233 message("REMOVE_TEAM\n", a_line, RemoveTeam), |
|
234 message("ROUNDFINISHED", opt_arg, |_| RoundFinished), |
|
235 message("PROTO\n", u16_line, Proto), |
|
236 message("QUIT", opt_arg, Quit), |
|
237 ))(input) |
|
238 } |
|
239 |
|
240 fn cmd_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { |
|
241 fn cmd_no_arg<'a>( |
|
242 name: &'a str, |
|
243 msg: HwProtocolMessage, |
|
244 ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { |
|
245 move |i| map(tag_no_case(name), |_| msg.clone())(i) |
|
246 } |
|
247 |
|
248 fn cmd_single_arg<'a, T, F, G>( |
|
249 name: &'a str, |
|
250 parser: F, |
|
251 constructor: G, |
|
252 ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwProtocolMessage> |
|
253 where |
|
254 F: Fn(&'a [u8]) -> HwResult<'a, T>, |
|
255 G: Fn(T) -> HwProtocolMessage, |
|
256 { |
|
257 map( |
|
258 preceded(pair(tag_no_case(name), spaces), parser), |
|
259 constructor, |
|
260 ) |
|
261 } |
|
262 |
|
263 fn cmd_no_arg_message(input: &[u8]) -> HwResult<HwProtocolMessage> { |
|
264 alt(( |
|
265 cmd_no_arg("STATS", Stats), |
|
266 cmd_no_arg("FIX", Fix), |
|
267 cmd_no_arg("UNFIX", Unfix), |
|
268 cmd_no_arg("REGISTERED_ONLY", ToggleServerRegisteredOnly), |
|
269 cmd_no_arg("SUPER_POWER", SuperPower), |
|
270 ))(input) |
|
271 } |
|
272 |
|
273 fn cmd_single_arg_message(input: &[u8]) -> HwResult<HwProtocolMessage> { |
|
274 alt(( |
|
275 cmd_single_arg("RESTART_SERVER", |i| tag("YES")(i), |_| RestartServer), |
|
276 cmd_single_arg("DELEGATE", a_line, Delegate), |
|
277 cmd_single_arg("DELETE", a_line, Delete), |
|
278 cmd_single_arg("SAVEROOM", a_line, SaveRoom), |
|
279 cmd_single_arg("LOADROOM", a_line, LoadRoom), |
|
280 cmd_single_arg("GLOBAL", a_line, Global), |
|
281 cmd_single_arg("WATCH", u32_line, Watch), |
|
282 cmd_single_arg("VOTE", yes_no_line, Vote), |
|
283 cmd_single_arg("FORCE", yes_no_line, ForceVote), |
|
284 cmd_single_arg("INFO", a_line, Info), |
|
285 cmd_single_arg("MAXTEAMS", u8_line, MaxTeams), |
|
286 cmd_single_arg("CALLVOTE", voting, |v| CallVote(Some(v))), |
|
287 ))(input) |
|
288 } |
|
289 |
|
290 preceded( |
|
291 tag("CMD\n"), |
|
292 alt(( |
|
293 cmd_no_arg_message, |
|
294 cmd_single_arg_message, |
|
295 map(tag_no_case("CALLVOTE"), |_| CallVote(None)), |
|
296 map(preceded(tag_no_case("GREETING"), opt_space_arg), Greeting), |
|
297 map(preceded(tag_no_case("PART"), opt_space_arg), Part), |
|
298 map(preceded(tag_no_case("QUIT"), opt_space_arg), Quit), |
|
299 map( |
|
300 preceded( |
|
301 tag_no_case("SAVE"), |
|
302 pair(preceded(spaces, cmd_arg), preceded(spaces, cmd_arg)), |
|
303 ), |
|
304 |(n, l)| Save(n, l), |
|
305 ), |
|
306 map( |
|
307 preceded( |
|
308 tag_no_case("RND"), |
|
309 alt(( |
|
310 map(peek(end_of_message), |_| vec![]), |
|
311 preceded(spaces, separated_list0(spaces, cmd_arg)), |
|
312 )), |
|
313 ), |
|
314 Rnd, |
|
315 ), |
|
316 )), |
|
317 )(input) |
|
318 } |
|
319 |
|
320 fn config_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { |
|
321 fn cfg_single_arg<'a, T, F, G>( |
|
322 name: &'a str, |
|
323 parser: F, |
|
324 constructor: G, |
|
325 ) -> impl FnMut(&'a [u8]) -> HwResult<'a, GameCfg> |
|
326 where |
|
327 F: Fn(&[u8]) -> HwResult<T>, |
|
328 G: Fn(T) -> GameCfg, |
|
329 { |
|
330 map(preceded(pair(tag(name), newline), parser), constructor) |
|
331 } |
|
332 |
|
333 let (i, cfg) = preceded( |
|
334 tag("CFG\n"), |
|
335 alt(( |
|
336 cfg_single_arg("THEME", a_line, GameCfg::Theme), |
|
337 cfg_single_arg("SCRIPT", a_line, GameCfg::Script), |
|
338 cfg_single_arg("MAP", a_line, GameCfg::MapType), |
|
339 cfg_single_arg("MAPGEN", u32_line, GameCfg::MapGenerator), |
|
340 cfg_single_arg("MAZE_SIZE", u32_line, GameCfg::MazeSize), |
|
341 cfg_single_arg("TEMPLATE", u32_line, GameCfg::Template), |
|
342 cfg_single_arg("FEATURE_SIZE", u32_line, GameCfg::FeatureSize), |
|
343 cfg_single_arg("SEED", a_line, GameCfg::Seed), |
|
344 cfg_single_arg("DRAWNMAP", a_line, GameCfg::DrawnMap), |
|
345 preceded(pair(tag("AMMO"), newline), |i| { |
|
346 let (i, name) = a_line(i)?; |
|
347 let (i, value) = opt_arg(i)?; |
|
348 Ok((i, GameCfg::Ammo(name, value))) |
|
349 }), |
|
350 preceded( |
|
351 pair(tag("SCHEME"), newline), |
|
352 map( |
|
353 pair( |
|
354 a_line, |
|
355 alt(( |
|
356 map(peek(end_of_message), |_| None), |
|
357 map(preceded(newline, separated_list0(newline, a_line)), Some), |
|
358 )), |
|
359 ), |
|
360 |(name, values)| GameCfg::Scheme(name, values.unwrap_or_default()), |
|
361 ), |
|
362 ), |
|
363 )), |
|
364 )(input)?; |
|
365 Ok((i, Cfg(cfg))) |
|
366 } |
|
367 |
|
368 fn server_var_message(input: &[u8]) -> HwResult<HwProtocolMessage> { |
|
369 map( |
|
370 preceded( |
|
371 tag("SET_SERVER_VAR\n"), |
|
372 alt(( |
|
373 map(preceded(tag("MOTD_NEW\n"), a_line), ServerVar::MOTDNew), |
|
374 map(preceded(tag("MOTD_OLD\n"), a_line), ServerVar::MOTDOld), |
|
375 map( |
|
376 preceded(tag("LATEST_PROTO\n"), u16_line), |
|
377 ServerVar::LatestProto, |
|
378 ), |
|
379 )), |
|
380 ), |
|
381 SetServerVar, |
|
382 )(input) |
|
383 } |
|
384 |
|
385 fn complex_message(input: &[u8]) -> HwResult<HwProtocolMessage> { |
|
386 alt(( |
|
387 preceded( |
|
388 pair(tag("PASSWORD"), newline), |
|
389 map(pair(terminated(a_line, newline), a_line), |(pass, salt)| { |
|
390 Password(pass, salt) |
|
391 }), |
|
392 ), |
|
393 preceded( |
|
394 pair(tag("CHECKER"), newline), |
|
395 map( |
|
396 tuple(( |
|
397 terminated(u16_line, newline), |
|
398 terminated(a_line, newline), |
|
399 a_line, |
|
400 )), |
|
401 |(protocol, name, pass)| Checker(protocol, name, pass), |
|
402 ), |
|
403 ), |
|
404 preceded( |
|
405 pair(tag("CREATE_ROOM"), newline), |
|
406 map(pair(a_line, opt_arg), |(name, pass)| CreateRoom(name, pass)), |
|
407 ), |
|
408 preceded( |
|
409 pair(tag("JOIN_ROOM"), newline), |
|
410 map(pair(a_line, opt_arg), |(name, pass)| JoinRoom(name, pass)), |
|
411 ), |
|
412 preceded( |
|
413 pair(tag("ADD_TEAM"), newline), |
|
414 map( |
|
415 tuple(( |
|
416 terminated(a_line, newline), |
|
417 terminated(u8_line, newline), |
|
418 terminated(a_line, newline), |
|
419 terminated(a_line, newline), |
|
420 terminated(a_line, newline), |
|
421 terminated(a_line, newline), |
|
422 terminated(u8_line, newline), |
|
423 hedgehog_array, |
|
424 )), |
|
425 |(name, color, grave, fort, voice_pack, flag, difficulty, hedgehogs)| { |
|
426 AddTeam(Box::new(TeamInfo { |
|
427 owner: String::new(), |
|
428 name, |
|
429 color, |
|
430 grave, |
|
431 fort, |
|
432 voice_pack, |
|
433 flag, |
|
434 difficulty, |
|
435 hedgehogs, |
|
436 hedgehogs_number: 0, |
|
437 })) |
|
438 }, |
|
439 ), |
|
440 ), |
|
441 preceded( |
|
442 pair(tag("HH_NUM"), newline), |
|
443 map( |
|
444 pair(terminated(a_line, newline), u8_line), |
|
445 |(name, count)| SetHedgehogsNumber(name, count), |
|
446 ), |
|
447 ), |
|
448 preceded( |
|
449 pair(tag("TEAM_COLOR"), newline), |
|
450 map( |
|
451 pair(terminated(a_line, newline), u8_line), |
|
452 |(name, color)| SetTeamColor(name, color), |
|
453 ), |
|
454 ), |
|
455 preceded( |
|
456 pair(tag("BAN"), newline), |
|
457 map( |
|
458 tuple(( |
|
459 terminated(a_line, newline), |
|
460 terminated(a_line, newline), |
|
461 u32_line, |
|
462 )), |
|
463 |(name, reason, time)| Ban(name, reason, time), |
|
464 ), |
|
465 ), |
|
466 preceded( |
|
467 pair(tag("BAN_IP"), newline), |
|
468 map( |
|
469 tuple(( |
|
470 terminated(a_line, newline), |
|
471 terminated(a_line, newline), |
|
472 u32_line, |
|
473 )), |
|
474 |(ip, reason, time)| BanIp(ip, reason, time), |
|
475 ), |
|
476 ), |
|
477 preceded( |
|
478 pair(tag("BAN_NICK"), newline), |
|
479 map( |
|
480 tuple(( |
|
481 terminated(a_line, newline), |
|
482 terminated(a_line, newline), |
|
483 u32_line, |
|
484 )), |
|
485 |(nick, reason, time)| BanNick(nick, reason, time), |
|
486 ), |
|
487 ), |
|
488 ))(input) |
|
489 } |
|
490 |
|
491 pub fn malformed_message(input: &[u8]) -> HwResult<()> { |
|
492 map(terminated(take_until(&b"\n\n"[..]), end_of_message), |_| ())(input) |
|
493 } |
|
494 |
|
495 pub fn message(input: &[u8]) -> HwResult<HwProtocolMessage> { |
|
496 delimited( |
|
497 take_while(|c| c == b'\n'), |
|
498 alt(( |
|
499 no_arg_message, |
|
500 single_arg_message, |
|
501 cmd_message, |
|
502 config_message, |
|
503 server_var_message, |
|
504 complex_message, |
|
505 )), |
|
506 end_of_message, |
|
507 )(input) |
|
508 } |
|
509 |
|
510 #[cfg(test)] |
|
511 mod test { |
|
512 use super::message; |
|
513 use crate::{ |
|
514 core::types::GameCfg, |
|
515 protocol::{messages::HwProtocolMessage::*, parser::HwProtocolError, test::gen_proto_msg}, |
|
516 }; |
|
517 use proptest::{proptest, proptest_helper}; |
|
518 |
|
519 #[cfg(test)] |
|
520 proptest! { |
|
521 #[test] |
|
522 fn is_parser_composition_idempotent(ref msg in gen_proto_msg()) { |
|
523 println!("!! Msg: {:?}, Bytes: {:?} !!", msg, msg.to_raw_protocol().as_bytes()); |
|
524 assert_eq!(message(msg.to_raw_protocol().as_bytes()), Ok((&b""[..], msg.clone()))) |
|
525 } |
|
526 } |
|
527 |
|
528 #[test] |
|
529 fn parse_test() { |
|
530 assert_eq!(message(b"PING\n\n"), Ok((&b""[..], Ping))); |
|
531 assert_eq!(message(b"START_GAME\n\n"), Ok((&b""[..], StartGame))); |
|
532 assert_eq!( |
|
533 message(b"NICK\nit's me\n\n"), |
|
534 Ok((&b""[..], Nick("it's me".to_string()))) |
|
535 ); |
|
536 assert_eq!(message(b"PROTO\n51\n\n"), Ok((&b""[..], Proto(51)))); |
|
537 assert_eq!( |
|
538 message(b"QUIT\nbye-bye\n\n"), |
|
539 Ok((&b""[..], Quit(Some("bye-bye".to_string())))) |
|
540 ); |
|
541 assert_eq!(message(b"QUIT\n\n"), Ok((&b""[..], Quit(None)))); |
|
542 assert_eq!( |
|
543 message(b"CMD\nwatch 49471\n\n"), |
|
544 Ok((&b""[..], Watch(49471))) |
|
545 ); |
|
546 assert_eq!( |
|
547 message(b"BAN\nme\nbad\n77\n\n"), |
|
548 Ok((&b""[..], Ban("me".to_string(), "bad".to_string(), 77))) |
|
549 ); |
|
550 |
|
551 assert_eq!(message(b"CMD\nPART\n\n"), Ok((&b""[..], Part(None)))); |
|
552 assert_eq!( |
|
553 message(b"CMD\nPART _msg_\n\n"), |
|
554 Ok((&b""[..], Part(Some("_msg_".to_string())))) |
|
555 ); |
|
556 |
|
557 assert_eq!(message(b"CMD\nRND\n\n"), Ok((&b""[..], Rnd(vec![])))); |
|
558 assert_eq!( |
|
559 message(b"CMD\nRND A B\n\n"), |
|
560 Ok((&b""[..], Rnd(vec![String::from("A"), String::from("B")]))) |
|
561 ); |
|
562 |
|
563 assert_eq!( |
|
564 message(b"CFG\nSCHEME\na\nA\n\n"), |
|
565 Ok(( |
|
566 &b""[..], |
|
567 Cfg(GameCfg::Scheme("a".to_string(), vec!["A".to_string()])) |
|
568 )) |
|
569 ); |
|
570 |
|
571 assert_eq!( |
|
572 message(b"QUIT\n1\n2\n\n"), |
|
573 Err(nom::Err::Error(HwProtocolError::new())) |
|
574 ); |
|
575 } |
|
576 } |
|