use std::fs::File;
use keepass::{
db::{fields, GroupRef},
Database, DatabaseKey,
};
use super::{config::KeePassConfig, error::KeePassError};
pub fn pull_entries(cfg: &KeePassConfig) -> Result<Vec<(String, String)>, KeePassError> {
let mut file =
File::open(&cfg.path).map_err(|e| KeePassError::Open(format!("{}: {e}", cfg.path)))?;
let key = build_database_key(cfg)?;
let db = Database::open(&mut file, key).map_err(|e| {
let msg = e.to_string();
if msg.contains("Incorrect key") || msg.contains("IncorrectKey") {
KeePassError::Auth
} else {
KeePassError::Open(format!("{}: {e}", cfg.path))
}
})?;
let root = db.root();
let mut results: Vec<(String, String)> = Vec::new();
match &cfg.group {
None => {
collect_entries(&root, cfg.recursive, &mut results);
}
Some(target_group) => {
let matched = root
.groups()
.find(|g| g.name.eq_ignore_ascii_case(target_group));
match matched {
None => return Err(KeePassError::GroupNotFound(target_group.clone())),
Some(group) => {
collect_entries(&group, cfg.recursive, &mut results);
}
}
}
}
Ok(results)
}
fn build_database_key(cfg: &KeePassConfig) -> Result<DatabaseKey, KeePassError> {
let mut key = DatabaseKey::new();
if let Some(pw) = &cfg.password {
key = key.with_password(pw);
}
if let Some(kf_path) = &cfg.keyfile_path {
let mut kf = File::open(kf_path).map_err(|e| KeePassError::KeyFile {
path: kf_path.clone(),
source: e,
})?;
key = key
.with_keyfile(&mut kf)
.map_err(|e| KeePassError::KeyFile {
path: kf_path.clone(),
source: e,
})?;
}
Ok(key)
}
fn collect_entries(group: &GroupRef<'_>, recursive: bool, out: &mut Vec<(String, String)>) {
for entry in group.entries() {
let Some(title) = entry.get(fields::TITLE) else {
continue;
};
if title.is_empty() {
continue;
}
let title_key = normalise_key_segment(title);
if let Some(v) = entry.get(fields::USERNAME) {
if !v.is_empty() {
out.push((format!("{title_key}_USERNAME"), v.to_string()));
}
}
if let Some(v) = entry.get(fields::PASSWORD) {
if !v.is_empty() {
out.push((format!("{title_key}_PASSWORD"), v.to_string()));
}
}
if let Some(v) = entry.get(fields::URL) {
if !v.is_empty() {
out.push((format!("{title_key}_URL"), v.to_string()));
}
}
for (field_name, value) in &entry.fields {
match field_name.as_str() {
fields::TITLE
| fields::USERNAME
| fields::PASSWORD
| fields::URL
| fields::NOTES => continue,
_ => {}
}
let val = value.as_str();
if val.is_empty() {
continue;
}
let field_key = normalise_key_segment(field_name);
out.push((format!("{title_key}_{field_key}"), val.to_string()));
}
}
if recursive {
for child in group.groups() {
collect_entries(&child, true, out);
}
}
}
pub(crate) fn normalise_key_segment(s: &str) -> String {
s.replace([' ', '-'], "_").to_uppercase()
}
#[cfg(test)]
mod tests {
use super::normalise_key_segment;
#[test]
fn normalise_spaces_and_hyphens_to_underscores_and_uppercase() {
assert_eq!(normalise_key_segment("My Entry"), "MY_ENTRY");
assert_eq!(normalise_key_segment("db-password"), "DB_PASSWORD");
assert_eq!(normalise_key_segment("API_KEY"), "API_KEY");
assert_eq!(normalise_key_segment("my field name"), "MY_FIELD_NAME");
}
}