1use colored::Colorize;
8
9#[must_use]
11pub fn heading(text: &str) -> String {
12 format!("{}", text.bright_cyan())
13}
14
15#[must_use]
17pub fn label(text: &str) -> String {
18 format!("{}", text.bright_blue())
19}
20
21#[must_use]
23pub fn muted(text: &str) -> String {
24 format!("{}", text.bright_black())
25}
26
27#[must_use]
29pub fn success(text: &str) -> String {
30 format!("{}", text.bright_green())
31}
32
33#[must_use]
35pub fn warning(text: &str) -> String {
36 format!("{}", text.yellow())
37}
38
39#[must_use]
41pub fn error(text: &str) -> String {
42 format!("{}", text.red())
43}
44
45#[must_use]
47pub fn info(text: &str) -> String {
48 format!("{} {}", "Info:".bright_blue().bold(), text)
49}
50
51#[must_use]
53pub fn warn_line(text: &str) -> String {
54 format!("[{}] {}", "Warning".yellow(), text)
55}
56
57#[must_use]
59pub fn err_line(text: &str) -> String {
60 format!("{} {}", "Error".red().bold(), text)
61}
62
63#[must_use]
65pub fn banner_invalid_input(text: &str) -> String {
66 format!("{} {}", " Invalid input ".white().on_red(), text.red())
67}
68
69#[must_use]
71pub fn banner_warning(text: &str) -> String {
72 format!("{} {}", " Warning ".white().on_bright_black(), text)
73}
74
75#[must_use]
77pub fn banner_error(text: &str) -> String {
78 format!("{} {}", " Error ".white().on_red().bold(), text)
79}
80
81#[must_use]
83pub fn banner_suggestion(text: &str) -> String {
84 format!("{} {}", " Suggestion ".white().on_bright_black(), text)
85}
86
87#[must_use]
89pub fn status_ok() -> String {
90 format!("{}", "OK".green())
91}
92
93#[must_use]
95pub fn status_error() -> String {
96 format!("{}", "ERROR".red())
97}
98
99#[must_use]
101pub fn status_warning(text: &str) -> String {
102 format!("{}", text.yellow())
103}
104
105#[must_use]
124pub fn mask_url(url: &str) -> String {
125 let (base, query) = match url.split_once('?') {
126 Some((base, query)) => (base, Some(query)),
127 None => (url, None),
128 };
129
130 let mut masked = String::with_capacity(url.len());
131 let scheme_end = base.find("://").map_or(0, |position| position + 3);
132 match base
133 .rfind('@')
134 .filter(|&at| at > scheme_end)
135 .and_then(|at| {
136 base[scheme_end..at]
137 .find(':')
138 .map(|colon| (scheme_end + colon, at))
139 }) {
140 Some((colon, at)) => {
141 masked.push_str(&base[..=colon]);
142 masked.push_str("****");
143 masked.push_str(&base[at..]);
144 }
145 None => masked.push_str(base),
146 }
147
148 if let Some(query) = query {
149 masked.push('?');
150 for (index, pair) in query.split('&').enumerate() {
151 if index > 0 {
152 masked.push('&');
153 }
154 match pair.split_once('=') {
155 Some((key, _)) if is_secret_parameter(key) => {
156 masked.push_str(key);
157 masked.push_str("=****");
158 }
159 _ => masked.push_str(pair),
160 }
161 }
162 }
163 masked
164}
165
166fn is_secret_parameter(key: &str) -> bool {
167 let key = key.to_ascii_lowercase().replace(['_', '-'], "");
168 matches!(
169 key.as_str(),
170 "authtoken" | "token" | "password" | "pass" | "passwd" | "pwd" | "secret" | "apikey"
171 )
172}
173
174#[cfg(test)]
175mod mask_url_tests {
176 use super::mask_url;
177
178 #[test]
179 fn masks_passwords_and_secret_parameters() {
180 assert_eq!(
181 mask_url("postgres://user:secret@localhost:5432/db"),
182 "postgres://user:****@localhost:5432/db"
183 );
184 assert_eq!(
185 mask_url("postgres://user:pa:ss@localhost/db?sslmode=require"),
186 "postgres://user:****@localhost/db?sslmode=require"
187 );
188 assert_eq!(
189 mask_url("libsql://app.turso.io?authToken=abc.def&tls=true"),
190 "libsql://app.turso.io?authToken=****&tls=true"
191 );
192 assert_eq!(
193 mask_url("mysql://root@localhost/app?password=hunter2"),
194 "mysql://root@localhost/app?password=****"
195 );
196 assert_eq!(mask_url("file:local.db"), "file:local.db");
197 }
198}