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