hosts-go/internal/core/models.go
phg d66ec51ebd feat: Initialize hosts-go project with foundational structure and core functionality
- Created activeContext.md and productContext.md to outline project goals and current focus.
- Established progress.md to track project milestones and tasks.
- Developed projectbrief.md detailing application overview, requirements, and directory structure.
- Documented systemPatterns.md to describe architecture and design patterns used.
- Compiled techContext.md to specify technologies and development setup.
- Implemented comprehensive unit tests in models_test.go for HostEntry and HostsFile functionalities.
2025-08-12 22:41:33 +02:00

221 lines
5.2 KiB
Go

package core
import (
"fmt"
"net"
"regexp"
"strings"
)
// HostEntry represents a single entry in the hosts file
type HostEntry struct {
IP string // IP address (IPv4 or IPv6)
Hostname string // Primary hostname
Aliases []string // Additional hostnames/aliases
Comment string // Inline comment
Active bool // Whether the entry is enabled (not commented out)
Original string // Original line from hosts file for preservation
}
// NewHostEntry creates a new host entry with validation
func NewHostEntry(ip, hostname string) (*HostEntry, error) {
entry := &HostEntry{
IP: strings.TrimSpace(ip),
Hostname: strings.TrimSpace(hostname),
Aliases: make([]string, 0),
Active: true,
}
if err := entry.Validate(); err != nil {
return nil, err
}
return entry, nil
}
// Validate checks if the host entry is valid
func (h *HostEntry) Validate() error {
if h.IP == "" {
return fmt.Errorf("IP address cannot be empty")
}
if h.Hostname == "" {
return fmt.Errorf("hostname cannot be empty")
}
// Validate IP address
if net.ParseIP(h.IP) == nil {
return fmt.Errorf("invalid IP address: %s", h.IP)
}
// Validate hostname format
if err := validateHostname(h.Hostname); err != nil {
return fmt.Errorf("invalid hostname: %w", err)
}
// Validate aliases
for _, alias := range h.Aliases {
if err := validateHostname(alias); err != nil {
return fmt.Errorf("invalid alias '%s': %w", alias, err)
}
}
return nil
}
// AddAlias adds an alias to the host entry
func (h *HostEntry) AddAlias(alias string) error {
alias = strings.TrimSpace(alias)
if err := validateHostname(alias); err != nil {
return fmt.Errorf("invalid alias: %w", err)
}
// Check for duplicates
for _, existing := range h.Aliases {
if existing == alias {
return fmt.Errorf("alias '%s' already exists", alias)
}
}
h.Aliases = append(h.Aliases, alias)
return nil
}
// AllHostnames returns the primary hostname and all aliases
func (h *HostEntry) AllHostnames() []string {
result := []string{h.Hostname}
result = append(result, h.Aliases...)
return result
}
// String returns the hosts file representation of the entry
func (h *HostEntry) String() string {
var parts []string
// Add IP and hostname
parts = append(parts, h.IP, h.Hostname)
// Add aliases
parts = append(parts, h.Aliases...)
line := strings.Join(parts, "\t")
// Add comment if present
if h.Comment != "" {
line += "\t# " + h.Comment
}
// Add comment prefix if inactive
if !h.Active {
line = "# " + line
}
return line
}
// HostsFile represents the entire hosts file
type HostsFile struct {
Entries []*HostEntry // All host entries
Comments []string // Standalone comment lines
Header []string // Header comments at the top of the file
}
// NewHostsFile creates a new empty hosts file
func NewHostsFile() *HostsFile {
return &HostsFile{
Entries: make([]*HostEntry, 0),
Comments: make([]string, 0),
Header: make([]string, 0),
}
}
// AddEntry adds a host entry to the file
func (hf *HostsFile) AddEntry(entry *HostEntry) error {
if err := entry.Validate(); err != nil {
return err
}
hf.Entries = append(hf.Entries, entry)
return nil
}
// FindEntry finds an entry by hostname
func (hf *HostsFile) FindEntry(hostname string) *HostEntry {
hostname = strings.TrimSpace(hostname)
for _, entry := range hf.Entries {
if entry.Hostname == hostname {
return entry
}
for _, alias := range entry.Aliases {
if alias == hostname {
return entry
}
}
}
return nil
}
// RemoveEntry removes an entry by hostname
func (hf *HostsFile) RemoveEntry(hostname string) bool {
for i, entry := range hf.Entries {
if entry.Hostname == hostname {
hf.Entries = append(hf.Entries[:i], hf.Entries[i+1:]...)
return true
}
for _, alias := range entry.Aliases {
if alias == hostname {
hf.Entries = append(hf.Entries[:i], hf.Entries[i+1:]...)
return true
}
}
}
return false
}
// ActiveEntries returns only the active (non-commented) entries
func (hf *HostsFile) ActiveEntries() []*HostEntry {
var active []*HostEntry
for _, entry := range hf.Entries {
if entry.Active {
active = append(active, entry)
}
}
return active
}
// validateHostname validates a hostname according to RFC standards
func validateHostname(hostname string) error {
if len(hostname) == 0 {
return fmt.Errorf("hostname cannot be empty")
}
if len(hostname) > 253 {
return fmt.Errorf("hostname too long (max 253 characters)")
}
// Cannot start or end with hyphen
if strings.HasPrefix(hostname, "-") || strings.HasSuffix(hostname, "-") {
return fmt.Errorf("hostname cannot start or end with hyphen")
}
// Split by dots and validate each label
labels := strings.Split(hostname, ".")
for _, label := range labels {
if len(label) == 0 {
return fmt.Errorf("invalid hostname format")
}
// Each label cannot start or end with hyphen
if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
return fmt.Errorf("hostname cannot start or end with hyphen")
}
// Each label must contain only alphanumeric characters and hyphens
labelRegex := regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
if !labelRegex.MatchString(label) {
return fmt.Errorf("invalid hostname format")
}
}
return nil
}