@@ -45,7 +45,10 @@ pub(crate) enum BackupState {
4545
4646pub ( crate ) fn ensure_hosts_backup ( paths : & AppPaths , source_path : & Path ) -> Result < ( ) > {
4747 match backup_state ( paths) {
48- BackupState :: Ready => return Ok ( ( ) ) ,
48+ BackupState :: Ready => {
49+ validate_hosts_backup ( paths, source_path) ?;
50+ return Ok ( ( ) ) ;
51+ }
4952 BackupState :: Inconsistent => {
5053 bail ! (
5154 "hosts backup state is inconsistent; expected both {} and {}" ,
@@ -118,23 +121,7 @@ pub(crate) fn restore_hosts_from_backup(
118121 source_path : & Path ,
119122 current_content : & str ,
120123) -> Result < ( ) > {
121- match backup_state ( paths) {
122- BackupState :: Ready => { }
123- BackupState :: Missing => {
124- bail ! (
125- "hosts backup is unavailable; expected both {} and {}" ,
126- paths. hosts_backup_path. display( ) ,
127- paths. hosts_backup_meta_path. display( )
128- ) ;
129- }
130- BackupState :: Inconsistent => {
131- bail ! (
132- "hosts backup state is inconsistent; expected both {} and {}" ,
133- paths. hosts_backup_path. display( ) ,
134- paths. hosts_backup_meta_path. display( )
135- ) ;
136- }
137- }
124+ validate_hosts_backup ( paths, source_path) ?;
138125
139126 let backup = fs:: read_to_string ( & paths. hosts_backup_path )
140127 . with_context ( || format ! ( "failed to read {}" , paths. hosts_backup_path. display( ) ) ) ?;
@@ -179,6 +166,61 @@ pub(crate) fn backup_state(paths: &AppPaths) -> BackupState {
179166 }
180167}
181168
169+ pub ( crate ) fn validate_hosts_backup ( paths : & AppPaths , source_path : & Path ) -> Result < ( ) > {
170+ match backup_state ( paths) {
171+ BackupState :: Ready => { }
172+ BackupState :: Missing => {
173+ bail ! (
174+ "hosts backup is unavailable; expected both {} and {}" ,
175+ paths. hosts_backup_path. display( ) ,
176+ paths. hosts_backup_meta_path. display( )
177+ ) ;
178+ }
179+ BackupState :: Inconsistent => {
180+ bail ! (
181+ "hosts backup state is inconsistent; expected both {} and {}" ,
182+ paths. hosts_backup_path. display( ) ,
183+ paths. hosts_backup_meta_path. display( )
184+ ) ;
185+ }
186+ }
187+
188+ let _ = fs:: read_to_string ( & paths. hosts_backup_path )
189+ . with_context ( || format ! ( "failed to read {}" , paths. hosts_backup_path. display( ) ) ) ?;
190+ let meta = load_backup_meta ( paths) ?;
191+ if !source_path_matches ( source_path, & meta. source_path ) {
192+ bail ! (
193+ "hosts backup source path mismatch; expected {}, got {}" ,
194+ source_path. display( ) ,
195+ meta. source_path
196+ ) ;
197+ }
198+ Ok ( ( ) )
199+ }
200+
201+ pub ( crate ) fn clear_hosts_backup ( paths : & AppPaths ) -> Result < ( ) > {
202+ let backup_temp_path = temp_backup_path ( & paths. hosts_backup_path ) ;
203+ let meta_temp_path = temp_backup_path ( & paths. hosts_backup_meta_path ) ;
204+ let mut errors = Vec :: new ( ) ;
205+
206+ for path in [
207+ & paths. hosts_backup_path ,
208+ & paths. hosts_backup_meta_path ,
209+ & backup_temp_path,
210+ & meta_temp_path,
211+ ] {
212+ if let Err ( error) = cleanup_temp_file ( path) {
213+ errors. push ( format ! ( "failed to remove {}: {error:#}" , path. display( ) ) ) ;
214+ }
215+ }
216+
217+ if errors. is_empty ( ) {
218+ Ok ( ( ) )
219+ } else {
220+ bail ! ( errors. join( "; " ) ) ;
221+ }
222+ }
223+
182224fn load_backup_meta ( paths : & AppPaths ) -> Result < HostsBackupMeta > {
183225 let raw = fs:: read ( & paths. hosts_backup_meta_path )
184226 . with_context ( || format ! ( "failed to read {}" , paths. hosts_backup_meta_path. display( ) ) ) ?;
@@ -451,7 +493,8 @@ mod tests {
451493 use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
452494
453495 use super :: {
454- ensure_hosts_backup, load_backup_meta, restore_hosts_from_backup, write_hosts_content,
496+ BackupState , backup_state, clear_hosts_backup, ensure_hosts_backup, load_backup_meta,
497+ restore_hosts_from_backup, validate_hosts_backup, write_hosts_content,
455498 } ;
456499 use crate :: paths:: AppPaths ;
457500
@@ -548,6 +591,52 @@ mod tests {
548591 cleanup_test_dir ( & test_dir) ;
549592 }
550593
594+ #[ test]
595+ fn validate_hosts_backup_rejects_broken_metadata ( ) {
596+ let test_dir = create_test_dir ( "validate_hosts_backup_rejects_broken_metadata" ) ;
597+ let paths = test_paths ( & test_dir) ;
598+ std:: fs:: create_dir_all ( & paths. runtime_dir ) . unwrap ( ) ;
599+ let source_path = test_dir. join ( "hosts" ) ;
600+
601+ std:: fs:: write ( & source_path, "original" ) . unwrap ( ) ;
602+ std:: fs:: write ( & paths. hosts_backup_path , "backup" ) . unwrap ( ) ;
603+ std:: fs:: write ( & paths. hosts_backup_meta_path , "{not-json" ) . unwrap ( ) ;
604+
605+ let error = validate_hosts_backup ( & paths, & source_path) . unwrap_err ( ) ;
606+ assert ! (
607+ error
608+ . to_string( )
609+ . contains( "failed to parse hosts backup metadata" )
610+ ) ;
611+
612+ cleanup_test_dir ( & test_dir) ;
613+ }
614+
615+ #[ test]
616+ fn clear_hosts_backup_removes_partial_and_temp_files ( ) {
617+ let test_dir = create_test_dir ( "clear_hosts_backup_removes_partial_and_temp_files" ) ;
618+ let paths = test_paths ( & test_dir) ;
619+ std:: fs:: create_dir_all ( & paths. runtime_dir ) . unwrap ( ) ;
620+
621+ std:: fs:: write ( & paths. hosts_backup_path , "backup" ) . unwrap ( ) ;
622+ let backup_temp_path = paths
623+ . hosts_backup_path
624+ . with_file_name ( "hosts.backup.linuxdo-accelerator.tmp" ) ;
625+ let meta_temp_path = paths
626+ . hosts_backup_meta_path
627+ . with_file_name ( "hosts.backup.json.linuxdo-accelerator.tmp" ) ;
628+ std:: fs:: write ( & backup_temp_path, "temp" ) . unwrap ( ) ;
629+ std:: fs:: write ( & meta_temp_path, "temp" ) . unwrap ( ) ;
630+
631+ clear_hosts_backup ( & paths) . unwrap ( ) ;
632+
633+ assert_eq ! ( backup_state( & paths) , BackupState :: Missing ) ;
634+ assert ! ( !backup_temp_path. exists( ) ) ;
635+ assert ! ( !meta_temp_path. exists( ) ) ;
636+
637+ cleanup_test_dir ( & test_dir) ;
638+ }
639+
551640 #[ cfg( windows) ]
552641 #[ test]
553642 fn windows_retryable_replace_errors_cover_common_lock_cases ( ) {
0 commit comments