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 if p is nonempty. 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) } data := make([]byte, el) bi := 0 for i := 0; i < len(s); i++ { switch s[i] { case '\x00': data[bi] = '\\' data[bi+1] = '0' bi += 2 case '\n': data[bi] = '\\' data[bi+1] = 'n' bi += 2 case ' ': data[bi] = '\\' data[bi+1] = '_' bi += 2 case '=': data[bi] = '\\' data[bi+1] = '-' bi += 2 case '\\': data[bi] = '\\' data[bi+1] = '\\' bi += 2 default: data[bi] = s[i] bi++ } } return data } 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 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(data); i++ { switch data[i] { case '\\': i++ if i >= len(data) { return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: unexpected end of string"} } switch data[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] = data[i] } bi++ } return string(buf[:bi]), nil }