@@ -459,8 +459,20 @@ fn scan_steam_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<ru
459459 let mut added = 0 ;
460460 let mut skipped = 0 ;
461461
462+ let mut steam_stmt = conn. prepare (
463+ "SELECT steam_app_id, id, cover_path FROM games WHERE steam_app_id IS NOT NULL AND deleted_at IS NULL"
464+ ) . map_err ( |e| e. to_string ( ) ) ?;
465+ let steam_existing: std:: collections:: HashMap < String , ( String , Option < String > ) > =
466+ steam_stmt. query_map ( [ ] , |r| Ok ( ( r. get :: < _ , String > ( 0 ) ?, r. get :: < _ , String > ( 1 ) ?, r. get :: < _ , Option < String > > ( 2 ) ?) ) )
467+ . map_err ( |e| e. to_string ( ) ) ?
468+ . filter_map ( |r| r. ok ( ) )
469+ . map ( |( app_id, id, cover) | ( app_id, ( id, cover) ) )
470+ . collect ( ) ;
471+ drop ( steam_stmt) ;
472+
462473 for entry in entries {
463- if let Some ( ( existing_id, _) ) = queries:: get_steam_game_cover ( & conn, & entry. app_id ) {
474+ if let Some ( ( existing_id, _) ) = steam_existing. get ( & entry. app_id ) {
475+ let existing_id = existing_id. clone ( ) ;
464476 if let Some ( ref cover) = entry. cover {
465477 let _ = queries:: update_cover_path ( & conn, & existing_id, cover) ;
466478 }
@@ -587,8 +599,20 @@ fn scan_epic_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<rus
587599 let mut added = 0 ;
588600 let mut skipped = 0 ;
589601
602+ let mut epic_stmt = conn. prepare (
603+ "SELECT epic_app_name, id, cover_path FROM games WHERE epic_app_name IS NOT NULL AND deleted_at IS NULL"
604+ ) . map_err ( |e| e. to_string ( ) ) ?;
605+ let epic_existing: std:: collections:: HashMap < String , ( String , Option < String > ) > =
606+ epic_stmt. query_map ( [ ] , |r| Ok ( ( r. get :: < _ , String > ( 0 ) ?, r. get :: < _ , String > ( 1 ) ?, r. get :: < _ , Option < String > > ( 2 ) ?) ) )
607+ . map_err ( |e| e. to_string ( ) ) ?
608+ . filter_map ( |r| r. ok ( ) )
609+ . map ( |( app_name, id, cover) | ( app_name, ( id, cover) ) )
610+ . collect ( ) ;
611+ drop ( epic_stmt) ;
612+
590613 for entry in entries {
591- if let Some ( ( existing_id, existing_cover) ) = queries:: get_epic_game_cover ( & conn, & entry. app_name ) {
614+ if let Some ( ( existing_id, existing_cover) ) = epic_existing. get ( & entry. app_name ) {
615+ let existing_id = existing_id. clone ( ) ;
592616 if existing_cover. is_none ( ) {
593617 if let Some ( ref cover) = entry. cover {
594618 let _ = queries:: update_cover_path ( & conn, & existing_id, cover) ;
@@ -734,14 +758,18 @@ fn scan_gog_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<rusq
734758 let mut added = 0 ;
735759 let mut skipped = 0 ;
736760
737- for entry in entries {
738- let exists: bool = conn. query_row (
739- "SELECT COUNT(*) FROM games WHERE install_dir = ?1 AND platform = 'gog'" ,
740- rusqlite:: params![ entry. install_dir] ,
741- |r| r. get :: < _ , i64 > ( 0 ) ,
742- ) . unwrap_or ( 0 ) > 0 ;
761+ let mut gog_stmt = conn. prepare (
762+ "SELECT install_dir FROM games WHERE platform = 'gog' AND install_dir IS NOT NULL AND deleted_at IS NULL"
763+ ) . map_err ( |e| e. to_string ( ) ) ?;
764+ let gog_existing: std:: collections:: HashSet < String > =
765+ gog_stmt. query_map ( [ ] , |r| r. get :: < _ , String > ( 0 ) )
766+ . map_err ( |e| e. to_string ( ) ) ?
767+ . filter_map ( |r| r. ok ( ) )
768+ . collect ( ) ;
769+ drop ( gog_stmt) ;
743770
744- if exists { skipped += 1 ; continue ; }
771+ for entry in entries {
772+ if gog_existing. contains ( & entry. install_dir ) { skipped += 1 ; continue ; }
745773
746774 let game = Game {
747775 id : uuid:: Uuid :: new_v4 ( ) . to_string ( ) ,
@@ -1052,8 +1080,31 @@ fn scan_folder_for_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mut
10521080#[ tauri:: command]
10531081pub fn set_game_cover ( state : State < DbState > , game_id : String , image_path : String ) -> Result < String , String > {
10541082 let src = std:: path:: Path :: new ( & image_path) ;
1055- if !src. exists ( ) {
1056- return Err ( "Image file not found" . to_string ( ) ) ;
1083+
1084+ let meta = std:: fs:: symlink_metadata ( src) . map_err ( |_| "Image file not found" . to_string ( ) ) ?;
1085+ if meta. file_type ( ) . is_symlink ( ) {
1086+ return Err ( "Symlinks are not allowed as cover images" . to_string ( ) ) ;
1087+ }
1088+
1089+ let ext_str = src. extension ( )
1090+ . and_then ( |e| e. to_str ( ) )
1091+ . map ( |s| s. to_lowercase ( ) )
1092+ . unwrap_or_default ( ) ;
1093+ const ALLOWED_EXTS : & [ & str ] = & [ "jpg" , "jpeg" , "png" , "webp" ] ;
1094+ if !ALLOWED_EXTS . contains ( & ext_str. as_str ( ) ) {
1095+ return Err ( format ! ( "Unsupported image format '{}'. Use jpg, png, or webp." , ext_str) ) ;
1096+ }
1097+
1098+ let mut img_file = std:: fs:: File :: open ( src) . map_err ( |e| e. to_string ( ) ) ?;
1099+ let mut magic = [ 0u8 ; 12 ] ;
1100+ let n = std:: io:: Read :: read ( & mut img_file, & mut magic) . unwrap_or ( 0 ) ;
1101+ let magic = & magic[ ..n] ;
1102+ let valid_magic =
1103+ ( magic. len ( ) >= 3 && magic[ 0 ] == 0xFF && magic[ 1 ] == 0xD8 && magic[ 2 ] == 0xFF ) ||
1104+ ( magic. len ( ) >= 4 && & magic[ 0 ..4 ] == b"\x89 PNG" ) ||
1105+ ( magic. len ( ) >= 12 && & magic[ 0 ..4 ] == b"RIFF" && & magic[ 8 ..12 ] == b"WEBP" ) ;
1106+ if !valid_magic {
1107+ return Err ( "File does not appear to be a valid image" . to_string ( ) ) ;
10571108 }
10581109
10591110 let data_dir = dirs:: data_local_dir ( )
@@ -1063,8 +1114,7 @@ pub fn set_game_cover(state: State<DbState>, game_id: String, image_path: String
10631114
10641115 std:: fs:: create_dir_all ( & data_dir) . map_err ( |e| e. to_string ( ) ) ?;
10651116
1066- let ext = src. extension ( ) . and_then ( |e| e. to_str ( ) ) . unwrap_or ( "png" ) ;
1067- let dest_name = format ! ( "{}.{}" , game_id, ext) ;
1117+ let dest_name = format ! ( "{}.{}" , game_id, ext_str) ;
10681118 let dest = data_dir. join ( & dest_name) ;
10691119
10701120 std:: fs:: copy ( src, & dest) . map_err ( |e| e. to_string ( ) ) ?;
0 commit comments