use super::{Backend, Error, RawStore};
use crate::{atomic, home};
use std::path::PathBuf;
pub(super) struct FileVault;
pub(super) const FILE: FileVault = FileVault;
impl FileVault {
fn dir(&self) -> PathBuf {
home::dir().join("vault")
}
fn path(&self, service: &str) -> Result<PathBuf, Error> {
if service.is_empty()
|| !service
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
{
return Err(Error::Write(format!(
"{service} is not a name this vault will store"
)));
}
Ok(self.dir().join(format!("{service}.json")))
}
}
impl RawStore for FileVault {
fn kind(&self) -> Backend {
Backend::File
}
fn contains(&self, service: &str) -> Result<bool, Error> {
super::exists(&self.path(service)?)
}
fn read(&self, service: &str) -> Result<Option<String>, Error> {
let path = self.path(service)?;
match std::fs::read_to_string(&path) {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Unreadable(format!(
"cannot read {}: {e}",
path.display()
))),
}
}
fn write(&self, service: &str, contents: &str) -> Result<(), Error> {
let path = self.path(service)?;
home::create_private(&self.dir()).map_err(|e| Error::Write(e.to_string()))?;
atomic::write(&path, contents.as_bytes(), atomic::Perms::Secret)
.map_err(|e| Error::Write(format!("cannot write {}: {e}", path.display())))?;
match self.read(service)? {
Some(back) if back == contents => Ok(()),
_ => Err(Error::NotDurable(format!(
"{} does not hold what was written",
path.display()
))),
}
}
fn delete(&self, service: &str) -> Result<(), Error> {
let path = self.path(service)?;
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::Write(e.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_names_pitboard_generates_are_accepted() {
for good in [
"pitboard-park-9aeb9c89-316c-4344-84c5-603d71dc5c9a-1789935600123",
"a.b_c-1",
] {
assert!(FILE.path(good).is_ok(), "{good}");
}
for bad in ["", "../escape", "has space", "a/b", "sl\\ash"] {
assert!(
FILE.path(bad).is_err(),
"{bad:?} must be refused, not escaped"
);
}
}
}