blob: 5530cf78778a967e29f989c1f55c833b8764d371 [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prpc
import (
"fmt"
"mime"
"strconv"
"strings"
"unicode"
)
// This file implements "Accept" HTTP header parser.
// Spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
// accept is a parsed "Accept" HTTP header.
type accept []acceptType
type acceptType struct {
MediaType string
MediaTypeParams map[string]string
QualityFactor float32
AcceptParams map[string]string
}
// parseAccept parses an "Accept" HTTP header.
//
// See spec http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
// Roughly:
// - accept is a list of types separated by ","
// - a type is like media type, except q parameter separates
// media type parameters and accept parameters.
// - q is quality factor.
//
// This implementation is slow. Does not support accept params.
func parseAccept(v string) (accept, error) {
if v == "" {
return nil, nil
}
var result accept
for _, t := range strings.Split(v, ",") {
t = strings.TrimSpace(t)
if t == "" {
return nil, fmt.Errorf("no media type")
}
mediaType, qValue, _ := qParamSplit(t)
at := acceptType{QualityFactor: 1.0}
var err error
at.MediaType, at.MediaTypeParams, err = mime.ParseMediaType(mediaType)
if err != nil {
return nil, fmt.Errorf("%s", strings.TrimPrefix(err.Error(), "mime: "))
}
if qValue != "" {
qualityFactor, err := strconv.ParseFloat(qValue, 32)
if err != nil {
return nil, fmt.Errorf("q parameter: expected a floating-point number")
}
at.QualityFactor = float32(qualityFactor)
}
result = append(result, at)
}
return result, nil
}
// qParamSplit splits media type and accept params by "q" parameter.
func qParamSplit(v string) (mediaType string, qValue string, acceptParams string) {
rest := v
for {
semicolon := strings.IndexRune(rest, ';')
if semicolon < 0 {
mediaType = v
return
}
semicolonAbs := len(v) - len(rest) + semicolon // mark
rest = rest[semicolon:]
rest = rest[1:] // consume ;
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
if rest == "" || (rest[0] != 'q' && rest[0] != 'Q') {
continue
}
rest = rest[1:] // consume q
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
if rest == "" || rest[0] != '=' {
continue
}
rest = rest[1:] // consume =
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
if rest == "" {
continue
}
qValueStartAbs := len(v) - len(rest) // mark
semicolon2 := strings.IndexRune(rest, ';')
if semicolon2 >= 0 {
semicolon2Abs := len(v) - len(rest) + semicolon2
mediaType = v[:semicolonAbs]
qValue = v[qValueStartAbs:semicolon2Abs]
acceptParams = v[semicolon2Abs+1:]
acceptParams = strings.TrimLeftFunc(acceptParams, unicode.IsSpace)
} else {
mediaType = v[:semicolonAbs]
qValue = v[qValueStartAbs:]
}
qValue = strings.TrimRightFunc(qValue, unicode.IsSpace)
return
}
}
// acceptFormat is a format specified in "Accept" header.
type acceptFormat struct {
Format Format
QualityFactor float32 // preference, range: [0.0, 0.1]
}
// acceptFormatSlice is sortable by quality factor (desc) and format.
type acceptFormatSlice []acceptFormat
func (s acceptFormatSlice) Len() int {
return len(s)
}
func (s acceptFormatSlice) Less(i, j int) bool {
a, b := s[i], s[j]
const epsilon = 0.000000001
// quality factor descending
if a.QualityFactor+epsilon > b.QualityFactor {
return true
}
if a.QualityFactor+epsilon < b.QualityFactor {
return false
}
// format ascending
return a.Format < b.Format
}
func (s acceptFormatSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}