feat: Enhance AddEntries and RemoveByHostname functions with subdomain and alias handling
This commit is contained in:
+15
-2
@@ -57,6 +57,19 @@ func ParseManagedBlock(lines []string) (prefix, managed, postfix []string, hasMa
|
||||
return prefix, managed, postfix, true, nil
|
||||
}
|
||||
|
||||
// lineMatchesHostname parses a hosts-file line ("IP<whitespace>hostname [aliases...]")
|
||||
// and returns true if any hostname field (including aliases) matches,
|
||||
// using case-insensitive comparison per RFC 4343.
|
||||
func lineMatchesHostname(line, hostname string) bool {
|
||||
fields := strings.Fields(line)
|
||||
for _, f := range fields[1:] {
|
||||
if strings.EqualFold(f, hostname) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddEntries removes existing entries for the same hostnames as the new entries,
|
||||
// then appends the new entries. Returns deduplicated managed content lines.
|
||||
func AddEntries(existing []string, entries []DNSEntry) []string {
|
||||
@@ -71,7 +84,7 @@ func AddEntries(existing []string, entries []DNSEntry) []string {
|
||||
for _, line := range existing {
|
||||
keep := true
|
||||
for h := range hostnames {
|
||||
if strings.Contains(line, h) {
|
||||
if lineMatchesHostname(line, h) {
|
||||
keep = false
|
||||
break
|
||||
}
|
||||
@@ -101,7 +114,7 @@ func RemoveByHostname(existing []string, hostnames []string) []string {
|
||||
for _, line := range existing {
|
||||
shouldRemove := false
|
||||
for _, host := range hostnames {
|
||||
if strings.Contains(line, host) {
|
||||
if lineMatchesHostname(line, host) {
|
||||
shouldRemove = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -201,6 +201,52 @@ func TestAddEntries_ReplacesExistingHostname(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEntries_DoesNotClobberSubdomains(t *testing.T) {
|
||||
// Regression: substring matching would cause adding "example.com" to
|
||||
// incorrectly remove "sub.example.com" entries.
|
||||
existing := []string{
|
||||
"1.1.1.1\tsub.example.com",
|
||||
"2.2.2.2\tother.example.com",
|
||||
}
|
||||
entries := []hostfile.DNSEntry{{IP: "9.9.9.9", Hostname: "example.com"}}
|
||||
result := hostfile.AddEntries(existing, entries)
|
||||
if len(result) != 3 {
|
||||
t.Fatalf("expected 3 entries (2 existing + 1 new), got %d: %v", len(result), result)
|
||||
}
|
||||
for _, line := range result {
|
||||
if line == "1.1.1.1\tsub.example.com" || line == "2.2.2.2\tother.example.com" || line == "9.9.9.9\texample.com" {
|
||||
continue
|
||||
}
|
||||
t.Errorf("unexpected line: %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEntries_MatchesAliasColumn(t *testing.T) {
|
||||
// A hosts-file line can have aliases: "IP primary alias1 alias2"
|
||||
// Replacing "alias1" should remove the entire line.
|
||||
existing := []string{"1.1.1.1\tprimary\talias1"}
|
||||
entries := []hostfile.DNSEntry{{IP: "9.9.9.9", Hostname: "alias1"}}
|
||||
result := hostfile.AddEntries(existing, entries)
|
||||
for _, line := range result {
|
||||
if strings.Contains(line, "primary") {
|
||||
t.Errorf("line with alias1 should have been removed, but found: %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEntries_CaseInsensitive(t *testing.T) {
|
||||
// RFC 4343: hostnames are case-insensitive.
|
||||
existing := []string{"1.1.1.1\tHost1.Example.COM"}
|
||||
entries := []hostfile.DNSEntry{{IP: "9.9.9.9", Hostname: "host1.example.com"}}
|
||||
result := hostfile.AddEntries(existing, entries)
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 entry (old replaced), got %d: %v", len(result), result)
|
||||
}
|
||||
if result[0] != "9.9.9.9\thost1.example.com" {
|
||||
t.Errorf("expected new entry, got %q", result[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEntries_Deduplicates(t *testing.T) {
|
||||
existing := []string{"1.1.1.1\thost1"}
|
||||
entries := []hostfile.DNSEntry{
|
||||
@@ -332,6 +378,54 @@ func TestRemoveByHostname_RegressionNoDuplicates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveByHostname_DoesNotClobberSubdomains(t *testing.T) {
|
||||
// Regression: substring matching would remove "sub.example.com" when
|
||||
// only "example.com" was requested for removal.
|
||||
existing := []string{
|
||||
"1.1.1.1\texample.com",
|
||||
"2.2.2.2\tsub.example.com",
|
||||
"3.3.3.3\tmy-example.com",
|
||||
}
|
||||
result := hostfile.RemoveByHostname(existing, []string{"example.com"})
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 remaining entries, got %d: %v", len(result), result)
|
||||
}
|
||||
if result[0] != "2.2.2.2\tsub.example.com" {
|
||||
t.Errorf("expected sub.example.com to remain, got %q", result[0])
|
||||
}
|
||||
if result[1] != "3.3.3.3\tmy-example.com" {
|
||||
t.Errorf("expected my-example.com to remain, got %q", result[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveByHostname_MatchesAliasColumn(t *testing.T) {
|
||||
existing := []string{
|
||||
"1.1.1.1\tprimary\talias1",
|
||||
"2.2.2.2\thost2",
|
||||
}
|
||||
result := hostfile.RemoveByHostname(existing, []string{"alias1"})
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 remaining entry, got %d: %v", len(result), result)
|
||||
}
|
||||
if result[0] != "2.2.2.2\thost2" {
|
||||
t.Errorf("expected host2 to remain, got %q", result[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveByHostname_CaseInsensitive(t *testing.T) {
|
||||
existing := []string{
|
||||
"1.1.1.1\tHost1.Example.COM",
|
||||
"2.2.2.2\thost2",
|
||||
}
|
||||
result := hostfile.RemoveByHostname(existing, []string{"host1.example.com"})
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 remaining entry, got %d: %v", len(result), result)
|
||||
}
|
||||
if result[0] != "2.2.2.2\thost2" {
|
||||
t.Errorf("expected host2 to remain, got %q", result[0])
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RemoveAll tests (T018)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user