Skip to main content

coreshift_core/log/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Backend-agnostic logging facade.
6
7mod android;
8mod null;
9mod stderr;
10
11/// Log severity levels.
12#[repr(i32)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum LogLevel {
15    Verbose = 2,
16    Debug = 3,
17    Info = 4,
18    Warn = 5,
19    Error = 6,
20    Fatal = 7,
21}
22
23/// Legacy alias for [`LogLevel`].
24pub type LogPriority = LogLevel;
25
26/// Available logging backends.
27#[repr(u8)]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum LogBackend {
30    /// Android system log (liblog).
31    Android = 0,
32    /// Standard error.
33    Stderr = 1,
34    /// Discard all messages.
35    Null = 2,
36}
37
38/// A handle for writing messages to a specific log backend.
39///
40/// Core follows a "no global mutable state" architecture. Callers that require
41/// a non-default logging backend must create a [`Logger`] instance and use
42/// it directly.
43///
44/// By default, macros like `alog_info!` use a platform-appropriate default
45/// logger ([`LogBackend::Android`] on Android, [`LogBackend::Stderr`] otherwise).
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct Logger {
48    backend: LogBackend,
49}
50
51impl Default for Logger {
52    fn default() -> Self {
53        #[cfg(target_os = "android")]
54        {
55            Self::new(LogBackend::Android)
56        }
57        #[cfg(not(target_os = "android"))]
58        {
59            Self::new(LogBackend::Stderr)
60        }
61    }
62}
63
64/// Strip C0/C1 control characters (except newline/tab) from a log message
65/// before it reaches any backend (CORE-M15).
66///
67/// The daemon logs attacker-controlled strings (package names, paths, error
68/// text). Without this, ANSI escapes (`\x1b[...]`) and forged newlines pass
69/// through verbatim and can spoof log lines or inject terminal-escape
70/// sequences into anything that renders the stream. Applied here, before
71/// backend dispatch, so every backend (including the Android log) is covered.
72pub(super) fn sanitize(s: &str) -> String {
73    let mut out = String::with_capacity(s.len());
74    for ch in s.chars() {
75        let keep = match ch {
76            '\n' | '\t' => true,
77            '\u{20}'..='\u{7e}' => true,
78            '\u{80}'..='\u{9f}' => false, // C1 control block (incl. ESC)
79            '\u{a0}'..='\u{10ffff}' => true, // non-ASCII printable/space
80            _ => false,                   // C0 controls except newline/tab (incl. ESC, BEL)
81        };
82        if keep {
83            out.push(ch);
84        }
85    }
86    out
87}
88
89impl Logger {
90    /// Create a new logger with the specified backend.
91    pub fn new(backend: LogBackend) -> Self {
92        Self { backend }
93    }
94
95    /// Write a message to the logger's active backend.
96    ///
97    /// Release-mode level stripping is applied here, matching the free
98    /// [`log`] function (CORE-M14): a caller-built `Logger` must not bypass
99    /// the documented release strip and let Verbose..Warn — including
100    /// attacker-controlled strings — reach the backend in release builds.
101    pub fn log(&self, level: LogLevel, tag: &str, msg: &str) {
102        #[cfg(not(debug_assertions))]
103        if (level as i32) < (LogLevel::Error as i32) {
104            return;
105        }
106        let msg = sanitize(msg);
107        match self.backend {
108            LogBackend::Android => android::log(level, tag, &msg),
109            LogBackend::Stderr => stderr::log(level, tag, &msg),
110            LogBackend::Null => null::log(level, tag, &msg),
111        }
112    }
113}
114
115/// Write a message using the platform default logger.
116///
117/// In release builds (`debug_assertions` off), Verbose/Debug/Info/Warn are
118/// stripped. Error and Fatal always emit regardless of build profile.
119pub fn log(level: LogLevel, tag: &str, msg: &str) {
120    #[cfg(not(debug_assertions))]
121    if (level as i32) < (LogLevel::Error as i32) {
122        return;
123    }
124    let msg = sanitize(msg);
125    #[cfg(target_os = "android")]
126    {
127        android::log(level, tag, &msg);
128    }
129    #[cfg(not(target_os = "android"))]
130    {
131        stderr::log(level, tag, &msg);
132    }
133}
134
135/// Legacy alias for [`log`].
136pub fn log_write(level: LogLevel, tag: &str, msg: &str) {
137    log(level, tag, msg);
138}
139
140#[macro_export]
141macro_rules! alog_verbose {
142    ($tag:expr, $($arg:tt)*) => {
143        $crate::log::log($crate::log::LogLevel::Verbose, $tag, &format!($($arg)*))
144    };
145    ($tag:expr) => {
146        $crate::log::log($crate::log::LogLevel::Verbose, $tag, "")
147    };
148}
149
150#[macro_export]
151macro_rules! alog_debug {
152    ($tag:expr, $($arg:tt)*) => {
153        $crate::log::log($crate::log::LogLevel::Debug, $tag, &format!($($arg)*))
154    };
155    ($tag:expr) => {
156        $crate::log::log($crate::log::LogLevel::Debug, $tag, "")
157    };
158}
159
160#[macro_export]
161macro_rules! alog_info {
162    ($tag:expr, $($arg:tt)*) => {
163        $crate::log::log($crate::log::LogLevel::Info, $tag, &format!($($arg)*))
164    };
165    ($tag:expr) => {
166        $crate::log::log($crate::log::LogLevel::Info, $tag, "")
167    };
168}
169
170#[macro_export]
171macro_rules! alog_warn {
172    ($tag:expr, $($arg:tt)*) => {
173        $crate::log::log($crate::log::LogLevel::Warn, $tag, &format!($($arg)*))
174    };
175    ($tag:expr) => {
176        $crate::log::log($crate::log::LogLevel::Warn, $tag, "")
177    };
178}
179
180#[macro_export]
181macro_rules! alog_error {
182    ($tag:expr, $($arg:tt)*) => {
183        $crate::log::log($crate::log::LogLevel::Error, $tag, &format!($($arg)*))
184    };
185    ($tag:expr) => {
186        $crate::log::log($crate::log::LogLevel::Error, $tag, "")
187    };
188}
189
190#[macro_export]
191macro_rules! alog_fatal {
192    ($tag:expr, $($arg:tt)*) => {
193        $crate::log::log($crate::log::LogLevel::Fatal, $tag, &format!($($arg)*))
194    };
195    ($tag:expr) => {
196        $crate::log::log($crate::log::LogLevel::Fatal, $tag, "")
197    };
198}
199
200#[macro_export]
201macro_rules! log_verbose {
202    ($tag:expr, $($arg:tt)*) => {
203        $crate::log::log($crate::log::LogLevel::Verbose, $tag, &format!($($arg)*))
204    };
205    ($tag:expr) => {
206        $crate::log::log($crate::log::LogLevel::Verbose, $tag, "")
207    };
208}
209
210#[macro_export]
211macro_rules! log_debug {
212    ($tag:expr, $($arg:tt)*) => {
213        $crate::log::log($crate::log::LogLevel::Debug, $tag, &format!($($arg)*))
214    };
215    ($tag:expr) => {
216        $crate::log::log($crate::log::LogLevel::Debug, $tag, "")
217    };
218}
219
220#[macro_export]
221macro_rules! log_info {
222    ($tag:expr, $($arg:tt)*) => {
223        $crate::log::log($crate::log::LogLevel::Info, $tag, &format!($($arg)*))
224    };
225    ($tag:expr) => {
226        $crate::log::log($crate::log::LogLevel::Info, $tag, "")
227    };
228}
229
230#[macro_export]
231macro_rules! log_warn {
232    ($tag:expr, $($arg:tt)*) => {
233        $crate::log::log($crate::log::LogLevel::Warn, $tag, &format!($($arg)*))
234    };
235    ($tag:expr) => {
236        $crate::log::log($crate::log::LogLevel::Warn, $tag, "")
237    };
238}
239
240#[macro_export]
241macro_rules! log_error {
242    ($tag:expr, $($arg:tt)*) => {
243        $crate::log::log($crate::log::LogLevel::Error, $tag, &format!($($arg)*))
244    };
245    ($tag:expr) => {
246        $crate::log::log($crate::log::LogLevel::Error, $tag, "")
247    };
248}
249
250#[macro_export]
251macro_rules! log_fatal {
252    ($tag:expr, $($arg:tt)*) => {
253        $crate::log::log($crate::log::LogLevel::Fatal, $tag, &format!($($arg)*))
254    };
255    ($tag:expr) => {
256        $crate::log::log($crate::log::LogLevel::Fatal, $tag, "")
257    };
258}