Skip to main content

runtime/sieve/
key.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Which command produced an output. The identity a filter matches
7//! and the differencing groups by: the arm, the program, its
8//! arguments. Ordered so it can key a `BTreeMap`, never a hash.
9
10use kernel::ExecArm;
11
12/// Which command produced an output: the identity a filter matches
13/// and the differencing groups by. Ordered so it can key a `BTreeMap`.
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
15pub struct CommandKey {
16    arm: String,
17    program: String,
18    args: Vec<String>,
19}
20
21impl CommandKey {
22    #[must_use]
23    pub fn of(arm: &ExecArm) -> CommandKey {
24        match arm {
25            ExecArm::Program { path, args } => CommandKey {
26                arm: "program".to_owned(),
27                program: path.clone(),
28                args: args.clone(),
29            },
30            ExecArm::Shell { text } => {
31                let mut words = text.split_whitespace().map(str::to_owned);
32                CommandKey {
33                    arm: "shell".to_owned(),
34                    program: words.next().unwrap_or_default(),
35                    args: words.collect(),
36                }
37            }
38            ExecArm::Python { code } => CommandKey {
39                arm: "python".to_owned(),
40                program: "python".to_owned(),
41                args: vec![code.clone()],
42            },
43        }
44    }
45
46    /// The program's file name, lower-cased, without `.exe`: what a
47    /// filter's `command` is compared to.
48    #[must_use]
49    pub fn command(&self) -> String {
50        let name = self
51            .program
52            .rsplit(['/', '\\'])
53            .next()
54            .unwrap_or_default()
55            .to_ascii_lowercase();
56        name.strip_suffix(".exe").unwrap_or(&name).to_owned()
57    }
58
59    /// The first argument that is not a flag.
60    #[must_use]
61    pub fn subcommand(&self) -> Option<&str> {
62        self.args
63            .iter()
64            .map(String::as_str)
65            .find(|arg| !arg.starts_with('-'))
66    }
67}