use std::io::{BufRead, IsTerminal as _};
use std::str::FromStr as _;
use shep_core::dogs::manifest::Manifest;
use shep_core::permissions::{Grants, Permission};
use crate::commands::copy::confirm::{Answer, Go, decide, read_answer};
use crate::exit::ExitCode;
use crate::output::Streams;
const PERMISSIONS_NOTICE: &str = "dog_permissions";
const PERMISSION_DROPPED_NOTICE: &str = "dog_permission_dropped";
#[derive(Debug, Clone, Copy)]
pub(super) struct Mode {
pub(super) yes: bool,
pub(super) stdin_is_terminal: bool,
}
#[derive(Debug)]
pub(super) struct Listed {
pub(super) name: String,
pub(super) required: bool,
pub(super) reason: String,
}
#[derive(Debug)]
pub(super) enum Review {
Undeclared,
Declared {
listed: Vec<Listed>,
dropped: Vec<String>,
},
}
#[derive(Debug)]
pub(super) struct UnknownRequired(Vec<String>);
impl UnknownRequired {
pub(super) fn report(&self, streams: &mut Streams<'_>, name: &str, done: &str) -> ExitCode {
let names = self
.0
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<_>>()
.join(", ");
let message = format!(
"{name} requires {names}, which this shep does not know, so it was not {done}: \
it would run half-working. Upgrade shep, or use a version of {name} that does \
not require it"
);
streams.fail(ExitCode::InvalidConfig, &message)
}
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Verdict {
Grant(Option<Grants>),
Declined,
}
pub(super) fn review(manifest: Option<&Manifest>) -> Result<Review, UnknownRequired> {
let Some(manifest) = manifest else {
return Ok(Review::Undeclared);
};
let mut listed: Vec<Listed> = Vec::new();
let mut dropped = Vec::new();
let mut unknown_required = Vec::new();
for declared in &manifest.permissions {
match (Permission::from_str(&declared.name), declared.required) {
(Ok(_), _) if listed.iter().any(|seen| seen.name == declared.name) => {}
(Ok(_), required) => listed.push(Listed {
name: declared.name.clone(),
required,
reason: declared.reason.clone(),
}),
(Err(_), true) => unknown_required.push(declared.name.clone()),
(Err(_), false) => dropped.push(declared.name.clone()),
}
}
if unknown_required.is_empty() {
Ok(Review::Declared { listed, dropped })
} else {
Err(UnknownRequired(unknown_required))
}
}
pub(super) fn consent(
streams: &mut Streams<'_>,
name: &str,
verb: &str,
review: &Review,
mode: Mode,
reader: &mut dyn BufRead,
) -> Result<Verdict, ExitCode> {
let (granted, listing, question) = match review {
Review::Undeclared => (
None,
format!(
"{name} declares nothing, runs with full access. {}",
not_confined(name)
),
format!("{verb} {name} with full access? [y/N] "),
),
Review::Declared { listed, dropped } => {
for dropped in dropped {
streams.aside(
PERMISSION_DROPPED_NOTICE,
&format!(
"{name} asks for the optional permission `{dropped}`, which this shep \
does not know; it is dropped and not granted"
),
);
}
let grants = Grants::from_names(listed.iter().map(|entry| entry.name.clone()));
if listed.is_empty() {
return Ok(Verdict::Grant(Some(grants)));
}
let lines = listed
.iter()
.map(|entry| {
let kind = if entry.required {
"required"
} else {
"optional"
};
format!(" {} ({kind}): {}", entry.name, entry.reason)
})
.collect::<Vec<_>>()
.join("\n");
(
Some(grants),
format!(
"{name} declares these permissions:\n{lines}\n{}",
not_confined(name)
),
format!("{verb} {name} and grant it these? [y/N] "),
)
}
};
let go = decide(false, mode.yes, mode.stdin_is_terminal, true);
if go == Go::Refuse {
return Err(streams.fail(
ExitCode::Usage,
&format!(
"nothing was changed: `shep {verb}` asks before it grants a dog its \
permissions, and this run has no terminal to ask on. Pass --yes to grant \
everything {name} declares"
),
));
}
streams.aside(PERMISSIONS_NOTICE, &listing);
if go == Go::Ask {
streams.aside("confirm", &question);
match read_answer(reader) {
Answer::Yes => {}
Answer::No => return Ok(Verdict::Declined),
Answer::Failed(reason) => {
return Err(streams.fail(
ExitCode::Failure,
&format!("nothing was changed: the question could not be read ({reason})"),
));
}
}
}
Ok(Verdict::Grant(granted))
}
pub(super) struct Input {
pub(super) is_terminal: bool,
pub(super) reader: Box<dyn BufRead + Send>,
}
impl Input {
pub(super) fn stdin() -> Self {
Self {
is_terminal: std::io::stdin().is_terminal(),
reader: Box::new(std::io::BufReader::new(std::io::stdin())),
}
}
}
pub(super) fn consent_from(
streams: &mut Streams<'_>,
name: &str,
verb: &str,
review: &Review,
yes: bool,
input: &mut Input,
) -> Result<Verdict, ExitCode> {
let mode = Mode {
yes,
stdin_is_terminal: input.is_terminal,
};
consent(streams, name, verb, review, mode, &mut input.reader)
}
pub(super) fn not_confined(name: &str) -> String {
format!(
"Grants keep {name}'s requests in bounds and do not confine it: it runs as you and \
can do anything you can."
)
}