summaryrefslogtreecommitdiffstats
path: root/header.go
diff options
context:
space:
mode:
Diffstat (limited to 'header.go')
-rw-r--r--header.go365
1 files changed, 365 insertions, 0 deletions
diff --git a/header.go b/header.go
new file mode 100644
index 0000000..e185dca
--- /dev/null
+++ b/header.go
@@ -0,0 +1,365 @@
+package cnp
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "sort"
+ "strconv"
+)
+
+const (
+ // IntentOK represents the "ok" response intent.
+ IntentOK = "ok"
+ // IntentNotModified represents the "not_modified" response intent.
+ IntentNotModified = "not_modified"
+ // IntentError represents the "error" response intent.
+ IntentError = "error"
+ // IntentRedirect represents the "redirect" response intent.
+ IntentRedirect = "redirect"
+)
+
+// Header represents a CNP message header
+type Header struct {
+ // VersionMajor is the major CNP version given in the header.
+ VersionMajor int
+ // VersionMinor is the minor CNP version given in the header.
+ VersionMinor int
+ // Intent is the intent string of the message.
+ Intent string
+ // Parameters is a decoded map of the message parameters.
+ Parameters Parameters
+}
+
+// NewHeader creates a new CNP header from an intent and optional parameter
+// map.
+func NewHeader(intent string, params Parameters) Header {
+ if params == nil {
+ params = make(Parameters)
+ }
+ return Header{
+ VersionMajor: VersionMajor,
+ VersionMinor: VersionMinor,
+ Intent: intent,
+ Parameters: params,
+ }
+}
+
+// ParseHeader parses a CNP header from a bytestring.
+// The line parameter must be a single line that ends with a line feed.
+func ParseHeader(line []byte) (h Header, err error) {
+ if bytes.IndexByte(line, '\x00') >= 0 {
+ err = ErrorSyntax{"invalid header: NUL byte"}
+ return
+ }
+
+ if !bytes.HasPrefix(line, []byte("cnp/")) {
+ err = ErrorSyntax{"invalid header: no version string"}
+ return
+ }
+ line = line[len("cnp/"):]
+
+ var o int
+
+ h.VersionMajor, o, err = parseHeaderNumber(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+
+ if len(line) == 0 || line[0] != '.' {
+ err = ErrorSyntax{"invalid header: invalid version number tuple"}
+ return
+ }
+ line = line[1:]
+ h.VersionMinor, o, err = parseHeaderNumber(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+
+ if len(line) == 0 || line[0] != ' ' {
+ err = ErrorSyntax{"invalid header: expected a space"}
+ return
+ }
+ line = line[1:]
+
+ h.Intent, o, err = parseHeaderIntent(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+
+ h.Parameters, o, err = parseHeaderParameters(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+
+ if len(line) != 1 || line[0] != '\n' {
+ err = ErrorSyntax{"invalid header: expected line feed and end"}
+ return
+ }
+
+ return
+}
+
+func parseHeaderNumber(line []byte) (n int, length int, err error) {
+ i := 0
+ for ; i < len(line); i++ {
+ b := line[i]
+ if '0' <= b && b <= '9' {
+ } else {
+ break
+ }
+ }
+ n, err = strconv.Atoi(string(line[:i]))
+ if err != nil || (i > 1 && line[0] == '0') {
+ err = ErrorSyntax{"invalid header: invalid version number format"}
+ return
+ }
+ length = i
+ return
+}
+
+func parseHeaderIntent(line []byte) (intent string, length int, err error) {
+ p := bytes.IndexAny(line, " \n=\x00")
+ if p < 0 {
+ p = len(line) - 1
+ }
+ if p == 0 {
+ err = ErrorSyntax{"invalid header: empty intent"}
+ return
+ }
+ intent, err = Unescape(line[:p])
+ length = p
+ return
+}
+
+func parseHeaderParameters(line []byte) (params Parameters, length int, err error) {
+ params = make(Parameters)
+
+ for len(line) > 1 {
+ if line[0] == '\n' {
+ break
+ } else if line[0] != ' ' {
+ err = ErrorSyntax{"invalid header: expected a space"}
+ return
+ }
+ line = line[1:]
+ length++
+
+ var k, v string
+ var o int
+ k, v, o, err = parseHeaderParameter(line)
+ if err != nil {
+ return
+ }
+ if _, ok := params[k]; ok {
+ err = ErrorSyntax{"invalid header: duplicate parameter"}
+ return
+ }
+ params[k] = v
+ line = line[o:]
+ length += o
+ }
+
+ return
+}
+
+func parseHeaderParameter(line []byte) (key, value string, length int, err error) {
+ var o int
+ key, o, err = parseHeaderParamString(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+ length += o
+
+ if len(line) == 0 || line[0] != '=' {
+ err = ErrorSyntax{"invalid header: expected an equals sign"}
+ return
+ }
+ line = line[1:]
+ length++
+
+ value, o, err = parseHeaderParamString(line)
+ if err != nil {
+ return
+ }
+ length += o
+
+ return
+}
+
+func parseHeaderParamString(line []byte) (s string, length int, err error) {
+ for ; length < len(line); length++ {
+ b := line[length]
+ if b == '\n' || b == ' ' || b == '=' {
+ break
+ }
+ }
+ s, err = Unescape(line[:length])
+ return
+}
+
+// Version returns the message's CNP version as a "cnp/X.Y" string.
+func (h Header) Version() string {
+ return "cnp/" + strconv.Itoa(h.VersionMajor) + "." + strconv.Itoa(h.VersionMinor)
+}
+
+// Write writes the CNP message header line in the wire format.
+// The written line ends with a line feed.
+func (h Header) Write(w io.Writer) (err error) {
+ bw := bufio.NewWriter(w)
+ _, err = bw.WriteString(h.Version())
+ if err != nil {
+ return
+ }
+ err = bw.WriteByte(' ')
+ if err != nil {
+ return
+ }
+ _, err = bw.Write(Escape(h.Intent))
+ if err != nil {
+ return
+ }
+ err = h.Parameters.Write(bw)
+ if err != nil {
+ return
+ }
+ err = bw.WriteByte('\n')
+ if err != nil {
+ return
+ }
+ return bw.Flush()
+}
+
+func (h Header) String() string {
+ var buf bytes.Buffer
+ _ = h.Write(&buf)
+ return buf.String()
+}
+
+// Parameters represents CNP message parameter key=value pairs.
+type Parameters map[string]string
+
+// Write writes the parameters encoded for inclusion in the wire format.
+// Includes a leading space.
+func (p Parameters) Write(w io.Writer) (err error) {
+ bw := bufio.NewWriter(w)
+ keys := []string{}
+ for k := range p {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ err = bw.WriteByte(' ')
+ if err != nil {
+ return
+ }
+ _, err = bw.Write(Escape(k))
+ if err != nil {
+ return
+ }
+ err = bw.WriteByte('=')
+ if err != nil {
+ return
+ }
+ _, err = bw.Write(Escape(p[k]))
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
+// Escape CNP-escapes the bytestring s.
+func Escape(s string) []byte {
+ el := escapeLength(s)
+ if el == len(s) {
+ return []byte(s)
+ }
+ bs := make([]byte, el)
+ bi := 0
+ for i := 0; i < len(s); i++ {
+ switch s[i] {
+ case '\x00':
+ bs[bi] = '\\'
+ bs[bi+1] = '0'
+ bi += 2
+
+ case '\n':
+ bs[bi] = '\\'
+ bs[bi+1] = 'n'
+ bi += 2
+
+ case ' ':
+ bs[bi] = '\\'
+ bs[bi+1] = '_'
+ bi += 2
+
+ case '=':
+ bs[bi] = '\\'
+ bs[bi+1] = '-'
+ bi += 2
+
+ case '\\':
+ bs[bi] = '\\'
+ bs[bi+1] = '\\'
+ bi += 2
+
+ default:
+ bs[bi] = s[i]
+ bi++
+ }
+ }
+ return bs
+}
+
+func escapeLength(s string) (l int) {
+ for i := 0; i < len(s); i++ {
+ switch s[i] {
+ case '\x00', '\n', ' ', '=', '\\':
+ l += 2
+ default:
+ l++
+ }
+ }
+ return
+}
+
+// Unescape unescapes the bs from wire format into a bytestring.
+func Unescape(bs []byte) (string, error) {
+ buf := make([]byte, len(bs))
+ bi := 0
+
+ for i := 0; i < len(bs); i++ {
+ switch bs[i] {
+ case '\\':
+ i++
+ if i >= len(bs) {
+ return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: unexpected end of string"}
+ }
+ switch bs[i] {
+ case '0':
+ buf[bi] = '\x00'
+ case 'n':
+ buf[bi] = '\n'
+ case '_':
+ buf[bi] = ' '
+ case '-':
+ buf[bi] = '='
+ case '\\':
+ buf[bi] = '\\'
+ default:
+ return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: undefined sequence"}
+ }
+ default:
+ buf[bi] = bs[i]
+ }
+ bi++
+ }
+
+ return string(buf[:bi]), nil
+}