//! Interfaces for adding custom transports to libgit2 use libc::{c_char, c_int, c_uint, c_void, size_t}; use std::ffi::{CStr, CString}; use std::io; use std::io::prelude::*; use std::mem; use std::ptr; use std::slice; use std::str; use crate::util::Binding; use crate::{panic, raw, Error, Remote}; /// A transport is a structure which knows how to transfer data to and from a /// remote. /// /// This transport is a representation of the raw transport underneath it, which /// is similar to a trait object in Rust. #[allow(missing_copy_implementations)] pub struct Transport { raw: *mut raw::git_transport, owned: bool, } /// Interface used by smart transports. /// /// The full-fledged definition of transports has to deal with lots of /// nitty-gritty details of the git protocol, but "smart transports" largely /// only need to deal with read() and write() of data over a channel. /// /// A smart subtransport is contained within an instance of a smart transport /// and is delegated to in order to actually conduct network activity to push or /// pull data from a remote. pub trait SmartSubtransport: Send + 'static { /// Indicates that this subtransport will be performing the specified action /// on the specified URL. /// /// This function is responsible for making any network connections and /// returns a stream which can be read and written from in order to /// negotiate the git protocol. fn action(&self, url: &str, action: Service) -> Result, Error>; /// Terminates a connection with the remote. /// /// Each subtransport is guaranteed a call to close() between calls to /// action(), except for the following two natural progressions of actions /// against a constant URL. /// /// 1. UploadPackLs -> UploadPack /// 2. ReceivePackLs -> ReceivePack fn close(&self) -> Result<(), Error>; } /// Actions that a smart transport can ask a subtransport to perform #[derive(Copy, Clone, PartialEq)] #[allow(missing_docs)] pub enum Service { UploadPackLs, UploadPack, ReceivePackLs, ReceivePack, } /// An instance of a stream over which a smart transport will communicate with a /// remote. /// /// Currently this only requires the standard `Read` and `Write` traits. This /// trait also does not need to be implemented manually as long as the `Read` /// and `Write` traits are implemented. pub trait SmartSubtransportStream: Read + Write + Send + 'static {} impl SmartSubtransportStream for T {} type TransportFactory = dyn Fn(&Remote<'_>) -> Result + Send + Sync + 'static; /// Boxed data payload used for registering new transports. /// /// Currently only contains a field which knows how to create transports. struct TransportData { factory: Box, } /// Instance of a `git_smart_subtransport`, must use `#[repr(C)]` to ensure that /// the C fields come first. #[repr(C)] struct RawSmartSubtransport { raw: raw::git_smart_subtransport, stream: Option<*mut raw::git_smart_subtransport_stream>, rpc: bool, obj: Box, } /// Instance of a `git_smart_subtransport_stream`, must use `#[repr(C)]` to /// ensure that the C fields come first. #[repr(C)] struct RawSmartSubtransportStream { raw: raw::git_smart_subtransport_stream, obj: Box, } /// Add a custom transport definition, to be used in addition to the built-in /// set of transports that come with libgit2. /// /// This function is unsafe as it needs to be externally synchronized with calls /// to creation of other transports. pub unsafe fn register(prefix: &str, factory: F) -> Result<(), Error> where F: Fn(&Remote<'_>) -> Result + Send + Sync + 'static, { crate::init(); let mut data = Box::new(TransportData { factory: Box::new(factory), }); let prefix = CString::new(prefix)?; let datap = (&mut *data) as *mut TransportData as *mut c_void; let factory: raw::git_transport_cb = Some(transport_factory); try_call!(raw::git_transport_register(prefix, factory, datap)); mem::forget(data); Ok(()) } impl Transport { /// Creates a new transport which will use the "smart" transport protocol /// for transferring data. /// /// A smart transport requires a *subtransport* over which data is actually /// communicated, but this subtransport largely just needs to be able to /// read() and write(). The subtransport provided will be used to make /// connections which can then be read/written from. /// /// The `rpc` argument is `true` if the protocol is stateless, false /// otherwise. For example `http://` is stateless but `git://` is not. pub fn smart(remote: &Remote<'_>, rpc: bool, subtransport: S) -> Result where S: SmartSubtransport, { let mut ret = ptr::null_mut(); let mut raw = Box::new(RawSmartSubtransport { raw: raw::git_smart_subtransport { action: Some(subtransport_action), close: Some(subtransport_close), free: Some(subtransport_free), }, stream: None, rpc, obj: Box::new(subtransport), }); let mut defn = raw::git_smart_subtransport_definition { callback: Some(smart_factory), rpc: rpc as c_uint, param: &mut *raw as *mut _ as *mut _, }; // Currently there's no way to pass a payload via the // git_smart_subtransport_definition structure, but it's only used as a // configuration for the initial creation of the smart transport (verified // by reading the current code, hopefully it doesn't change!). // // We, however, need some state (gotta pass in our // `RawSmartSubtransport`). This also means that this block must be // entirely synchronized with a lock (boo!) unsafe { try_call!(raw::git_transport_smart( &mut ret, remote.raw(), &mut defn as *mut _ as *mut _ )); mem::forget(raw); // ownership transport to `ret` } return Ok(Transport { raw: ret, owned: true, }); extern "C" fn smart_factory( out: *mut *mut raw::git_smart_subtransport, _owner: *mut raw::git_transport, ptr: *mut c_void, ) -> c_int { unsafe { *out = ptr as *mut raw::git_smart_subtransport; 0 } } } } impl Drop for Transport { fn drop(&mut self) { if self.owned { unsafe { (*self.raw).free.unwrap()(self.raw) } } } } // callback used by register() to create new transports extern "C" fn transport_factory( out: *mut *mut raw::git_transport, owner: *mut raw::git_remote, param: *mut c_void, ) -> c_int { struct Bomb<'a> { remote: Option>, } impl<'a> Drop for Bomb<'a> { fn drop(&mut self) { // TODO: maybe a method instead? mem::forget(self.remote.take()); } } panic::wrap(|| unsafe { let remote = Bomb { remote: Some(Binding::from_raw(owner)), }; let data = &mut *(param as *mut TransportData); match (data.factory)(remote.remote.as_ref().unwrap()) { Ok(mut transport) => { *out = transport.raw; transport.owned = false; 0 } Err(e) => e.raw_code() as c_int, } }) .unwrap_or(-1) } // callback used by smart transports to delegate an action to a // `SmartSubtransport` trait object. extern "C" fn subtransport_action( stream: *mut *mut raw::git_smart_subtransport_stream, raw_transport: *mut raw::git_smart_subtransport, url: *const c_char, action: raw::git_smart_service_t, ) -> c_int { panic::wrap(|| unsafe { let url = CStr::from_ptr(url).to_bytes(); let url = match str::from_utf8(url).ok() { Some(s) => s, None => return -1, }; let action = match action { raw::GIT_SERVICE_UPLOADPACK_LS => Service::UploadPackLs, raw::GIT_SERVICE_UPLOADPACK => Service::UploadPack, raw::GIT_SERVICE_RECEIVEPACK_LS => Service::ReceivePackLs, raw::GIT_SERVICE_RECEIVEPACK => Service::ReceivePack, n => panic!("unknown action: {}", n), }; let transport = &mut *(raw_transport as *mut RawSmartSubtransport); // Note: we only need to generate if rpc is on. Else, for receive-pack and upload-pack // libgit2 reuses the stream generated for receive-pack-ls or upload-pack-ls. let generate_stream = transport.rpc || action == Service::UploadPackLs || action == Service::ReceivePackLs; if generate_stream { let obj = match transport.obj.action(url, action) { Ok(s) => s, Err(e) => { set_err(&e); return e.raw_code() as c_int; } }; *stream = mem::transmute(Box::new(RawSmartSubtransportStream { raw: raw::git_smart_subtransport_stream { subtransport: raw_transport, read: Some(stream_read), write: Some(stream_write), free: Some(stream_free), }, obj, })); transport.stream = Some(*stream); } else { if transport.stream.is_none() { return -1; } *stream = transport.stream.unwrap(); } 0 }) .unwrap_or(-1) } // callback used by smart transports to close a `SmartSubtransport` trait // object. extern "C" fn subtransport_close(transport: *mut raw::git_smart_subtransport) -> c_int { let ret = panic::wrap(|| unsafe { let transport = &mut *(transport as *mut RawSmartSubtransport); transport.obj.close() }); match ret { Some(Ok(())) => 0, Some(Err(e)) => e.raw_code() as c_int, None => -1, } } // callback used by smart transports to free a `SmartSubtransport` trait // object. extern "C" fn subtransport_free(transport: *mut raw::git_smart_subtransport) { let _ = panic::wrap(|| unsafe { mem::transmute::<_, Box>(transport); }); } // callback used by smart transports to read from a `SmartSubtransportStream` // object. extern "C" fn stream_read( stream: *mut raw::git_smart_subtransport_stream, buffer: *mut c_char, buf_size: size_t, bytes_read: *mut size_t, ) -> c_int { let ret = panic::wrap(|| unsafe { let transport = &mut *(stream as *mut RawSmartSubtransportStream); let buf = slice::from_raw_parts_mut(buffer as *mut u8, buf_size as usize); match transport.obj.read(buf) { Ok(n) => { *bytes_read = n as size_t; Ok(n) } e => e, } }); match ret { Some(Ok(_)) => 0, Some(Err(e)) => unsafe { set_err_io(&e); -2 }, None => -1, } } // callback used by smart transports to write to a `SmartSubtransportStream` // object. extern "C" fn stream_write( stream: *mut raw::git_smart_subtransport_stream, buffer: *const c_char, len: size_t, ) -> c_int { let ret = panic::wrap(|| unsafe { let transport = &mut *(stream as *mut RawSmartSubtransportStream); let buf = slice::from_raw_parts(buffer as *const u8, len as usize); transport.obj.write_all(buf) }); match ret { Some(Ok(())) => 0, Some(Err(e)) => unsafe { set_err_io(&e); -2 }, None => -1, } } unsafe fn set_err_io(e: &io::Error) { let s = CString::new(e.to_string()).unwrap(); raw::git_error_set_str(raw::GIT_ERROR_NET as c_int, s.as_ptr()); } unsafe fn set_err(e: &Error) { let s = CString::new(e.message()).unwrap(); raw::git_error_set_str(e.raw_class() as c_int, s.as_ptr()); } // callback used by smart transports to free a `SmartSubtransportStream` // object. extern "C" fn stream_free(stream: *mut raw::git_smart_subtransport_stream) { let _ = panic::wrap(|| unsafe { mem::transmute::<_, Box>(stream); }); } #[cfg(test)] mod tests { use super::*; use crate::{ErrorClass, ErrorCode}; use std::sync::Once; struct DummyTransport; // in lieu of lazy_static fn dummy_error() -> Error { Error::new(ErrorCode::Ambiguous, ErrorClass::Net, "bleh") } impl SmartSubtransport for DummyTransport { fn action( &self, _url: &str, _service: Service, ) -> Result, Error> { Err(dummy_error()) } fn close(&self) -> Result<(), Error> { Ok(()) } } #[test] fn transport_error_propagates() { static INIT: Once = Once::new(); unsafe { INIT.call_once(|| { register("dummy", move |remote| { Transport::smart(&remote, true, DummyTransport) }) .unwrap(); }) } let (_td, repo) = crate::test::repo_init(); t!(repo.remote("origin", "dummy://ball")); let mut origin = t!(repo.find_remote("origin")); match origin.fetch(&["main"], None, None) { Ok(()) => unreachable!(), Err(e) => assert_eq!(e, dummy_error()), } } }