787 lines
22 KiB
Rust
787 lines
22 KiB
Rust
//! Typed read-only password-store operations.
|
|
|
|
use std::{collections::BTreeMap, error::Error, fmt, path::Path};
|
|
|
|
use regex::bytes::{Regex, RegexBuilder};
|
|
|
|
use crate::{
|
|
command::{EXIT_FAILURE, GrepRequest, Presentation, ShowRequest},
|
|
crypto::{CryptoError, KeyStore, SecretProvider},
|
|
repository::{
|
|
DirectoryPath, EntryPath, Repository, RepositoryError, RepositorySnapshot, ResolvedObject,
|
|
SecretBytes,
|
|
},
|
|
};
|
|
|
|
const EXTENSIONS_DIRECTORY: &str = ".extensions";
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
pub enum TreeNodeKind {
|
|
Directory,
|
|
Entry,
|
|
}
|
|
|
|
/// Stable storage-owned identity for a navigation tree object.
|
|
///
|
|
/// Frontends retain this value across refreshed [`TreeModel`] instances
|
|
/// instead of reconstructing identity from rendered labels.
|
|
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
pub enum TreeNodeId {
|
|
Directory(DirectoryPath),
|
|
Entry(EntryPath),
|
|
}
|
|
|
|
impl TreeNodeId {
|
|
pub fn path(&self) -> &Path {
|
|
match self {
|
|
Self::Directory(path) => path.as_path(),
|
|
Self::Entry(path) => path.as_path(),
|
|
}
|
|
}
|
|
|
|
pub fn kind(&self) -> TreeNodeKind {
|
|
match self {
|
|
Self::Directory(_) => TreeNodeKind::Directory,
|
|
Self::Entry(_) => TreeNodeKind::Entry,
|
|
}
|
|
}
|
|
|
|
pub fn is_directory(&self) -> bool {
|
|
matches!(self, Self::Directory(_))
|
|
}
|
|
|
|
pub fn entry(&self) -> Option<&EntryPath> {
|
|
match self {
|
|
Self::Entry(path) => Some(path),
|
|
Self::Directory(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Presentation-safe state calculated by storage services for a tree object.
|
|
/// Frontends render these flags and never infer them from names or paths.
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
pub struct TreeNodeIndicators {
|
|
locked: bool,
|
|
changed: bool,
|
|
conflict: bool,
|
|
}
|
|
|
|
impl TreeNodeIndicators {
|
|
pub const fn new(locked: bool, changed: bool, conflict: bool) -> Self {
|
|
Self {
|
|
locked,
|
|
changed,
|
|
conflict,
|
|
}
|
|
}
|
|
|
|
pub const fn is_locked(self) -> bool {
|
|
self.locked
|
|
}
|
|
|
|
pub const fn is_changed(self) -> bool {
|
|
self.changed
|
|
}
|
|
|
|
pub const fn has_conflict(self) -> bool {
|
|
self.conflict
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct TreeNode {
|
|
name: String,
|
|
id: TreeNodeId,
|
|
indicators: TreeNodeIndicators,
|
|
children: Vec<TreeNode>,
|
|
}
|
|
|
|
impl TreeNode {
|
|
pub fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
pub fn path(&self) -> &str {
|
|
self.id
|
|
.path()
|
|
.to_str()
|
|
.expect("tree construction rejects non-UTF-8 paths")
|
|
}
|
|
|
|
pub fn kind(&self) -> TreeNodeKind {
|
|
self.id.kind()
|
|
}
|
|
|
|
pub fn id(&self) -> &TreeNodeId {
|
|
&self.id
|
|
}
|
|
|
|
pub fn indicators(&self) -> TreeNodeIndicators {
|
|
self.indicators
|
|
}
|
|
|
|
pub fn children(&self) -> &[TreeNode] {
|
|
&self.children
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct TreeModel {
|
|
title: String,
|
|
root: DirectoryPath,
|
|
children: Vec<TreeNode>,
|
|
}
|
|
|
|
impl TreeModel {
|
|
pub fn title(&self) -> &str {
|
|
&self.title
|
|
}
|
|
|
|
pub fn root(&self) -> &DirectoryPath {
|
|
&self.root
|
|
}
|
|
|
|
pub fn children(&self) -> &[TreeNode] {
|
|
&self.children
|
|
}
|
|
|
|
/// Render the stable, uncolored tree used by the CLI adapter.
|
|
pub fn render_plain(&self) -> String {
|
|
let mut rendered = String::new();
|
|
rendered.push_str(&self.title);
|
|
rendered.push('\n');
|
|
render_children(&mut rendered, &self.children, "");
|
|
rendered
|
|
}
|
|
}
|
|
|
|
pub enum ShowResult {
|
|
Entry(SecretBytes),
|
|
Directory(TreeModel),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum PresentationChannel {
|
|
Clipboard,
|
|
QrCode,
|
|
}
|
|
|
|
pub struct PresentationSecret {
|
|
entry: EntryPath,
|
|
line: usize,
|
|
channel: PresentationChannel,
|
|
contents: SecretBytes,
|
|
}
|
|
|
|
impl PresentationSecret {
|
|
pub fn entry(&self) -> &EntryPath {
|
|
&self.entry
|
|
}
|
|
|
|
pub fn line(&self) -> usize {
|
|
self.line
|
|
}
|
|
|
|
pub fn channel(&self) -> PresentationChannel {
|
|
self.channel
|
|
}
|
|
|
|
pub fn contents(&self) -> &SecretBytes {
|
|
&self.contents
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for PresentationSecret {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("PresentationSecret")
|
|
.field("entry", &self.entry)
|
|
.field("line", &self.line)
|
|
.field("channel", &self.channel)
|
|
.field("contents", &self.contents)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
pub enum ShowOutput {
|
|
Display(ShowResult),
|
|
Present(PresentationSecret),
|
|
}
|
|
|
|
impl fmt::Debug for ShowOutput {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Display(result) => formatter.debug_tuple("Display").field(result).finish(),
|
|
Self::Present(secret) => formatter.debug_tuple("Present").field(secret).finish(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for ShowResult {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Entry(secret) => formatter.debug_tuple("Entry").field(secret).finish(),
|
|
Self::Directory(tree) => formatter.debug_tuple("Directory").field(tree).finish(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct NameMatch {
|
|
id: TreeNodeId,
|
|
}
|
|
|
|
impl NameMatch {
|
|
pub fn path(&self) -> &str {
|
|
self.id
|
|
.path()
|
|
.to_str()
|
|
.expect("search construction rejects non-UTF-8 paths")
|
|
}
|
|
|
|
pub fn kind(&self) -> TreeNodeKind {
|
|
self.id.kind()
|
|
}
|
|
|
|
pub fn id(&self) -> &TreeNodeId {
|
|
&self.id
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct FindResults {
|
|
terms: Vec<String>,
|
|
matches: Vec<NameMatch>,
|
|
tree: TreeModel,
|
|
}
|
|
|
|
impl FindResults {
|
|
pub fn terms(&self) -> &[String] {
|
|
&self.terms
|
|
}
|
|
|
|
pub fn matches(&self) -> &[NameMatch] {
|
|
&self.matches
|
|
}
|
|
|
|
pub fn tree(&self) -> &TreeModel {
|
|
&self.tree
|
|
}
|
|
|
|
pub fn render_plain(&self) -> String {
|
|
format!(
|
|
"Search Terms: {}\n{}",
|
|
self.terms.join(","),
|
|
self.tree.render_plain()
|
|
)
|
|
}
|
|
}
|
|
|
|
pub struct GrepLine {
|
|
number: usize,
|
|
contents: SecretBytes,
|
|
}
|
|
|
|
impl GrepLine {
|
|
pub fn number(&self) -> usize {
|
|
self.number
|
|
}
|
|
|
|
pub fn contents(&self) -> &SecretBytes {
|
|
&self.contents
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for GrepLine {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("GrepLine")
|
|
.field("number", &self.number)
|
|
.field("contents", &self.contents)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
pub struct GrepEntry {
|
|
path: EntryPath,
|
|
lines: Vec<GrepLine>,
|
|
}
|
|
|
|
impl GrepEntry {
|
|
pub fn path(&self) -> &EntryPath {
|
|
&self.path
|
|
}
|
|
|
|
pub fn lines(&self) -> &[GrepLine] {
|
|
&self.lines
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for GrepEntry {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("GrepEntry")
|
|
.field("path", &self.path)
|
|
.field("line_count", &self.lines.len())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct GrepResults {
|
|
entries: Vec<GrepEntry>,
|
|
line_numbers: bool,
|
|
}
|
|
|
|
impl GrepResults {
|
|
pub fn entries(&self) -> &[GrepEntry] {
|
|
&self.entries
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
|
|
pub fn includes_line_numbers(&self) -> bool {
|
|
self.line_numbers
|
|
}
|
|
|
|
/// Render matched bytes without requiring the CLI to reconstruct entry semantics.
|
|
pub fn render_plain(&self) -> SecretBytes {
|
|
let mut rendered = Vec::new();
|
|
for entry in &self.entries {
|
|
rendered.extend_from_slice(entry.path.to_string().as_bytes());
|
|
rendered.extend_from_slice(b":\n");
|
|
for line in &entry.lines {
|
|
if self.line_numbers {
|
|
rendered.extend_from_slice(line.number.to_string().as_bytes());
|
|
rendered.push(b':');
|
|
}
|
|
rendered.extend_from_slice(line.contents.expose());
|
|
rendered.push(b'\n');
|
|
}
|
|
}
|
|
SecretBytes::new(rendered)
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for GrepResults {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("GrepResults")
|
|
.field("entry_count", &self.entries.len())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
pub struct VaultReader<'a> {
|
|
repository: &'a Repository,
|
|
keys: &'a KeyStore,
|
|
}
|
|
|
|
impl<'a> VaultReader<'a> {
|
|
pub fn new(repository: &'a Repository, keys: &'a KeyStore) -> Self {
|
|
Self { repository, keys }
|
|
}
|
|
|
|
pub fn list(&self, directory: &DirectoryPath) -> Result<TreeModel, ReadError> {
|
|
list_tree(self.repository, directory)
|
|
}
|
|
|
|
/// Implement explicit or implicit show dispatch. No path means the root tree; a directory
|
|
/// produces a tree and an entry is decrypted in full.
|
|
pub fn show(
|
|
&self,
|
|
input: Option<&str>,
|
|
provider: &mut impl SecretProvider,
|
|
) -> Result<ShowResult, ReadError> {
|
|
let snapshot = self.repository.snapshot()?;
|
|
let Some(input) = input else {
|
|
return Ok(ShowResult::Directory(build_tree(
|
|
&snapshot,
|
|
&DirectoryPath::root(),
|
|
None,
|
|
)?));
|
|
};
|
|
match snapshot.resolve(input)? {
|
|
ResolvedObject::Entry(entry) => {
|
|
let ciphertext = self.repository.read_entry(entry.path())?;
|
|
Ok(ShowResult::Entry(self.keys.decrypt(&ciphertext, provider)?))
|
|
}
|
|
ResolvedObject::Directory(directory) => Ok(ShowResult::Directory(build_tree(
|
|
&snapshot,
|
|
directory.path(),
|
|
None,
|
|
)?)),
|
|
}
|
|
}
|
|
|
|
/// Execute the typed show request without inspecting a rendered command or display string.
|
|
pub fn execute_show(
|
|
&self,
|
|
request: &ShowRequest,
|
|
provider: &mut impl SecretProvider,
|
|
) -> Result<ShowOutput, ReadError> {
|
|
let Some(input) = request.entry.as_deref() else {
|
|
return self.show(None, provider).map(ShowOutput::Display);
|
|
};
|
|
let snapshot = self.repository.snapshot()?;
|
|
match snapshot.resolve(input)? {
|
|
ResolvedObject::Directory(_) => {
|
|
self.show(Some(input), provider).map(ShowOutput::Display)
|
|
}
|
|
ResolvedObject::Entry(entry) => match request.presentation {
|
|
Presentation::Terminal => self.show(Some(input), provider).map(ShowOutput::Display),
|
|
Presentation::Clipboard { line } => Ok(ShowOutput::Present(PresentationSecret {
|
|
entry: entry.path().clone(),
|
|
line: line.get(),
|
|
channel: PresentationChannel::Clipboard,
|
|
contents: self.select_line(entry.path(), line.get(), provider)?,
|
|
})),
|
|
Presentation::QrCode { line } => Ok(ShowOutput::Present(PresentationSecret {
|
|
entry: entry.path().clone(),
|
|
line: line.get(),
|
|
channel: PresentationChannel::QrCode,
|
|
contents: self.select_line(entry.path(), line.get(), provider)?,
|
|
})),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn select_line(
|
|
&self,
|
|
entry: &EntryPath,
|
|
line: usize,
|
|
provider: &mut impl SecretProvider,
|
|
) -> Result<SecretBytes, ReadError> {
|
|
if line == 0 {
|
|
return Err(ReadError::LineNotFound {
|
|
entry: entry.clone(),
|
|
line,
|
|
});
|
|
}
|
|
let ciphertext = self.repository.read_entry(entry)?;
|
|
let plaintext = self.keys.decrypt(&ciphertext, provider)?;
|
|
let selected = plaintext
|
|
.expose()
|
|
.split(|byte| *byte == b'\n')
|
|
.nth(line - 1)
|
|
.filter(|selected| !selected.is_empty())
|
|
.ok_or_else(|| ReadError::LineNotFound {
|
|
entry: entry.clone(),
|
|
line,
|
|
})?;
|
|
Ok(SecretBytes::new(selected.to_vec()))
|
|
}
|
|
|
|
pub fn find(&self, terms: &[String]) -> Result<FindResults, ReadError> {
|
|
if terms.is_empty() {
|
|
return Err(ReadError::MissingSearchTerms);
|
|
}
|
|
let folded = terms
|
|
.iter()
|
|
.map(|term| term.to_lowercase())
|
|
.collect::<Vec<_>>();
|
|
let snapshot = self.repository.snapshot()?;
|
|
let mut matches = Vec::new();
|
|
for directory in snapshot.directories() {
|
|
if directory.path().as_path().as_os_str().is_empty()
|
|
|| hidden_path(directory.path().as_path())
|
|
{
|
|
continue;
|
|
}
|
|
let name = display_name(directory.path().as_path())?;
|
|
if folded.iter().any(|term| name.to_lowercase().contains(term)) {
|
|
matches.push(NameMatch {
|
|
id: TreeNodeId::Directory(directory.path().clone()),
|
|
});
|
|
}
|
|
}
|
|
for entry in snapshot.entries() {
|
|
if hidden_path(entry.path().as_path()) {
|
|
continue;
|
|
}
|
|
let name = display_name(entry.path().as_path())?;
|
|
if folded.iter().any(|term| name.to_lowercase().contains(term)) {
|
|
matches.push(NameMatch {
|
|
id: TreeNodeId::Entry(entry.path().clone()),
|
|
});
|
|
}
|
|
}
|
|
matches.sort_by(|left, right| left.path().cmp(right.path()));
|
|
let included = matches.iter().map(NameMatch::path).collect::<Vec<_>>();
|
|
let tree = build_tree(&snapshot, &DirectoryPath::root(), Some(included.as_slice()))?;
|
|
Ok(FindResults {
|
|
terms: terms.to_vec(),
|
|
matches,
|
|
tree,
|
|
})
|
|
}
|
|
|
|
pub fn grep(
|
|
&self,
|
|
request: &GrepRequest,
|
|
provider: &mut impl SecretProvider,
|
|
) -> Result<GrepResults, ReadError> {
|
|
let regex = build_regex(request)?;
|
|
let snapshot = self.repository.snapshot()?;
|
|
let mut results = Vec::new();
|
|
for record in snapshot.entries() {
|
|
if hidden_path(record.path().as_path()) {
|
|
continue;
|
|
}
|
|
let ciphertext = self.repository.read_entry(record.path())?;
|
|
let plaintext = self.keys.decrypt(&ciphertext, provider)?;
|
|
let mut lines = Vec::new();
|
|
let mut plaintext_lines = plaintext
|
|
.expose()
|
|
.split(|byte| *byte == b'\n')
|
|
.collect::<Vec<_>>();
|
|
if plaintext.expose().ends_with(b"\n") {
|
|
plaintext_lines.pop();
|
|
}
|
|
for (index, line) in plaintext_lines.into_iter().enumerate() {
|
|
let matched = regex.is_match(line);
|
|
if matched != request.invert_match {
|
|
lines.push(GrepLine {
|
|
number: index + 1,
|
|
contents: SecretBytes::new(line.to_vec()),
|
|
});
|
|
}
|
|
}
|
|
if !lines.is_empty() {
|
|
results.push(GrepEntry {
|
|
path: record.path().clone(),
|
|
lines,
|
|
});
|
|
}
|
|
}
|
|
Ok(GrepResults {
|
|
entries: results,
|
|
line_numbers: request.line_number,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Build a locked navigation model without loading key material or decrypting entries.
|
|
pub(crate) fn list_tree(
|
|
repository: &Repository,
|
|
directory: &DirectoryPath,
|
|
) -> Result<TreeModel, ReadError> {
|
|
let snapshot = repository.snapshot()?;
|
|
build_tree(&snapshot, directory, None)
|
|
}
|
|
|
|
fn build_regex(request: &GrepRequest) -> Result<Regex, ReadError> {
|
|
let pattern = if request.fixed_strings {
|
|
regex::escape(&request.pattern)
|
|
} else {
|
|
request.pattern.clone()
|
|
};
|
|
RegexBuilder::new(&pattern)
|
|
.case_insensitive(request.ignore_case)
|
|
.build()
|
|
.map_err(|_| ReadError::InvalidRegex)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MutableNode {
|
|
directory: bool,
|
|
children: BTreeMap<String, MutableNode>,
|
|
}
|
|
|
|
fn build_tree(
|
|
snapshot: &RepositorySnapshot,
|
|
root: &DirectoryPath,
|
|
included: Option<&[&str]>,
|
|
) -> Result<TreeModel, ReadError> {
|
|
if snapshot
|
|
.directories()
|
|
.all(|directory| directory.path() != root)
|
|
{
|
|
return Err(ReadError::Repository(RepositoryError::NotFound {
|
|
path: root.as_path().to_owned(),
|
|
}));
|
|
}
|
|
let mut mutable = MutableNode {
|
|
directory: true,
|
|
children: BTreeMap::new(),
|
|
};
|
|
for directory in snapshot.directories() {
|
|
let path = directory.path().as_path();
|
|
if path == root.as_path() || !path.starts_with(root.as_path()) || hidden_path(path) {
|
|
continue;
|
|
}
|
|
if included.is_some_and(|included| !is_included_path(path, included, true)) {
|
|
continue;
|
|
}
|
|
insert_path(
|
|
&mut mutable,
|
|
path.strip_prefix(root.as_path()).expect("prefix"),
|
|
true,
|
|
)?;
|
|
}
|
|
for entry in snapshot.entries() {
|
|
let path = entry.path().as_path();
|
|
if !path.starts_with(root.as_path()) || hidden_path(path) {
|
|
continue;
|
|
}
|
|
if included.is_some_and(|included| !is_included_path(path, included, false)) {
|
|
continue;
|
|
}
|
|
insert_path(
|
|
&mut mutable,
|
|
path.strip_prefix(root.as_path()).expect("prefix"),
|
|
false,
|
|
)?;
|
|
}
|
|
let title = if root.as_path().as_os_str().is_empty() {
|
|
"Password Store".to_owned()
|
|
} else {
|
|
path_text(root.as_path())?
|
|
};
|
|
Ok(TreeModel {
|
|
title,
|
|
root: root.clone(),
|
|
children: finalize_children(mutable, root.as_path())?,
|
|
})
|
|
}
|
|
|
|
fn insert_path(root: &mut MutableNode, path: &Path, directory: bool) -> Result<(), ReadError> {
|
|
let components = path
|
|
.components()
|
|
.map(|component| {
|
|
component
|
|
.as_os_str()
|
|
.to_str()
|
|
.map(str::to_owned)
|
|
.ok_or(ReadError::NonUtf8Path)
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let mut node = root;
|
|
for (index, component) in components.iter().enumerate() {
|
|
node = node.children.entry(component.clone()).or_default();
|
|
if index + 1 < components.len() || directory {
|
|
node.directory = true;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn finalize_children(node: MutableNode, parent: &Path) -> Result<Vec<TreeNode>, ReadError> {
|
|
node.children
|
|
.into_iter()
|
|
.map(|(name, child)| {
|
|
let path = parent.join(&name);
|
|
let kind = if child.directory {
|
|
TreeNodeKind::Directory
|
|
} else {
|
|
TreeNodeKind::Entry
|
|
};
|
|
let children = finalize_children(child, &path)?;
|
|
Ok(TreeNode {
|
|
name,
|
|
id: match kind {
|
|
TreeNodeKind::Directory => TreeNodeId::Directory(DirectoryPath::parse(&path)?),
|
|
TreeNodeKind::Entry => TreeNodeId::Entry(EntryPath::parse(&path)?),
|
|
},
|
|
indicators: TreeNodeIndicators::default(),
|
|
children,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn render_children(rendered: &mut String, children: &[TreeNode], prefix: &str) {
|
|
for (index, child) in children.iter().enumerate() {
|
|
let last = index + 1 == children.len();
|
|
rendered.push_str(prefix);
|
|
rendered.push_str(if last { "└── " } else { "├── " });
|
|
rendered.push_str(child.name());
|
|
rendered.push('\n');
|
|
let continuation = format!("{prefix}{}", if last { " " } else { "│ " });
|
|
render_children(rendered, child.children(), &continuation);
|
|
}
|
|
}
|
|
|
|
fn is_included_path(path: &Path, included: &[&str], directory: bool) -> bool {
|
|
let text = path.to_string_lossy();
|
|
included.iter().any(|candidate| {
|
|
let candidate = Path::new(candidate);
|
|
candidate == path
|
|
|| (directory && candidate.starts_with(path))
|
|
|| path.starts_with(candidate)
|
|
}) || included.iter().any(|candidate| *candidate == text)
|
|
}
|
|
|
|
pub(crate) fn hidden_path(path: &Path) -> bool {
|
|
path.components()
|
|
.any(|component| component.as_os_str() == EXTENSIONS_DIRECTORY)
|
|
}
|
|
|
|
fn display_name(path: &Path) -> Result<&str, ReadError> {
|
|
path.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.ok_or(ReadError::NonUtf8Path)
|
|
}
|
|
|
|
fn path_text(path: &Path) -> Result<String, ReadError> {
|
|
path.to_str()
|
|
.map(str::to_owned)
|
|
.ok_or(ReadError::NonUtf8Path)
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum ReadError {
|
|
Repository(RepositoryError),
|
|
Crypto(CryptoError),
|
|
MissingSearchTerms,
|
|
InvalidRegex,
|
|
NonUtf8Path,
|
|
LineNotFound { entry: EntryPath, line: usize },
|
|
}
|
|
|
|
impl fmt::Display for ReadError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Repository(error) => error.fmt(formatter),
|
|
Self::Crypto(error) => error.fmt(formatter),
|
|
Self::MissingSearchTerms => formatter.write_str("at least one search term is required"),
|
|
Self::InvalidRegex => formatter.write_str("decrypted grep pattern is invalid"),
|
|
Self::NonUtf8Path => formatter.write_str("password-store path is not valid UTF-8"),
|
|
Self::LineNotFound { entry, line } => {
|
|
write!(formatter, "entry {entry} has no nonempty line {line}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for ReadError {
|
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
match self {
|
|
Self::Repository(error) => Some(error),
|
|
Self::Crypto(error) => Some(error),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RepositoryError> for ReadError {
|
|
fn from(error: RepositoryError) -> Self {
|
|
Self::Repository(error)
|
|
}
|
|
}
|
|
|
|
impl From<CryptoError> for ReadError {
|
|
fn from(error: CryptoError) -> Self {
|
|
Self::Crypto(error)
|
|
}
|
|
}
|
|
|
|
impl ReadError {
|
|
pub fn exit_code(&self) -> u8 {
|
|
EXIT_FAILURE
|
|
}
|
|
}
|