setdns 0.1.0

Temporarily apply and restore system DNS configuration, including global and split DNS.
Documentation
use std::process::{Command, ExitStatus};

use base64::{Engine as _, engine::general_purpose::STANDARD};

use crate::Error;

#[derive(Debug, thiserror::Error)]
pub(crate) enum PowerShellError {
    #[error("failed to launch PowerShell for {operation}: {source}")]
    Launch {
        operation: &'static str,
        #[source]
        source: std::io::Error,
    },

    #[error(
        "PowerShell failed for {operation} with status {status}: stdout: {stdout}; stderr: \
         {stderr}"
    )]
    Status {
        operation: &'static str,
        status: Status,
        stdout: OutputText,
        stderr: OutputText,
    },
}

impl From<PowerShellError> for Error {
    fn from(error: PowerShellError) -> Self {
        Self::Backend(Box::new(error))
    }
}

#[derive(Clone, Debug)]
pub(crate) struct OutputText(Vec<u8>);

impl std::fmt::Display for OutputText {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&String::from_utf8_lossy(&self.0))
    }
}

#[derive(Copy, Clone, Debug)]
pub(crate) struct Status(ExitStatus);

impl std::fmt::Display for Status {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.0.code() {
            Some(code) => write!(formatter, "exit code {code}"),
            None => formatter.write_str("terminated by signal"),
        }
    }
}

pub(crate) fn run(operation: &'static str, script: &str) -> Result<Vec<u8>, PowerShellError> {
    log::debug!("running PowerShell for {operation}");
    let encoded = encode_command(script);
    let output = Command::new("powershell.exe")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-EncodedCommand",
            &encoded,
        ])
        .output()
        .map_err(|source| PowerShellError::Launch { operation, source })?;
    if output.status.success() {
        log::debug!(
            "PowerShell completed {operation}: stdout={} bytes, stderr={} bytes",
            output.stdout.len(),
            output.stderr.len()
        );
        Ok(output.stdout)
    } else {
        log::warn!(
            "PowerShell failed during {operation}: {}",
            Status(output.status)
        );
        Err(PowerShellError::Status {
            operation,
            status: Status(output.status),
            stdout: OutputText(output.stdout),
            stderr: OutputText(output.stderr),
        })
    }
}

fn encode_command(script: &str) -> String {
    let mut bytes = Vec::with_capacity(script.len() * 2);
    for unit in script.encode_utf16() {
        bytes.extend_from_slice(&unit.to_le_bytes());
    }
    STANDARD.encode(bytes)
}