From 9ca364d8753a5f2c7529c5b3dd7178bfd51effc6 Mon Sep 17 00:00:00 2001
From: clsr <clsr@clsr.net>
Date: Fri, 25 Aug 2017 17:04:42 +0200
Subject: Add support for the draft/cnp-select "select" request/response
 parameter

---
 cnp.go           |  2 +-
 common.go        | 24 ++++++++++++++++++++++++
 header.go        | 44 ++++++++++++++++++++++----------------------
 message.go       | 15 ++++++++-------
 request.go       | 30 +++++++++++++++++++++++++-----
 request_test.go  | 24 +++++++++++++++++++++++-
 response.go      | 37 ++++++++++++++++++++++++++++---------
 response_test.go | 23 +++++++++++++++++++++++
 server.go        |  6 +++---
 9 files changed, 157 insertions(+), 48 deletions(-)

diff --git a/cnp.go b/cnp.go
index 9ba8444..d5a3210 100644
--- a/cnp.go
+++ b/cnp.go
@@ -11,6 +11,6 @@ const (
 	// VersionMajor is the major CNP version (X in cnp/X.Y).
 	VersionMajor = 0
 
-	// VersionMinor is the major CNP version (Y in cnp/X.Y).
+	// VersionMinor is the minor CNP version (Y in cnp/X.Y).
 	VersionMinor = 3
 )
diff --git a/common.go b/common.go
index e6173b0..310da86 100644
--- a/common.go
+++ b/common.go
@@ -139,3 +139,27 @@ func setTime(m *Message, param string, t time.Time) {
 		m.SetParam(param, t.UTC().Format(time.RFC3339))
 	}
 }
+
+func getSelect(m *Message, param string) (string, string, error) {
+	s := m.Param(param)
+	if s == "" {
+		return "", "", nil
+	}
+	ss := strings.SplitN(s, ":", 2)
+	if len(ss) != 2 || ss[0] == "" {
+		return "", "", ErrorInvalid{"invalid parameter: " + param + " is not a valid selector"}
+	}
+	return ss[0], ss[1], nil
+}
+
+func setSelect(m *Message, param string, selector, query string) error {
+	if strings.ContainsRune(selector, ':') {
+		return ErrorInvalid{"invalid parameter: " + param + " is not a valid selector name"}
+	}
+	if selector == "" {
+		m.SetParam(param, "")
+	} else {
+		m.SetParam(param, selector+":"+query)
+	}
+	return nil
+}
diff --git a/header.go b/header.go
index e185dca..cb16db5 100644
--- a/header.go
+++ b/header.go
@@ -245,7 +245,7 @@ func (h Header) String() string {
 type Parameters map[string]string
 
 // Write writes the parameters encoded for inclusion in the wire format.
-// Includes a leading space.
+// Includes a leading space if p is nonempty.
 func (p Parameters) Write(w io.Writer) (err error) {
 	bw := bufio.NewWriter(w)
 	keys := []string{}
@@ -280,41 +280,41 @@ func Escape(s string) []byte {
 	if el == len(s) {
 		return []byte(s)
 	}
-	bs := make([]byte, el)
+	data := make([]byte, el)
 	bi := 0
 	for i := 0; i < len(s); i++ {
 		switch s[i] {
 		case '\x00':
-			bs[bi] = '\\'
-			bs[bi+1] = '0'
+			data[bi] = '\\'
+			data[bi+1] = '0'
 			bi += 2
 
 		case '\n':
-			bs[bi] = '\\'
-			bs[bi+1] = 'n'
+			data[bi] = '\\'
+			data[bi+1] = 'n'
 			bi += 2
 
 		case ' ':
-			bs[bi] = '\\'
-			bs[bi+1] = '_'
+			data[bi] = '\\'
+			data[bi+1] = '_'
 			bi += 2
 
 		case '=':
-			bs[bi] = '\\'
-			bs[bi+1] = '-'
+			data[bi] = '\\'
+			data[bi+1] = '-'
 			bi += 2
 
 		case '\\':
-			bs[bi] = '\\'
-			bs[bi+1] = '\\'
+			data[bi] = '\\'
+			data[bi+1] = '\\'
 			bi += 2
 
 		default:
-			bs[bi] = s[i]
+			data[bi] = s[i]
 			bi++
 		}
 	}
-	return bs
+	return data
 }
 
 func escapeLength(s string) (l int) {
@@ -329,19 +329,19 @@ func escapeLength(s string) (l int) {
 	return
 }
 
-// Unescape unescapes the bs from wire format into a bytestring.
-func Unescape(bs []byte) (string, error) {
-	buf := make([]byte, len(bs))
+// Unescape unescapes data from wire format into a bytestring.
+func Unescape(data []byte) (string, error) {
+	buf := make([]byte, len(data))
 	bi := 0
 
-	for i := 0; i < len(bs); i++ {
-		switch bs[i] {
+	for i := 0; i < len(data); i++ {
+		switch data[i] {
 		case '\\':
 			i++
-			if i >= len(bs) {
+			if i >= len(data) {
 				return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: unexpected end of string"}
 			}
-			switch bs[i] {
+			switch data[i] {
 			case '0':
 				buf[bi] = '\x00'
 			case 'n':
@@ -356,7 +356,7 @@ func Unescape(bs []byte) (string, error) {
 				return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: undefined sequence"}
 			}
 		default:
-			buf[bi] = bs[i]
+			buf[bi] = data[i]
 		}
 		bi++
 	}
diff --git a/message.go b/message.go
index eb3d2b9..fdb16b3 100644
--- a/message.go
+++ b/message.go
@@ -31,7 +31,7 @@ func NewMessage(intent string, body io.Reader) *Message {
 
 // ParseMessage parses a CNP message.
 //
-// The message's Body field is set to a bufio.Reader wrapping r. If r is a
+// The message's Body field is set to a bufio.Reader wrapping r. If r is an
 // io.Closer, it is also stored separately for usage with Message.Close().
 func ParseMessage(r io.Reader) (*Message, error) {
 	br := bufio.NewReader(r)
@@ -77,7 +77,7 @@ func (msg *Message) Close() error {
 
 // ComputeLength sets the length header parameter based on the message body.
 // First, msg.TryComputeLength() is attempted; if that fails, the request is
-// fully read into a buffer and msg.Body is set to a bytes.Reader.
+// fully read into a bytes.Buffer and msg.Body is set to it.
 func (msg *Message) ComputeLength() error {
 	if !msg.TryComputeLength() {
 		buf, err := ioutil.ReadAll(msg.Body)
@@ -94,9 +94,10 @@ func (msg *Message) ComputeLength() error {
 }
 
 // TryComputeLength sets the length header parameter to the length of the
-// message body if it's one of *bytes.Buffer, *bytes.Reader or *strings.Reader
-// and returns true. If msg.Body is nil, the length parameter is unset and the
-// function returns true. Otherwise, false is returned.
+// message body if the body's type is one of *bytes.Buffer, *bytes.Reader or
+// *strings.Reader and returns true. If msg.Body is nil, the length parameter
+// is unset and the function returns true. Otherwise, false is returned and the
+// length parameter remains unchanged.
 func (msg *Message) TryComputeLength() bool {
 	switch v := msg.Body.(type) {
 	case *bytes.Buffer:
@@ -131,13 +132,13 @@ func (msg *Message) Length() int64 {
 	return n
 }
 
-// Param retrieves a header parameter.
+// Param retrieves a header parameter. It performs no value validation.
 func (msg *Message) Param(key string) string {
 	return msg.Header.Parameters[key]
 }
 
 // SetParam sets a header parameter. If the value is empty, the parameter is
-// unset.
+// unset. It performs no value validation.
 func (msg *Message) SetParam(key, value string) {
 	if len(value) == 0 {
 		delete(msg.Header.Parameters, key)
diff --git a/request.go b/request.go
index 8c5a34e..0fb27a2 100644
--- a/request.go
+++ b/request.go
@@ -29,7 +29,7 @@ func NewRequest(host, pth string, body []byte) (*Request, error) {
 	return req, nil
 }
 
-// NewRequestURL creates a new Request from a URL and body data.
+// NewRequestURL creates a new Request from a URL and optional body data.
 func NewRequestURL(urlStr string, body []byte) (*Request, error) {
 	// XXX: handle //example.com/path URLs
 	if strings.HasPrefix(urlStr, "//") {
@@ -163,7 +163,7 @@ func (r *Request) Name() string {
 
 // SetName sets the name request parameter.
 //
-// An error is raised if the name includes characters not valid in a filename
+// Returns an error if the name includes characters not valid in a filename
 // (slash, null byte).
 func (r *Request) SetName(name string) error {
 	return setFilename(&r.Message, "name", name)
@@ -180,7 +180,7 @@ func (r *Request) Type() string {
 
 // SetType sets the type request parameter.
 //
-// An error is raised if typ is not a valid format for a MIME type.
+// Returns an error if typ is not a valid format for a MIME type.
 func (r *Request) SetType(typ string) error {
 	return setType(&r.Message, "type", typ)
 }
@@ -204,8 +204,23 @@ func (r *Request) SetIfModified(t time.Time) {
 	setTime(&r.Message, "if_modified", t)
 }
 
+// Select retrieves the select request parameter.
+//
+// If the parameter isn't a valid selector, empty strings are returned.
+func (r *Request) Select() (selector, query string) {
+	selector, query, _ = getSelect(&r.Message, "select")
+	return
+}
+
+// SetSelect sets the select request parameter.
+//
+// If the selector name is empty, the select parameter is unset.
+func (r *Request) SetSelect(selector, query string) error {
+	return setSelect(&r.Message, "select", selector, query)
+}
+
 // Validate validates the request header intent and parameter value format
-// (length, name, type, if_modified)
+// (length, name, type, if_modified, select)
 func (r *Request) Validate() error {
 	if err := validateRequestIntent(r.Intent()); err != nil {
 		return err
@@ -222,6 +237,9 @@ func (r *Request) Validate() error {
 	if _, err := getTime(&r.Message, "if_modified"); err != nil {
 		return err
 	}
+	if _, _, err := getSelect(&r.Message, "select"); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -250,7 +268,9 @@ func (r *Request) Write(w io.Writer) error {
 	return r.Message.Write(w)
 }
 
-// Clean cleans a CNP intent path.
+// Clean cleans a CNP request intent path.
+//
+// This works the same as path.Clean(), but preserves a trailing slash.
 func Clean(s string) string {
 	c := path.Clean(s)
 	if len(s) > 0 && len(c) > 0 && s[len(s)-1] == '/' && c[len(c)-1] != '/' {
diff --git a/request_test.go b/request_test.go
index bb4aa9b..23b8ac0 100644
--- a/request_test.go
+++ b/request_test.go
@@ -64,6 +64,10 @@ var requestTests = []requestTest{
 	{"", "/", Parameters{"if_modified": "12345-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
 	{"", "/", Parameters{"if_modified": "-5-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
 	{"", "/", Parameters{"if_modified": "-2005-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+	{"", "/", Parameters{"select": "w"}, nil, ErrorInvalid{}},
+	{"", "/", Parameters{"select": " "}, nil, ErrorInvalid{}},
+	{"", "/", Parameters{"select": ":"}, nil, ErrorInvalid{}},
+	{"", "/", Parameters{"select": ":foobar"}, nil, ErrorInvalid{}},
 
 	// valid simple requests
 	{"", "/", nil, nil, nil},
@@ -73,7 +77,7 @@ var requestTests = []requestTest{
 	{"example.com", "/ f=#\\oo///.././.../~/\x01/\xff/ba\nr", nil, nil, nil},
 
 	// valid request params
-	{"", "/", Parameters{"length": "", "name": "", "type": "", "if_modified": "", "": "", "q\x00we": "=a s\nd"}, nil, nil},
+	{"", "/", Parameters{"length": "", "name": "", "type": "", "if_modified": "", "select": "", "": "", "q\x00we": "=a s\nd"}, nil, nil},
 	{"", "/", Parameters{"length": "0"}, nil, nil},
 	{"", "/", Parameters{"length": "1"}, nil, nil},
 	{"", "/", Parameters{"length": "12345670089000000"}, nil, nil},
@@ -90,6 +94,10 @@ var requestTests = []requestTest{
 	{"", "/", Parameters{"if_modified": "0123-05-06T07:08:09Z"}, nil, nil},
 	{"", "/", Parameters{"if_modified": "0000-02-29T00:00:00Z"}, nil, nil},
 	{"", "/", Parameters{"if_modified": "2000-02-29T00:00:00Z"}, nil, nil},
+	{"", "/", Parameters{"select": "\x00:\x00"}, nil, nil},
+	{"", "/", Parameters{"select": "foo:bar:baz"}, nil, nil},
+	{"", "/", Parameters{"select": "byte:5-"}, nil, nil},
+	{"", "/", Parameters{"select": "cnm:#/foo/bar"}, nil, nil},
 }
 
 func TestNewRequest(t *testing.T) {
@@ -274,6 +282,20 @@ func TestRequestGetSet(t *testing.T) {
 					tm2 = req.IfModified().Format(time.RFC3339)
 				}
 				c(k, v, tst.v, tm2)
+			case "select":
+				if tst.v != nil {
+					continue
+				}
+				sel := ""
+				ss := strings.SplitN(v, ":", 2)
+				if len(ss) == 2 {
+					req.SetSelect(ss[0], ss[1])
+					a, b := req.Select()
+					if a != "" {
+						sel = a + ":" + b
+					}
+				}
+				c(k, v, tst.v, sel)
 			default:
 				req.SetParam(k, v)
 				c(k, v, tst.v, req.Param(k))
diff --git a/response.go b/response.go
index bf23f20..3ecb961 100644
--- a/response.go
+++ b/response.go
@@ -76,7 +76,7 @@ func (r *Response) Name() string {
 
 // SetName sets the name response parameter.
 //
-// An error is raised if the name includes characters not valid in a filename
+// Returns an error if the name includes characters not valid in a filename
 // (slash, null byte).
 func (r *Response) SetName(name string) error {
 	return setFilename(&r.Message, "name", name)
@@ -93,7 +93,7 @@ func (r *Response) Type() string {
 
 // SetType sets the type response parameter.
 //
-// An error is raised if typ is not a valid format for a MIME type.
+// Returns an error if typ is not a valid format for a MIME type.
 func (r *Response) SetType(typ string) error {
 	return setType(&r.Message, "type", typ)
 }
@@ -131,11 +131,11 @@ func (r *Response) Modified() time.Time {
 
 // SetModified sets the modified response parameter.
 //
-// If the time response parameter is empty, it's set to the current time.
-// If t is the zero time value, the modified parameter is unset.
+// If t is the zero time value, the modified parameter is unset. Otherwise, if
+// the time response parameter is empty, it's set to the current time.
 func (r *Response) SetModified(t time.Time) {
 	setTime(&r.Message, "modified", t)
-	if r.Time().IsZero() {
+	if !t.IsZero() && r.Time().IsZero() {
 		r.SetTime(time.Now())
 	}
 }
@@ -159,14 +159,15 @@ func (r *Response) Location() (host, path string, err error) {
 
 // SetLocation sets the location response parameter to host and path.
 //
-// If the host or path are invalid
+// Returns an error if the host or path are invalid.
 func (r *Response) SetLocation(host, path string) error {
+	err := ErrorInvalid{"invalid response: invalid location parameter"}
 	if strings.ContainsRune(host, '/') {
-		return ErrorInvalid{"invalid response: invalid location parameter"}
+		return err
 	}
 	l := host + path
 	if err := validateRequestIntent(l); err != nil {
-		return ErrorInvalid{"invalid response: invalid location parameter"}
+		return err
 	}
 	r.SetParam("location", l)
 	return nil
@@ -208,8 +209,23 @@ func (r *Response) SetReason(reason string) error {
 	return nil
 }
 
+// Select retrieves the select response parameter.
+//
+// If the parameter isn't a valid selector, empty strings are returned.
+func (r *Response) Select() (selector, query string) {
+	selector, query, _ = getSelect(&r.Message, "select")
+	return
+}
+
+// SetSelect sets the select response parameter.
+//
+// If the selector name is empty, the select parameter is unset.
+func (r *Response) SetSelect(selector, query string) error {
+	return setSelect(&r.Message, "select", selector, query)
+}
+
 // Validate validates the response intent and header parameter value format
-// (length, name, type, time, modified, location, reason)
+// (length, name, type, time, modified, location, reason, select)
 func (r *Response) Validate() error {
 	if !responseIntents[r.Intent()] {
 		return ErrorInvalid{"invalid response: unknown response intent"}
@@ -237,6 +253,9 @@ func (r *Response) Validate() error {
 	if !responseErrorReasons[r.Param("reason")] {
 		return ErrorInvalid{"invalid response: unknown error reason"}
 	}
+	if _, _, err := getSelect(&r.Message, "select"); err != nil {
+		return err
+	}
 	return nil
 }
 
diff --git a/response_test.go b/response_test.go
index 8033335..97e810b 100644
--- a/response_test.go
+++ b/response_test.go
@@ -101,6 +101,10 @@ var (
 		{"error", Parameters{"reason": "syntax\n"}, nil, ErrorInvalid{}},
 		{"error", Parameters{"reason": " server_error"}, nil, ErrorInvalid{}},
 		{"error", Parameters{"reason": "invalid "}, nil, ErrorInvalid{}},
+		{"ok", Parameters{"select": "w"}, nil, ErrorInvalid{}},
+		{"ok", Parameters{"select": " "}, nil, ErrorInvalid{}},
+		{"ok", Parameters{"select": ":"}, nil, ErrorInvalid{}},
+		{"ok", Parameters{"select": ":foobar"}, nil, ErrorInvalid{}},
 
 		// invalid: redirect *requires* the location parameter
 		{"redirect", nil, nil, ErrorInvalid{}},
@@ -163,6 +167,11 @@ var (
 		{"redirect", Parameters{"location": "/bar"}, nil, nil},
 		{"redirect", Parameters{"location": "[::1]:12345/ foo\n\x01\xff/"}, nil, nil},
 		{"redirect", Parameters{"location": "/../../////././.."}, nil, nil},
+
+		{"ok", Parameters{"select": "\x00:\x00"}, nil, nil},
+		{"ok", Parameters{"select": "foo:bar:baz"}, nil, nil},
+		{"ok", Parameters{"select": "byte:5-"}, nil, nil},
+		{"ok", Parameters{"select": "cnm:#/foo/bar"}, nil, nil},
 	}
 )
 
@@ -308,6 +317,20 @@ func TestResponseGetSet(t *testing.T) {
 			case "reason":
 				e(k, v, tst.v, resp.SetReason(v))
 				c(k, v, tst.v, resp.Reason())
+			case "select":
+				if tst.v != nil {
+					continue
+				}
+				sel := ""
+				ss := strings.SplitN(v, ":", 2)
+				if len(ss) == 2 {
+					resp.SetSelect(ss[0], ss[1])
+					a, b := resp.Select()
+					if a != "" {
+						sel = a + ":" + b
+					}
+				}
+				c(k, v, tst.v, sel)
 			default:
 				resp.SetParam(k, v)
 				c(k, v, tst.v, resp.Param(k))
diff --git a/server.go b/server.go
index 4320f41..6d23ddd 100644
--- a/server.go
+++ b/server.go
@@ -35,14 +35,14 @@ type Server struct {
 	Handler Handler
 
 	// Validate enables request parameter value validation; invalid requests
-	// are responded with errors.
+	// are responded to with errors.
 	Validate bool
 
 	sock net.Conn
 }
 
-// NewServer creates a new Server with default access and errors logs and sets
-// the listen address to "localhost".
+// NewServer creates a new Server with default access and errors loggers,
+// validation enabled and the listen address set to "localhost".
 func NewServer() *Server {
 	return &Server{
 		AccessLogger: log.New(os.Stdout, "", 0),
-- 
cgit