Cache TOTP discovery and report progress (#74)

This commit is contained in:
2026-08-11 23:15:23 +02:00
parent 6edcb5fc86
commit 9759162eff
12 changed files with 1577 additions and 198 deletions

View File

@@ -1,7 +1,7 @@
//! Capability-scoped password-store repository discovery and atomic file access.
use std::{
collections::{BTreeMap, BTreeSet},
collections::BTreeMap,
error::Error,
ffi::{OsStr, OsString},
fmt, fs,
@@ -9,6 +9,9 @@ use std::{
path::{Component, Path, PathBuf},
};
#[cfg(test)]
use std::collections::BTreeSet;
use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile;
use zeroize::Zeroize;
@@ -262,7 +265,6 @@ pub struct RepositorySnapshot {
recipients: BTreeMap<DirectoryPath, RecipientPolicy>,
git_repositories: BTreeMap<DirectoryPath, GitRepository>,
auxiliary_files: BTreeMap<PathBuf, AuxiliaryFile>,
collisions: BTreeSet<PathBuf>,
}
impl RepositorySnapshot {
@@ -286,10 +288,6 @@ impl RepositorySnapshot {
self.auxiliary_files.values()
}
pub fn collisions(&self) -> impl ExactSizeIterator<Item = &PathBuf> {
self.collisions.iter()
}
/// Resolve an upstream-style display path. A trailing slash explicitly selects a directory.
pub fn resolve(&self, input: &str) -> Result<ResolvedObject<'_>, RepositoryError> {
let directory_only = input.ends_with('/') || (cfg!(windows) && input.ends_with('\\'));
@@ -310,10 +308,7 @@ impl RepositorySnapshot {
let entry = EntryPath::parse(trimmed)?;
let entry_record = self.entries.get(&entry);
match (entry_record, directory_record) {
(Some(_), Some(_)) => Err(RepositoryError::AmbiguousPath {
path: entry.0.clone(),
}),
(Some(entry), None) => Ok(ResolvedObject::Entry(entry)),
(Some(entry), _) => Ok(ResolvedObject::Entry(entry)),
(None, Some(directory)) => Ok(ResolvedObject::Directory(directory)),
(None, None) => Err(RepositoryError::NotFound {
path: entry.0.clone(),
@@ -390,14 +385,6 @@ impl Repository {
pub fn snapshot(&self) -> Result<RepositorySnapshot, RepositoryError> {
let mut snapshot = RepositorySnapshot::default();
scan_directory(&self.root, &DirectoryPath::root(), &mut snapshot)?;
for entry in snapshot.entries.keys() {
if snapshot
.directories
.contains_key(&DirectoryPath(entry.0.clone()))
{
snapshot.collisions.insert(entry.0.clone());
}
}
Ok(snapshot)
}
@@ -540,7 +527,7 @@ impl Repository {
{
let (parent, file_name, created) = self.create_entry_parent(path)?;
let encrypted_path = path.encrypted_relative_path();
if let Err(error) = validate_write_target(&parent, &file_name, &path.0, &encrypted_path) {
if let Err(error) = validate_write_target(&parent, &file_name, &encrypted_path) {
return self.rollback_created(created, error);
}
@@ -598,7 +585,6 @@ impl Repository {
.0
.file_name()
.expect("non-root directory has a file name");
reject_entry_directory_collision(&parent_handle, name, &parent.0)?;
let metadata = child_metadata(&parent_handle, name, &directory.0)?;
let Some(metadata) = metadata else {
current = directory.parent();
@@ -651,7 +637,6 @@ impl Repository {
.as_path()
.file_name()
.expect("non-root directory has a file name");
reject_entry_directory_collision(&parent_handle, name, parent.as_path())?;
let Some(metadata) = child_metadata(&parent_handle, name, directory.as_path())? else {
return Ok(false);
};
@@ -691,7 +676,6 @@ impl Repository {
file_name.push(".gpg");
return Ok((directory, file_name));
}
reject_entry_directory_collision(&directory, name, &relative)?;
relative.push(name);
let metadata = child_metadata(&directory, name, &relative)?;
let Some(metadata) = metadata else {
@@ -725,9 +709,6 @@ impl Repository {
file_name.push(".gpg");
return Ok((directory, file_name, created));
}
if let Err(error) = reject_entry_directory_collision(&directory, name, &relative) {
return self.rollback_created(created, error);
}
relative.push(name);
match child_metadata(&directory, name, &relative) {
Ok(Some(metadata)) => {
@@ -773,9 +754,6 @@ impl Repository {
let Component::Normal(name) = component else {
unreachable!("DirectoryPath is validated")
};
if let Err(error) = reject_entry_directory_collision(&directory, name, &relative) {
return self.rollback_created(created, error);
}
relative.push(name);
match child_metadata(&directory, name, &relative) {
Ok(Some(metadata)) => {
@@ -845,7 +823,6 @@ impl Repository {
let Component::Normal(name) = component else {
unreachable!("normalized directory path has only normal components")
};
reject_entry_directory_collision(&directory, name, &relative)?;
relative.push(name);
let metadata = child_metadata(&directory, name, &relative)?.ok_or_else(|| {
RepositoryError::NotFound {
@@ -899,9 +876,6 @@ pub enum RepositoryError {
Collision {
path: PathBuf,
},
AmbiguousPath {
path: PathBuf,
},
NotFound {
path: PathBuf,
},
@@ -962,11 +936,6 @@ impl fmt::Display for RepositoryError {
"password-store entry collides with a directory: {}",
path.display()
),
Self::AmbiguousPath { path } => write!(
formatter,
"password-store path is both an entry and directory; add a trailing slash for the directory: {}",
path.display()
),
Self::NotFound { path } => {
write!(
formatter,
@@ -1198,38 +1167,11 @@ fn require_regular_file(
}
}
fn reject_entry_directory_collision(
directory: &Dir,
name: &OsStr,
parent: &Path,
) -> Result<(), RepositoryError> {
let mut encrypted_name = name.to_os_string();
encrypted_name.push(".gpg");
let logical = parent.join(name);
if child_metadata(directory, &encrypted_name, &logical)?.is_some() {
return Err(RepositoryError::Collision { path: logical });
}
Ok(())
}
fn validate_write_target(
parent: &Dir,
file_name: &OsStr,
logical: &Path,
encrypted: &Path,
) -> Result<(), RepositoryError> {
if let Some(metadata) =
child_metadata(parent, logical.file_name().unwrap_or_default(), logical)?
{
if metadata.is_dir() {
return Err(RepositoryError::Collision {
path: logical.to_owned(),
});
}
return Err(RepositoryError::UnsupportedFileType {
path: logical.to_owned(),
});
}
if let Some(metadata) = child_metadata(parent, file_name, encrypted)? {
require_regular_file(metadata, encrypted)?;
}