├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── link.go └── link_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.1 4 | - tip 5 | before_install: 6 | - go get launchpad.net/gocheck 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tent.is, LLC. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Tent.is, LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http-link-go [![Build Status](https://travis-ci.org/tent/http-link-go.png?branch=master)](https://travis-ci.org/tent/http-link-go) 2 | 3 | http-link-go implements parsing and serialization of Link header values as 4 | defined in [RFC 5988](https://tools.ietf.org/html/rfc5988). 5 | 6 | [**Documentation**](http://godoc.org/github.com/tent/http-link-go) 7 | 8 | ## Installation 9 | 10 | ```text 11 | go get github.com/tent/http-link-go 12 | ``` 13 | -------------------------------------------------------------------------------- /link.go: -------------------------------------------------------------------------------- 1 | // Package link implements parsing and serialization of Link header values as 2 | // defined in RFC 5988. 3 | package link 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "sort" 9 | "unicode" 10 | ) 11 | 12 | type Link struct { 13 | URI string 14 | Rel string 15 | Params map[string]string 16 | } 17 | 18 | // Format serializes a slice of Links into a header value. It does not currently 19 | // implement RFC 2231 handling of non-ASCII character encoding and language 20 | // information. 21 | func Format(links []Link) string { 22 | buf := &bytes.Buffer{} 23 | for i, link := range links { 24 | if i > 0 { 25 | buf.Write([]byte(", ")) 26 | } 27 | buf.WriteByte('<') 28 | buf.WriteString(link.URI) 29 | buf.WriteByte('>') 30 | 31 | writeParam(buf, "rel", link.Rel) 32 | 33 | keys := make([]string, 0, len(link.Params)) 34 | for k := range link.Params { 35 | keys = append(keys, k) 36 | } 37 | sort.Strings(keys) 38 | 39 | for _, k := range keys { 40 | writeParam(buf, k, link.Params[k]) 41 | } 42 | } 43 | 44 | return buf.String() 45 | } 46 | 47 | func writeParam(buf *bytes.Buffer, key, value string) { 48 | buf.Write([]byte("; ")) 49 | buf.WriteString(key) 50 | buf.Write([]byte(`="`)) 51 | buf.WriteString(value) 52 | buf.WriteByte('"') 53 | } 54 | 55 | // Parse parses a Link header value into a slice of Links. It does not currently 56 | // implement RFC 2231 handling of non-ASCII character encoding and language 57 | // information. 58 | func Parse(l string) ([]Link, error) { 59 | v := []byte(l) 60 | v = bytes.TrimSpace(v) 61 | if len(v) == 0 { 62 | return nil, nil 63 | } 64 | 65 | links := make([]Link, 0, 1) 66 | for len(v) > 0 { 67 | if v[0] != '<' { 68 | return nil, errors.New("link: does not start with <") 69 | } 70 | lend := bytes.IndexByte(v, '>') 71 | if lend == -1 { 72 | return nil, errors.New("link: does not contain ending >") 73 | } 74 | 75 | params := make(map[string]string) 76 | link := Link{URI: string(v[1:lend]), Params: params} 77 | links = append(links, link) 78 | 79 | // trim off parsed url 80 | v = v[lend+1:] 81 | if len(v) == 0 { 82 | break 83 | } 84 | v = bytes.TrimLeftFunc(v, unicode.IsSpace) 85 | 86 | for len(v) > 0 { 87 | if v[0] != ';' && v[0] != ',' { 88 | return nil, errors.New(`link: expected ";" or "'", got "` + string(v[0:1]) + `"`) 89 | } 90 | var next bool 91 | if v[0] == ',' { 92 | next = true 93 | } 94 | v = bytes.TrimLeftFunc(v[1:], unicode.IsSpace) 95 | if next || len(v) == 0 { 96 | break 97 | } 98 | var key, value []byte 99 | key, value, v = consumeParam(v) 100 | if key == nil || value == nil { 101 | return nil, errors.New("link: malformed param") 102 | } 103 | if k := string(key); k == "rel" { 104 | if links[len(links)-1].Rel == "" { 105 | links[len(links)-1].Rel = string(value) 106 | } 107 | } else { 108 | params[k] = string(value) 109 | } 110 | v = bytes.TrimLeftFunc(v, unicode.IsSpace) 111 | } 112 | } 113 | 114 | return links, nil 115 | } 116 | 117 | func isTokenChar(r rune) bool { 118 | return r > 0x20 && r < 0x7f && r != '"' && r != ',' && r != '=' && r != ';' 119 | } 120 | 121 | func isNotTokenChar(r rune) bool { return !isTokenChar(r) } 122 | 123 | func consumeToken(v []byte) (token, rest []byte) { 124 | notPos := bytes.IndexFunc(v, isNotTokenChar) 125 | if notPos == -1 { 126 | return v, nil 127 | } 128 | if notPos == 0 { 129 | return nil, v 130 | } 131 | return v[0:notPos], v[notPos:] 132 | } 133 | 134 | func consumeValue(v []byte) (value, rest []byte) { 135 | if v[0] != '"' { 136 | return nil, v 137 | } 138 | 139 | rest = v[1:] 140 | buffer := &bytes.Buffer{} 141 | var nextIsLiteral bool 142 | for idx, r := range string(rest) { 143 | switch { 144 | case nextIsLiteral: 145 | buffer.WriteRune(r) 146 | nextIsLiteral = false 147 | case r == '"': 148 | return buffer.Bytes(), rest[idx+1:] 149 | case r == '\\': 150 | nextIsLiteral = true 151 | case r != '\r' && r != '\n': 152 | buffer.WriteRune(r) 153 | default: 154 | return nil, v 155 | } 156 | } 157 | return nil, v 158 | } 159 | 160 | func consumeParam(v []byte) (param, value, rest []byte) { 161 | param, rest = consumeToken(v) 162 | param = bytes.ToLower(param) 163 | if param == nil { 164 | return nil, nil, v 165 | } 166 | 167 | rest = bytes.TrimLeftFunc(rest, unicode.IsSpace) 168 | if len(rest) == 0 || rest[0] != '=' { 169 | return nil, nil, v 170 | } 171 | rest = rest[1:] // consume equals sign 172 | rest = bytes.TrimLeftFunc(rest, unicode.IsSpace) 173 | if len(rest) == 0 { 174 | return nil, nil, v 175 | } 176 | if rest[0] != '"' { 177 | value, rest = consumeToken(rest) 178 | } else { 179 | value, rest = consumeValue(rest) 180 | } 181 | if value == nil { 182 | return nil, nil, v 183 | } 184 | return param, value, rest 185 | } 186 | -------------------------------------------------------------------------------- /link_test.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | import ( 4 | "testing" 5 | 6 | . "launchpad.net/gocheck" 7 | ) 8 | 9 | // Hook up gocheck into the "go test" runner. 10 | func Test(t *testing.T) { TestingT(t) } 11 | 12 | type LinkSuite struct{} 13 | 14 | var _ = Suite(&LinkSuite{}) 15 | 16 | // TODO: add more tests 17 | var linkParseTests = []struct { 18 | in string 19 | out []Link 20 | }{ 21 | { 22 | "; rel=\"previous\";\n title=\"previous chapter\"", 23 | []Link{{URI: "http://example.com/TheBook/chapter2", Rel: "previous", Params: map[string]string{"title": "previous chapter"}}}, 24 | }, 25 | { 26 | ";\n rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel,\n ;\n rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel", 27 | []Link{ 28 | {URI: "/TheBook/chapter2", Rel: "previous", Params: map[string]string{"title*": "UTF-8'de'letztes%20Kapitel"}}, 29 | {URI: "/TheBook/chapter4", Rel: "next", Params: map[string]string{"title*": "UTF-8'de'n%c3%a4chstes%20Kapitel"}}, 30 | }, 31 | }, 32 | } 33 | 34 | func (s *LinkSuite) TestLinkParsing(c *C) { 35 | for i, t := range linkParseTests { 36 | res, err := Parse(t.in) 37 | c.Assert(err, IsNil, Commentf("test %d", i)) 38 | c.Assert(res, DeepEquals, t.out, Commentf("test %d", i)) 39 | } 40 | } 41 | 42 | var linkFormatTests = []struct { 43 | in []Link 44 | out string 45 | }{ 46 | { 47 | []Link{{URI: "/a", Rel: "foo", Params: map[string]string{"a": "b", "c": "d"}}}, 48 | `; rel="foo"; a="b"; c="d"`, 49 | }, 50 | { 51 | []Link{ 52 | {URI: "/b", Rel: "foo", Params: map[string]string{"a": "b", "c": "d"}}, 53 | {URI: "/a", Rel: "foo", Params: map[string]string{"a": "b", "c": "d"}}}, 54 | `; rel="foo"; a="b"; c="d", ; rel="foo"; a="b"; c="d"`, 55 | }, 56 | } 57 | 58 | func (s *LinkSuite) TestLinkGeneration(c *C) { 59 | for i, t := range linkFormatTests { 60 | res := Format(t.in) 61 | cm := Commentf("test %d", i) 62 | c.Assert(res, Equals, t.out, cm) 63 | parsed, err := Parse(res) 64 | c.Assert(err, IsNil, cm) 65 | c.Assert(parsed, DeepEquals, t.in, cm) 66 | } 67 | } 68 | --------------------------------------------------------------------------------