naming/endpoint: escape address field

  'address' field of endpoint can be any string. Since we use '@' as a
  separator, 'address' with '@' cause a parse failure.
  This changes to escape '@' in 'address' with hex encoding.

  Assume that we don allow '@' in 'prototol' name.

Change-Id: Ic72038034fc5e56a90ff0ecab364054f3068f3d8
diff --git a/profiles/internal/naming/endpoint.go b/profiles/internal/naming/endpoint.go
index 74b1b2b..5190d82 100644
--- a/profiles/internal/naming/endpoint.go
+++ b/profiles/internal/naming/endpoint.go
@@ -117,7 +117,10 @@
 		ep.Protocol = naming.UnknownProtocol
 	}
 
-	ep.Address = parts[2]
+	var err error
+	if ep.Address, err = unescape(parts[2]); err != nil {
+		return fmt.Errorf("invalid address: %v", err)
+	}
 	if len(ep.Address) == 0 {
 		ep.Address = net.JoinHostPort("", "0")
 	}
@@ -126,7 +129,6 @@
 		return fmt.Errorf("invalid routing id: %v", err)
 	}
 
-	var err error
 	if ep.IsMountTable, ep.IsLeaf, err = parseMountTableFlag(parts[4]); err != nil {
 		return fmt.Errorf("invalid mount table flag: %v", err)
 	}
@@ -162,7 +164,7 @@
 		}
 		blessings := strings.Join(ep.Blessings, blessingsSeparator)
 		return fmt.Sprintf("@5@%s@%s@%s@%s@%s@@",
-			ep.Protocol, ep.Address, ep.RID, mt, blessings)
+			ep.Protocol, escape(ep.Address), ep.RID, mt, blessings)
 	}
 }
 
@@ -207,3 +209,72 @@
 func (a *addr) String() string {
 	return a.address
 }
+
+func escape(s string) string {
+	if !strings.ContainsAny(s, "%@") {
+		return s
+	}
+	t := make([]byte, len(s)*3)
+	j := 0
+	for i := 0; i < len(s); i++ {
+		switch c := s[i]; c {
+		case '@', '%':
+			t[j] = '%'
+			t[j+1] = "0123456789ABCDEF"[c>>4]
+			t[j+2] = "0123456789ABCDEF"[c&15]
+			j += 3
+		default:
+			t[j] = c
+			j++
+		}
+	}
+	return string(t[:j])
+}
+
+func ishex(c byte) bool {
+	switch {
+	case '0' <= c && c <= '9':
+		return true
+	case 'a' <= c && c <= 'f':
+		return true
+	case 'A' <= c && c <= 'F':
+		return true
+	}
+	return false
+}
+
+func unhex(c byte) byte {
+	switch {
+	case '0' <= c && c <= '9':
+		return c - '0'
+	case 'a' <= c && c <= 'f':
+		return c - 'a' + 10
+	case 'A' <= c && c <= 'F':
+		return c - 'A' + 10
+	}
+	return 0
+}
+
+func unescape(s string) (string, error) {
+	if !strings.Contains(s, "%") {
+		return s, nil
+	}
+	t := make([]byte, len(s))
+	j := 0
+	for i := 0; i < len(s); {
+		switch s[i] {
+		case '%':
+			if len(s) <= i+2 || !ishex(s[i+1]) || !ishex(s[i+2]) {
+				return s, fmt.Errorf("invalid escape %q", s)
+			}
+			t[j] = unhex(s[i+1])<<4 | unhex(s[i+2])
+			j++
+			i += 3
+		default:
+			t[j] = s[i]
+			j++
+			i++
+		}
+	}
+	return string(t[:j]), nil
+}
diff --git a/profiles/internal/naming/endpoint_test.go b/profiles/internal/naming/endpoint_test.go
index 6c4fdb6..f1d6d56 100644
--- a/profiles/internal/naming/endpoint_test.go
+++ b/profiles/internal/naming/endpoint_test.go
@@ -92,7 +92,7 @@
 		}
 		ep, err := NewEndpoint(test.String)
 		if err != nil {
-			t.Errorf("Test %d: Endpoint(%q) failed with %v", i, test.String, err)
+			t.Errorf("Test %d: NewEndpoint(%q) failed with %v", i, test.String, err)
 			continue
 		}
 		if !reflect.DeepEqual(ep, test.Endpoint) {
@@ -179,3 +179,25 @@
 		}
 	}
 }
+
+func TestEscapeEndpoint(t *testing.T) {
+	defver := defaultVersion
+	defer func() {
+		defaultVersion = defver
+	}()
+	testcases := []naming.Endpoint{
+		&Endpoint{Protocol: "unix", Address: "@", RID: naming.FixedRoutingID(0xdabbad00)},
+		&Endpoint{Protocol: "unix", Address: "@/%", RID: naming.FixedRoutingID(0xdabbad00)},
+	}
+	for i, ep := range testcases {
+		epstr := ep.String()
+		got, err := NewEndpoint(epstr)
+		if err != nil {
+			t.Errorf("Test %d: NewEndpoint(%q) failed with %v", i, epstr, err)
+			continue
+		}
+		if !reflect.DeepEqual(ep, got) {
+			t.Errorf("Test %d: Got endpoint %#v, want %#v", i, got, ep)
+		}
+	}
+}