package hostfile import ( "fmt" "strings" ) // ParseManagedBlock splits raw file lines into prefix, managed, and postfix sections. // Returns an error if the managed block markers are corrupt. // // Uses -1 as sentinel for "not found" because the start marker can appear at line 0. // The current code's zero-index bug (treating index 0 as "not found") is avoided here. func ParseManagedBlock(lines []string) (prefix, managed, postfix []string, hasManagedBlock bool, err error) { startIdx := -1 // sentinel: not found endIdx := -1 // sentinel: not found startCount := 0 endCount := 0 for i, line := range lines { // Recognise both current and legacy markers so that hosts files // written by the old "DNSHelper" binary are parsed correctly. if strings.Contains(line, "ekDNSHelper <<-> START CONFIG") || strings.Contains(line, "DNSHelper <<-> START CONFIG") { startCount++ if startIdx == -1 { startIdx = i } } if strings.Contains(line, "ekDNSHelper <->> END CONFIG") || strings.Contains(line, "DNSHelper <->> END CONFIG") { endCount++ if endIdx == -1 { endIdx = i } } } // Corruption detection (R-008): scan ALL lines first, then evaluate. if startCount > 1 || endCount > 1 { return nil, nil, nil, false, fmt.Errorf("managed block is corrupt: duplicate start/end markers found") } if startIdx >= 0 && endIdx < 0 { return nil, nil, nil, false, fmt.Errorf("managed block is corrupt: start marker found at line %d but no end marker", startIdx+1) } if endIdx >= 0 && startIdx < 0 { return nil, nil, nil, false, fmt.Errorf("managed block is corrupt: end marker found at line %d but no start marker", endIdx+1) } if startIdx >= 0 && endIdx >= 0 && endIdx < startIdx { return nil, nil, nil, false, fmt.Errorf("managed block is corrupt: end marker (line %d) appears before start marker (line %d)", endIdx+1, startIdx+1) } if startIdx < 0 && endIdx < 0 { // No managed block — all lines are prefix. return lines, nil, nil, false, nil } // Valid managed block. prefix = lines[:startIdx] managed = lines[startIdx+1 : endIdx] postfix = lines[endIdx+1:] return prefix, managed, postfix, true, nil } // lineMatchesHostname parses a hosts-file line ("IPhostname [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 { // Collect hostnames being added. hostnames := make(map[string]bool) for _, e := range entries { hostnames[e.Hostname] = true } // Keep existing lines that don't match any new hostname. var result []string for _, line := range existing { keep := true for h := range hostnames { if lineMatchesHostname(line, h) { keep = false break } } if keep { result = append(result, line) } } // Append new entries. for _, e := range entries { result = append(result, e.String()) } // Deduplicate preserving order. return deduplicateLines(result) } // RemoveByHostname removes all managed entries matching any of the specified // hostnames. Returns the remaining content. // // The correct algorithm iterates lines on the outer loop (not hosts), which // avoids the duplication bug in the legacy removeHostsFromExistingContent // function (dataprep.go). func RemoveByHostname(existing []string, hostnames []string) []string { var result []string for _, line := range existing { shouldRemove := false for _, host := range hostnames { if lineMatchesHostname(line, host) { shouldRemove = true break } } if !shouldRemove { result = append(result, line) } } return result } // RemoveAll returns nil, indicating all managed entries should be removed. // When AssembleContent receives nil/empty managed content, it omits the // managed block entirely (markers included). func RemoveAll() []string { return nil } // AssembleContent builds the full file content from sections. // If managed is empty, the managed block (markers included) is omitted entirely. func AssembleContent(prefix, managed, postfix []string) []string { var result []string result = append(result, prefix...) if len(managed) > 0 { result = append(result, StartMarker) result = append(result, managed...) result = append(result, EndMarker) } result = append(result, postfix...) return result } // deduplicateLines removes duplicate lines preserving order. func deduplicateLines(lines []string) []string { seen := make(map[string]bool) var result []string for _, line := range lines { if !seen[line] { seen[line] = true result = append(result, line) } } return result }