From da77deba78c8a7447b4a38324d2422a5df293b26 Mon Sep 17 00:00:00 2001 From: clsr Date: Fri, 18 Aug 2017 13:46:10 +0200 Subject: Initial commit --- header.go | 365 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 header.go (limited to 'header.go') 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 +} -- cgit