use std::ffi::OsString;
use std::fs;
use std::path::{self, Path, PathBuf};
use anyhow::{ensure, Result};
use thiserror::Error;
use walkdir::{DirEntry, WalkDir};
use crate::{
crypto::{self, prelude::*},
sync::Sync,
vendor::shellexpand,
Recipients,
};
pub const SECRET_SUFFIX: &str = ".gpg";
#[derive(Clone)]
pub struct Store {
pub root: PathBuf,
}
impl Store {
pub fn open<P: AsRef<str>>(root: P) -> Result<Self> {
let root: PathBuf = shellexpand::full(&root)
.map_err(Err::ExpandPath)?
.as_ref()
.into();
ensure!(root.is_dir(), Err::NoRootDir(root));
Ok(Self { root })
}
pub fn recipients(&self) -> Result<Recipients> {
Recipients::load(&self)
}
pub fn sync(&self) -> Sync {
Sync::new(&self)
}
pub fn secret_iter(&self) -> SecretIter {
self.secret_iter_config(SecretIterConfig::default())
}
pub fn secret_iter_config(&self, config: SecretIterConfig) -> SecretIter {
SecretIter::new(self.root.clone(), config)
}
pub fn secrets(&self, filter: Option<String>) -> Vec<Secret> {
self.secret_iter().filter_name(filter).collect()
}
pub fn find_at(&self, path: &str) -> Option<Secret> {
let path = self.root.as_path().join(path);
let path = path.to_str()?;
let with_suffix = PathBuf::from(format!("{}{}", path, SECRET_SUFFIX));
if with_suffix.is_file() {
return Some(Secret::from(&self, with_suffix));
}
let without_suffix = Path::new(path);
if without_suffix.is_file() {
return Some(Secret::from(&self, without_suffix.to_path_buf()));
}
None
}
pub fn find(&self, query: Option<String>) -> FindSecret {
if let Some(query) = &query {
if let Some(secret) = self.find_at(&query) {
return FindSecret::Exact(secret);
}
}
FindSecret::Many(self.secrets(query))
}
pub fn normalize_secret_path<P: AsRef<Path>>(
&self,
target: P,
name_hint: Option<&str>,
create_dirs: bool,
) -> Result<PathBuf> {
let mut path = PathBuf::from(target.as_ref());
if let Some(path_str) = path.to_str() {
path = PathBuf::from(
shellexpand::full(path_str)
.map_err(Err::ExpandPath)?
.as_ref(),
);
}
let target_is_dir = path.is_dir()
|| target
.as_ref()
.to_str()
.and_then(|s| s.chars().last())
.map(|s| path::is_separator(s))
.unwrap_or(false);
if let Ok(tmp) = path.strip_prefix(&self.root) {
path = tmp.into();
}
if path.is_absolute() {
path = PathBuf::from(format!(".{}{}", path::MAIN_SEPARATOR, path.display()));
}
path = self.root.as_path().join(path);
if target_is_dir {
path.push(name_hint.ok_or_else(|| Err::TargetDirWithoutNamehint(path.clone()))?);
}
let ext: OsString = SECRET_SUFFIX.trim_start_matches(".").into();
if path.extension() != Some(&ext) {
let mut tmp = path.as_os_str().to_owned();
tmp.push(SECRET_SUFFIX);
path = PathBuf::from(tmp);
}
if create_dirs {
let parent = path.parent().unwrap();
if !parent.is_dir() {
fs::create_dir_all(parent).map_err(Err::CreateDir)?;
}
}
Ok(path)
}
}
pub enum FindSecret {
Exact(Secret),
Many(Vec<Secret>),
}
#[derive(Debug, Clone)]
pub struct Secret {
pub name: String,
pub path: PathBuf,
}
impl Secret {
pub fn from(store: &Store, path: PathBuf) -> Self {
Self::in_root(&store.root, path)
}
pub fn in_root(root: &Path, path: PathBuf) -> Self {
let name: String = relative_path(root, &path)
.ok()
.and_then(|f| f.to_str())
.map(|f| f.trim_end_matches(SECRET_SUFFIX))
.unwrap_or_else(|| "?")
.to_string();
Self { name, path }
}
pub fn relative_path<'a>(
&'a self,
root: &'a Path,
) -> Result<&'a Path, std::path::StripPrefixError> {
relative_path(root, &self.path)
}
pub fn alias_target(&self, store: &Store) -> Result<Secret> {
let mut path = self.path.parent().unwrap().join(fs::read_link(&self.path)?);
if let Ok(canonical_path) = path.canonicalize() {
path = canonical_path;
}
Ok(Secret::from(store, path))
}
}
pub fn relative_path<'a>(
root: &'a Path,
path: &'a PathBuf,
) -> Result<&'a Path, std::path::StripPrefixError> {
path.strip_prefix(&root)
}
#[derive(Clone, Debug)]
pub struct SecretIterConfig {
pub find_files: bool,
pub find_symlink_files: bool,
}
impl Default for SecretIterConfig {
fn default() -> Self {
Self {
find_files: true,
find_symlink_files: true,
}
}
}
pub struct SecretIter {
root: PathBuf,
walker: Box<dyn Iterator<Item = DirEntry>>,
}
impl SecretIter {
pub fn new(root: PathBuf, config: SecretIterConfig) -> Self {
let walker = WalkDir::new(&root)
.follow_links(true)
.into_iter()
.filter_entry(|e| !is_hidden_subdir(e))
.filter_map(|e| e.ok())
.filter(is_secret_file)
.filter(move |entry| filter_by_config(entry, &config));
Self {
root,
walker: Box::new(walker),
}
}
pub fn filter_name(self, filter: Option<String>) -> FilterSecretIter<Self> {
FilterSecretIter::new(self, filter)
}
}
impl Iterator for SecretIter {
type Item = Secret;
fn next(&mut self) -> Option<Self::Item> {
self.walker
.next()
.map(|e| Secret::in_root(&self.root, e.path().into()))
}
}
fn is_hidden_subdir(entry: &DirEntry) -> bool {
entry.depth() > 0
&& entry
.file_name()
.to_str()
.map(|s| s.starts_with("."))
.unwrap_or(false)
}
fn is_secret_file(entry: &DirEntry) -> bool {
entry.file_type().is_file()
&& entry
.file_name()
.to_str()
.map(|s| s.ends_with(SECRET_SUFFIX))
.unwrap_or(false)
}
fn filter_by_config(entry: &DirEntry, config: &SecretIterConfig) -> bool {
if config.find_files && config.find_symlink_files {
return true;
}
if config.find_symlink_files && entry.path_is_symlink() {
return true;
}
if !config.find_symlink_files && entry.path_is_symlink() {
return false;
}
if !config.find_files && !entry.path_is_symlink() {
return false;
}
true
}
pub fn can_decrypt(store: &Store) -> bool {
store
.secret_iter()
.next()
.map(|secret| {
crypto::context(crypto::PROTO)
.map(|mut context| context.can_decrypt_file(&secret.path).unwrap_or(true))
.unwrap_or(false)
})
.unwrap_or(true)
}
pub struct FilterSecretIter<I>
where
I: Iterator<Item = Secret>,
{
inner: I,
filter: Option<String>,
}
impl<I> FilterSecretIter<I>
where
I: Iterator<Item = Secret>,
{
pub fn new(inner: I, filter: Option<String>) -> Self {
Self { inner, filter }
}
}
impl<I> Iterator for FilterSecretIter<I>
where
I: Iterator<Item = Secret>,
{
type Item = Secret;
fn next(&mut self) -> Option<Self::Item> {
if self.filter.is_none() {
return self.inner.next();
}
let filter = self.filter.as_ref().unwrap();
while let Some(secret) = self.inner.next() {
if secret.name.contains(filter) {
return Some(secret);
}
}
None
}
}
#[derive(Debug, Error)]
pub enum Err {
#[error("failed to expand store root path")]
ExpandPath(#[source] shellexpand::LookupError<std::env::VarError>),
#[error("failed to open password store, not a directory: {0}")]
NoRootDir(PathBuf),
#[error("failed to create directory")]
CreateDir(#[source] std::io::Error),
#[error("cannot use directory as target without name hint")]
TargetDirWithoutNamehint(PathBuf),
}