pkg/net: added package

Support:
- various IP types
- FQDNs

Also supports:
- use of single-param builtins as types
  (consistent with curried builtins).
- fixed bug in interpretation of top
- allow unexported functions to be used by builtins

Change-Id: I3568f2383510a904e00d4cf941c753efd03679ce
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2664
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/pkg/net/doc.go b/pkg/net/doc.go
new file mode 100644
index 0000000..1e7a237
--- /dev/null
+++ b/pkg/net/doc.go
@@ -0,0 +1,27 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package net provides net-related type definitions.
+//
+// The IP-related defintions can be represented as either a string or a list of
+// byte values. To allow one format over an other these types can be further
+// constraint using string or [...]. For instance,
+//
+//    // multicast defines a multicast IP address in string form.
+//    multicast: net.MulticastIP & string
+//
+//    // unicast defines a global unicast IP address in list form.
+//    unicast: net.GlobalUnicastIP & [...]
+//
+package net
diff --git a/pkg/net/host.go b/pkg/net/host.go
new file mode 100644
index 0000000..a690fac
--- /dev/null
+++ b/pkg/net/host.go
@@ -0,0 +1,105 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net
+
+import (
+	"fmt"
+	"net"
+	"strconv"
+	"unicode/utf8"
+
+	"cuelang.org/go/cue"
+	"golang.org/x/net/idna"
+)
+
+var idnaProfile = idna.New(
+	idna.ValidateLabels(true),
+	idna.VerifyDNSLength(true),
+	idna.StrictDomainName(true),
+)
+
+// SplitHostPort splits a network address of the form "host:port",
+// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or host%zone
+// and port.
+//
+// A literal IPv6 address in hostport must be enclosed in square brackets, as in
+// "[::1]:80", "[::1%lo0]:80".
+func SplitHostPort(s string) (hostport []string, err error) {
+	host, port, err := net.SplitHostPort(s)
+	if err != nil {
+		return nil, err
+	}
+	return []string{host, port}, nil
+}
+
+// JoinHostPort combines host and port into a network address of the
+// form "host:port". If host contains a colon, as found in literal
+// IPv6 addresses, then JoinHostPort returns "[host]:port".
+//
+// See func Dial for a description of the host and port parameters.
+func JoinHostPort(host, port cue.Value) (string, error) {
+	var err error
+	hostStr := ""
+	switch host.Kind() {
+	case cue.ListKind:
+		ipdata := netGetIP(host)
+		if len(ipdata) != 4 && len(ipdata) != 16 {
+			err = fmt.Errorf("invalid host %q", host)
+		}
+		hostStr = ipdata.String()
+	case cue.BytesKind:
+		var b []byte
+		b, err = host.Bytes()
+		hostStr = string(b)
+	default:
+		hostStr, err = host.String()
+	}
+	if err != nil {
+		return "", err
+	}
+
+	portStr := ""
+	switch port.Kind() {
+	case cue.StringKind:
+		portStr, err = port.String()
+	case cue.BytesKind:
+		var b []byte
+		b, err = port.Bytes()
+		portStr = string(b)
+	default:
+		var i int64
+		i, err = port.Int64()
+		portStr = strconv.Itoa(int(i))
+	}
+	if err != nil {
+		return "", err
+	}
+
+	return net.JoinHostPort(hostStr, portStr), nil
+}
+
+// FQDN reports whether is is a valid fully qualified domain name.
+//
+// FQDN allows only ASCII characters as prescribed by RFC 1034 (A-Z, a-z, 0-9
+// and the hyphen).
+func FQDN(s string) bool {
+	for i := 0; i < len(s); i++ {
+		if s[i] >= utf8.RuneSelf {
+			return false
+		}
+	}
+	_, err := idnaProfile.ToASCII(s)
+	return err == nil
+}
diff --git a/pkg/net/ip.go b/pkg/net/ip.go
new file mode 100644
index 0000000..6b658e3
--- /dev/null
+++ b/pkg/net/ip.go
@@ -0,0 +1,193 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package net defines net-related types.
+package net
+
+import (
+	"fmt"
+	"net"
+
+	"cuelang.org/go/cue"
+)
+
+// IP address lengths (bytes).
+const (
+	IPv4len = 4
+	IPv6len = 16
+)
+
+func netGetIP(ip cue.Value) (goip net.IP) {
+	switch ip.Kind() {
+	case cue.StringKind:
+		s, err := ip.String()
+		if err != nil {
+			return nil
+		}
+		goip := net.ParseIP(s)
+		if goip == nil {
+			return nil
+		}
+		return goip
+
+	case cue.BytesKind:
+		b, err := ip.Bytes()
+		if err != nil {
+			return nil
+		}
+		goip := net.ParseIP(string(b))
+		if goip == nil {
+			return nil
+		}
+		return goip
+
+	case cue.ListKind:
+		iter, err := ip.List()
+		if err != nil {
+			return nil
+		}
+		for iter.Next() {
+			v, err := iter.Value().Int64()
+			if err != nil {
+				return nil
+			}
+			if v < 0 || 255 < v {
+				return nil
+			}
+			goip = append(goip, byte(v))
+		}
+		return goip
+
+	default:
+		// TODO: return canonical invalid type.
+		return nil
+	}
+}
+
+// ParseIP parses s as an IP address, returning the result.
+// The string s can be in dotted decimal ("192.0.2.1")
+// or IPv6 ("2001:db8::68") form.
+// If s is not a valid textual representation of an IP address,
+// ParseIP returns nil.
+func ParseIP(s string) ([]uint, error) {
+	goip := net.ParseIP(s)
+	if goip == nil {
+		return nil, fmt.Errorf("invalid IP address %q", s)
+	}
+	return netToList(goip), nil
+}
+
+func netToList(ip net.IP) []uint {
+	a := make([]uint, len(ip))
+	for i, p := range ip {
+		a[i] = uint(p)
+	}
+	return a
+}
+
+// IPv4 reports whether s is a valid IPv4 address.
+//
+// The address may be a string or list of bytes.
+func IPv4(ip cue.Value) bool {
+	// TODO: convert to native CUE.
+	return netGetIP(ip).To4() != nil
+}
+
+// IP reports whether s is a valid IPv4 or IPv6 address.
+//
+// The address may be a string or list of bytes.
+func IP(ip cue.Value) bool {
+	// TODO: convert to native CUE.
+	return netGetIP(ip) != nil
+}
+
+// LoopbackIP reports whether ip is a loopback address.
+func LoopbackIP(ip cue.Value) bool {
+	return netGetIP(ip).IsLoopback()
+}
+
+// MulticastIP reports whether ip is a multicast address.
+func MulticastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsMulticast()
+}
+
+// InterfaceLocalMulticastIP reports whether ip is an interface-local multicast
+// address.
+func InterfaceLocalMulticastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsInterfaceLocalMulticast()
+}
+
+// LinkLocalMulticast reports whether ip is a link-local multicast address.
+func LinkLocalMulticastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsLinkLocalMulticast()
+}
+
+// LinkLocalUnicastIP reports whether ip is a link-local unicast address.
+func LinkLocalUnicastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsLinkLocalUnicast()
+}
+
+// GlobalUnicastIP reports whether ip is a global unicast address.
+//
+// The identification of global unicast addresses uses address type
+// identification as defined in RFC 1122, RFC 4632 and RFC 4291 with the
+// exception of IPv4 directed broadcast addresses. It returns true even if ip is
+// in IPv4 private address space or local IPv6 unicast address space.
+func GlobalUnicastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsGlobalUnicast()
+}
+
+// UnspecifiedIP reports whether ip is an unspecified address, either the IPv4
+// address "0.0.0.0" or the IPv6 address "::".
+func UnspecifiedIP(ip cue.Value) bool {
+	return netGetIP(ip).IsUnspecified()
+}
+
+// ToIP4 converts a given IP address, which may be a string or a list, to its
+// 4-byte representation.
+func ToIP4(ip cue.Value) ([]uint, error) {
+	ipdata := netGetIP(ip)
+	if ipdata == nil {
+		return nil, fmt.Errorf("invalid IP %q", ip)
+	}
+	ipv4 := ipdata.To4()
+	if ipv4 == nil {
+		return nil, fmt.Errorf("cannot convert %q to IPv4", ipdata)
+	}
+	return netToList(ipv4), nil
+}
+
+// ToIP16 converts a given IP address, which may be a string or a list, to its
+// 16-byte representation.
+func ToIP16(ip cue.Value) ([]uint, error) {
+	ipdata := netGetIP(ip)
+	if ipdata == nil {
+		return nil, fmt.Errorf("invalid IP %q", ip)
+	}
+	return netToList(ipdata), nil
+}
+
+// IPString returns the string form of the IP address ip. It returns one of 4 forms:
+//
+// - "<nil>", if ip has length 0
+// - dotted decimal ("192.0.2.1"), if ip is an IPv4 or IP4-mapped IPv6 address
+// - IPv6 ("2001:db8::1"), if ip is a valid IPv6 address
+// - the hexadecimal form of ip, without punctuation, if no other cases apply
+func IPString(ip cue.Value) (string, error) {
+	ipdata := netGetIP(ip)
+	if ipdata == nil {
+		return "", fmt.Errorf("invalid IP %q", ip)
+	}
+	return ipdata.String(), nil
+}