Skip to main content

drizzle_cli/
output.rs

1//! CLI output helpers for consistent, drizzle-kit-like formatting.
2
3use colored::Colorize;
4
5#[must_use]
6pub fn heading(text: &str) -> String {
7    format!("{}", text.bright_cyan())
8}
9
10#[must_use]
11pub fn label(text: &str) -> String {
12    format!("{}", text.bright_blue())
13}
14
15#[must_use]
16pub fn muted(text: &str) -> String {
17    format!("{}", text.bright_black())
18}
19
20#[must_use]
21pub fn success(text: &str) -> String {
22    format!("{}", text.bright_green())
23}
24
25#[must_use]
26pub fn warning(text: &str) -> String {
27    format!("{}", text.yellow())
28}
29
30#[must_use]
31pub fn error(text: &str) -> String {
32    format!("{}", text.red())
33}
34
35#[must_use]
36pub fn info(text: &str) -> String {
37    format!("{} {}", "Info:".bright_blue().bold(), text)
38}
39
40#[must_use]
41pub fn warn_line(text: &str) -> String {
42    format!("[{}] {}", "Warning".yellow(), text)
43}
44
45#[must_use]
46pub fn err_line(text: &str) -> String {
47    format!("{} {}", "Error".red().bold(), text)
48}
49
50#[must_use]
51pub fn banner_invalid_input(text: &str) -> String {
52    format!("{} {}", " Invalid input ".white().on_red(), text.red())
53}
54
55#[must_use]
56pub fn banner_warning(text: &str) -> String {
57    format!("{} {}", " Warning ".white().on_bright_black(), text)
58}
59
60#[must_use]
61pub fn banner_error(text: &str) -> String {
62    format!("{} {}", " Error ".white().on_red().bold(), text)
63}
64
65#[must_use]
66pub fn banner_suggestion(text: &str) -> String {
67    format!("{} {}", " Suggestion ".white().on_bright_black(), text)
68}
69
70#[must_use]
71pub fn status_ok() -> String {
72    format!("{}", "OK".green())
73}
74
75#[must_use]
76pub fn status_error() -> String {
77    format!("{}", "ERROR".red())
78}
79
80#[must_use]
81pub fn status_warning(text: &str) -> String {
82    format!("{}", text.yellow())
83}
84
85/// Hides the secrets in a connection URL: the password of `user:password@`
86/// and the value of `authToken`, `password`, `token` and similar query
87/// parameters.
88#[must_use]
89pub fn mask_url(url: &str) -> String {
90    let (base, query) = match url.split_once('?') {
91        Some((base, query)) => (base, Some(query)),
92        None => (url, None),
93    };
94
95    let mut masked = String::with_capacity(url.len());
96    let scheme_end = base.find("://").map_or(0, |position| position + 3);
97    match base
98        .rfind('@')
99        .filter(|&at| at > scheme_end)
100        .and_then(|at| {
101            base[scheme_end..at]
102                .find(':')
103                .map(|colon| (scheme_end + colon, at))
104        }) {
105        Some((colon, at)) => {
106            masked.push_str(&base[..=colon]);
107            masked.push_str("****");
108            masked.push_str(&base[at..]);
109        }
110        None => masked.push_str(base),
111    }
112
113    if let Some(query) = query {
114        masked.push('?');
115        for (index, pair) in query.split('&').enumerate() {
116            if index > 0 {
117                masked.push('&');
118            }
119            match pair.split_once('=') {
120                Some((key, _)) if is_secret_parameter(key) => {
121                    masked.push_str(key);
122                    masked.push_str("=****");
123                }
124                _ => masked.push_str(pair),
125            }
126        }
127    }
128    masked
129}
130
131fn is_secret_parameter(key: &str) -> bool {
132    let key = key.to_ascii_lowercase().replace(['_', '-'], "");
133    matches!(
134        key.as_str(),
135        "authtoken" | "token" | "password" | "pass" | "passwd" | "pwd" | "secret" | "apikey"
136    )
137}
138
139#[cfg(test)]
140mod mask_url_tests {
141    use super::mask_url;
142
143    #[test]
144    fn masks_passwords_and_secret_parameters() {
145        assert_eq!(
146            mask_url("postgres://user:secret@localhost:5432/db"),
147            "postgres://user:****@localhost:5432/db"
148        );
149        assert_eq!(
150            mask_url("postgres://user:pa:ss@localhost/db?sslmode=require"),
151            "postgres://user:****@localhost/db?sslmode=require"
152        );
153        assert_eq!(
154            mask_url("libsql://app.turso.io?authToken=abc.def&tls=true"),
155            "libsql://app.turso.io?authToken=****&tls=true"
156        );
157        assert_eq!(
158            mask_url("mysql://root@localhost/app?password=hunter2"),
159            "mysql://root@localhost/app?password=****"
160        );
161        assert_eq!(mask_url("file:local.db"), "file:local.db");
162    }
163}