hex-patch 1.12.5

HexPatch is a binary patcher and editor with terminal user interface (TUI), it's capable of disassembling instructions and assembling patches. It supports a variety of architectures and file formats. Also, it can edit remote files via SSH.
Documentation
use std::{error::Error, fmt::Display, path::PathBuf, sync::Arc};

use russh::client::{self, AuthResult, Handler};
use russh::keys::key::PrivateKeyWithHashAlg;
use russh_sftp::client::SftpSession;

use crate::app::files::path;

pub struct SSHClient;
impl Handler for SSHClient {
    type Error = russh::Error;

    async fn check_server_key(
        &mut self,
        _server_public_key: &russh::keys::ssh_key::PublicKey,
    ) -> Result<bool, Self::Error> {
        Ok(true)
    }
}

pub struct Connection {
    runtime: tokio::runtime::Runtime,
    sftp: SftpSession,
    connection_str: String,
}

impl Connection {
    fn get_key_files() -> Result<(PathBuf, PathBuf), String> {
        let home_dir = dirs::home_dir().ok_or_else(|| t!("errors.home_not_found").to_string())?;

        let ssh_dir = home_dir.join(".ssh");
        if !ssh_dir.is_dir() {
            return Err(t!("errors.ssh_dir_not_found").into());
        }
        if ssh_dir.join("id_rsa").is_file() {
            Ok((ssh_dir.join("id_rsa"), ssh_dir.join("id_rsa.pub")))
        } else if ssh_dir.join("id_ed25519").is_file() {
            Ok((ssh_dir.join("id_ed25519"), ssh_dir.join("id_ed25519.pub")))
        } else if ssh_dir.join("id_ecdsa").is_file() {
            Ok((ssh_dir.join("id_ecdsa"), ssh_dir.join("id_ecdsa.pub")))
        } else if ssh_dir.join("id_dsa").is_file() {
            Ok((ssh_dir.join("id_dsa"), ssh_dir.join("id_dsa.pub")))
        } else {
            Err(t!("errors.no_private_key").into())
        }
    }

    pub fn new(connection_str: &str, password: Option<&str>) -> Result<Self, Box<dyn Error>> {
        let runtime = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()?;
        let (username, host) = connection_str
            .split_once('@')
            .ok_or_else(|| Box::<dyn Error>::from(t!("errors.invalid_connection_string")))?;

        let (hostname, port) =
            host.split_once(':')
                .map_or(Ok((host, 22)), |(hostname, port)| {
                    port.parse::<u16>()
                        .map(|port| (hostname, port))
                        .map_err(|_| Box::<dyn Error>::from(t!("errors.invalid_port")))
                })?;

        let config = client::Config::default();

        let mut session = runtime.block_on(client::connect(
            config.into(),
            (hostname, port),
            SSHClient {},
        ))?;
        if let Some(password) = password {
            if let AuthResult::Failure {
                remaining_methods: _,
                partial_success: _,
            } = runtime.block_on(session.authenticate_password(username, password))?
            {
                return Err(t!("errors.authentication_failed").into());
            }
        } else {
            let (private_key, _public_key) = Self::get_key_files()?;
            let keypair = russh::keys::load_secret_key(private_key, None)?;
            let keypair = PrivateKeyWithHashAlg::new(Arc::new(keypair), None);
            if let AuthResult::Failure {
                remaining_methods: _,
                partial_success: _,
            } = runtime.block_on(session.authenticate_publickey(username, keypair))?
            {
                return Err(t!("errors.authentication_failed").into());
            }
        }

        let channel = runtime.block_on(session.channel_open_session())?;
        runtime.block_on(channel.request_subsystem(true, "sftp"))?;

        let sftp = runtime.block_on(SftpSession::new(channel.into_stream()))?;

        Ok(Self {
            runtime,
            sftp,
            connection_str: connection_str.to_string(),
        })
    }

    pub fn separator(&self) -> char {
        match self.runtime.block_on(self.sftp.canonicalize("/")) {
            Ok(_) => '/',
            Err(_) => '\\',
        }
    }

    pub fn canonicalize(&self, path: &str) -> Result<String, Box<dyn Error>> {
        Ok(self.runtime.block_on(self.sftp.canonicalize(path))?)
    }

    pub fn read(&self, path: &str) -> Result<Vec<u8>, Box<dyn Error>> {
        let remote_file = self.runtime.block_on(self.sftp.read(path))?;
        Ok(remote_file)
    }

    pub fn mkdirs(&self, path: &str) -> Result<(), Box<dyn Error>> {
        self.runtime.block_on(async {
            let mut paths = vec![path];
            let mut current = path;
            while let Some(parent) = path::parent(current) {
                paths.push(parent);
                current = parent;
            }
            paths.reverse();
            for path in paths {
                if self.sftp.read_dir(path).await.is_ok() {
                    continue;
                };
                self.sftp.create_dir(path).await?;
            }
            Ok::<(), Box<dyn Error>>(())
        })?;
        Ok(())
    }

    pub fn create(&self, path: &str) -> Result<(), Box<dyn Error>> {
        self.runtime.block_on(self.sftp.create(path))?;
        Ok(())
    }

    pub fn write(&self, path: &str, data: &[u8]) -> Result<(), Box<dyn Error>> {
        self.runtime.block_on(self.sftp.write(path, data))?;
        Ok(())
    }

    pub fn ls(&self, path: &str) -> Result<Vec<String>, Box<dyn Error>> {
        let dir = self.runtime.block_on(self.sftp.read_dir(path))?;
        dir.into_iter()
            .map(|entry| Ok(path::join(path, &entry.file_name(), self.separator()).to_string()))
            .collect()
    }

    pub fn is_file(&self, path: &str) -> bool {
        self.runtime
            .block_on(self.sftp.metadata(path))
            .is_ok_and(|metadata| !metadata.is_dir())
    }

    pub fn is_dir(&self, path: &str) -> bool {
        self.runtime
            .block_on(self.sftp.metadata(path))
            .is_ok_and(|metadata| metadata.is_dir())
    }
}

impl Display for Connection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.connection_str)
    }
}