bc_envelope_cli/
utils.rs

1use std::{
2    collections::HashSet,
3    env,
4    io::Read,
5    path::{Path, PathBuf},
6    process::Command,
7    str,
8};
9
10use anyhow::{Result, bail};
11use bc_components::XID;
12use bc_envelope::prelude::*;
13use bc_xid::XIDDocument;
14
15/// Reads a password either from the provided argument, via the system's askpass
16/// tool when enabled, or interactively via rpassword.
17///
18/// # Arguments
19///
20/// * `prompt` - The prompt to show the user.
21/// * `password` - An optional password string.
22/// * `use_askpass` - Boolean flag to indicate if the system's askpass should be
23///   used.
24///
25/// # Returns
26///
27/// A Result wrapping the password string.
28/// Ask the user for a password, honoring explicit overrides, graphical helpers,
29/// and finally falling back to a plain TTY prompt.
30pub fn read_password(
31    prompt: &str,
32    password_override: Option<&str>,
33    use_askpass: bool,
34) -> anyhow::Result<String> {
35    // 1. If the caller already supplied a password, trust it and return.
36    if let Some(p) = password_override {
37        return Ok(p.to_owned());
38    }
39
40    // 2. If the caller wants a GUI prompt, try to discover and invoke one.
41    let password = if use_askpass {
42        if let Some(cmd) = resolve_askpass() {
43            let out = Command::new(cmd).arg(prompt).output()?;
44
45            if out.status.success() {
46                let pass = str::from_utf8(&out.stdout)
47                    .map_err(|e| {
48                        anyhow::anyhow!("askpass produced invalid UTF‑8: {e}")
49                    })?
50                    .trim_end_matches(&['\n', '\r'][..])
51                    .to_owned();
52                Some(pass)
53            } else {
54                // A non‑zero exit from askpass is treated as a soft failure;
55                // we fall through to the TTY prompt instead of aborting.
56                eprintln!("askpass exited with {}", out.status);
57                None
58            }
59        } else {
60            None
61        }
62    } else {
63        None
64    }
65    .unwrap_or_else(|| {
66        // 3. Last resort: prompt on the terminal.
67        rpassword::prompt_password(prompt).unwrap_or_default()
68    });
69
70    // 4. If the password is empty, return an error.
71    if password.is_empty() {
72        bail!("Password cannot be empty");
73    }
74    Ok(password)
75}
76
77/// Locate a suitable askpass helper.
78///
79/// The search order is:
80///   •  `$SSH_ASKPASS` or `$ASKPASS` if either is set.
81///   •  Well‑known install locations on macOS and Linux.
82///   •  The first `askpass`‑named binary found in `$PATH`.
83fn resolve_askpass() -> Option<PathBuf> {
84    // Explicit environment overrides take precedence.
85    if let Ok(path) = env::var("SSH_ASKPASS").or_else(|_| env::var("ASKPASS")) {
86        return Some(PathBuf::from(path));
87    }
88
89    // Common absolute paths used by package managers and system installs.
90    const CANDIDATES: &[&str] = &[
91        "/usr/libexec/ssh-askpass",
92        "/usr/lib/ssh/ssh-askpass",
93        "/usr/local/bin/ssh-askpass",
94        "/opt/homebrew/bin/ssh-askpass",
95    ];
96    for cand in CANDIDATES {
97        let p = Path::new(cand);
98        if p.exists() && p.is_file() {
99            return Some(p.to_path_buf());
100        }
101    }
102
103    // Finally, fall back to whatever “askpass” the user might have on $PATH.
104    which::which("askpass").ok()
105}
106
107pub fn read_argument(argument: Option<&str>) -> Result<String> {
108    let mut string = String::new();
109    if argument.is_none() {
110        std::io::stdin().read_to_string(&mut string)?;
111    } else {
112        string = argument.as_ref().unwrap().to_string();
113    }
114    if string.is_empty() {
115        bail!("No argument provided");
116    }
117    Ok(string.to_string())
118}
119
120pub fn read_envelope(envelope: Option<&str>) -> Result<Envelope> {
121    let mut ur_string = String::new();
122    if envelope.is_none() {
123        std::io::stdin().read_line(&mut ur_string)?;
124    } else {
125        ur_string = envelope.as_ref().unwrap().to_string();
126    }
127    if ur_string.is_empty() {
128        bail!("No envelope provided");
129    }
130    // Just try to parse the envelope as a ur:envelope string first
131    if let Ok(envelope) = Envelope::from_ur_string(ur_string.trim()) {
132        Ok(envelope)
133        // If that fails, try to parse the envelope as a ur:<any> string
134    } else if let Ok(ur) = UR::from_ur_string(ur_string.trim()) {
135        let cbor = ur.cbor();
136        // Try to parse the CBOR into an envelope
137        if let Ok(envelope) = Envelope::from_tagged_cbor(cbor) {
138            Ok(envelope)
139        } else if ur.ur_type_str() == "xid" {
140            let xid = XID::from_untagged_cbor(ur.cbor())?;
141            let doc = XIDDocument::from(xid);
142            Ok(doc.into_envelope())
143        } else {
144            todo!();
145        }
146    } else {
147        bail!("Invalid envelope");
148    }
149}
150
151pub fn parse_digest(target: &str) -> Result<Digest> {
152    let ur = UR::from_ur_string(target)?;
153    let digest = match ur.ur_type_str() {
154        "digest" => Digest::from_ur(&ur)?,
155        "envelope" => Envelope::from_ur(&ur)?.digest().into_owned(),
156        _ => {
157            bail!("Invalid digest type: {}", ur.ur_type_str());
158        }
159    };
160    Ok(digest)
161}
162
163pub fn parse_digests(target: &str) -> Result<HashSet<Digest>> {
164    let target = target.trim();
165    if target.is_empty() {
166        Ok(HashSet::new())
167    } else {
168        target
169            .split(' ')
170            .map(parse_digest)
171            .collect::<Result<HashSet<Digest>>>()
172    }
173}