2023-06-22 16:20:15 -04:00
// notes for migration:
// account names must be made lowercase
use std ::{ fs ::File , io ::Read , path ::Path } ;
use log ::debug ;
2023-06-23 08:40:23 -04:00
use serde ::de ::Error ;
2023-06-22 16:20:15 -04:00
use steamguard ::SteamGuardAccount ;
2023-06-23 08:40:23 -04:00
use thiserror ::Error ;
2023-06-22 16:20:15 -04:00
use super ::{
legacy ::{ SdaAccount , SdaManifest } ,
manifest ::ManifestV1 ,
EntryEncryptionParams , EntryLoader , Manifest ,
} ;
2023-06-23 08:40:23 -04:00
pub ( crate ) fn load_and_migrate (
2023-06-22 16:20:15 -04:00
manifest_path : & Path ,
passkey : Option < & String > ,
2023-06-23 08:40:23 -04:00
) -> Result < ( Manifest , Vec < SteamGuardAccount > ) , MigrationError > {
2023-06-22 16:20:15 -04:00
backup_file ( manifest_path ) ? ;
let parent = manifest_path . parent ( ) . unwrap ( ) ;
parent . read_dir ( ) ? . for_each ( | e | {
let entry = e . unwrap ( ) ;
if entry . file_type ( ) . unwrap ( ) . is_file ( ) {
let path = entry . path ( ) ;
if path . extension ( ) . unwrap ( ) = = " maFile " {
backup_file ( & path ) . unwrap ( ) ;
}
}
} ) ;
do_migrate ( manifest_path , passkey )
}
fn do_migrate (
manifest_path : & Path ,
passkey : Option < & String > ,
2023-06-23 08:40:23 -04:00
) -> Result < ( Manifest , Vec < SteamGuardAccount > ) , MigrationError > {
2023-06-22 16:20:15 -04:00
let mut file = File ::open ( manifest_path ) ? ;
let mut buffer = String ::new ( ) ;
file . read_to_string ( & mut buffer ) ? ;
2023-06-23 13:36:23 -04:00
let mut manifest : MigratingManifest =
deserialize_manifest ( buffer ) . map_err ( MigrationError ::ManifestDeserializeFailed ) ? ;
2023-06-23 08:40:23 -04:00
if manifest . is_encrypted ( ) & & passkey . is_none ( ) {
return Err ( MigrationError ::MissingPasskey ) ;
} else if ! manifest . is_encrypted ( ) & & passkey . is_some ( ) {
// no custom error because this is an edge case, mostly user error
return Err ( MigrationError ::UnexpectedError ( anyhow ::anyhow! ( " A passkey was provided but the manifest is not encrypted. Aborting migration because it would encrypt the maFiles, and you probably didn't mean to do that. " ) ) ) ;
}
2023-06-22 16:20:15 -04:00
let folder = manifest_path . parent ( ) . unwrap ( ) ;
let mut accounts = manifest . load_all_accounts ( folder , passkey ) ? ;
while ! manifest . is_latest ( ) {
manifest = manifest . upgrade ( ) ;
for account in accounts . iter_mut ( ) {
* account = account . clone ( ) . upgrade ( ) ;
}
}
// HACK: force account names onto manifest entries
let mut manifest : Manifest = manifest . into ( ) ;
let accounts : Vec < SteamGuardAccount > = accounts . into_iter ( ) . map ( | a | a . into ( ) ) . collect ( ) ;
for ( i , entry ) in manifest . entries . iter_mut ( ) . enumerate ( ) {
entry . account_name = accounts [ i ] . account_name . to_lowercase ( ) ;
}
Ok ( ( manifest , accounts ) )
}
fn backup_file ( path : & Path ) -> anyhow ::Result < ( ) > {
let backup_path = Path ::join (
path . parent ( ) . unwrap ( ) ,
format! ( " {} .bak " , path . file_name ( ) . unwrap ( ) . to_str ( ) . unwrap ( ) ) ,
) ;
std ::fs ::copy ( path , backup_path ) ? ;
Ok ( ( ) )
}
2023-06-23 08:40:23 -04:00
#[ derive(Debug, Error) ]
pub ( crate ) enum MigrationError {
#[ error( " Passkey is required to decrypt manifest " ) ]
MissingPasskey ,
#[ error( " Failed to deserialize manifest: {0} " ) ]
ManifestDeserializeFailed ( serde_json ::Error ) ,
#[ error( " IO error when upgrading manifest: {0} " ) ]
IoError ( #[ from ] std ::io ::Error ) ,
#[ error( " An unexpected error occurred during manifest migration: {0} " ) ]
UnexpectedError ( #[ from ] anyhow ::Error ) ,
}
2023-06-22 16:20:15 -04:00
#[ derive(Debug) ]
enum MigratingManifest {
2023-06-23 13:36:23 -04:00
Sda ( SdaManifest ) ,
2023-06-22 16:20:15 -04:00
ManifestV1 ( ManifestV1 ) ,
}
impl MigratingManifest {
pub fn upgrade ( self ) -> Self {
match self {
2023-06-23 13:36:23 -04:00
Self ::Sda ( sda ) = > Self ::ManifestV1 ( sda . into ( ) ) ,
2023-06-22 16:20:15 -04:00
Self ::ManifestV1 ( _ ) = > self ,
}
}
pub fn is_latest ( & self ) -> bool {
2023-06-23 13:36:23 -04:00
matches! ( self , Self ::ManifestV1 ( _ ) )
2023-06-22 16:20:15 -04:00
}
2023-06-23 08:40:23 -04:00
pub fn is_encrypted ( & self ) -> bool {
match self {
2023-06-23 13:36:23 -04:00
Self ::Sda ( manifest ) = > manifest . entries . iter ( ) . any ( | e | e . encryption . is_some ( ) ) ,
2023-06-23 08:40:23 -04:00
Self ::ManifestV1 ( manifest ) = > manifest . entries . iter ( ) . any ( | e | e . encryption . is_some ( ) ) ,
}
}
2023-06-22 16:20:15 -04:00
pub fn load_all_accounts (
& self ,
folder : & Path ,
passkey : Option < & String > ,
) -> anyhow ::Result < Vec < MigratingAccount > > {
debug! ( " loading all accounts for migration " ) ;
let accounts = match self {
2023-06-23 13:36:23 -04:00
Self ::Sda ( sda ) = > {
2023-06-22 16:20:15 -04:00
let ( accounts , errors ) = sda
. entries
. iter ( )
. map ( | e | {
let params : Option < EntryEncryptionParams > =
e . encryption . clone ( ) . map ( | e | e . into ( ) ) ;
e . load ( & Path ::join ( folder , & e . filename ) , passkey , params . as_ref ( ) )
} )
. partition ::< Vec < _ > , _ > ( Result ::is_ok ) ;
let accounts : Vec < _ > = accounts . into_iter ( ) . map ( Result ::unwrap ) . collect ( ) ;
let errors : Vec < _ > = errors . into_iter ( ) . map ( Result ::unwrap_err ) . collect ( ) ;
if ! errors . is_empty ( ) {
return Err ( anyhow ::anyhow! (
" Failed to load some accounts: {:?} " ,
errors
) ) ;
}
accounts . into_iter ( ) . map ( MigratingAccount ::Sda ) . collect ( )
}
Self ::ManifestV1 ( manifest ) = > {
let ( accounts , errors ) = manifest
. entries
. iter ( )
. map ( | e | {
e . load (
& Path ::join ( folder , & e . filename ) ,
passkey ,
e . encryption . as_ref ( ) ,
)
} )
. partition ::< Vec < _ > , _ > ( Result ::is_ok ) ;
let accounts : Vec < _ > = accounts . into_iter ( ) . map ( Result ::unwrap ) . collect ( ) ;
let errors : Vec < _ > = errors . into_iter ( ) . map ( Result ::unwrap_err ) . collect ( ) ;
if ! errors . is_empty ( ) {
return Err ( anyhow ::anyhow! (
" Failed to load some accounts: {:?} " ,
errors
) ) ;
}
accounts
. into_iter ( )
. map ( MigratingAccount ::ManifestV1 )
. collect ( )
}
} ;
Ok ( accounts )
}
}
impl From < MigratingManifest > for Manifest {
fn from ( migrating : MigratingManifest ) -> Self {
match migrating {
MigratingManifest ::ManifestV1 ( manifest ) = > manifest ,
_ = > panic! ( " Manifest is not at the latest version! " ) ,
}
}
}
2023-06-23 08:40:23 -04:00
fn deserialize_manifest ( text : String ) -> Result < MigratingManifest , serde_json ::Error > {
2023-06-22 16:20:15 -04:00
let json : serde_json ::Value = serde_json ::from_str ( & text ) ? ;
debug! ( " deserializing manifest: version {} " , json [ " version " ] ) ;
if json [ " version " ] = = 1 {
let manifest : ManifestV1 = serde_json ::from_str ( & text ) ? ;
Ok ( MigratingManifest ::ManifestV1 ( manifest ) )
2023-06-23 08:40:23 -04:00
} else if json [ " version " ] = = serde_json ::Value ::Null {
2023-06-22 16:20:15 -04:00
let manifest : SdaManifest = serde_json ::from_str ( & text ) ? ;
2023-06-23 13:36:23 -04:00
Ok ( MigratingManifest ::Sda ( manifest ) )
2023-06-23 08:40:23 -04:00
} else {
Err ( serde_json ::Error ::custom ( format! (
" Unknown manifest version: {} " ,
json [ " version " ]
) ) )
2023-06-22 16:20:15 -04:00
}
}
#[ derive(Debug, Clone) ]
enum MigratingAccount {
Sda ( SdaAccount ) ,
ManifestV1 ( SteamGuardAccount ) ,
}
impl MigratingAccount {
pub fn upgrade ( self ) -> Self {
match self {
Self ::Sda ( sda ) = > Self ::ManifestV1 ( sda . into ( ) ) ,
Self ::ManifestV1 ( _ ) = > self ,
}
}
pub fn is_latest ( & self ) -> bool {
2023-06-23 13:36:23 -04:00
matches! ( self , Self ::ManifestV1 ( _ ) )
2023-06-22 16:20:15 -04:00
}
}
impl From < MigratingAccount > for SteamGuardAccount {
fn from ( migrating : MigratingAccount ) -> Self {
match migrating {
MigratingAccount ::ManifestV1 ( account ) = > account ,
_ = > panic! ( " Account is not at the latest version! " ) ,
}
}
}
pub fn load_and_upgrade_sda_account ( path : & Path ) -> anyhow ::Result < SteamGuardAccount > {
let file = File ::open ( path ) ? ;
let account : SdaAccount = serde_json ::from_reader ( file ) ? ;
let mut account = MigratingAccount ::Sda ( account ) ;
while ! account . is_latest ( ) {
account = account . upgrade ( ) ;
}
Ok ( account . into ( ) )
}
#[ cfg(test) ]
mod tests {
use crate ::accountmanager ::CURRENT_MANIFEST_VERSION ;
use super ::* ;
#[ test ]
fn should_migrate_to_latest_version ( ) -> anyhow ::Result < ( ) > {
#[ derive(Debug) ]
struct Test {
manifest : & 'static str ,
passkey : Option < String > ,
}
let cases = vec! [
Test {
manifest : " src/fixtures/maFiles/compat/1-account/manifest.json " ,
passkey : None ,
} ,
Test {
manifest : " src/fixtures/maFiles/compat/1-account-encrypted/manifest.json " ,
passkey : Some ( " password " . into ( ) ) ,
} ,
Test {
manifest : " src/fixtures/maFiles/compat/2-account/manifest.json " ,
passkey : None ,
} ,
Test {
manifest : " src/fixtures/maFiles/compat/missing-account-name/manifest.json " ,
passkey : None ,
} ,
Test {
manifest : " src/fixtures/maFiles/compat/no-webcookie/manifest.json " ,
passkey : None ,
} ,
] ;
for case in cases {
eprintln! ( " testing: {:?} " , case ) ;
let ( manifest , accounts ) = do_migrate ( Path ::new ( case . manifest ) , case . passkey . as_ref ( ) ) ? ;
assert_eq! ( manifest . version , CURRENT_MANIFEST_VERSION ) ;
assert_eq! ( manifest . entries [ 0 ] . account_name , " example " ) ;
assert_eq! ( manifest . entries [ 0 ] . steam_id , 1234 ) ;
assert_eq! ( accounts [ 0 ] . account_name , " example " ) ;
assert_eq! ( accounts [ 0 ] . steam_id , 1234 ) ;
}
Ok ( ( ) )
}
}