##// END OF EJS Templates
rust-chg: add config validation and process returned instructions...
Yuya Nishihara -
r45173:9ce613d6 default
parent child Browse files
Show More
@@ -1,386 +1,483
1 // Copyright 2011, 2018 Yuya Nishihara <yuya@tcha.org>
1 // Copyright 2011, 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 locating command-server process.
6 //! Utility for locating command-server process.
7
7
8 use futures::future::{self, Either, Loop};
8 use futures::future::{self, Either, Loop};
9 use std::env;
9 use std::env;
10 use std::ffi::{OsStr, OsString};
10 use std::ffi::{OsStr, OsString};
11 use std::fs::{self, DirBuilder};
11 use std::fs::{self, DirBuilder};
12 use std::io;
12 use std::io;
13 use std::os::unix::ffi::{OsStrExt, OsStringExt};
13 use std::os::unix::ffi::{OsStrExt, OsStringExt};
14 use std::os::unix::fs::{DirBuilderExt, MetadataExt};
14 use std::os::unix::fs::{DirBuilderExt, MetadataExt};
15 use std::path::{Path, PathBuf};
15 use std::path::{Path, PathBuf};
16 use std::process::{self, Command};
16 use std::process::{self, Command};
17 use std::time::Duration;
17 use std::time::Duration;
18 use tokio::prelude::*;
18 use tokio::prelude::*;
19 use tokio_hglib::UnixClient;
19 use tokio_hglib::UnixClient;
20 use tokio_process::{Child, CommandExt};
20 use tokio_process::{Child, CommandExt};
21 use tokio_timer;
21 use tokio_timer;
22
22
23 use super::clientext::ChgClientExt;
23 use super::clientext::ChgClientExt;
24 use super::message::ServerSpec;
24 use super::message::{Instruction, ServerSpec};
25 use super::procutil;
25 use super::procutil;
26
26
27 const REQUIRED_SERVER_CAPABILITIES: &[&str] = &["attachio", "chdir", "runcommand", "setenv"];
27 const REQUIRED_SERVER_CAPABILITIES: &[&str] =
28 &["attachio", "chdir", "runcommand", "setenv", "validate"];
28
29
29 /// Helper to connect to and spawn a server process.
30 /// Helper to connect to and spawn a server process.
30 #[derive(Clone, Debug)]
31 #[derive(Clone, Debug)]
31 pub struct Locator {
32 pub struct Locator {
32 hg_command: OsString,
33 hg_command: OsString,
33 hg_early_args: Vec<OsString>,
34 hg_early_args: Vec<OsString>,
34 current_dir: PathBuf,
35 current_dir: PathBuf,
35 env_vars: Vec<(OsString, OsString)>,
36 env_vars: Vec<(OsString, OsString)>,
36 process_id: u32,
37 process_id: u32,
37 base_sock_path: PathBuf,
38 base_sock_path: PathBuf,
39 redirect_sock_path: Option<PathBuf>,
38 timeout: Duration,
40 timeout: Duration,
39 }
41 }
40
42
41 impl Locator {
43 impl Locator {
42 /// Creates locator capturing the current process environment.
44 /// Creates locator capturing the current process environment.
43 ///
45 ///
44 /// If no `$CHGSOCKNAME` is specified, the socket directory will be
46 /// If no `$CHGSOCKNAME` is specified, the socket directory will be
45 /// created as necessary.
47 /// created as necessary.
46 pub fn prepare_from_env() -> io::Result<Locator> {
48 pub fn prepare_from_env() -> io::Result<Locator> {
47 Ok(Locator {
49 Ok(Locator {
48 hg_command: default_hg_command(),
50 hg_command: default_hg_command(),
49 hg_early_args: Vec::new(),
51 hg_early_args: Vec::new(),
50 current_dir: env::current_dir()?,
52 current_dir: env::current_dir()?,
51 env_vars: env::vars_os().collect(),
53 env_vars: env::vars_os().collect(),
52 process_id: process::id(),
54 process_id: process::id(),
53 base_sock_path: prepare_server_socket_path()?,
55 base_sock_path: prepare_server_socket_path()?,
56 redirect_sock_path: None,
54 timeout: default_timeout(),
57 timeout: default_timeout(),
55 })
58 })
56 }
59 }
57
60
58 /// Temporary socket path for this client process.
61 /// Temporary socket path for this client process.
59 fn temp_sock_path(&self) -> PathBuf {
62 fn temp_sock_path(&self) -> PathBuf {
60 let src = self.base_sock_path.as_os_str().as_bytes();
63 let src = self.base_sock_path.as_os_str().as_bytes();
61 let mut buf = Vec::with_capacity(src.len() + 6); // "{src}.{pid}".len()
64 let mut buf = Vec::with_capacity(src.len() + 6); // "{src}.{pid}".len()
62 buf.extend_from_slice(src);
65 buf.extend_from_slice(src);
63 buf.extend_from_slice(format!(".{}", self.process_id).as_bytes());
66 buf.extend_from_slice(format!(".{}", self.process_id).as_bytes());
64 OsString::from_vec(buf).into()
67 OsString::from_vec(buf).into()
65 }
68 }
66
69
67 /// Specifies the arguments to be passed to the server at start.
70 /// Specifies the arguments to be passed to the server at start.
68 pub fn set_early_args<I, P>(&mut self, args: I)
71 pub fn set_early_args<I, P>(&mut self, args: I)
69 where
72 where
70 I: IntoIterator<Item = P>,
73 I: IntoIterator<Item = P>,
71 P: AsRef<OsStr>,
74 P: AsRef<OsStr>,
72 {
75 {
73 self.hg_early_args = args.into_iter().map(|a| a.as_ref().to_owned()).collect();
76 self.hg_early_args = args.into_iter().map(|a| a.as_ref().to_owned()).collect();
74 }
77 }
75
78
76 /// Connects to the server.
79 /// Connects to the server.
77 ///
80 ///
78 /// The server process will be spawned if not running.
81 /// The server process will be spawned if not running.
79 pub fn connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
82 pub fn connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
80 self.try_connect()
83 future::loop_fn((self, 0), |(loc, cnt)| {
84 if cnt < 10 {
85 let fut = loc
86 .try_connect()
87 .and_then(|(loc, client)| {
88 client
89 .validate(&loc.hg_early_args)
90 .map(|(client, instructions)| (loc, client, instructions))
91 })
92 .and_then(move |(loc, client, instructions)| {
93 loc.run_instructions(client, instructions, cnt)
94 });
95 Either::A(fut)
96 } else {
97 let msg = format!(
98 concat!(
99 "too many redirections.\n",
100 "Please make sure {:?} is not a wrapper which ",
101 "changes sensitive environment variables ",
102 "before executing hg. If you have to use a ",
103 "wrapper, wrap chg instead of hg.",
104 ),
105 loc.hg_command
106 );
107 Either::B(future::err(io::Error::new(io::ErrorKind::Other, msg)))
108 }
109 })
110 }
111
112 /// Runs instructions received from the server.
113 fn run_instructions(
114 mut self,
115 client: UnixClient,
116 instructions: Vec<Instruction>,
117 cnt: usize,
118 ) -> io::Result<Loop<(Self, UnixClient), (Self, usize)>> {
119 let mut reconnect = false;
120 for inst in instructions {
121 debug!("instruction: {:?}", inst);
122 match inst {
123 Instruction::Exit(_) => {
124 // Just returns the current connection to run the
125 // unparsable command and report the error
126 return Ok(Loop::Break((self, client)));
127 }
128 Instruction::Reconnect => {
129 reconnect = true;
130 }
131 Instruction::Redirect(path) => {
132 if path.parent() != self.base_sock_path.parent() {
133 let msg = format!(
134 "insecure redirect instruction from server: {}",
135 path.display()
136 );
137 return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
138 }
139 self.redirect_sock_path = Some(path);
140 reconnect = true;
141 }
142 Instruction::Unlink(path) => {
143 if path.parent() != self.base_sock_path.parent() {
144 let msg = format!(
145 "insecure unlink instruction from server: {}",
146 path.display()
147 );
148 return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
149 }
150 fs::remove_file(path).unwrap_or(()); // may race
151 }
152 }
153 }
154
155 if reconnect {
156 Ok(Loop::Continue((self, cnt + 1)))
157 } else {
158 Ok(Loop::Break((self, client)))
159 }
81 }
160 }
82
161
83 /// Tries to connect to the existing server, or spawns new if not running.
162 /// Tries to connect to the existing server, or spawns new if not running.
84 fn try_connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
163 fn try_connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
85 debug!("try connect to {}", self.base_sock_path.display());
164 let sock_path = self
86 UnixClient::connect(self.base_sock_path.clone())
165 .redirect_sock_path
87 .then(|res| match res {
166 .as_ref()
88 Ok(client) => Either::A(future::ok((self, client))),
167 .unwrap_or(&self.base_sock_path)
89 Err(_) => Either::B(self.spawn_connect()),
168 .clone();
169 debug!("try connect to {}", sock_path.display());
170 UnixClient::connect(sock_path)
171 .then(|res| {
172 match res {
173 Ok(client) => Either::A(future::ok((self, client))),
174 Err(_) => {
175 // Prevent us from being re-connected to the outdated
176 // master server: We were told by the server to redirect
177 // to redirect_sock_path, which didn't work. We do not
178 // want to connect to the same master server again
179 // because it would probably tell us the same thing.
180 if self.redirect_sock_path.is_some() {
181 fs::remove_file(&self.base_sock_path).unwrap_or(());
182 // may race
183 }
184 Either::B(self.spawn_connect())
185 }
186 }
90 })
187 })
91 .and_then(|(loc, client)| {
188 .and_then(|(loc, client)| {
92 check_server_capabilities(client.server_spec())?;
189 check_server_capabilities(client.server_spec())?;
93 Ok((loc, client))
190 Ok((loc, client))
94 })
191 })
95 .and_then(|(loc, client)| {
192 .and_then(|(loc, client)| {
96 client
193 client
97 .set_current_dir(&loc.current_dir)
194 .set_current_dir(&loc.current_dir)
98 .map(|client| (loc, client))
195 .map(|client| (loc, client))
99 })
196 })
100 .and_then(|(loc, client)| {
197 .and_then(|(loc, client)| {
101 client
198 client
102 .set_env_vars_os(loc.env_vars.iter().cloned())
199 .set_env_vars_os(loc.env_vars.iter().cloned())
103 .map(|client| (loc, client))
200 .map(|client| (loc, client))
104 })
201 })
105 }
202 }
106
203
107 /// Spawns new server process and connects to it.
204 /// Spawns new server process and connects to it.
108 ///
205 ///
109 /// The server will be spawned at the current working directory, then
206 /// The server will be spawned at the current working directory, then
110 /// chdir to "/", so that the server will load configs from the target
207 /// chdir to "/", so that the server will load configs from the target
111 /// repository.
208 /// repository.
112 fn spawn_connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
209 fn spawn_connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
113 let sock_path = self.temp_sock_path();
210 let sock_path = self.temp_sock_path();
114 debug!("start cmdserver at {}", sock_path.display());
211 debug!("start cmdserver at {}", sock_path.display());
115 Command::new(&self.hg_command)
212 Command::new(&self.hg_command)
116 .arg("serve")
213 .arg("serve")
117 .arg("--cmdserver")
214 .arg("--cmdserver")
118 .arg("chgunix")
215 .arg("chgunix")
119 .arg("--address")
216 .arg("--address")
120 .arg(&sock_path)
217 .arg(&sock_path)
121 .arg("--daemon-postexec")
218 .arg("--daemon-postexec")
122 .arg("chdir:/")
219 .arg("chdir:/")
123 .args(&self.hg_early_args)
220 .args(&self.hg_early_args)
124 .current_dir(&self.current_dir)
221 .current_dir(&self.current_dir)
125 .env_clear()
222 .env_clear()
126 .envs(self.env_vars.iter().cloned())
223 .envs(self.env_vars.iter().cloned())
127 .env("CHGINTERNALMARK", "")
224 .env("CHGINTERNALMARK", "")
128 .spawn_async()
225 .spawn_async()
129 .into_future()
226 .into_future()
130 .and_then(|server| self.connect_spawned(server, sock_path))
227 .and_then(|server| self.connect_spawned(server, sock_path))
131 .and_then(|(loc, client, sock_path)| {
228 .and_then(|(loc, client, sock_path)| {
132 debug!(
229 debug!(
133 "rename {} to {}",
230 "rename {} to {}",
134 sock_path.display(),
231 sock_path.display(),
135 loc.base_sock_path.display()
232 loc.base_sock_path.display()
136 );
233 );
137 fs::rename(&sock_path, &loc.base_sock_path)?;
234 fs::rename(&sock_path, &loc.base_sock_path)?;
138 Ok((loc, client))
235 Ok((loc, client))
139 })
236 })
140 }
237 }
141
238
142 /// Tries to connect to the just spawned server repeatedly until timeout
239 /// Tries to connect to the just spawned server repeatedly until timeout
143 /// exceeded.
240 /// exceeded.
144 fn connect_spawned(
241 fn connect_spawned(
145 self,
242 self,
146 server: Child,
243 server: Child,
147 sock_path: PathBuf,
244 sock_path: PathBuf,
148 ) -> impl Future<Item = (Self, UnixClient, PathBuf), Error = io::Error> {
245 ) -> impl Future<Item = (Self, UnixClient, PathBuf), Error = io::Error> {
149 debug!("try connect to {} repeatedly", sock_path.display());
246 debug!("try connect to {} repeatedly", sock_path.display());
150 let connect = future::loop_fn(sock_path, |sock_path| {
247 let connect = future::loop_fn(sock_path, |sock_path| {
151 UnixClient::connect(sock_path.clone()).then(|res| {
248 UnixClient::connect(sock_path.clone()).then(|res| {
152 match res {
249 match res {
153 Ok(client) => Either::A(future::ok(Loop::Break((client, sock_path)))),
250 Ok(client) => Either::A(future::ok(Loop::Break((client, sock_path)))),
154 Err(_) => {
251 Err(_) => {
155 // try again with slight delay
252 // try again with slight delay
156 let fut = tokio_timer::sleep(Duration::from_millis(10))
253 let fut = tokio_timer::sleep(Duration::from_millis(10))
157 .map(|()| Loop::Continue(sock_path))
254 .map(|()| Loop::Continue(sock_path))
158 .map_err(|err| io::Error::new(io::ErrorKind::Other, err));
255 .map_err(|err| io::Error::new(io::ErrorKind::Other, err));
159 Either::B(fut)
256 Either::B(fut)
160 }
257 }
161 }
258 }
162 })
259 })
163 });
260 });
164
261
165 // waits for either connection established or server failed to start
262 // waits for either connection established or server failed to start
166 connect
263 connect
167 .select2(server)
264 .select2(server)
168 .map_err(|res| res.split().0)
265 .map_err(|res| res.split().0)
169 .timeout(self.timeout)
266 .timeout(self.timeout)
170 .map_err(|err| {
267 .map_err(|err| {
171 err.into_inner().unwrap_or_else(|| {
268 err.into_inner().unwrap_or_else(|| {
172 io::Error::new(
269 io::Error::new(
173 io::ErrorKind::TimedOut,
270 io::ErrorKind::TimedOut,
174 "timed out while connecting to server",
271 "timed out while connecting to server",
175 )
272 )
176 })
273 })
177 })
274 })
178 .and_then(|res| {
275 .and_then(|res| {
179 match res {
276 match res {
180 Either::A(((client, sock_path), server)) => {
277 Either::A(((client, sock_path), server)) => {
181 server.forget(); // continue to run in background
278 server.forget(); // continue to run in background
182 Ok((self, client, sock_path))
279 Ok((self, client, sock_path))
183 }
280 }
184 Either::B((st, _)) => Err(io::Error::new(
281 Either::B((st, _)) => Err(io::Error::new(
185 io::ErrorKind::Other,
282 io::ErrorKind::Other,
186 format!("server exited too early: {}", st),
283 format!("server exited too early: {}", st),
187 )),
284 )),
188 }
285 }
189 })
286 })
190 }
287 }
191 }
288 }
192
289
193 /// Determines the server socket to connect to.
290 /// Determines the server socket to connect to.
194 ///
291 ///
195 /// If no `$CHGSOCKNAME` is specified, the socket directory will be created
292 /// If no `$CHGSOCKNAME` is specified, the socket directory will be created
196 /// as necessary.
293 /// as necessary.
197 fn prepare_server_socket_path() -> io::Result<PathBuf> {
294 fn prepare_server_socket_path() -> io::Result<PathBuf> {
198 if let Some(s) = env::var_os("CHGSOCKNAME") {
295 if let Some(s) = env::var_os("CHGSOCKNAME") {
199 Ok(PathBuf::from(s))
296 Ok(PathBuf::from(s))
200 } else {
297 } else {
201 let mut path = default_server_socket_dir();
298 let mut path = default_server_socket_dir();
202 create_secure_dir(&path)?;
299 create_secure_dir(&path)?;
203 path.push("server");
300 path.push("server");
204 Ok(path)
301 Ok(path)
205 }
302 }
206 }
303 }
207
304
208 /// Determines the default server socket path as follows.
305 /// Determines the default server socket path as follows.
209 ///
306 ///
210 /// 1. `$XDG_RUNTIME_DIR/chg`
307 /// 1. `$XDG_RUNTIME_DIR/chg`
211 /// 2. `$TMPDIR/chg$UID`
308 /// 2. `$TMPDIR/chg$UID`
212 /// 3. `/tmp/chg$UID`
309 /// 3. `/tmp/chg$UID`
213 pub fn default_server_socket_dir() -> PathBuf {
310 pub fn default_server_socket_dir() -> PathBuf {
214 // XDG_RUNTIME_DIR should be ignored if it has an insufficient permission.
311 // XDG_RUNTIME_DIR should be ignored if it has an insufficient permission.
215 // https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
312 // https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
216 if let Some(Ok(s)) = env::var_os("XDG_RUNTIME_DIR").map(check_secure_dir) {
313 if let Some(Ok(s)) = env::var_os("XDG_RUNTIME_DIR").map(check_secure_dir) {
217 let mut path = PathBuf::from(s);
314 let mut path = PathBuf::from(s);
218 path.push("chg");
315 path.push("chg");
219 path
316 path
220 } else {
317 } else {
221 let mut path = env::temp_dir();
318 let mut path = env::temp_dir();
222 path.push(format!("chg{}", procutil::get_effective_uid()));
319 path.push(format!("chg{}", procutil::get_effective_uid()));
223 path
320 path
224 }
321 }
225 }
322 }
226
323
227 /// Determines the default hg command.
324 /// Determines the default hg command.
228 pub fn default_hg_command() -> OsString {
325 pub fn default_hg_command() -> OsString {
229 // TODO: maybe allow embedding the path at compile time (or load from hgrc)
326 // TODO: maybe allow embedding the path at compile time (or load from hgrc)
230 env::var_os("CHGHG")
327 env::var_os("CHGHG")
231 .or(env::var_os("HG"))
328 .or(env::var_os("HG"))
232 .unwrap_or(OsStr::new("hg").to_owned())
329 .unwrap_or(OsStr::new("hg").to_owned())
233 }
330 }
234
331
235 fn default_timeout() -> Duration {
332 fn default_timeout() -> Duration {
236 let secs = env::var("CHGTIMEOUT")
333 let secs = env::var("CHGTIMEOUT")
237 .ok()
334 .ok()
238 .and_then(|s| s.parse().ok())
335 .and_then(|s| s.parse().ok())
239 .unwrap_or(60);
336 .unwrap_or(60);
240 Duration::from_secs(secs)
337 Duration::from_secs(secs)
241 }
338 }
242
339
243 /// Creates a directory which the other users cannot access to.
340 /// Creates a directory which the other users cannot access to.
244 ///
341 ///
245 /// If the directory already exists, tests its permission.
342 /// If the directory already exists, tests its permission.
246 fn create_secure_dir<P>(path: P) -> io::Result<()>
343 fn create_secure_dir<P>(path: P) -> io::Result<()>
247 where
344 where
248 P: AsRef<Path>,
345 P: AsRef<Path>,
249 {
346 {
250 DirBuilder::new()
347 DirBuilder::new()
251 .mode(0o700)
348 .mode(0o700)
252 .create(path.as_ref())
349 .create(path.as_ref())
253 .or_else(|err| {
350 .or_else(|err| {
254 if err.kind() == io::ErrorKind::AlreadyExists {
351 if err.kind() == io::ErrorKind::AlreadyExists {
255 check_secure_dir(path).map(|_| ())
352 check_secure_dir(path).map(|_| ())
256 } else {
353 } else {
257 Err(err)
354 Err(err)
258 }
355 }
259 })
356 })
260 }
357 }
261
358
262 fn check_secure_dir<P>(path: P) -> io::Result<P>
359 fn check_secure_dir<P>(path: P) -> io::Result<P>
263 where
360 where
264 P: AsRef<Path>,
361 P: AsRef<Path>,
265 {
362 {
266 let a = fs::symlink_metadata(path.as_ref())?;
363 let a = fs::symlink_metadata(path.as_ref())?;
267 if a.is_dir() && a.uid() == procutil::get_effective_uid() && (a.mode() & 0o777) == 0o700 {
364 if a.is_dir() && a.uid() == procutil::get_effective_uid() && (a.mode() & 0o777) == 0o700 {
268 Ok(path)
365 Ok(path)
269 } else {
366 } else {
270 Err(io::Error::new(io::ErrorKind::Other, "insecure directory"))
367 Err(io::Error::new(io::ErrorKind::Other, "insecure directory"))
271 }
368 }
272 }
369 }
273
370
274 fn check_server_capabilities(spec: &ServerSpec) -> io::Result<()> {
371 fn check_server_capabilities(spec: &ServerSpec) -> io::Result<()> {
275 let unsupported: Vec<_> = REQUIRED_SERVER_CAPABILITIES
372 let unsupported: Vec<_> = REQUIRED_SERVER_CAPABILITIES
276 .iter()
373 .iter()
277 .cloned()
374 .cloned()
278 .filter(|&s| !spec.capabilities.contains(s))
375 .filter(|&s| !spec.capabilities.contains(s))
279 .collect();
376 .collect();
280 if unsupported.is_empty() {
377 if unsupported.is_empty() {
281 Ok(())
378 Ok(())
282 } else {
379 } else {
283 let msg = format!(
380 let msg = format!(
284 "insufficient server capabilities: {}",
381 "insufficient server capabilities: {}",
285 unsupported.join(", ")
382 unsupported.join(", ")
286 );
383 );
287 Err(io::Error::new(io::ErrorKind::Other, msg))
384 Err(io::Error::new(io::ErrorKind::Other, msg))
288 }
385 }
289 }
386 }
290
387
291 /// Collects arguments which need to be passed to the server at start.
388 /// Collects arguments which need to be passed to the server at start.
292 pub fn collect_early_args<I, P>(args: I) -> Vec<OsString>
389 pub fn collect_early_args<I, P>(args: I) -> Vec<OsString>
293 where
390 where
294 I: IntoIterator<Item = P>,
391 I: IntoIterator<Item = P>,
295 P: AsRef<OsStr>,
392 P: AsRef<OsStr>,
296 {
393 {
297 let mut args_iter = args.into_iter();
394 let mut args_iter = args.into_iter();
298 let mut early_args = Vec::new();
395 let mut early_args = Vec::new();
299 while let Some(arg) = args_iter.next() {
396 while let Some(arg) = args_iter.next() {
300 let argb = arg.as_ref().as_bytes();
397 let argb = arg.as_ref().as_bytes();
301 if argb == b"--" {
398 if argb == b"--" {
302 break;
399 break;
303 } else if argb.starts_with(b"--") {
400 } else if argb.starts_with(b"--") {
304 let mut split = argb[2..].splitn(2, |&c| c == b'=');
401 let mut split = argb[2..].splitn(2, |&c| c == b'=');
305 match split.next().unwrap() {
402 match split.next().unwrap() {
306 b"traceback" => {
403 b"traceback" => {
307 if split.next().is_none() {
404 if split.next().is_none() {
308 early_args.push(arg.as_ref().to_owned());
405 early_args.push(arg.as_ref().to_owned());
309 }
406 }
310 }
407 }
311 b"config" | b"cwd" | b"repo" | b"repository" => {
408 b"config" | b"cwd" | b"repo" | b"repository" => {
312 if split.next().is_some() {
409 if split.next().is_some() {
313 // --<flag>=<val>
410 // --<flag>=<val>
314 early_args.push(arg.as_ref().to_owned());
411 early_args.push(arg.as_ref().to_owned());
315 } else {
412 } else {
316 // --<flag> <val>
413 // --<flag> <val>
317 args_iter.next().map(|val| {
414 args_iter.next().map(|val| {
318 early_args.push(arg.as_ref().to_owned());
415 early_args.push(arg.as_ref().to_owned());
319 early_args.push(val.as_ref().to_owned());
416 early_args.push(val.as_ref().to_owned());
320 });
417 });
321 }
418 }
322 }
419 }
323 _ => {}
420 _ => {}
324 }
421 }
325 } else if argb.starts_with(b"-R") {
422 } else if argb.starts_with(b"-R") {
326 if argb.len() > 2 {
423 if argb.len() > 2 {
327 // -R<val>
424 // -R<val>
328 early_args.push(arg.as_ref().to_owned());
425 early_args.push(arg.as_ref().to_owned());
329 } else {
426 } else {
330 // -R <val>
427 // -R <val>
331 args_iter.next().map(|val| {
428 args_iter.next().map(|val| {
332 early_args.push(arg.as_ref().to_owned());
429 early_args.push(arg.as_ref().to_owned());
333 early_args.push(val.as_ref().to_owned());
430 early_args.push(val.as_ref().to_owned());
334 });
431 });
335 }
432 }
336 }
433 }
337 }
434 }
338
435
339 early_args
436 early_args
340 }
437 }
341
438
342 #[cfg(test)]
439 #[cfg(test)]
343 mod tests {
440 mod tests {
344 use super::*;
441 use super::*;
345
442
346 #[test]
443 #[test]
347 fn collect_early_args_some() {
444 fn collect_early_args_some() {
348 assert!(collect_early_args(&[] as &[&OsStr]).is_empty());
445 assert!(collect_early_args(&[] as &[&OsStr]).is_empty());
349 assert!(collect_early_args(&["log"]).is_empty());
446 assert!(collect_early_args(&["log"]).is_empty());
350 assert_eq!(
447 assert_eq!(
351 collect_early_args(&["log", "-Ra", "foo"]),
448 collect_early_args(&["log", "-Ra", "foo"]),
352 os_string_vec_from(&[b"-Ra"])
449 os_string_vec_from(&[b"-Ra"])
353 );
450 );
354 assert_eq!(
451 assert_eq!(
355 collect_early_args(&["log", "-R", "repo", "", "--traceback", "a"]),
452 collect_early_args(&["log", "-R", "repo", "", "--traceback", "a"]),
356 os_string_vec_from(&[b"-R", b"repo", b"--traceback"])
453 os_string_vec_from(&[b"-R", b"repo", b"--traceback"])
357 );
454 );
358 assert_eq!(
455 assert_eq!(
359 collect_early_args(&["log", "--config", "diff.git=1", "-q"]),
456 collect_early_args(&["log", "--config", "diff.git=1", "-q"]),
360 os_string_vec_from(&[b"--config", b"diff.git=1"])
457 os_string_vec_from(&[b"--config", b"diff.git=1"])
361 );
458 );
362 assert_eq!(
459 assert_eq!(
363 collect_early_args(&["--cwd=..", "--repository", "r", "log"]),
460 collect_early_args(&["--cwd=..", "--repository", "r", "log"]),
364 os_string_vec_from(&[b"--cwd=..", b"--repository", b"r"])
461 os_string_vec_from(&[b"--cwd=..", b"--repository", b"r"])
365 );
462 );
366 assert_eq!(
463 assert_eq!(
367 collect_early_args(&["log", "--repo=r", "--repos", "a"]),
464 collect_early_args(&["log", "--repo=r", "--repos", "a"]),
368 os_string_vec_from(&[b"--repo=r"])
465 os_string_vec_from(&[b"--repo=r"])
369 );
466 );
370 }
467 }
371
468
372 #[test]
469 #[test]
373 fn collect_early_args_orphaned() {
470 fn collect_early_args_orphaned() {
374 assert!(collect_early_args(&["log", "-R"]).is_empty());
471 assert!(collect_early_args(&["log", "-R"]).is_empty());
375 assert!(collect_early_args(&["log", "--config"]).is_empty());
472 assert!(collect_early_args(&["log", "--config"]).is_empty());
376 }
473 }
377
474
378 #[test]
475 #[test]
379 fn collect_early_args_unwanted_value() {
476 fn collect_early_args_unwanted_value() {
380 assert!(collect_early_args(&["log", "--traceback="]).is_empty());
477 assert!(collect_early_args(&["log", "--traceback="]).is_empty());
381 }
478 }
382
479
383 fn os_string_vec_from(v: &[&[u8]]) -> Vec<OsString> {
480 fn os_string_vec_from(v: &[&[u8]]) -> Vec<OsString> {
384 v.iter().map(|s| OsStr::from_bytes(s).to_owned()).collect()
481 v.iter().map(|s| OsStr::from_bytes(s).to_owned()).collect()
385 }
482 }
386 }
483 }
General Comments 0
You need to be logged in to leave comments. Login now