Documentation
/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

R50

Copyright (C) 2018-2019, 2021-2025  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2018-2019".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

//! # Client handler

use {
    std::{
        io::{BufReader, Error, ErrorKind, Read},
        os::unix::{
            net::UnixStream,
            process::CommandExt,
        },
        process::Command,
    },
    crate::{MAP_KIND, shared},
    blackhole::Job,
    nairud::{Array, Decoder, Encoder, Map, Nairud},
    namaste::UdsxUnixStream,
    r50::Result,
};

/// # Client handler
#[derive(Debug)]
pub struct ClientHandler {
    stream: UnixStream,
}

impl ClientHandler {

    /// # Makes new instance
    pub const fn new(stream: UnixStream) -> Self {
        Self {
            stream,
        }
    }

}

impl Job for ClientHandler {

    fn run(&mut self) -> Option<Box<dyn Job>> {
        let [stdin, stdout, stderr] = match unsafe {
            self.stream.recv_ioe(shared::ID_OF_IOE)
        } {
            Ok(std) => std,
            Err(err) => {
                eprintln!("[{stream:?}] <- failed to receive standard streams: {err:?}", stream=self.stream, err=err);
                return None;
            },
        };
        let cred = match self.stream.peer_cred() {
            Ok(cred) => cred,
            Err(err) => {
                eprintln!("[{stream:?}] <- failed to receive credentials: {err:?}", stream=self.stream, err=err);
                return None;
            },
        };
        let mut cmd = match recv_cmd(&mut self.stream) {
            Ok(cmd) => cmd,
            Err(err) => {
                eprintln!("[{stream:?}] <- failed to receive command: {err:?}", stream=self.stream, err=err);
                return None;
            },
        };

        let cmd_as_string = format!("{:?}", &cmd);
        match cmd.uid(cred.uid).gid(cred.gid).stdin(stdin).stdout(stdout).stderr(stderr).spawn() {
            Ok(mut proc) => {
                if let Err(err) = self.stream.encode(proc.id()) {
                    eprintln!(
                        "[{stream:?}] Failed to send process ID #{proc_id} to client: {err:?}", stream=self.stream, proc_id=proc.id(), err=err,
                    );
                }
                match proc.wait() {
                    Ok(status) => {
                        if let Err(err) = self.stream.encode(status.code()) {
                            eprintln!(
                                "[{stream:?}] Failed to send exit-status {status:?} to client: {err:?}",
                                stream=self.stream, status=status, err=err,
                            );
                        }
                    },
                    Err(err) => if let Err(err) = self.stream.encode(format!(
                        "Failed to wait for process #{proc_id}: {err:?}", proc_id=proc.id(), err=err,
                    )) {
                        eprintln!("[{stream:?}] Failed to send error message to client: {err:?}", stream=self.stream, err=err);
                    },
                };
            },
            Err(err) => if let Err(err) = self.stream.encode(format!(
                "Failed to run command {cmd:?}: {err:?}", cmd=cmd_as_string, err=err,
            )) {
                eprintln!("[{stream:?}] Failed to send error message to client: {err:?}", stream=self.stream, err=err);
            },
        };

        None
    }

}

/// # Receives command
fn recv_cmd(stream: &mut UnixStream) -> Result<Command> {
    let mut stream = BufReader::new(stream);
    let content_size = u16::try_from(stream.decode(MAP_KIND)?.ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing content size"))?)?;
    let mut stream = stream.take(content_size.into());
    let work_dir = String::try_from(stream.decode(MAP_KIND)?.ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing work directory"))?)?;
    let mut args = Array::try_from(stream.decode(MAP_KIND)?.ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing command"))?)?;
    let env_vars = match stream.decode(MAP_KIND)? {
        None => None,
        Some(env_vars) => Some(Map::try_from(env_vars)?),
    };
    if args.is_empty() {
        return Err(Error::new(ErrorKind::InvalidData, "Missing command name"));
    }

    let mut result = match args.remove(0) {
        Nairud::String(cmd) => Command::new(cmd),
        other => return Err(Error::new(
            ErrorKind::InvalidData, format!("Invalid command name: expected Nairud::String, got: {:?}", other),
        )),
    };

    result.env_clear();
    result.current_dir(work_dir);

    for arg in args {
        match arg {
            Nairud::String(arg) => result.arg(arg),
            other => return Err(Error::new(
                ErrorKind::InvalidData, format!("Invalid command argument: expected Nairud::String, got: {:?}", other),
            )),
        };
    }

    if let Some(env_vars) = env_vars {
        for (key, value) in env_vars.into_iter() {
            match value {
                Nairud::String(value) => result.env(
                    String::from_utf8(key).map_err(|_| Error::new(ErrorKind::InvalidData, "Key is not UTF-8 string"))?,
                    value,
                ),
                other => return Err(Error::new(
                    ErrorKind::InvalidData, format!("Invalid environment value of {:?}: expected Nairud::String, got: {:?}", key, other),
                )),
            };
        }
    }

    Ok(result)
}