qubit_command/command.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9use std::{
10 ffi::{
11 OsStr,
12 OsString,
13 },
14 path::{
15 Path,
16 PathBuf,
17 },
18};
19
20use crate::command_stdin::CommandStdin;
21
22#[cfg(windows)]
23use std::os::windows::ffi::OsStrExt;
24
25#[cfg(windows)]
26const CSTR_EQUAL: i32 = 2;
27
28#[cfg(windows)]
29#[link(name = "kernel32")]
30unsafe extern "system" {
31 #[link_name = "CompareStringOrdinal"]
32 fn compare_string_ordinal(
33 left: *const u16,
34 left_len: i32,
35 right: *const u16,
36 right_len: i32,
37 ignore_case: i32,
38 ) -> i32;
39}
40
41/// Structured description of an external command to run.
42///
43/// `Command` stores a program and argument vector instead of parsing a
44/// shell-like command line. This avoids quoting ambiguity and accidental shell
45/// injection. Use [`Self::shell`] only when shell parsing, redirection,
46/// expansion, or pipes are intentionally required.
47///
48/// # Author
49///
50/// Haixing Hu
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Command {
53 /// Program executable name or path.
54 program: OsString,
55 /// Positional arguments passed to the program.
56 args: Vec<OsString>,
57 /// Working directory override for this command.
58 working_directory: Option<PathBuf>,
59 /// Whether the command should clear inherited environment variables.
60 clear_environment: bool,
61 /// Environment variables added or overridden for this command.
62 envs: Vec<(OsString, OsString)>,
63 /// Environment variables removed for this command.
64 removed_envs: Vec<OsString>,
65 /// Standard input configuration for this command.
66 stdin: CommandStdin,
67}
68
69impl Command {
70 /// Creates a command from a program name or path.
71 ///
72 /// # Parameters
73 ///
74 /// * `program` - Executable name or path to run.
75 ///
76 /// # Returns
77 ///
78 /// A command with no arguments or per-command overrides.
79 #[inline]
80 pub fn new(program: &str) -> Self {
81 Self::new_os(program)
82 }
83
84 /// Creates a command from a program name or path that may not be UTF-8.
85 ///
86 /// # Parameters
87 ///
88 /// * `program` - Executable name or path to run.
89 ///
90 /// # Returns
91 ///
92 /// A command with no arguments or per-command overrides.
93 #[inline]
94 pub fn new_os<S>(program: S) -> Self
95 where
96 S: AsRef<OsStr>,
97 {
98 Self {
99 program: program.as_ref().to_owned(),
100 args: Vec::new(),
101 working_directory: None,
102 clear_environment: false,
103 envs: Vec::new(),
104 removed_envs: Vec::new(),
105 stdin: CommandStdin::Null,
106 }
107 }
108
109 /// Creates a command executed through the platform shell.
110 ///
111 /// On Unix-like platforms this creates `sh -c <command_line>`. On Windows
112 /// this creates `cmd /C <command_line>`. Prefer [`Self::new`] with explicit
113 /// arguments when shell parsing is not required.
114 ///
115 /// # Parameters
116 ///
117 /// * `command_line` - Shell command line to execute.
118 ///
119 /// # Returns
120 ///
121 /// A command that invokes the platform shell.
122 #[cfg(not(windows))]
123 #[inline]
124 pub fn shell(command_line: &str) -> Self {
125 Self::new("sh").arg("-c").arg(command_line)
126 }
127
128 /// Creates a command executed through the platform shell.
129 ///
130 /// On Windows this creates `cmd /C <command_line>`. Prefer [`Self::new`]
131 /// with explicit arguments when shell parsing is not required.
132 ///
133 /// # Parameters
134 ///
135 /// * `command_line` - Shell command line to execute.
136 ///
137 /// # Returns
138 ///
139 /// A command that invokes the platform shell.
140 #[cfg(windows)]
141 #[inline]
142 pub fn shell(command_line: &str) -> Self {
143 Self::new("cmd").arg("/C").arg(command_line)
144 }
145
146 /// Adds one positional argument.
147 ///
148 /// # Parameters
149 ///
150 /// * `arg` - Argument to append.
151 ///
152 /// # Returns
153 ///
154 /// The updated command.
155 #[inline]
156 pub fn arg(mut self, arg: &str) -> Self {
157 self.args.push(OsString::from(arg));
158 self
159 }
160
161 /// Adds one positional argument that may not be UTF-8.
162 ///
163 /// # Parameters
164 ///
165 /// * `arg` - Argument to append.
166 ///
167 /// # Returns
168 ///
169 /// The updated command.
170 #[inline]
171 pub fn arg_os<S>(mut self, arg: S) -> Self
172 where
173 S: AsRef<OsStr>,
174 {
175 self.args.push(arg.as_ref().to_owned());
176 self
177 }
178
179 /// Adds multiple positional arguments.
180 ///
181 /// # Parameters
182 ///
183 /// * `args` - Arguments to append in order.
184 ///
185 /// # Returns
186 ///
187 /// The updated command.
188 #[inline]
189 pub fn args(mut self, args: &[&str]) -> Self {
190 self.args.extend(args.iter().map(OsString::from));
191 self
192 }
193
194 /// Adds multiple positional arguments that may not be UTF-8.
195 ///
196 /// # Parameters
197 ///
198 /// * `args` - Arguments to append in order.
199 ///
200 /// # Returns
201 ///
202 /// The updated command.
203 pub fn args_os<I, S>(mut self, args: I) -> Self
204 where
205 I: IntoIterator<Item = S>,
206 S: AsRef<OsStr>,
207 {
208 self.args
209 .extend(args.into_iter().map(|arg| arg.as_ref().to_owned()));
210 self
211 }
212
213 /// Sets a per-command working directory.
214 ///
215 /// # Parameters
216 ///
217 /// * `working_directory` - Directory used as the child process working
218 /// directory.
219 ///
220 /// # Returns
221 ///
222 /// The updated command.
223 #[inline]
224 pub fn working_directory<P>(mut self, working_directory: P) -> Self
225 where
226 P: Into<PathBuf>,
227 {
228 self.working_directory = Some(working_directory.into());
229 self
230 }
231
232 /// Adds or overrides an environment variable for this command.
233 ///
234 /// # Parameters
235 ///
236 /// * `key` - Environment variable name.
237 /// * `value` - Environment variable value.
238 ///
239 /// # Returns
240 ///
241 /// The updated command.
242 #[inline]
243 pub fn env(mut self, key: &str, value: &str) -> Self {
244 self = self.env_os(key, value);
245 self
246 }
247
248 /// Adds or overrides an environment variable that may not be UTF-8.
249 ///
250 /// # Parameters
251 ///
252 /// * `key` - Environment variable name.
253 /// * `value` - Environment variable value.
254 ///
255 /// # Returns
256 ///
257 /// The updated command.
258 pub fn env_os<K, V>(mut self, key: K, value: V) -> Self
259 where
260 K: AsRef<OsStr>,
261 V: AsRef<OsStr>,
262 {
263 let key = key.as_ref().to_owned();
264 let value = value.as_ref().to_owned();
265 self.removed_envs
266 .retain(|removed| !env_key_eq(removed, &key));
267 self.envs
268 .retain(|(existing_key, _)| !env_key_eq(existing_key, &key));
269 self.envs.push((key, value));
270 self
271 }
272
273 /// Removes an inherited or previously configured environment variable.
274 ///
275 /// # Parameters
276 ///
277 /// * `key` - Environment variable name to remove.
278 ///
279 /// # Returns
280 ///
281 /// The updated command.
282 #[inline]
283 pub fn env_remove(mut self, key: &str) -> Self {
284 self = self.env_remove_os(key);
285 self
286 }
287
288 /// Removes an environment variable whose name may not be UTF-8.
289 ///
290 /// # Parameters
291 ///
292 /// * `key` - Environment variable name to remove.
293 ///
294 /// # Returns
295 ///
296 /// The updated command.
297 pub fn env_remove_os<S>(mut self, key: S) -> Self
298 where
299 S: AsRef<OsStr>,
300 {
301 let key = key.as_ref().to_owned();
302 self.envs
303 .retain(|(existing_key, _)| !env_key_eq(existing_key, &key));
304 self.removed_envs
305 .retain(|removed| !env_key_eq(removed, &key));
306 self.removed_envs.push(key);
307 self
308 }
309
310 /// Clears all inherited environment variables for this command.
311 ///
312 /// Environment variables added after this call are still passed to the child
313 /// process.
314 ///
315 /// # Returns
316 ///
317 /// The updated command.
318 pub fn env_clear(mut self) -> Self {
319 self.clear_environment = true;
320 self.envs.clear();
321 self.removed_envs.clear();
322 self
323 }
324
325 /// Connects the command stdin to null input.
326 ///
327 /// # Returns
328 ///
329 /// The updated command.
330 pub fn stdin_null(mut self) -> Self {
331 self.stdin = CommandStdin::Null;
332 self
333 }
334
335 /// Inherits stdin from the parent process.
336 ///
337 /// # Returns
338 ///
339 /// The updated command.
340 pub fn stdin_inherit(mut self) -> Self {
341 self.stdin = CommandStdin::Inherit;
342 self
343 }
344
345 /// Writes bytes to the child process stdin.
346 ///
347 /// The runner writes the bytes on a helper thread after spawning the child
348 /// process, then closes stdin so the child can observe EOF.
349 ///
350 /// # Parameters
351 ///
352 /// * `bytes` - Bytes to send to stdin.
353 ///
354 /// # Returns
355 ///
356 /// The updated command.
357 pub fn stdin_bytes<B>(mut self, bytes: B) -> Self
358 where
359 B: Into<Vec<u8>>,
360 {
361 self.stdin = CommandStdin::Bytes(bytes.into());
362 self
363 }
364
365 /// Reads child process stdin from a file.
366 ///
367 /// # Parameters
368 ///
369 /// * `path` - File path to open and connect to stdin.
370 ///
371 /// # Returns
372 ///
373 /// The updated command.
374 pub fn stdin_file<P>(mut self, path: P) -> Self
375 where
376 P: Into<PathBuf>,
377 {
378 self.stdin = CommandStdin::File(path.into());
379 self
380 }
381
382 /// Returns the executable name or path.
383 ///
384 /// # Returns
385 ///
386 /// Program executable name or path as an [`OsStr`].
387 #[inline]
388 pub fn program(&self) -> &OsStr {
389 &self.program
390 }
391
392 /// Returns the configured argument list.
393 ///
394 /// # Returns
395 ///
396 /// Borrowed argument list in submission order.
397 #[inline]
398 pub fn arguments(&self) -> &[OsString] {
399 &self.args
400 }
401
402 /// Returns the per-command working directory override.
403 ///
404 /// # Returns
405 ///
406 /// `Some(path)` when the command has a working directory override, or
407 /// `None` when the runner default should be used.
408 #[inline]
409 pub fn working_directory_override(&self) -> Option<&Path> {
410 self.working_directory.as_deref()
411 }
412
413 /// Returns environment variable overrides.
414 ///
415 /// # Returns
416 ///
417 /// Borrowed environment variable entries in insertion order.
418 #[inline]
419 pub fn environment(&self) -> &[(OsString, OsString)] {
420 &self.envs
421 }
422
423 /// Returns environment variable removals.
424 ///
425 /// # Returns
426 ///
427 /// Borrowed environment variable names removed before spawning the command.
428 #[inline]
429 pub fn removed_environment(&self) -> &[OsString] {
430 &self.removed_envs
431 }
432
433 /// Returns whether the inherited environment is cleared.
434 ///
435 /// # Returns
436 ///
437 /// `true` when the command should start from an empty environment.
438 #[inline]
439 pub const fn clears_environment(&self) -> bool {
440 self.clear_environment
441 }
442
443 /// Consumes the command and returns the configured stdin behavior.
444 ///
445 /// # Returns
446 ///
447 /// Owned stdin configuration used by the runner.
448 #[inline]
449 pub(crate) fn into_stdin_configuration(self) -> CommandStdin {
450 self.stdin
451 }
452
453 /// Formats this command for diagnostics.
454 ///
455 /// # Returns
456 ///
457 /// An argv-style command string suitable for logs and errors.
458 pub(crate) fn display_command(&self) -> String {
459 let mut parts = Vec::with_capacity(self.args.len() + 1);
460 parts.push(self.program.as_os_str());
461 for arg in &self.args {
462 parts.push(arg.as_os_str());
463 }
464 format!("{parts:?}")
465 }
466}
467
468/// Compares environment variable names using platform semantics.
469///
470/// # Parameters
471///
472/// * `left` - First environment variable name.
473/// * `right` - Second environment variable name.
474///
475/// # Returns
476///
477/// `true` when both names refer to the same environment entry on the current
478/// platform. Unix uses byte-preserving exact comparison; Windows uses
479/// case-insensitive comparison because Windows environment variable names are
480/// case-insensitive.
481#[cfg(not(windows))]
482fn env_key_eq(left: &OsStr, right: &OsStr) -> bool {
483 left == right
484}
485
486/// Compares environment variable names using Windows semantics.
487///
488/// # Parameters
489///
490/// * `left` - First environment variable name.
491/// * `right` - Second environment variable name.
492///
493/// # Returns
494///
495/// `true` when both names are equal according to Windows ordinal
496/// case-insensitive UTF-16 comparison.
497#[cfg(windows)]
498fn env_key_eq(left: &OsStr, right: &OsStr) -> bool {
499 let left = left.encode_wide().collect::<Vec<_>>();
500 let right = right.encode_wide().collect::<Vec<_>>();
501 let Ok(left_len) = i32::try_from(left.len()) else {
502 return false;
503 };
504 let Ok(right_len) = i32::try_from(right.len()) else {
505 return false;
506 };
507 // SAFETY: The pointers refer to the collected UTF-16 buffers and remain
508 // valid for the duration of the call. The lengths are checked above.
509 let comparison =
510 unsafe { compare_string_ordinal(left.as_ptr(), left_len, right.as_ptr(), right_len, 1) };
511 if comparison == 0 {
512 log::debug!(
513 "failed to compare Windows environment variable names; treating keys as distinct"
514 );
515 }
516 comparison == CSTR_EQUAL
517}