Skip to main content

shuck_run/
types.rs

1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Result, anyhow, bail};
7use serde::Deserialize;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum Shell {
11    Bash,
12    Gbash,
13    Bashkit,
14    Zsh,
15    Dash,
16    Mksh,
17    Busybox,
18}
19
20impl Shell {
21    pub fn as_str(self) -> &'static str {
22        match self {
23            Self::Bash => "bash",
24            Self::Gbash => "gbash",
25            Self::Bashkit => "bashkit",
26            Self::Zsh => "zsh",
27            Self::Dash => "dash",
28            Self::Mksh => "mksh",
29            Self::Busybox => "busybox",
30        }
31    }
32
33    pub fn from_name(name: &str) -> Option<Self> {
34        match name.trim().to_ascii_lowercase().as_str() {
35            "bash" => Some(Self::Bash),
36            "gbash" => Some(Self::Gbash),
37            "bashkit" => Some(Self::Bashkit),
38            "zsh" => Some(Self::Zsh),
39            "dash" | "sh" => Some(Self::Dash),
40            "mksh" | "ksh" => Some(Self::Mksh),
41            "busybox" => Some(Self::Busybox),
42            _ => None,
43        }
44    }
45
46    pub(crate) fn ensure_supported_on_current_platform(self) -> Result<()> {
47        if self != Self::Busybox || std::env::consts::OS == "linux" {
48            return Ok(());
49        }
50
51        bail!("busybox is only supported on Linux");
52    }
53
54    pub(crate) fn infer(source: &str, path: Option<&Path>) -> Option<Self> {
55        Self::infer_from_shebang(source).or_else(|| {
56            path.and_then(|path| {
57                let ext = path.extension()?.to_str()?;
58                Self::from_name(ext)
59            })
60        })
61    }
62
63    fn infer_from_shebang(source: &str) -> Option<Self> {
64        let interpreter = shuck_parser::shebang::interpreter_name(source.lines().next()?)?;
65        Self::from_name(interpreter)
66    }
67}
68
69impl fmt::Display for Shell {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.write_str(self.as_str())
72    }
73}
74
75#[derive(Debug, Clone)]
76pub struct Version {
77    raw: String,
78    tokens: Vec<VersionToken>,
79    segment_count: usize,
80    prefix_match: bool,
81}
82
83impl Version {
84    pub fn parse(raw: &str) -> Result<Self> {
85        let raw = raw.trim();
86        if raw.is_empty() {
87            bail!("version cannot be empty");
88        }
89        if !raw
90            .chars()
91            .all(|ch| ch.is_ascii_alphanumeric() || ch == '.')
92        {
93            bail!("invalid version `{raw}`");
94        }
95        if raw.split('.').any(|segment| segment.is_empty()) {
96            bail!("invalid version `{raw}`");
97        }
98
99        let tokens = tokenize_version(raw)?;
100        if tokens.is_empty() {
101            bail!("invalid version `{raw}`");
102        }
103
104        let segment_count = raw.split('.').count();
105        let prefix_match = should_treat_as_prefix(raw, &tokens, segment_count);
106
107        Ok(Self {
108            raw: raw.to_owned(),
109            tokens,
110            segment_count,
111            prefix_match,
112        })
113    }
114
115    pub fn as_str(&self) -> &str {
116        &self.raw
117    }
118
119    fn matches_prefix(&self, other: &Self) -> bool {
120        if !self.prefix_match {
121            return self == other;
122        }
123
124        let mut matched_segments = 0usize;
125        let mut left = self.tokens.iter().peekable();
126        let mut right = other.tokens.iter().peekable();
127        while let Some(left_token) = left.next() {
128            let Some(right_token) = right.next() else {
129                return false;
130            };
131            if left_token != right_token {
132                return false;
133            }
134            if matches!(left_token, VersionToken::Numeric(_) | VersionToken::Text(_))
135                && (left.peek().is_none()
136                    || matches!(
137                        left.peek(),
138                        Some(VersionToken::Numeric(_) | VersionToken::Text(_))
139                    ))
140            {
141                matched_segments += 1;
142            }
143        }
144
145        matched_segments >= self.segment_count
146    }
147}
148
149impl PartialEq for Version {
150    fn eq(&self, other: &Self) -> bool {
151        self.cmp(other) == Ordering::Equal
152    }
153}
154
155impl Eq for Version {}
156
157impl fmt::Display for Version {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        f.write_str(&self.raw)
160    }
161}
162
163impl PartialOrd for Version {
164    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
165        Some(self.cmp(other))
166    }
167}
168
169impl Ord for Version {
170    fn cmp(&self, other: &Self) -> Ordering {
171        let mut left = self.tokens.iter();
172        let mut right = other.tokens.iter();
173        loop {
174            match (left.next(), right.next()) {
175                (Some(a), Some(b)) => {
176                    let ordering = a.cmp(b);
177                    if ordering != Ordering::Equal {
178                        return ordering;
179                    }
180                }
181                (Some(_), None) => return Ordering::Greater,
182                (None, Some(_)) => return Ordering::Less,
183                (None, None) => return Ordering::Equal,
184            }
185        }
186    }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
190enum VersionToken {
191    Numeric(u64),
192    Text(String),
193}
194
195fn tokenize_version(raw: &str) -> Result<Vec<VersionToken>> {
196    let mut tokens = Vec::new();
197    let mut chars = raw.chars().peekable();
198    while let Some(ch) = chars.peek().copied() {
199        if ch.is_ascii_digit() {
200            let mut digits = String::new();
201            while let Some(next) = chars.peek().copied() {
202                if next.is_ascii_digit() {
203                    digits.push(next);
204                    chars.next();
205                } else {
206                    break;
207                }
208            }
209            let value = digits
210                .parse::<u64>()
211                .map_err(|_| anyhow!("invalid version `{raw}`"))?;
212            tokens.push(VersionToken::Numeric(value));
213            continue;
214        }
215
216        if ch.is_ascii_alphabetic() {
217            let mut text = String::new();
218            while let Some(next) = chars.peek().copied() {
219                if next.is_ascii_alphabetic() {
220                    text.push(next.to_ascii_lowercase());
221                    chars.next();
222                } else {
223                    break;
224                }
225            }
226            tokens.push(VersionToken::Text(text));
227            continue;
228        }
229
230        if ch == '.' {
231            chars.next();
232            continue;
233        }
234
235        break;
236    }
237
238    Ok(tokens)
239}
240
241fn should_treat_as_prefix(raw: &str, tokens: &[VersionToken], segment_count: usize) -> bool {
242    raw.chars().all(|ch| ch.is_ascii_digit() || ch == '.')
243        && segment_count == 2
244        && tokens
245            .iter()
246            .all(|token| matches!(token, VersionToken::Numeric(_)))
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub enum VersionConstraint {
251    Latest,
252    Exact(Version),
253    ExactPrefix(Version),
254    Range(Vec<VersionPredicate>),
255}
256
257impl VersionConstraint {
258    pub fn parse(raw: &str) -> Result<Self> {
259        let raw = raw.trim();
260        if raw.eq_ignore_ascii_case("latest") {
261            return Ok(Self::Latest);
262        }
263
264        if raw.contains('>') || raw.contains('<') || raw.contains('=') {
265            let mut predicates = Vec::new();
266            for item in raw.split(',') {
267                let item = item.trim();
268                if item.is_empty() {
269                    bail!("invalid version constraint `{raw}`");
270                }
271                predicates.push(VersionPredicate::parse(item)?);
272            }
273            return Ok(Self::Range(predicates));
274        }
275
276        let version = Version::parse(raw)?;
277        if version.prefix_match {
278            Ok(Self::ExactPrefix(version))
279        } else {
280            Ok(Self::Exact(version))
281        }
282    }
283
284    pub fn matches(&self, version: &Version) -> bool {
285        match self {
286            Self::Latest => true,
287            Self::Exact(expected) => expected == version,
288            Self::ExactPrefix(prefix) => prefix.matches_prefix(version),
289            Self::Range(predicates) => predicates
290                .iter()
291                .all(|predicate| predicate.matches(version)),
292        }
293    }
294
295    pub(crate) fn describe(&self) -> String {
296        match self {
297            Self::Latest => "latest".to_owned(),
298            Self::Exact(version) | Self::ExactPrefix(version) => version.as_str().to_owned(),
299            Self::Range(predicates) => predicates
300                .iter()
301                .map(VersionPredicate::to_string)
302                .collect::<Vec<_>>()
303                .join(","),
304        }
305    }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct AvailableShell {
310    pub shell: Shell,
311    pub versions: Vec<Version>,
312}
313
314#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
315#[serde(default, rename_all = "kebab-case")]
316pub struct RunConfig {
317    pub shell: Option<String>,
318    pub shell_version: Option<String>,
319    pub shells: BTreeMap<String, String>,
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct ResolvedInterpreter {
324    pub shell: Shell,
325    pub version: Version,
326    pub path: PathBuf,
327    pub source: ResolutionSource,
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum ResolutionSource {
332    Managed,
333    System,
334}
335
336#[derive(Debug, Clone)]
337pub struct ResolveOptions<'a> {
338    pub shell: Option<Shell>,
339    pub version: Option<VersionConstraint>,
340    pub system: bool,
341    pub implicit_system_fallback: bool,
342    pub script: Option<&'a Path>,
343    pub config: Option<&'a RunConfig>,
344    pub verbose: bool,
345    pub refresh_registry: bool,
346}
347
348impl<'a> ResolveOptions<'a> {
349    pub fn new(
350        shell: Option<Shell>,
351        version: Option<VersionConstraint>,
352        system: bool,
353        script: Option<&'a Path>,
354        config: Option<&'a RunConfig>,
355    ) -> Self {
356        Self {
357            shell,
358            version,
359            system,
360            implicit_system_fallback: false,
361            script,
362            config,
363            verbose: false,
364            refresh_registry: false,
365        }
366    }
367}
368
369pub(crate) fn parse_shell_name(raw: &str) -> Result<Shell> {
370    Shell::from_name(raw).ok_or_else(|| {
371        anyhow!(
372            "unsupported shell `{raw}`; expected one of: bash, gbash, bashkit, zsh, dash, mksh, busybox"
373        )
374    })
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct VersionPredicate {
379    operator: VersionOperator,
380    version: Version,
381}
382
383impl VersionPredicate {
384    fn parse(raw: &str) -> Result<Self> {
385        let (operator, version) = if let Some(version) = raw.strip_prefix(">=") {
386            (VersionOperator::GreaterOrEqual, version)
387        } else if let Some(version) = raw.strip_prefix("<=") {
388            (VersionOperator::LessOrEqual, version)
389        } else if let Some(version) = raw.strip_prefix('>') {
390            (VersionOperator::Greater, version)
391        } else if let Some(version) = raw.strip_prefix('<') {
392            (VersionOperator::Less, version)
393        } else if let Some(version) = raw.strip_prefix('=') {
394            (VersionOperator::Equal, version)
395        } else {
396            bail!("invalid version predicate `{raw}`");
397        };
398
399        Ok(Self {
400            operator,
401            version: Version::parse(version)?,
402        })
403    }
404
405    fn matches(&self, version: &Version) -> bool {
406        match self.operator {
407            VersionOperator::Greater => version > &self.version,
408            VersionOperator::GreaterOrEqual => version >= &self.version,
409            VersionOperator::Less => version < &self.version,
410            VersionOperator::LessOrEqual => version <= &self.version,
411            VersionOperator::Equal => version == &self.version,
412        }
413    }
414}
415
416impl fmt::Display for VersionPredicate {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        write!(f, "{}{}", self.operator, self.version)
419    }
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423enum VersionOperator {
424    Greater,
425    GreaterOrEqual,
426    Less,
427    LessOrEqual,
428    Equal,
429}
430
431impl fmt::Display for VersionOperator {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        let symbol = match self {
434            Self::Greater => ">",
435            Self::GreaterOrEqual => ">=",
436            Self::Less => "<",
437            Self::LessOrEqual => "<=",
438            Self::Equal => "=",
439        };
440        f.write_str(symbol)
441    }
442}