use super::{
no_args_or_input, no_pipeline, options, record, refuse_in_transaction, stderr_tail, MAX_NAMES,
RUN_TIMEOUT_SECS,
};
use crate::safety;
use crate::value::Value;
use anyhow::Result;
use std::collections::HashSet;
use std::process::Output;
const ID_MAX: usize = 128;
const NO_PACKAGE_FOUND: u32 = 0x8A15_0014;
const NO_APPLICABLE_UPDATE: u32 = 0x8A15_002B;
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Ran {
Done,
NoChange,
NotFound,
}
pub(super) fn classify(code: u32) -> std::result::Result<Ran, u32> {
match code {
0 => Ok(Ran::Done),
NO_APPLICABLE_UPDATE => Ok(Ran::NoChange),
NO_PACKAGE_FOUND => Ok(Ran::NotFound),
other => Err(other),
}
}
pub(super) fn id_ok(id: &str) -> bool {
!id.is_empty()
&& id.len() <= ID_MAX
&& !id.starts_with('-')
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-' | b'_' | b'+'))
}
pub(super) fn install(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "pkg_install";
no_pipeline(B, &input)?;
let ids = one_list(B, &args)?;
refuse_in_transaction(B)?;
for id in &ids {
let id = id.as_str();
let argv = [
"show",
"--id",
id,
"--exact",
"--source",
"winget",
"--accept-source-agreements",
"--disable-interactivity",
];
if winget(B, &argv)? == Ran::NotFound {
return Err(safety::not_found(B, "package", id));
}
}
for id in &ids {
let id = id.as_str();
let argv = [
"install",
"--id",
id,
"--exact",
"--source",
"winget",
"--accept-package-agreements",
"--accept-source-agreements",
"--disable-interactivity",
"--silent",
];
if winget(B, &argv)? == Ran::NotFound {
return Err(safety::not_found(B, "package", id));
}
}
let installed = ids
.iter()
.map(|id| record(vec![("name", Value::Str(id.clone()))]))
.collect();
Ok(record(vec![("installed", Value::Array(installed))]))
}
pub(super) fn remove(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "pkg_remove";
no_pipeline(B, &input)?;
let ids = one_list(B, &args)?;
refuse_in_transaction(B)?;
for id in &ids {
let argv = [
"list",
"--id",
id.as_str(),
"--exact",
"--source",
"winget",
"--accept-source-agreements",
"--disable-interactivity",
];
if winget(B, &argv)? == Ran::NotFound {
return Err(safety::not_found(B, "installed package", id));
}
}
for id in &ids {
let argv = [
"uninstall",
"--id",
id.as_str(),
"--exact",
"--source",
"winget",
"--accept-source-agreements",
"--disable-interactivity",
"--silent",
];
if winget(B, &argv)? == Ran::NotFound {
return Err(safety::not_found(B, "installed package", id));
}
}
let removed = ids.iter().map(|id| Value::Str(id.clone())).collect();
Ok(record(vec![("removed", Value::Array(removed))]))
}
pub(super) fn update(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "pkg_update";
no_args_or_input(B, &args, &input)?;
refuse_in_transaction(B)?;
match winget(B, &["source", "update", "--disable-interactivity"])? {
Ran::Done | Ran::NoChange => Ok(record(vec![("updated", Value::Bool(true))])),
Ran::NotFound => Err(safety::tool_failed_code(
B,
"winget",
NO_PACKAGE_FOUND,
"winget reported that no source matched",
)),
}
}
pub(super) fn upgrade(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "pkg_upgrade";
no_pipeline(B, &input)?;
if args.len() > 1 {
return Err(safety::arg_err(format!(
"{B} takes an optional options Record"
)));
}
let opts = options(B, args.first(), &["id"])?;
let id = match opts.get("id") {
None => None,
Some(Value::Str(id)) if id_ok(id) => Some(id.clone()),
Some(Value::Str(id)) => {
return Err(safety::bad_arg(B, WINGET_ID_EXPECTED, &format!("{id:?}")))
}
Some(other) => {
return Err(safety::bad_arg(
B,
"String for option id",
other.type_name(),
))
}
};
refuse_in_transaction(B)?;
let mut argv = vec!["upgrade"];
match &id {
Some(id) => argv.extend(["--id", id.as_str(), "--exact"]),
None => argv.push("--all"),
}
argv.extend([
"--source",
"winget",
"--accept-package-agreements",
"--accept-source-agreements",
"--disable-interactivity",
"--silent",
]);
let changed = match winget(B, &argv)? {
Ran::Done => true,
Ran::NoChange => false,
Ran::NotFound => {
return Err(safety::not_found(
B,
"package",
id.as_deref().unwrap_or("(all)"),
))
}
};
Ok(record(vec![("changed", Value::Bool(changed))]))
}
const WINGET_ID_EXPECTED: &str = "a winget identifier: letters, digits, '.', '-', '_' or '+', \
at most 128 characters, not beginning with '-'";
fn one_list(builtin: &str, args: &[Value]) -> Result<Vec<String>> {
if args.len() != 1 {
return Err(safety::arg_err(format!(
"{builtin} takes one Array of package identifiers"
)));
}
identifiers(builtin, &args[0])
}
fn identifiers(builtin: &str, value: &Value) -> Result<Vec<String>> {
let items = match value {
Value::Array(items) => items,
other => {
return Err(safety::bad_arg(
builtin,
"Array of String",
other.type_name(),
))
}
};
if items.is_empty() {
return Err(safety::arg_err(format!(
"{builtin}: identifiers must be a nonempty Array of String"
)));
}
if items.len() > MAX_NAMES {
return Err(safety::arg_err(format!(
"{builtin}: at most {MAX_NAMES} identifiers per call, got {}",
items.len()
)));
}
let mut ids = Vec::with_capacity(items.len());
let mut seen = HashSet::new();
for item in items {
let text = match item {
Value::Str(s) => s.clone(),
other => return Err(safety::bad_arg(builtin, "String", other.type_name())),
};
if !id_ok(&text) {
return Err(safety::bad_arg(
builtin,
WINGET_ID_EXPECTED,
&format!("{text:?}"),
));
}
if !seen.insert(text.to_ascii_lowercase()) {
return Err(safety::arg_err(format!(
"{builtin}: package {text:?} is named more than once"
)));
}
ids.push(text);
}
Ok(ids)
}
fn winget(builtin: &str, args: &[&str]) -> Result<Ran> {
let output = run(builtin, args)?;
let Some(code) = output.status.code() else {
return Err(safety::tool_failed(builtin, "winget", None, &tail(&output)));
};
match classify(code as u32) {
Ok(ran) => Ok(ran),
Err(code) => Err(safety::tool_failed_code(
builtin,
"winget",
code,
&tail(&output),
)),
}
}
fn run(builtin: &str, args: &[&str]) -> Result<Output> {
use std::process::Stdio;
use std::time::Duration;
let mut cmd = crate::safety::child_command("winget");
cmd.args(args).stdin(Stdio::null());
let run = super::super::run_tree_cmd(
cmd,
None,
Duration::from_secs(RUN_TIMEOUT_SECS),
super::super::Keep::Head(usize::MAX),
super::super::Keep::Head(usize::MAX),
)
.map_err(|e| match e {
super::super::TreeError::Spawn(e) | super::super::TreeError::Wait(e) => {
safety::spawn_error(builtin, "winget", &e)
}
})?;
match run.status {
Some(status) if !run.timed_out => Ok(Output {
status,
stdout: run.stdout,
stderr: run.stderr,
}),
_ => Err(safety::budget_exceeded(
builtin,
format!(
"`winget` did not finish within {RUN_TIMEOUT_SECS} s and its process tree \
was killed; whether it changed anything is UNKNOWN and must be checked"
),
"check the state with pkg_list or pkg_info before retrying",
)),
}
}
fn tail(output: &Output) -> String {
let bytes = if output.stderr.iter().any(|b| !b.is_ascii_whitespace()) {
&output.stderr
} else {
&output.stdout
};
stderr_tail(bytes)
}