use std::io::Read;
use std::time::Duration;
use anyhow::{bail, Result};
use crate::cli::env_resolver::Source;
use crate::cli::route;
use crate::cli::trace::CommandTrace;
use crate::session::backend::open_backend;
use crate::session::with_scratch_recovery;
const TYPE_TIMEOUT: Duration = Duration::from_secs(30);
pub async fn run(
browser: Option<String>,
text: Option<String>,
stdin: bool,
submit: bool,
press_sequentially: bool,
target: Option<String>,
) -> Result<()> {
let mut trace = CommandTrace::new("type");
let value = match (text, stdin) {
(Some(_), true) => bail!("`--text` and `--stdin` are mutually exclusive; pass one"),
(None, false) => bail!("one of `--text` or `--stdin` is required"),
(Some(t), false) => t,
(None, true) => read_stdin()?,
};
let result = run_inner(
browser,
&value,
submit,
press_sequentially,
target,
&mut trace,
)
.await;
trace.finish(result)?;
println!("typed {} characters", value.chars().count());
Ok(())
}
fn read_stdin() -> Result<String> {
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.map_err(|e| anyhow::anyhow!("reading stdin: {e}"))?;
let value = buf.strip_suffix('\n').unwrap_or(&buf);
let value = value.strip_suffix('\r').unwrap_or(value);
if value.trim().is_empty() {
bail!("stdin was empty; the resolver produced no value (a mis-scoped token?)");
}
if value.contains('\n') {
bail!(
"stdin held {} lines; a resolver should print exactly one secret \
(a warning banner on stdout?)",
value.lines().count()
);
}
Ok(value.to_string())
}
async fn run_inner(
browser: Option<String>,
value: &str,
submit: bool,
press_sequentially: bool,
target: Option<String>,
trace: &mut CommandTrace,
) -> Result<()> {
let r = route::preamble(browser, target.as_deref(), trace).await?;
let resolved = &r.resolved;
match (r.tab_name.clone(), target) {
(Some(name), None) => {
trace.route("named-tab").tab_name(&name);
let value = value.to_string();
route::run_named_tab(
&r,
&name,
"named tabs (`<browser>/<name>`) require a registered browser; \
external endpoints can't carry tab names",
move |b, target_id| {
let value = value.clone();
async move {
b.type_into_focused(
&target_id,
&value,
press_sequentially,
submit,
TYPE_TIMEOUT,
)
.await
}
},
)
.await
}
(None, None) => {
let browser_name = match &resolved.source {
Source::Registered { name } => name.clone(),
Source::External => bail!(
"`type` needs a registered browser or an explicit `--target`; \
external endpoints have no tab to focus"
),
};
trace.route("scratch");
let backend = open_backend(&resolved.endpoint, resolved.engine).await?;
let value = value.to_string();
with_scratch_recovery(&backend, &r.registry, &browser_name, move |b, target_id| {
let value = value.clone();
async move {
b.type_into_focused(
&target_id,
&value,
press_sequentially,
submit,
TYPE_TIMEOUT,
)
.await
}
})
.await
}
(None, Some(regex)) => {
trace.route("target-regex");
let backend = open_backend(&resolved.endpoint, resolved.engine).await?;
let target_id = target_matching(&backend, ®ex).await?;
let out = backend
.type_into_focused(&target_id, value, press_sequentially, submit, TYPE_TIMEOUT)
.await;
backend.release().await;
out
}
_ => unreachable!("mutual exclusion checked in preamble"),
}
}
pub(crate) async fn target_matching(
backend: &crate::session::backend::TabBackend,
regex: &str,
) -> Result<String> {
let re = regex::Regex::new(regex).map_err(|e| anyhow::anyhow!("bad --target regex: {e}"))?;
let targets = backend.live_targets().await?;
targets
.into_iter()
.find(|t| re.is_match(&t.url))
.map(|t| t.id)
.ok_or_else(|| anyhow::anyhow!("no tab matched `{regex}`"))
}
#[cfg(test)]
mod tests {
#[test]
fn trailing_newline_is_stripped_but_inner_content_kept() {
let cases = [
("hunter2\n", "hunter2"),
("hunter2\r\n", "hunter2"),
("hunter2", "hunter2"),
];
for (raw, want) in cases {
let v = raw.strip_suffix('\n').unwrap_or(raw);
let v = v.strip_suffix('\r').unwrap_or(v);
assert_eq!(v, want);
}
}
#[test]
fn a_password_may_contain_spaces_and_symbols() {
let raw = "p@ss word!+/=\n";
let v = raw.strip_suffix('\n').unwrap();
assert_eq!(v, "p@ss word!+/=");
assert!(!v.trim().is_empty());
assert!(!v.contains('\n'));
}
}