git2/
tracing.rs

1use std::{
2    ffi::CStr,
3    sync::atomic::{AtomicPtr, Ordering},
4};
5
6use libc::{c_char, c_int};
7
8use crate::{raw, util::Binding, Error};
9
10/// Available tracing levels.  When tracing is set to a particular level,
11/// callers will be provided tracing at the given level and all lower levels.
12#[derive(Copy, Clone, Debug)]
13pub enum TraceLevel {
14    /// No tracing will be performed.
15    None,
16
17    /// Severe errors that may impact the program's execution
18    Fatal,
19
20    /// Errors that do not impact the program's execution
21    Error,
22
23    /// Warnings that suggest abnormal data
24    Warn,
25
26    /// Informational messages about program execution
27    Info,
28
29    /// Detailed data that allows for debugging
30    Debug,
31
32    /// Exceptionally detailed debugging data
33    Trace,
34}
35
36impl Binding for TraceLevel {
37    type Raw = raw::git_trace_level_t;
38    unsafe fn from_raw(raw: raw::git_trace_level_t) -> Self {
39        match raw {
40            raw::GIT_TRACE_NONE => Self::None,
41            raw::GIT_TRACE_FATAL => Self::Fatal,
42            raw::GIT_TRACE_ERROR => Self::Error,
43            raw::GIT_TRACE_WARN => Self::Warn,
44            raw::GIT_TRACE_INFO => Self::Info,
45            raw::GIT_TRACE_DEBUG => Self::Debug,
46            raw::GIT_TRACE_TRACE => Self::Trace,
47            _ => panic!("Unknown git trace level"),
48        }
49    }
50    fn raw(&self) -> raw::git_trace_level_t {
51        match *self {
52            Self::None => raw::GIT_TRACE_NONE,
53            Self::Fatal => raw::GIT_TRACE_FATAL,
54            Self::Error => raw::GIT_TRACE_ERROR,
55            Self::Warn => raw::GIT_TRACE_WARN,
56            Self::Info => raw::GIT_TRACE_INFO,
57            Self::Debug => raw::GIT_TRACE_DEBUG,
58            Self::Trace => raw::GIT_TRACE_TRACE,
59        }
60    }
61}
62
63/// Callback type used to pass tracing events to the subscriber.
64/// see `trace_set` to register a subscriber.
65pub type TracingCb = fn(TraceLevel, &[u8]);
66
67/// Use an atomic pointer to store the global tracing subscriber function.
68static CALLBACK: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut());
69
70/// Set the global subscriber called when libgit2 produces a tracing message.
71pub fn trace_set(level: TraceLevel, cb: TracingCb) -> Result<(), Error> {
72    // Store the callback in the global atomic.
73    CALLBACK.store(cb as *mut (), Ordering::SeqCst);
74
75    // git_trace_set returns 0 if there was no error.
76    let return_code: c_int = unsafe { raw::git_trace_set(level.raw(), Some(tracing_cb_c)) };
77
78    if return_code != 0 {
79        Err(Error::last_error(return_code))
80    } else {
81        Ok(())
82    }
83}
84
85/// The tracing callback we pass to libgit2 (C ABI compatible).
86extern "C" fn tracing_cb_c(level: raw::git_trace_level_t, msg: *const c_char) {
87    // Load the callback function pointer from the global atomic.
88    let cb: *mut () = CALLBACK.load(Ordering::SeqCst);
89
90    // Transmute the callback pointer into the function pointer we know it to be.
91    //
92    // SAFETY: We only ever set the callback pointer with something cast from a TracingCb
93    // so transmuting back to a TracingCb is safe. This is notably not an integer-to-pointer
94    // transmute as described in the mem::transmute documentation and is in-line with the
95    // example in that documentation for casing between *const () to fn pointers.
96    let cb: TracingCb = unsafe { std::mem::transmute(cb) };
97
98    // If libgit2 passes us a message that is null, drop it and do not pass it to the callback.
99    // This is to avoid ever exposing rust code to a null ref, which would be Undefined Behavior.
100    if msg.is_null() {
101        return;
102    }
103
104    // Convert the message from a *const c_char to a &[u8] and pass it to the callback.
105    //
106    // SAFETY: We've just checked that the pointer is not null. The other safety requirements are left to
107    // libgit2 to enforce -- namely that it gives us a valid, nul-terminated, C string, that that string exists
108    // entirely in one allocation, that the string will not be mutated once passed to us, and that the nul-terminator is
109    // within isize::MAX bytes from the given pointers data address.
110    let msg: &CStr = unsafe { CStr::from_ptr(msg) };
111
112    // Convert from a CStr to &[u8] to pass to the rust code callback.
113    let msg: &[u8] = CStr::to_bytes(msg);
114
115    // Do not bother with wrapping any of the following calls in `panic::wrap`:
116    //
117    // The previous implementation used `panic::wrap` here but never called `panic::check` to determine if the
118    // trace callback had panicked, much less what caused it.
119    //
120    // This had the potential to lead to lost errors/unwinds, confusing to debugging situations, and potential issues
121    // catching panics in other parts of the `git2-rs` codebase.
122    //
123    // Instead, we simply call the next two lines, both of which may panic, directly. We can rely on the
124    // `extern "C"` semantics to appropriately catch the panics generated here and abort the process:
125    //
126    // Per <https://doc.rust-lang.org/std/panic/fn.catch_unwind.html>:
127    // > Rust functions that are expected to be called from foreign code that does not support
128    // > unwinding (such as C compiled with -fno-exceptions) should be defined using extern "C", which ensures
129    // > that if the Rust code panics, it is automatically caught and the process is aborted. If this is the desired
130    // > behavior, it is not necessary to use catch_unwind explicitly. This function should instead be used when
131    // > more graceful error-handling is needed.
132
133    // Convert the raw trace level into a type we can pass to the rust callback fn.
134    //
135    // SAFETY: Currently the implementation of this function (above) may panic, but is only marked as unsafe to match
136    // the trait definition, thus we can consider this call safe.
137    let level: TraceLevel = unsafe { Binding::from_raw(level) };
138
139    // Call the user-supplied callback (which may panic).
140    (cb)(level, msg);
141}
142
143#[cfg(test)]
144mod tests {
145    use super::TraceLevel;
146
147    // Test that using the above function to set a tracing callback doesn't panic.
148    #[test]
149    fn smoke() {
150        super::trace_set(TraceLevel::Trace, |level, msg| {
151            dbg!(level, msg);
152        })
153        .expect("libgit2 can set global trace callback");
154    }
155}