#![forbid(unsafe_code)]
use crate::errors::SshCliError;
use tokio::io::{AsyncRead, AsyncWrite};
pub use super::connection::ConnectionConfig;
#[derive(Debug, Clone)]
pub struct ExecutionOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: Option<i32>,
pub truncated_stdout: bool,
pub truncated_stderr: bool,
pub duration_ms: u64,
}
#[derive(Debug, Clone)]
pub struct TransferResult {
pub bytes_transferred: u64,
pub duration_ms: u64,
}
pub(crate) const EXEC_CAPTURE_HARD_MAX_BYTES: usize = 16 * 1024 * 1024;
#[must_use]
pub(crate) fn exec_capture_byte_cap(max_chars: usize) -> usize {
if max_chars == 0 {
return 0;
}
max_chars
.saturating_mul(4)
.saturating_add(4)
.min(EXEC_CAPTURE_HARD_MAX_BYTES)
}
pub(crate) fn append_capped(buf: &mut Vec<u8>, data: &[u8], cap: usize, truncated: &mut bool) {
if data.is_empty() {
return;
}
if cap == 0 || buf.len() >= cap {
*truncated = true;
return;
}
let room = cap - buf.len();
if data.len() <= room {
buf.extend_from_slice(data);
} else {
buf.extend_from_slice(&data[..room]);
*truncated = true;
}
}
pub use super::session_io::truncate_utf8;
pub(crate) use super::session_io::take_utf8_capped;
use async_trait::async_trait;
use std::path::Path;
pub trait TunnelChannel: AsyncRead + AsyncWrite + Unpin + Send {}
impl<T> TunnelChannel for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
#[async_trait]
pub trait SshClientTrait: Send + Sync + 'static {
async fn connect(cfg: ConnectionConfig) -> Result<Box<Self>, SshCliError>
where
Self: Sized;
async fn run_command(
&mut self,
cmd: &str,
max_chars: usize,
stdin_data: Option<Vec<u8>>,
) -> Result<ExecutionOutput, SshCliError>;
async fn upload(
&self,
local: &Path,
remote: &Path,
) -> Result<TransferResult, SshCliError>;
async fn download(
&self,
remote: &Path,
local: &Path,
) -> Result<TransferResult, SshCliError>;
async fn open_tunnel_channel(
&self,
remote_host: &str,
remote_port: u16,
origin_addr: &str,
origin_port: u16,
) -> Result<Box<dyn TunnelChannel>, SshCliError>;
async fn disconnect(&self) -> Result<(), SshCliError>;
}
#[cfg(test)]
pub mod mocks {
use super::*;
use mockall::mock;
mock! {
pub SshClient {}
#[async_trait]
impl crate::ssh::client::SshClientTrait for SshClient {
async fn connect(cfg: ConnectionConfig) -> Result<Box<Self>, SshCliError>;
async fn run_command(&mut self, cmd: &str, max_chars: usize, stdin_data: Option<Vec<u8>>) -> Result<ExecutionOutput, SshCliError>;
async fn upload(&self, local: &Path, remote: &Path) -> Result<TransferResult, SshCliError>;
async fn download(&self, remote: &Path, local: &Path) -> Result<TransferResult, SshCliError>;
async fn open_tunnel_channel(
&self,
remote_host: &str,
remote_port: u16,
origin_addr: &str,
origin_port: u16,
) -> Result<Box<dyn TunnelChannel>, SshCliError>;
async fn disconnect(&self) -> Result<(), SshCliError>;
}
}
}
#[cfg(feature = "ssh-real")]
#[path = "client_real.rs"]
mod real;
#[cfg(feature = "ssh-real")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssh-real")))]
pub use real::{SshClient, ClientHandler};
#[cfg(not(feature = "ssh-real"))]
#[path = "client_stub.rs"]
mod stub;
#[cfg(not(feature = "ssh-real"))]
#[cfg_attr(docsrs, doc(cfg(not(feature = "ssh-real"))))]
pub use stub::SshClient;
#[cfg(test)]
#[path = "client_tests.rs"]
mod tests;