Skip to content

Commit

Permalink
Added the resource/httplink package.
Browse files Browse the repository at this point in the history
  • Loading branch information
yuizumi committed Feb 3, 2020
1 parent aac510b commit 3147822
Show file tree
Hide file tree
Showing 9 changed files with 691 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -7,4 +7,5 @@ require (
github.com/google/go-cmp v0.4.0
github.com/hashicorp/go-multierror v1.0.0
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
)
1 change: 1 addition & 0 deletions go.sum
Expand Up @@ -26,5 +26,6 @@ golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
3 changes: 3 additions & 0 deletions internal/httpheader/link.go
Expand Up @@ -21,6 +21,9 @@ import (
"strings"
)

// BUG(yuizumi): Link and ParseLink in this package has been deprecated and
// will be removed soon. Use the httplink package instead.

const (
// https://tools.ietf.org/html/rfc7230#section-3.2.6
token = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]+"
Expand Down
66 changes: 66 additions & 0 deletions resource/httplink/link.go
@@ -0,0 +1,66 @@
// Copyright 2019 Google LLC
//
// 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 httplink defines a representation of Web Linkings.
package httplink

import (
"fmt"
"net/url"
"strings"
)

// Link represents a Web Linking [RFC 8288], aka. the Link HTTP header.
type Link struct {
URL *url.URL
Params LinkParams
}

// NewLink creates and initializes a new Link with the provided URL u and
// the provided rel parameter.
func NewLink(u *url.URL, rel string) *Link {
p := make(LinkParams, 1)
p.Set(ParamRel, rel)
return &Link{u, p}
}

// IsPreload reports whether the Link involves preloading of the resource.
func (l *Link) IsPreload() bool {
// TODO(yuizumi): Maybe include rel="prefetch" and similar.
for _, s := range strings.Fields(l.Params.Get(ParamRel)) {
if strings.EqualFold(s, RelPreload) {
return true
}
}
return false
}

// String serializes the Link as it appears in the HTTP header.
func (l *Link) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "<%s>", l.URL)
l.Params.write(&sb)
return sb.String()
}

// GoString implements the GoStringer interface.
func (l *Link) GoString() string {
return fmt.Sprintf("&httplink.Link{URL:&%#v, Params:%#v}", *l.URL, l.Params)
}

// Equal reports whether l and m have the same URL and parameters. The URLs
// are compared by strings.
func (l *Link) Equal(m *Link) bool {
return l.URL.String() == m.URL.String() && l.Params.Equal(m.Params)
}
99 changes: 99 additions & 0 deletions resource/httplink/link_test.go
@@ -0,0 +1,99 @@
// Copyright 2019 Google LLC
//
// 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 httplink_test

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/webpackager/internal/urlutil"
"github.com/google/webpackager/resource/httplink"
)

func TestNewLink(t *testing.T) {
u := urlutil.MustParse("https://example.com/style.css")
rel := "preload"

want := &httplink.Link{u, httplink.LinkParams{"rel": rel}}
got := httplink.NewLink(u, rel)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("NewLink(%q, %q) mismatch (-want +got):\n%s", u, rel, diff)
}
}

func TestString(t *testing.T) {
tests := []struct {
name string
link *httplink.Link
want string
}{
{
name: "AbsoluteURL",
link: &httplink.Link{
URL: urlutil.MustParse("https://example.com/style.css"),
Params: httplink.LinkParams{
httplink.ParamRel: "preload",
httplink.ParamAs: "style",
},
},
want: `<https://example.com/style.css>;rel="preload";as="style"`,
},
{
name: "RelativeURL",
link: &httplink.Link{
URL: urlutil.MustParse("style.css"),
Params: httplink.LinkParams{
httplink.ParamRel: "preload",
httplink.ParamAs: "style",
},
},
want: `<style.css>;rel="preload";as="style"`,
},
{
name: "CrossOrigin_Anonymous",
link: &httplink.Link{
URL: urlutil.MustParse("https://example.org/world.html"),
Params: httplink.LinkParams{
httplink.ParamRel: "preload",
httplink.ParamCrossOrigin: "anonymous",
httplink.ParamAs: "document",
},
},
want: `<https://example.org/world.html>;rel="preload";` +
`as="document";crossorigin`,
},
{
name: "CrossOrigin_UserCredentials",
link: &httplink.Link{
URL: urlutil.MustParse("https://example.org/world.html"),
Params: httplink.LinkParams{
httplink.ParamRel: "preload",
httplink.ParamCrossOrigin: "user-credentials",
httplink.ParamAs: "document",
},
},
want: `<https://example.org/world.html>;rel="preload";` +
`as="document";crossorigin="user-credentials"`,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if diff := cmp.Diff(test.want, test.link.String()); diff != "" {
t.Errorf("link.String() mismatch (-want +got):\n%s", diff)
}
})
}
}
131 changes: 131 additions & 0 deletions resource/httplink/params.go
@@ -0,0 +1,131 @@
// Copyright 2019 Google LLC
//
// 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 httplink

import (
"fmt"
"io"
"reflect"
"sort"
"strings"
)

// A few common parameter names for use with LinkParams.
const (
ParamRel = "rel"
ParamAs = "as"
ParamCrossOrigin = "crossorigin"
ParamMedia = "media"
ParamType = "type"
)

// Special parameter values recognized by LinkParams.
const (
// Value(s) for the "rel" parameter.
RelPreload = "preload"

// Value(s) for the "crossorigin" parameter.
CrossOriginAnonymous = "anonymous"
)

// LinkParams represents the parameters of a Web Linking.
type LinkParams map[string]string

// Get returns the value of the parameter specified by key. key gets lowered,
// thus is case-insensitive.
func (p LinkParams) Get(key string) string {
return p[strings.ToLower(key)]
}

// Set changes the value of the parameter specified by key. key gets lowered,
// thus is case-insensitive. Set also normalizes the provided value for some
// parameters, e.g. removes extra spaces for the rel parameter. To get around
// the normalization, access the map entry directly.
func (p LinkParams) Set(key, val string) {
key = strings.ToLower(key)
p[key] = normalizeValue(key, val)
}

// Clone returns a deep copy of the LinkParams p.
func (p LinkParams) Clone() LinkParams {
q := make(LinkParams, len(p))
for k, v := range p {
q[k] = v
}
return q
}

// Equal reports whether p and q contain the same set of key-value pairs.
func (p LinkParams) Equal(q LinkParams) bool {
return reflect.DeepEqual(p, q)
}

func (p LinkParams) write(w io.Writer) {
keys := make([]string, 0, len(p))
for key := range p {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[j] == ParamRel {
return false
}
if keys[i] == ParamRel {
return true
}
return keys[i] < keys[j]
})
for _, key := range keys {
if shouldElideValue(key, p[key]) {
fmt.Fprintf(w, ";%s", key)
} else {
fmt.Fprintf(w, ";%s=%q", key, p[key])
}
}
}

func normalizeValue(key, val string) string {
switch key {
case ParamRel:
// [RFC 8288] requires the relation types to be compared character
// by character in a case-insensitive fashion, whether they are
// registered (well-known) or external (represented by URIs). Note
// also [RFC 8288] recommends URIs to be all lowercase.
//
// [RFC 8288]: https://tools.ietf.org/html/rfc8288
vals := strings.Fields(val)
for i := range vals {
vals[i] = strings.ToLower(vals[i])
}
return strings.Join(vals, " ")

case ParamAs, ParamType:
return strings.ToLower(val)

case ParamCrossOrigin:
if val == "" {
return CrossOriginAnonymous
}
return strings.ToLower(val)

default:
return val // Do not normalize unknown parameters.
}
}

func shouldElideValue(key, val string) bool {
// Elide the value from `crossorigin="anonymous"` since the bare
// `crossorigin` is presumably more popular.
return key == ParamCrossOrigin && val == CrossOriginAnonymous
}

0 comments on commit 3147822

Please sign in to comment.