mirror of
https://github.com/shokinn/hosts-go.git
synced 2025-08-23 08:33:02 +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