1use 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#[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}