1use anyhow::{Context, Result};
2use rusqlite::Connection;
3use std::collections::HashSet;
4use std::path::Path;
5
6use crate::config::Config;
7use crate::db::{self, NewCommand};
8use crate::filter;
9
10#[derive(Debug, Default, Clone)]
12pub struct ImportStats {
13 pub total: usize,
14 pub imported: usize,
15 pub skipped_sensitive: usize,
16 pub skipped_duplicate: usize,
17}
18
19pub fn import_bash_history(conn: &Connection, path: &Path, config: &Config) -> Result<ImportStats> {
21 let content = std::fs::read_to_string(path)
22 .with_context(|| format!("lecture de l'historique {}", path.display()))?;
23 import_from_str(conn, &content, config)
24}
25
26pub fn import_from_str(conn: &Connection, content: &str, config: &Config) -> Result<ImportStats> {
28 let mut stats = ImportStats::default();
29 let mut seen: HashSet<String> = HashSet::new();
31 let created_at = db::now_timestamp();
32
33 for raw in content.lines() {
34 let line = raw.trim();
35 if line.is_empty() {
36 continue;
37 }
38 if is_history_timestamp(line) {
40 continue;
41 }
42
43 stats.total += 1;
44
45 if filter::is_sensitive(line, &config.sensitive_keywords) {
46 stats.skipped_sensitive += 1;
47 continue;
48 }
49
50 let hash = db::compute_hash(line, None);
51 if !seen.insert(hash) {
52 stats.skipped_duplicate += 1;
53 continue;
54 }
55
56 let cmd = NewCommand {
57 command: line.to_string(),
58 cwd: None,
59 shell: Some("bash".to_string()),
60 hostname: None,
61 exit_code: None,
62 created_at: created_at.clone(),
63 ..Default::default()
64 };
65
66 if db::insert_command(conn, &cmd)? {
67 stats.imported += 1;
68 } else {
69 stats.skipped_duplicate += 1;
70 }
71 }
72
73 Ok(stats)
74}
75
76fn is_history_timestamp(line: &str) -> bool {
77 line.len() > 1 && line.starts_with('#') && line[1..].chars().all(|c| c.is_ascii_digit())
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::db;
84
85 #[test]
86 fn import_filtre_et_dedoublonne() {
87 let conn = db::open_in_memory().unwrap();
88 let cfg = Config::default();
89 let content = "ls -la\n#1700000000\nls -la\nexport TOKEN=abc\n\ngit status\n";
90
91 let stats = import_from_str(&conn, content, &cfg).unwrap();
92
93 assert_eq!(stats.total, 4);
94 assert_eq!(stats.imported, 2); assert_eq!(stats.skipped_sensitive, 1); assert_eq!(stats.skipped_duplicate, 1); assert_eq!(db::count(&conn).unwrap(), 2);
98 }
99
100 #[test]
101 fn detecte_les_lignes_timestamp() {
102 assert!(is_history_timestamp("#1700000000"));
103 assert!(!is_history_timestamp("# un commentaire"));
104 assert!(!is_history_timestamp("echo #1234"));
105 }
106}