1use crate::{
2 err::{assert_child_io, assert_child_status, Errors, LogicError, PassFuResult},
3 opt::{Command, Config, Opt, PassOpt, ShowMode, ShowOpt},
4};
5use std::{
6 ffi::OsString,
7 io::{self, BufRead, BufReader, Write},
8 iter::once,
9 path::{Component, PathBuf},
10 process::Stdio,
11};
12
13pub fn main() {
14 if let Err(err) = run(std::env::args_os(), std::env::vars_os()) {
15 eprintln!("Error {err}");
16 std::process::exit(err.code())
17 }
18}
19
20pub fn run(
21 args: impl IntoIterator<Item = OsString>,
22 env: impl IntoIterator<Item = (OsString, OsString)>,
23) -> PassFuResult<()> {
24 let Opt {
25 config, command, ..
26 } = Opt::parse(args, env)
27 .map_err(LogicError::new)
28 .ctx("parsing arguments")?;
29
30 match command {
31 Command::Show(opt) => search(config, opt),
32 Command::Other(opt) => pass_through(config, opt),
33 }
34}
35fn pass_through(cfg: Config, opt: PassOpt) -> PassFuResult<()> {
36 let status = std::process::Command::new(&cfg.pass)
37 .args(once(opt.command).chain(opt.rest))
38 .status();
39
40 assert_child_status(cfg.pass.to_str(), status)
41}
42
43fn search(cfg: Config, mut opt: ShowOpt) -> PassFuResult<()> {
44 opt.path = {
45 let paths = find(&cfg, &opt).ctx("finding paths")?;
46 pick(&cfg, paths).ctx("picking a path")?.map(|p| p.into())
47 };
48
49 const OTP_CODE: &str = "OTP code";
50 let (mut content, line) = if opt.line.is_empty() {
51 let content = fetch(&cfg, &opt).ctx("fetching content")?;
53
54 let choice = {
55 let mut choices = vec![];
57 for (i, l) in content.iter().enumerate() {
58 let mut l = l.as_str();
59 if l.trim().starts_with("otpauth://") {
60 l = "OTP URI";
61 choices.push(format!("{i}: {OTP_CODE}"));
62 } else if i == 0 {
63 l = "the main password";
64 } else {
65 l = "*******";
66 }
67 choices.push(format!("{i}: {l}"));
68 }
69
70 if let [single] = &mut choices[..] {
71 std::mem::take(single)
72 } else {
73 choices.insert(0, "All".to_owned());
74 pick(&cfg, choices)
75 .ctx("picking a line")?
76 .unwrap_or_default()
77 }
78 };
79
80 let (line, kind) = choice
81 .split_once(":")
82 .map(|(line, kind)| (line.parse::<usize>().ok(), kind))
83 .unwrap_or_default();
84
85 if kind.trim() == OTP_CODE {
86 let code = match line {
87 Some(l) => otp(&content[l])?,
88 None => otp(&content.join("\n"))?,
89 };
90 (vec![code], "0".to_owned())
91 } else {
92 (content, line.map(|l| l.to_string()).unwrap_or_default())
93 }
94 } else {
95 (vec![], opt.line)
96 };
97 opt.line = line;
98
99 match &opt.mode {
100 ShowMode::Clip | ShowMode::QrCode => {
101 return send(&cfg, &opt);
105 }
106 _ => {
107 }
109 }
110
111 if content.is_empty() {
112 content = fetch(&cfg, &opt).ctx("loading content2")?;
113 }
114 eprintln!("line {}", opt.line);
115 let secret = if opt.line.is_empty() {
116 content.join("\n")
117 } else {
118 let line = opt
119 .line
120 .parse::<usize>()
121 .map_err(LogicError::new)
122 .ctx(format!("parsing line number for {:?} option", opt.mode))?;
123 content.get(line).map(|l| l.to_owned()).unwrap_or_default()
124 };
125
126 match opt.mode {
127 ShowMode::Output => print!("{}", secret),
128 ShowMode::Clip => unreachable!("this is handled by pass"),
129 ShowMode::QrCode => unreachable!("this is handled by pass"),
130 ShowMode::Type => typeit(&cfg, &secret)?,
131 }
132
133 Ok(())
134}
135fn find(cfg: &Config, opt: &ShowOpt) -> PassFuResult<Vec<String>> {
137 let mut base = cfg.dir.clone();
138 if let Some(ref p) = opt.path {
139 base.extend(p.components().filter(|c| !matches!(c, Component::RootDir)));
141 if !base.exists() {
142 let mut basefile = base.as_os_str().to_owned();
144 basefile.push(".gpg");
145 let basefile: PathBuf = basefile.into();
146 if basefile.exists() && (basefile.is_file() || basefile.is_symlink()) {
147 return Ok(vec![p.to_str().unwrap_or_default().to_owned()]);
149 } else {
150 eprintln!("base path does not exist {basefile:?}");
152 }
153 }
154 }
155
156 base =
157 assert_child_io(&cfg.find, base.canonicalize()).ctx(format!("searching dir {base:?}"))?;
158
159 let child = std::process::Command::new(&cfg.find)
161 .arg(base)
162 .args(["-iname", "*.gpg"])
163 .stdout(Stdio::piped())
164 .stderr(Stdio::inherit())
165 .spawn();
166 let mut child = assert_child_io(&cfg.find, child)?;
167 let mut paths = vec![];
168 for line in BufReader::new(child.stdout.as_mut().expect("find output as instructed")).lines() {
169 let path = assert_child_io(
170 &cfg.find,
171 PathBuf::from(assert_child_io(&cfg.find, line)?)
172 .with_extension("")
173 .strip_prefix(&cfg.dir)
174 .map(|path| path.to_str().unwrap_or_default().to_owned())
175 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)),
176 )?;
177 paths.push(path);
178 }
179
180 assert_child_status(&cfg.find, child.wait())?;
181 Ok(paths)
182}
183fn pick(cfg: &Config, items: Vec<String>) -> PassFuResult<Option<String>> {
185 if items.len() == 0 {
186 return Ok(None);
188 }
189 if items.len() == 1 {
190 return Ok(items.into_iter().next());
192 }
193
194 let child = std::process::Command::new(&cfg.picker)
196 .args(&cfg.picker_args)
197 .stdin(Stdio::piped())
198 .stdout(Stdio::piped())
199 .stderr(Stdio::inherit())
200 .spawn();
201 let mut child = assert_child_io(&cfg.picker, child)?;
202
203 let picker = cfg.picker.clone();
204 let mut feed = child.stdin.take().expect("picker input as instructed");
205 std::thread::spawn(move || {
206 for path in items {
207 assert_child_io(&picker, feed.write_all(path.as_bytes())).unwrap();
208 assert_child_io(&picker, feed.write_all(b"\n")).unwrap();
209 }
210 assert_child_io(&picker, feed.flush()).unwrap();
211 });
212
213 let mut paths = vec![];
214 for path in BufReader::new(child.stdout.as_mut().expect("picker output as instructed")).lines()
215 {
216 paths.push(assert_child_io(&cfg.picker, path)?);
217 }
218
219 assert_child_status(&cfg.picker, child.wait())?;
220
221 Ok(match &mut paths[..] {
222 [] => Err(LogicError::new("No entries picked")),
223 [_, _, ..] => Err(LogicError::new("Too many entries picked")),
224 [single] => Ok(Some(std::mem::take(single))),
225 }?)
226}
227fn fetch(cfg: &Config, opt: &ShowOpt) -> PassFuResult<Vec<String>> {
229 let child = std::process::Command::new(&cfg.pass)
230 .args(&opt.path)
231 .stdout(Stdio::piped())
232 .stderr(Stdio::inherit())
233 .stdin(Stdio::piped())
234 .spawn();
235 let mut child = assert_child_io(&cfg.pass, child)?;
236
237 let mut lines = vec![];
238
239 for line in BufReader::new(child.stdout.as_mut().expect("pass output as instructed")).lines() {
240 lines.push(assert_child_io(&cfg.pass, line)?);
241 }
242
243 assert_child_status(&cfg.pass, child.wait())?;
244 Ok(lines)
245}
246
247fn otp(otpauth_url: &str) -> PassFuResult<String> {
249 let totp = totp_rs::TOTP::from_url(otpauth_url)?;
250 Ok(totp.generate_current()?)
251}
252fn send(cfg: &Config, opt: &ShowOpt) -> PassFuResult<()> {
254 let option_fmt = |o: &str, v: &String| {
255 if v.is_empty() {
256 o.to_owned()
257 } else {
258 format!("{o}={v}")
259 }
260 };
261 let mode_arg = match opt.mode {
262 ShowMode::Output if opt.line.is_empty() => {
263 None
265 }
266 ShowMode::Clip => Some(option_fmt("--clip", &opt.line)),
267 ShowMode::QrCode => Some(option_fmt("--qrcode", &opt.line)),
268 ref mode => unimplemented!("The original pass does not implement {mode:?}"),
269 };
270
271 let status = std::process::Command::new(&cfg.pass)
272 .args(mode_arg)
273 .args(&opt.path)
274 .status();
275
276 assert_child_status(&cfg.pass, status)
277}
278
279fn typeit(cfg: &Config, secret: &str) -> PassFuResult<()> {
281 let child = std::process::Command::new(&cfg.typist)
282 .args(&cfg.typist_args)
283 .stdout(Stdio::inherit())
284 .stderr(Stdio::inherit())
285 .stdin(Stdio::piped())
286 .spawn();
287 let mut child = assert_child_io(&cfg.picker, child)?;
288 let feed = child.stdin.as_mut().expect("typist input as instructed");
289 assert_child_io(&cfg.typist, feed.write_all(secret.as_bytes())).unwrap();
290 assert_child_status(&cfg.pass, child.wait())
291}