Enhance HostsManager to prevent modification and movement of default system entries; add is_default_entry method to HostEntry and update sorting methods to prioritize default entries.
This commit is contained in:
		
							parent
							
								
									8c1cd2047e
								
							
						
					
					
						commit
						3084650c27
					
				
					 6 changed files with 122 additions and 45 deletions
				
			
		| 
						 | 
				
			
			@ -181,6 +181,11 @@ class HostsManager:
 | 
			
		|||
        
 | 
			
		||||
        try:
 | 
			
		||||
            entry = hosts_file.entries[index]
 | 
			
		||||
            
 | 
			
		||||
            # Prevent modification of default system entries
 | 
			
		||||
            if entry.is_default_entry():
 | 
			
		||||
                return False, "Cannot modify default system entries"
 | 
			
		||||
            
 | 
			
		||||
            old_state = "active" if entry.is_active else "inactive"
 | 
			
		||||
            entry.is_active = not entry.is_active
 | 
			
		||||
            new_state = "active" if entry.is_active else "inactive"
 | 
			
		||||
| 
						 | 
				
			
			@ -207,6 +212,13 @@ class HostsManager:
 | 
			
		|||
            return False, "Cannot move entry up"
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            entry = hosts_file.entries[index]
 | 
			
		||||
            target_entry = hosts_file.entries[index - 1]
 | 
			
		||||
            
 | 
			
		||||
            # Prevent moving default system entries or moving entries above default entries
 | 
			
		||||
            if entry.is_default_entry() or target_entry.is_default_entry():
 | 
			
		||||
                return False, "Cannot move default system entries"
 | 
			
		||||
            
 | 
			
		||||
            # Swap with previous entry
 | 
			
		||||
            hosts_file.entries[index], hosts_file.entries[index - 1] = \
 | 
			
		||||
                hosts_file.entries[index - 1], hosts_file.entries[index]
 | 
			
		||||
| 
						 | 
				
			
			@ -232,6 +244,13 @@ class HostsManager:
 | 
			
		|||
            return False, "Cannot move entry down"
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            entry = hosts_file.entries[index]
 | 
			
		||||
            target_entry = hosts_file.entries[index + 1]
 | 
			
		||||
            
 | 
			
		||||
            # Prevent moving default system entries or moving entries below default entries
 | 
			
		||||
            if entry.is_default_entry() or target_entry.is_default_entry():
 | 
			
		||||
                return False, "Cannot move default system entries"
 | 
			
		||||
            
 | 
			
		||||
            # Swap with next entry
 | 
			
		||||
            hosts_file.entries[index], hosts_file.entries[index + 1] = \
 | 
			
		||||
                hosts_file.entries[index + 1], hosts_file.entries[index]
 | 
			
		||||
| 
						 | 
				
			
			@ -262,6 +281,12 @@ class HostsManager:
 | 
			
		|||
            return False, "Invalid entry index"
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            entry = hosts_file.entries[index]
 | 
			
		||||
            
 | 
			
		||||
            # Prevent modification of default system entries
 | 
			
		||||
            if entry.is_default_entry():
 | 
			
		||||
                return False, "Cannot modify default system entries"
 | 
			
		||||
            
 | 
			
		||||
            # Create new entry to validate
 | 
			
		||||
            new_entry = HostEntry(
 | 
			
		||||
                ip_address=ip_address,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,28 @@ class HostEntry:
 | 
			
		|||
        """Validate the entry after initialization."""
 | 
			
		||||
        self.validate()
 | 
			
		||||
 | 
			
		||||
    def is_default_entry(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Check if this entry is a system default entry.
 | 
			
		||||
        
 | 
			
		||||
        Returns:
 | 
			
		||||
            True if this is a default system entry (localhost, broadcasthost, ::1)
 | 
			
		||||
        """
 | 
			
		||||
        if not self.hostnames:
 | 
			
		||||
            return False
 | 
			
		||||
        
 | 
			
		||||
        canonical_hostname = self.hostnames[0]
 | 
			
		||||
        default_entries = [
 | 
			
		||||
            {"ip": "127.0.0.1", "hostname": "localhost"},
 | 
			
		||||
            {"ip": "255.255.255.255", "hostname": "broadcasthost"},
 | 
			
		||||
            {"ip": "::1", "hostname": "localhost"},
 | 
			
		||||
        ]
 | 
			
		||||
        
 | 
			
		||||
        for entry in default_entries:
 | 
			
		||||
            if entry["ip"] == self.ip_address and entry["hostname"] == canonical_hostname:
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def validate(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Validate the host entry data.
 | 
			
		||||
| 
						 | 
				
			
			@ -176,13 +198,42 @@ class HostsFile:
 | 
			
		|||
        """Get all inactive entries."""
 | 
			
		||||
        return [entry for entry in self.entries if not entry.is_active]
 | 
			
		||||
 | 
			
		||||
    def sort_by_ip(self) -> None:
 | 
			
		||||
        """Sort entries by IP address."""
 | 
			
		||||
        self.entries.sort(key=lambda entry: ipaddress.ip_address(entry.ip_address))
 | 
			
		||||
    def sort_by_ip(self, ascending: bool = True) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Sort entries by IP address, keeping default entries on top.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            ascending: Sort in ascending order if True, descending if False
 | 
			
		||||
        """
 | 
			
		||||
        def sort_key(entry):
 | 
			
		||||
            try:
 | 
			
		||||
                ip_str = entry.ip_address.lstrip('# ')
 | 
			
		||||
                ip_obj = ipaddress.ip_address(ip_str)
 | 
			
		||||
                # Default entries always come first (priority 0), others get priority 1
 | 
			
		||||
                priority = 0 if entry.is_default_entry() else 1
 | 
			
		||||
                # Create a tuple for sorting: (priority, version, ip_int)
 | 
			
		||||
                return (priority, ip_obj.version, int(ip_obj))
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                # If IP parsing fails, use string comparison with high sort priority
 | 
			
		||||
                priority = 0 if entry.is_default_entry() else 1
 | 
			
		||||
                return (priority, 999, entry.ip_address)
 | 
			
		||||
        
 | 
			
		||||
        self.entries.sort(key=sort_key, reverse=not ascending)
 | 
			
		||||
 | 
			
		||||
    def sort_by_hostname(self) -> None:
 | 
			
		||||
        """Sort entries by first hostname."""
 | 
			
		||||
        self.entries.sort(key=lambda entry: entry.hostnames[0].lower())
 | 
			
		||||
    def sort_by_hostname(self, ascending: bool = True) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Sort entries by first hostname, keeping default entries on top.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            ascending: Sort in ascending order if True, descending if False
 | 
			
		||||
        """
 | 
			
		||||
        def sort_key(entry):
 | 
			
		||||
            # Default entries always come first (priority 0), others get priority 1
 | 
			
		||||
            priority = 0 if entry.is_default_entry() else 1
 | 
			
		||||
            hostname = (entry.hostnames[0] if entry.hostnames else "").lower()
 | 
			
		||||
            return (priority, hostname)
 | 
			
		||||
        
 | 
			
		||||
        self.entries.sort(key=sort_key, reverse=not ascending)
 | 
			
		||||
 | 
			
		||||
    def find_entries_by_hostname(self, hostname: str) -> List[int]:
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -240,8 +240,17 @@ class HostsManagerApp(App):
 | 
			
		|||
            # Get the canonical hostname (first hostname)
 | 
			
		||||
            canonical_hostname = entry.hostnames[0] if entry.hostnames else ""
 | 
			
		||||
            
 | 
			
		||||
            # Add row with styling based on active status
 | 
			
		||||
            if entry.is_active:
 | 
			
		||||
            # Check if this is a default system entry
 | 
			
		||||
            is_default = entry.is_default_entry()
 | 
			
		||||
            
 | 
			
		||||
            # Add row with styling based on active status and default entry status
 | 
			
		||||
            if is_default:
 | 
			
		||||
                # Default entries are always shown in dim grey regardless of active status
 | 
			
		||||
                active_text = Text("✓" if entry.is_active else "", style="dim white")
 | 
			
		||||
                ip_text = Text(entry.ip_address, style="dim white")
 | 
			
		||||
                hostname_text = Text(canonical_hostname, style="dim white")
 | 
			
		||||
                table.add_row(active_text, ip_text, hostname_text)
 | 
			
		||||
            elif entry.is_active:
 | 
			
		||||
                # Active entries in green with checkmark
 | 
			
		||||
                active_text = Text("✓", style="bold green")
 | 
			
		||||
                ip_text = Text(entry.ip_address, style="bold green")
 | 
			
		||||
| 
						 | 
				
			
			@ -304,6 +313,12 @@ class HostsManagerApp(App):
 | 
			
		|||
            f"Status: {'Active' if entry.is_active else 'Inactive'}",
 | 
			
		||||
        ]
 | 
			
		||||
        
 | 
			
		||||
        # Add notice for default system entries
 | 
			
		||||
        if entry.is_default_entry():
 | 
			
		||||
            details_lines.append("")
 | 
			
		||||
            details_lines.append("⚠️  SYSTEM DEFAULT ENTRY")
 | 
			
		||||
            details_lines.append("This is a default system entry and cannot be modified.")
 | 
			
		||||
        
 | 
			
		||||
        if entry.comment:
 | 
			
		||||
            details_lines.append(f"Comment: {entry.comment}")
 | 
			
		||||
        
 | 
			
		||||
| 
						 | 
				
			
			@ -379,20 +394,8 @@ class HostsManagerApp(App):
 | 
			
		|||
            self.sort_column = "ip"
 | 
			
		||||
            self.sort_ascending = True
 | 
			
		||||
        
 | 
			
		||||
        # Sort the entries
 | 
			
		||||
        import ipaddress
 | 
			
		||||
        def ip_sort_key(entry):
 | 
			
		||||
            try:
 | 
			
		||||
                ip_str = entry.ip_address.lstrip('# ')
 | 
			
		||||
                ip_obj = ipaddress.ip_address(ip_str)
 | 
			
		||||
                # Create a tuple for sorting: (version, ip_int)
 | 
			
		||||
                # This ensures IPv4 comes before IPv6, and within each version they're sorted numerically
 | 
			
		||||
                return (ip_obj.version, int(ip_obj))
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                # If IP parsing fails, use string comparison with high sort priority
 | 
			
		||||
                return (999, entry.ip_address)
 | 
			
		||||
        
 | 
			
		||||
        self.hosts_file.entries.sort(key=ip_sort_key, reverse=not self.sort_ascending)
 | 
			
		||||
        # Sort the entries using the new method that keeps defaults on top
 | 
			
		||||
        self.hosts_file.sort_by_ip(self.sort_ascending)
 | 
			
		||||
        self.populate_entries_table()
 | 
			
		||||
        
 | 
			
		||||
        direction = "ascending" if self.sort_ascending else "descending"
 | 
			
		||||
| 
						 | 
				
			
			@ -407,11 +410,8 @@ class HostsManagerApp(App):
 | 
			
		|||
            self.sort_column = "hostname"
 | 
			
		||||
            self.sort_ascending = True
 | 
			
		||||
        
 | 
			
		||||
        # Sort the entries
 | 
			
		||||
        self.hosts_file.entries.sort(
 | 
			
		||||
            key=lambda entry: (entry.hostnames[0] if entry.hostnames else "").lower(),
 | 
			
		||||
            reverse=not self.sort_ascending
 | 
			
		||||
        )
 | 
			
		||||
        # Sort the entries using the new method that keeps defaults on top
 | 
			
		||||
        self.hosts_file.sort_by_hostname(self.sort_ascending)
 | 
			
		||||
        self.populate_entries_table()
 | 
			
		||||
        
 | 
			
		||||
        direction = "ascending" if self.sort_ascending else "descending"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -326,9 +326,9 @@ class TestHostsManagerApp:
 | 
			
		|||
            
 | 
			
		||||
            app.action_sort_by_ip()
 | 
			
		||||
            
 | 
			
		||||
            # Check that entries are sorted
 | 
			
		||||
            assert app.hosts_file.entries[0].ip_address == "10.0.0.1"
 | 
			
		||||
            assert app.hosts_file.entries[1].ip_address == "127.0.0.1"
 | 
			
		||||
            # Check that entries are sorted with default entries on top
 | 
			
		||||
            assert app.hosts_file.entries[0].ip_address == "127.0.0.1"  # Default entry first
 | 
			
		||||
            assert app.hosts_file.entries[1].ip_address == "10.0.0.1"   # Then sorted non-defaults
 | 
			
		||||
            assert app.hosts_file.entries[2].ip_address == "192.168.1.1"
 | 
			
		||||
            
 | 
			
		||||
            assert app.sort_column == "ip"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -321,7 +321,7 @@ class TestHostsManager:
 | 
			
		|||
            manager.edit_mode = True
 | 
			
		||||
            
 | 
			
		||||
            hosts_file = HostsFile()
 | 
			
		||||
            entry = HostEntry("127.0.0.1", ["localhost"], is_active=True)
 | 
			
		||||
            entry = HostEntry("192.168.1.1", ["router"], is_active=True)  # Non-default entry
 | 
			
		||||
            hosts_file.entries.append(entry)
 | 
			
		||||
            
 | 
			
		||||
            success, message = manager.toggle_entry(hosts_file, 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -363,7 +363,7 @@ class TestHostsManager:
 | 
			
		|||
            manager.edit_mode = True
 | 
			
		||||
            
 | 
			
		||||
            hosts_file = HostsFile()
 | 
			
		||||
            entry1 = HostEntry("127.0.0.1", ["localhost"])
 | 
			
		||||
            entry1 = HostEntry("10.0.0.1", ["test1"])  # Non-default entries
 | 
			
		||||
            entry2 = HostEntry("192.168.1.1", ["router"])
 | 
			
		||||
            hosts_file.entries.extend([entry1, entry2])
 | 
			
		||||
            
 | 
			
		||||
| 
						 | 
				
			
			@ -372,7 +372,7 @@ class TestHostsManager:
 | 
			
		|||
            assert success
 | 
			
		||||
            assert "moved up" in message
 | 
			
		||||
            assert hosts_file.entries[0].hostnames[0] == "router"
 | 
			
		||||
            assert hosts_file.entries[1].hostnames[0] == "localhost"
 | 
			
		||||
            assert hosts_file.entries[1].hostnames[0] == "test1"
 | 
			
		||||
    
 | 
			
		||||
    def test_move_entry_up_invalid_index(self):
 | 
			
		||||
        """Test moving entry up with invalid index."""
 | 
			
		||||
| 
						 | 
				
			
			@ -396,7 +396,7 @@ class TestHostsManager:
 | 
			
		|||
            manager.edit_mode = True
 | 
			
		||||
            
 | 
			
		||||
            hosts_file = HostsFile()
 | 
			
		||||
            entry1 = HostEntry("127.0.0.1", ["localhost"])
 | 
			
		||||
            entry1 = HostEntry("10.0.0.1", ["test1"])  # Non-default entries
 | 
			
		||||
            entry2 = HostEntry("192.168.1.1", ["router"])
 | 
			
		||||
            hosts_file.entries.extend([entry1, entry2])
 | 
			
		||||
            
 | 
			
		||||
| 
						 | 
				
			
			@ -405,7 +405,7 @@ class TestHostsManager:
 | 
			
		|||
            assert success
 | 
			
		||||
            assert "moved down" in message
 | 
			
		||||
            assert hosts_file.entries[0].hostnames[0] == "router"
 | 
			
		||||
            assert hosts_file.entries[1].hostnames[0] == "localhost"
 | 
			
		||||
            assert hosts_file.entries[1].hostnames[0] == "test1"
 | 
			
		||||
    
 | 
			
		||||
    def test_move_entry_down_invalid_index(self):
 | 
			
		||||
        """Test moving entry down with invalid index."""
 | 
			
		||||
| 
						 | 
				
			
			@ -429,7 +429,7 @@ class TestHostsManager:
 | 
			
		|||
            manager.edit_mode = True
 | 
			
		||||
            
 | 
			
		||||
            hosts_file = HostsFile()
 | 
			
		||||
            entry = HostEntry("127.0.0.1", ["localhost"])
 | 
			
		||||
            entry = HostEntry("10.0.0.1", ["test"])  # Non-default entry
 | 
			
		||||
            hosts_file.entries.append(entry)
 | 
			
		||||
            
 | 
			
		||||
            success, message = manager.update_entry(
 | 
			
		||||
| 
						 | 
				
			
			@ -449,7 +449,7 @@ class TestHostsManager:
 | 
			
		|||
            manager.edit_mode = True
 | 
			
		||||
            
 | 
			
		||||
            hosts_file = HostsFile()
 | 
			
		||||
            entry = HostEntry("127.0.0.1", ["localhost"])
 | 
			
		||||
            entry = HostEntry("127.0.0.1", ["localhost"])  # Default entry - cannot be modified
 | 
			
		||||
            hosts_file.entries.append(entry)
 | 
			
		||||
            
 | 
			
		||||
            success, message = manager.update_entry(
 | 
			
		||||
| 
						 | 
				
			
			@ -457,7 +457,7 @@ class TestHostsManager:
 | 
			
		|||
            )
 | 
			
		||||
            
 | 
			
		||||
            assert not success
 | 
			
		||||
            assert "Invalid entry data" in message
 | 
			
		||||
            assert "Cannot modify default system entries" in message
 | 
			
		||||
    
 | 
			
		||||
    @patch('tempfile.NamedTemporaryFile')
 | 
			
		||||
    @patch('subprocess.run')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -224,20 +224,21 @@ class TestHostsFile:
 | 
			
		|||
        assert inactive_entries[0] == inactive_entry
 | 
			
		||||
    
 | 
			
		||||
    def test_sort_by_ip(self):
 | 
			
		||||
        """Test sorting entries by IP address."""
 | 
			
		||||
        """Test sorting entries by IP address with default entries on top."""
 | 
			
		||||
        hosts_file = HostsFile()
 | 
			
		||||
        entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["router"])
 | 
			
		||||
        entry2 = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])
 | 
			
		||||
        entry2 = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])  # Default entry
 | 
			
		||||
        entry3 = HostEntry(ip_address="10.0.0.1", hostnames=["test"])
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        hosts_file.add_entry(entry1)
 | 
			
		||||
        hosts_file.add_entry(entry2)
 | 
			
		||||
        hosts_file.add_entry(entry3)
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        hosts_file.sort_by_ip()
 | 
			
		||||
        
 | 
			
		||||
        assert hosts_file.entries[0].ip_address == "10.0.0.1"
 | 
			
		||||
        assert hosts_file.entries[1].ip_address == "127.0.0.1"
 | 
			
		||||
 | 
			
		||||
        # Default entries should come first, then sorted non-default entries
 | 
			
		||||
        assert hosts_file.entries[0].ip_address == "127.0.0.1"  # Default entry first
 | 
			
		||||
        assert hosts_file.entries[1].ip_address == "10.0.0.1"   # Then sorted non-defaults
 | 
			
		||||
        assert hosts_file.entries[2].ip_address == "192.168.1.1"
 | 
			
		||||
    
 | 
			
		||||
    def test_sort_by_hostname(self):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue