Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
568 changes: 443 additions & 125 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[package]
name = "keepass-diff"
edition = "2015"
description = "This CLI-tool reads two Keepass (.kdbx) files and prints their differences."
keywords = ["cli", "diff", "keepass"]
repository = "https://github.com/Narigo/keepass-diff"
Expand All @@ -16,8 +17,7 @@ exclude = [
]

[dependencies]
base64 = "0.21.5"
clap = { version = "4.4.7", features = ["cargo", "env", "derive", "wrap_help"] }
keepass = "0.6.6"
keepass = "0.12.0"
rpassword = "7.2.0"
termcolor = "1.3.0"
42 changes: 17 additions & 25 deletions src/diff/entry.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use base64::{engine::general_purpose, Engine as _};
use keepass::db::Value;
use std::collections::HashMap;

use crate::diff::field::{Field, ValueType};
use crate::diff::field::Field;
use crate::diff::{Diff, DiffResult, DiffResultFormat};

#[derive(Clone, Debug, PartialEq, Eq)]
Expand All @@ -13,7 +11,11 @@ pub struct Entry {
}

impl Entry {
pub fn from_keepass(e: &keepass::db::Entry, use_verbose: bool, mask_passwords: bool) -> Self {
pub fn from_keepass(
e: keepass::db::EntryRef<'_>,
use_verbose: bool,
mask_passwords: bool,
) -> Self {
// username, password, etc. are just fields
let fields = e
.fields
Expand All @@ -23,18 +25,8 @@ impl Entry {
k.to_owned(),
Field {
name: k.to_owned(),
value: match v {
Value::Bytes(b) => general_purpose::STANDARD_NO_PAD.encode(b),
Value::Unprotected(v) => v.to_owned(),
Value::Protected(p) => String::from_utf8(p.unsecure().to_owned())
.unwrap()
.to_owned(),
},
kind: match v {
Value::Bytes(_) => ValueType::Binary,
Value::Unprotected(_) => ValueType::Unprotected,
Value::Protected(_) => ValueType::Protected,
},
value: v.get().to_string(),
protected: v.is_protected(),
use_verbose,
mask_passwords,
},
Expand All @@ -52,10 +44,15 @@ impl Entry {

impl Diff for Entry {
fn diff<'a>(&'a self, other: &'a Self) -> DiffResult<'a, Self> {
let (has_differences, field_differences) =
crate::diff::diff_entry(&self.fields, &other.fields);
let field_differences =
crate::diff::diff_hashmap(&self.fields, &other.fields, |(ka, _), (kb, _)| ka.cmp(&kb));

if has_differences {
if field_differences.is_empty() {
DiffResult::Identical {
left: self,
right: other,
}
} else {
let mut inner_differences: Vec<Box<dyn DiffResultFormat>> = Vec::new();

for dr in field_differences {
Expand All @@ -67,11 +64,6 @@ impl Diff for Entry {
right: other,
inner_differences,
}
} else {
DiffResult::Identical {
left: self,
right: other,
}
}
}
}
Expand All @@ -84,7 +76,7 @@ impl std::fmt::Display for Entry {
.unwrap_or(&Field {
name: "Title".to_string(),
value: "".to_string(),
kind: ValueType::Unprotected,
protected: false,
use_verbose: self.use_verbose,
mask_passwords: self.mask_passwords,
})
Expand Down
35 changes: 9 additions & 26 deletions src/diff/field.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
use crate::diff::{Diff, DiffResult};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ValueType {
Binary,
Unprotected,
Protected,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Field {
pub name: String,
pub value: String,
pub kind: ValueType,
pub protected: bool,
pub use_verbose: bool,
pub mask_passwords: bool,
}
Expand All @@ -34,26 +27,16 @@ impl Diff for Field {

impl std::fmt::Display for Field {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let value = if self.mask_passwords && self.protected {
"***"
} else {
&self.value
};

if self.use_verbose {
write!(
f,
"Field '{}' = '{}'",
self.name,
match (self.mask_passwords, self.kind) {
(true, ValueType::Protected) => "***".to_owned(),
_ => self.value.to_owned(),
}
)
write!(f, "Field '{}' = '{}'", self.name, value)
} else {
write!(
f,
"{} = {}",
self.name,
match (self.mask_passwords, self.kind) {
(true, ValueType::Protected) => "***".to_owned(),
_ => self.value.to_owned(),
}
)
write!(f, "{} = {}", self.name, value)
}
}
}
75 changes: 42 additions & 33 deletions src/diff/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,35 @@ use std::collections::HashMap;
/// Corresponds to a sorted Vec of KdbxEntry objects that can be diffed
#[derive(Debug)]
pub struct Group {
name: String,
child_groups: HashMap<String, Vec<Group>>,
entries: HashMap<String, Vec<Entry>>,
use_verbose: bool,
pub name: String,
pub child_groups: HashMap<keepass::db::GroupId, Group>,
pub entries: HashMap<keepass::db::EntryId, Entry>,
pub use_verbose: bool,
}

impl Group {
/// Create an entries list from a keepass::Group
pub fn from_keepass(
group: &keepass::db::Group,
group: keepass::db::GroupRef<'_>,
use_verbose: bool,
mask_passwords: bool,
) -> Self {
let name = group.name.to_owned();

let mut child_groups: HashMap<String, Vec<Group>> = HashMap::new();
for node in group.children.iter() {
match node {
keepass::db::Node::Group(g) => child_groups
.entry(g.name.clone())
.or_insert(Vec::new())
.push(Group::from_keepass(g, use_verbose, mask_passwords)),
_ => {}
}
let mut child_groups: HashMap<keepass::db::GroupId, Group> = HashMap::new();
for child_group in group.groups() {
child_groups.insert(
child_group.id(),
Group::from_keepass(child_group, use_verbose, mask_passwords),
);
}

let mut entries: HashMap<String, Vec<Entry>> = HashMap::new();
for node in group.children.iter() {
match node {
keepass::db::Node::Entry(e) => entries
.entry(e.get("Title").unwrap_or_default().to_owned())
.or_insert(Vec::new())
.push(Entry::from_keepass(e, use_verbose, mask_passwords)),
_ => {}
}
let mut entries: HashMap<keepass::db::EntryId, Entry> = HashMap::new();
for entry in group.entries() {
entries.insert(
entry.id(),
Entry::from_keepass(entry, use_verbose, mask_passwords),
);
}

Group {
Expand All @@ -65,13 +59,33 @@ impl std::fmt::Display for Group {
/// Groups can be diffed.
impl Diff for Group {
fn diff<'a>(&'a self, other: &'a Group) -> DiffResult<'a, Self> {
let (has_differences_groups, acc_groups) =
crate::diff::diff_hashmap(&self.child_groups, &other.child_groups);
let acc_groups =
crate::diff::diff_hashmap(&self.child_groups, &other.child_groups, |(_, a), (_, b)| {
a.name.cmp(&b.name)
});

let acc_entries =
crate::diff::diff_hashmap(&self.entries, &other.entries, |(_, a), (_, b)| {
let a_title = a
.fields
.get("Title")
.map(|f| f.value.as_str())
.unwrap_or("");
let b_title = b
.fields
.get("Title")
.map(|f| f.value.as_str())
.unwrap_or("");

let (has_differences_entries, acc_entries) =
crate::diff::diff_hashmap(&self.entries, &other.entries);
a_title.cmp(b_title)
});

if has_differences_groups || has_differences_entries {
if acc_groups.is_empty() && acc_entries.is_empty() {
return DiffResult::Identical {
left: self,
right: other,
};
} else {
let mut inner_differences: Vec<Box<dyn DiffResultFormat>> = Vec::new();

for dr in acc_groups {
Expand All @@ -87,11 +101,6 @@ impl Diff for Group {
right: other,
inner_differences,
}
} else {
DiffResult::Identical {
left: self,
right: other,
}
}
}
}
Loading
Loading