1use std::{
39 collections::{HashMap, HashSet},
40 ffi::OsString,
41 io,
42 num::NonZeroU32,
43 path::{Path, PathBuf},
44 time::Duration,
45};
46
47use super::{sweep, sys, AdoptedIdentity, SweepReport};
48
49#[derive(Clone, Debug, Default, Eq, PartialEq)]
52pub struct ProcessQuery {
53 exe: Option<PathBuf>,
54 args_exact: Vec<OsString>,
55 args_containing: Vec<String>,
56}
57
58impl ProcessQuery {
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 pub fn exe(mut self, path: impl Into<PathBuf>) -> Self {
65 self.exe = Some(path.into());
66 self
67 }
68
69 pub fn arg(mut self, token: impl Into<OsString>) -> Self {
72 self.args_exact.push(token.into());
73 self
74 }
75
76 pub fn arg_containing(mut self, needle: impl Into<String>) -> Self {
79 self.args_containing.push(needle.into());
80 self
81 }
82
83 fn validate(&self) -> io::Result<()> {
84 let invalid = |message: &'static str| io::Error::new(io::ErrorKind::InvalidInput, message);
85 if self.exe.is_none() && self.args_exact.is_empty() && self.args_containing.is_empty() {
86 return Err(invalid("a process query needs at least one criterion"));
87 }
88 if self
89 .exe
90 .as_ref()
91 .is_some_and(|path| path.as_os_str().is_empty())
92 {
93 return Err(invalid("the executable path of a process query is empty"));
94 }
95 if self.args_exact.iter().any(|token| token.is_empty()) {
96 return Err(invalid("an argument token of a process query is empty"));
97 }
98 if self.args_containing.iter().any(String::is_empty) {
99 return Err(invalid("an argument substring of a process query is empty"));
100 }
101 Ok(())
102 }
103
104 fn needs_args(&self) -> bool {
105 !self.args_exact.is_empty() || !self.args_containing.is_empty()
106 }
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct MatchedProcess {
112 identity: AdoptedIdentity,
113 args: Option<Vec<OsString>>,
114}
115
116impl MatchedProcess {
117 pub fn pid(&self) -> u32 {
118 self.identity.pid()
119 }
120
121 pub fn identity(&self) -> &AdoptedIdentity {
124 &self.identity
125 }
126
127 pub fn exe(&self) -> Option<&Path> {
129 self.identity.exe()
130 }
131
132 pub fn args(&self) -> Option<&[OsString]> {
135 self.args.as_deref()
136 }
137}
138
139#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct MatchSweepOptions {
142 grace: Duration,
143 dry_run: bool,
144}
145
146impl Default for MatchSweepOptions {
147 fn default() -> Self {
148 Self {
149 grace: Duration::from_secs(2),
150 dry_run: false,
151 }
152 }
153}
154
155impl MatchSweepOptions {
156 pub fn new() -> Self {
157 Self::default()
158 }
159
160 pub fn grace(mut self, grace: Duration) -> Self {
163 self.grace = grace;
164 self
165 }
166
167 pub fn dry_run(mut self, dry_run: bool) -> Self {
169 self.dry_run = dry_run;
170 self
171 }
172}
173
174#[derive(Clone, Debug, Eq, PartialEq)]
176pub struct MatchSweepReport {
177 pub matched: Vec<MatchedProcess>,
179 pub dry_run: bool,
181 pub sweep: SweepReport,
183}
184
185pub fn find_processes(query: &ProcessQuery) -> io::Result<Vec<MatchedProcess>> {
189 query.validate()?;
190 let wanted_exe = query.exe.as_deref().map(exe_candidates);
191 let me = os::Me::current()?;
192 let pairs = sys::process_pairs()?;
193 let this = std::process::id();
194 let excluded = ancestors(&pairs, this);
195 let mut found = Vec::new();
196 for &(pid, _) in &pairs {
197 if pid == 0 || pid == this || excluded.contains(&pid) {
198 continue;
199 }
200 if let Ok(Some(matched)) = inspect(query, wanted_exe.as_deref(), &me, pid) {
201 found.push(matched);
202 }
203 }
204 found.sort_by_key(|matched| std::cmp::Reverse(matched.identity.start_token()));
205 Ok(found)
206}
207
208pub fn sweep_matching(
211 query: &ProcessQuery,
212 options: &MatchSweepOptions,
213) -> io::Result<MatchSweepReport> {
214 let matched = find_processes(query)?;
215 if options.dry_run || matched.is_empty() {
216 return Ok(MatchSweepReport {
217 matched,
218 dry_run: options.dry_run,
219 sweep: SweepReport::default(),
220 });
221 }
222 let targets: Vec<AdoptedIdentity> =
223 matched.iter().map(|found| found.identity.clone()).collect();
224 let report = sweep(&targets, options.grace)?;
225 Ok(MatchSweepReport {
226 matched,
227 dry_run: false,
228 sweep: report,
229 })
230}
231
232fn ancestors(pairs: &[(u32, u32)], this: u32) -> HashSet<u32> {
235 let parents: HashMap<u32, u32> = pairs.iter().copied().collect();
236 let mut found = HashSet::new();
237 let mut current = this;
238 while let Some(&parent) = parents.get(¤t) {
239 if parent == 0 || parent == this || !found.insert(parent) {
240 break;
241 }
242 current = parent;
243 }
244 found
245}
246
247fn inspect(
248 query: &ProcessQuery,
249 wanted_exe: Option<&[PathBuf]>,
250 me: &os::Me,
251 pid: u32,
252) -> io::Result<Option<MatchedProcess>> {
253 let Some(nonzero) = NonZeroU32::new(pid) else {
254 return Ok(None);
255 };
256 let Some(first) = sys::read(pid)? else {
257 return Ok(None);
258 };
259 if let Some(wanted) = wanted_exe {
261 if !first
262 .exe
263 .as_deref()
264 .is_some_and(|seen| exe_matches(seen, wanted))
265 {
266 return Ok(None);
267 }
268 }
269 if !me.owns(pid)? {
270 return Ok(None);
271 }
272 let args = if query.needs_args() {
273 let Some(args) = os::command_line(pid)? else {
274 return Ok(None);
275 };
276 if !args_match(query, &args) {
277 return Ok(None);
278 }
279 Some(args)
280 } else {
281 None
282 };
283 let Some(last) = sys::read(pid)? else {
286 return Ok(None);
287 };
288 if last.start != first.start {
289 return Ok(None);
290 }
291 if let Some(wanted) = wanted_exe {
292 if !last
293 .exe
294 .as_deref()
295 .is_some_and(|seen| exe_matches(seen, wanted))
296 {
297 return Ok(None);
298 }
299 }
300 Ok(Some(MatchedProcess {
301 identity: AdoptedIdentity {
302 pid: nonzero,
303 start: last.start,
304 exe: last.exe,
305 },
306 args,
307 }))
308}
309
310fn args_match(query: &ProcessQuery, args: &[OsString]) -> bool {
311 query
312 .args_exact
313 .iter()
314 .all(|token| args.iter().any(|arg| arg == token))
315 && query.args_containing.iter().all(|needle| {
316 args.iter()
317 .any(|arg| arg.to_string_lossy().contains(needle.as_str()))
318 })
319}
320
321#[cfg(unix)]
323fn exe_candidates(wanted: &Path) -> Vec<PathBuf> {
324 let mut candidates = vec![wanted.to_path_buf()];
325 if let Ok(resolved) = std::fs::canonicalize(wanted) {
326 if resolved != wanted {
327 candidates.push(resolved);
328 }
329 }
330 candidates
331}
332
333#[cfg(not(unix))]
334fn exe_candidates(wanted: &Path) -> Vec<PathBuf> {
335 vec![wanted.to_path_buf()]
336}
337
338fn exe_matches(seen: &Path, wanted: &[PathBuf]) -> bool {
339 wanted.iter().any(|path| same_path(seen, path))
340}
341
342#[cfg(windows)]
343fn same_path(a: &Path, b: &Path) -> bool {
344 fn normal(path: &Path) -> String {
345 let text = path.to_string_lossy().to_lowercase().replace('/', "\\");
346 text.strip_prefix("\\\\?\\").unwrap_or(&text).to_owned()
347 }
348 normal(a) == normal(b)
349}
350
351#[cfg(not(windows))]
352fn same_path(a: &Path, b: &Path) -> bool {
353 a == b
354}
355
356#[cfg(any(windows, test))]
359fn split_windows_command_line(line: &str) -> Vec<String> {
360 let chars: Vec<char> = line.chars().collect();
361 let n = chars.len();
362 let blank = |c: char| c == ' ' || c == '\t';
363 let mut i = 0;
364 let mut out = Vec::new();
365 while i < n && blank(chars[i]) {
366 i += 1;
367 }
368 if i >= n {
369 return out;
370 }
371 let mut program = String::new();
372 if chars[i] == '"' {
373 i += 1;
374 while i < n && chars[i] != '"' {
375 program.push(chars[i]);
376 i += 1;
377 }
378 if i < n {
379 i += 1;
380 }
381 } else {
382 while i < n && !blank(chars[i]) {
383 program.push(chars[i]);
384 i += 1;
385 }
386 }
387 out.push(program);
388 loop {
389 while i < n && blank(chars[i]) {
390 i += 1;
391 }
392 if i >= n {
393 break;
394 }
395 let mut arg = String::new();
396 let mut quoted = false;
397 while i < n {
398 let c = chars[i];
399 if c == '\\' {
400 let mut run = 0;
401 while i < n && chars[i] == '\\' {
402 run += 1;
403 i += 1;
404 }
405 if i < n && chars[i] == '"' {
406 arg.extend(std::iter::repeat_n('\\', run / 2));
407 if run % 2 == 1 {
408 arg.push('"');
409 i += 1;
410 }
411 } else {
412 arg.extend(std::iter::repeat_n('\\', run));
413 }
414 } else if c == '"' {
415 if quoted && i + 1 < n && chars[i + 1] == '"' {
416 arg.push('"');
417 i += 2;
418 } else {
419 quoted = !quoted;
420 i += 1;
421 }
422 } else if blank(c) && !quoted {
423 break;
424 } else {
425 arg.push(c);
426 i += 1;
427 }
428 }
429 out.push(arg);
430 }
431 out
432}
433
434#[cfg(any(target_os = "macos", all(test, unix)))]
437fn parse_procargs2(buffer: &[u8]) -> Option<Vec<OsString>> {
438 use std::os::unix::ffi::OsStringExt;
439 let count = i32::from_ne_bytes(buffer.get(..4)?.try_into().ok()?);
440 let count = usize::try_from(count).ok()?;
441 let mut i = 4;
442 while i < buffer.len() && buffer[i] != 0 {
443 i += 1;
444 }
445 while i < buffer.len() && buffer[i] == 0 {
446 i += 1;
447 }
448 let mut args = Vec::with_capacity(count);
449 for _ in 0..count {
450 if i >= buffer.len() {
451 return None;
452 }
453 let start = i;
454 while i < buffer.len() && buffer[i] != 0 {
455 i += 1;
456 }
457 if i >= buffer.len() {
458 return None; }
460 args.push(OsString::from_vec(buffer[start..i].to_vec()));
461 i += 1;
462 }
463 Some(args)
464}
465
466#[cfg(any(target_os = "linux", target_os = "android"))]
467mod os {
468 use std::{ffi::OsString, io, os::unix::ffi::OsStringExt};
469
470 pub(super) struct Me {
471 uid: u32,
472 euid: u32,
473 }
474
475 impl Me {
476 pub(super) fn current() -> io::Result<Self> {
477 Ok(Self {
479 uid: unsafe { libc::getuid() },
480 euid: unsafe { libc::geteuid() },
481 })
482 }
483
484 pub(super) fn owns(&self, pid: u32) -> io::Result<bool> {
486 let text = match std::fs::read_to_string(format!("/proc/{pid}/status")) {
487 Ok(text) => text,
488 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
489 Err(error) => return Err(error),
490 };
491 let Some(line) = text.lines().find_map(|line| line.strip_prefix("Uid:")) else {
492 return Ok(false);
493 };
494 let mut ids = line.split_whitespace().map(|id| id.parse::<u32>().ok());
495 Ok(ids.next().flatten() == Some(self.uid) && ids.next().flatten() == Some(self.euid))
496 }
497 }
498
499 pub(super) fn command_line(pid: u32) -> io::Result<Option<Vec<OsString>>> {
500 let mut bytes = match std::fs::read(format!("/proc/{pid}/cmdline")) {
501 Ok(bytes) => bytes,
502 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
503 Err(error) => return Err(error),
504 };
505 if bytes.is_empty() {
506 return Ok(None); }
508 if bytes.last() == Some(&0) {
509 bytes.pop();
510 }
511 Ok(Some(
512 bytes
513 .split(|byte| *byte == 0)
514 .map(|part| OsString::from_vec(part.to_vec()))
515 .collect(),
516 ))
517 }
518}
519
520#[cfg(target_os = "macos")]
521mod os {
522 use std::{ffi::OsString, io};
523
524 pub(super) struct Me {
525 uid: libc::uid_t,
526 euid: libc::uid_t,
527 }
528
529 impl Me {
530 pub(super) fn current() -> io::Result<Self> {
531 Ok(Self {
533 uid: unsafe { libc::getuid() },
534 euid: unsafe { libc::geteuid() },
535 })
536 }
537
538 pub(super) fn owns(&self, pid: u32) -> io::Result<bool> {
540 let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
542 let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
543 let written = unsafe {
545 libc::proc_pidinfo(
546 pid as libc::pid_t,
547 libc::PROC_PIDTBSDINFO,
548 0,
549 (&mut info as *mut libc::proc_bsdinfo).cast(),
550 size,
551 )
552 };
553 if written != size || info.pbi_pid != pid {
554 return Ok(false);
555 }
556 Ok(info.pbi_uid == self.euid && info.pbi_ruid == self.uid)
557 }
558 }
559
560 pub(super) fn command_line(pid: u32) -> io::Result<Option<Vec<OsString>>> {
561 let mut name = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as libc::c_int];
562 let mut capacity: usize = 0;
563 let probed = unsafe {
565 libc::sysctl(
566 name.as_mut_ptr(),
567 3,
568 std::ptr::null_mut(),
569 &mut capacity,
570 std::ptr::null_mut(),
571 0,
572 )
573 };
574 if probed != 0 {
575 return gone_or_error();
576 }
577 let mut buffer = vec![0u8; capacity + 4096];
579 let mut length = buffer.len();
580 let read = unsafe {
582 libc::sysctl(
583 name.as_mut_ptr(),
584 3,
585 buffer.as_mut_ptr().cast(),
586 &mut length,
587 std::ptr::null_mut(),
588 0,
589 )
590 };
591 if read != 0 {
592 return gone_or_error();
593 }
594 buffer.truncate(length);
595 Ok(super::parse_procargs2(&buffer))
596 }
597
598 fn gone_or_error() -> io::Result<Option<Vec<OsString>>> {
601 let error = io::Error::last_os_error();
602 match error.raw_os_error() {
603 Some(libc::ESRCH) | Some(libc::EINVAL) | Some(libc::EPERM) => Ok(None),
604 _ => Err(error),
605 }
606 }
607}
608
609#[cfg(windows)]
610mod os {
611 use std::{ffi::c_void, ffi::OsString, io};
612
613 use windows::Win32::{
614 Foundation::{CloseHandle, ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, HANDLE},
615 Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER},
616 System::Threading::{
617 GetCurrentProcess, OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
618 },
619 };
620
621 use crate::process_windows::os_error;
622
623 #[link(name = "ntdll")]
624 extern "system" {
625 fn NtQueryInformationProcess(
626 process: *mut c_void,
627 class: u32,
628 information: *mut c_void,
629 length: u32,
630 returned: *mut u32,
631 ) -> i32;
632 }
633
634 const PROCESS_COMMAND_LINE_INFORMATION: u32 = 60;
636 const STATUS_INFO_LENGTH_MISMATCH: u32 = 0xC000_0004;
637 const STATUS_BUFFER_OVERFLOW: u32 = 0x8000_0005;
638 const STATUS_BUFFER_TOO_SMALL: u32 = 0xC000_0023;
639
640 #[repr(C)]
641 struct UnicodeString {
642 length: u16,
643 maximum_length: u16,
644 buffer: *const u16,
645 }
646
647 struct Owned(HANDLE);
648 impl Drop for Owned {
649 fn drop(&mut self) {
650 let _ = unsafe { CloseHandle(self.0) };
652 }
653 }
654
655 fn open(pid: u32) -> io::Result<Option<Owned>> {
657 match unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) } {
659 Ok(handle) => Ok(Some(Owned(handle))),
660 Err(error)
661 if error.code() == ERROR_INVALID_PARAMETER.to_hresult()
662 || error.code() == ERROR_ACCESS_DENIED.to_hresult() =>
663 {
664 Ok(None)
665 }
666 Err(error) => Err(os_error(error)),
667 }
668 }
669
670 fn user_sid(process: HANDLE) -> io::Result<Option<Vec<u8>>> {
673 let mut token = HANDLE::default();
674 match unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } {
676 Ok(()) => {}
677 Err(error) if error.code() == ERROR_ACCESS_DENIED.to_hresult() => return Ok(None),
678 Err(error) => return Err(os_error(error)),
679 }
680 let token = Owned(token);
681 let mut needed = 0u32;
682 let _ = unsafe { GetTokenInformation(token.0, TokenUser, None, 0, &mut needed) };
684 if needed == 0 {
685 return Err(io::Error::other("the token user size could not be read"));
686 }
687 let mut buffer = vec![0u64; (needed as usize).div_ceil(8)];
689 unsafe {
691 GetTokenInformation(
692 token.0,
693 TokenUser,
694 Some(buffer.as_mut_ptr().cast::<c_void>()),
695 needed,
696 &mut needed,
697 )
698 }
699 .map_err(os_error)?;
700 let user = unsafe { &*(buffer.as_ptr().cast::<TOKEN_USER>()) };
703 let sid = user.User.Sid.0 as *const u8;
704 let base = buffer.as_ptr() as usize;
705 let end = base + buffer.len() * 8;
706 if sid.is_null() || (sid as usize) < base || (sid as usize) + 8 > end {
707 return Err(io::Error::from(io::ErrorKind::InvalidData));
708 }
709 let length = 8 + 4 * usize::from(unsafe { *sid.add(1) });
711 if (sid as usize) + length > end {
712 return Err(io::Error::from(io::ErrorKind::InvalidData));
713 }
714 Ok(Some(
716 unsafe { std::slice::from_raw_parts(sid, length) }.to_vec(),
717 ))
718 }
719
720 pub(super) struct Me {
721 sid: Vec<u8>,
722 }
723
724 impl Me {
725 pub(super) fn current() -> io::Result<Self> {
726 let sid = user_sid(unsafe { GetCurrentProcess() })?
728 .ok_or_else(|| io::Error::other("the current user could not be read"))?;
729 Ok(Self { sid })
730 }
731
732 pub(super) fn owns(&self, pid: u32) -> io::Result<bool> {
734 let Some(process) = open(pid)? else {
735 return Ok(false);
736 };
737 Ok(user_sid(process.0)?.is_some_and(|sid| sid == self.sid))
738 }
739 }
740
741 pub(super) fn command_line(pid: u32) -> io::Result<Option<Vec<OsString>>> {
742 let Some(process) = open(pid)? else {
743 return Ok(None);
744 };
745 let mut size: usize = 1024;
746 for _ in 0..4 {
747 let mut buffer = vec![0u64; size.div_ceil(8)];
748 let mut returned = 0u32;
749 let status = unsafe {
751 NtQueryInformationProcess(
752 process.0 .0,
753 PROCESS_COMMAND_LINE_INFORMATION,
754 buffer.as_mut_ptr().cast(),
755 (buffer.len() * 8) as u32,
756 &mut returned,
757 )
758 } as u32;
759 match status {
760 0 => return Ok(read_unicode_string(&buffer)),
761 STATUS_INFO_LENGTH_MISMATCH | STATUS_BUFFER_OVERFLOW | STATUS_BUFFER_TOO_SMALL => {
762 size = (returned as usize).max(size * 2);
763 }
764 _ => return Ok(None), }
766 }
767 Ok(None)
768 }
769
770 fn read_unicode_string(buffer: &[u64]) -> Option<Vec<OsString>> {
771 let base = buffer.as_ptr() as usize;
772 let end = base + buffer.len() * 8;
773 if buffer.len() * 8 < std::mem::size_of::<UnicodeString>() {
774 return None;
775 }
776 let header = unsafe { &*(buffer.as_ptr().cast::<UnicodeString>()) };
778 let text = header.buffer as usize;
779 let bytes = usize::from(header.length);
780 if bytes == 0 {
781 return Some(Vec::new());
782 }
783 if text < base + std::mem::size_of::<UnicodeString>()
785 || !text.is_multiple_of(2)
786 || !bytes.is_multiple_of(2)
787 || text + bytes > end
788 {
789 return None;
790 }
791 let units = unsafe { std::slice::from_raw_parts(header.buffer, bytes / 2) };
793 let line = String::from_utf16_lossy(units);
794 Some(
795 super::split_windows_command_line(&line)
796 .into_iter()
797 .map(OsString::from)
798 .collect(),
799 )
800 }
801}
802
803#[cfg(not(any(
804 target_os = "linux",
805 target_os = "android",
806 target_os = "macos",
807 windows
808)))]
809mod os {
810 use std::{ffi::OsString, io};
811
812 pub(super) struct Me;
813
814 impl Me {
815 pub(super) fn current() -> io::Result<Self> {
816 Err(io::Error::from(io::ErrorKind::Unsupported))
817 }
818
819 pub(super) fn owns(&self, _pid: u32) -> io::Result<bool> {
820 Err(io::Error::from(io::ErrorKind::Unsupported))
821 }
822 }
823
824 pub(super) fn command_line(_pid: u32) -> io::Result<Option<Vec<OsString>>> {
825 Err(io::Error::from(io::ErrorKind::Unsupported))
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832
833 fn strings(parts: &[&str]) -> Vec<OsString> {
834 parts.iter().map(OsString::from).collect()
835 }
836
837 #[test]
838 fn an_empty_query_and_empty_criteria_are_refused() {
839 for query in [
840 ProcessQuery::new(),
841 ProcessQuery::new().exe(""),
842 ProcessQuery::new().arg(""),
843 ProcessQuery::new().arg_containing(""),
844 ] {
845 let error = find_processes(&query).unwrap_err();
846 assert_eq!(error.kind(), io::ErrorKind::InvalidInput, "{query:?}");
847 }
848 }
849
850 #[test]
851 fn arguments_match_exactly_and_by_substring_and_all_criteria_must_hold() {
852 let args = strings(&["chrome", "--user-data-dir=/p/rk-profile-1", "--flag"]);
853 let both = ProcessQuery::new()
854 .arg("--flag")
855 .arg_containing("rk-profile-1");
856 assert!(args_match(&both, &args));
857 assert!(!args_match(&ProcessQuery::new().arg("--fla"), &args));
858 assert!(!args_match(
859 &ProcessQuery::new().arg_containing("rk-profile-2"),
860 &args
861 ));
862 assert!(!args_match(&both.arg("--missing"), &args));
863 }
864
865 #[test]
866 fn a_substring_is_found_inside_one_argument_not_across_two() {
867 let args = strings(&["tool", "ab", "cd"]);
868 assert!(args_match(&ProcessQuery::new().arg_containing("ab"), &args));
869 assert!(!args_match(
870 &ProcessQuery::new().arg_containing("b c"),
871 &args
872 ));
873 assert!(!args_match(
874 &ProcessQuery::new().arg_containing("bcd"),
875 &args
876 ));
877 }
878
879 #[test]
880 fn ancestors_follow_the_parent_chain_and_stop_on_a_loop() {
881 let table = [(10, 5), (5, 2), (2, 1), (1, 0), (7, 9), (9, 7)];
882 assert_eq!(ancestors(&table, 10), HashSet::from([5, 2, 1]));
883 assert_eq!(ancestors(&table, 7), HashSet::from([9]));
884 assert!(ancestors(&table, 99).is_empty());
885 }
886
887 #[test]
888 fn windows_command_lines_split_like_the_c_runtime() {
889 assert_eq!(
890 split_windows_command_line(r#""C:\Program Files\x.exe" a "b c" d\"e "f\\" g"#),
891 vec![r"C:\Program Files\x.exe", "a", "b c", "d\"e", r"f\", "g"]
892 );
893 assert_eq!(
894 split_windows_command_line("prog one\ttwo"),
895 vec!["prog", "one", "two"]
896 );
897 assert_eq!(split_windows_command_line(r#"p "" x"#), vec!["p", "", "x"]);
898 assert_eq!(
899 split_windows_command_line(r#"p "a""b" c"#),
900 vec!["p", "a\"b", "c"]
901 );
902 assert_eq!(split_windows_command_line(r"p a\\b"), vec!["p", r"a\\b"]);
903 assert!(split_windows_command_line(" ").is_empty());
904 }
905
906 #[cfg(unix)]
907 #[test]
908 fn procargs2_yields_the_arguments_and_not_the_environment() {
909 let mut buffer = 3i32.to_ne_bytes().to_vec();
910 buffer.extend_from_slice(b"/bin/sh\0\0\0sh\0-c\0read x\0HOME=/root\0");
911 assert_eq!(
912 parse_procargs2(&buffer).unwrap(),
913 strings(&["sh", "-c", "read x"])
914 );
915 let mut short = 3i32.to_ne_bytes().to_vec();
917 short.extend_from_slice(b"/bin/sh\0\0sh\0-c\0rea");
918 assert!(parse_procargs2(&short).is_none());
919 assert!(parse_procargs2(&[1, 0]).is_none());
920 }
921
922 #[cfg(windows)]
923 #[test]
924 fn windows_paths_compare_without_case_or_slash_style() {
925 assert!(same_path(
926 Path::new(r"C:\Program Files\App\x.exe"),
927 Path::new("c:/program files/app/X.EXE")
928 ));
929 assert!(!same_path(
930 Path::new(r"C:\a\x.exe"),
931 Path::new(r"C:\b\x.exe")
932 ));
933 }
934}