tsproto_packets/
commands.rs1use std::borrow::Cow;
2use std::str::{self, FromStr};
3
4use crate::{Error, Result};
5
6#[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 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 escapes: usize,
31}
32
33impl<'a> CommandParser<'a> {
34 #[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 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 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 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 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 "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", "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 "cmd=1 cid=2",
265 "channellistfinished",
266 "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}