git2/
transport.rs

1//! Interfaces for adding custom transports to libgit2
2
3use libc::{c_char, c_int, c_uint, c_void, size_t};
4use std::ffi::{CStr, CString};
5use std::io;
6use std::io::prelude::*;
7use std::mem;
8use std::ptr;
9use std::slice;
10use std::str;
11
12use crate::util::Binding;
13use crate::{panic, raw, Error, Remote};
14
15/// A transport is a structure which knows how to transfer data to and from a
16/// remote.
17///
18/// This transport is a representation of the raw transport underneath it, which
19/// is similar to a trait object in Rust.
20#[allow(missing_copy_implementations)]
21pub struct Transport {
22    raw: *mut raw::git_transport,
23    owned: bool,
24}
25
26/// Interface used by smart transports.
27///
28/// The full-fledged definition of transports has to deal with lots of
29/// nitty-gritty details of the git protocol, but "smart transports" largely
30/// only need to deal with read() and write() of data over a channel.
31///
32/// A smart subtransport is contained within an instance of a smart transport
33/// and is delegated to in order to actually conduct network activity to push or
34/// pull data from a remote.
35pub trait SmartSubtransport: Send + 'static {
36    /// Indicates that this subtransport will be performing the specified action
37    /// on the specified URL.
38    ///
39    /// This function is responsible for making any network connections and
40    /// returns a stream which can be read and written from in order to
41    /// negotiate the git protocol.
42    fn action(&self, url: &str, action: Service)
43        -> Result<Box<dyn SmartSubtransportStream>, Error>;
44
45    /// Terminates a connection with the remote.
46    ///
47    /// Each subtransport is guaranteed a call to close() between calls to
48    /// action(), except for the following two natural progressions of actions
49    /// against a constant URL.
50    ///
51    /// 1. UploadPackLs -> UploadPack
52    /// 2. ReceivePackLs -> ReceivePack
53    fn close(&self) -> Result<(), Error>;
54}
55
56/// Actions that a smart transport can ask a subtransport to perform
57#[derive(Copy, Clone, PartialEq, Debug)]
58#[allow(missing_docs)]
59pub enum Service {
60    UploadPackLs,
61    UploadPack,
62    ReceivePackLs,
63    ReceivePack,
64}
65
66/// An instance of a stream over which a smart transport will communicate with a
67/// remote.
68///
69/// Currently this only requires the standard `Read` and `Write` traits. This
70/// trait also does not need to be implemented manually as long as the `Read`
71/// and `Write` traits are implemented.
72pub trait SmartSubtransportStream: Read + Write + Send + 'static {}
73
74impl<T: Read + Write + Send + 'static> SmartSubtransportStream for T {}
75
76type TransportFactory = dyn Fn(&Remote<'_>) -> Result<Transport, Error> + Send + Sync + 'static;
77
78/// Boxed data payload used for registering new transports.
79///
80/// Currently only contains a field which knows how to create transports.
81struct TransportData {
82    factory: Box<TransportFactory>,
83}
84
85/// Instance of a `git_smart_subtransport`, must use `#[repr(C)]` to ensure that
86/// the C fields come first.
87#[repr(C)]
88struct RawSmartSubtransport {
89    raw: raw::git_smart_subtransport,
90    stream: Option<*mut raw::git_smart_subtransport_stream>,
91    rpc: bool,
92    obj: Box<dyn SmartSubtransport>,
93}
94
95/// Instance of a `git_smart_subtransport_stream`, must use `#[repr(C)]` to
96/// ensure that the C fields come first.
97#[repr(C)]
98struct RawSmartSubtransportStream {
99    raw: raw::git_smart_subtransport_stream,
100    obj: Box<dyn SmartSubtransportStream>,
101}
102
103/// Add a custom transport definition, to be used in addition to the built-in
104/// set of transports that come with libgit2.
105///
106/// This function is unsafe as it needs to be externally synchronized with calls
107/// to creation of other transports.
108pub unsafe fn register<F>(prefix: &str, factory: F) -> Result<(), Error>
109where
110    F: Fn(&Remote<'_>) -> Result<Transport, Error> + Send + Sync + 'static,
111{
112    crate::init();
113    let mut data = Box::new(TransportData {
114        factory: Box::new(factory),
115    });
116    let prefix = CString::new(prefix)?;
117    let datap = (&mut *data) as *mut TransportData as *mut c_void;
118    let factory: raw::git_transport_cb = Some(transport_factory);
119    try_call!(raw::git_transport_register(prefix, factory, datap));
120    mem::forget(data);
121    Ok(())
122}
123
124impl Transport {
125    /// Creates a new transport which will use the "smart" transport protocol
126    /// for transferring data.
127    ///
128    /// A smart transport requires a *subtransport* over which data is actually
129    /// communicated, but this subtransport largely just needs to be able to
130    /// read() and write(). The subtransport provided will be used to make
131    /// connections which can then be read/written from.
132    ///
133    /// The `rpc` argument is `true` if the protocol is stateless, false
134    /// otherwise. For example `http://` is stateless but `git://` is not.
135    pub fn smart<S>(remote: &Remote<'_>, rpc: bool, subtransport: S) -> Result<Transport, Error>
136    where
137        S: SmartSubtransport,
138    {
139        let mut ret = ptr::null_mut();
140
141        let mut raw = Box::new(RawSmartSubtransport {
142            raw: raw::git_smart_subtransport {
143                action: Some(subtransport_action),
144                close: Some(subtransport_close),
145                free: Some(subtransport_free),
146            },
147            stream: None,
148            rpc,
149            obj: Box::new(subtransport),
150        });
151        let mut defn = raw::git_smart_subtransport_definition {
152            callback: Some(smart_factory),
153            rpc: rpc as c_uint,
154            param: &mut *raw as *mut _ as *mut _,
155        };
156
157        // Currently there's no way to pass a payload via the
158        // git_smart_subtransport_definition structure, but it's only used as a
159        // configuration for the initial creation of the smart transport (verified
160        // by reading the current code, hopefully it doesn't change!).
161        //
162        // We, however, need some state (gotta pass in our
163        // `RawSmartSubtransport`). This also means that this block must be
164        // entirely synchronized with a lock (boo!)
165        unsafe {
166            try_call!(raw::git_transport_smart(
167                &mut ret,
168                remote.raw(),
169                &mut defn as *mut _ as *mut _
170            ));
171            mem::forget(raw); // ownership transport to `ret`
172        }
173        return Ok(Transport {
174            raw: ret,
175            owned: true,
176        });
177
178        extern "C" fn smart_factory(
179            out: *mut *mut raw::git_smart_subtransport,
180            _owner: *mut raw::git_transport,
181            ptr: *mut c_void,
182        ) -> c_int {
183            unsafe {
184                *out = ptr as *mut raw::git_smart_subtransport;
185                0
186            }
187        }
188    }
189}
190
191impl Drop for Transport {
192    fn drop(&mut self) {
193        if self.owned {
194            unsafe { (*self.raw).free.unwrap()(self.raw) }
195        }
196    }
197}
198
199// callback used by register() to create new transports
200extern "C" fn transport_factory(
201    out: *mut *mut raw::git_transport,
202    owner: *mut raw::git_remote,
203    param: *mut c_void,
204) -> c_int {
205    struct Bomb<'a> {
206        remote: Option<Remote<'a>>,
207    }
208    impl<'a> Drop for Bomb<'a> {
209        fn drop(&mut self) {
210            // TODO: maybe a method instead?
211            mem::forget(self.remote.take());
212        }
213    }
214
215    panic::wrap(|| unsafe {
216        let remote = Bomb {
217            remote: Some(Binding::from_raw(owner)),
218        };
219        let data = &mut *(param as *mut TransportData);
220        match (data.factory)(remote.remote.as_ref().unwrap()) {
221            Ok(mut transport) => {
222                *out = transport.raw;
223                transport.owned = false;
224                0
225            }
226            Err(e) => e.raw_code() as c_int,
227        }
228    })
229    .unwrap_or(-1)
230}
231
232// callback used by smart transports to delegate an action to a
233// `SmartSubtransport` trait object.
234extern "C" fn subtransport_action(
235    stream: *mut *mut raw::git_smart_subtransport_stream,
236    raw_transport: *mut raw::git_smart_subtransport,
237    url: *const c_char,
238    action: raw::git_smart_service_t,
239) -> c_int {
240    panic::wrap(|| unsafe {
241        let url = CStr::from_ptr(url).to_bytes();
242        let url = match str::from_utf8(url).ok() {
243            Some(s) => s,
244            None => return -1,
245        };
246        let action = match action {
247            raw::GIT_SERVICE_UPLOADPACK_LS => Service::UploadPackLs,
248            raw::GIT_SERVICE_UPLOADPACK => Service::UploadPack,
249            raw::GIT_SERVICE_RECEIVEPACK_LS => Service::ReceivePackLs,
250            raw::GIT_SERVICE_RECEIVEPACK => Service::ReceivePack,
251            n => panic!("unknown action: {}", n),
252        };
253
254        let transport = &mut *(raw_transport as *mut RawSmartSubtransport);
255        // Note: we only need to generate if rpc is on. Else, for receive-pack and upload-pack
256        // libgit2 reuses the stream generated for receive-pack-ls or upload-pack-ls.
257        let generate_stream =
258            transport.rpc || action == Service::UploadPackLs || action == Service::ReceivePackLs;
259        if generate_stream {
260            let obj = match transport.obj.action(url, action) {
261                Ok(s) => s,
262                Err(e) => return e.raw_set_git_error(),
263            };
264            *stream = mem::transmute(Box::new(RawSmartSubtransportStream {
265                raw: raw::git_smart_subtransport_stream {
266                    subtransport: raw_transport,
267                    read: Some(stream_read),
268                    write: Some(stream_write),
269                    free: Some(stream_free),
270                },
271                obj,
272            }));
273            transport.stream = Some(*stream);
274        } else {
275            if transport.stream.is_none() {
276                return -1;
277            }
278            *stream = transport.stream.unwrap();
279        }
280        0
281    })
282    .unwrap_or(-1)
283}
284
285// callback used by smart transports to close a `SmartSubtransport` trait
286// object.
287extern "C" fn subtransport_close(transport: *mut raw::git_smart_subtransport) -> c_int {
288    let ret = panic::wrap(|| unsafe {
289        let transport = &mut *(transport as *mut RawSmartSubtransport);
290        transport.obj.close()
291    });
292    match ret {
293        Some(Ok(())) => 0,
294        Some(Err(e)) => e.raw_code() as c_int,
295        None => -1,
296    }
297}
298
299// callback used by smart transports to free a `SmartSubtransport` trait
300// object.
301extern "C" fn subtransport_free(transport: *mut raw::git_smart_subtransport) {
302    let _ = panic::wrap(|| unsafe {
303        mem::transmute::<_, Box<RawSmartSubtransport>>(transport);
304    });
305}
306
307// callback used by smart transports to read from a `SmartSubtransportStream`
308// object.
309extern "C" fn stream_read(
310    stream: *mut raw::git_smart_subtransport_stream,
311    buffer: *mut c_char,
312    buf_size: size_t,
313    bytes_read: *mut size_t,
314) -> c_int {
315    let ret = panic::wrap(|| unsafe {
316        let transport = &mut *(stream as *mut RawSmartSubtransportStream);
317        let buf = slice::from_raw_parts_mut(buffer as *mut u8, buf_size as usize);
318        match transport.obj.read(buf) {
319            Ok(n) => {
320                *bytes_read = n as size_t;
321                Ok(n)
322            }
323            e => e,
324        }
325    });
326    match ret {
327        Some(Ok(_)) => 0,
328        Some(Err(e)) => unsafe {
329            set_err_io(&e);
330            -2
331        },
332        None => -1,
333    }
334}
335
336// callback used by smart transports to write to a `SmartSubtransportStream`
337// object.
338extern "C" fn stream_write(
339    stream: *mut raw::git_smart_subtransport_stream,
340    buffer: *const c_char,
341    len: size_t,
342) -> c_int {
343    let ret = panic::wrap(|| unsafe {
344        let transport = &mut *(stream as *mut RawSmartSubtransportStream);
345        let buf = slice::from_raw_parts(buffer as *const u8, len as usize);
346        transport.obj.write_all(buf)
347    });
348    match ret {
349        Some(Ok(())) => 0,
350        Some(Err(e)) => unsafe {
351            set_err_io(&e);
352            -2
353        },
354        None => -1,
355    }
356}
357
358unsafe fn set_err_io(e: &io::Error) {
359    let s = CString::new(e.to_string()).unwrap();
360    raw::git_error_set_str(raw::GIT_ERROR_NET as c_int, s.as_ptr());
361}
362
363// callback used by smart transports to free a `SmartSubtransportStream`
364// object.
365extern "C" fn stream_free(stream: *mut raw::git_smart_subtransport_stream) {
366    let _ = panic::wrap(|| unsafe {
367        mem::transmute::<_, Box<RawSmartSubtransportStream>>(stream);
368    });
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::{ErrorClass, ErrorCode};
375    use std::sync::Once;
376
377    struct DummyTransport;
378
379    // in lieu of lazy_static
380    fn dummy_error() -> Error {
381        Error::new(ErrorCode::Ambiguous, ErrorClass::Net, "bleh")
382    }
383
384    impl SmartSubtransport for DummyTransport {
385        fn action(
386            &self,
387            _url: &str,
388            _service: Service,
389        ) -> Result<Box<dyn SmartSubtransportStream>, Error> {
390            Err(dummy_error())
391        }
392
393        fn close(&self) -> Result<(), Error> {
394            Ok(())
395        }
396    }
397
398    #[test]
399    fn transport_error_propagates() {
400        static INIT: Once = Once::new();
401
402        unsafe {
403            INIT.call_once(|| {
404                register("dummy", move |remote| {
405                    Transport::smart(&remote, true, DummyTransport)
406                })
407                .unwrap();
408            })
409        }
410
411        let (_td, repo) = crate::test::repo_init();
412        t!(repo.remote("origin", "dummy://ball"));
413
414        let mut origin = t!(repo.find_remote("origin"));
415
416        match origin.fetch(&["main"], None, None) {
417            Ok(()) => unreachable!(),
418            Err(e) => assert_eq!(e, dummy_error()),
419        }
420    }
421}