use crate::error::{Result, SmartPassError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmartPassword {
pub public_key: String,
pub description: String,
pub length: usize,
}
impl SmartPassword {
pub fn new(public_key: String, description: String, length: usize) -> Result<Self> {
if public_key.is_empty() {
return Err(SmartPassError::EmptyPublicKey);
}
if description.is_empty() {
return Err(SmartPassError::EmptyDescription);
}
if !(12..=100).contains(&length) {
return Err(SmartPassError::InvalidPasswordLength {
min: 12,
max: 100,
current: length,
});
}
Ok(Self {
public_key,
description,
length,
})
}
pub fn update(&mut self, description: Option<&str>, length: Option<usize>) -> Result<()> {
if let Some(desc) = description {
self.description = desc.to_string();
}
if let Some(len) = length {
if !(12..=100).contains(&len) {
return Err(SmartPassError::InvalidPasswordLength {
min: 12,
max: 100,
current: len,
});
}
self.length = len;
}
Ok(())
}
}
pub struct SmartPasswordManager {
passwords: HashMap<String, SmartPassword>,
filename: PathBuf,
}
impl SmartPasswordManager {
pub fn new() -> Result<Self> {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".to_string());
let config_dir = Path::new(&home)
.join(".config")
.join("smart_password_manager");
let filename = config_dir.join("passwords.json");
let mut manager = Self {
passwords: HashMap::new(),
filename,
};
manager.load_data()?;
Ok(manager)
}
pub fn with_filename<P: AsRef<Path>>(filename: P) -> Result<Self> {
let mut manager = Self {
passwords: HashMap::new(),
filename: filename.as_ref().to_path_buf(),
};
manager.load_data()?;
Ok(manager)
}
pub fn passwords(&self) -> &HashMap<String, SmartPassword> {
&self.passwords
}
pub fn len(&self) -> usize {
self.passwords.len()
}
pub fn is_empty(&self) -> bool {
self.passwords.is_empty()
}
pub fn file_path(&self) -> &Path {
&self.filename
}
pub fn add(&mut self, password: SmartPassword) -> Result<()> {
self.passwords.insert(password.public_key.clone(), password);
self.save_data()?;
Ok(())
}
pub fn get(&self, public_key: &str) -> Option<&SmartPassword> {
self.passwords.get(public_key)
}
pub fn get_mut(&mut self, public_key: &str) -> Option<&mut SmartPassword> {
self.passwords.get_mut(public_key)
}
pub fn update(
&mut self,
public_key: &str,
description: Option<&str>,
length: Option<usize>,
) -> Result<bool> {
if let Some(password) = self.passwords.get_mut(public_key) {
password.update(description, length)?;
self.save_data()?;
Ok(true)
} else {
Ok(false)
}
}
pub fn delete(&mut self, public_key: &str) -> Result<bool> {
if self.passwords.remove(public_key).is_some() {
self.save_data()?;
Ok(true)
} else {
Ok(false)
}
}
pub fn clear(&mut self) -> Result<()> {
self.passwords.clear();
self.save_data()?;
Ok(())
}
fn load_data(&mut self) -> Result<()> {
if !self.filename.exists() {
return Ok(());
}
let content = fs::read_to_string(&self.filename)?;
if content.trim().is_empty() {
return Ok(());
}
let data: HashMap<String, SmartPassword> = serde_json::from_str(&content)?;
self.passwords = data;
Ok(())
}
fn save_data(&self) -> Result<()> {
if let Some(parent) = self.filename.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(&self.passwords)?;
fs::write(&self.filename, json)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_smart_password_new() {
let sp = SmartPassword::new("key".to_string(), "Service".to_string(), 16).unwrap();
assert_eq!(sp.public_key, "key");
assert_eq!(sp.description, "Service");
assert_eq!(sp.length, 16);
assert!(SmartPassword::new("".to_string(), "desc".to_string(), 12).is_err());
assert!(SmartPassword::new("key".to_string(), "".to_string(), 12).is_err());
assert!(SmartPassword::new("key".to_string(), "desc".to_string(), 8).is_err());
}
#[test]
fn test_manager() {
let dir = tempdir().unwrap();
let file = dir.path().join("test.json");
let mut manager = SmartPasswordManager::with_filename(&file).unwrap();
assert_eq!(manager.len(), 0);
let sp = SmartPassword::new("key_1".to_string(), "Service 1".to_string(), 16).unwrap();
manager.add(sp).unwrap();
assert_eq!(manager.len(), 1);
let retrieved = manager.get("key_1").unwrap();
assert_eq!(retrieved.description, "Service 1");
manager.update("key_1", Some("Updated"), Some(20)).unwrap();
let updated = manager.get("key_1").unwrap();
assert_eq!(updated.description, "Updated");
assert_eq!(updated.length, 20);
assert!(manager.delete("key_1").unwrap());
assert_eq!(manager.len(), 0);
}
}