1use crate::file_io::{CipherFile, RoPlainFile, RwPlainFile};
4use crate::{utils, PassError, Result};
5use std::collections::hash_set::Iter as HashSetIter;
6use std::collections::HashSet;
7use std::fs::File;
8use std::hash::{Hash, Hasher};
9use std::io::{BufRead, BufReader};
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Eq, PartialEq, Clone, Hash)]
14pub enum StoreEntry {
15 Directory(StoreDirectoryRef),
17 File(StoreFileRef),
19}
20
21impl StoreEntry {
22 pub fn name(&self) -> Result<String> {
27 match self {
28 Self::Directory(dir) => dir.name(),
29 Self::File(file) => file.name(),
30 }
31 }
32
33 pub(crate) fn verify(&self) -> Result<()> {
35 match self {
36 Self::Directory(dir) => dir.verify(),
37 Self::File(file) => file.verify(),
38 }
39 }
40}
41
42#[derive(Debug, Eq, Clone)]
44pub struct StoreDirectoryRef {
45 pub path: PathBuf,
47 pub content: HashSet<StoreEntry>,
49}
50
51impl StoreDirectoryRef {
52 pub fn name(&self) -> Result<String> {
57 Ok(utils::path2str(utils::abspath2relpath(&self.path)?)?.to_string())
58 }
59
60 pub(crate) fn verify(&self) -> Result<()> {
62 if self.path.exists() && self.path.is_dir() {
63 Ok(())
64 } else {
65 Err(PassError::InvalidStoreFormat(
66 self.path.to_owned(),
67 "Path either does not exist or is not a directory".to_string(),
68 ))
69 }
70 }
71
72 pub fn iter(&self) -> StoreDirectoryIter {
78 StoreDirectoryIter {
79 entries: self.content.iter(),
80 current_dir: None,
81 }
82 }
83}
84
85impl Hash for StoreDirectoryRef {
86 fn hash<H: Hasher>(&self, state: &mut H) {
87 self.path.hash(state);
88 }
89}
90
91impl PartialEq for StoreDirectoryRef {
92 fn eq(&self, other: &Self) -> bool {
93 self.path == other.path
94 }
95}
96
97impl<'a> IntoIterator for &'a StoreDirectoryRef {
98 type Item = &'a StoreEntry;
99 type IntoIter = StoreDirectoryIter<'a>;
100
101 fn into_iter(self) -> Self::IntoIter {
102 self.iter()
103 }
104}
105
106#[derive(Debug)]
109pub struct StoreDirectoryIter<'a> {
110 entries: HashSetIter<'a, StoreEntry>,
111 current_dir: Option<Box<StoreDirectoryIter<'a>>>,
112}
113
114impl<'a> Iterator for StoreDirectoryIter<'a> {
115 type Item = &'a StoreEntry;
116
117 fn next(&mut self) -> Option<Self::Item> {
118 match self.current_dir {
119 Some(ref mut entry) => match entry.next() {
120 Some(next_entry) => Some(next_entry),
121 None => {
122 self.current_dir = None;
123 self.next()
124 }
125 },
126 None => match self.entries.next() {
127 Some(next_entry) => match next_entry {
128 StoreEntry::File(_) => Some(next_entry),
129 StoreEntry::Directory(dir) => {
130 self.current_dir = Some(Box::new(dir.iter()));
131 self.next()
132 }
133 },
134 None => None,
135 },
136 }
137 }
138}
139
140#[derive(Debug, Eq, PartialEq, Clone, Hash)]
142pub struct StoreFileRef {
143 pub path: PathBuf,
145}
146
147impl StoreFileRef {
148 pub fn name(&self) -> Result<String> {
153 let relative_path = utils::path2str(utils::abspath2relpath(&self.path)?)?;
154
155 Ok(relative_path
156 .strip_suffix(".gpg")
157 .ok_or_else(|| {
158 PassError::InvalidStoreFormat(
159 self.path.to_owned(),
160 "File does not end with .gpg extension".to_string(),
161 )
162 })?
163 .to_string())
164 }
165
166 pub fn encryption_keys(&self) -> Result<Vec<gpgme::Key>> {
190 log::warn!(
191 "Looking for encryption keys for entry at {}",
192 self.path.display()
193 );
194
195 fn look_for_keys_file_from_dir(path: &Path) -> Result<PathBuf> {
197 log::trace!("Looking for .gpg-id file in directory {}", path.display());
198
199 let gpg_id_path = path.join(".gpg-id");
200 if gpg_id_path.exists() {
201 if gpg_id_path.is_file() {
202 Ok(gpg_id_path)
203 } else {
204 Err(PassError::InvalidStoreFormat(
205 gpg_id_path,
206 "Path is a directory but should be a file containing encryption key ids"
207 .to_string(),
208 ))
209 }
210 } else {
211 look_for_keys_file_from_dir(path.parent().ok_or_else(|| {
213 PassError::InvalidStoreFormat(
214 path.to_owned(),
215 "Path does not hava a parent but a .gpg-id file has not yet been found"
216 .to_string(),
217 )
218 })?)
219 }
220 }
221
222 let keys_path = look_for_keys_file_from_dir(self.path.parent().ok_or_else(|| {
224 PassError::InvalidStoreFormat(
225 self.path.to_owned(),
226 "File does not have a parent which means it is not contained in a password store"
227 .to_string(),
228 )
229 })?)?;
230
231 log::trace!(
233 "Found .gpg-id file at {}, inspecting gpg keys from it",
234 keys_path.display()
235 );
236 let mut gpg_ctx = utils::create_gpg_context()?;
237 let file = File::open(keys_path)?;
238 let buffered_reader = BufReader::new(file);
239 buffered_reader
240 .lines()
241 .map(|maybe_line| match maybe_line {
242 Err(e) => Err(PassError::from(e)),
243 Ok(line) => {
244 log::trace!("Loading key {}", line);
245 Ok(gpg_ctx
246 .get_key(&line)
247 .map_err(|_| PassError::GpgKeyNotFoundError(line))?)
248 }
249 })
250 .collect()
251 }
252
253 pub fn cipher_io(&self) -> Result<CipherFile> {
255 CipherFile::new(&self.path)
256 }
257
258 pub fn plain_io_rw(&self) -> Result<RwPlainFile> {
260 RwPlainFile::new(&self.path, self.encryption_keys()?)
261 }
262
263 pub fn plain_io_ro(&self) -> Result<RoPlainFile> {
265 RoPlainFile::new(&self.path)
266 }
267
268 pub(crate) fn verify(&self) -> Result<()> {
270 if self.path.exists()
271 && self.path.is_file()
272 && match self.path.extension() {
273 None => false,
274 Some(extension) => extension == "gpg",
275 }
276 {
277 Ok(())
278 } else {
279 Err(PassError::InvalidStoreFormat(self.path.to_owned(), "Path either does not exist, is not a regular file or does not have a .gpg extension".to_string()))
280 }
281 }
282}