bc_envelope_cli/
utils.rs

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