33use anyhow:: { anyhow, bail, Result } ;
44use clap:: Parser ;
55use clap_verbosity_flag:: Verbosity ;
6+ use directories:: { BaseDirs , ProjectDirs , UserDirs } ;
67use is_terminal:: IsTerminal ;
7- use keepass:: { Database , Entry , NodeRef } ;
8+ use keepass:: { Database , Entry , Icon , NodeRef } ;
89use lexpr:: { print, sexp, Value } ;
910use libreauth:: oath:: TOTPBuilder ;
11+ use once_cell:: sync:: OnceCell ;
1012use rustyline:: error:: ReadlineError ;
1113use rustyline:: Editor ;
1214use std:: char;
1315use std:: collections:: BTreeMap ;
16+ use std:: fs;
1417use std:: fs:: File ;
1518use std:: io:: Read ;
1619use std:: io:: { self , Write } ;
17- use std:: path:: Path ;
20+ use std:: path:: { Path , PathBuf } ;
1821use std:: time:: SystemTime ;
1922use tracing:: { debug, info} ;
2023use tracing_core:: Level ;
2124use tracing_subscriber:: { filter, prelude:: * } ;
2225use tracing_subscriber:: { fmt, prelude:: * , EnvFilter } ;
26+
2327use url:: Url ;
2428shadow_rs:: shadow!( build) ;
2529
2630const COOKIE_PRE : char = 254 as char ;
2731const COOKIE_POST : char = 255 as char ;
2832
33+ /// project directories
34+ static PROJECT_DIRS : OnceCell < ProjectDirs > = OnceCell :: new ( ) ;
35+
36+ fn get_project_dirs ( ) -> & ' static ProjectDirs {
37+ & PROJECT_DIRS . get ( ) . unwrap ( )
38+ }
39+
40+ fn get_data_dir ( ) -> & ' static Path {
41+ & PROJECT_DIRS . get ( ) . unwrap ( ) . data_dir ( )
42+ }
43+
44+ fn get_custom_icon_dir ( ) -> PathBuf {
45+ let data_dir = get_data_dir ( ) ;
46+ let custom_icon_path = data_dir. join ( "custom_icons" ) ;
47+ custom_icon_path
48+ }
49+
50+ fn get_custom_icon_path ( uuid : & String ) -> PathBuf {
51+ let custom_icon_dir = get_custom_icon_dir ( ) ;
52+ let bytes = base64:: decode ( uuid) . unwrap ( ) ;
53+ let new_id = base58:: ToBase58 :: to_base58 ( bytes. as_slice ( ) ) ;
54+ custom_icon_dir. join ( new_id)
55+ }
56+
57+ fn get_builtin_icon_dir ( ) -> PathBuf {
58+ let data_dir = get_data_dir ( ) ;
59+ let icon_path = data_dir. join ( "icons" ) ;
60+ icon_path
61+ }
62+
63+ fn get_builtin_icon_path ( id : u8 ) -> PathBuf {
64+ let builtin_icon_dir = get_builtin_icon_dir ( ) ;
65+ let icon_file = format ! ( "{:02}.{}" , id, "svg" ) ;
66+ builtin_icon_dir. join ( icon_file)
67+ }
68+
2969#[ derive( Parser , Debug , Clone ) ]
3070#[ clap( author, version, about) ]
3171pub struct Args {
@@ -51,6 +91,10 @@ pub struct Args {
5191 #[ clap( long = "database" , short = 'd' ) ]
5292 database : Option < String > ,
5393
94+ /// icons
95+ #[ clap( long = "icon" , short = 'i' , default_value = "false" , global = true ) ]
96+ icon : bool ,
97+
5498 #[ clap( flatten) ]
5599 verbose : clap_verbosity_flag:: Verbosity ,
56100}
@@ -140,6 +184,7 @@ pub struct ParsedEntry {
140184 pub id : u64 ,
141185 pub fields : BTreeMap < String , Field > ,
142186 pub has_otp : bool ,
187+ pub icon : Icon ,
143188}
144189
145190impl ParsedEntry {
@@ -154,6 +199,20 @@ impl ParsedEntry {
154199 values. push ( Value :: string ( self . get_otp_code ( ) ?) ) ;
155200 } else if field == "has-otp" {
156201 values. push ( Value :: Bool ( self . has_otp ) ) ;
202+ } else if field == "icon" {
203+ let value = match & self . icon {
204+ Icon :: IconID ( id) => {
205+ Value :: string ( get_builtin_icon_path ( * id) . display ( ) . to_string ( ) )
206+ }
207+ Icon :: CustomIcon ( uuid) => {
208+ Value :: string ( get_custom_icon_path ( uuid) . display ( ) . to_string ( ) )
209+ }
210+ Icon :: None => {
211+ // NOTE: 0 as default icon for keepassxc
212+ Value :: string ( get_builtin_icon_path ( 0 ) . display ( ) . to_string ( ) )
213+ }
214+ } ;
215+ values. push ( value) ;
157216 } else {
158217 let val = self . fields . get ( field) ;
159218 if let Some ( v) = val {
@@ -290,9 +349,11 @@ impl TryFrom<&Entry> for ParsedEntry {
290349 fields. insert ( f. index ( ) , f) ;
291350 }
292351 }
352+ let mut icon = e. icon . clone ( ) ;
293353 Ok ( Self {
294354 fields,
295355 has_otp,
356+ icon,
296357 ..Default :: default ( )
297358 } )
298359 }
@@ -384,6 +445,25 @@ impl<'a, 'b> KPClient<'a> {
384445 }
385446 }
386447
448+ pub fn load_icons ( & ' a self ) -> Result < ( ) > {
449+ let custom_icon_path = get_custom_icon_dir ( ) ;
450+ debug ! ( "cicon {custom_icon_path:#?}" ) ;
451+ fs:: create_dir_all ( & custom_icon_path) ?;
452+ for ( uuid, icon_data) in self . db_manager . db . meta . custom_icons . iter ( ) {
453+ // uuid is base64, which is not good!
454+ // transform it to base58 instead; can also be base62
455+ let bytes = base64:: decode ( uuid) . unwrap ( ) ;
456+ let new_id = base58:: ToBase58 :: to_base58 ( bytes. as_slice ( ) ) ;
457+ debug ! ( "old {uuid} new {new_id}" ) ;
458+ let image_path = custom_icon_path. join ( & new_id) ;
459+ if !image_path. try_exists ( ) ? {
460+ let icon_bytes = base64:: decode ( icon_data) . unwrap ( ) ;
461+ fs:: write ( image_path, icon_bytes) ;
462+ }
463+ }
464+ Ok ( ( ) )
465+ }
466+
387467 pub fn do_list (
388468 & ' a self ,
389469 fields : & Option < Vec < String > > ,
@@ -445,7 +525,7 @@ pub struct DatabaseManager<'a> {
445525impl < ' a > DatabaseManager < ' a > {
446526 pub fn new ( path : & ' a Path , password : String ) -> Result < Self > {
447527 let db = Database :: open ( & mut File :: open ( path) ?, Some ( password. as_str ( ) ) , None ) ?;
448- debug ! ( "{:#?}" , & db) ;
528+ // debug!("{:#?}", &db);
449529 // std::process::exit(0);
450530 Ok ( Self { path, password, db } )
451531 }
@@ -462,6 +542,10 @@ impl<'a> DatabaseManager<'a> {
462542}
463543
464544fn main ( ) -> Result < ( ) > {
545+ if let Some ( proj_dirs) = ProjectDirs :: from ( "" , "" , "keepass-cli" ) {
546+ PROJECT_DIRS . set ( proj_dirs) . unwrap ( ) ;
547+ }
548+
465549 let args = Args :: parse ( ) ;
466550
467551 let filter = filter:: Targets :: new ( )
@@ -499,6 +583,11 @@ fn main() -> Result<()> {
499583 db_manager. reload ( ) ;
500584 let mut kp_client = KPClient :: new ( & db_manager) ?;
501585
586+ if args. icon {
587+ kp_client. load_icons ( ) ;
588+ // return Ok(());
589+ }
590+
502591 match & args. action {
503592 Action :: List { fields } => {
504593 kp_client. reload ( ) ;
0 commit comments