tsproto_packets/
commands.rs

1use std::borrow::Cow;
2use std::str::{self, FromStr};
3
4use crate::{Error, Result};
5
6/// Parses arguments of a command.
7#[derive(Clone, Debug)]
8pub struct CommandParser<'a> {
9	data: &'a [u8],
10	index: usize,
11}
12
13#[derive(Clone, Debug)]
14pub enum CommandItem<'a> {
15	Argument(CommandArgument<'a>),
16	/// Pipe symbol marking the start of the next command.
17	NextCommand,
18}
19
20#[derive(Clone, Debug)]
21pub struct CommandArgument<'a> {
22	name: &'a [u8],
23	value: CommandArgumentValue<'a>,
24}
25
26#[derive(Clone, Debug)]
27pub struct CommandArgumentValue<'a> {
28	raw: &'a [u8],
29	/// The number of escape sequences in this value.
30	escapes: usize,
31}
32
33impl<'a> CommandParser<'a> {
34	/// Returns the name and arguments of the given command.
35	#[inline]
36	pub fn new(data: &'a [u8]) -> (&'a [u8], Self) {
37		let mut name_end = 0;
38		while name_end < data.len() {
39			if !data[name_end].is_ascii_alphanumeric() {
40				if data[name_end] == b' ' {
41					break;
42				}
43				// Not a command name
44				name_end = 0;
45				break;
46			}
47			name_end += 1;
48		}
49
50		(&data[..name_end], Self { data, index: name_end })
51	}
52
53	fn cur(&self) -> u8 { self.data[self.index] }
54	fn cur_in(&self, cs: &[u8]) -> bool { cs.contains(&self.cur()) }
55	/// Advance
56	fn adv(&mut self) { self.index += 1; }
57	fn at_end(&self) -> bool { self.index >= self.data.len() }
58
59	fn skip_space(&mut self) {
60		while !self.at_end() && self.cur_in(b"\x0b\x0c\t\r\n ") {
61			self.adv();
62		}
63	}
64}
65
66impl<'a> Iterator for CommandParser<'a> {
67	type Item = CommandItem<'a>;
68	fn next(&mut self) -> Option<Self::Item> {
69		self.skip_space();
70		if self.at_end() {
71			return None;
72		}
73		if self.cur() == b'|' {
74			self.adv();
75			return Some(CommandItem::NextCommand);
76		}
77
78		let name_start = self.index;
79		while !self.at_end() && !self.cur_in(b" =|") {
80			self.adv();
81		}
82		let name_end = self.index;
83		if self.at_end() || self.cur() != b'=' {
84			return Some(CommandItem::Argument(CommandArgument {
85				name: &self.data[name_start..name_end],
86				value: CommandArgumentValue { raw: &[], escapes: 0 },
87			}));
88		}
89
90		self.adv();
91		let value_start = self.index;
92		let mut escapes = 0;
93		while !self.at_end() {
94			if self.cur_in(b"\x0b\x0c\t\r\n| ") {
95				break;
96			}
97			if self.cur() == b'\\' {
98				escapes += 1;
99				self.adv();
100				if self.at_end() || self.cur_in(b"\x0b\x0c\t\r\n| ") {
101					break;
102				}
103			}
104			self.adv();
105		}
106		let value_end = self.index;
107		Some(CommandItem::Argument(CommandArgument {
108			name: &self.data[name_start..name_end],
109			value: CommandArgumentValue { raw: &self.data[value_start..value_end], escapes },
110		}))
111	}
112}
113
114impl<'a> CommandArgument<'a> {
115	#[inline]
116	pub fn name(&self) -> &'a [u8] { self.name }
117	#[inline]
118	pub fn value(&self) -> &CommandArgumentValue<'a> { &self.value }
119}
120
121impl<'a> CommandArgumentValue<'a> {
122	fn unescape(&self) -> Vec<u8> {
123		let mut res = Vec::with_capacity(self.raw.len() - self.escapes);
124		let mut i = 0;
125		while i < self.raw.len() {
126			if self.raw[i] == b'\\' {
127				i += 1;
128				if i == self.raw.len() {
129					return res;
130				}
131				res.push(match self.raw[i] {
132					b'v' => b'\x0b',
133					b'f' => b'\x0c',
134					b't' => b'\t',
135					b'r' => b'\r',
136					b'n' => b'\n',
137					b'p' => b'|',
138					b's' => b' ',
139					c => c,
140				});
141			} else {
142				res.push(self.raw[i]);
143			}
144			i += 1;
145		}
146		res
147	}
148
149	#[inline]
150	pub fn get_raw(&self) -> &'a [u8] { self.raw }
151	#[inline]
152	pub fn get(&self) -> Cow<'a, [u8]> {
153		if self.escapes == 0 { Cow::Borrowed(self.raw) } else { Cow::Owned(self.unescape()) }
154	}
155
156	#[inline]
157	pub fn get_str(&self) -> Result<Cow<'a, str>> {
158		if self.escapes == 0 {
159			Ok(Cow::Borrowed(str::from_utf8(self.raw)?))
160		} else {
161			Ok(Cow::Owned(String::from_utf8(self.unescape())?))
162		}
163	}
164
165	#[inline]
166	pub fn get_parse<E, T: FromStr>(&self) -> std::result::Result<T, E>
167	where
168		E: From<<T as FromStr>::Err>,
169		E: From<Error>,
170	{
171		Ok(self.get_str()?.as_ref().parse()?)
172	}
173}
174
175#[cfg(test)]
176mod tests {
177	use super::*;
178	use crate::packets::{Direction, Flags, OutCommand, PacketType};
179	use std::str;
180
181	/// Parse and write again.
182	fn test_loop_with_result(data: &[u8], result: &[u8]) {
183		let (name, parser) = CommandParser::new(data);
184		let name = str::from_utf8(name).unwrap();
185
186		let mut out_command =
187			OutCommand::new(Direction::S2C, Flags::empty(), PacketType::Command, name);
188
189		println!("\nParsing {}", str::from_utf8(data).unwrap());
190		for item in parser {
191			println!("Item: {:?}", item);
192			match item {
193				CommandItem::NextCommand => out_command.start_new_part(),
194				CommandItem::Argument(arg) => {
195					out_command.write_arg(
196						str::from_utf8(arg.name()).unwrap(),
197						&arg.value().get_str().unwrap(),
198					);
199				}
200			}
201		}
202
203		let packet = out_command.into_packet();
204		let in_str = str::from_utf8(result).unwrap();
205		let out_str = str::from_utf8(packet.content()).unwrap();
206		assert_eq!(in_str, out_str);
207	}
208
209	/// Parse and write again.
210	fn test_loop(data: &[u8]) { test_loop_with_result(data, data); }
211
212	const TEST_COMMANDS: &[&str] = &[
213		"cmd a=1 b=2 c=3",
214		"cmd a=\\s\\\\ b=\\p c=abc\\tdef",
215		"cmd a=1 c=3 b=2|b=4|b=5",
216		"initivexpand2 l=AQCVXTlKF+UQc0yga99dOQ9FJCwLaJqtDb1G7xYPMvHFMwIKVfKADF6zAAcAAAAgQW5vbnltb3VzAAAKQo71lhtEMbqAmtuMLlY8Snr0k2Wmymv4hnHNU6tjQCALKHewCykgcA== beta=\\/8kL8lcAYyMJovVOP6MIUC1oZASyuL\\/Y\\/qjVG06R4byuucl9oPAvR7eqZI7z8jGm9jkGmtJ6 omega=MEsDAgcAAgEgAiBxu2eCLQf8zLnuJJ6FtbVjfaOa1210xFgedoXuGzDbTgIgcGk35eqFavKxS4dROi5uKNSNsmzIL4+fyh5Z\\/+FWGxU= ot=1 proof=MEUCIQDRCP4J9e+8IxMJfCLWWI1oIbNPGcChl+3Jr2vIuyDxzAIgOrzRAFPOuJZF4CBw\\/xgbzEsgKMtEtgNobF6WXVNhfUw= tvd time=1544221457",
217
218		"clientinitiv alpha=41Te9Ar7hMPx+A== omega=MEwDAgcAAgEgAiEAq2iCMfcijKDZ5tn2tuZcH+\\/GF+dmdxlXjDSFXLPGadACIHzUnbsPQ0FDt34Su4UXF46VFI0+4wjMDNszdoDYocu0 ip",
219
220		// Well, that's more corrupted packet, but the parser should be robust
221		"initserver virtualserver_name=Server\\sder\\sVerplanten \
222		 virtualserver_welcomemessage=This\\sis\\sSplamys\\sWorld \
223		 virtualserver_platform=Linux \
224		 virtualserver_version=3.0.13.8\\s[Build:\\s1500452811] \
225		 virtualserver_maxclients=32 virtualserver_created=0 \
226		 virtualserver_nodec_encryption_mode=1 \
227		 virtualserver_hostmessage=Lé\\sServer\\sde\\sSplamy \
228		 virtualserver_name=Server_mode=0 virtualserver_default_server \
229		 group=8 virtualserver_default_channel_group=8 \
230		 virtualserver_hostbanner_url virtualserver_hostmessagegfx_url \
231		 virtualserver_hostmessagegfx_interval=2000 \
232		 virtualserver_priority_speaker_dimm_modificat",
233
234		"channellist cid=2 cpid=0 channel_name=Trusted\\sChannel \
235		 channel_topic channel_codec=0 channel_codec_quality=0 \
236		 channel_maxclients=0 channel_maxfamilyclients=-1 channel_order=1 \
237		 channel_flag_permanent=1 channel_flag_semi_permanent=0 \
238		 channel_flag_default=0 channel_flag_password=0 \
239		 channel_codec_latency_factor=1 channel_codec_is_unencrypted=1 \
240		 channel_delete_delay=0 channel_flag_maxclients_unlimited=0 \
241		 channel_flag_maxfamilyclients_unlimited=0 \
242		 channel_flag_maxfamilyclients_inherited=1 \
243		 channel_needed_talk_power=0 channel_forced_silence=0 \
244		 channel_name_phonetic channel_icon_id=0 \
245		 channel_flag_private=0|cid=4 cpid=2 \
246		 channel_name=Ding\\s•\\s1\\s\\p\\sSplamy´s\\sBett channel_topic \
247		 channel_codec=4 channel_codec_quality=7 channel_maxclients=-1 \
248		 channel_maxfamilyclients=-1 channel_order=0 \
249		 channel_flag_permanent=1 channel_flag_semi_permanent=0 \
250		 channel_flag_default=0 channel_flag_password=0 \
251		 channel_codec_latency_factor=1 channel_codec_is_unencrypted=1 \
252		 channel_delete_delay=0 channel_flag_maxclients_unlimited=1 \
253		 channel_flag_maxfamilyclients_unlimited=0 \
254		 channel_flag_maxfamilyclients_inherited=1 \
255		 channel_needed_talk_power=0 channel_forced_silence=0 \
256		 channel_name_phonetic=Neo\\sSeebi\\sEvangelion channel_icon_id=0 \
257		 channel_flag_private=0", //|cid=6 cpid=2 channel_name=Ding\\s\xe2\x80\xa2\\s2\\s\\p\\sThe\\sBook\\sof\\sHeavy\\sMetal channel_topic channel_codec=2 channel_codec_quality=7 channel_maxclients=-1 channel_maxfamilyclients=-1 channel_order=4 channel_flag_permanent=1 channel_flag_semi_permanent=0 channel_flag_default=0 channel_flag_password=0 channel_codec_latency_factor=1 channel_codec_is_unencrypted=1 channel_delete_delay=0 channel_flag_maxclients_unlimited=1 channel_flag_maxfamilyclients_unlimited=0 channel_flag_maxfamilyclients_inherited=1 channel_needed_talk_power=0 channel_forced_silence=0 channel_name_phonetic=Not\\senought\\sChannels channel_icon_id=0 channel_flag_private=0|cid=30 cpid=2 channel_name=Ding\\s\xe2\x80\xa2\\s3\\s\\p\\sSenpai\\sGef\xc3\xa4hrlich channel_topic channel_codec=2 channel_codec_quality=7 channel_maxclients=-1 channel_maxfamilyclients=-1 channel_order=6 channel_flag_permanent=1 channel_flag_semi_permanent=0 channel_flag_default=0 channel_flag_password=0 channel_codec_latency_factor=1 channel_codec_is_unencrypted=1 channel_delete_delay=0 channel_flag_maxclients_unlimited=1 channel_flag_maxfamilyclients_unlimited=0 channel_flag_maxfamilyclients_inherited=1 channel_needed_talk_power=0 channel_forced_silence=0 channel_name_phonetic=The\\strashcan\\shas\\sthe\\strash channel_icon_id=0 channel_flag_private=0",
258
259		"notifychannelsubscribed cid=2|cid=4 es=3867|cid=5 es=18694|cid=6 es=18694|cid=7 es=18694|cid=11 es=18694|cid=13 es=18694|cid=14 es=18694|cid=16 es=18694|cid=22 es=18694|cid=23 es=18694|cid=24 es=18694|cid=25 es=18694|cid=30 es=18694|cid=163 es=18694",
260
261		"notifypermissionlist group_id_end=0|group_id_end=7|group_id_end=13|group_id_end=18|group_id_end=21|group_id_end=21|group_id_end=33|group_id_end=47|group_id_end=77|group_id_end=82|group_id_end=83|group_id_end=106|group_id_end=126|group_id_end=132|group_id_end=143|group_id_end=151|group_id_end=160|group_id_end=162|group_id_end=170|group_id_end=172|group_id_end=190|group_id_end=197|group_id_end=215|group_id_end=227|group_id_end=232|group_id_end=248|permname=b_serverinstance_help_view permdesc=Retrieve\\sinformation\\sabout\\sServerQuery\\scommands|permname=b_serverinstance_version_view permdesc=Retrieve\\sglobal\\sserver\\sversion\\s(including\\splatform\\sand\\sbuild\\snumber)|permname=b_serverinstance_info_view permdesc=Retrieve\\sglobal\\sserver\\sinformation|permname=b_serverinstance_virtualserver_list permdesc=List\\svirtual\\sservers\\sstored\\sin\\sthe\\sdatabase",
262
263		// Server query
264		"cmd=1 cid=2",
265		"channellistfinished",
266		// With newlines
267		"sendtextmessage text=\\nmess\\nage\\n return_code=11",
268	];
269
270	#[test]
271	fn loop_test() {
272		for cmd in TEST_COMMANDS {
273			test_loop(cmd.as_bytes());
274		}
275	}
276
277	#[test]
278	fn optional_arg() {
279		test_loop(b"cmd a");
280		test_loop(b"cmd a b=1");
281
282		test_loop_with_result(b"cmd a=", b"cmd a");
283		test_loop_with_result(b"cmd a= b=1", b"cmd a b=1");
284	}
285
286	#[test]
287	fn no_slash_escape() {
288		let in_cmd = "clientinitiv alpha=giGMvmfHzbY3ig== omega=MEsDAgcAAgEgAiAIXJBlj1hQbaH0Eq0DuLlCmH8bl+veTAO2+k9EQjEYSgIgNnImcmKo7ls5mExb6skfK2Tw+u54aeDr0OP1ITsC/50= ot=1 ip";
289		let out_cmd = "clientinitiv alpha=giGMvmfHzbY3ig== omega=MEsDAgcAAgEgAiAIXJBlj1hQbaH0Eq0DuLlCmH8bl+veTAO2+k9EQjEYSgIgNnImcmKo7ls5mExb6skfK2Tw+u54aeDr0OP1ITsC\\/50= ot=1 ip";
290
291		test_loop_with_result(in_cmd.as_bytes(), out_cmd.as_bytes());
292	}
293}