1use crate::mascaramento::mascarar;
8use crate::ssh::SaidaExecucao;
9use crate::vps::modelo::VpsRegistro;
10use secrecy::ExposeSecret;
11use serde_json::json;
12use std::io::{self, Write};
13use std::sync::atomic::{AtomicBool, Ordering};
14
15static QUIET: AtomicBool = AtomicBool::new(false);
17
18static JSON_ERROS: AtomicBool = AtomicBool::new(false);
20
21pub fn definir_quiet(quiet: bool) {
23 QUIET.store(quiet, Ordering::SeqCst);
24}
25
26pub fn definir_json_erros(json: bool) {
28 JSON_ERROS.store(json, Ordering::SeqCst);
29}
30
31#[must_use]
33pub fn esta_quiet() -> bool {
34 QUIET.load(Ordering::SeqCst)
35}
36
37#[must_use]
39pub fn quer_json_erros() -> bool {
40 JSON_ERROS.load(Ordering::SeqCst)
41}
42
43pub fn escrever_linha(conteudo: &str) -> io::Result<()> {
51 let stdout = io::stdout();
52 let mut handle = stdout.lock();
53 handle.write_all(conteudo.as_bytes())?;
54 handle.write_all(b"\n")?;
55 handle.flush()?;
56 Ok(())
57}
58
59pub fn imprimir_sucesso(mensagem: &str) {
61 if esta_quiet() {
62 return;
63 }
64 println!("{mensagem}");
65}
66
67pub fn imprimir_banner_humano(mensagem: &str) {
71 if esta_quiet() || quer_json_erros() {
72 return;
73 }
74 if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
75 return;
76 }
77 if std::env::var_os("SSH_CLI_FORCE_TEXT").is_none() {
78 }
80 println!("{mensagem}");
81}
82
83pub fn imprimir_erro(mensagem: &str) {
85 eprintln!("{mensagem}");
86}
87
88pub fn imprimir_erro_envelope(
90 exit_code: i32,
91 message: &str,
92 remote_exit_code: Option<i32>,
93) -> io::Result<()> {
94 let mut v = json!({
95 "exit_code": exit_code,
96 "message": message,
97 });
98 if let Some(r) = remote_exit_code {
99 v["remote_exit_code"] = json!(r);
100 }
101 let s = serde_json::to_string(&v).unwrap_or_else(|_| {
102 format!(r#"{{"exit_code":{exit_code},"message":"erro de serialização"}}"#)
103 });
104 let mut err = io::stderr().lock();
105 err.write_all(s.as_bytes())?;
106 err.write_all(b"\n")?;
107 err.flush()?;
108 Ok(())
109}
110
111pub fn imprimir_json_value(v: &serde_json::Value) -> io::Result<()> {
113 let s = serde_json::to_string_pretty(v).map_err(io::Error::other)?;
114 escrever_linha(&s)
115}
116
117#[allow(clippy::too_many_arguments)]
119pub fn imprimir_doctor_texto(
120 camada: &str,
121 config_path: &str,
122 existe: bool,
123 perms: &str,
124 schema_version: u32,
125 hosts: usize,
126 known_hosts: &str,
127 active_file: &str,
128 secrets_at_rest: &str,
129 secrets_key_source: &str,
130 secrets_key_file: &str,
131 plaintext_opt_out: bool,
132) {
133 if esta_quiet() {
134 return;
135 }
136 println!("Camada vencedora: {camada}");
137 println!("Config path: {config_path}");
138 println!("Existe: {existe}");
139 println!("Permissões: {perms}");
140 println!("Schema: {schema_version}");
141 println!("Hosts: {hosts}");
142 println!("known_hosts: {known_hosts}");
143 println!("active file: {active_file}");
144 println!("Secrets at-rest: {secrets_at_rest} (key source: {secrets_key_source})");
145 println!("Secrets key file: {secrets_key_file}");
146 println!(
147 "Plaintext opt-out: {}",
148 if plaintext_opt_out { "yes" } else { "no" }
149 );
150 println!("Telemetria: desabilitada");
151}
152
153pub fn imprimir_lista_texto(registros: &[VpsRegistro]) {
155 if esta_quiet() {
156 return;
157 }
158 if registros.is_empty() {
159 println!(
160 "{}",
161 crate::i18n::t(crate::i18n::Mensagem::VpsRegistroVazio)
162 );
163 return;
164 }
165
166 println!(
167 "{:<20} {:<30} {:<6} {:<15} {:<20}",
168 "NOME", "HOST", "PORTA", "USUÁRIO", "SENHA"
169 );
170 for r in registros {
171 println!(
172 "{:<20} {:<30} {:<6} {:<15} {:<20}",
173 r.nome,
174 r.host,
175 r.porta,
176 r.usuario,
177 mascarar(r.senha.expose_secret())
178 );
179 }
180}
181
182pub fn imprimir_lista_json(registros: &[VpsRegistro]) {
184 let lista: Vec<_> = registros.iter().map(registro_para_json_mascarado).collect();
185 match serde_json::to_string_pretty(&lista) {
186 Ok(s) => println!("{s}"),
187 Err(erro) => eprintln!("erro ao serializar JSON: {erro}"),
188 }
189}
190
191pub fn imprimir_detalhes_texto(r: &VpsRegistro) {
193 if esta_quiet() {
194 return;
195 }
196 println!("Nome: {}", r.nome);
197 println!("Host: {}", r.host);
198 println!("Porta: {}", r.porta);
199 println!("Usuário: {}", r.usuario);
200 println!(
202 "Senha: {}",
203 if r.senha.expose_secret().is_empty() {
204 "(não definida)".to_string()
205 } else {
206 mascarar(r.senha.expose_secret())
207 }
208 );
209 println!(
210 "Key path: {}",
211 r.key_path.as_deref().unwrap_or("(não definida)")
212 );
213 println!(
214 "Senha sudo: {}",
215 r.senha_sudo
216 .as_ref()
217 .map_or_else(|| "(não definida)".into(), |s| mascarar(s.expose_secret()))
218 );
219 println!(
220 "Senha su: {}",
221 r.senha_su
222 .as_ref()
223 .map_or_else(|| "(não definida)".into(), |s| mascarar(s.expose_secret()))
224 );
225 println!("Timeout (ms): {}", r.timeout_ms);
226 println!("Max cmd chars: {}", r.max_command_chars);
227 println!("Max out chars: {}", r.max_output_chars);
228 println!("Disable sudo: {}", r.disable_sudo);
229 println!("Schema version: {}", r.schema_version);
230 println!("Adicionado em: {}", r.adicionado_em);
231}
232
233pub fn imprimir_detalhes_json(r: &VpsRegistro) {
235 let v = registro_para_json_mascarado(r);
236 match serde_json::to_string_pretty(&v) {
237 Ok(s) => println!("{s}"),
238 Err(erro) => eprintln!("erro ao serializar JSON: {erro}"),
239 }
240}
241
242fn registro_para_json_mascarado(r: &VpsRegistro) -> serde_json::Value {
243 let password = if r.senha.expose_secret().is_empty() {
245 json!(null)
246 } else {
247 json!(mascarar(r.senha.expose_secret()))
248 };
249 json!({
250 "name": r.nome,
251 "host": r.host,
252 "port": r.porta,
253 "user": r.usuario,
254 "password": password,
255 "key_path": r.key_path,
256 "key_passphrase": r.key_passphrase.as_ref().map(|s| mascarar(s.expose_secret())),
257 "sudo_password": r.senha_sudo.as_ref().map(|s| mascarar(s.expose_secret())),
258 "su_password": r.senha_su.as_ref().map(|s| mascarar(s.expose_secret())),
259 "timeout_ms": r.timeout_ms,
260 "max_command_chars": r.max_command_chars,
261 "max_output_chars": r.max_output_chars,
262 "disable_sudo": r.disable_sudo,
263 "schema_version": r.schema_version,
264 "added_at": r.adicionado_em,
265 })
266}
267
268pub fn imprimir_saida_execucao(saida: &SaidaExecucao) {
279 println!("--- stdout ---");
280 if saida.stdout.is_empty() {
281 println!("(vazio)");
282 } else {
283 println!("{}", saida.stdout);
284 }
285 println!("--- stderr ---");
286 if saida.stderr.is_empty() {
287 println!("(vazio)");
288 } else {
289 println!("{}", saida.stderr);
290 }
291 let code_str = saida
292 .exit_code
293 .map(|c| c.to_string())
294 .unwrap_or_else(|| "N/A".to_string());
295 println!("--- exit code: {} ({}ms) ---", code_str, saida.duracao_ms);
296 if saida.truncado_stdout {
297 println!("(stdout foi truncado)");
298 }
299 if saida.truncado_stderr {
300 println!("(stderr foi truncado)");
301 }
302}
303
304pub fn imprimir_saida_execucao_json(saida: &SaidaExecucao) {
306 let v = json!({
307 "stdout": saida.stdout,
308 "stderr": saida.stderr,
309 "exit_code": saida.exit_code,
310 "truncated_stdout": saida.truncado_stdout,
311 "truncated_stderr": saida.truncado_stderr,
312 "duration_ms": saida.duracao_ms,
313 });
314 match serde_json::to_string_pretty(&v) {
315 Ok(s) => println!("{s}"),
316 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
317 }
318}
319
320pub fn imprimir_health_check(nome: &str, latencia_ms: u64) {
322 if esta_quiet() {
323 return;
324 }
325 println!(
326 "{}",
327 crate::i18n::t(crate::i18n::Mensagem::HealthCheckOk {
328 nome: nome.to_string(),
329 })
330 );
331 println!(" latência: {latencia_ms}ms");
332}
333
334pub fn imprimir_health_check_json(nome: &str, latencia_ms: u64) {
336 let v = json!({
337 "name": nome,
338 "status": "ok",
339 "latency_ms": latencia_ms,
340 });
341 match serde_json::to_string_pretty(&v) {
342 Ok(s) => println!("{s}"),
343 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
344 }
345}
346
347pub fn imprimir_transferencia_json(
349 direction: &str,
350 vps: &str,
351 local: &str,
352 remote: &str,
353 bytes: u64,
354 duration_ms: u64,
355) {
356 let v = json!({
358 "ok": true,
359 "event": "scp-transfer",
360 "direction": direction,
361 "vps": vps,
362 "local": local,
363 "remote": remote,
364 "bytes": bytes,
365 "duration_ms": duration_ms,
366 });
367 match serde_json::to_string_pretty(&v) {
368 Ok(s) => {
369 let _ = escrever_linha(&s);
370 }
371 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
372 }
373}
374
375pub fn imprimir_tunnel_listening_json(
377 vps: &str,
378 local_port: u16,
379 remote_host: &str,
380 remote_port: u16,
381 timeout_ms: u64,
382) {
383 let v = json!({
384 "ok": true,
385 "event": "tunnel_listening",
386 "vps": vps,
387 "local_port": local_port,
388 "remote_host": remote_host,
389 "remote_port": remote_port,
390 "timeout_ms": timeout_ms,
391 });
392 match serde_json::to_string_pretty(&v) {
393 Ok(s) => {
394 let _ = escrever_linha(&s);
395 }
396 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
397 }
398}
399
400#[cfg(test)]
401mod testes {
402 use super::*;
403 use crate::ssh::SaidaExecucao;
404 use crate::vps::modelo::VpsRegistro;
405 use secrecy::SecretString;
406
407 fn registro_teste() -> VpsRegistro {
408 VpsRegistro::novo(
409 "vps-teste".into(),
410 "1.2.3.4".into(),
411 22,
412 "root".into(),
413 SecretString::from("senha-super-secreta".to_string()),
414 None,
415 None,
416 Some(5000),
417 Some(1000),
418 Some(1000),
419 Some(SecretString::from("sudo-password-longa-aqui".to_string())),
420 None,
421 false,
422 )
423 }
424
425 #[test]
426 fn registro_para_json_mascarado_contem_campos_obrigatorios() {
427 let r = registro_teste();
428 let json = registro_para_json_mascarado(&r);
429 assert_eq!(json["name"], "vps-teste");
430 assert_eq!(json["host"], "1.2.3.4");
431 assert_eq!(json["port"], 22);
432 assert_eq!(json["user"], "root");
433 assert_eq!(json["password"].as_str().unwrap(), "***");
434 assert_eq!(json["sudo_password"].as_str().unwrap(), "***");
435 assert!(json["su_password"].is_null());
436 assert_eq!(json["timeout_ms"], 5000);
437 assert_eq!(json["max_command_chars"], 1000);
438 assert_eq!(json["max_output_chars"], 1000);
439 assert_eq!(json["schema_version"], 2);
440 }
441
442 #[test]
443 fn registro_para_json_mascarado_senha_sudo_nula_quando_nao_definida() {
444 let mut r = registro_teste();
445 r.senha_sudo = None;
446 let json = registro_para_json_mascarado(&r);
447 assert!(json["sudo_password"].is_null());
448 }
449
450 #[test]
451 fn registro_para_json_mascarado_su_password_presente() {
452 let mut r = registro_teste();
453 r.senha_su = Some(SecretString::from("senha-su-muito-longa-aqui".to_string()));
454 let json = registro_para_json_mascarado(&r);
455 assert_eq!(json["su_password"].as_str().unwrap(), "***");
456 }
457
458 #[test]
459 fn registro_para_json_mascarado_password_null_quando_vazio() {
460 let mut r = registro_teste();
461 r.senha = SecretString::from(String::new());
462 let json = registro_para_json_mascarado(&r);
463 assert!(json["password"].is_null());
464 }
465
466 #[test]
467 fn escribir_linha_ok() {
468 let resultado = escrever_linha("teste de escrita");
469 assert!(resultado.is_ok());
470 }
471
472 #[test]
473 fn escribir_linha_com_caracteres_especiais() {
474 let resultado = escrever_linha("linha com \t tab e \"aspas\"");
475 assert!(resultado.is_ok());
476 }
477
478 #[test]
479 fn salida_execucao_completa_formatada() {
480 let saida = SaidaExecucao {
481 stdout: "output do comando".to_string(),
482 stderr: "erro do comando".to_string(),
483 exit_code: Some(0),
484 truncado_stdout: false,
485 truncado_stderr: false,
486 duracao_ms: 150,
487 };
488 let resultado = escrever_linha(&format!(
489 "stdout: {}, stderr: {}, exit: {:?}",
490 saida.stdout, saida.stderr, saida.exit_code
491 ));
492 assert!(resultado.is_ok());
493 }
494
495 #[test]
496 fn salida_execucao_sem_exit_code() {
497 let saida = SaidaExecucao {
498 stdout: "".to_string(),
499 stderr: "".to_string(),
500 exit_code: None,
501 truncado_stdout: false,
502 truncado_stderr: false,
503 duracao_ms: 0,
504 };
505 let code_str = saida
506 .exit_code
507 .map(|c| c.to_string())
508 .unwrap_or_else(|| "N/A".to_string());
509 assert_eq!(code_str, "N/A");
510 }
511
512 #[test]
513 fn vps_registro_debug_nao_expoe_senha() {
514 let r = registro_teste();
515 let json = registro_para_json_mascarado(&r);
516 let json_str = serde_json::to_string(&json).unwrap();
517 assert!(!json_str.contains("senha-super-secreta"));
518 assert!(!json_str.contains("sudo-password-longa-aqui"));
519 }
520
521 #[test]
522 fn salida_execucao_truncada_mostra_aviso() {
523 let saida = SaidaExecucao {
524 stdout: "output".to_string(),
525 stderr: "erro".to_string(),
526 exit_code: Some(1),
527 truncado_stdout: true,
528 truncado_stderr: true,
529 duracao_ms: 100,
530 };
531 assert!(saida.truncado_stdout);
532 assert!(saida.truncado_stderr);
533 }
534
535 #[test]
536 fn salida_execucao_com_exit_code_numerico() {
537 let saida = SaidaExecucao {
538 stdout: "".to_string(),
539 stderr: "".to_string(),
540 exit_code: Some(127),
541 truncado_stdout: false,
542 truncado_stderr: false,
543 duracao_ms: 0,
544 };
545 let code_str = saida
546 .exit_code
547 .map(|c| c.to_string())
548 .unwrap_or_else(|| "N/A".to_string());
549 assert_eq!(code_str, "127");
550 }
551
552 #[test]
553 fn escribir_linha_string_vazia() {
554 let resultado = escrever_linha("");
555 assert!(resultado.is_ok());
556 }
557
558 #[test]
559 fn escribir_linha_com_unicode_brasileiro() {
560 let resultado = escrever_linha("ação você está Itaú");
561 assert!(resultado.is_ok());
562 }
563
564 #[test]
565 fn escribir_linha_com_emojis() {
566 let resultado = escrever_linha("texto com 🚀 e 🔐");
567 assert!(resultado.is_ok());
568 }
569
570 #[test]
571 fn escribir_linha_com_newlines() {
572 let resultado = escrever_linha("linha1\nlinha2\nlinha3");
573 assert!(resultado.is_ok());
574 }
575
576 #[test]
577 fn escribir_linha_longo_texto() {
578 let texto_longo = "a".repeat(10000);
579 let resultado = escrever_linha(&texto_longo);
580 assert!(resultado.is_ok());
581 }
582
583 #[test]
584 fn registro_para_json_mascarado_com_senha_curta_mascara_com_asteriscos() {
585 let mut r = registro_teste();
586 r.senha = SecretString::from("curta".to_string());
587 let json = registro_para_json_mascarado(&r);
588 let senha_str = json["password"].as_str().unwrap();
589 assert_eq!(senha_str, "***");
590 }
591
592 #[test]
593 fn registro_para_json_mascarado_com_sudo_e_su_definidos() {
594 let mut r = registro_teste();
595 r.senha_sudo = Some(SecretString::from("sudo-pass-longa-aqui".to_string()));
596 r.senha_su = Some(SecretString::from("su-pass-longa-aqui".to_string()));
597 let json = registro_para_json_mascarado(&r);
598 assert!(!json["sudo_password"].is_null());
599 assert!(!json["su_password"].is_null());
600 assert_eq!(json["sudo_password"].as_str().unwrap(), "***");
601 assert_eq!(json["su_password"].as_str().unwrap(), "***");
602 }
603
604 #[test]
605 fn saida_execucao_formatacao_completa() {
606 let saida = SaidaExecucao {
607 stdout: "comando executado".to_string(),
608 stderr: "aviso harmless".to_string(),
609 exit_code: Some(0),
610 truncado_stdout: false,
611 truncado_stderr: false,
612 duracao_ms: 1000,
613 };
614 assert_eq!(saida.stdout, "comando executado");
615 assert_eq!(saida.stderr, "aviso harmless");
616 assert_eq!(saida.exit_code, Some(0));
617 assert_eq!(saida.duracao_ms, 1000);
618 assert!(!saida.truncado_stdout);
619 assert!(!saida.truncado_stderr);
620 }
621
622 #[test]
623 fn saida_execucao_sem_stderr() {
624 let saida = SaidaExecucao {
625 stdout: "ok".to_string(),
626 stderr: String::new(),
627 exit_code: Some(0),
628 truncado_stdout: false,
629 truncado_stderr: false,
630 duracao_ms: 50,
631 };
632 assert!(saida.stderr.is_empty());
633 }
634
635 #[test]
636 fn saida_execucao_com_sinal_em_vez_de_exit_code() {
637 let saida = SaidaExecucao {
638 stdout: String::new(),
639 stderr: "signal received".to_string(),
640 exit_code: None,
641 truncado_stdout: false,
642 truncado_stderr: false,
643 duracao_ms: 5000,
644 };
645 assert!(saida.exit_code.is_none());
646 }
647
648 #[test]
649 fn saida_execucao_json_contem_campos_obrigatorios() {
650 let saida = SaidaExecucao {
651 stdout: "output".to_string(),
652 stderr: "erro".to_string(),
653 exit_code: Some(0),
654 truncado_stdout: false,
655 truncado_stderr: false,
656 duracao_ms: 100,
657 };
658 imprimir_saida_execucao_json(&saida);
659 }
660}