pax_global_header00006660000000000000000000000064137636522000014516gustar00rootroot0000000000000052 comment=935b70b38dcb69f249f00f338774bf75822c090a httphead-0.1.0/000077500000000000000000000000001376365220000133155ustar00rootroot00000000000000httphead-0.1.0/LICENSE000066400000000000000000000020721376365220000143230ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017 Sergey Kamardin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. httphead-0.1.0/README.md000066400000000000000000000031071376365220000145750ustar00rootroot00000000000000# httphead.[go](https://golang.org) [![GoDoc][godoc-image]][godoc-url] > Tiny HTTP header value parsing library in go. ## Overview This library contains low-level functions for scanning HTTP RFC2616 compatible header value grammars. ## Install ```shell go get github.com/gobwas/httphead ``` ## Example The example below shows how multiple-choise HTTP header value could be parsed with this library: ```go options, ok := httphead.ParseOptions([]byte(`foo;bar=1,baz`), nil) fmt.Println(options, ok) // Output: [{foo map[bar:1]} {baz map[]}] true ``` The low-level example below shows how to optimize keys skipping and selection of some key: ```go // The right part of full header line like: // X-My-Header: key;foo=bar;baz,key;baz header := []byte(`foo;a=0,foo;a=1,foo;a=2,foo;a=3`) // We want to search key "foo" with an "a" parameter that equal to "2". var ( foo = []byte(`foo`) a = []byte(`a`) v = []byte(`2`) ) var found bool httphead.ScanOptions(header, func(i int, key, param, value []byte) Control { if !bytes.Equal(key, foo) { return ControlSkip } if !bytes.Equal(param, a) { if bytes.Equal(value, v) { // Found it! found = true return ControlBreak } return ControlSkip } return ControlContinue }) ``` For more usage examples please see [docs][godoc-url] or package tests. [godoc-image]: https://godoc.org/github.com/gobwas/httphead?status.svg [godoc-url]: https://godoc.org/github.com/gobwas/httphead [travis-image]: https://travis-ci.org/gobwas/httphead.svg?branch=master [travis-url]: https://travis-ci.org/gobwas/httphead httphead-0.1.0/cookie.go000066400000000000000000000111201376365220000151100ustar00rootroot00000000000000package httphead import ( "bytes" ) // ScanCookie scans cookie pairs from data using DefaultCookieScanner.Scan() // method. func ScanCookie(data []byte, it func(key, value []byte) bool) bool { return DefaultCookieScanner.Scan(data, it) } // DefaultCookieScanner is a CookieScanner which is used by ScanCookie(). // Note that it is intended to have the same behavior as http.Request.Cookies() // has. var DefaultCookieScanner = CookieScanner{} // CookieScanner contains options for scanning cookie pairs. // See https://tools.ietf.org/html/rfc6265#section-4.1.1 type CookieScanner struct { // DisableNameValidation disables name validation of a cookie. If false, // only RFC2616 "tokens" are accepted. DisableNameValidation bool // DisableValueValidation disables value validation of a cookie. If false, // only RFC6265 "cookie-octet" characters are accepted. // // Note that Strict option also affects validation of a value. // // If Strict is false, then scanner begins to allow space and comma // characters inside the value for better compatibility with non standard // cookies implementations. DisableValueValidation bool // BreakOnPairError sets scanner to immediately return after first pair syntax // validation error. // If false, scanner will try to skip invalid pair bytes and go ahead. BreakOnPairError bool // Strict enables strict RFC6265 mode scanning. It affects name and value // validation, as also some other rules. // If false, it is intended to bring the same behavior as // http.Request.Cookies(). Strict bool } // Scan maps data to name and value pairs. Usually data represents value of the // Cookie header. func (c CookieScanner) Scan(data []byte, it func(name, value []byte) bool) bool { lexer := &Scanner{data: data} const ( statePair = iota stateBefore ) state := statePair for lexer.Buffered() > 0 { switch state { case stateBefore: // Pairs separated by ";" and space, according to the RFC6265: // cookie-pair *( ";" SP cookie-pair ) // // Cookie pairs MUST be separated by (";" SP). So our only option // here is to fail as syntax error. a, b := lexer.Peek2() if a != ';' { return false } state = statePair advance := 1 if b == ' ' { advance++ } else if c.Strict { return false } lexer.Advance(advance) case statePair: if !lexer.FetchUntil(';') { return false } var value []byte name := lexer.Bytes() if i := bytes.IndexByte(name, '='); i != -1 { value = name[i+1:] name = name[:i] } else if c.Strict { if !c.BreakOnPairError { goto nextPair } return false } if !c.Strict { trimLeft(name) } if !c.DisableNameValidation && !ValidCookieName(name) { if !c.BreakOnPairError { goto nextPair } return false } if !c.Strict { value = trimRight(value) } value = stripQuotes(value) if !c.DisableValueValidation && !ValidCookieValue(value, c.Strict) { if !c.BreakOnPairError { goto nextPair } return false } if !it(name, value) { return true } nextPair: state = stateBefore } } return true } // ValidCookieValue reports whether given value is a valid RFC6265 // "cookie-octet" bytes. // // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E // ; US-ASCII characters excluding CTLs, // ; whitespace DQUOTE, comma, semicolon, // ; and backslash // // Note that the false strict parameter disables errors on space 0x20 and comma // 0x2c. This could be useful to bring some compatibility with non-compliant // clients/servers in the real world. // It acts the same as standard library cookie parser if strict is false. func ValidCookieValue(value []byte, strict bool) bool { if len(value) == 0 { return true } for _, c := range value { switch c { case '"', ';', '\\': return false case ',', ' ': if strict { return false } default: if c <= 0x20 { return false } if c >= 0x7f { return false } } } return true } // ValidCookieName reports wheter given bytes is a valid RFC2616 "token" bytes. func ValidCookieName(name []byte) bool { for _, c := range name { if !OctetTypes[c].IsToken() { return false } } return true } func stripQuotes(bts []byte) []byte { if last := len(bts) - 1; last > 0 && bts[0] == '"' && bts[last] == '"' { return bts[1:last] } return bts } func trimLeft(p []byte) []byte { var i int for i < len(p) && OctetTypes[p[i]].IsSpace() { i++ } return p[i:] } func trimRight(p []byte) []byte { j := len(p) for j > 0 && OctetTypes[p[j-1]].IsSpace() { j-- } return p[:j] } httphead-0.1.0/cookie_test.go000066400000000000000000000160511376365220000161570ustar00rootroot00000000000000package httphead import ( "bytes" "fmt" "net/http" "testing" ) type cookieTuple struct { name, value []byte } var cookieCases = []struct { label string in []byte ok bool exp []cookieTuple c CookieScanner }{ { label: "simple", in: []byte(`foo=bar`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, }, }, { label: "simple", in: []byte(`foo=bar; bar=baz`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, {[]byte(`bar`), []byte(`baz`)}, }, }, { label: "duplicate", in: []byte(`foo=bar; bar=baz; foo=bar`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, {[]byte(`bar`), []byte(`baz`)}, {[]byte(`foo`), []byte(`bar`)}, }, }, { label: "quoted", in: []byte(`foo="bar"`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, }, }, { label: "empty value", in: []byte(`foo=`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte{}}, }, }, { label: "empty value", in: []byte(`foo=; bar=baz`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte{}}, {[]byte(`bar`), []byte(`baz`)}, }, }, { label: "quote as value", in: []byte(`foo="; bar=baz`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte{'"'}}, {[]byte(`bar`), []byte(`baz`)}, }, c: CookieScanner{ DisableValueValidation: true, }, }, { label: "quote as value", in: []byte(`foo="; bar=baz`), ok: true, exp: []cookieTuple{ {[]byte(`bar`), []byte(`baz`)}, }, }, { label: "skip invalid key", in: []byte(`foo@example.com=1; bar=baz`), ok: true, exp: []cookieTuple{ {[]byte("bar"), []byte("baz")}, }, }, { label: "skip invalid value", in: []byte(`foo="1; bar=baz`), ok: true, exp: []cookieTuple{ {[]byte("bar"), []byte("baz")}, }, }, { label: "trailing semicolon", in: []byte(`foo=bar;`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, }, }, { label: "trailing semicolon strict", in: []byte(`foo=bar;`), ok: false, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, }, c: CookieScanner{ Strict: true, }, }, { label: "want space between", in: []byte(`foo=bar;bar=baz`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, {[]byte(`bar`), []byte(`baz`)}, }, }, { label: "want space between strict", in: []byte(`foo=bar;bar=baz`), ok: false, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, }, c: CookieScanner{ Strict: true, }, }, { label: "value single dquote", in: []byte(`foo="bar`), ok: true, }, { label: "value single dquote", in: []byte(`foo=bar"`), ok: true, }, { label: "value single dquote", in: []byte(`foo="bar`), ok: false, c: CookieScanner{ BreakOnPairError: true, }, }, { label: "value single dquote", in: []byte(`foo=bar"`), ok: false, c: CookieScanner{ BreakOnPairError: true, }, }, { label: "value whitespace", in: []byte(`foo=bar `), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`bar`)}, }, }, { label: "value whitespace strict", in: []byte(`foo=bar `), ok: false, c: CookieScanner{ Strict: true, BreakOnPairError: true, }, }, { label: "value whitespace", in: []byte(`foo=b ar`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`b ar`)}, }, }, { label: "value whitespace strict", in: []byte(`foo=b ar`), ok: false, c: CookieScanner{ Strict: true, BreakOnPairError: true, }, }, { label: "value whitespace strict", in: []byte(`foo= bar`), ok: false, c: CookieScanner{ Strict: true, BreakOnPairError: true, }, }, { label: "value quoted whitespace", in: []byte(`foo="b ar"`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(`b ar`)}, }, }, { label: "value quoted whitespace strict", in: []byte(`foo="b ar"`), c: CookieScanner{ Strict: true, BreakOnPairError: true, }, }, { label: "parse ok without values", in: []byte(`foo;bar;baz=10`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(``)}, {[]byte(`bar`), []byte(``)}, {[]byte(`baz`), []byte(`10`)}, }, c: CookieScanner{ Strict: false, }, }, { label: "strict parse ok without values", in: []byte(`foo; bar; baz=10`), ok: true, exp: []cookieTuple{ {[]byte(`baz`), []byte(`10`)}, }, c: CookieScanner{ Strict: true, }, }, { label: "parse ok without values", in: []byte(`foo;`), ok: true, exp: []cookieTuple{ {[]byte(`foo`), []byte(``)}, }, c: CookieScanner{ Strict: false, }, }, { label: "strict parse err without values", in: []byte(`foo;`), ok: false, exp: []cookieTuple{}, c: CookieScanner{ Strict: true, }, }, } func TestScanCookie(t *testing.T) { for _, test := range cookieCases { t.Run(test.label, func(t *testing.T) { var act []cookieTuple ok := test.c.Scan(test.in, func(k, v []byte) bool { act = append(act, cookieTuple{k, v}) return true }) if ok != test.ok { t.Errorf("unexpected result: %v; want %v", ok, test.ok) } if an, en := len(act), len(test.exp); an != en { t.Errorf("unexpected length of result: %d; want %d", an, en) } else { for i, ev := range test.exp { if av := act[i]; !bytes.Equal(av.name, ev.name) || !bytes.Equal(av.value, ev.value) { t.Errorf( "unexpected %d-th tuple: %#q=%#q; want %#q=%#q", i, string(av.name), string(av.value), string(ev.name), string(ev.value), ) } } } if test.c != DefaultCookieScanner { return } // Compare with standard library. req := http.Request{ Header: http.Header{ "Cookie": []string{string(test.in)}, }, } std := req.Cookies() if an, sn := len(act), len(std); an != sn { t.Errorf("length of result: %d; standard lib returns %d; details:\n%s", an, sn, dumpActStd(act, std)) } else { for i := 0; i < an; i++ { if a, s := act[i], std[i]; string(a.name) != s.Name || string(a.value) != s.Value { t.Errorf("%d-th cookie not equal:\n%s", i, dumpActStd(act, std)) break } } } }) } } func BenchmarkScanCookie(b *testing.B) { for _, test := range cookieCases { b.Run(test.label, func(b *testing.B) { for i := 0; i < b.N; i++ { test.c.Scan(test.in, func(_, _ []byte) bool { return true }) } }) if test.c == DefaultCookieScanner { b.Run(test.label+"_std", func(b *testing.B) { r := http.Request{ Header: http.Header{ "Cookie": []string{string(test.in)}, }, } for i := 0; i < b.N; i++ { _ = r.Cookies() } }) } } } func dumpActStd(act []cookieTuple, std []*http.Cookie) string { var buf bytes.Buffer fmt.Fprintf(&buf, "actual:\n") for i, p := range act { fmt.Fprintf(&buf, "\t#%d: %#q=%#q\n", i, p.name, p.value) } fmt.Fprintf(&buf, "standard:\n") for i, c := range std { fmt.Fprintf(&buf, "\t#%d: %#q=%#q\n", i, c.Name, c.Value) } return buf.String() } httphead-0.1.0/go.mod000066400000000000000000000000531376365220000144210ustar00rootroot00000000000000module github.com/gobwas/httphead go 1.15 httphead-0.1.0/head.go000066400000000000000000000140611376365220000145470ustar00rootroot00000000000000package httphead import ( "bufio" "bytes" ) // Version contains protocol major and minor version. type Version struct { Major int Minor int } // RequestLine contains parameters parsed from the first request line. type RequestLine struct { Method []byte URI []byte Version Version } // ResponseLine contains parameters parsed from the first response line. type ResponseLine struct { Version Version Status int Reason []byte } // SplitRequestLine splits given slice of bytes into three chunks without // parsing. func SplitRequestLine(line []byte) (method, uri, version []byte) { return split3(line, ' ') } // ParseRequestLine parses http request line like "GET / HTTP/1.0". func ParseRequestLine(line []byte) (r RequestLine, ok bool) { var i int for i = 0; i < len(line); i++ { c := line[i] if !OctetTypes[c].IsToken() { if i > 0 && c == ' ' { break } return } } if i == len(line) { return } var proto []byte r.Method = line[:i] r.URI, proto = split2(line[i+1:], ' ') if len(r.URI) == 0 { return } if major, minor, ok := ParseVersion(proto); ok { r.Version.Major = major r.Version.Minor = minor return r, true } return r, false } // SplitResponseLine splits given slice of bytes into three chunks without // parsing. func SplitResponseLine(line []byte) (version, status, reason []byte) { return split3(line, ' ') } // ParseResponseLine parses first response line into ResponseLine struct. func ParseResponseLine(line []byte) (r ResponseLine, ok bool) { var ( proto []byte status []byte ) proto, status, r.Reason = split3(line, ' ') if major, minor, ok := ParseVersion(proto); ok { r.Version.Major = major r.Version.Minor = minor } else { return r, false } if n, ok := IntFromASCII(status); ok { r.Status = n } else { return r, false } // TODO(gobwas): parse here r.Reason fot TEXT rule: // TEXT = return r, true } var ( httpVersion10 = []byte("HTTP/1.0") httpVersion11 = []byte("HTTP/1.1") httpVersionPrefix = []byte("HTTP/") ) // ParseVersion parses major and minor version of HTTP protocol. // It returns parsed values and true if parse is ok. func ParseVersion(bts []byte) (major, minor int, ok bool) { switch { case bytes.Equal(bts, httpVersion11): return 1, 1, true case bytes.Equal(bts, httpVersion10): return 1, 0, true case len(bts) < 8: return case !bytes.Equal(bts[:5], httpVersionPrefix): return } bts = bts[5:] dot := bytes.IndexByte(bts, '.') if dot == -1 { return } major, ok = IntFromASCII(bts[:dot]) if !ok { return } minor, ok = IntFromASCII(bts[dot+1:]) if !ok { return } return major, minor, true } // ReadLine reads line from br. It reads until '\n' and returns bytes without // '\n' or '\r\n' at the end. // It returns err if and only if line does not end in '\n'. Note that read // bytes returned in any case of error. // // It is much like the textproto/Reader.ReadLine() except the thing that it // returns raw bytes, instead of string. That is, it avoids copying bytes read // from br. // // textproto/Reader.ReadLineBytes() is also makes copy of resulting bytes to be // safe with future I/O operations on br. // // We could control I/O operations on br and do not need to make additional // copy for safety. func ReadLine(br *bufio.Reader) ([]byte, error) { var line []byte for { bts, err := br.ReadSlice('\n') if err == bufio.ErrBufferFull { // Copy bytes because next read will discard them. line = append(line, bts...) continue } // Avoid copy of single read. if line == nil { line = bts } else { line = append(line, bts...) } if err != nil { return line, err } // Size of line is at least 1. // In other case bufio.ReadSlice() returns error. n := len(line) // Cut '\n' or '\r\n'. if n > 1 && line[n-2] == '\r' { line = line[:n-2] } else { line = line[:n-1] } return line, nil } } // ParseHeaderLine parses HTTP header as key-value pair. It returns parsed // values and true if parse is ok. func ParseHeaderLine(line []byte) (k, v []byte, ok bool) { colon := bytes.IndexByte(line, ':') if colon == -1 { return } k = trim(line[:colon]) for _, c := range k { if !OctetTypes[c].IsToken() { return nil, nil, false } } v = trim(line[colon+1:]) return k, v, true } // IntFromASCII converts ascii encoded decimal numeric value from HTTP entities // to an integer. func IntFromASCII(bts []byte) (ret int, ok bool) { // ASCII numbers all start with the high-order bits 0011. // If you see that, and the next bits are 0-9 (0000 - 1001) you can grab those // bits and interpret them directly as an integer. var n int if n = len(bts); n < 1 { return 0, false } for i := 0; i < n; i++ { if bts[i]&0xf0 != 0x30 { return 0, false } ret += int(bts[i]&0xf) * pow(10, n-i-1) } return ret, true } const ( toLower = 'a' - 'A' // for use with OR. toUpper = ^byte(toLower) // for use with AND. ) // CanonicalizeHeaderKey is like standard textproto/CanonicalMIMEHeaderKey, // except that it operates with slice of bytes and modifies it inplace without // copying. func CanonicalizeHeaderKey(k []byte) { upper := true for i, c := range k { if upper && 'a' <= c && c <= 'z' { k[i] &= toUpper } else if !upper && 'A' <= c && c <= 'Z' { k[i] |= toLower } upper = c == '-' } } // pow for integers implementation. // See Donald Knuth, The Art of Computer Programming, Volume 2, Section 4.6.3 func pow(a, b int) int { p := 1 for b > 0 { if b&1 != 0 { p *= a } b >>= 1 a *= a } return p } func split3(p []byte, sep byte) (p1, p2, p3 []byte) { a := bytes.IndexByte(p, sep) b := bytes.IndexByte(p[a+1:], sep) if a == -1 || b == -1 { return p, nil, nil } b += a + 1 return p[:a], p[a+1 : b], p[b+1:] } func split2(p []byte, sep byte) (p1, p2 []byte) { i := bytes.IndexByte(p, sep) if i == -1 { return p, nil } return p[:i], p[i+1:] } func trim(p []byte) []byte { var i, j int for i = 0; i < len(p) && (p[i] == ' ' || p[i] == '\t'); { i++ } for j = len(p); j > i && (p[j-1] == ' ' || p[j-1] == '\t'); { j-- } return p[i:j] } httphead-0.1.0/head_test.go000066400000000000000000000062251376365220000156110ustar00rootroot00000000000000package httphead import ( "bytes" "testing" ) func TestParseRequestLine(t *testing.T) { for _, test := range []struct { name string line string exp RequestLine fail bool }{ { line: "", fail: true, }, { line: "GET", fail: true, }, { line: "GET ", fail: true, }, { line: "GET ", fail: true, }, { line: "GET ", fail: true, }, { line: "GET / HTTP/1.1", exp: RequestLine{ Method: []byte("GET"), URI: []byte("/"), Version: Version{1, 1}, }, }, } { t.Run(test.name, func(t *testing.T) { r, ok := ParseRequestLine([]byte(test.line)) if test.fail && ok { t.Fatalf("unexpected successful parsing") } if !test.fail && !ok { t.Fatalf("unexpected parse error") } if test.fail { return } if act, exp := r.Method, test.exp.Method; !bytes.Equal(act, exp) { t.Errorf("unexpected parsed method: %q; want %q", act, exp) } if act, exp := r.URI, test.exp.URI; !bytes.Equal(act, exp) { t.Errorf("unexpected parsed uri: %q; want %q", act, exp) } if act, exp := r.Version, test.exp.Version; act != exp { t.Errorf("unexpected parsed version: %+v; want %+v", act, exp) } }) } } func TestParseResponseLine(t *testing.T) { for _, test := range []struct { name string line string exp ResponseLine fail bool }{ { line: "", fail: true, }, { line: "HTTP/1.1", fail: true, }, { line: "HTTP/1.1 ", fail: true, }, { line: "HTTP/1.1 ", fail: true, }, { line: "HTTP/1.1 ", fail: true, }, { line: "HTTP/1.1 200 OK", exp: ResponseLine{ Version: Version{1, 1}, Status: 200, Reason: []byte("OK"), }, }, } { t.Run(test.name, func(t *testing.T) { r, ok := ParseResponseLine([]byte(test.line)) if test.fail && ok { t.Fatalf("unexpected successful parsing") } if !test.fail && !ok { t.Fatalf("unexpected parse error") } if test.fail { return } if act, exp := r.Version, test.exp.Version; act != exp { t.Errorf("unexpected parsed version: %+v; want %+v", act, exp) } if act, exp := r.Status, test.exp.Status; act != exp { t.Errorf("unexpected parsed status: %d; want %d", act, exp) } if act, exp := r.Reason, test.exp.Reason; !bytes.Equal(act, exp) { t.Errorf("unexpected parsed reason: %q; want %q", act, exp) } }) } } var versionCases = []struct { in []byte major int minor int ok bool }{ {[]byte("HTTP/1.1"), 1, 1, true}, {[]byte("HTTP/1.0"), 1, 0, true}, {[]byte("HTTP/1.2"), 1, 2, true}, {[]byte("HTTP/42.1092"), 42, 1092, true}, } func TestParseHttpVersion(t *testing.T) { for _, c := range versionCases { t.Run(string(c.in), func(t *testing.T) { major, minor, ok := ParseVersion(c.in) if major != c.major || minor != c.minor || ok != c.ok { t.Errorf( "parseHttpVersion([]byte(%q)) = %v, %v, %v; want %v, %v, %v", string(c.in), major, minor, ok, c.major, c.minor, c.ok, ) } }) } } func BenchmarkParseHttpVersion(b *testing.B) { for _, c := range versionCases { b.Run(string(c.in), func(b *testing.B) { for i := 0; i < b.N; i++ { _, _, _ = ParseVersion(c.in) } }) } } httphead-0.1.0/httphead.go000066400000000000000000000172671376365220000154620ustar00rootroot00000000000000// Package httphead contains utils for parsing HTTP and HTTP-grammar compatible // text protocols headers. // // That is, this package first aim is to bring ability to easily parse // constructions, described here https://tools.ietf.org/html/rfc2616#section-2 package httphead import ( "bytes" "strings" ) // ScanTokens parses data in this form: // // list = 1#token // // It returns false if data is malformed. func ScanTokens(data []byte, it func([]byte) bool) bool { lexer := &Scanner{data: data} var ok bool for lexer.Next() { switch lexer.Type() { case ItemToken: ok = true if !it(lexer.Bytes()) { return true } case ItemSeparator: if !isComma(lexer.Bytes()) { return false } default: return false } } return ok && !lexer.err } // ParseOptions parses all header options and appends it to given slice of // Option. It returns flag of successful (wellformed input) parsing. // // Note that appended options are all consist of subslices of data. That is, // mutation of data will mutate appended options. func ParseOptions(data []byte, options []Option) ([]Option, bool) { var i int index := -1 return options, ScanOptions(data, func(idx int, name, attr, val []byte) Control { if idx != index { index = idx i = len(options) options = append(options, Option{Name: name}) } if attr != nil { options[i].Parameters.Set(attr, val) } return ControlContinue }) } // SelectFlag encodes way of options selection. type SelectFlag byte // String represetns flag as string. func (f SelectFlag) String() string { var flags [2]string var n int if f&SelectCopy != 0 { flags[n] = "copy" n++ } if f&SelectUnique != 0 { flags[n] = "unique" n++ } return "[" + strings.Join(flags[:n], "|") + "]" } const ( // SelectCopy causes selector to copy selected option before appending it // to resulting slice. // If SelectCopy flag is not passed to selector, then appended options will // contain sub-slices of the initial data. SelectCopy SelectFlag = 1 << iota // SelectUnique causes selector to append only not yet existing option to // resulting slice. Unique is checked by comparing option names. SelectUnique ) // OptionSelector contains configuration for selecting Options from header value. type OptionSelector struct { // Check is a filter function that applied to every Option that possibly // could be selected. // If Check is nil all options will be selected. Check func(Option) bool // Flags contains flags for options selection. Flags SelectFlag // Alloc used to allocate slice of bytes when selector is configured with // SelectCopy flag. It will be called with number of bytes needed for copy // of single Option. // If Alloc is nil make is used. Alloc func(n int) []byte } // Select parses header data and appends it to given slice of Option. // It also returns flag of successful (wellformed input) parsing. func (s OptionSelector) Select(data []byte, options []Option) ([]Option, bool) { var current Option var has bool index := -1 alloc := s.Alloc if alloc == nil { alloc = defaultAlloc } check := s.Check if check == nil { check = defaultCheck } ok := ScanOptions(data, func(idx int, name, attr, val []byte) Control { if idx != index { if has && check(current) { if s.Flags&SelectCopy != 0 { current = current.Copy(alloc(current.Size())) } options = append(options, current) has = false } if s.Flags&SelectUnique != 0 { for i := len(options) - 1; i >= 0; i-- { if bytes.Equal(options[i].Name, name) { return ControlSkip } } } index = idx current = Option{Name: name} has = true } if attr != nil { current.Parameters.Set(attr, val) } return ControlContinue }) if has && check(current) { if s.Flags&SelectCopy != 0 { current = current.Copy(alloc(current.Size())) } options = append(options, current) } return options, ok } func defaultAlloc(n int) []byte { return make([]byte, n) } func defaultCheck(Option) bool { return true } // Control represents operation that scanner should perform. type Control byte const ( // ControlContinue causes scanner to continue scan tokens. ControlContinue Control = iota // ControlBreak causes scanner to stop scan tokens. ControlBreak // ControlSkip causes scanner to skip current entity. ControlSkip ) // ScanOptions parses data in this form: // // values = 1#value // value = token *( ";" param ) // param = token [ "=" (token | quoted-string) ] // // It calls given callback with the index of the option, option itself and its // parameter (attribute and its value, both could be nil). Index is useful when // header contains multiple choises for the same named option. // // Given callback should return one of the defined Control* values. // ControlSkip means that passed key is not in caller's interest. That is, all // parameters of that key will be skipped. // ControlBreak means that no more keys and parameters should be parsed. That // is, it must break parsing immediately. // ControlContinue means that caller want to receive next parameter and its // value or the next key. // // It returns false if data is malformed. func ScanOptions(data []byte, it func(index int, option, attribute, value []byte) Control) bool { lexer := &Scanner{data: data} var ok bool var state int const ( stateKey = iota stateParamBeforeName stateParamName stateParamBeforeValue stateParamValue ) var ( index int key, param, value []byte mustCall bool ) for lexer.Next() { var ( call bool growIndex int ) t := lexer.Type() v := lexer.Bytes() switch t { case ItemToken: switch state { case stateKey, stateParamBeforeName: key = v state = stateParamBeforeName mustCall = true case stateParamName: param = v state = stateParamBeforeValue mustCall = true case stateParamValue: value = v state = stateParamBeforeName call = true default: return false } case ItemString: if state != stateParamValue { return false } value = v state = stateParamBeforeName call = true case ItemSeparator: switch { case isComma(v) && state == stateKey: // Nothing to do. case isComma(v) && state == stateParamBeforeName: state = stateKey // Make call only if we have not called this key yet. call = mustCall if !call { // If we have already called callback with the key // that just ended. index++ } else { // Else grow the index after calling callback. growIndex = 1 } case isComma(v) && state == stateParamBeforeValue: state = stateKey growIndex = 1 call = true case isSemicolon(v) && state == stateParamBeforeName: state = stateParamName case isSemicolon(v) && state == stateParamBeforeValue: state = stateParamName call = true case isEquality(v) && state == stateParamBeforeValue: state = stateParamValue default: return false } default: return false } if call { switch it(index, key, param, value) { case ControlBreak: // User want to stop to parsing parameters. return true case ControlSkip: // User want to skip current param. state = stateKey lexer.SkipEscaped(',') case ControlContinue: // User is interested in rest of parameters. // Nothing to do. default: panic("unexpected control value") } ok = true param = nil value = nil mustCall = false index += growIndex } } if mustCall { ok = true it(index, key, param, value) } return ok && !lexer.err } func isComma(b []byte) bool { return len(b) == 1 && b[0] == ',' } func isSemicolon(b []byte) bool { return len(b) == 1 && b[0] == ';' } func isEquality(b []byte) bool { return len(b) == 1 && b[0] == '=' } httphead-0.1.0/httphead_test.go000066400000000000000000000234211376365220000165060ustar00rootroot00000000000000package httphead import ( "bytes" "fmt" "math/rand" "testing" ) func ExampleScanTokens() { var values []string ScanTokens([]byte(`a,b,c`), func(v []byte) bool { values = append(values, string(v)) return v[0] != 'b' }) fmt.Println(values) // Output: [a b] } func ExampleScanOptions() { foo := map[string]string{} ScanOptions([]byte(`foo;bar=1;baz`), func(index int, key, param, value []byte) Control { foo[string(param)] = string(value) return ControlContinue }) fmt.Printf("bar:%s baz:%s", foo["bar"], foo["baz"]) // Output: bar:1 baz: } func ExampleParseOptions() { options, ok := ParseOptions([]byte(`foo;bar=1,baz`), nil) fmt.Println(options, ok) // Output: [{foo [bar:1]} {baz []}] true } func ExampleParseOptionsLifetime() { data := []byte(`foo;bar=1,baz`) options, ok := ParseOptions(data, nil) copy(data, []byte(`xxx;yyy=0,zzz`)) fmt.Println(options, ok) // Output: [{xxx [yyy:0]} {zzz []}] true } var listCases = []struct { label string in []byte ok bool exp [][]byte }{ { label: "simple", in: []byte(`a,b,c`), ok: true, exp: [][]byte{ []byte(`a`), []byte(`b`), []byte(`c`), }, }, { label: "simple", in: []byte(`a,b,,c`), ok: true, exp: [][]byte{ []byte(`a`), []byte(`b`), []byte(`c`), }, }, { label: "simple", in: []byte(`a,b;c`), ok: false, exp: [][]byte{ []byte(`a`), []byte(`b`), }, }, } func TestScanTokens(t *testing.T) { for _, test := range listCases { t.Run(test.label, func(t *testing.T) { var act [][]byte ok := ScanTokens(test.in, func(v []byte) bool { act = append(act, v) return true }) if ok != test.ok { t.Errorf("unexpected result: %v; want %v", ok, test.ok) } if an, en := len(act), len(test.exp); an != en { t.Errorf("unexpected length of result: %d; want %d", an, en) } else { for i, ev := range test.exp { if av := act[i]; !bytes.Equal(av, ev) { t.Errorf("unexpected %d-th value: %#q; want %#q", i, string(av), string(ev)) } } } }) } } func BenchmarkScanTokens(b *testing.B) { for _, bench := range listCases { b.Run(bench.label, func(b *testing.B) { for i := 0; i < b.N; i++ { _ = ScanTokens(bench.in, func(v []byte) bool { return true }) } }) } } func randASCII(dst []byte) { for i := 0; i < len(dst); i++ { dst[i] = byte(rand.Intn('z'-'a')) + 'a' } } type tuple struct { index int option, attribute, value []byte } var parametersCases = []struct { label string in []byte ok bool exp []tuple }{ { label: "simple", in: []byte(`a,b,c`), ok: true, exp: []tuple{ {index: 0, option: []byte(`a`)}, {index: 1, option: []byte(`b`)}, {index: 2, option: []byte(`c`)}, }, }, { label: "simple", in: []byte(`a,b,c;foo=1;bar=2`), ok: true, exp: []tuple{ {index: 0, option: []byte(`a`)}, {index: 1, option: []byte(`b`)}, {index: 2, option: []byte(`c`), attribute: []byte(`foo`), value: []byte(`1`)}, {index: 2, option: []byte(`c`), attribute: []byte(`bar`), value: []byte(`2`)}, }, }, { label: "simple", in: []byte(`c;foo;bar=2`), ok: true, exp: []tuple{ {index: 0, option: []byte(`c`), attribute: []byte(`foo`)}, {index: 0, option: []byte(`c`), attribute: []byte(`bar`), value: []byte(`2`)}, }, }, { label: "simple", in: []byte(`foo;bar=1;baz`), ok: true, exp: []tuple{ {index: 0, option: []byte(`foo`), attribute: []byte(`bar`), value: []byte(`1`)}, {index: 0, option: []byte(`foo`), attribute: []byte(`baz`)}, }, }, { label: "simple_quoted", in: []byte(`c;bar="2"`), ok: true, exp: []tuple{ {index: 0, option: []byte(`c`), attribute: []byte(`bar`), value: []byte(`2`)}, }, }, { label: "simple_dup", in: []byte(`c;bar=1,c;bar=2`), ok: true, exp: []tuple{ {index: 0, option: []byte(`c`), attribute: []byte(`bar`), value: []byte(`1`)}, {index: 1, option: []byte(`c`), attribute: []byte(`bar`), value: []byte(`2`)}, }, }, { label: "all", in: []byte(`foo;a=1;b=2;c=3,bar;z,baz`), ok: true, exp: []tuple{ {index: 0, option: []byte(`foo`), attribute: []byte(`a`), value: []byte(`1`)}, {index: 0, option: []byte(`foo`), attribute: []byte(`b`), value: []byte(`2`)}, {index: 0, option: []byte(`foo`), attribute: []byte(`c`), value: []byte(`3`)}, {index: 1, option: []byte(`bar`), attribute: []byte(`z`)}, {index: 2, option: []byte(`baz`)}, }, }, { label: "comma", in: []byte(`foo;a=1,, , ,bar;b=2`), ok: true, exp: []tuple{ {index: 0, option: []byte(`foo`), attribute: []byte(`a`), value: []byte(`1`)}, {index: 1, option: []byte(`bar`), attribute: []byte(`b`), value: []byte(`2`)}, }, }, } func TestParameters(t *testing.T) { for _, test := range parametersCases { t.Run(test.label, func(t *testing.T) { var act []tuple ok := ScanOptions(test.in, func(index int, key, param, value []byte) Control { act = append(act, tuple{index, key, param, value}) return ControlContinue }) if ok != test.ok { t.Errorf("unexpected result: %v; want %v", ok, test.ok) } if an, en := len(act), len(test.exp); an != en { t.Errorf("unexpected length of result: %d; want %d", an, en) return } for i, e := range test.exp { a := act[i] if a.index != e.index || !bytes.Equal(a.option, e.option) || !bytes.Equal(a.attribute, e.attribute) || !bytes.Equal(a.value, e.value) { t.Errorf( "unexpected %d-th tuple: #%d %#q[%#q = %#q]; want #%d %#q[%#q = %#q]", i, a.index, string(a.option), string(a.attribute), string(a.value), e.index, string(e.option), string(e.attribute), string(e.value), ) } } }) } } func BenchmarkParameters(b *testing.B) { for _, bench := range parametersCases { b.Run(bench.label, func(b *testing.B) { for i := 0; i < b.N; i++ { _ = ScanOptions(bench.in, func(_ int, _, _, _ []byte) Control { return ControlContinue }) } }) } } var selectOptionsCases = []struct { label string selector OptionSelector in []byte p []Option exp []Option ok bool }{ { label: "simple", selector: OptionSelector{ Flags: SelectCopy | SelectUnique, }, in: []byte(`foo;a=1,foo;a=2`), p: nil, exp: []Option{ NewOption("foo", map[string]string{"a": "1"}), }, ok: true, }, { label: "simple", selector: OptionSelector{ Flags: SelectUnique, }, in: []byte(`foo;a=1,foo;a=2`), p: make([]Option, 0, 2), exp: []Option{ NewOption("foo", map[string]string{"a": "1"}), }, ok: true, }, { label: "multiparam_stack", selector: OptionSelector{ Flags: SelectUnique, }, in: []byte(`foo;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8,bar`), p: make([]Option, 0, 2), exp: []Option{ NewOption("foo", map[string]string{ "a": "1", "b": "2", "c": "3", "d": "4", "e": "5", "f": "6", "g": "7", "h": "8", }), NewOption("bar", nil), }, ok: true, }, { label: "multiparam_stack", selector: OptionSelector{ Flags: SelectCopy | SelectUnique, }, in: []byte(`foo;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8,bar`), p: make([]Option, 0, 2), exp: []Option{ NewOption("foo", map[string]string{ "a": "1", "b": "2", "c": "3", "d": "4", "e": "5", "f": "6", "g": "7", "h": "8", }), NewOption("bar", nil), }, ok: true, }, { label: "multiparam_heap", selector: OptionSelector{ Flags: SelectCopy | SelectUnique, }, in: []byte(`foo;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8;i=9;j=10,bar`), p: make([]Option, 0, 2), exp: []Option{ NewOption("foo", map[string]string{ "a": "1", "b": "2", "c": "3", "d": "4", "e": "5", "f": "6", "g": "7", "h": "8", "i": "9", "j": "10", }), NewOption("bar", nil), }, ok: true, }, } func TestSelectOptions(t *testing.T) { for _, test := range selectOptionsCases { t.Run(test.label+test.selector.Flags.String(), func(t *testing.T) { act, ok := test.selector.Select(test.in, test.p) if ok != test.ok { t.Errorf("SelectOptions(%q) wellformed sign is %v; want %v", string(test.in), ok, test.ok) } if !optionsEqual(act, test.exp) { t.Errorf("SelectOptions(%q) = %v; want %v", string(test.in), act, test.exp) } }) } } func BenchmarkSelectOptions(b *testing.B) { for _, test := range selectOptionsCases { s := test.selector b.Run(test.label+s.Flags.String(), func(b *testing.B) { for i := 0; i < b.N; i++ { _, _ = s.Select(test.in, test.p) } }) } } func optionsEqual(a, b []Option) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if !a[i].Equal(b[i]) { return false } } return true } func TestOptionCopy(t *testing.T) { for i, test := range []struct { pairs int }{ {4}, {16}, } { name := []byte(fmt.Sprintf("test:%d", i)) n := make([]byte, len(name)) copy(n, name) opt := Option{Name: n} pairs := make([]pair, test.pairs) for i := 0; i < len(pairs); i++ { pair := pair{make([]byte, 8), make([]byte, 8)} randASCII(pair.key) randASCII(pair.value) pairs[i] = pair k, v := make([]byte, len(pair.key)), make([]byte, len(pair.value)) copy(k, pair.key) copy(v, pair.value) opt.Parameters.Set(k, v) } cp := opt.Copy(make([]byte, opt.Size())) memset(opt.Name, 'x') for _, p := range opt.Parameters.data() { memset(p.key, 'x') memset(p.value, 'x') } if !bytes.Equal(cp.Name, name) { t.Errorf("name was not copied properly: %q; want %q", string(cp.Name), string(name)) } for i, p := range cp.Parameters.data() { exp := pairs[i] if !bytes.Equal(p.key, exp.key) || !bytes.Equal(p.value, exp.value) { t.Errorf( "%d-th pair was not copied properly: %q=%q; want %q=%q", i, string(p.key), string(p.value), string(exp.key), string(exp.value), ) } } } } func memset(dst []byte, v byte) { copy(dst, bytes.Repeat([]byte{v}, len(dst))) } httphead-0.1.0/lexer.go000066400000000000000000000153261376365220000147720ustar00rootroot00000000000000package httphead import ( "bytes" ) // ItemType encodes type of the lexing token. type ItemType int const ( // ItemUndef reports that token is undefined. ItemUndef ItemType = iota // ItemToken reports that token is RFC2616 token. ItemToken // ItemSeparator reports that token is RFC2616 separator. ItemSeparator // ItemString reports that token is RFC2616 quouted string. ItemString // ItemComment reports that token is RFC2616 comment. ItemComment // ItemOctet reports that token is octet slice. ItemOctet ) // Scanner represents header tokens scanner. // See https://tools.ietf.org/html/rfc2616#section-2 type Scanner struct { data []byte pos int itemType ItemType itemBytes []byte err bool } // NewScanner creates new RFC2616 data scanner. func NewScanner(data []byte) *Scanner { return &Scanner{data: data} } // Next scans for next token. It returns true on successful scanning, and false // on error or EOF. func (l *Scanner) Next() bool { c, ok := l.nextChar() if !ok { return false } switch c { case '"': // quoted-string; return l.fetchQuotedString() case '(': // comment; return l.fetchComment() case '\\', ')': // unexpected chars; l.err = true return false default: return l.fetchToken() } } // FetchUntil fetches ItemOctet from current scanner position to first // occurence of the c or to the end of the underlying data. func (l *Scanner) FetchUntil(c byte) bool { l.resetItem() if l.pos == len(l.data) { return false } return l.fetchOctet(c) } // Peek reads byte at current position without advancing it. On end of data it // returns 0. func (l *Scanner) Peek() byte { if l.pos == len(l.data) { return 0 } return l.data[l.pos] } // Peek2 reads two first bytes at current position without advancing it. // If there not enough data it returs 0. func (l *Scanner) Peek2() (a, b byte) { if l.pos == len(l.data) { return 0, 0 } if l.pos+1 == len(l.data) { return l.data[l.pos], 0 } return l.data[l.pos], l.data[l.pos+1] } // Buffered reporst how many bytes there are left to scan. func (l *Scanner) Buffered() int { return len(l.data) - l.pos } // Advance moves current position index at n bytes. It returns true on // successful move. func (l *Scanner) Advance(n int) bool { l.pos += n if l.pos > len(l.data) { l.pos = len(l.data) return false } return true } // Skip skips all bytes until first occurence of c. func (l *Scanner) Skip(c byte) { if l.err { return } // Reset scanner state. l.resetItem() if i := bytes.IndexByte(l.data[l.pos:], c); i == -1 { // Reached the end of data. l.pos = len(l.data) } else { l.pos += i + 1 } } // SkipEscaped skips all bytes until first occurence of non-escaped c. func (l *Scanner) SkipEscaped(c byte) { if l.err { return } // Reset scanner state. l.resetItem() if i := ScanUntil(l.data[l.pos:], c); i == -1 { // Reached the end of data. l.pos = len(l.data) } else { l.pos += i + 1 } } // Type reports current token type. func (l *Scanner) Type() ItemType { return l.itemType } // Bytes returns current token bytes. func (l *Scanner) Bytes() []byte { return l.itemBytes } func (l *Scanner) nextChar() (byte, bool) { // Reset scanner state. l.resetItem() if l.err { return 0, false } l.pos += SkipSpace(l.data[l.pos:]) if l.pos == len(l.data) { return 0, false } return l.data[l.pos], true } func (l *Scanner) resetItem() { l.itemType = ItemUndef l.itemBytes = nil } func (l *Scanner) fetchOctet(c byte) bool { i := l.pos if j := bytes.IndexByte(l.data[l.pos:], c); j == -1 { // Reached the end of data. l.pos = len(l.data) } else { l.pos += j } l.itemType = ItemOctet l.itemBytes = l.data[i:l.pos] return true } func (l *Scanner) fetchToken() bool { n, t := ScanToken(l.data[l.pos:]) if n == -1 { l.err = true return false } l.itemType = t l.itemBytes = l.data[l.pos : l.pos+n] l.pos += n return true } func (l *Scanner) fetchQuotedString() (ok bool) { l.pos++ n := ScanUntil(l.data[l.pos:], '"') if n == -1 { l.err = true return false } l.itemType = ItemString l.itemBytes = RemoveByte(l.data[l.pos:l.pos+n], '\\') l.pos += n + 1 return true } func (l *Scanner) fetchComment() (ok bool) { l.pos++ n := ScanPairGreedy(l.data[l.pos:], '(', ')') if n == -1 { l.err = true return false } l.itemType = ItemComment l.itemBytes = RemoveByte(l.data[l.pos:l.pos+n], '\\') l.pos += n + 1 return true } // ScanUntil scans for first non-escaped character c in given data. // It returns index of matched c and -1 if c is not found. func ScanUntil(data []byte, c byte) (n int) { for { i := bytes.IndexByte(data[n:], c) if i == -1 { return -1 } n += i if n == 0 || data[n-1] != '\\' { break } n++ } return } // ScanPairGreedy scans for complete pair of opening and closing chars in greedy manner. // Note that first opening byte must not be present in data. func ScanPairGreedy(data []byte, open, close byte) (n int) { var m int opened := 1 for { i := bytes.IndexByte(data[n:], close) if i == -1 { return -1 } n += i // If found index is not escaped then it is the end. if n == 0 || data[n-1] != '\\' { opened-- } for m < i { j := bytes.IndexByte(data[m:i], open) if j == -1 { break } m += j + 1 opened++ } if opened == 0 { break } n++ m = n } return } // RemoveByte returns data without c. If c is not present in data it returns // the same slice. If not, it copies data without c. func RemoveByte(data []byte, c byte) []byte { j := bytes.IndexByte(data, c) if j == -1 { return data } n := len(data) - 1 // If character is present, than allocate slice with n-1 capacity. That is, // resulting bytes could be at most n-1 length. result := make([]byte, n) k := copy(result, data[:j]) for i := j + 1; i < n; { j = bytes.IndexByte(data[i:], c) if j != -1 { k += copy(result[k:], data[i:i+j]) i = i + j + 1 } else { k += copy(result[k:], data[i:]) break } } return result[:k] } // SkipSpace skips spaces and lws-sequences from p. // It returns number ob bytes skipped. func SkipSpace(p []byte) (n int) { for len(p) > 0 { switch { case len(p) >= 3 && p[0] == '\r' && p[1] == '\n' && OctetTypes[p[2]].IsSpace(): p = p[3:] n += 3 case OctetTypes[p[0]].IsSpace(): p = p[1:] n++ default: return } } return } // ScanToken scan for next token in p. It returns length of the token and its // type. It do not trim p. func ScanToken(p []byte) (n int, t ItemType) { if len(p) == 0 { return 0, ItemUndef } c := p[0] switch { case OctetTypes[c].IsSeparator(): return 1, ItemSeparator case OctetTypes[c].IsToken(): for n = 1; n < len(p); n++ { c := p[n] if !OctetTypes[c].IsToken() { break } } return n, ItemToken default: return -1, ItemUndef } } httphead-0.1.0/lexer_test.go000066400000000000000000000052771376365220000160350ustar00rootroot00000000000000package httphead import ( "bytes" "testing" ) func TestScannerSkipEscaped(t *testing.T) { for _, test := range []struct { in []byte c byte pos int }{ { in: []byte(`foo,bar`), c: ',', pos: 4, }, { in: []byte(`foo\,bar,baz`), c: ',', pos: 9, }, } { s := NewScanner(test.in) s.SkipEscaped(test.c) if act, exp := s.pos, test.pos; act != exp { t.Errorf("unexpected scanner pos: %v; want %v", act, exp) } } } type readCase struct { label string in []byte out []byte err bool } var quotedStringCases = []readCase{ { label: "nonterm", in: []byte(`"`), out: []byte(``), err: true, }, { label: "empty", in: []byte(`""`), out: []byte(``), }, { label: "simple", in: []byte(`"hello, world!"`), out: []byte(`hello, world!`), }, { label: "quoted", in: []byte(`"hello, \"world\"!"`), out: []byte(`hello, "world"!`), }, { label: "quoted", in: []byte(`"\"hello\", \"world\"!"`), out: []byte(`"hello", "world"!`), }, } var commentCases = []readCase{ { label: "nonterm", in: []byte(`(hello`), out: []byte(``), err: true, }, { label: "empty", in: []byte(`()`), out: []byte(``), }, { label: "simple", in: []byte(`(hello)`), out: []byte(`hello`), }, { label: "quoted", in: []byte(`(hello\)\(world)`), out: []byte(`hello)(world`), }, { label: "nested", in: []byte(`(hello(world))`), out: []byte(`hello(world)`), }, } type readTest struct { label string cases []readCase fn func(*Scanner) bool } var readTests = []readTest{ { "ReadString", quotedStringCases, (*Scanner).fetchQuotedString, }, { "ReadComment", commentCases, (*Scanner).fetchComment, }, } func TestScannerRead(t *testing.T) { for _, bunch := range readTests { for _, test := range bunch.cases { t.Run(bunch.label+" "+test.label, func(t *testing.T) { l := &Scanner{data: []byte(test.in)} if ok := bunch.fn(l); ok != !test.err { t.Errorf("l.%s() = %v; want %v", bunch.label, ok, !test.err) return } if !bytes.Equal(test.out, l.itemBytes) { t.Errorf("l.%s() = %s; want %s", bunch.label, string(l.itemBytes), string(test.out)) } }) } } } func BenchmarkScannerReadString(b *testing.B) { for _, bench := range quotedStringCases { b.Run(bench.label, func(b *testing.B) { for i := 0; i < b.N; i++ { l := &Scanner{data: []byte(bench.in)} _ = l.fetchQuotedString() } }) } } func BenchmarkScannerReadComment(b *testing.B) { for _, bench := range commentCases { b.Run(bench.label, func(b *testing.B) { for i := 0; i < b.N; i++ { l := &Scanner{data: []byte(bench.in)} _ = l.fetchComment() } }) } } httphead-0.1.0/octet.go000066400000000000000000000047461376365220000147750ustar00rootroot00000000000000package httphead // OctetType desribes character type. // // From the "Basic Rules" chapter of RFC2616 // See https://tools.ietf.org/html/rfc2616#section-2.2 // // OCTET = // CHAR = // UPALPHA = // LOALPHA = // ALPHA = UPALPHA | LOALPHA // DIGIT = // CTL = // CR = // LF = // SP = // HT = // <"> = // CRLF = CR LF // LWS = [CRLF] 1*( SP | HT ) // // Many HTTP/1.1 header field values consist of words separated by LWS // or special characters. These special characters MUST be in a quoted // string to be used within a parameter value (as defined in section // 3.6). // // token = 1* // separators = "(" | ")" | "<" | ">" | "@" // | "," | ";" | ":" | "\" | <"> // | "/" | "[" | "]" | "?" | "=" // | "{" | "}" | SP | HT type OctetType byte // IsChar reports whether octet is CHAR. func (t OctetType) IsChar() bool { return t&octetChar != 0 } // IsControl reports whether octet is CTL. func (t OctetType) IsControl() bool { return t&octetControl != 0 } // IsSeparator reports whether octet is separator. func (t OctetType) IsSeparator() bool { return t&octetSeparator != 0 } // IsSpace reports whether octet is space (SP or HT). func (t OctetType) IsSpace() bool { return t&octetSpace != 0 } // IsToken reports whether octet is token. func (t OctetType) IsToken() bool { return t&octetToken != 0 } const ( octetChar OctetType = 1 << iota octetControl octetSpace octetSeparator octetToken ) // OctetTypes is a table of octets. var OctetTypes [256]OctetType func init() { for c := 32; c < 256; c++ { var t OctetType if c <= 127 { t |= octetChar } if 0 <= c && c <= 31 || c == 127 { t |= octetControl } switch c { case '(', ')', '<', '>', '@', ',', ';', ':', '"', '/', '[', ']', '?', '=', '{', '}', '\\': t |= octetSeparator case ' ', '\t': t |= octetSpace | octetSeparator } if t.IsChar() && !t.IsControl() && !t.IsSeparator() && !t.IsSpace() { t |= octetToken } OctetTypes[c] = t } } httphead-0.1.0/option.go000066400000000000000000000077751376365220000151740ustar00rootroot00000000000000package httphead import ( "bytes" "sort" ) // Option represents a header option. type Option struct { Name []byte Parameters Parameters } // Size returns number of bytes need to be allocated for use in opt.Copy. func (opt Option) Size() int { return len(opt.Name) + opt.Parameters.bytes } // Copy copies all underlying []byte slices into p and returns new Option. // Note that p must be at least of opt.Size() length. func (opt Option) Copy(p []byte) Option { n := copy(p, opt.Name) opt.Name = p[:n] opt.Parameters, p = opt.Parameters.Copy(p[n:]) return opt } // Clone is a shorthand for making slice of opt.Size() sequenced with Copy() // call. func (opt Option) Clone() Option { return opt.Copy(make([]byte, opt.Size())) } // String represents option as a string. func (opt Option) String() string { return "{" + string(opt.Name) + " " + opt.Parameters.String() + "}" } // NewOption creates named option with given parameters. func NewOption(name string, params map[string]string) Option { p := Parameters{} for k, v := range params { p.Set([]byte(k), []byte(v)) } return Option{ Name: []byte(name), Parameters: p, } } // Equal reports whether option is equal to b. func (opt Option) Equal(b Option) bool { if bytes.Equal(opt.Name, b.Name) { return opt.Parameters.Equal(b.Parameters) } return false } // Parameters represents option's parameters. type Parameters struct { pos int bytes int arr [8]pair dyn []pair } // Equal reports whether a equal to b. func (p Parameters) Equal(b Parameters) bool { switch { case p.dyn == nil && b.dyn == nil: case p.dyn != nil && b.dyn != nil: default: return false } ad, bd := p.data(), b.data() if len(ad) != len(bd) { return false } sort.Sort(pairs(ad)) sort.Sort(pairs(bd)) for i := 0; i < len(ad); i++ { av, bv := ad[i], bd[i] if !bytes.Equal(av.key, bv.key) || !bytes.Equal(av.value, bv.value) { return false } } return true } // Size returns number of bytes that needed to copy p. func (p *Parameters) Size() int { return p.bytes } // Copy copies all underlying []byte slices into dst and returns new // Parameters. // Note that dst must be at least of p.Size() length. func (p *Parameters) Copy(dst []byte) (Parameters, []byte) { ret := Parameters{ pos: p.pos, bytes: p.bytes, } if p.dyn != nil { ret.dyn = make([]pair, len(p.dyn)) for i, v := range p.dyn { ret.dyn[i], dst = v.copy(dst) } } else { for i, p := range p.arr { ret.arr[i], dst = p.copy(dst) } } return ret, dst } // Get returns value by key and flag about existence such value. func (p *Parameters) Get(key string) (value []byte, ok bool) { for _, v := range p.data() { if string(v.key) == key { return v.value, true } } return nil, false } // Set sets value by key. func (p *Parameters) Set(key, value []byte) { p.bytes += len(key) + len(value) if p.pos < len(p.arr) { p.arr[p.pos] = pair{key, value} p.pos++ return } if p.dyn == nil { p.dyn = make([]pair, len(p.arr), len(p.arr)+1) copy(p.dyn, p.arr[:]) } p.dyn = append(p.dyn, pair{key, value}) } // ForEach iterates over parameters key-value pairs and calls cb for each one. func (p *Parameters) ForEach(cb func(k, v []byte) bool) { for _, v := range p.data() { if !cb(v.key, v.value) { break } } } // String represents parameters as a string. func (p *Parameters) String() (ret string) { ret = "[" for i, v := range p.data() { if i > 0 { ret += " " } ret += string(v.key) + ":" + string(v.value) } return ret + "]" } func (p *Parameters) data() []pair { if p.dyn != nil { return p.dyn } return p.arr[:p.pos] } type pair struct { key, value []byte } func (p pair) copy(dst []byte) (pair, []byte) { n := copy(dst, p.key) p.key = dst[:n] m := n + copy(dst[n:], p.value) p.value = dst[n:m] dst = dst[m:] return p, dst } type pairs []pair func (p pairs) Len() int { return len(p) } func (p pairs) Less(a, b int) bool { return bytes.Compare(p[a].key, p[b].key) == -1 } func (p pairs) Swap(a, b int) { p[a], p[b] = p[b], p[a] } httphead-0.1.0/writer.go000066400000000000000000000043351376365220000151650ustar00rootroot00000000000000package httphead import "io" var ( comma = []byte{','} equality = []byte{'='} semicolon = []byte{';'} quote = []byte{'"'} escape = []byte{'\\'} ) // WriteOptions write options list to the dest. // It uses the same form as {Scan,Parse}Options functions: // values = 1#value // value = token *( ";" param ) // param = token [ "=" (token | quoted-string) ] // // It wraps valuse into the quoted-string sequence if it contains any // non-token characters. func WriteOptions(dest io.Writer, options []Option) (n int, err error) { w := writer{w: dest} for i, opt := range options { if i > 0 { w.write(comma) } writeTokenSanitized(&w, opt.Name) for _, p := range opt.Parameters.data() { w.write(semicolon) writeTokenSanitized(&w, p.key) if len(p.value) != 0 { w.write(equality) writeTokenSanitized(&w, p.value) } } } return w.result() } // writeTokenSanitized writes token as is or as quouted string if it contains // non-token characters. // // Note that is is not expects LWS sequnces be in s, cause LWS is used only as // header field continuation: // "A CRLF is allowed in the definition of TEXT only as part of a header field // continuation. It is expected that the folding LWS will be replaced with a // single SP before interpretation of the TEXT value." // See https://tools.ietf.org/html/rfc2616#section-2 // // That is we sanitizing s for writing, so there could not be any header field // continuation. // That is any CRLF will be escaped as any other control characters not allowd in TEXT. func writeTokenSanitized(bw *writer, bts []byte) { var qt bool var pos int for i := 0; i < len(bts); i++ { c := bts[i] if !OctetTypes[c].IsToken() && !qt { qt = true bw.write(quote) } if OctetTypes[c].IsControl() || c == '"' { if !qt { qt = true bw.write(quote) } bw.write(bts[pos:i]) bw.write(escape) bw.write(bts[i : i+1]) pos = i + 1 } } if !qt { bw.write(bts) } else { bw.write(bts[pos:]) bw.write(quote) } } type writer struct { w io.Writer n int err error } func (w *writer) write(p []byte) { if w.err != nil { return } var n int n, w.err = w.w.Write(p) w.n += n return } func (w *writer) result() (int, error) { return w.n, w.err } httphead-0.1.0/writer_test.go000066400000000000000000000025671376365220000162310ustar00rootroot00000000000000package httphead import ( "bufio" "bytes" "fmt" "testing" ) func ExampleWriteOptions() { opts := []Option{ NewOption("foo", map[string]string{ "param": "hello, world!", }), NewOption("bar", nil), NewOption("b a z", nil), } buf := bytes.Buffer{} bw := bufio.NewWriter(&buf) WriteOptions(bw, opts) bw.Flush() // Output: foo;param="hello, world!",bar,"b a z" fmt.Println(buf.String()) } func TestWriteOptions(t *testing.T) { for _, test := range []struct { options []Option exp string }{ { options: []Option{ NewOption("foo", map[string]string{"bar": "baz"}), }, exp: "foo;bar=baz", }, { options: []Option{ NewOption("foo", map[string]string{"bar": "baz"}), NewOption("a", nil), NewOption("b", map[string]string{"c": "10"}), }, exp: "foo;bar=baz,a,b;c=10", }, { options: []Option{ NewOption("foo", map[string]string{"a b c": "10,2"}), }, exp: `foo;"a b c"="10,2"`, }, { options: []Option{ NewOption(`"foo"`, nil), NewOption(`"bar"`, nil), }, exp: `"\"foo\"","\"bar\""`, }, } { t.Run("", func(t *testing.T) { buf := bytes.Buffer{} bw := bufio.NewWriter(&buf) WriteOptions(bw, test.options) if err := bw.Flush(); err != nil { t.Fatal(err) } if act := buf.String(); act != test.exp { t.Errorf("WriteOptions = %#q; want %#q", act, test.exp) } }) } }