]> Sergey Matveev's repositories - btrtrc.git/blobdiff - iplist/iplist.go
Drop support for go 1.20
[btrtrc.git] / iplist / iplist.go
index 958af5a94d5fe00897373748e3164e73075cc9f8..d6d70a96256653224e9227f1fbfe6684a7bab28d 100644 (file)
@@ -1,13 +1,25 @@
+// Package iplist handles the P2P Plaintext Format described by
+// https://en.wikipedia.org/wiki/PeerGuardian#P2P_plaintext_format.
 package iplist
 
 import (
+       "bufio"
        "bytes"
        "errors"
        "fmt"
+       "io"
        "net"
        "sort"
 )
 
+// An abstraction of IP list implementations.
+type Ranger interface {
+       // Return a Range containing the IP.
+       Lookup(net.IP) (r Range, ok bool)
+       // If your ranges hurt, use this.
+       NumRanges() int
+}
+
 type IPList struct {
        ranges []Range
 }
@@ -17,8 +29,8 @@ type Range struct {
        Description string
 }
 
-func (r *Range) String() string {
-       return fmt.Sprintf("%s-%s (%s)", r.First, r.Last, r.Description)
+func (r Range) String() string {
+       return fmt.Sprintf("%s-%s: %s", r.First, r.Last, r.Description)
 }
 
 // Create a new IP list. The given ranges must already sorted by the lower
@@ -30,53 +42,75 @@ func New(initSorted []Range) *IPList {
        }
 }
 
-func (me *IPList) NumRanges() int {
-       if me == nil {
+func (ipl *IPList) NumRanges() int {
+       if ipl == nil {
                return 0
        }
-       return len(me.ranges)
+       return len(ipl.ranges)
 }
 
-// Return the range the given IP is in. Returns nil if no range is found.
-func (me *IPList) Lookup(ip net.IP) (r *Range) {
-       if me == nil {
-               return nil
+// Return the range the given IP is in. ok if false if no range is found.
+func (ipl *IPList) Lookup(ip net.IP) (r Range, ok bool) {
+       if ipl == nil {
+               return
        }
        // TODO: Perhaps all addresses should be converted to IPv6, if the future
        // of IP is to always be backwards compatible. But this will cost 4x the
        // memory for IPv4 addresses?
-       if v4 := ip.To4(); v4 != nil {
-               r = me.lookup(v4)
-               if r != nil {
+       v4 := ip.To4()
+       if v4 != nil {
+               r, ok = ipl.lookup(v4)
+               if ok {
                        return
                }
        }
-       if v6 := ip.To16(); v6 != nil {
-               return me.lookup(v6)
+       v6 := ip.To16()
+       if v6 != nil {
+               return ipl.lookup(v6)
        }
-       return nil
+       if v4 == nil && v6 == nil {
+               r = Range{
+                       Description: "bad IP",
+               }
+               ok = true
+       }
+       return
 }
 
-// Return the range the given IP is in. Returns nil if no range is found.
-func (me *IPList) lookup(ip net.IP) (r *Range) {
+// Return a range that contains ip, or nil.
+func lookup(
+       first func(i int) net.IP,
+       full func(i int) Range,
+       n int,
+       ip net.IP,
+) (
+       r Range, ok bool,
+) {
        // Find the index of the first range for which the following range exceeds
        // it.
-       i := sort.Search(len(me.ranges), func(i int) bool {
-               if i+1 >= len(me.ranges) {
+       i := sort.Search(n, func(i int) bool {
+               if i+1 >= n {
                        return true
                }
-               return bytes.Compare(ip, me.ranges[i+1].First) < 0
+               return bytes.Compare(ip, first(i+1)) < 0
        })
-       if i == len(me.ranges) {
+       if i == n {
                return
        }
-       r = &me.ranges[i]
-       if bytes.Compare(ip, r.First) < 0 || bytes.Compare(ip, r.Last) > 0 {
-               r = nil
-       }
+       r = full(i)
+       ok = bytes.Compare(r.First, ip) <= 0 && bytes.Compare(ip, r.Last) <= 0
        return
 }
 
+// Return the range the given IP is in. Returns nil if no range is found.
+func (ipl *IPList) lookup(ip net.IP) (Range, bool) {
+       return lookup(func(i int) net.IP {
+               return ipl.ranges[i].First
+       }, func(i int) Range {
+               return ipl.ranges[i]
+       }, len(ipl.ranges), ip)
+}
+
 func minifyIP(ip *net.IP) {
        v4 := ip.To4()
        if v4 != nil {
@@ -116,3 +150,36 @@ func ParseBlocklistP2PLine(l []byte) (r Range, ok bool, err error) {
        ok = true
        return
 }
+
+// Creates an IPList from a line-delimited P2P Plaintext file.
+func NewFromReader(f io.Reader) (ret *IPList, err error) {
+       var ranges []Range
+       // There's a lot of similar descriptions, so we maintain a pool and reuse
+       // them to reduce memory overhead.
+       uniqStrs := make(map[string]string)
+       scanner := bufio.NewScanner(f)
+       lineNum := 1
+       for scanner.Scan() {
+               r, ok, lineErr := ParseBlocklistP2PLine(scanner.Bytes())
+               if lineErr != nil {
+                       err = fmt.Errorf("error parsing line %d: %s", lineNum, lineErr)
+                       return
+               }
+               lineNum++
+               if !ok {
+                       continue
+               }
+               if s, ok := uniqStrs[r.Description]; ok {
+                       r.Description = s
+               } else {
+                       uniqStrs[r.Description] = r.Description
+               }
+               ranges = append(ranges, r)
+       }
+       err = scanner.Err()
+       if err != nil {
+               return
+       }
+       ret = New(ranges)
+       return
+}