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
15pub fn read_password(
31 prompt: &str,
32 password_override: Option<&str>,
33 use_askpass: bool,
34) -> anyhow::Result<String> {
35 if let Some(p) = password_override {
37 return Ok(p.to_owned());
38 }
39
40 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 eprintln!("askpass exited with {}", out.status);
57 None
58 }
59 } else {
60 None
61 }
62 } else {
63 None
64 }
65 .unwrap_or_else(|| {
66 rpassword::prompt_password(prompt).unwrap_or_default()
68 });
69
70 if password.is_empty() {
72 bail!("Password cannot be empty");
73 }
74 Ok(password)
75}
76
77fn resolve_askpass() -> Option<PathBuf> {
84 if let Ok(path) = env::var("SSH_ASKPASS").or_else(|_| env::var("ASKPASS")) {
86 return Some(PathBuf::from(path));
87 }
88
89 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 which::which("askpass").ok()
105}
106
107pub fn read_argument(argument: Option<&str>) -> Result<String> {
108 let string = if let Some(arg) = argument {
109 arg.to_string()
110 } else {
111 let mut s = String::new();
112 std::io::stdin().read_to_string(&mut s)?;
113 s
114 };
115 if string.is_empty() {
116 bail!("No argument provided");
117 }
118 Ok(string)
119}
120
121pub fn envelope_from_ur(ur: &UR) -> Result<Envelope> {
122 if let Ok(envelope) = Envelope::from_ur(ur) {
123 return Ok(envelope);
124 }
125 if let Ok(envelope) = Envelope::from_tagged_cbor(ur.cbor()) {
126 return Ok(envelope);
127 }
128 if ur.ur_type_str() == "xid" {
129 let xid = XID::from_untagged_cbor(ur.cbor())?;
130 let doc = XIDDocument::from(xid);
131 return Ok(doc.into_envelope());
132 }
133 bail!("Invalid envelope");
134}
135
136pub fn read_envelope(envelope: Option<&str>) -> Result<Envelope> {
137 let ur_string = if let Some(env) = envelope {
138 env.to_string()
139 } else {
140 let mut s = String::new();
141 std::io::stdin().read_line(&mut s)?;
142 s
143 };
144 let ur_string = ur_string.trim();
145 if ur_string.is_empty() {
146 bail!("No envelope provided");
147 }
148 Envelope::from_ur_string(ur_string)
149 .or_else(|_| envelope_from_ur(&UR::from_ur_string(ur_string)?))
150}
151
152pub fn parse_digest(target: &str) -> Result<Digest> {
153 let ur = UR::from_ur_string(target)?;
154 let digest = match ur.ur_type_str() {
155 "digest" => Digest::from_ur(&ur)?,
156 "envelope" => Envelope::from_ur(&ur)?.digest(),
157 _ => {
158 bail!("Invalid digest type: {}", ur.ur_type_str());
159 }
160 };
161 Ok(digest)
162}
163
164pub fn parse_digests(target: &str) -> Result<HashSet<Digest>> {
165 let target = target.trim();
166 if target.is_empty() {
167 Ok(HashSet::new())
168 } else {
169 target
170 .split(' ')
171 .map(parse_digest)
172 .collect::<Result<HashSet<Digest>>>()
173 }
174}