Skip to content

Commit 8048ed7

Browse files
authored
gen-bundle: Add support for Variants (#539)
The index section of bundle maps URLs to a Variants value + a list of the responses for each possible Variant-Key (#450). But before this patch gen-bundle could not generate a bundle that have multiple variants for single URL. This patch teaches indexSection.Finalize() to generate entries with non-empty variants-value, based on the responses' Variant and Variant-Key headers [1]. [1] https://tools.ietf.org/html/draft-ietf-httpbis-variants-05
1 parent 743d89d commit 8048ed7

4 files changed

Lines changed: 303 additions & 24 deletions

File tree

‎go/bundle/encoder.go‎

Lines changed: 199 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@ import (
44
"bytes"
55
"crypto/sha256"
66
"encoding/binary"
7+
"errors"
78
"fmt"
89
"io"
10+
"net/http"
911
"net/url"
1012
"strconv"
1113
"strings"
1214

1315
"github.com/WICG/webpackage/go/bundle/version"
1416
"github.com/WICG/webpackage/go/signedexchange/cbor"
17+
"github.com/WICG/webpackage/go/signedexchange/structuredheader"
1518
)
1619

20+
const maxNumVariantsForSingleURL = 10000
21+
1722
func normalizeHeaderValues(values []string) string {
1823
// RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1
1924
// 4.2 Message Headers
@@ -66,13 +71,15 @@ func (r Response) HeaderSha256() ([]byte, error) {
6671

6772
var _ = io.WriterTo(&Bundle{})
6873

69-
type requestEntry struct {
74+
type indexEntry struct {
7075
Request
71-
Offset uint64 // Offset within the responses section
72-
Length uint64
76+
Variants string
77+
VariantKey string
78+
Offset uint64 // Offset within the responses section
79+
Length uint64
7380
}
7481

75-
func (r requestEntry) String() string {
82+
func (r indexEntry) String() string {
7683
return fmt.Sprintf("{URL: %v, Header: %v, Offset: %d, Length: %d}", r.URL, r.Header, r.Offset, r.Length)
7784
}
7885

@@ -84,15 +91,19 @@ type section interface {
8491

8592
// staging area for writing index section
8693
type indexSection struct {
87-
es []requestEntry
94+
es []*indexEntry
8895
bytes []byte
8996
}
9097

91-
func (is *indexSection) addRequest(r Request, offset, length int) error {
92-
ent := requestEntry{
93-
Request: r,
94-
Offset: uint64(offset),
95-
Length: uint64(length),
98+
func (is *indexSection) addExchange(e *Exchange, offset, length int) error {
99+
variants := normalizeHeaderValues(e.Response.Header[http.CanonicalHeaderKey("variants")])
100+
variantKey := normalizeHeaderValues(e.Response.Header[http.CanonicalHeaderKey("variant-key")])
101+
ent := &indexEntry{
102+
Request: e.Request,
103+
Variants: variants,
104+
VariantKey: variantKey,
105+
Offset: uint64(offset),
106+
Length: uint64(length),
96107
}
97108
is.es = append(is.es, ent)
98109
return nil
@@ -111,25 +122,42 @@ func (is *indexSection) Finalize(ver version.Version) error {
111122
// index = {* whatwg-url => [ variants-value, +location-in-responses ] }
112123
// variants-value = bstr
113124
// location-in-responses = (offset: uint, length: uint)
114-
mes := []*cbor.MapEntryEncoder{}
125+
m := make(map[string][]*indexEntry)
115126
for _, e := range is.es {
116-
me := cbor.GenerateMapEntry(func(keyE *cbor.Encoder, valueE *cbor.Encoder) {
117-
if err := keyE.EncodeTextString(e.URL.String()); err != nil {
118-
panic(err)
127+
url := e.URL.String()
128+
m[url] = append(m[url], e)
129+
}
130+
131+
mes := []*cbor.MapEntryEncoder{}
132+
for url, es := range m {
133+
var variantsValue []byte
134+
if len(es) > 1 {
135+
variantsValue = []byte(es[0].Variants)
136+
var err error
137+
es, err = entriesInPossibleKeyOrder(es)
138+
if err != nil {
139+
return fmt.Errorf("bundle: cannot construct index entry for %s: %v", url, err)
119140
}
120-
// Currently, this encoder does not support variants. So, the
121-
// map value is always a three-element array ['', offset, length].
122-
if err := valueE.EncodeArrayHeader(3); err != nil {
141+
}
142+
143+
me := cbor.GenerateMapEntry(func(keyE *cbor.Encoder, valueE *cbor.Encoder) {
144+
if err := keyE.EncodeTextString(url); err != nil {
123145
panic(err)
124146
}
125-
if err := valueE.EncodeByteString(nil); err != nil {
147+
148+
if err := valueE.EncodeArrayHeader(1 + len(es)*2); err != nil {
126149
panic(err)
127150
}
128-
if err := valueE.EncodeUint(e.Offset); err != nil {
151+
if err := valueE.EncodeByteString(variantsValue); err != nil {
129152
panic(err)
130153
}
131-
if err := valueE.EncodeUint(e.Length); err != nil {
132-
panic(err)
154+
for _, e := range es {
155+
if err := valueE.EncodeUint(e.Offset); err != nil {
156+
panic(err)
157+
}
158+
if err := valueE.EncodeUint(e.Length); err != nil {
159+
panic(err)
160+
}
133161
}
134162
})
135163
mes = append(mes, me)
@@ -190,6 +218,155 @@ func (is *indexSection) Finalize(ver version.Version) error {
190218
return nil
191219
}
192220

221+
// entriesInPossibleKeyOrder reorders es by VariantKey, in the order they should
222+
// appear in the index section; the row-major order of possible keys for
223+
// Variants. All entries in es must have the same Variants value.
224+
//
225+
// For example, if the Variants value is
226+
// "Accept-Language;en;fr, Accept-Encoding:gzip;br", the result would satisfy
227+
// this:
228+
// result[0].VariantKey == "en;gzip"
229+
// result[1].VariantKey == "en;br"
230+
// result[2].VariantKey == "fr;gzip"
231+
// result[3].VariantKey == "fr;br"
232+
//
233+
// Note that VariantKey can have multiple keys (e.g. "en;gzip, fr;gzip"). Such
234+
// entrys will appear multiple times in the result. e.g.:
235+
// result[0].VariantKey == "en;gzip, fr;gzip"
236+
// result[1].VariantKey == "en;br"
237+
// result[2] == result[0]
238+
// result[3].VariantKey == "fr;br"
239+
//
240+
// If entries in es do not cover all combination of possible keys or two entries
241+
// have overwrapping possible keys, this returns an error.
242+
func entriesInPossibleKeyOrder(es []*indexEntry) ([]*indexEntry, error) {
243+
if es[0].Variants == "" {
244+
return nil, errors.New("no Variants header")
245+
}
246+
variants, err := parseVariants(es[0].Variants)
247+
if err != nil {
248+
return nil, fmt.Errorf("cannot parse Variants header value %q: %v", es[0].Variants, err)
249+
}
250+
numPossibleKeys, err := variants.numberOfPossibleKeys()
251+
if err != nil {
252+
return nil, fmt.Errorf("invalid Variants header value %q: %v", es[0].Variants, err)
253+
}
254+
255+
result := make([]*indexEntry, numPossibleKeys)
256+
for _, e := range es {
257+
// TODO: Compare Variants values as lists
258+
// (e.g. "Accept;foo;bar" == "Accept; foo; bar").
259+
if e.Variants != es[0].Variants {
260+
return nil, fmt.Errorf("inconsistent Variants value. %q != %q", e.Variants, es[0].Variants)
261+
}
262+
vks, err := parseListOfStringLists(e.VariantKey)
263+
if err != nil {
264+
return nil, fmt.Errorf("cannot parse Variant-Key header %q: %v", e.VariantKey, err)
265+
}
266+
for _, vk := range vks {
267+
i := variants.indexInPossibleKeys(vk)
268+
if i == -1 {
269+
return nil, fmt.Errorf("Variant-Key %q is not covered by variants %q", e.VariantKey, e.Variants)
270+
}
271+
if result[i] != nil {
272+
return nil, fmt.Errorf("duplicated entries with Variant-Key %q", vk)
273+
}
274+
result[i] = e
275+
}
276+
}
277+
for i, e := range result {
278+
if e == nil {
279+
return nil, fmt.Errorf("no entry for Variant-Key %v", variants.possibleKeyAt(i))
280+
}
281+
}
282+
return result, nil
283+
}
284+
285+
func parseListOfStringLists(s string) ([][]string, error) {
286+
ll, err := structuredheader.ParseListOfLists(s)
287+
if err != nil {
288+
return nil, err
289+
}
290+
// Convert [][]structuredheader.Item to [][]string.
291+
var result [][]string
292+
for _, l := range ll {
293+
var sl []string
294+
for _, item := range l {
295+
switch v := item.(type) {
296+
case string:
297+
sl = append(sl, v)
298+
case structuredheader.Token:
299+
sl = append(sl, string(v))
300+
default:
301+
return nil, fmt.Errorf("unexpected value of type %T", v)
302+
}
303+
}
304+
result = append(result, sl)
305+
}
306+
return result, nil
307+
}
308+
309+
// Variants represents a Variants: header value.
310+
type Variants [][]string
311+
312+
func parseVariants(s string) (Variants, error) {
313+
vs, err := parseListOfStringLists(s)
314+
return Variants(vs), err
315+
}
316+
317+
func (v Variants) numberOfPossibleKeys() (int, error) {
318+
n := 1
319+
for _, vals := range v {
320+
// vals is [header-name, possible-value1, possible-value2, ...]
321+
if len(vals) <= 1 {
322+
return 0, errors.New("no possible key")
323+
}
324+
n *= len(vals) - 1
325+
if n > maxNumVariantsForSingleURL {
326+
return 0, errors.New("too many possible keys")
327+
}
328+
}
329+
return n, nil
330+
}
331+
332+
// indexInPossibleKeys returns the index of variantKey within the all possible
333+
// key combinations for v. If variantKey is not a possible key for v, this
334+
// returns -1.
335+
func (v Variants) indexInPossibleKeys(variantKey []string) int {
336+
if len(v) != len(variantKey) {
337+
return -1
338+
}
339+
340+
index := 0
341+
OuterLoop:
342+
for i, vals := range v {
343+
vals = vals[1:] // Drop header-name
344+
for indexInAxis, val := range vals {
345+
if val == variantKey[i] {
346+
index = index*len(vals) + indexInAxis
347+
continue OuterLoop
348+
}
349+
}
350+
return -1
351+
}
352+
return index
353+
}
354+
355+
// possibleKeyAt returns a variant key at given index within the all possible
356+
// key combinations for v, or nil if index is out of range.
357+
func (v Variants) possibleKeyAt(index int) []string {
358+
keys := make([]string, len(v))
359+
for i := len(v) - 1; i >= 0; i-- {
360+
vals := v[i][1:] // Drop header-name
361+
keys[i] = vals[index%len(vals)]
362+
index /= len(vals)
363+
}
364+
if index != 0 {
365+
return nil // index out of range
366+
}
367+
return keys
368+
}
369+
193370
func (is *indexSection) Name() string {
194371
return "index"
195372
}
@@ -318,7 +495,7 @@ func addExchange(is *indexSection, rs *responsesSection, e *Exchange) error {
318495
return err
319496
}
320497

321-
if err := is.addRequest(e.Request, offset, length); err != nil {
498+
if err := is.addExchange(e, offset, length); err != nil {
322499
return err
323500
}
324501
return nil

‎go/bundle/encoder_test.go‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package bundle
2+
3+
import (
4+
"net/http"
5+
"net/url"
6+
"reflect"
7+
"testing"
8+
9+
"github.com/WICG/webpackage/go/bundle/version"
10+
"github.com/WICG/webpackage/go/internal/testhelper"
11+
)
12+
13+
func urlMustParse(rawurl string) *url.URL {
14+
u, err := url.Parse(rawurl)
15+
if err != nil {
16+
panic(err)
17+
}
18+
return u
19+
}
20+
21+
func TestVariants(t *testing.T) {
22+
variants, err := parseVariants("Accept-Encoding;gzip;br, Accept-Language;en;fr;ja")
23+
if err != nil {
24+
t.Errorf("parseListOfStringLists unexpectedly failed: %v", err)
25+
}
26+
if nk, err := variants.numberOfPossibleKeys(); nk != 6 || err != nil {
27+
t.Errorf("numberOfPossibleKeys: got: (%v, %v) want: (%v, %v)", nk, err, 6, nil)
28+
}
29+
30+
cases := []struct {
31+
index int
32+
variantKey []string
33+
}{
34+
{0, []string{"gzip", "en"}},
35+
{1, []string{"gzip", "fr"}},
36+
{2, []string{"gzip", "ja"}},
37+
{3, []string{"br", "en"}},
38+
{4, []string{"br", "fr"}},
39+
{5, []string{"br", "ja"}},
40+
{-1, []string{"gzip", "es"}},
41+
{-1, []string{}},
42+
{-1, []string{"gzip"}},
43+
{-1, []string{"gzip", "en", "foo"}},
44+
}
45+
for _, c := range cases {
46+
if i := variants.indexInPossibleKeys(c.variantKey); i != c.index {
47+
t.Errorf("indexInPossibleKeys: got: %v want: %v", i, c.index)
48+
}
49+
50+
if c.index != -1 {
51+
key := variants.possibleKeyAt(c.index)
52+
if !reflect.DeepEqual(key, c.variantKey) {
53+
t.Errorf("possibleKeyAt(%d): got: %v\nwant: %v", c.index, key, c.variantKey)
54+
}
55+
}
56+
}
57+
}
58+
59+
func TestIndexSectionWithVariants(t *testing.T) {
60+
url := urlMustParse("https://example.com/")
61+
variants := []string{"Accept-Encoding;gzip;br, Accept-Language;en;fr"}
62+
is := &indexSection{}
63+
is.addExchange(
64+
&Exchange{
65+
Request{URL: url},
66+
Response{Header: http.Header{
67+
"Variants": variants,
68+
"Variant-Key": []string{"gzip;fr, br;en"},
69+
}},
70+
}, 20, 2)
71+
is.addExchange(
72+
&Exchange{
73+
Request{URL: url},
74+
Response{Header: http.Header{
75+
"Variants": variants,
76+
"Variant-Key": []string{"gzip;en"},
77+
}},
78+
}, 10, 1)
79+
is.addExchange(
80+
&Exchange{
81+
Request{URL: url},
82+
Response{Header: http.Header{
83+
"Variants": variants,
84+
"Variant-Key": []string{"br;fr"},
85+
}},
86+
}, 30, 3)
87+
if err := is.Finalize(version.VersionB1); err != nil {
88+
t.Fatal(err)
89+
}
90+
91+
want := `map["https://example.com/":["Accept-Encoding;gzip;br, Accept-Language;en;fr" 10 1 20 2 20 2 30 3]]`
92+
93+
got, err := testhelper.CborBinaryToReadableString(is.bytes)
94+
if err != nil {
95+
t.Fatal(err)
96+
}
97+
if got != want {
98+
t.Errorf("got: %s\nwant: %s", got, want)
99+
}
100+
}

go/signedexchange/internal/testhelper/cbor.go renamed to go/internal/testhelper/cbor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ func readableString(v interface{}) string {
3737
return "map[" + strings.Join(vals, " ") + "]"
3838
case string, []byte:
3939
return fmt.Sprintf("%q", v)
40+
case uint64:
41+
return fmt.Sprintf("%d", v)
4042
default:
4143
panic(fmt.Sprintf("not supported type: %T", v))
4244
}

0 commit comments

Comments
 (0)