Skip to content

Commit

Permalink
updating readme and comments
Browse files Browse the repository at this point in the history
  • Loading branch information
luthermonson committed Oct 16, 2023
1 parent 39225b3 commit fb8a066
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 76 deletions.
92 changes: 45 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,62 @@
# Go library for working with a system's hostsfile
[![codecov](https://codecov.io/gh/goodhosts/hostsfile/branch/master/graph/badge.svg?token=BJQH16QQEH)](https://codecov.io/gh/goodhosts/hostsfile)
## Usage
# Go package for working with a system's hostsfile
[![codecov](https://codecov.io/gh/goodhosts/hostsfile/branch/main/graph/badge.svg?token=BJQH16QQEH)](https://codecov.io/gh/goodhosts/hostsfile)
[![Go Reference](https://pkg.go.dev/badge/github.com/goodhosts/hostsfile.svg)](https://pkg.go.dev/github.com/goodhosts/hostsfile)

Using system default hosts file
Reads the content of a file in the [hosts format](https://en.wikipedia.org/wiki/Hosts_(file)) into go structs for easy manipulation in go programs. When all changes are complete you can `Flush` the hosts file back to disk to save your changes. Supports an indexing system on both ips and hosts for quick management of large hosts files.

## Simple Usage
Simple usage reading in your system's hosts file and adding an entry for the ip `192.168.1.1` and the host `my-hostname`

```go
package main

import (
"log"

"github.com/goodhosts/hostsfile"
)

func main() {
hosts, err := hostsfile.NewHosts()
if err != nil {
log.Fatal(err.Error())
}
if err := hosts.Add("192.168.1.1", "my-hostname"); err != nil {
log.Fatal(err.Error())
}
if err := hosts.Flush(); err != nil {
log.Fatal(err.Error())
}
}
```
hfile, err := hostsfile.NewHosts()

### Other Usage
Read in a hosts file from a custom location which is not the system default, this is useful for tests or systems with non-standard hosts file locations.
```
hosts, err := hostsfile.NewCustomHosts("./my-custom-hostsfile")
```

Using a custom hostsfile at a specific location
Use `Add` to put an ip and host combination in the hosts file
```
hfile, err := hostsfile.NewCustomHosts("./my-custom-hostsfile")
err := hosts.Add("192.168.1.1", "my-hostname")
```

Add an ip entry with it's hosts
`Add` is variadic and can take multiple hosts to add for the same ip
```
err := hfile.Add("192.168.1.1", "my-hostname", "another-hostname")
err := hosts.Add("192.168.1.1", "my-hostname", "another-hostname")
```

Remove an ip/host combination
Use `Remove` to drop an ip and host combination from the hosts file
```
err := hfile.Remove("192.168.1.1", "another-hostname")
err := hosts.Remove("192.168.1.1", "my-hostname")
```

Flush the hostfile changes back to disk
`Remove` is variadic and can take multiple hosts to remove from the same ip
```
err := hfile.Flush()
err := hosts.Remove("192.168.1.1", "my-hostname", "another-hostname")
```

# Full API
Flush the hosts file changes back to disk
```
err := hosts.Flush()
```
type Hosts
func NewCustomHosts(osHostsFilePath string) (*Hosts, error)
func NewHosts() (*Hosts, error)
func (h *Hosts) Add(ip string, hosts ...string) error
func (h *Hosts) AddRaw(raw ...string) error
func (h *Hosts) Clean()
func (h *Hosts) Clear()
func (h *Hosts) Flush() error
func (h *Hosts) Has(ip string, host string) bool
func (h *Hosts) HasHostname(host string) bool
func (h *Hosts) HasIp(ip string) bool
func (h *Hosts) HostsPerLine(count int)
func (h *Hosts) IsWritable() bool
func (h *Hosts) Load() error
func (h *Hosts) Remove(ip string, hosts ...string) error
func (h *Hosts) RemoveByHostname(host string) error
func (h *Hosts) RemoveByIp(ip string) error
func (h *Hosts) RemoveDuplicateHosts()
func (h *Hosts) RemoveDuplicateIps()
func (h *Hosts) SortByIp()
func (h *Hosts) SortHosts()
type HostsLine
func NewHostsLine(raw string) HostsLine
func (l *HostsLine) Combine(hostline HostsLine)
func (l *HostsLine) HasComment() bool
func (l *HostsLine) IsComment() bool
func (l *HostsLine) IsMalformed() bool
func (l *HostsLine) IsValid() bool
func (l *HostsLine) RegenRaw()
func (l *HostsLine) RemoveDuplicateHosts()
func (l *HostsLine) SortHosts()
func (l *HostsLine) ToRaw() string
```
20 changes: 8 additions & 12 deletions hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import (
"github.com/dimchansky/utfbom"
)

// Hosts represents hosts file with the path and parsed contents of each line
type Hosts struct {
Path string
Lines []HostsLine
Path string // Path to the location of the hosts file that will be loaded/flushed
Lines []HostsLine // Slice containing all the lines parsed from the hosts file

ips lookup
hosts lookup
}
Expand Down Expand Up @@ -51,10 +53,8 @@ func NewCustomHosts(osHostsFilePath string) (*Hosts, error) {
func (h *Hosts) String() string {
buf := new(bytes.Buffer)
for _, line := range h.Lines {
if _, err := fmt.Fprintf(buf, "%s%s", line.ToRaw(), eol); err != nil {
// unlikely we will error during writing to a string buffer? maybe we dont need to do anything here
return err.Error()
}
// bytes buffers doesn't actually throw errors but the io.Writer interface requires it
fmt.Fprintf(buf, "%s%s", line.ToRaw(), eol)
}
return buf.String()
}
Expand Down Expand Up @@ -178,16 +178,12 @@ func (h *Hosts) Add(ip string, hosts ...string) error {
hostsCopy := h.Lines[position[0]].Hosts
for _, addHost := range hosts {
if h.Has(ip, addHost) {
// this combo already exists
continue
continue // this combo already exists
}

if !govalidator.IsDNSName(addHost) {
return fmt.Errorf("hostname is not a valid dns name: %s", addHost)
}
if itemInSliceString(addHost, hostsCopy) {
continue // host exists for ip already
}

hostsCopy = append(hostsCopy, addHost)
h.hosts.add(addHost, position[0])
Expand Down Expand Up @@ -367,7 +363,7 @@ func (h *Hosts) combineIP(ip string) {
for _, line := range lines {
if line.IP == ip {
// if you find the ip combine it into newline
newLine.Combine(line)
newLine.combine(line)
continue
}
// add everyone else
Expand Down
113 changes: 102 additions & 11 deletions hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package hostsfile

import (
"errors"
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"strings"
"sync"
"testing"

Expand All @@ -25,8 +27,8 @@ func randomString(n int) string {

func newHosts() *Hosts {
return &Hosts{
ips: lookup{l: make(map[string][]int)},
hosts: lookup{l: make(map[string][]int)},
ips: newLookup(),
hosts: newLookup(),
}
}

Expand Down Expand Up @@ -469,11 +471,18 @@ func TestHosts_Clean(t *testing.T) {

func TestHosts_Add(t *testing.T) {
hosts := newHosts()
assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) // valid use with variatic args
assert.Error(t, assert.AnError, hosts.Add("127.0.0.2", "host11 host12 host13 host14 host15 host16 host17 host18 hosts19 hosts20")) // invalid use

assert.Error(t, hosts.Add("badip", "hosts1"))
assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) // valid use with variatic args
assert.Len(t, hosts.Lines, 1)
assert.Nil(t, hosts.Add("127.0.0.3", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10"))
assert.Error(t, hosts.Add("127.0.0.2", "host11 host12 host13 host14 host15 host16 host17 host18 hosts19 hosts20")) // invalid use
assert.Len(t, hosts.Lines, 1)
assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10"))
assert.Len(t, hosts.Lines, 1)

// add the same hosts twice (should be noop with nothing new)
assert.Nil(t, hosts.Add("127.0.0.3", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10"))

assert.Error(t, assert.AnError, hosts.Add("127.0.0.3", "invalid hostname"))
assert.Error(t, assert.AnError, hosts.Add("127.0.0.3", ".invalid*hostname"))

Expand Down Expand Up @@ -510,6 +519,7 @@ func TestHosts_Add(t *testing.T) {
// add a new ip with 10 hosts, should remove first ip
assert.Nil(t, hosts.Add("127.0.0.3", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10"))
assert.False(t, hosts.HasIP("127.0.0.2"))
assert.False(t, hosts.HasIp("127.0.0.2"))
assert.Len(t, hosts.Lines, 1)
assert.Len(t, hosts.hosts.l, 10)
assert.Len(t, hosts.ips.l, 1)
Expand All @@ -525,6 +535,22 @@ func TestHosts_Add(t *testing.T) {
assert.Equal(t, expectedLines, hosts.Lines)
}

func TestHosts_AddRaw(t *testing.T) {
hosts := newHosts()

assert.Nil(t, hosts.AddRaw("127.0.0.1 yadda"))
assert.Len(t, hosts.Lines, 1)

assert.Nil(t, hosts.AddRaw("127.0.0.2 nada"))
assert.Len(t, hosts.Lines, 2)

assert.Nil(t, hosts.AddRaw("127.0.0.3 host1", "127.0.0.4 host2"))
assert.Len(t, hosts.Lines, 4)

assert.Error(t, hosts.AddRaw("badip host1")) // fail ip parse
assert.Error(t, hosts.AddRaw("127.0.0.1 host1%")) // fail host DNS validation
}

func TestHosts_HostsPerLine(t *testing.T) {
hosts := newHosts()
assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10"))
Expand Down Expand Up @@ -563,7 +589,26 @@ func TestHosts_HostsPerLine(t *testing.T) {
hosts.Clear()
assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10"))
hosts.HostsPerLine(8)
assert.Len(t, hosts.Lines, 2)
assert.Len(t, hosts.ips.l, 1)
assert.Len(t, hosts.hosts.l, 10)

hosts.HostsPerLine(0) // noop
assert.Len(t, hosts.Lines, 2)
assert.Len(t, hosts.ips.l, 1)
assert.Len(t, hosts.hosts.l, 10)

assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10"))

}

func BenchmarkHosts_Add(b *testing.B) {
for _, c := range []int{10000, 25000, 50000, 100000, 250000, 500000} {
b.Run(fmt.Sprintf("%d", c), func(b *testing.B) {
benchmarkHosts_Add(c, b)
// mem()
})
}
}

func BenchmarkHosts_Add10k(b *testing.B) {
Expand All @@ -583,7 +628,14 @@ func BenchmarkHosts_Add250k(b *testing.B) {
}

func benchmarkHosts_Add(c int, b *testing.B) {
hosts, err := NewCustomHosts("hostsfile")
fp := "hostsfile"
f, err := os.Create(fp)
assert.Nil(b, err)
defer func() {
assert.Nil(b, f.Close())
assert.Nil(b, os.Remove(fp))
}()
hosts, err := NewCustomHosts(fp)
assert.Nil(b, err)
for i := 0; i < c; i++ {
assert.Nil(b, hosts.Add(fake.IPv4(), randomString(63)))
Expand All @@ -606,10 +658,14 @@ func BenchmarkHosts_Flush500k(b *testing.B) {
benchmarkHosts_Flush(50, b)
}

// benchmarks flushing a hostsfile and confirms the hashmap lookup for ips/hosts is thread save via mutex + locking
// benchmarks flushing a hostsfile and confirms the hashmap lookup for ips/hosts is thread safe via mutex + locking
func benchmarkHosts_Flush(c int, b *testing.B) {
_, err := os.Create("hostsfile")
assert.Nil(b, err)
defer func() {
assert.Nil(b, os.Remove("hostsfile"))
}()

hosts, err := NewCustomHosts("hostsfile")
assert.Nil(b, err)

Expand All @@ -626,7 +682,6 @@ func benchmarkHosts_Flush(c int, b *testing.B) {
wg.Wait()

assert.Nil(b, hosts.Flush())
assert.Nil(b, os.Remove("hostsfile"))
}

func TestHosts_Flush(t *testing.T) {
Expand Down Expand Up @@ -696,9 +751,45 @@ func TestHosts_RemoveDuplicateHosts(t *testing.T) {

func TestHosts_CombineDuplicateIPs(t *testing.T) {
hosts := newHosts()
assert.Nil(t, hosts.loadString(`127.0.0.1 test1 test1 test2 test2`+eol+`127.0.0.1 test1 test1 test2 test2`+eol))
assert.Nil(t, hosts.loadString(`# comment`+eol+`127.0.0.1 test1 test1 test2 test2`+eol+`127.0.0.1 test1 test1 test2 test2`+eol))

hosts.CombineDuplicateIPs()
assert.Len(t, hosts.Lines, 1)
assert.Equal(t, "127.0.0.1 test1 test1 test1 test1 test2 test2 test2 test2"+eol, hosts.String())
assert.Len(t, hosts.Lines, 2)
assert.Equal(t, "# comment"+eol+"127.0.0.1 test1 test1 test1 test1 test2 test2 test2 test2"+eol, hosts.String())

// deprecated
hosts = newHosts()
assert.Nil(t, hosts.loadString(`# comment`+eol+`127.0.0.1 test1 test1 test2 test2`+eol+`127.0.0.1 test1 test1 test2 test2`+eol))
hosts.RemoveDuplicateIps()
assert.Len(t, hosts.Lines, 2)
assert.Equal(t, "# comment"+eol+"127.0.0.1 test1 test1 test1 test1 test2 test2 test2 test2"+eol, hosts.String())
}

func TestHosts_SortIPs(t *testing.T) {
hosts := newHosts()
assert.Nil(t, hosts.loadString(`# comment `+eol+`127.0.0.3 host3`+eol+`127.0.0.2 host2`+eol+`127.0.0.1 host1`+eol))

hosts.SortIPs()
assert.Len(t, hosts.Lines, 4)
assert.Equal(t, strings.Join([]string{
"# comment ",
"127.0.0.1 host1",
"127.0.0.2 host2",
"127.0.0.3 host3",
"",
}, eol), hosts.String())

// deprecated
hosts = newHosts()
assert.Nil(t, hosts.loadString(`# comment `+eol+`127.0.0.3 host3`+eol+`127.0.0.2 host2`+eol+`127.0.0.1 host1`+eol))

hosts.SortByIp()
assert.Len(t, hosts.Lines, 4)
assert.Equal(t, strings.Join([]string{
"# comment ",
"127.0.0.1 host1",
"127.0.0.2 host2",
"127.0.0.3 host3",
"",
}, eol), hosts.String())
}
Loading

0 comments on commit fb8a066

Please # to comment.