1+ use std:: collections:: HashSet ;
12use std:: fs;
23use std:: path:: PathBuf ;
34
@@ -111,7 +112,14 @@ pub fn validate_hosts_backup_file(paths: &AppPaths) -> Result<()> {
111112#[ cfg( not( target_os = "android" ) ) ]
112113fn render_managed_hosts ( original : & str , config : & AppConfig ) -> String {
113114 let newline = detect_newline ( original) ;
114- let mut content = strip_managed_block ( original) ;
115+ let managed_hosts = config
116+ . hosts_domains ( )
117+ . iter ( )
118+ . filter ( |host| !host. starts_with ( "*." ) )
119+ . map ( |host| host. to_ascii_lowercase ( ) )
120+ . collect :: < HashSet < _ > > ( ) ;
121+ let mut content =
122+ strip_conflicting_host_entries ( & strip_managed_block ( original) , & managed_hosts) ;
115123
116124 if !content. is_empty ( ) && !content. ends_with ( newline) {
117125 content. push_str ( newline) ;
@@ -131,6 +139,57 @@ fn render_managed_hosts(original: &str, config: &AppConfig) -> String {
131139 content
132140}
133141
142+ #[ cfg( not( target_os = "android" ) ) ]
143+ fn strip_conflicting_host_entries ( content : & str , managed_hosts : & HashSet < String > ) -> String {
144+ if managed_hosts. is_empty ( ) {
145+ return content. to_string ( ) ;
146+ }
147+
148+ let newline = detect_newline ( content) ;
149+ let mut output = Vec :: new ( ) ;
150+
151+ for raw_line in content. lines ( ) {
152+ let trimmed = raw_line. trim ( ) ;
153+ if trimmed. is_empty ( ) || trimmed. starts_with ( '#' ) {
154+ output. push ( raw_line. to_string ( ) ) ;
155+ continue ;
156+ }
157+
158+ let ( line_body, comment) = match raw_line. find ( '#' ) {
159+ Some ( index) => ( & raw_line[ ..index] , Some ( & raw_line[ index..] ) ) ,
160+ None => ( raw_line, None ) ,
161+ } ;
162+
163+ let fields = line_body. split_whitespace ( ) . collect :: < Vec < _ > > ( ) ;
164+ if fields. len ( ) < 2 {
165+ output. push ( raw_line. to_string ( ) ) ;
166+ continue ;
167+ }
168+
169+ let address = fields[ 0 ] ;
170+ let remaining_hosts = fields[ 1 ..]
171+ . iter ( )
172+ . copied ( )
173+ . filter ( |host| !managed_hosts. contains ( & host. to_ascii_lowercase ( ) ) )
174+ . collect :: < Vec < _ > > ( ) ;
175+
176+ if remaining_hosts. is_empty ( ) {
177+ continue ;
178+ }
179+
180+ let mut rebuilt = format ! ( "{address} {}" , remaining_hosts. join( " " ) ) ;
181+ if let Some ( comment) = comment {
182+ if !comment. trim ( ) . is_empty ( ) {
183+ rebuilt. push ( ' ' ) ;
184+ rebuilt. push_str ( comment. trim_start ( ) ) ;
185+ }
186+ }
187+ output. push ( rebuilt) ;
188+ }
189+
190+ output. join ( newline)
191+ }
192+
134193#[ cfg( not( target_os = "android" ) ) ]
135194fn backup_baseline_content ( original : & str ) -> String {
136195 strip_managed_block ( original)
@@ -245,8 +304,12 @@ fn hosts_path() -> PathBuf {
245304
246305#[ cfg( test) ]
247306mod tests {
248- use super :: { backup_baseline_content, render_managed_hosts, strip_managed_block} ;
307+ use super :: {
308+ backup_baseline_content, render_managed_hosts, strip_conflicting_host_entries,
309+ strip_managed_block,
310+ } ;
249311 use crate :: config:: AppConfig ;
312+ use std:: collections:: HashSet ;
250313
251314 #[ test]
252315 fn strip_managed_block_only_removes_owned_lines ( ) {
@@ -318,4 +381,25 @@ mod tests {
318381 let backup = backup_baseline_content ( & content) ;
319382 assert_eq ! ( backup, "127.0.0.1 localhost\n 1.1.1.1 example.com" ) ;
320383 }
384+
385+ #[ test]
386+ fn strip_conflicting_host_entries_removes_older_duplicate_host_mappings ( ) {
387+ let content = [
388+ "127.0.0.1 localhost" ,
389+ "172.66.166.61 linux.do" ,
390+ "8.8.8.8 example.com www.linux.do # keep example only" ,
391+ "1.1.1.1 untouched.example" ,
392+ ]
393+ . join ( "\n " ) ;
394+ let managed_hosts = [ "linux.do" , "www.linux.do" ]
395+ . into_iter ( )
396+ . map ( str:: to_string)
397+ . collect :: < HashSet < _ > > ( ) ;
398+
399+ let stripped = strip_conflicting_host_entries ( & content, & managed_hosts) ;
400+
401+ assert ! ( !stripped. contains( "172.66.166.61 linux.do" ) ) ;
402+ assert ! ( stripped. contains( "8.8.8.8 example.com # keep example only" ) ) ;
403+ assert ! ( stripped. contains( "1.1.1.1 untouched.example" ) ) ;
404+ }
321405}
0 commit comments