#![allow(clippy::doc_markdown)]
use crate::locked;
use crate::prelude::Error;
#[derive(Copy, Clone, Debug)]
pub enum InputKind {
Secret,
Visible,
}
pub fn prompt(
title: &str,
message: &str,
button: &str,
kind: InputKind,
) -> Result<locked::Password, Error> {
#[cfg(target_os = "macos")]
{
imp::prompt(title, message, button, kind)
}
#[cfg(not(target_os = "macos"))]
{
let _ = (title, message, button, kind);
Err(Error::NativePromptUnsupported)
}
}
pub fn prompt_master_password(
title: &str,
message: &str,
) -> Result<locked::Password, Error> {
prompt(title, message, "Unlock", InputKind::Secret)
}
#[cfg(target_os = "macos")]
mod imp {
use std::process::Command;
use zeroize::Zeroize as _;
use super::{locked, Error, InputKind};
fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'\\' | '"' => {
out.push('\\');
out.push(ch);
}
_ => out.push(ch),
}
}
out.push('"');
out
}
const MARKER: &str = ", text returned:";
pub fn prompt(
title: &str,
message: &str,
button: &str,
kind: InputKind,
) -> Result<locked::Password, Error> {
let hidden = match kind {
InputKind::Secret => "with hidden answer",
InputKind::Visible => "",
};
let script = format!(
"display dialog {msg} with title {title} \
default answer \"\" {hidden} \
buttons {{\"Cancel\", {btn}}} default button {btn} \
with icon caution",
msg = escape(message),
title = escape(title),
btn = escape(button),
);
let mut output = Command::new("/usr/bin/osascript")
.arg("-e")
.arg(&script)
.output()
.map_err(|e| Error::NativePromptFailed {
code: e.raw_os_error().unwrap_or(-1),
stage: "osascript spawn",
})?;
let result = extract_password(&output);
output.stdout.zeroize();
output.stderr.zeroize();
result
}
fn extract_password(
output: &std::process::Output,
) -> Result<locked::Password, Error> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("User canceled") || stderr.contains("-128") {
return Err(Error::PinentryCancelled);
}
return Err(Error::NativePromptFailed {
code: output.status.code().unwrap_or(-1),
stage: "osascript exit",
});
}
let Ok(stdout) = std::str::from_utf8(&output.stdout) else {
return Err(Error::NativePromptFailed {
code: 0,
stage: "osascript stdout utf8",
});
};
let value_str = stdout
.find(MARKER)
.map(|idx| stdout[idx + MARKER.len()..].trim_end_matches('\n'))
.ok_or(Error::NativePromptFailed {
code: 0,
stage: "osascript stdout parse",
})?;
let mut buf = locked::Vec::new();
buf.extend(value_str.as_bytes().iter().copied());
Ok(locked::Password::new(buf))
}
}