810 lines
28 KiB
Rust
810 lines
28 KiB
Rust
//! Rollback-safe remove, move, and copy transactions.
|
|
|
|
use std::{error::Error, fmt};
|
|
|
|
use crate::{
|
|
command::{CopyRequest, MoveRequest, RemoveRequest},
|
|
crypto::{KeyStore, SecretProvider},
|
|
recipient::{RecipientPolicyManager, SigningPolicy},
|
|
repository::{
|
|
DirectoryPath, EncryptedEntry, EntryPath, Repository, RepositoryError, ResolvedObject,
|
|
},
|
|
write::OverwriteDecision,
|
|
};
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum MutationAction {
|
|
Remove,
|
|
Move,
|
|
Copy,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct TreeCommit {
|
|
action: MutationAction,
|
|
source: String,
|
|
destination: Option<String>,
|
|
changed_paths: Vec<std::path::PathBuf>,
|
|
message: String,
|
|
}
|
|
|
|
impl TreeCommit {
|
|
pub fn action(&self) -> MutationAction {
|
|
self.action
|
|
}
|
|
pub fn source(&self) -> &str {
|
|
&self.source
|
|
}
|
|
pub fn destination(&self) -> Option<&str> {
|
|
self.destination.as_deref()
|
|
}
|
|
pub fn changed_paths(&self) -> &[std::path::PathBuf] {
|
|
&self.changed_paths
|
|
}
|
|
pub fn message(&self) -> &str {
|
|
&self.message
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct TreeCommitError(String);
|
|
|
|
impl TreeCommitError {
|
|
pub fn new(message: impl Into<String>) -> Self {
|
|
Self(message.into())
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for TreeCommitError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter.write_str(&self.0)
|
|
}
|
|
}
|
|
|
|
impl Error for TreeCommitError {}
|
|
|
|
/// Atomically records a completed tree mutation.
|
|
///
|
|
/// An error must mean that no commit was created. `TreeMutator` restores the
|
|
/// filesystem transaction when an error is returned.
|
|
pub trait TreeCommitter {
|
|
fn commit(&mut self, change: &TreeCommit) -> Result<(), TreeCommitError>;
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct NoGitTreeCommitter;
|
|
|
|
impl TreeCommitter for NoGitTreeCommitter {
|
|
fn commit(&mut self, _change: &TreeCommit) -> Result<(), TreeCommitError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct MutationOutcome {
|
|
action: MutationAction,
|
|
entries: Vec<EntryPath>,
|
|
}
|
|
|
|
impl MutationOutcome {
|
|
pub fn action(&self) -> MutationAction {
|
|
self.action
|
|
}
|
|
pub fn entries(&self) -> &[EntryPath] {
|
|
&self.entries
|
|
}
|
|
}
|
|
|
|
pub struct TreeMutator<'a> {
|
|
repository: &'a Repository,
|
|
keys: &'a KeyStore,
|
|
}
|
|
|
|
impl<'a> TreeMutator<'a> {
|
|
pub fn new(repository: &'a Repository, keys: &'a KeyStore) -> Self {
|
|
Self { repository, keys }
|
|
}
|
|
|
|
pub fn remove(
|
|
&self,
|
|
request: &RemoveRequest,
|
|
decision: OverwriteDecision,
|
|
committer: &mut impl TreeCommitter,
|
|
) -> Result<MutationOutcome, MutationError> {
|
|
let snapshot = self.repository.snapshot()?;
|
|
let resolved = snapshot.resolve(&request.entry)?;
|
|
if !request.force && decision == OverwriteDecision::Decline {
|
|
return Err(MutationError::Cancelled);
|
|
}
|
|
let (entries, policies, directories, cleanup, display) = match resolved {
|
|
ResolvedObject::Entry(record) => (
|
|
vec![EntryState::source(
|
|
record.path().clone(),
|
|
self.repository.read_entry(record.path())?,
|
|
)],
|
|
Vec::new(),
|
|
Vec::new(),
|
|
record.path().parent_directory(),
|
|
record.path().to_string(),
|
|
),
|
|
ResolvedObject::Directory(record) => {
|
|
if record.path().as_path().as_os_str().is_empty() {
|
|
return Err(MutationError::RootMutation);
|
|
}
|
|
if !request.recursive {
|
|
return Err(MutationError::RecursiveRequired);
|
|
}
|
|
self.reject_unmanaged(record.path())?;
|
|
(
|
|
self.source_entries(record.path())?,
|
|
self.source_policies(record.path())?,
|
|
self.source_directories(record.path())?,
|
|
record.path().clone(),
|
|
format!("{}/", record.path()),
|
|
)
|
|
}
|
|
};
|
|
if let Err(operation) = self.remove_sources(&entries, &policies, &directories) {
|
|
self.restore_sources(&entries, &policies, &directories)?;
|
|
return Err(operation);
|
|
}
|
|
let change = TreeCommit {
|
|
action: MutationAction::Remove,
|
|
source: display.clone(),
|
|
destination: None,
|
|
changed_paths: entries
|
|
.iter()
|
|
.map(|entry| entry.source.encrypted_relative_path())
|
|
.chain(policies.iter().flat_map(|policy| {
|
|
[
|
|
policy.directory.as_path().join(".gpg-id"),
|
|
policy.directory.as_path().join(".gpg-id.sig"),
|
|
]
|
|
}))
|
|
.collect(),
|
|
message: format!("Remove {display} from store."),
|
|
};
|
|
if let Err(error) = committer.commit(&change) {
|
|
self.restore_sources(&entries, &policies, &directories)?;
|
|
return Err(MutationError::Commit(error));
|
|
}
|
|
self.repository.cleanup_empty_directories(&cleanup)?;
|
|
Ok(MutationOutcome {
|
|
action: MutationAction::Remove,
|
|
entries: entries.into_iter().map(|entry| entry.source).collect(),
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn copy(
|
|
&self,
|
|
request: &CopyRequest,
|
|
decision: OverwriteDecision,
|
|
signing: Option<&SigningPolicy>,
|
|
provider: &mut impl SecretProvider,
|
|
committer: &mut impl TreeCommitter,
|
|
) -> Result<MutationOutcome, MutationError> {
|
|
self.transfer(
|
|
&request.source,
|
|
&request.destination,
|
|
request.force,
|
|
decision,
|
|
false,
|
|
signing,
|
|
provider,
|
|
committer,
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn move_tree(
|
|
&self,
|
|
request: &MoveRequest,
|
|
decision: OverwriteDecision,
|
|
signing: Option<&SigningPolicy>,
|
|
provider: &mut impl SecretProvider,
|
|
committer: &mut impl TreeCommitter,
|
|
) -> Result<MutationOutcome, MutationError> {
|
|
self.transfer(
|
|
&request.source,
|
|
&request.destination,
|
|
request.force,
|
|
decision,
|
|
true,
|
|
signing,
|
|
provider,
|
|
committer,
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn transfer(
|
|
&self,
|
|
source: &str,
|
|
destination: &str,
|
|
force: bool,
|
|
decision: OverwriteDecision,
|
|
moving: bool,
|
|
signing: Option<&SigningPolicy>,
|
|
provider: &mut impl SecretProvider,
|
|
committer: &mut impl TreeCommitter,
|
|
) -> Result<MutationOutcome, MutationError> {
|
|
let snapshot = self.repository.snapshot()?;
|
|
let resolved = snapshot.resolve(source)?;
|
|
let requested_destination = DirectoryPath::parse(destination)?;
|
|
let destination_is_directory = snapshot
|
|
.directories()
|
|
.any(|record| record.path() == &requested_destination);
|
|
if has_trailing_separator(destination) && !destination_is_directory {
|
|
return Err(MutationError::DestinationDirectoryMissing {
|
|
directory: requested_destination,
|
|
});
|
|
}
|
|
let (
|
|
source_entries,
|
|
source_policies,
|
|
source_directories,
|
|
source_root,
|
|
destination_root,
|
|
destination_entry,
|
|
) = match resolved {
|
|
ResolvedObject::Entry(record) => (
|
|
vec![EntryState::source(
|
|
record.path().clone(),
|
|
self.repository.read_entry(record.path())?,
|
|
)],
|
|
Vec::new(),
|
|
Vec::new(),
|
|
record.path().parent_directory(),
|
|
if destination_is_directory {
|
|
requested_destination.clone()
|
|
} else {
|
|
EntryPath::parse(destination)?.parent_directory()
|
|
},
|
|
Some(if destination_is_directory {
|
|
let name = record
|
|
.path()
|
|
.as_path()
|
|
.file_name()
|
|
.expect("validated entry has a file name");
|
|
EntryPath::parse(requested_destination.as_path().join(name))?
|
|
} else {
|
|
EntryPath::parse(destination)?
|
|
}),
|
|
),
|
|
ResolvedObject::Directory(record) => {
|
|
if record.path().as_path().as_os_str().is_empty() {
|
|
return Err(MutationError::RootMutation);
|
|
}
|
|
let destination = if destination_is_directory {
|
|
let name = record
|
|
.path()
|
|
.as_path()
|
|
.file_name()
|
|
.expect("non-root directory has a file name");
|
|
DirectoryPath::parse(requested_destination.as_path().join(name))?
|
|
} else {
|
|
requested_destination
|
|
};
|
|
if destination == *record.path() {
|
|
return Err(MutationError::SameObject);
|
|
}
|
|
if destination.as_path().starts_with(record.path().as_path()) {
|
|
return Err(MutationError::DestinationInsideSource);
|
|
}
|
|
if snapshot
|
|
.entries()
|
|
.any(|entry| entry.path().as_path() == destination.as_path())
|
|
{
|
|
return Err(MutationError::DestinationTypeCollision {
|
|
path: destination.as_path().to_owned(),
|
|
});
|
|
}
|
|
self.reject_unmanaged(record.path())?;
|
|
(
|
|
self.source_entries(record.path())?,
|
|
self.source_policies(record.path())?,
|
|
self.source_directories(record.path())?,
|
|
record.path().clone(),
|
|
destination,
|
|
None,
|
|
)
|
|
}
|
|
};
|
|
if destination_entry
|
|
.as_ref()
|
|
.is_some_and(|destination| destination == &source_entries[0].source)
|
|
{
|
|
return Err(MutationError::SameObject);
|
|
}
|
|
let mut entries = Vec::new();
|
|
for source_entry in source_entries {
|
|
let destination_path = if let Some(destination) = &destination_entry {
|
|
destination.clone()
|
|
} else {
|
|
let relative = source_entry
|
|
.source
|
|
.as_path()
|
|
.strip_prefix(source_root.as_path())
|
|
.expect("source prefix");
|
|
EntryPath::parse(destination_root.as_path().join(relative))?
|
|
};
|
|
if snapshot
|
|
.directories()
|
|
.any(|directory| directory.path().as_path() == destination_path.as_path())
|
|
{
|
|
return Err(MutationError::DestinationTypeCollision {
|
|
path: destination_path.as_path().to_owned(),
|
|
});
|
|
}
|
|
let old_destination = match self.repository.read_entry(&destination_path) {
|
|
Ok(ciphertext) => Some(ciphertext),
|
|
Err(RepositoryError::NotFound { .. }) => None,
|
|
Err(error) => return Err(error.into()),
|
|
};
|
|
if old_destination.is_some() && !force && decision == OverwriteDecision::Decline {
|
|
return Err(MutationError::Cancelled);
|
|
}
|
|
let recipients = if destination_entry.is_none()
|
|
&& snapshot
|
|
.recipient_policy(&source_entry.source)
|
|
.is_some_and(|policy| {
|
|
policy
|
|
.directory()
|
|
.as_path()
|
|
.starts_with(source_root.as_path())
|
|
}) {
|
|
RecipientPolicyManager::new(self.repository, self.keys)
|
|
.resolve_for_entry(&source_entry.source, signing)?
|
|
} else {
|
|
RecipientPolicyManager::new(self.repository, self.keys)
|
|
.resolve_for_entry(&destination_path, signing)?
|
|
};
|
|
let replacement = if self
|
|
.keys
|
|
.is_encrypted_for(&source_entry.ciphertext, recipients.recipients())?
|
|
{
|
|
source_entry.ciphertext.clone()
|
|
} else {
|
|
let plaintext = self.keys.decrypt(&source_entry.ciphertext, provider)?;
|
|
self.keys.encrypt(plaintext, recipients.recipients())?
|
|
};
|
|
entries.push(TransferEntry {
|
|
source: source_entry,
|
|
destination: destination_path,
|
|
replacement,
|
|
old_destination,
|
|
});
|
|
}
|
|
let mut policies = Vec::new();
|
|
for policy in source_policies {
|
|
let relative = policy
|
|
.directory
|
|
.as_path()
|
|
.strip_prefix(source_root.as_path())
|
|
.expect("policy prefix");
|
|
let destination_directory =
|
|
DirectoryPath::parse(destination_root.as_path().join(relative))?;
|
|
let old_recipients = self
|
|
.repository
|
|
.read_policy_file(&destination_directory, false)?;
|
|
let old_signature = self
|
|
.repository
|
|
.read_policy_file(&destination_directory, true)?;
|
|
if old_recipients.is_some() || old_signature.is_some() {
|
|
return Err(MutationError::UnsafePolicyOverwrite {
|
|
directory: destination_directory,
|
|
});
|
|
}
|
|
policies.push(TransferPolicy {
|
|
source: policy,
|
|
destination: destination_directory,
|
|
});
|
|
}
|
|
|
|
let source_entries = entries
|
|
.iter()
|
|
.map(|entry| entry.source.clone())
|
|
.collect::<Vec<_>>();
|
|
let source_policies = policies
|
|
.iter()
|
|
.map(|policy| policy.source.clone())
|
|
.collect::<Vec<_>>();
|
|
let destination_directories = if let Some(entry) = &destination_entry {
|
|
vec![entry.parent_directory()]
|
|
} else {
|
|
source_directories
|
|
.iter()
|
|
.map(|directory| {
|
|
let relative = directory
|
|
.as_path()
|
|
.strip_prefix(source_root.as_path())
|
|
.expect("directory prefix");
|
|
DirectoryPath::parse(destination_root.as_path().join(relative))
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
};
|
|
let mut created_directories = Vec::new();
|
|
let applied = (|| -> Result<(), MutationError> {
|
|
for directory in &destination_directories {
|
|
created_directories.extend(self.repository.ensure_directory(directory)?);
|
|
}
|
|
for entry in &entries {
|
|
self.repository
|
|
.write_entry(&entry.destination, &entry.replacement)?;
|
|
}
|
|
for policy in &policies {
|
|
self.repository.replace_policy_file(
|
|
&policy.destination,
|
|
false,
|
|
Some(&policy.source.recipients),
|
|
)?;
|
|
if let Some(signature) = &policy.source.signature {
|
|
self.repository.replace_policy_file(
|
|
&policy.destination,
|
|
true,
|
|
Some(signature),
|
|
)?;
|
|
}
|
|
}
|
|
if moving {
|
|
self.remove_sources(&source_entries, &source_policies, &source_directories)?;
|
|
}
|
|
Ok(())
|
|
})();
|
|
if let Err(operation) = applied {
|
|
if moving {
|
|
self.restore_sources(&source_entries, &source_policies, &source_directories)?;
|
|
}
|
|
self.restore_destinations(&entries, &policies)?;
|
|
self.remove_created_directories(&created_directories)?;
|
|
return Err(operation);
|
|
}
|
|
let action = if moving {
|
|
MutationAction::Move
|
|
} else {
|
|
MutationAction::Copy
|
|
};
|
|
let verb = if moving { "Rename" } else { "Copy" };
|
|
let mut changed_paths = entries
|
|
.iter()
|
|
.map(|entry| entry.destination.encrypted_relative_path())
|
|
.chain(policies.iter().flat_map(|policy| {
|
|
[
|
|
policy.destination.as_path().join(".gpg-id"),
|
|
policy.destination.as_path().join(".gpg-id.sig"),
|
|
]
|
|
}))
|
|
.collect::<Vec<_>>();
|
|
if moving {
|
|
changed_paths.extend(
|
|
source_entries
|
|
.iter()
|
|
.map(|entry| entry.source.encrypted_relative_path()),
|
|
);
|
|
changed_paths.extend(source_policies.iter().flat_map(|policy| {
|
|
[
|
|
policy.directory.as_path().join(".gpg-id"),
|
|
policy.directory.as_path().join(".gpg-id.sig"),
|
|
]
|
|
}));
|
|
}
|
|
let change = TreeCommit {
|
|
action,
|
|
source: source.to_owned(),
|
|
destination: Some(destination.to_owned()),
|
|
changed_paths,
|
|
message: format!("{verb} {source} to {destination}."),
|
|
};
|
|
if let Err(error) = committer.commit(&change) {
|
|
if moving {
|
|
self.restore_sources(&source_entries, &source_policies, &source_directories)?;
|
|
}
|
|
self.restore_destinations(&entries, &policies)?;
|
|
self.remove_created_directories(&created_directories)?;
|
|
return Err(MutationError::Commit(error));
|
|
}
|
|
if moving {
|
|
self.repository.cleanup_empty_directories(&source_root)?;
|
|
}
|
|
Ok(MutationOutcome {
|
|
action,
|
|
entries: entries.into_iter().map(|entry| entry.destination).collect(),
|
|
})
|
|
}
|
|
|
|
fn source_entries(&self, root: &DirectoryPath) -> Result<Vec<EntryState>, MutationError> {
|
|
self.repository
|
|
.snapshot()?
|
|
.entries()
|
|
.filter(|entry| entry.path().as_path().starts_with(root.as_path()))
|
|
.map(|entry| {
|
|
Ok(EntryState::source(
|
|
entry.path().clone(),
|
|
self.repository.read_entry(entry.path())?,
|
|
))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn source_policies(&self, root: &DirectoryPath) -> Result<Vec<PolicyState>, MutationError> {
|
|
self.repository
|
|
.snapshot()?
|
|
.recipient_policies()
|
|
.filter(|policy| policy.directory().as_path().starts_with(root.as_path()))
|
|
.map(|policy| {
|
|
Ok(PolicyState {
|
|
directory: policy.directory().clone(),
|
|
recipients: self
|
|
.repository
|
|
.read_policy_file(policy.directory(), false)?
|
|
.expect("snapshot policy exists"),
|
|
signature: self.repository.read_policy_file(policy.directory(), true)?,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn source_directories(
|
|
&self,
|
|
root: &DirectoryPath,
|
|
) -> Result<Vec<DirectoryPath>, MutationError> {
|
|
Ok(self
|
|
.repository
|
|
.snapshot()?
|
|
.directories()
|
|
.filter(|directory| directory.path().as_path().starts_with(root.as_path()))
|
|
.map(|directory| directory.path().clone())
|
|
.collect())
|
|
}
|
|
|
|
fn reject_unmanaged(&self, root: &DirectoryPath) -> Result<(), MutationError> {
|
|
let snapshot = self.repository.snapshot()?;
|
|
if let Some(file) = snapshot
|
|
.auxiliary_files()
|
|
.find(|file| file.path().starts_with(root.as_path()))
|
|
{
|
|
return Err(MutationError::UnsupportedAuxiliary {
|
|
path: file.path().to_owned(),
|
|
});
|
|
}
|
|
if let Some(repository) = snapshot
|
|
.git_repositories()
|
|
.find(|repository| repository.work_tree().as_path().starts_with(root.as_path()))
|
|
{
|
|
return Err(MutationError::UnsupportedGitRepository {
|
|
path: repository.git_directory().to_owned(),
|
|
});
|
|
}
|
|
if let Some(path) = snapshot
|
|
.collisions()
|
|
.find(|path| path.starts_with(root.as_path()))
|
|
{
|
|
return Err(MutationError::SourceTypeCollision {
|
|
path: (*path).clone(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn remove_sources(
|
|
&self,
|
|
entries: &[EntryState],
|
|
policies: &[PolicyState],
|
|
directories: &[DirectoryPath],
|
|
) -> Result<(), MutationError> {
|
|
for entry in entries.iter().rev() {
|
|
self.repository.remove_entry(&entry.source)?;
|
|
}
|
|
for policy in policies.iter().rev() {
|
|
if policy.signature.is_some() {
|
|
self.repository
|
|
.replace_policy_file(&policy.directory, true, None)?;
|
|
}
|
|
self.repository
|
|
.replace_policy_file(&policy.directory, false, None)?;
|
|
}
|
|
for directory in directories.iter().rev() {
|
|
if !self.repository.remove_empty_directory(directory)? {
|
|
return Err(MutationError::TreeChanged {
|
|
path: directory.as_path().to_owned(),
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn restore_sources(
|
|
&self,
|
|
entries: &[EntryState],
|
|
policies: &[PolicyState],
|
|
directories: &[DirectoryPath],
|
|
) -> Result<(), MutationError> {
|
|
for directory in directories {
|
|
self.repository.ensure_directory(directory)?;
|
|
}
|
|
for policy in policies {
|
|
self.repository.replace_policy_file(
|
|
&policy.directory,
|
|
false,
|
|
Some(&policy.recipients),
|
|
)?;
|
|
if let Some(signature) = &policy.signature {
|
|
self.repository
|
|
.replace_policy_file(&policy.directory, true, Some(signature))?;
|
|
}
|
|
}
|
|
for entry in entries {
|
|
self.repository
|
|
.write_entry(&entry.source, &entry.ciphertext)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn remove_created_directories(
|
|
&self,
|
|
directories: &[DirectoryPath],
|
|
) -> Result<(), MutationError> {
|
|
for directory in directories.iter().rev() {
|
|
if !self.repository.remove_empty_directory(directory)? {
|
|
return Err(MutationError::TreeChanged {
|
|
path: directory.as_path().to_owned(),
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn restore_destinations(
|
|
&self,
|
|
entries: &[TransferEntry],
|
|
policies: &[TransferPolicy],
|
|
) -> Result<(), MutationError> {
|
|
for policy in policies.iter().rev() {
|
|
if policy.source.signature.is_some() {
|
|
self.repository
|
|
.replace_policy_file(&policy.destination, true, None)?;
|
|
}
|
|
self.repository
|
|
.replace_policy_file(&policy.destination, false, None)?;
|
|
}
|
|
for entry in entries.iter().rev() {
|
|
if let Some(original) = &entry.old_destination {
|
|
self.repository.write_entry(&entry.destination, original)?;
|
|
} else {
|
|
match self.repository.remove_entry(&entry.destination) {
|
|
Ok(_) | Err(RepositoryError::NotFound { .. }) => {}
|
|
Err(error) => return Err(error.into()),
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct EntryState {
|
|
source: EntryPath,
|
|
ciphertext: EncryptedEntry,
|
|
}
|
|
|
|
impl EntryState {
|
|
fn source(source: EntryPath, ciphertext: EncryptedEntry) -> Self {
|
|
Self { source, ciphertext }
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct PolicyState {
|
|
directory: DirectoryPath,
|
|
recipients: Vec<u8>,
|
|
signature: Option<Vec<u8>>,
|
|
}
|
|
|
|
struct TransferEntry {
|
|
source: EntryState,
|
|
destination: EntryPath,
|
|
replacement: EncryptedEntry,
|
|
old_destination: Option<EncryptedEntry>,
|
|
}
|
|
|
|
struct TransferPolicy {
|
|
source: PolicyState,
|
|
destination: DirectoryPath,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum MutationError {
|
|
Repository(RepositoryError),
|
|
Recipient(crate::recipient::RecipientPolicyError),
|
|
Crypto(crate::crypto::CryptoError),
|
|
Cancelled,
|
|
RecursiveRequired,
|
|
RootMutation,
|
|
SameObject,
|
|
DestinationInsideSource,
|
|
DestinationDirectoryMissing { directory: DirectoryPath },
|
|
SourceTypeCollision { path: std::path::PathBuf },
|
|
DestinationTypeCollision { path: std::path::PathBuf },
|
|
UnsafePolicyOverwrite { directory: DirectoryPath },
|
|
UnsupportedAuxiliary { path: std::path::PathBuf },
|
|
UnsupportedGitRepository { path: std::path::PathBuf },
|
|
TreeChanged { path: std::path::PathBuf },
|
|
Commit(TreeCommitError),
|
|
}
|
|
|
|
impl fmt::Display for MutationError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Repository(error) => error.fmt(formatter),
|
|
Self::Recipient(error) => error.fmt(formatter),
|
|
Self::Crypto(error) => error.fmt(formatter),
|
|
Self::Cancelled => formatter.write_str("tree mutation was cancelled"),
|
|
Self::RecursiveRequired => {
|
|
formatter.write_str("directory removal requires --recursive")
|
|
}
|
|
Self::RootMutation => formatter.write_str("the password-store root cannot be mutated"),
|
|
Self::SameObject => formatter.write_str("source and destination are the same object"),
|
|
Self::DestinationInsideSource => {
|
|
formatter.write_str("destination is inside source subtree")
|
|
}
|
|
Self::DestinationDirectoryMissing { directory } => write!(
|
|
formatter,
|
|
"trailing-slash destination directory does not exist: {directory}"
|
|
),
|
|
Self::SourceTypeCollision { path } => write!(
|
|
formatter,
|
|
"source subtree contains an entry/directory collision: {}",
|
|
path.display()
|
|
),
|
|
Self::DestinationTypeCollision { path } => write!(
|
|
formatter,
|
|
"directory destination collides with password entry: {}",
|
|
path.display()
|
|
),
|
|
Self::UnsafePolicyOverwrite { directory } => write!(
|
|
formatter,
|
|
"destination recipient policy already exists at {directory}"
|
|
),
|
|
Self::UnsupportedAuxiliary { path } => write!(
|
|
formatter,
|
|
"subtree contains unsupported auxiliary file {}",
|
|
path.display()
|
|
),
|
|
Self::UnsupportedGitRepository { path } => write!(
|
|
formatter,
|
|
"subtree contains a nested Git repository at {}",
|
|
path.display()
|
|
),
|
|
Self::TreeChanged { path } => write!(
|
|
formatter,
|
|
"password-store subtree changed during mutation: {}",
|
|
path.display()
|
|
),
|
|
Self::Commit(error) => write!(formatter, "cannot commit tree mutation: {error}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn has_trailing_separator(path: &str) -> bool {
|
|
path.ends_with('/') || (cfg!(windows) && path.ends_with('\\'))
|
|
}
|
|
|
|
impl Error for MutationError {}
|
|
|
|
impl From<RepositoryError> for MutationError {
|
|
fn from(error: RepositoryError) -> Self {
|
|
Self::Repository(error)
|
|
}
|
|
}
|
|
impl From<crate::recipient::RecipientPolicyError> for MutationError {
|
|
fn from(error: crate::recipient::RecipientPolicyError) -> Self {
|
|
Self::Recipient(error)
|
|
}
|
|
}
|
|
impl From<crate::crypto::CryptoError> for MutationError {
|
|
fn from(error: crate::crypto::CryptoError) -> Self {
|
|
Self::Crypto(error)
|
|
}
|
|
}
|