Implement read-only vault domains (#6)
This commit is contained in:
691
crates/storage/src/read.rs
Normal file
691
crates/storage/src/read.rs
Normal file
@@ -0,0 +1,691 @@
|
||||
//! 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, PartialEq)]
|
||||
pub enum TreeNodeKind {
|
||||
Directory,
|
||||
Entry,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TreeNode {
|
||||
name: String,
|
||||
path: String,
|
||||
kind: TreeNodeKind,
|
||||
children: Vec<TreeNode>,
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> TreeNodeKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
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 {
|
||||
path: String,
|
||||
kind: TreeNodeKind,
|
||||
}
|
||||
|
||||
impl NameMatch {
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> TreeNodeKind {
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let snapshot = self.repository.snapshot()?;
|
||||
build_tree(&snapshot, directory, None)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
path: path_text(directory.path().as_path())?,
|
||||
kind: TreeNodeKind::Directory,
|
||||
});
|
||||
}
|
||||
}
|
||||
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 {
|
||||
path: path_text(entry.path().as_path())?,
|
||||
kind: TreeNodeKind::Entry,
|
||||
});
|
||||
}
|
||||
}
|
||||
matches.sort_by(|left, right| left.path.cmp(&right.path));
|
||||
let included = matches
|
||||
.iter()
|
||||
.map(|matched| matched.path.as_str())
|
||||
.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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
path: path_text(&path)?,
|
||||
kind,
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user