##// END OF EJS Templates
rust-chg: add helper to parse instructions sent from server...
Yuya Nishihara -
r45170:82adc720 default
parent child Browse files
Show More
@@ -1,232 +1,317 b''
1 // Copyright 2018 Yuya Nishihara <yuya@tcha.org>
1 // Copyright 2018 Yuya Nishihara <yuya@tcha.org>
2 //
2 //
3 // This software may be used and distributed according to the terms of the
3 // This software may be used and distributed according to the terms of the
4 // GNU General Public License version 2 or any later version.
4 // GNU General Public License version 2 or any later version.
5
5
6 //! Utility for parsing and building command-server messages.
6 //! Utility for parsing and building command-server messages.
7
7
8 use bytes::{BufMut, Bytes, BytesMut};
8 use bytes::{BufMut, Bytes, BytesMut};
9 use std::error;
9 use std::error;
10 use std::ffi::{OsStr, OsString};
10 use std::ffi::{OsStr, OsString};
11 use std::io;
11 use std::io;
12 use std::os::unix::ffi::OsStrExt;
12 use std::os::unix::ffi::OsStrExt;
13 use std::path::PathBuf;
13
14
14 pub use tokio_hglib::message::*; // re-exports
15 pub use tokio_hglib::message::*; // re-exports
15
16
16 /// Shell command type requested by the server.
17 /// Shell command type requested by the server.
17 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
18 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
18 pub enum CommandType {
19 pub enum CommandType {
19 /// Pager should be spawned.
20 /// Pager should be spawned.
20 Pager,
21 Pager,
21 /// Shell command should be executed to send back the result code.
22 /// Shell command should be executed to send back the result code.
22 System,
23 System,
23 }
24 }
24
25
25 /// Shell command requested by the server.
26 /// Shell command requested by the server.
26 #[derive(Clone, Debug, Eq, PartialEq)]
27 #[derive(Clone, Debug, Eq, PartialEq)]
27 pub struct CommandSpec {
28 pub struct CommandSpec {
28 pub command: OsString,
29 pub command: OsString,
29 pub current_dir: OsString,
30 pub current_dir: OsString,
30 pub envs: Vec<(OsString, OsString)>,
31 pub envs: Vec<(OsString, OsString)>,
31 }
32 }
32
33
33 /// Parses "S" channel request into command type and spec.
34 /// Parses "S" channel request into command type and spec.
34 pub fn parse_command_spec(data: Bytes) -> io::Result<(CommandType, CommandSpec)> {
35 pub fn parse_command_spec(data: Bytes) -> io::Result<(CommandType, CommandSpec)> {
35 let mut split = data.split(|&c| c == b'\0');
36 let mut split = data.split(|&c| c == b'\0');
36 let ctype = parse_command_type(split.next().ok_or(new_parse_error("missing type"))?)?;
37 let ctype = parse_command_type(split.next().ok_or(new_parse_error("missing type"))?)?;
37 let command = split.next().ok_or(new_parse_error("missing command"))?;
38 let command = split.next().ok_or(new_parse_error("missing command"))?;
38 let current_dir = split.next().ok_or(new_parse_error("missing current dir"))?;
39 let current_dir = split.next().ok_or(new_parse_error("missing current dir"))?;
39
40
40 let mut envs = Vec::new();
41 let mut envs = Vec::new();
41 for l in split {
42 for l in split {
42 let mut s = l.splitn(2, |&c| c == b'=');
43 let mut s = l.splitn(2, |&c| c == b'=');
43 let k = s.next().unwrap();
44 let k = s.next().unwrap();
44 let v = s.next().ok_or(new_parse_error("malformed env"))?;
45 let v = s.next().ok_or(new_parse_error("malformed env"))?;
45 envs.push((
46 envs.push((
46 OsStr::from_bytes(k).to_owned(),
47 OsStr::from_bytes(k).to_owned(),
47 OsStr::from_bytes(v).to_owned(),
48 OsStr::from_bytes(v).to_owned(),
48 ));
49 ));
49 }
50 }
50
51
51 let spec = CommandSpec {
52 let spec = CommandSpec {
52 command: OsStr::from_bytes(command).to_owned(),
53 command: OsStr::from_bytes(command).to_owned(),
53 current_dir: OsStr::from_bytes(current_dir).to_owned(),
54 current_dir: OsStr::from_bytes(current_dir).to_owned(),
54 envs: envs,
55 envs: envs,
55 };
56 };
56 Ok((ctype, spec))
57 Ok((ctype, spec))
57 }
58 }
58
59
59 fn parse_command_type(value: &[u8]) -> io::Result<CommandType> {
60 fn parse_command_type(value: &[u8]) -> io::Result<CommandType> {
60 match value {
61 match value {
61 b"pager" => Ok(CommandType::Pager),
62 b"pager" => Ok(CommandType::Pager),
62 b"system" => Ok(CommandType::System),
63 b"system" => Ok(CommandType::System),
63 _ => Err(new_parse_error(format!(
64 _ => Err(new_parse_error(format!(
64 "unknown command type: {}",
65 "unknown command type: {}",
65 decode_latin1(value)
66 decode_latin1(value)
66 ))),
67 ))),
67 }
68 }
68 }
69 }
69
70
71 /// Client-side instruction requested by the server.
72 #[derive(Clone, Debug, Eq, PartialEq)]
73 pub enum Instruction {
74 Exit(i32),
75 Reconnect,
76 Redirect(PathBuf),
77 Unlink(PathBuf),
78 }
79
80 /// Parses validation result into instructions.
81 pub fn parse_instructions(data: Bytes) -> io::Result<Vec<Instruction>> {
82 let mut instructions = Vec::new();
83 for l in data.split(|&c| c == b'\0') {
84 if l.is_empty() {
85 continue;
86 }
87 let mut s = l.splitn(2, |&c| c == b' ');
88 let inst = match (s.next().unwrap(), s.next()) {
89 (b"exit", Some(arg)) => decode_latin1(arg)
90 .parse()
91 .map(Instruction::Exit)
92 .map_err(|_| new_parse_error(format!("invalid exit code: {:?}", arg)))?,
93 (b"reconnect", None) => Instruction::Reconnect,
94 (b"redirect", Some(arg)) => {
95 Instruction::Redirect(OsStr::from_bytes(arg).to_owned().into())
96 }
97 (b"unlink", Some(arg)) => Instruction::Unlink(OsStr::from_bytes(arg).to_owned().into()),
98 _ => {
99 return Err(new_parse_error(format!("unknown command: {:?}", l)));
100 }
101 };
102 instructions.push(inst);
103 }
104 Ok(instructions)
105 }
106
70 // allocate large buffer as environment variables can be quite long
107 // allocate large buffer as environment variables can be quite long
71 const INITIAL_PACKED_ENV_VARS_CAPACITY: usize = 4096;
108 const INITIAL_PACKED_ENV_VARS_CAPACITY: usize = 4096;
72
109
73 /// Packs environment variables of platform encoding into bytes.
110 /// Packs environment variables of platform encoding into bytes.
74 ///
111 ///
75 /// # Panics
112 /// # Panics
76 ///
113 ///
77 /// Panics if key or value contains `\0` character, or key contains '='
114 /// Panics if key or value contains `\0` character, or key contains '='
78 /// character.
115 /// character.
79 pub fn pack_env_vars_os<I, P>(vars: I) -> Bytes
116 pub fn pack_env_vars_os<I, P>(vars: I) -> Bytes
80 where
117 where
81 I: IntoIterator<Item = (P, P)>,
118 I: IntoIterator<Item = (P, P)>,
82 P: AsRef<OsStr>,
119 P: AsRef<OsStr>,
83 {
120 {
84 let mut vars_iter = vars.into_iter();
121 let mut vars_iter = vars.into_iter();
85 if let Some((k, v)) = vars_iter.next() {
122 if let Some((k, v)) = vars_iter.next() {
86 let mut dst = BytesMut::with_capacity(INITIAL_PACKED_ENV_VARS_CAPACITY);
123 let mut dst = BytesMut::with_capacity(INITIAL_PACKED_ENV_VARS_CAPACITY);
87 pack_env_into(&mut dst, k.as_ref(), v.as_ref());
124 pack_env_into(&mut dst, k.as_ref(), v.as_ref());
88 for (k, v) in vars_iter {
125 for (k, v) in vars_iter {
89 dst.reserve(1);
126 dst.reserve(1);
90 dst.put_u8(b'\0');
127 dst.put_u8(b'\0');
91 pack_env_into(&mut dst, k.as_ref(), v.as_ref());
128 pack_env_into(&mut dst, k.as_ref(), v.as_ref());
92 }
129 }
93 dst.freeze()
130 dst.freeze()
94 } else {
131 } else {
95 Bytes::new()
132 Bytes::new()
96 }
133 }
97 }
134 }
98
135
99 fn pack_env_into(dst: &mut BytesMut, k: &OsStr, v: &OsStr) {
136 fn pack_env_into(dst: &mut BytesMut, k: &OsStr, v: &OsStr) {
100 assert!(!k.as_bytes().contains(&0), "key shouldn't contain NUL");
137 assert!(!k.as_bytes().contains(&0), "key shouldn't contain NUL");
101 assert!(!k.as_bytes().contains(&b'='), "key shouldn't contain '='");
138 assert!(!k.as_bytes().contains(&b'='), "key shouldn't contain '='");
102 assert!(!v.as_bytes().contains(&0), "value shouldn't contain NUL");
139 assert!(!v.as_bytes().contains(&0), "value shouldn't contain NUL");
103 dst.reserve(k.as_bytes().len() + 1 + v.as_bytes().len());
140 dst.reserve(k.as_bytes().len() + 1 + v.as_bytes().len());
104 dst.put_slice(k.as_bytes());
141 dst.put_slice(k.as_bytes());
105 dst.put_u8(b'=');
142 dst.put_u8(b'=');
106 dst.put_slice(v.as_bytes());
143 dst.put_slice(v.as_bytes());
107 }
144 }
108
145
109 fn decode_latin1<S>(s: S) -> String
146 fn decode_latin1<S>(s: S) -> String
110 where
147 where
111 S: AsRef<[u8]>,
148 S: AsRef<[u8]>,
112 {
149 {
113 s.as_ref().iter().map(|&c| c as char).collect()
150 s.as_ref().iter().map(|&c| c as char).collect()
114 }
151 }
115
152
116 fn new_parse_error<E>(error: E) -> io::Error
153 fn new_parse_error<E>(error: E) -> io::Error
117 where
154 where
118 E: Into<Box<error::Error + Send + Sync>>,
155 E: Into<Box<error::Error + Send + Sync>>,
119 {
156 {
120 io::Error::new(io::ErrorKind::InvalidData, error)
157 io::Error::new(io::ErrorKind::InvalidData, error)
121 }
158 }
122
159
123 #[cfg(test)]
160 #[cfg(test)]
124 mod tests {
161 mod tests {
125 use super::*;
162 use super::*;
126 use std::os::unix::ffi::OsStringExt;
163 use std::os::unix::ffi::OsStringExt;
127 use std::panic;
164 use std::panic;
128
165
129 #[test]
166 #[test]
130 fn parse_command_spec_good() {
167 fn parse_command_spec_good() {
131 let src = [
168 let src = [
132 b"pager".as_ref(),
169 b"pager".as_ref(),
133 b"less -FRX".as_ref(),
170 b"less -FRX".as_ref(),
134 b"/tmp".as_ref(),
171 b"/tmp".as_ref(),
135 b"LANG=C".as_ref(),
172 b"LANG=C".as_ref(),
136 b"HGPLAIN=".as_ref(),
173 b"HGPLAIN=".as_ref(),
137 ]
174 ]
138 .join(&0);
175 .join(&0);
139 let spec = CommandSpec {
176 let spec = CommandSpec {
140 command: os_string_from(b"less -FRX"),
177 command: os_string_from(b"less -FRX"),
141 current_dir: os_string_from(b"/tmp"),
178 current_dir: os_string_from(b"/tmp"),
142 envs: vec![
179 envs: vec![
143 (os_string_from(b"LANG"), os_string_from(b"C")),
180 (os_string_from(b"LANG"), os_string_from(b"C")),
144 (os_string_from(b"HGPLAIN"), os_string_from(b"")),
181 (os_string_from(b"HGPLAIN"), os_string_from(b"")),
145 ],
182 ],
146 };
183 };
147 assert_eq!(
184 assert_eq!(
148 parse_command_spec(Bytes::from(src)).unwrap(),
185 parse_command_spec(Bytes::from(src)).unwrap(),
149 (CommandType::Pager, spec)
186 (CommandType::Pager, spec)
150 );
187 );
151 }
188 }
152
189
153 #[test]
190 #[test]
154 fn parse_command_spec_too_short() {
191 fn parse_command_spec_too_short() {
155 assert!(parse_command_spec(Bytes::from_static(b"")).is_err());
192 assert!(parse_command_spec(Bytes::from_static(b"")).is_err());
156 assert!(parse_command_spec(Bytes::from_static(b"pager")).is_err());
193 assert!(parse_command_spec(Bytes::from_static(b"pager")).is_err());
157 assert!(parse_command_spec(Bytes::from_static(b"pager\0less")).is_err());
194 assert!(parse_command_spec(Bytes::from_static(b"pager\0less")).is_err());
158 }
195 }
159
196
160 #[test]
197 #[test]
161 fn parse_command_spec_malformed_env() {
198 fn parse_command_spec_malformed_env() {
162 assert!(parse_command_spec(Bytes::from_static(b"pager\0less\0/tmp\0HOME")).is_err());
199 assert!(parse_command_spec(Bytes::from_static(b"pager\0less\0/tmp\0HOME")).is_err());
163 }
200 }
164
201
165 #[test]
202 #[test]
166 fn parse_command_spec_unknown_type() {
203 fn parse_command_spec_unknown_type() {
167 assert!(parse_command_spec(Bytes::from_static(b"paper\0less")).is_err());
204 assert!(parse_command_spec(Bytes::from_static(b"paper\0less")).is_err());
168 }
205 }
169
206
170 #[test]
207 #[test]
208 fn parse_instructions_good() {
209 let src = [
210 b"exit 123".as_ref(),
211 b"reconnect".as_ref(),
212 b"redirect /whatever".as_ref(),
213 b"unlink /someother".as_ref(),
214 ]
215 .join(&0);
216 let insts = vec![
217 Instruction::Exit(123),
218 Instruction::Reconnect,
219 Instruction::Redirect(path_buf_from(b"/whatever")),
220 Instruction::Unlink(path_buf_from(b"/someother")),
221 ];
222 assert_eq!(parse_instructions(Bytes::from(src)).unwrap(), insts);
223 }
224
225 #[test]
226 fn parse_instructions_empty() {
227 assert_eq!(parse_instructions(Bytes::new()).unwrap(), vec![]);
228 assert_eq!(
229 parse_instructions(Bytes::from_static(b"\0")).unwrap(),
230 vec![]
231 );
232 }
233
234 #[test]
235 fn parse_instructions_malformed_exit_code() {
236 assert!(parse_instructions(Bytes::from_static(b"exit foo")).is_err());
237 }
238
239 #[test]
240 fn parse_instructions_missing_argument() {
241 assert!(parse_instructions(Bytes::from_static(b"exit")).is_err());
242 assert!(parse_instructions(Bytes::from_static(b"redirect")).is_err());
243 assert!(parse_instructions(Bytes::from_static(b"unlink")).is_err());
244 }
245
246 #[test]
247 fn parse_instructions_unknown_command() {
248 assert!(parse_instructions(Bytes::from_static(b"quit 0")).is_err());
249 }
250
251 #[test]
171 fn pack_env_vars_os_good() {
252 fn pack_env_vars_os_good() {
172 assert_eq!(
253 assert_eq!(
173 pack_env_vars_os(vec![] as Vec<(OsString, OsString)>),
254 pack_env_vars_os(vec![] as Vec<(OsString, OsString)>),
174 Bytes::new()
255 Bytes::new()
175 );
256 );
176 assert_eq!(
257 assert_eq!(
177 pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"bar")]),
258 pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"bar")]),
178 Bytes::from_static(b"FOO=bar")
259 Bytes::from_static(b"FOO=bar")
179 );
260 );
180 assert_eq!(
261 assert_eq!(
181 pack_env_vars_os(vec![
262 pack_env_vars_os(vec![
182 os_string_pair_from(b"FOO", b""),
263 os_string_pair_from(b"FOO", b""),
183 os_string_pair_from(b"BAR", b"baz")
264 os_string_pair_from(b"BAR", b"baz")
184 ]),
265 ]),
185 Bytes::from_static(b"FOO=\0BAR=baz")
266 Bytes::from_static(b"FOO=\0BAR=baz")
186 );
267 );
187 }
268 }
188
269
189 #[test]
270 #[test]
190 fn pack_env_vars_os_large_key() {
271 fn pack_env_vars_os_large_key() {
191 let mut buf = vec![b'A'; INITIAL_PACKED_ENV_VARS_CAPACITY];
272 let mut buf = vec![b'A'; INITIAL_PACKED_ENV_VARS_CAPACITY];
192 let envs = vec![os_string_pair_from(&buf, b"")];
273 let envs = vec![os_string_pair_from(&buf, b"")];
193 buf.push(b'=');
274 buf.push(b'=');
194 assert_eq!(pack_env_vars_os(envs), Bytes::from(buf));
275 assert_eq!(pack_env_vars_os(envs), Bytes::from(buf));
195 }
276 }
196
277
197 #[test]
278 #[test]
198 fn pack_env_vars_os_large_value() {
279 fn pack_env_vars_os_large_value() {
199 let mut buf = vec![b'A', b'='];
280 let mut buf = vec![b'A', b'='];
200 buf.resize(INITIAL_PACKED_ENV_VARS_CAPACITY + 1, b'a');
281 buf.resize(INITIAL_PACKED_ENV_VARS_CAPACITY + 1, b'a');
201 let envs = vec![os_string_pair_from(&buf[..1], &buf[2..])];
282 let envs = vec![os_string_pair_from(&buf[..1], &buf[2..])];
202 assert_eq!(pack_env_vars_os(envs), Bytes::from(buf));
283 assert_eq!(pack_env_vars_os(envs), Bytes::from(buf));
203 }
284 }
204
285
205 #[test]
286 #[test]
206 fn pack_env_vars_os_nul_eq() {
287 fn pack_env_vars_os_nul_eq() {
207 assert!(panic::catch_unwind(|| {
288 assert!(panic::catch_unwind(|| {
208 pack_env_vars_os(vec![os_string_pair_from(b"\0", b"")])
289 pack_env_vars_os(vec![os_string_pair_from(b"\0", b"")])
209 })
290 })
210 .is_err());
291 .is_err());
211 assert!(panic::catch_unwind(|| {
292 assert!(panic::catch_unwind(|| {
212 pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"\0bar")])
293 pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"\0bar")])
213 })
294 })
214 .is_err());
295 .is_err());
215 assert!(panic::catch_unwind(|| {
296 assert!(panic::catch_unwind(|| {
216 pack_env_vars_os(vec![os_string_pair_from(b"FO=", b"bar")])
297 pack_env_vars_os(vec![os_string_pair_from(b"FO=", b"bar")])
217 })
298 })
218 .is_err());
299 .is_err());
219 assert_eq!(
300 assert_eq!(
220 pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"=ba")]),
301 pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"=ba")]),
221 Bytes::from_static(b"FOO==ba")
302 Bytes::from_static(b"FOO==ba")
222 );
303 );
223 }
304 }
224
305
225 fn os_string_from(s: &[u8]) -> OsString {
306 fn os_string_from(s: &[u8]) -> OsString {
226 OsString::from_vec(s.to_vec())
307 OsString::from_vec(s.to_vec())
227 }
308 }
228
309
229 fn os_string_pair_from(k: &[u8], v: &[u8]) -> (OsString, OsString) {
310 fn os_string_pair_from(k: &[u8], v: &[u8]) -> (OsString, OsString) {
230 (os_string_from(k), os_string_from(v))
311 (os_string_from(k), os_string_from(v))
231 }
312 }
313
314 fn path_buf_from(s: &[u8]) -> PathBuf {
315 os_string_from(s).into()
232 }
316 }
317 }
General Comments 0
You need to be logged in to leave comments. Login now