From 9c60aef6371c608d715f3ded92afa91007397c48 Mon Sep 17 00:00:00 2001
From: Ainar Garipov <a.garipov@adguard.com>
Date: Thu, 20 May 2021 14:22:06 +0300
Subject: [PATCH] Pull request: home: imp whois parse

Updates #2646.

Squashed commit of the following:

commit 0a5ff6ae74c532a296c0594a598a99c7cfaccf8c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu May 20 14:07:08 2021 +0300

    home: imp code

commit 2af0f463a77b81e827d9faca079a19c5437e1cd9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu May 20 13:48:08 2021 +0300

    home: imp whois parse
---
 internal/home/whois.go      | 107 ++++++++++++++++++++----------------
 internal/home/whois_test.go |  74 +++++++++++++++++++++++++
 2 files changed, 135 insertions(+), 46 deletions(-)

diff --git a/internal/home/whois.go b/internal/home/whois.go
index 7c77a903..78721edd 100644
--- a/internal/home/whois.go
+++ b/internal/home/whois.go
@@ -10,7 +10,6 @@ import (
 	"time"
 
 	"github.com/AdguardTeam/AdGuardHome/internal/aghio"
-	"github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
 	"github.com/AdguardTeam/golibs/cache"
 	"github.com/AdguardTeam/golibs/log"
 )
@@ -66,55 +65,71 @@ func trimValue(s string) string {
 	return s[:maxValueLength-3] + "..."
 }
 
-// Parse plain-text data from the response
-func whoisParse(data string) map[string]string {
-	m := map[string]string{}
-	descr := ""
-	netname := ""
-	for len(data) != 0 {
-		ln := aghstrings.SplitNext(&data, '\n')
-		if len(ln) == 0 || ln[0] == '#' || ln[0] == '%' {
-			continue
-		}
-
-		kv := strings.SplitN(ln, ":", 2)
-		if len(kv) != 2 {
-			continue
-		}
-		k := strings.TrimSpace(kv[0])
-		k = strings.ToLower(k)
-		v := strings.TrimSpace(kv[1])
-
-		switch k {
-		case "org-name":
-			m["orgname"] = trimValue(v)
-		case "city", "country", "orgname":
-			m[k] = trimValue(v)
-		case "descr":
-			if len(descr) == 0 {
-				descr = v
-			}
-		case "netname":
-			netname = v
-		case "whois": // "whois: whois.arin.net"
-			m["whois"] = v
-		case "referralserver": // "ReferralServer:  whois://whois.ripe.net"
-			if strings.HasPrefix(v, "whois://") {
-				m["whois"] = v[len("whois://"):]
-			}
+// coalesceStr returns the first non-empty string.
+//
+// TODO(a.garipov): Move to aghstrings?
+func coalesceStr(strs ...string) (res string) {
+	for _, s := range strs {
+		if s != "" {
+			return s
 		}
 	}
 
-	_, ok := m["orgname"]
-	if !ok {
-		// Set orgname from either descr or netname for the frontent.
-		//
-		// TODO(a.garipov): Perhaps don't do that in the V1 HTTP API?
-		if descr != "" {
-			m["orgname"] = trimValue(descr)
-		} else if netname != "" {
-			m["orgname"] = trimValue(netname)
+	return ""
+}
+
+// isWhoisComment returns true if the string is empty or is a WHOIS comment.
+func isWhoisComment(s string) (ok bool) {
+	return len(s) == 0 || s[0] == '#' || s[0] == '%'
+}
+
+// strmap is an alias for convenience.
+type strmap = map[string]string
+
+// whoisParse parses a subset of plain-text data from the WHOIS response into
+// a string map.
+func whoisParse(data string) (m strmap) {
+	m = strmap{}
+
+	var orgname string
+	lines := strings.Split(data, "\n")
+	for _, l := range lines {
+		if isWhoisComment(l) {
+			continue
 		}
+
+		kv := strings.SplitN(l, ":", 2)
+		if len(kv) != 2 {
+			continue
+		}
+
+		k := strings.ToLower(strings.TrimSpace(kv[0]))
+		v := strings.TrimSpace(kv[1])
+		if v == "" {
+			continue
+		}
+
+		switch k {
+		case "orgname", "org-name":
+			k = "orgname"
+			v = trimValue(v)
+			orgname = v
+		case "city", "country":
+			v = trimValue(v)
+		case "descr", "netname":
+			k = "orgname"
+			v = coalesceStr(orgname, v)
+			orgname = v
+		case "whois":
+			k = "whois"
+		case "referralserver":
+			k = "whois"
+			v = strings.TrimPrefix(v, "whois://")
+		default:
+			continue
+		}
+
+		m[k] = v
 	}
 
 	return m
diff --git a/internal/home/whois_test.go b/internal/home/whois_test.go
index 12511bbd..f87fa4ae 100644
--- a/internal/home/whois_test.go
+++ b/internal/home/whois_test.go
@@ -76,3 +76,77 @@ func TestWhois(t *testing.T) {
 	assert.Equal(t, "Imagiland", m["country"])
 	assert.Equal(t, "Nonreal", m["city"])
 }
+
+func TestWhoisParse(t *testing.T) {
+	const (
+		city    = "Nonreal"
+		country = "Imagiland"
+		orgname = "FakeOrgLLC"
+		whois   = "whois.example.net"
+	)
+
+	testCases := []struct {
+		want strmap
+		name string
+		in   string
+	}{{
+		want: strmap{},
+		name: "empty",
+		in:   ``,
+	}, {
+		want: strmap{},
+		name: "comments",
+		in:   "%\n#",
+	}, {
+		want: strmap{},
+		name: "no_colon",
+		in:   "city",
+	}, {
+		want: strmap{},
+		name: "no_value",
+		in:   "city:",
+	}, {
+		want: strmap{"city": city},
+		name: "city",
+		in:   `city: ` + city,
+	}, {
+		want: strmap{"country": country},
+		name: "country",
+		in:   `country: ` + country,
+	}, {
+		want: strmap{"orgname": orgname},
+		name: "orgname",
+		in:   `orgname: ` + orgname,
+	}, {
+		want: strmap{"orgname": orgname},
+		name: "orgname_hyphen",
+		in:   `org-name: ` + orgname,
+	}, {
+		want: strmap{"orgname": orgname},
+		name: "orgname_descr",
+		in:   `descr: ` + orgname,
+	}, {
+		want: strmap{"orgname": orgname},
+		name: "orgname_netname",
+		in:   `netname: ` + orgname,
+	}, {
+		want: strmap{"whois": whois},
+		name: "whois",
+		in:   `whois: ` + whois,
+	}, {
+		want: strmap{"whois": whois},
+		name: "referralserver",
+		in:   `referralserver: whois://` + whois,
+	}, {
+		want: strmap{},
+		name: "other",
+		in:   `other: value`,
+	}}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := whoisParse(tc.in)
+			assert.Equal(t, tc.want, got)
+		})
+	}
+}