terraform/vendor/github.com/tent/http-link-go/link.go

186 lines
3.9 KiB
Go

// Package link implements parsing and serialization of Link header values as
// defined in RFC 5988.
package link
import (
"bytes"
"errors"
"sort"
"unicode"
)
type Link struct {
URI string
Rel string
Params map[string]string
}
// Format serializes a slice of Links into a header value. It does not currently
// implement RFC 2231 handling of non-ASCII character encoding and language
// information.
func Format(links []Link) string {
buf := &bytes.Buffer{}
for i, link := range links {
if i > 0 {
buf.Write([]byte(", "))
}
buf.WriteByte('<')
buf.WriteString(link.URI)
buf.WriteByte('>')
writeParam(buf, "rel", link.Rel)
keys := make([]string, 0, len(link.Params))
for k := range link.Params {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
writeParam(buf, k, link.Params[k])
}
}
return buf.String()
}
func writeParam(buf *bytes.Buffer, key, value string) {
buf.Write([]byte("; "))
buf.WriteString(key)
buf.Write([]byte(`="`))
buf.WriteString(value)
buf.WriteByte('"')
}
// Parse parses a Link header value into a slice of Links. It does not currently
// implement RFC 2231 handling of non-ASCII character encoding and language
// information.
func Parse(l string) ([]Link, error) {
v := []byte(l)
v = bytes.TrimSpace(v)
if len(v) == 0 {
return nil, nil
}
links := make([]Link, 0, 1)
for len(v) > 0 {
if v[0] != '<' {
return nil, errors.New("link: does not start with <")
}
lend := bytes.IndexByte(v, '>')
if lend == -1 {
return nil, errors.New("link: does not contain ending >")
}
params := make(map[string]string)
link := Link{URI: string(v[1:lend]), Params: params}
links = append(links, link)
// trim off parsed url
v = v[lend+1:]
if len(v) == 0 {
break
}
v = bytes.TrimLeftFunc(v, unicode.IsSpace)
for len(v) > 0 {
if v[0] != ';' && v[0] != ',' {
return nil, errors.New(`link: expected ";" or "'", got "` + string(v[0:1]) + `"`)
}
var next bool
if v[0] == ',' {
next = true
}
v = bytes.TrimLeftFunc(v[1:], unicode.IsSpace)
if next || len(v) == 0 {
break
}
var key, value []byte
key, value, v = consumeParam(v)
if key == nil || value == nil {
return nil, errors.New("link: malformed param")
}
if k := string(key); k == "rel" {
if links[len(links)-1].Rel == "" {
links[len(links)-1].Rel = string(value)
}
} else {
params[k] = string(value)
}
v = bytes.TrimLeftFunc(v, unicode.IsSpace)
}
}
return links, nil
}
func isTokenChar(r rune) bool {
return r > 0x20 && r < 0x7f && r != '"' && r != ',' && r != '=' && r != ';'
}
func isNotTokenChar(r rune) bool { return !isTokenChar(r) }
func consumeToken(v []byte) (token, rest []byte) {
notPos := bytes.IndexFunc(v, isNotTokenChar)
if notPos == -1 {
return v, nil
}
if notPos == 0 {
return nil, v
}
return v[0:notPos], v[notPos:]
}
func consumeValue(v []byte) (value, rest []byte) {
if v[0] != '"' {
return nil, v
}
rest = v[1:]
buffer := &bytes.Buffer{}
var nextIsLiteral bool
for idx, r := range string(rest) {
switch {
case nextIsLiteral:
buffer.WriteRune(r)
nextIsLiteral = false
case r == '"':
return buffer.Bytes(), rest[idx+1:]
case r == '\\':
nextIsLiteral = true
case r != '\r' && r != '\n':
buffer.WriteRune(r)
default:
return nil, v
}
}
return nil, v
}
func consumeParam(v []byte) (param, value, rest []byte) {
param, rest = consumeToken(v)
param = bytes.ToLower(param)
if param == nil {
return nil, nil, v
}
rest = bytes.TrimLeftFunc(rest, unicode.IsSpace)
if len(rest) == 0 || rest[0] != '=' {
return nil, nil, v
}
rest = rest[1:] // consume equals sign
rest = bytes.TrimLeftFunc(rest, unicode.IsSpace)
if len(rest) == 0 {
return nil, nil, v
}
if rest[0] != '"' {
value, rest = consumeToken(rest)
} else {
value, rest = consumeValue(rest)
}
if value == nil {
return nil, nil, v
}
return param, value, rest
}