svn 0.1.8

Async Rust SVN client for Subversion svn://, svn+ssh://, and ra_svn workflows.
Documentation
use super::SvnItem;
use super::encode_item;
use super::parse::{parse_repos_info, parse_server_error};
#[cfg(feature = "cyrus-sasl")]
use super::sasl::{CyrusSasl, SASL_CONTINUE, base64_decode, base64_encode};
use super::wire::encode_command_item;

use hmac::{Hmac, KeyInit, Mac};
use md5::Md5;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::time::Instant;
use tracing::debug;

use crate::{Capability, SvnError};

mod auth;
mod call;
mod handshake;
mod io;
#[cfg(test)]
mod tests;

type AuthMechanismChoice = (String, Option<Vec<u8>>);
type AuthMechanismChoices = Vec<AuthMechanismChoice>;

#[cfg(feature = "cyrus-sasl")]
trait SaslSecurityLayer: Send {
    fn max_outbuf(&self) -> u32;
    fn encode(&mut self, input: &[u8]) -> Result<Vec<u8>, SvnError>;
    fn decode(&mut self, input: &[u8]) -> Result<Vec<u8>, SvnError>;
}

#[cfg(feature = "cyrus-sasl")]
impl SaslSecurityLayer for CyrusSasl {
    fn max_outbuf(&self) -> u32 {
        CyrusSasl::max_outbuf(self)
    }

    fn encode(&mut self, input: &[u8]) -> Result<Vec<u8>, SvnError> {
        CyrusSasl::encode(self, input)
    }

    fn decode(&mut self, input: &[u8]) -> Result<Vec<u8>, SvnError> {
        CyrusSasl::decode(self, input)
    }
}

#[derive(Debug)]
pub(crate) struct CommandResponse {
    success: bool,
    params: Vec<SvnItem>,
    errors: Vec<SvnItem>,
}

impl CommandResponse {
    pub(crate) fn is_failure(&self) -> bool {
        !self.success
    }

    pub(crate) fn success_params(&self, ctx: &str) -> Result<&[SvnItem], SvnError> {
        if self.success {
            Ok(&self.params)
        } else {
            Err(self.failure(ctx))
        }
    }

    pub(crate) fn ensure_success(&self, ctx: &str) -> Result<(), SvnError> {
        let _ = self.success_params(ctx)?;
        Ok(())
    }

    pub(crate) fn failure(&self, ctx: &str) -> SvnError {
        SvnError::Server(self.failure_server_error().with_context(ctx.to_string()))
    }

    pub(crate) fn failure_server_error(&self) -> crate::ServerError {
        parse_server_error(&self.errors)
    }

    pub(crate) fn failure_message(&self) -> String {
        self.failure_server_error().message_summary()
    }
}

pub(crate) struct RaSvnConnectionConfig {
    pub(crate) username: Option<String>,
    pub(crate) password: Option<String>,
    #[cfg(feature = "cyrus-sasl")]
    pub(crate) host: String,
    #[cfg(feature = "cyrus-sasl")]
    pub(crate) local_addrport: Option<String>,
    #[cfg(feature = "cyrus-sasl")]
    pub(crate) remote_addrport: Option<String>,
    pub(crate) is_tunneled: bool,
    pub(crate) url: String,
    pub(crate) ra_client: String,
    pub(crate) read_timeout: Duration,
    pub(crate) write_timeout: Duration,
}

type DynRead = Box<dyn AsyncRead + Unpin + Send>;
type DynWrite = Box<dyn AsyncWrite + Unpin + Send>;

pub(crate) struct RaSvnConnection {
    read: DynRead,
    write: DynWrite,
    buf: Vec<u8>,
    pos: usize,
    write_buf: Vec<u8>,
    username: Option<String>,
    password: Option<String>,
    #[cfg(feature = "cyrus-sasl")]
    host: String,
    #[cfg(feature = "cyrus-sasl")]
    local_addrport: Option<String>,
    #[cfg(feature = "cyrus-sasl")]
    remote_addrport: Option<String>,
    is_tunneled: bool,
    url: String,
    ra_client: String,
    read_timeout: Duration,
    write_timeout: Duration,
    server_caps: Vec<String>,
    #[cfg(feature = "cyrus-sasl")]
    sasl: Option<Box<dyn SaslSecurityLayer>>,
}

impl RaSvnConnection {
    pub(crate) fn new(read: DynRead, write: DynWrite, config: RaSvnConnectionConfig) -> Self {
        Self {
            read,
            write,
            buf: Vec::new(),
            pos: 0,
            write_buf: Vec::new(),
            username: config.username,
            password: config.password,
            #[cfg(feature = "cyrus-sasl")]
            host: config.host,
            #[cfg(feature = "cyrus-sasl")]
            local_addrport: config.local_addrport,
            #[cfg(feature = "cyrus-sasl")]
            remote_addrport: config.remote_addrport,
            is_tunneled: config.is_tunneled,
            url: config.url,
            ra_client: config.ra_client,
            read_timeout: config.read_timeout,
            write_timeout: config.write_timeout,
            server_caps: Vec::new(),
            #[cfg(feature = "cyrus-sasl")]
            sasl: None,
        }
    }

    pub(crate) fn server_has_cap(&self, cap: &str) -> bool {
        self.server_caps.iter().any(|c| c == cap)
    }

    #[cfg(test)]
    pub(crate) fn set_server_caps_for_test(&mut self, caps: &[&str]) {
        self.server_caps = caps.iter().map(|cap| (*cap).to_string()).collect();
    }

    pub(crate) fn set_session_url(&mut self, url: String) {
        self.url = url;
    }
}