@@ -347,10 +347,44 @@ enum Commands {
347347
348348 #[ command( name = "repair" , about = "Repair the keyring" ) ]
349349 Repair ,
350+
351+ #[ command(
352+ name = "migrate" ,
353+ about = "Migrate a legacy keyring to the current format" ,
354+ after_help = format!( "{H_STYLE}Examples:{H_STYLE:#}\n {} migrate ~/.local/share/keyrings/login.keyring\n {0} migrate --format kwallet ~/.local/share/kwalletd/kdewallet.kwl" , BINARY_NAME )
355+ ) ]
356+ Migrate {
357+ #[ arg( help = "Path to the legacy keyring file" ) ]
358+ file : PathBuf ,
359+ #[ arg( short, long, default_value_t, help = "Format of the legacy keyring" ) ]
360+ format : MigrateFormat ,
361+ } ,
362+ }
363+
364+ #[ derive( Clone , Default , clap:: ValueEnum , fmt:: Debug ) ]
365+ enum MigrateFormat {
366+ #[ default]
367+ V0 ,
368+ #[ cfg( feature = "kwallet_migration" ) ]
369+ Kwallet ,
370+ }
371+
372+ impl fmt:: Display for MigrateFormat {
373+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
374+ match self {
375+ Self :: V0 => write ! ( f, "v0" ) ,
376+ #[ cfg( feature = "kwallet_migration" ) ]
377+ Self :: Kwallet => write ! ( f, "kwallet" ) ,
378+ }
379+ }
350380}
351381
352382impl Commands {
353383 async fn execute ( self , args : Arguments ) -> Result < ( ) , Error > {
384+ if matches ! ( self , Commands :: Migrate { .. } ) {
385+ return self . migrate ( args. secret ) . await ;
386+ }
387+
354388 let service = Service :: new ( ) . await ?;
355389 if args. app_id . is_some ( ) && args. keyring . is_some ( ) {
356390 return Err ( Error :: new (
@@ -434,6 +468,7 @@ impl Commands {
434468 } ;
435469
436470 let output = match self {
471+ Commands :: Migrate { .. } => unreachable ! ( ) ,
437472 Commands :: Delete { attributes } => {
438473 match keyring {
439474 Keyring :: Collection ( collection) => {
@@ -645,6 +680,90 @@ impl Commands {
645680
646681 Ok ( ( ) )
647682 }
683+
684+ async fn migrate ( self , secret : Option < oo7:: Secret > ) -> Result < ( ) , Error > {
685+ let Self :: Migrate { file, format } = self else {
686+ unreachable ! ( ) ;
687+ } ;
688+ if !file. exists ( ) {
689+ return Err ( Error :: Owned ( format ! (
690+ "File '{}' does not exist." ,
691+ file. display( )
692+ ) ) ) ;
693+ }
694+
695+ let name = file
696+ . file_stem ( )
697+ . and_then ( |s| s. to_str ( ) )
698+ . ok_or_else ( || Error :: new ( "Cannot determine keyring name from file path" ) ) ?;
699+
700+ let parent = file
701+ . parent ( )
702+ . ok_or_else ( || Error :: new ( "Cannot determine parent directory of keyring file" ) ) ?;
703+ let target_path = parent. join ( "v1" ) . join ( format ! ( "{name}.keyring" ) ) ;
704+
705+ match format {
706+ MigrateFormat :: V0 => {
707+ let keyring =
708+ oo7:: file:: UnlockedKeyring :: load_from_v0 ( & file, & target_path, secret) . await ?;
709+ let n_items = keyring. n_items ( ) . await ;
710+ keyring. write ( ) . await ?;
711+
712+ println ! (
713+ "Migrated v0 keyring '{name}' ({n_items} items) to {}." ,
714+ target_path. display( )
715+ ) ;
716+ }
717+ #[ cfg( feature = "kwallet_migration" ) ]
718+ MigrateFormat :: Kwallet => {
719+ let secret = secret. ok_or_else ( || {
720+ Error :: new ( "KWallet migration requires a secret (use --secret)" )
721+ } ) ?;
722+
723+ let path = file. to_path_buf ( ) ;
724+ let password = secret. to_vec ( ) ;
725+ let wallet = tokio:: task:: spawn_blocking ( move || {
726+ kwallet_parser:: KWalletFile :: open ( & path, & password)
727+ } )
728+ . await
729+ . map_err ( |e| Error :: Owned ( format ! ( "Task join error: {e}" ) ) ) ?
730+ . map_err ( |e| Error :: Owned ( format ! ( "Failed to open KWallet file: {e}" ) ) ) ?;
731+
732+ let keyring =
733+ oo7:: file:: UnlockedKeyring :: open_at ( parent, name, Some ( secret) ) . await ?;
734+
735+ let mut n_items = 0usize ;
736+ for ( folder_name, folder) in wallet. wallet ( ) {
737+ for ( entry_key, entry) in folder {
738+ match kwallet_parser:: convert_entry ( folder_name, entry_key, entry) {
739+ Ok ( ss_entry) => {
740+ keyring
741+ . create_item (
742+ ss_entry. label ( ) ,
743+ ss_entry. attributes ( ) ,
744+ oo7:: Secret :: blob ( ss_entry. secret ( ) ) ,
745+ true ,
746+ )
747+ . await ?;
748+ n_items += 1 ;
749+ }
750+ Err ( e) => {
751+ eprintln ! ( "Skipping entry {folder_name}/{entry_key}: {e}" ) ;
752+ }
753+ }
754+ }
755+ }
756+ keyring. write ( ) . await ?;
757+
758+ println ! (
759+ "Migrated KWallet '{name}' ({n_items} items) to {}." ,
760+ target_path. display( )
761+ ) ;
762+ }
763+ }
764+
765+ Ok ( ( ) )
766+ }
648767}
649768
650769#[ derive( Parser ) ]
0 commit comments