tauri_plugin_tracing/
strip_ansi.rs

1//! ANSI escape code stripping for clean file output.
2
3use std::io::Write;
4use std::sync::{Mutex, MutexGuard};
5
6/// A writer wrapper that strips ANSI escape codes from all output.
7///
8/// This is used for file logging to ensure clean output even when stdout
9/// layers use ANSI colors. Due to how `tracing_subscriber` shares span field
10/// formatting between layers, ANSI codes from one layer can leak into others.
11/// This wrapper strips those codes at write time.
12///
13/// Uses a zero-copy fast path when no ANSI codes are present. Thread-safe
14/// via internal Mutex.
15///
16/// # Example
17///
18/// ```rust,no_run
19/// use tauri_plugin_tracing::StripAnsiWriter;
20/// use tauri_plugin_tracing::tracing_appender::non_blocking;
21/// use tauri_plugin_tracing::tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
22///
23/// let file_appender = tracing_appender::rolling::daily("/tmp/logs", "app.log");
24/// let (non_blocking, _guard) = non_blocking(file_appender);
25///
26/// tracing_subscriber::registry()
27///     .with(fmt::layer())  // stdout with ANSI
28///     .with(fmt::layer().with_writer(StripAnsiWriter::new(non_blocking)).with_ansi(false))
29///     .init();
30/// ```
31pub struct StripAnsiWriter<W> {
32    pub(crate) inner: Mutex<W>,
33}
34
35impl<W> StripAnsiWriter<W> {
36    /// Creates a new `StripAnsiWriter` that wraps the given writer.
37    pub fn new(inner: W) -> Self {
38        Self {
39            inner: Mutex::new(inner),
40        }
41    }
42}
43
44/// Strips ANSI escape codes from input and writes to output.
45/// Returns the number of bytes from input that were processed.
46pub(crate) fn strip_ansi_and_write<W: Write>(writer: &mut W, buf: &[u8]) -> std::io::Result<usize> {
47    let input_len = buf.len();
48
49    // Fast path: use memchr to check for ESC byte. If none, write directly.
50    let Some(first_esc) = memchr::memchr(0x1b, buf) else {
51        writer.write_all(buf)?;
52        return Ok(input_len);
53    };
54
55    // Slow path: ANSI codes present, need to strip them
56    // Pre-allocate with capacity for worst case (all kept)
57    let mut output = Vec::with_capacity(input_len);
58
59    // Copy everything before first ESC
60    output.extend_from_slice(&buf[..first_esc]);
61    let mut i = first_esc;
62
63    while i < buf.len() {
64        if buf[i] == 0x1b && i + 1 < buf.len() && buf[i + 1] == b'[' {
65            // Found ESC[, skip the SGR sequence
66            i += 2;
67            while i < buf.len() {
68                let c = buf[i];
69                i += 1;
70                if c == b'm' {
71                    break;
72                }
73                if !c.is_ascii_digit() && c != b';' {
74                    break;
75                }
76            }
77        } else {
78            output.push(buf[i]);
79            i += 1;
80        }
81    }
82
83    writer.write_all(&output)?;
84    Ok(input_len)
85}
86
87/// A writer handle returned by the [`MakeWriter`](tracing_subscriber::fmt::MakeWriter) implementation.
88///
89/// This type implements [`std::io::Write`] and strips ANSI codes during writes.
90pub struct StripAnsiWriterGuard<'a, W> {
91    guard: MutexGuard<'a, W>,
92}
93
94impl<W: Write> Write for StripAnsiWriterGuard<'_, W> {
95    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
96        strip_ansi_and_write(&mut *self.guard, buf)
97    }
98
99    fn flush(&mut self) -> std::io::Result<()> {
100        self.guard.flush()
101    }
102}
103
104// Implement MakeWriter so this can be used with fmt::layer().with_writer()
105impl<'a, W: Write + 'a> tracing_subscriber::fmt::MakeWriter<'a> for StripAnsiWriter<W> {
106    type Writer = StripAnsiWriterGuard<'a, W>;
107
108    fn make_writer(&'a self) -> Self::Writer {
109        StripAnsiWriterGuard {
110            guard: self.inner.lock().unwrap_or_else(|e| e.into_inner()),
111        }
112    }
113}
114
115#[cfg(test)]
116#[allow(clippy::unwrap_used)]
117mod tests {
118    use super::*;
119    use tracing_subscriber::fmt::MakeWriter;
120
121    #[test]
122    fn strip_ansi_fast_path_no_escape() {
123        let mut output = Vec::new();
124        let input = b"Hello, world!";
125        let written = strip_ansi_and_write(&mut output, input).unwrap();
126        assert_eq!(written, input.len());
127        assert_eq!(output, input);
128    }
129
130    #[test]
131    fn strip_ansi_removes_sgr_sequence() {
132        let mut output = Vec::new();
133        let input = b"\x1b[32mgreen\x1b[0m";
134        let written = strip_ansi_and_write(&mut output, input).unwrap();
135        assert_eq!(written, input.len());
136        assert_eq!(output, b"green");
137    }
138
139    #[test]
140    fn strip_ansi_removes_multiple_sequences() {
141        let mut output = Vec::new();
142        let input = b"\x1b[1m\x1b[31mBold Red\x1b[0m Normal";
143        let written = strip_ansi_and_write(&mut output, input).unwrap();
144        assert_eq!(written, input.len());
145        assert_eq!(output, b"Bold Red Normal");
146    }
147
148    #[test]
149    fn strip_ansi_handles_complex_sgr() {
150        let mut output = Vec::new();
151        // SGR with multiple parameters: ESC[1;31;42m
152        let input = b"\x1b[1;31;42mStyled\x1b[0m";
153        let written = strip_ansi_and_write(&mut output, input).unwrap();
154        assert_eq!(written, input.len());
155        assert_eq!(output, b"Styled");
156    }
157
158    #[test]
159    fn strip_ansi_preserves_non_sgr_escape() {
160        let mut output = Vec::new();
161        // ESC not followed by [ should be preserved
162        let input = b"Hello\x1bWorld";
163        let written = strip_ansi_and_write(&mut output, input).unwrap();
164        assert_eq!(written, input.len());
165        assert_eq!(output, b"Hello\x1bWorld");
166    }
167
168    #[test]
169    fn strip_ansi_handles_escape_at_end() {
170        let mut output = Vec::new();
171        let input = b"Hello\x1b";
172        let written = strip_ansi_and_write(&mut output, input).unwrap();
173        assert_eq!(written, input.len());
174        assert_eq!(output, b"Hello\x1b");
175    }
176
177    #[test]
178    fn strip_ansi_handles_incomplete_sequence() {
179        let mut output = Vec::new();
180        // ESC[ without terminator
181        let input = b"Hello\x1b[31";
182        let written = strip_ansi_and_write(&mut output, input).unwrap();
183        assert_eq!(written, input.len());
184        // Incomplete sequence is stripped up to where parsing stops
185        assert_eq!(output, b"Hello");
186    }
187
188    #[test]
189    fn strip_ansi_writer_works() {
190        let inner = Vec::new();
191        let writer = StripAnsiWriter::new(inner);
192        {
193            let mut guard = writer.make_writer();
194            guard.write_all(b"\x1b[32mtest\x1b[0m").unwrap();
195        }
196        let result = writer.inner.lock().unwrap();
197        assert_eq!(&*result, b"test");
198    }
199
200    #[test]
201    fn strip_ansi_empty_input() {
202        let mut output = Vec::new();
203        let input = b"";
204        let written = strip_ansi_and_write(&mut output, input).unwrap();
205        assert_eq!(written, 0);
206        assert_eq!(output, b"");
207    }
208
209    #[test]
210    fn strip_ansi_only_escape_sequences() {
211        let mut output = Vec::new();
212        let input = b"\x1b[31m\x1b[0m";
213        let written = strip_ansi_and_write(&mut output, input).unwrap();
214        assert_eq!(written, input.len());
215        assert_eq!(output, b"");
216    }
217}