mirror of
				https://github.com/shokinn/hosts-go.git
				synced 2025-11-04 12:38:34 +00:00 
			
		
		
		
	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.
This commit is contained in:
		
						commit
						d66ec51ebd
					
				
					 12 changed files with 1747 additions and 0 deletions
				
			
		
							
								
								
									
										221
									
								
								internal/core/models.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								internal/core/models.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,221 @@
 | 
			
		|||
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
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue