1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! This crate allows users from the other side of the FFI to hook into Rust's
//! `log` crate, which is used by us and several of our dependencies. The
//! primary use case is providing logs to Android and iOS in a way that is more
//! flexible than writing to liblog (which goes to logcat, which cannot be
//! accessed by programs on the device, short of rooting it), or stdout/stderr.
//!
//! See the header comment in android.rs and fallback.rs for details.
//!
//! It's worth noting that the log crate is rather inflexable, in that
//! it does not allow users to change loggers after the first initialization. We
//! work around this using our `settable_log` module.

#![allow(unknown_lints)]
#![warn(rust_2018_idioms)]
// We always include both modules when doing test builds, so for test builds,
// allow dead code.
#![cfg_attr(test, allow(dead_code))]

use std::ffi::CString;

// Import this in tests (even on non-android builds / cases where the
// force_android feature is not enabled) so we can check that it compiles
// easily.
#[cfg(any(test, os = "android", feature = "force_android"))]
pub mod android;
// Import this in tests (even if we're building for android or force_android is
// turned on) so we can check that it compiles easily
#[cfg(any(test, not(any(os = "android", feature = "force_android"))))]
pub mod ios;

mod settable_log;

cfg_if::cfg_if! {
    if #[cfg(any(os = "android", feature = "force_android"))] {
        use crate::android as imp;
    } else {
        use crate::ios as imp;
    }
}

pub(crate) fn string_to_cstring_lossy(s: String) -> CString {
    let mut bytes = s.into_bytes();
    for byte in bytes.iter_mut() {
        if *byte == 0 {
            *byte = b'?';
        }
    }
    CString::new(bytes).expect("Bug in string_to_cstring_lossy!")
}

#[derive(Debug, PartialEq, Clone, Copy)]
#[repr(i32)]
pub enum LogLevel {
    // Android logger levels
    VERBOSE = 2,
    DEBUG = 3,
    INFO = 4,
    WARN = 5,
    ERROR = 6,
}

impl LogLevel {
    /// Equivalent to the `into()` conversion but avoids reporting network
    /// errors as errors, and downgrades them into warnings.
    pub(crate) fn from_level_and_message(mut level: log::Level, msg: &str) -> Self {
        if level == log::Level::Error && msg.contains("[no-sentry]") {
            level = log::Level::Warn;
        }
        level.into()
    }
}

impl From<log::Level> for LogLevel {
    fn from(l: log::Level) -> Self {
        match l {
            log::Level::Trace => LogLevel::VERBOSE,
            log::Level::Debug => LogLevel::DEBUG,
            log::Level::Info => LogLevel::INFO,
            log::Level::Warn => LogLevel::WARN,
            log::Level::Error => LogLevel::ERROR,
        }
    }
}

#[no_mangle]
pub extern "C" fn rc_log_adapter_create(
    callback: imp::LogCallback,
    out_err: &mut ffi_support::ExternError,
) -> *mut imp::LogAdapterState {
    ffi_support::call_with_output(out_err, || imp::LogAdapterState::init(callback))
}

// Note: keep in sync with LogLevelFilter in kotlin.
fn level_filter_from_i32(level_arg: i32) -> log::LevelFilter {
    match level_arg {
        4 => log::LevelFilter::Debug,
        3 => log::LevelFilter::Info,
        2 => log::LevelFilter::Warn,
        1 => log::LevelFilter::Error,
        // We clamp out of bounds level values.
        n if n <= 0 => log::LevelFilter::Off,
        n if n >= 5 => log::LevelFilter::Trace,
        _ => unreachable!("This is actually exhaustive"),
    }
}

#[no_mangle]
pub extern "C" fn rc_log_adapter_set_max_level(level: i32, out_err: &mut ffi_support::ExternError) {
    ffi_support::call_with_output(out_err, || log::set_max_level(level_filter_from_i32(level)))
}

// Can't use define_box_destructor because this can panic. TODO: Maybe we should
// keep this around globally (as lazy_static or something) and basically just
// turn it on/off in create/destroy... Might be more reliable?
/// # Safety
/// Unsafe because it frees it's argument.
#[no_mangle]
pub unsafe extern "C" fn rc_log_adapter_destroy(to_destroy: *mut imp::LogAdapterState) {
    ffi_support::abort_on_panic::call_with_output(move || {
        log::set_max_level(log::LevelFilter::Off);
        drop(Box::from_raw(to_destroy));
        settable_log::unset_logger();
    })
}

// Used just to allow tests to produce logs.
#[no_mangle]
pub extern "C" fn rc_log_adapter_test__log_msg(msg: ffi_support::FfiStr<'_>) {
    ffi_support::abort_on_panic::call_with_output(|| {
        log::info!("testing: {}", msg.as_str());
    });
}

ffi_support::define_string_destructor!(rc_log_adapter_destroy_string);

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_level_msg() {
        assert_eq!(
            LogLevel::ERROR,
            LogLevel::from_level_and_message(
                log::Level::Error,
                "Rusqlite Error: The database is wrong and bad.",
            ),
            "Normal errors should come through as errors",
        );

        assert_eq!(
            LogLevel::WARN,
            LogLevel::from_level_and_message(
                log::Level::Error,
                "Network Error: [no-sentry] Network error: the server is furious at you.",
            ),
            "[no-sentry] errors should come through as warnings",
        );

        assert_eq!(
            LogLevel::INFO,
            LogLevel::from_level_and_message(log::Level::Info, "[no-sentry] 🙀"),
            "Everything else should be unchanged, even if it has a [no-sentry] tag",
        );
    }
}