hosts-translator/internal/parser/parser.go

137 lines
3.2 KiB
Go

package parser
import (
"errors"
"log"
"os"
"path/filepath"
"strings"
"go.dev.pztrn.name/hosts-translator/internal/configuration"
"go.dev.pztrn.name/hosts-translator/internal/models"
)
var ErrParserError = errors.New("hosts parser")
// Parser is a controlling structure for hosts file (both parsed and unparsed data).
type Parser struct {
parsedHosts []models.Host
}
// NewParser creates new hosts file parsing controlling structure.
func NewParser() *Parser {
p := &Parser{}
p.initialize()
return p
}
// Returns file data as bytes.
func (p *Parser) getFileData() ([]string, error) {
// Before everything we should normalize file path.
filePath := configuration.HostsFilePath
// Replace possible "~" in the beginning as file reading function unable
// to expand it. Also we should check only beginning because tilde is actually
// a very valid directory or file name.
if strings.HasPrefix(filePath, "~") {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
filePath = strings.Replace(filePath, "~", homeDir, 1)
}
// Get absolute file path.
absolutePath, err := filepath.Abs(filePath)
if err != nil {
return nil, err
}
log.Println("Reading file data from", absolutePath)
// Read file data.
rawData, err := os.ReadFile(absolutePath)
if err != nil {
return nil, err
}
data := strings.Split(string(rawData), "\n")
return data, nil
}
// GetParsedData returns parsed data slice.
func (p *Parser) GetParsedData() []models.Host {
return p.parsedHosts
}
// Initializes internal state of parser as well as CLI flags.
func (p *Parser) initialize() {
p.parsedHosts = make([]models.Host, 0)
}
// Parse parses hosts file into internal representation.
func (p *Parser) Parse() error {
log.Println("Starting hosts file parsing. File located at", configuration.HostsFilePath)
data, err := p.getFileData()
if err != nil {
return err
}
for _, line := range data {
// We should skip commented lines.
if strings.HasPrefix(line, "#") {
continue
}
// Every line is a two-or-more-not-empty-strings. First string is always
// an IP address.
// Also there are a non-zero possibility that line will contain tabs, so as
// very first action we should replace them with spaces.
if strings.Contains(line, "\t") {
line = strings.Replace(line, "\t", " ", -1)
}
lineSplitted := strings.Split(line, " ")
// As one IP address can be bound to multiple domains we should take care
// of that situation by creating multiple Host structures.
var address string
for _, lineData := range lineSplitted {
// Also there might be a case when address placed first in line but
// line itself has spaces in the beginning.
if address == "" && lineData != "" {
address = lineData
continue
}
if lineData == "" {
continue
}
domainToAdd := lineData
if configuration.DomainPostfix != "" && !strings.HasSuffix(domainToAdd, configuration.DomainPostfix) {
domainToAdd += "." + configuration.DomainPostfix
}
p.parsedHosts = append(p.parsedHosts, models.Host{
Domain: domainToAdd,
Address: address,
})
}
}
log.Println("Got", len(p.parsedHosts), "domains from hosts file")
// ToDo: hide under CLI parameter like '-debug'?
// log.Printf("%+v\n", p.parsedHosts)
return nil
}