Repository: dragonflyoss/Dragonfly2
Pull request #250 (2021-05-24 07:34:48)

Feature: support basic auth for proxy
<!--- Provide a general summary of your changes in the Title above -->

## Description

1. Support basic auth for proxy
2. Merge client StorageSize to unit.Bytes
3. Fix some test

## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

## Screenshots (if appropriate):

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [x] Documentation Update (if none of the other choices apply)

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] Code compiles correctly.
- [x] My code follows the code style of this project.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have read the **CONTRIBUTING** document.
- [x] I have added tests to cover my changes.
- [ ] All new and existing tests passed.



Patch:
diff --git a/client/clientutil/types.go b/client/clientutil/types.go
index f3592069..56e18265 100644
--- a/client/clientutil/types.go
+++ b/client/clientutil/types.go
@@ -124,39 +124,3 @@ func (d *Duration) unmarshal(v interface{}) error {
 		return errors.New("invalid duration")
 	}
 }
-
-type StorageSize struct {
-	SizeInBytes int64
-}
-
-func (s *StorageSize) UnmarshalJSON(b []byte) error {
-	return s.unmarshal(json.Unmarshal, b)
-}
-
-func (s *StorageSize) UnmarshalYAML(node *yaml.Node) error {
-	return s.unmarshal(yaml.Unmarshal, []byte(node.Value))
-}
-
-func (s *StorageSize) unmarshal(unmarshal func(in []byte, out interface{}) (err error), b []byte) error {
-	var v interface{}
-	if err := unmarshal(b, &v); err != nil {
-		return err
-	}
-	switch value := v.(type) {
-	case float64:
-		s.SizeInBytes = int64(value)
-		return nil
-	case int:
-		s.SizeInBytes = int64(value)
-		return nil
-	case string:
-		size, err := units.RAMInBytes(value)
-		if err != nil {
-			return errors.WithMessage(err, "invalid storage size")
-		}
-		s.SizeInBytes = size
-		return nil
-	default:
-		return errors.New("invalid storage size")
-	}
-}
diff --git a/client/config/peerhost.go b/client/config/peerhost.go
index ef417fe0..861de66c 100644
--- a/client/config/peerhost.go
+++ b/client/config/peerhost.go
@@ -31,6 +31,7 @@ import (
 	"time"
 
 	"d7y.io/dragonfly/v2/cmd/dependency/base"
+	"d7y.io/dragonfly/v2/pkg/unit"
 	"d7y.io/dragonfly/v2/pkg/util/stringutils"
 	"github.com/pkg/errors"
 	"gopkg.in/yaml.v3"
@@ -153,6 +154,7 @@ type DownloadOption struct {
 type ProxyOption struct {
 	// WARNING: when add more option, please update ProxyOption.unmarshal function
 	ListenOption   `mapstructure:",squash" yaml:",inline"`
+	BasicAuth      *BasicAuth      `mapstructure:"basic_auth" yaml:"basic_auth"`
 	DefaultFilter  string          `mapstructure:"default_filter" yaml:"default_filter"`
 	MaxConcurrency int64           `mapstructure:"max_concurrency" yaml:"max_concurrency"`
 	RegistryMirror *RegistryMirror `mapstructure:"registry_mirror" yaml:"registry_mirror"`
@@ -242,6 +244,7 @@ func (p *ProxyOption) unmarshal(unmarshal func(in []byte, out interface{}) (err
 		Proxies        []*Proxy        `json:"proxies" yaml:"proxies"`
 		HijackHTTPS    *HijackConfig   `json:"hijack_https" yaml:"hijack_https"`
 		WhiteList      []*WhiteList    `json:"white_list" yaml:"white_list"`
+		BasicAuth      *BasicAuth      `json:"basic_auth" yaml:"basic_auth"`
 	}{}
 
 	if err := unmarshal(b, &pt); err != nil {
@@ -255,6 +258,7 @@ func (p *ProxyOption) unmarshal(unmarshal func(in []byte, out interface{}) (err
 	p.WhiteList = pt.WhiteList
 	p.MaxConcurrency = pt.MaxConcurrency
 	p.DefaultFilter = pt.DefaultFilter
+	p.BasicAuth = pt.BasicAuth
 
 	return nil
 }
@@ -381,9 +385,10 @@ type StorageOption struct {
 	DataPath string `mapstructure:"data_path" yaml:"data_path"`
 	// TaskExpireTime indicates caching duration for which cached file keeps no accessed by any process,
 	// after this period cache file will be gc
-	TaskExpireTime  clientutil.Duration    `mapstructure:"task_expire_time" yaml:"task_expire_time"`
-	StoreStrategy   StoreStrategy          `mapstructure:"strategy" yaml:"strategy"`
-	DiskGCThreshold clientutil.StorageSize `mapstructure:"disk_gc_threshold" yaml:"disk_gc_threshold"`
+	TaskExpireTime clientutil.Duration `mapstructure:"task_expire_time" yaml:"task_expire_time"`
+	// DiskGCThreshold indicates the threshold to gc the oldest tasks
+	DiskGCThreshold unit.Bytes    `mapstructure:"disk_gc_threshold" yaml:"disk_gc_threshold"`
+	StoreStrategy   StoreStrategy `mapstructure:"strategy" yaml:"strategy"`
 }
 
 type StoreStrategy string
@@ -686,3 +691,8 @@ type WhiteList struct {
 	Regx  *Regexp  `yaml:"regx" mapstructure:"regx"`
 	Ports []string `yaml:"ports" mapstructure:"ports"`
 }
+
+type BasicAuth struct {
+	Username string `json:"username" yaml:"username"`
+	Password string `json:"password" yaml:"password"`
+}
diff --git a/client/daemon/peerhost.go b/client/daemon/peerhost.go
index 9ddbfef3..a3e06994 100644
--- a/client/daemon/peerhost.go
+++ b/client/daemon/peerhost.go
@@ -76,7 +76,6 @@ type peerHost struct {
 }
 
 func New(opt *config.PeerHostOption) (PeerHost, error) {
-
 	host := &scheduler.PeerHost{
 		Uuid:           idgen.UUIDString(),
 		Ip:             opt.Host.AdvertiseIP,
diff --git a/client/daemon/proxy/proxy.go b/client/daemon/proxy/proxy.go
index 781465b0..8bf9f591 100644
--- a/client/daemon/proxy/proxy.go
+++ b/client/daemon/proxy/proxy.go
@@ -18,11 +18,13 @@ package proxy
 
 import (
 	"crypto/tls"
+	"encoding/base64"
 	"fmt"
 	"io"
 	"net"
 	"net/http"
 	"net/http/httputil"
+	"strings"
 	"sync"
 	"time"
 
@@ -32,6 +34,7 @@ import (
 	logger "d7y.io/dragonfly/v2/pkg/dflog"
 	"d7y.io/dragonfly/v2/pkg/rpc/scheduler"
 	"d7y.io/dragonfly/v2/pkg/util/stringutils"
+	"github.com/go-http-utils/headers"
 	"github.com/golang/groupcache/lru"
 	"github.com/pkg/errors"
 	"go.opentelemetry.io/otel"
@@ -85,6 +88,8 @@ type Proxy struct {
 
 	// tracer is used for telemetry
 	tracer trace.Tracer
+
+	basicAuth *config.BasicAuth
 }
 
 // Option is a functional option for configuring the proxy
@@ -175,6 +180,14 @@ func WithDefaultFilter(f string) Option {
 	}
 }
 
+// WithBasicAuth sets basic auth info for proxy
+func WithBasicAuth(auth *config.BasicAuth) Option {
+	return func(p *Proxy) *Proxy {
+		p.basicAuth = auth
+		return p
+	}
+}
+
 // NewProxy returns a new transparent proxy from the given options
 func NewProxy(options ...Option) (*Proxy, error) {
 	return NewProxyWithOptions(options...)
@@ -208,6 +221,24 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// TODO(jim): only support HTTP scheme, need support HTTPS scheme
 	r = r.WithContext(ctx)
 
+	// check authenticity
+	if proxy.basicAuth != nil {
+		user, pass, ok := proxyBasicAuth(r)
+		if !ok {
+			status := http.StatusProxyAuthRequired
+			http.Error(w, http.StatusText(status), status)
+			logger.Debugf("empty auth info: %s, url：%s", r.Host, r.URL.String())
+			return
+		}
+		// TODO dynamic auth config via manager
+		if user != proxy.basicAuth.Username || pass != proxy.basicAuth.Password {
+			status := http.StatusUnauthorized
+			http.Error(w, http.StatusText(status), status)
+			logger.Debugf("mismatch auth info (%s/%s): %s, url：%s", user, pass, r.Host, r.URL.String())
+			return
+		}
+	}
+
 	// check whiteList
 	if !proxy.checkWhiteList(r) {
 		status := http.StatusUnauthorized
@@ -240,6 +271,34 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 }
 
+func proxyBasicAuth(r *http.Request) (username, password string, ok bool) {
+	auth := r.Header.Get(headers.ProxyAuthorization)
+	if auth == "" {
+		return
+	}
+	return parseBasicAuth(auth)
+}
+
+// parseBasicAuth parses an HTTP Basic Authentication string.
+// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
+func parseBasicAuth(auth string) (username, password string, ok bool) {
+	const prefix = "Basic "
+	// Case insensitive prefix match. See Issue 22736.
+	if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
+		return
+	}
+	c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
+	if err != nil {
+		return
+	}
+	cs := string(c)
+	s := strings.IndexByte(cs, ':')
+	if s < 0 {
+		return
+	}
+	return cs[:s], cs[s+1:], true
+}
+
 func (proxy *Proxy) handleHTTP(span trace.Span, w http.ResponseWriter, req *http.Request) {
 	resp, err := proxy.newTransport(nil).RoundTrip(req)
 	if err != nil {
diff --git a/client/daemon/proxy/proxy_manager.go b/client/daemon/proxy/proxy_manager.go
index f7c03ba0..ac3b7cdf 100644
--- a/client/daemon/proxy/proxy_manager.go
+++ b/client/daemon/proxy/proxy_manager.go
@@ -61,6 +61,7 @@ func NewProxyManager(peerHost *scheduler.PeerHost, peerTaskManager peer.PeerTask
 		WithWhiteList(whiteList),
 		WithMaxConcurrency(opts.MaxConcurrency),
 		WithDefaultFilter(opts.DefaultFilter),
+		WithBasicAuth(opts.BasicAuth),
 	}
 
 	if registry != nil {
diff --git a/client/daemon/storage/storage_manager.go b/client/daemon/storage/storage_manager.go
index b0a08b8d..5d66f6a3 100644
--- a/client/daemon/storage/storage_manager.go
+++ b/client/daemon/storage/storage_manager.go
@@ -470,7 +470,7 @@ func (s *storageManager) TryGC() (bool, error) {
 		return true
 	})
 
-	if s.storeOption.DiskGCThreshold.SizeInBytes > 0 && totalNotMarkedSize > s.storeOption.DiskGCThreshold.SizeInBytes {
+	if s.storeOption.DiskGCThreshold > 0 && totalNotMarkedSize > int64(s.storeOption.DiskGCThreshold) {
 		logger.Infof("quota threshold reached, start gc oldest task")
 		var tasks []*localTaskStore
 		s.tasks.Range(func(key, task interface{}) bool {
@@ -491,7 +491,7 @@ func (s *storageManager) TryGC() (bool, error) {
 				task.TaskID, task.PeerID, task.lastAccess.Format(time.RFC3339Nano),
 				units.BytesSize(float64(task.ContentLength)))
 			totalNotMarkedSize -= task.ContentLength
-			if totalNotMarkedSize < s.storeOption.DiskGCThreshold.SizeInBytes {
+			if totalNotMarkedSize < int64(s.storeOption.DiskGCThreshold) {
 				break
 			}
 		}
diff --git a/cmd/dependency/dependency.go b/cmd/dependency/dependency.go
index 9aa63b06..35fb72f2 100644
--- a/cmd/dependency/dependency.go
+++ b/cmd/dependency/dependency.go
@@ -201,8 +201,7 @@ func initDecoderConfig(dc *mapstructure.DecoderConfig) {
 			reflect.TypeOf(config.FileString("")),
 			reflect.TypeOf(config.URL{}),
 			reflect.TypeOf(config.CertPool{}),
-			reflect.TypeOf(config.Regexp{}),
-			reflect.TypeOf(clientutil.StorageSize{}):
+			reflect.TypeOf(config.Regexp{}):
 
 			b, _ := yaml.Marshal(v)
 			p := reflect.New(to)
diff --git a/go.mod b/go.mod
index 5507daf4..09d6f9de 100644
--- a/go.mod
+++ b/go.mod
@@ -50,7 +50,7 @@ require (
 	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect
-	golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
+	golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
 	golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
 	google.golang.org/grpc v1.36.0
 	google.golang.org/protobuf v1.25.0
diff --git a/pkg/unit/byte.go b/pkg/unit/byte.go
index 8d476165..3ba0fa76 100644
--- a/pkg/unit/byte.go
+++ b/pkg/unit/byte.go
@@ -17,6 +17,7 @@
 package unit
 
 import (
+	"encoding/json"
 	"fmt"
 	"regexp"
 	"strconv"
@@ -24,6 +25,7 @@ import (
 
 	"d7y.io/dragonfly/v2/pkg/util/stringutils"
 	"github.com/pkg/errors"
+	"gopkg.in/yaml.v3"
 )
 
 type Bytes int64
@@ -35,6 +37,7 @@ const (
 	GB       = 1024 * MB
 	TB       = 1024 * GB
 	PB       = 1024 * TB
+	EB       = 1024 * PB
 )
 
 func (f Bytes) ToNumber() int64 {
@@ -89,7 +92,7 @@ func (f Bytes) String() string {
 	return fmt.Sprintf("%.1f%s", float64(f)/float64(unit), symbol)
 }
 
-var sizeRegexp = regexp.MustCompile(`^([0-9]+)(\.0*)?(MB?|m|KB?|k|GB?|g|B)?$`)
+var sizeRegexp = regexp.MustCompile(`^([0-9]+)(\.0*)?([MmKkGgTtPpEe])?[iI]?[bB]?$`)
 
 func parseSize(fsize string) (Bytes, error) {
 	fsize = strings.TrimSpace(fsize)
@@ -104,12 +107,18 @@ func parseSize(fsize string) (Bytes, error) {
 
 	var unit Bytes
 	switch matches[3] {
-	case "k", "K", "KB":
+	case "k", "K":
 		unit = KB
-	case "m", "M", "MB":
+	case "m", "M":
 		unit = MB
-	case "g", "G", "GB":
+	case "g", "G":
 		unit = GB
+	case "t", "T":
+		unit = TB
+	case "p", "P":
+		unit = PB
+	case "e", "E":
+		unit = EB
 	default:
 		unit = B
 	}
@@ -127,17 +136,37 @@ func (f Bytes) MarshalYAML() (interface{}, error) {
 	return result, nil
 }
 
-// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (f *Bytes) UnmarshalYAML(unmarshal func(interface{}) error) error {
-	var fsizeStr string
-	if err := unmarshal(&fsizeStr); err != nil {
-		return err
-	}
+func (f *Bytes) UnmarshalJSON(b []byte) error {
+	return f.unmarshal(json.Unmarshal, b)
+}
 
-	fsize, err := parseSize(fsizeStr)
-	if err != nil {
+func (f *Bytes) UnmarshalYAML(node *yaml.Node) error {
+	return f.unmarshal(yaml.Unmarshal, []byte(node.Value))
+}
+
+func (f *Bytes) unmarshal(unmarshal func(in []byte, out interface{}) (err error), b []byte) error {
+	var v interface{}
+	if err := unmarshal(b, &v); err != nil {
 		return err
 	}
-	*f = fsize
-	return nil
+	switch value := v.(type) {
+	case float64:
+		*f = Bytes(int64(value))
+		return nil
+	case int:
+		*f = Bytes(int64(value))
+		return nil
+	case int64:
+		*f = Bytes(value)
+		return nil
+	case string:
+		size, err := parseSize(value)
+		if err != nil {
+			return errors.WithMessage(err, "invalid byte size")
+		}
+		*f = size
+		return nil
+	default:
+		return errors.New("invalid byte size")
+	}
 }


Test patch:
diff --git a/client/config/peerhost_test.go b/client/config/peerhost_test.go
index faa6417b..efa17084 100644
--- a/client/config/peerhost_test.go
+++ b/client/config/peerhost_test.go
@@ -23,6 +23,7 @@ import (
 	"testing"
 	"time"
 
+	"d7y.io/dragonfly/v2/pkg/unit"
 	"gopkg.in/yaml.v3"
 
 	testifyassert "github.com/stretchr/testify/assert"
@@ -145,11 +146,9 @@ listen:
 disk_gc_threshold: 1Ki
 `,
 			target: &struct {
-				Size clientutil.StorageSize `yaml:"disk_gc_threshold"`
+				Size unit.Bytes `yaml:"disk_gc_threshold"`
 			}{
-				Size: clientutil.StorageSize{
-					SizeInBytes: 1024,
-				},
+				Size: unit.Bytes(1024),
 			},
 		},
 	}
diff --git a/client/daemon/peer/piece_manager_test.go b/client/daemon/peer/piece_manager_test.go
index 388a7bf4..beb2878f 100644
--- a/client/daemon/peer/piece_manager_test.go
+++ b/client/daemon/peer/piece_manager_test.go
@@ -124,7 +124,7 @@ func TestPieceManager_DownloadSource(t *testing.T) {
 		func() {
 			/********** prepare test start **********/
 			mockPeerTask := NewMockPeerTask(ctrl)
-			mockPeerTask.EXPECT().SetContentLength(gomock.Any()).DoAndReturn(
+			mockPeerTask.EXPECT().SetContentLength(gomock.Any()).AnyTimes().DoAndReturn(
 				func(arg0 int64) error {
 					return nil
 				})
diff --git a/client/daemon/transport/transport_test.go b/client/daemon/transport/transport_test.go
index a0995c87..571fb15e 100644
--- a/client/daemon/transport/transport_test.go
+++ b/client/daemon/transport/transport_test.go
@@ -134,14 +134,14 @@ func TestTransport_mapToHeader(t *testing.T) {
 		{
 			name: "normal conversion",
 			m: map[string]string{
-				"foo": "foo",
-				"bar": "bar",
+				"Foo": "foo",
+				"Bar": "bar",
 			},
 			expect: func(t *testing.T, data interface{}) {
 				assert := testifyassert.New(t)
 				assert.EqualValues(data, http.Header{
-					"foo": {"foo"},
-					"bar": {"bar"},
+					"Foo": {"foo"},
+					"Bar": {"bar"},
 				})
 			},
 		},
@@ -174,26 +174,26 @@ func TestTransport_pickHeader(t *testing.T) {
 		{
 			name: "Pick the existing key",
 			header: http.Header{
-				"foo": {"foo"},
-				"bar": {"bar"},
+				"Foo": {"foo"},
+				"Bar": {"bar"},
 			},
-			key:          "foo",
+			key:          "Foo",
 			defaultValue: "",
 			expect: func(t *testing.T, data string, header http.Header) {
 				assert := testifyassert.New(t)
-				assert.Equal(data, "foo")
-				assert.Equal(header.Get("foo"), "")
+				assert.Equal("foo", data)
+				assert.Equal("", header.Get("Foo"))
 			},
 		},
 		{
 			name:         "Pick the non-existent key",
 			header:       http.Header{},
-			key:          "foo",
+			key:          "Foo",
 			defaultValue: "bar",
 			expect: func(t *testing.T, data string, header http.Header) {
 				assert := testifyassert.New(t)
 				assert.Equal(data, "bar")
-				assert.Equal(header.Get("foo"), "")
+				assert.Equal(header.Get("Foo"), "")
 			},
 		},
 	}
diff --git a/pkg/unit/byte_test.go b/pkg/unit/byte_test.go
new file mode 100644
index 00000000..bf6fc3f4
--- /dev/null
+++ b/pkg/unit/byte_test.go
@@ -0,0 +1,164 @@
+/*
+ *     Copyright 2020 The Dragonfly 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 unit
+
+import (
+	"testing"
+
+	testifyassert "github.com/stretchr/testify/assert"
+	"gopkg.in/yaml.v3"
+)
+
+func Test_parseByte(t *testing.T) {
+	assert := testifyassert.New(t)
+	testCases := []struct {
+		data   []string
+		size   int64
+		failed bool
+	}{
+		{
+			data: []string{
+				"1Ka",
+				"1Kia",
+				"1KBa",
+				"1KiBa",
+				"1024a",
+			},
+			failed: true,
+		},
+		{
+			data: []string{
+				"1K",
+				"1Ki",
+				"1KB",
+				"1KiB",
+				"1024",
+			},
+			size: 1024,
+		},
+		{
+			data: []string{
+				"1024K",
+				"1024Ki",
+				"1M",
+				"1Mi",
+			},
+			size: int64(MB),
+		},
+		{
+			data: []string{
+				"1024M",
+				"1024Mi",
+				"1G",
+				"1Gi",
+			},
+			size: int64(GB),
+		},
+		{
+			data: []string{
+				"1024G",
+				"1024Gi",
+				"1T",
+				"1Ti",
+			},
+			size: int64(TB),
+		},
+		{
+			data: []string{
+				"1024T",
+				"1024Ti",
+				"1P",
+				"1Pi",
+			},
+			size: int64(PB),
+		},
+		{
+			data: []string{
+				"1024P",
+				"1024Pi",
+				"1E",
+				"1Ei",
+			},
+			size: int64(EB),
+		},
+	}
+
+	for _, tc := range testCases {
+		for _, d := range tc.data {
+			b, err := parseSize(d)
+			if tc.failed {
+				assert.NotNil(err)
+			} else {
+				assert.Nil(err)
+			}
+			if err != nil {
+				continue
+			}
+			assert.Equal(tc.size, b.ToNumber())
+		}
+	}
+}
+
+func TestByteUnmarshal(t *testing.T) {
+	assert := testifyassert.New(t)
+	testCases := []struct {
+		data   string
+		size   int64
+		failed bool
+	}{
+		{
+			data: `
+size: 1K
+`,
+			size: 1024,
+		},
+		{
+			data: `
+size: 1Ki
+`,
+			size: 1024,
+		},
+		{
+			data: `
+size: 1Mi
+`,
+			size: int64(MB),
+		},
+		{
+			data: `
+size: 1Mix
+`,
+			size:   int64(MB),
+			failed: true,
+		},
+	}
+	for _, tc := range testCases {
+		data := struct {
+			Size Bytes `yaml:"size"`
+		}{}
+		err := yaml.Unmarshal([]byte(tc.data), &data)
+		if tc.failed {
+			assert.NotNil(err)
+		} else {
+			assert.Nil(err)
+		}
+		if err != nil {
+			continue
+		}
+		assert.Equal(tc.size, data.Size.ToNumber())
+	}
+}