1use std::ffi::OsString;
4use std::fs;
5use std::path::{self, Path, PathBuf};
6
7use anyhow::{Result, ensure};
8use thiserror::Error;
9use walkdir::{DirEntry, WalkDir};
10
11#[cfg(all(feature = "tomb", target_os = "linux"))]
12use crate::tomb::Tomb;
13use crate::{
14 Recipients,
15 crypto::{self, prelude::*},
16 sync::Sync,
17};
18
19pub const SECRET_SUFFIX: &str = ".gpg";
21
22#[derive(Clone)]
24pub struct Store {
25 pub root: PathBuf,
29}
30
31impl Store {
32 pub fn open<P: AsRef<str>>(root: P) -> Result<Self> {
34 let root: PathBuf = shellexpand::full(&root)
35 .map_err(Err::ExpandPath)?
36 .as_ref()
37 .into();
38 let root = root.canonicalize().map_err(Err::CanonicalizePath)?;
39
40 ensure!(root.is_dir(), Err::NoRootDir(root));
42
43 Ok(Self { root })
46 }
47
48 pub fn recipients(&self) -> Result<Recipients> {
50 Recipients::load(self)
51 }
52
53 pub fn sync(&self) -> Sync<'_> {
55 Sync::new(self)
56 }
57
58 #[cfg(all(feature = "tomb", target_os = "linux"))]
60 pub fn tomb(&self, quiet: bool, verbose: bool, force: bool) -> Tomb<'_> {
61 Tomb::new(self, quiet, verbose, force)
62 }
63
64 pub fn secret_iter(&self) -> SecretIter {
66 self.secret_iter_config(SecretIterConfig::default())
67 }
68
69 pub fn secret_iter_config(&self, config: SecretIterConfig) -> SecretIter {
71 SecretIter::new(self.root.clone(), config)
72 }
73
74 pub fn secrets(&self, filter: Option<String>) -> Vec<Secret> {
76 self.secret_iter().filter_name(filter).collect()
77 }
78
79 pub fn find_at(&self, path: &str) -> Option<Secret> {
81 let path = self.root.as_path().join(path);
83 let path = path.to_str()?;
84
85 let with_suffix = PathBuf::from(format!("{path}{SECRET_SUFFIX}"));
87 if with_suffix.is_file() {
88 return Some(Secret::from(self, with_suffix));
89 }
90
91 let without_suffix = Path::new(path);
93 if without_suffix.is_file() {
94 return Some(Secret::from(self, without_suffix.to_path_buf()));
95 }
96
97 None
98 }
99
100 pub fn find(&self, query: Option<String>) -> FindSecret {
105 if let Some(query) = &query
107 && let Some(secret) = self.find_at(query)
108 {
109 return FindSecret::Exact(secret);
110 }
111
112 FindSecret::Many(self.secrets(query))
114 }
115
116 pub fn normalize_secret_path<P: AsRef<Path>>(
123 &self,
124 target: P,
125 name_hint: Option<&str>,
126 create_dirs: bool,
127 ) -> Result<PathBuf> {
128 let mut path = PathBuf::from(target.as_ref());
130
131 if let Some(path_str) = path.to_str() {
133 path = PathBuf::from(
134 shellexpand::full(path_str)
135 .map_err(Err::ExpandPath)?
136 .as_ref(),
137 );
138 }
139
140 let target_is_dir = path.is_dir()
141 || target
142 .as_ref()
143 .to_str()
144 .and_then(|s| s.chars().last())
145 .map(path::is_separator)
146 .unwrap_or(false);
147
148 if let Ok(tmp) = path.strip_prefix(&self.root) {
150 path = tmp.into();
151 }
152
153 if path.is_absolute() {
155 path = PathBuf::from(format!(".{}{}", path::MAIN_SEPARATOR, path.display()));
156 }
157
158 path = self.root.as_path().join(path);
160
161 if target_is_dir {
163 path.push(name_hint.ok_or_else(|| Err::TargetDirWithoutNamehint(path.clone()))?);
164 }
165
166 let ext: OsString = SECRET_SUFFIX.trim_start_matches('.').into();
168 if path.extension() != Some(&ext) {
169 let mut tmp = path.as_os_str().to_owned();
170 tmp.push(SECRET_SUFFIX);
171 path = PathBuf::from(tmp);
172 }
173
174 if create_dirs {
176 let parent = path.parent().unwrap();
177 if !parent.is_dir() {
178 fs::create_dir_all(parent).map_err(Err::CreateDir)?;
179 }
180 }
181
182 Ok(path)
183 }
184}
185
186pub enum FindSecret {
188 Exact(Secret),
190
191 Many(Vec<Secret>),
193}
194
195#[derive(Debug, Clone)]
197pub struct Secret {
198 pub name: String,
200
201 pub path: PathBuf,
203}
204
205impl Secret {
206 pub fn from(store: &Store, path: PathBuf) -> Self {
208 Self::in_root(&store.root, path)
209 }
210
211 pub fn in_root(root: &Path, path: PathBuf) -> Self {
213 let name: String = relative_path(root, &path)
214 .ok()
215 .and_then(|f| f.to_str())
216 .map(|f| f.trim_end_matches(SECRET_SUFFIX))
217 .unwrap_or_else(|| "?")
218 .to_string();
219 Self { name, path }
220 }
221
222 pub fn relative_path<'a>(
224 &'a self,
225 root: &'a Path,
226 ) -> Result<&'a Path, std::path::StripPrefixError> {
227 relative_path(root, &self.path)
228 }
229
230 pub fn alias_target(&self, store: &Store) -> Result<Secret> {
237 let mut path = self.path.parent().unwrap().join(fs::read_link(&self.path)?);
239 if let Ok(canonical_path) = path.canonicalize() {
240 path = canonical_path;
241 }
242
243 Ok(Secret::from(store, path))
244 }
245}
246
247pub fn relative_path<'a>(
249 root: &'a Path,
250 path: &'a Path,
251) -> Result<&'a Path, std::path::StripPrefixError> {
252 path.strip_prefix(root)
253}
254
255#[derive(Clone, Debug)]
259pub struct SecretIterConfig {
260 pub find_files: bool,
262
263 pub find_symlink_files: bool,
267}
268
269impl Default for SecretIterConfig {
270 fn default() -> Self {
271 Self {
272 find_files: true,
273 find_symlink_files: true,
274 }
275 }
276}
277
278pub struct SecretIter {
283 root: PathBuf,
285
286 walker: Box<dyn Iterator<Item = DirEntry>>,
288}
289
290impl SecretIter {
291 pub fn new(root: PathBuf, config: SecretIterConfig) -> Self {
293 let walker = WalkDir::new(&root)
294 .follow_links(true)
295 .into_iter()
296 .filter_entry(|e| !is_hidden_subdir(e))
297 .filter_map(|e| e.ok())
298 .filter(is_secret_file)
299 .filter(move |entry| filter_by_config(entry, &config));
300 Self {
301 root,
302 walker: Box::new(walker),
303 }
304 }
305
306 pub fn filter_name(self, filter: Option<String>) -> FilterSecretIter<Self> {
308 FilterSecretIter::new(self, filter)
309 }
310}
311
312impl Iterator for SecretIter {
313 type Item = Secret;
314
315 fn next(&mut self) -> Option<Self::Item> {
316 self.walker
317 .next()
318 .map(|e| Secret::in_root(&self.root, e.path().into()))
319 }
320}
321
322fn is_hidden_subdir(entry: &DirEntry) -> bool {
324 entry.depth() > 0
325 && entry
326 .file_name()
327 .to_str()
328 .map(|s| s.starts_with('.') || s == "lost+found")
329 .unwrap_or(false)
330}
331
332fn is_secret_file(entry: &DirEntry) -> bool {
334 entry.file_type().is_file()
335 && entry
336 .file_name()
337 .to_str()
338 .map(|s| s.ends_with(SECRET_SUFFIX))
339 .unwrap_or(false)
340}
341
342fn filter_by_config(entry: &DirEntry, config: &SecretIterConfig) -> bool {
344 if config.find_files && config.find_symlink_files {
346 return true;
347 }
348
349 if config.find_symlink_files && entry.path_is_symlink() {
351 return true;
352 }
353
354 if !config.find_symlink_files && entry.path_is_symlink() {
356 return false;
357 }
358
359 if !config.find_files && !entry.path_is_symlink() {
361 return false;
362 }
363
364 true
365}
366
367pub fn can_decrypt(store: &Store) -> bool {
373 store
375 .secret_iter()
376 .next()
377 .map(|secret| {
378 crypto::context(&crate::CONFIG)
379 .map(|mut context| context.can_decrypt_file(&secret.path).unwrap_or(true))
380 .unwrap_or(false)
381 })
382 .unwrap_or(true)
383}
384
385pub struct FilterSecretIter<I>
387where
388 I: Iterator<Item = Secret>,
389{
390 inner: I,
391 filter: Option<String>,
392}
393
394impl<I> FilterSecretIter<I>
395where
396 I: Iterator<Item = Secret>,
397{
398 pub fn new(inner: I, filter: Option<String>) -> Self {
400 Self { inner, filter }
401 }
402}
403
404impl<I> Iterator for FilterSecretIter<I>
405where
406 I: Iterator<Item = Secret>,
407{
408 type Item = Secret;
409
410 fn next(&mut self) -> Option<Self::Item> {
411 let filter = match &self.filter {
413 None => return self.inner.next(),
414 Some(filter) => filter.to_lowercase(),
415 };
416
417 self.inner
418 .find(|secret| secret.name.to_lowercase().contains(&filter))
419 }
420}
421
422#[derive(Debug, Error)]
424pub enum Err {
425 #[error("failed to expand store root path")]
426 ExpandPath(#[source] shellexpand::LookupError<std::env::VarError>),
427
428 #[error("failed to canonicalize store root path")]
429 CanonicalizePath(#[source] std::io::Error),
430
431 #[error("failed to open password store, not a directory: {0}")]
432 NoRootDir(PathBuf),
433
434 #[error("failed to create directory")]
435 CreateDir(#[source] std::io::Error),
436
437 #[error("cannot use directory as target without name hint")]
438 TargetDirWithoutNamehint(PathBuf),
439}