Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add read and verify utility method #324

Merged
merged 11 commits into from
Sep 21, 2022
135 changes: 135 additions & 0 deletions content/reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
Copyright The ORAS 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 content

import (
"errors"
"fmt"
"io"

"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

var (
// ErrInvalidDescriptorSize is returned by ReadAll() when
// the descriptor has an invalid size.
ErrInvalidDescriptorSize = errors.New("invalid descriptor size")

// ErrMismatchedDigest is returned by ReadAll() when
// the descriptor has an invalid digest.
ErrMismatchedDigest = errors.New("mismatched digest")

// ErrTrailingData is returned by ReadAll() when
// there exists trailing data unread when the read terminates.
ErrTrailingData = errors.New("trailing data")
)

// VerifyReader reads the content described by its descriptor and verifies
// against its size and digest.
type VerifyReader struct {
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
base *io.LimitedReader
verifier digest.Verifier
verified bool
err error
}

// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered.
func (vr *VerifyReader) Read(p []byte) (n int, err error) {
if vr.err != nil {
return 0, vr.err
}

n, err = vr.base.Read(p)
if err != nil {
if err == io.EOF && vr.base.N > 0 {
err = io.ErrUnexpectedEOF
}
vr.err = err
}
return
}

// Verify verifies the read content against the size and the digest.
func (vr *VerifyReader) Verify() error {
if vr.verified {
return nil
}
if vr.err == nil {
if vr.base.N > 0 {
return errors.New("early verify")
}
} else if vr.err != io.EOF {
return vr.err
}

if err := ensureEOF(vr.base.R); err != nil {
vr.err = err
return vr.err
}
if !vr.verifier.Verified() {
vr.err = ErrMismatchedDigest
return vr.err
}

vr.verified = true
vr.err = io.EOF
return nil
}

// NewVerifyReader returns a pointer to a new VerifyReader.
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
func NewVerifyReader(r io.Reader, desc ocispec.Descriptor) *VerifyReader {
verifier := desc.Digest.Verifier()
lr := &io.LimitedReader{
R: io.TeeReader(r, verifier),
N: desc.Size,
}
return &VerifyReader{
base: lr,
verifier: verifier,
}
}

// ReadAll safely reads the content described by the descriptor.
// The read content is verified against the size and the digest
// using a VerifyReader.
func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) {
if desc.Size < 0 {
return nil, ErrInvalidDescriptorSize
}
buf := make([]byte, desc.Size)

vr := NewVerifyReader(r, desc)
if _, err := io.ReadFull(vr, buf); err != nil {
return nil, fmt.Errorf("read failed: %w", err)
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
}
if err := vr.Verify(); err != nil {
return nil, err
}
return buf, nil
}

// ensureEOF ensures the read operation ends with an EOF and no
// trailing data is present.
func ensureEOF(r io.Reader) error {
var peek [1]byte
_, err := io.ReadFull(r, peek[:])
if err != io.EOF {
return ErrTrailingData
}
return nil
}
225 changes: 225 additions & 0 deletions content/reader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
Copyright The ORAS 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 content

import (
"bytes"
_ "crypto/sha256"
"errors"
"io"
"testing"

"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func TestVerifyReader_Read(t *testing.T) {
// matched content and descriptor with small buffer
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content))}
r := bytes.NewReader(content)
vr := NewVerifyReader(r, desc)
buf := make([]byte, 5)
n, err := vr.Read(buf)

wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
t.Fatal("Read() error = ", err)
}
if !bytes.Equal(buf, []byte("examp")) {
t.Fatalf("incorrect read content: %s", buf)
}
if n != 5 {
t.Fatalf("incorrect number of bytes read: %d", n)
}

// matched content and descriptor with sufficient buffer
content = []byte("foo foo")
desc = ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content))}
r = bytes.NewReader(content)
vr = NewVerifyReader(r, desc)
buf = make([]byte, len(content))
n, err = vr.Read(buf)
if err != nil {
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
t.Fatal("Read() error = ", err)
}
if n != len(content) {
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
t.Fatalf("incorrect number of bytes read: %d", n)
}

// mismatched content and descriptor with sufficient buffer
r = bytes.NewReader([]byte("bar"))
vr = NewVerifyReader(r, desc)
buf = make([]byte, 5)
n, err = vr.Read(buf)
if err != nil {
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
t.Fatal("Read() error = ", err)
}
if n != 3 {
t.Fatalf("incorrect number of bytes read: %d", n)
}
}

func TestVerifyReader_Verify(t *testing.T) {
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
// matched content and descriptor
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content))}
r := bytes.NewReader(content)
vr := NewVerifyReader(r, desc)
buf := make([]byte, len(content))
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
if _, err := vr.Read(buf); err != nil {
t.Fatal("Read() error = ", err)
}
if err := vr.Verify(); err != nil {
t.Fatal("Verify() error = ", err)
}

// mismatched content and descriptor, read size larger than descriptor size
content = []byte("foo")
r = bytes.NewReader(content)
desc = ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content)) - 1}
vr = NewVerifyReader(r, desc)
buf = make([]byte, len(content))
if _, err := vr.Read(buf); err != nil {
t.Fatal("Read() error = ", err)
}
if err := vr.Verify(); !errors.Is(err, ErrTrailingData) {
t.Fatalf("Verify() error = %v, want %v", err, ErrTrailingData)
}

// call vr.Verify again, the result should be the same
if err := vr.Verify(); !errors.Is(err, ErrTrailingData) {
t.Fatalf("2nd Verify() error = %v, want %v", err, ErrTrailingData)
}

// mismatched content and descriptor, wrong digest
content = []byte("bar")
r = bytes.NewReader(content)
desc = ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes([]byte("foo")),
Size: int64(len(content))}
vr = NewVerifyReader(r, desc)
buf = make([]byte, len(content))
if _, err := vr.Read(buf); err != nil {
t.Fatal("Read() error = ", err)
}
if err := vr.Verify(); !errors.Is(err, ErrMismatchedDigest) {
t.Fatalf("Verify() error = %v, want %v", err, ErrMismatchedDigest)
}

// call vr.Verify again, the result should be the same
if err := vr.Verify(); !errors.Is(err, ErrMismatchedDigest) {
t.Fatalf("2nd Verify() error = %v, want %v", err, ErrMismatchedDigest)
}
}

func TestReadAll_CorrectDescriptor(t *testing.T) {
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content))}
r := bytes.NewReader([]byte(content))
got, err := ReadAll(r, desc)
if err != nil {
t.Fatal("ReadAll() error = ", err)
}
if !bytes.Equal(got, content) {
t.Errorf("ReadAll() = %v, want %v", got, content)
}
}

func TestReadAll_ReadSizeSmallerThanDescriptorSize(t *testing.T) {
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content) + 1)}
r := bytes.NewReader([]byte(content))
_, err := ReadAll(r, desc)
if err == nil || !errors.Is(err, io.ErrUnexpectedEOF) {
t.Errorf("ReadAll() error = %v, want %v", err, io.ErrUnexpectedEOF)
}
}

func TestReadAll_ReadSizeLargerThanDescriptorSize(t *testing.T) {
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content) - 1)}
r := bytes.NewReader([]byte(content))
_, err := ReadAll(r, desc)
if err == nil || !errors.Is(err, ErrTrailingData) {
t.Errorf("ReadAll() error = %v, want %v", err, ErrTrailingData)
}
}

func TestReadAll_InvalidDigest(t *testing.T) {
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes([]byte("wrong content")),
Size: int64(len(content))}
r := bytes.NewReader([]byte(content))
_, err := ReadAll(r, desc)
if err == nil || !errors.Is(err, ErrMismatchedDigest) {
t.Errorf("ReadAll() error = %v, want %v", err, ErrMismatchedDigest)
}
}

func TestReadAll_EmptyContent(t *testing.T) {
content := []byte("")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
r := bytes.NewReader([]byte(content))
got, err := ReadAll(r, desc)
if err != nil {
t.Fatal("ReadAll() error = ", err)
}
if !bytes.Equal(got, content) {
t.Errorf("ReadAll() = %v, want %v", got, content)
}
}

func TestReadAll_InvalidDescriptorSize(t *testing.T) {
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: -1,
}
r := bytes.NewReader([]byte(content))
_, err := ReadAll(r, desc)
if err == nil || !errors.Is(err, ErrInvalidDescriptorSize) {
t.Errorf("ReadAll() error = %v, want %v", err, ErrInvalidDescriptorSize)
}
}
Loading