psh_db/
lib.rs

1//! This crate provides alias database backend for [`psh`] using plain file as a storage.
2//!
3//! [`psh`]: https://docs.rs/psh/latest/psh
4
5use std::fs::{self, File, Permissions};
6use std::io::{BufRead, BufReader, BufWriter, Write};
7use std::path::{Path, PathBuf};
8use std::os::unix::fs::PermissionsExt;
9
10use anyhow::Result;
11use zeroize::Zeroize;
12
13use psh::{PshStore, ZeroizingString};
14
15/// Default filename to be used within user home directory unless specified.
16pub const DB_FILE: &str = ".psh.db";
17const DEBUG_DB_PATH: &str = "/tmp/psh.db";
18
19/// `psh` alias database.
20pub struct PshDb {
21    path: PathBuf,
22}
23
24impl PshDb {
25    /// Creates new instance of database with specified `path`. If given `path` is relative,
26    /// prepends it with user home directory.
27    ///
28    /// # Panics
29    ///
30    /// Panics if user has no [`home directory`].
31    ///
32    /// [`home directory`]: https://docs.rs/home/latest/home/
33    pub fn new(path: &Path) -> Self {
34        let db_path =
35            if path.has_root() {
36                path.to_path_buf()
37            } else {
38                let mut db_path = home::home_dir()
39                    .expect("User has no home directory");
40                db_path.push(path);
41                db_path
42            };
43
44        Self {
45            path: db_path,
46        }
47    }
48
49    fn tmp_path(&self) -> PathBuf {
50        let db_path = self.path.clone();
51        let mut db_tmp_path = db_path.into_os_string();
52        db_tmp_path.push(".tmp");
53        db_tmp_path.into()
54    }
55}
56
57impl Default for PshDb {
58    fn default() -> Self {
59        let mut db_path = home::home_dir()
60            .expect("User has no home directory");
61        db_path.push(DB_FILE);
62
63        // Substitute database path for testing purposes in debug builds
64        if cfg!(debug_assertions) {
65            db_path = PathBuf::from(DEBUG_DB_PATH);
66        }
67
68        Self::new(&db_path)
69    }
70}
71
72impl PshStore for PshDb {
73    fn exists(&self) -> bool {
74        if self.path.exists() {
75            let metadata = fs::metadata(&self.path).unwrap();
76            if metadata.len() > 0 {
77                return true;
78            }
79        }
80        false
81    }
82
83    fn records(&self) -> Box<dyn Iterator<Item=ZeroizingString>> {
84        if self.exists() {
85            let db = File::open(&self.path).expect("Unable to open file for reading");
86            let reader = BufReader::new(db);
87            Box::new(PshDbIter { reader })
88        } else {
89            Box::new(PshDbIter { reader: std::io::empty() })
90        }
91    }
92
93    fn append(&mut self, record: &ZeroizingString) -> Result<()> {
94        let mut db = File::options().create(true).append(true).open(&self.path)?;
95        let user_only_perms = Permissions::from_mode(0o600);
96        db.set_permissions(user_only_perms)?;
97
98        let mut record = record.to_string();
99        record.push('\n');
100        db.write_all(record.as_bytes())?;
101        record.zeroize();
102
103        Ok(())
104    }
105
106    fn delete(&mut self, record: &ZeroizingString) -> Result<()> {
107        let db = File::open(&self.path)?;
108        let db_temp = File::create(self.tmp_path())?;
109        let user_only_perms = Permissions::from_mode(0o600);
110        db_temp.set_permissions(user_only_perms)?;
111
112        let mut reader = BufReader::new(&db);
113        let mut writer = BufWriter::new(&db_temp);
114
115        let mut buf = String::new();
116        loop {
117            match reader.read_line(&mut buf) {
118                Ok(0) => break,
119                Ok(_) => {
120                    if **record != buf.trim() {
121                        writeln!(writer, "{}", buf.trim())?;
122                    }
123                    buf.zeroize();
124                }
125                Err(e) => panic!("Failed to read from file: {}", e),
126            }
127        }
128        buf.zeroize();
129
130        fs::rename(self.tmp_path(), &self.path)?;
131
132        Ok(())
133    }
134}
135
136struct PshDbIter<T: BufRead> {
137    reader: T,
138}
139
140impl<T: BufRead> Iterator for PshDbIter<T> {
141    type Item = ZeroizingString;
142
143    fn next(&mut self) -> Option<Self::Item> {
144        let mut buf = String::new();
145        match self.reader.read_line(&mut buf) {
146            Ok(0) => None,
147            Ok(_) => {
148                let item = Some(ZeroizingString::new(buf.trim().to_string()));
149                buf.zeroize();
150                item
151            }
152            Err(e) => panic!("Failed to read from file: {}", e),
153        }
154    }
155}