From cf47d837b75e14e9a2d75a321740901f8471cdb4 Mon Sep 17 00:00:00 2001 From: Brandon Mitchell Date: Tue, 26 Sep 2023 20:18:19 -0400 Subject: [PATCH] Refactoring the blob package The main goal was to remove unnecessary interfaces. To avoid breaking users, type aliases were used on the old interface names. Comments were updated to better align with the godoc style. Signed-off-by: Brandon Mitchell --- types/blob/blob.go | 41 +++++++++--- types/blob/blob_test.go | 100 ++++++++++++++++------------ types/blob/common.go | 60 ++++++++--------- types/blob/ociconfig.go | 105 ++++++++++++++++++------------ types/blob/reader.go | 140 +++++++++++++++++++--------------------- types/blob/tar.go | 50 +++++++------- 6 files changed, 273 insertions(+), 223 deletions(-) diff --git a/types/blob/blob.go b/types/blob/blob.go index 563239e..84979c6 100644 --- a/types/blob/blob.go +++ b/types/blob/blob.go @@ -1,19 +1,39 @@ -// Package blob is the underlying type for pushing and pulling blobs +// Package blob is the underlying type for pushing and pulling blobs. package blob import ( "io" "net/http" + "github.com/opencontainers/go-digest" "github.com/regclient/regclient/types" v1 "github.com/regclient/regclient/types/oci/v1" "github.com/regclient/regclient/types/ref" ) -// Blob interface is used for returning blobs +// Blob interface is used for returning blobs. type Blob interface { - Common + // GetDescriptor returns the descriptor associated with the blob. + GetDescriptor() types.Descriptor + // RawBody returns the raw content of the blob. RawBody() ([]byte, error) + // RawHeaders returns the headers received from the registry. + RawHeaders() http.Header + // Response returns the response associated with the blob. + Response() *http.Response + + // Digest returns the provided or calculated digest of the blob. + // + // Deprecated: Digest should be replaced by GetDescriptor().Digest. + Digest() digest.Digest + // Length returns the provided or calculated length of the blob. + // + // Deprecated: Length should be replaced by GetDescriptor().Size. + Length() int64 + // MediaType returns the Content-Type header received from the registry. + // + // Deprecated: MediaType should be replaced by GetDescriptor().MediaType. + MediaType() string } type blobConfig struct { @@ -26,51 +46,52 @@ type blobConfig struct { rawBody []byte } +// Opts is used for options to create a new blob. type Opts func(*blobConfig) -// WithDesc specifies the descriptor associated with the blob +// WithDesc specifies the descriptor associated with the blob. func WithDesc(d types.Descriptor) Opts { return func(bc *blobConfig) { bc.desc = d } } -// WithHeader defines the headers received when pulling a blob +// WithHeader defines the headers received when pulling a blob. func WithHeader(header http.Header) Opts { return func(bc *blobConfig) { bc.header = header } } -// WithImage provides the OCI Image config needed for config blobs +// WithImage provides the OCI Image config needed for config blobs. func WithImage(image v1.Image) Opts { return func(bc *blobConfig) { bc.image = &image } } -// WithRawBody defines the raw blob contents for OCIConfig +// WithRawBody defines the raw blob contents for OCIConfig. func WithRawBody(raw []byte) Opts { return func(bc *blobConfig) { bc.rawBody = raw } } -// WithReader defines the reader for a new blob +// WithReader defines the reader for a new blob. func WithReader(rc io.Reader) Opts { return func(bc *blobConfig) { bc.rdr = rc } } -// WithRef specifies the reference where the blob was pulled from +// WithRef specifies the reference where the blob was pulled from. func WithRef(r ref.Ref) Opts { return func(bc *blobConfig) { bc.r = r } } -// WithResp includes the http response, which is used to extract the headers and reader +// WithResp includes the http response, which is used to extract the headers and reader. func WithResp(resp *http.Response) Opts { return func(bc *blobConfig) { bc.resp = resp diff --git a/types/blob/blob_test.go b/types/blob/blob_test.go index e4805b1..99be176 100644 --- a/types/blob/blob_test.go +++ b/types/blob/blob_test.go @@ -257,16 +257,11 @@ func TestReader(t *testing.T) { if i != bl { t.Errorf("read length, expected %d, received %d", bl, i) } - bSeek, ok := b.(io.Seeker) - if !ok { - t.Errorf("seek interface missing") - return - } - _, err = bSeek.Seek(5, io.SeekStart) + _, err = b.Seek(5, io.SeekStart) if err == nil { t.Errorf("seek to non-zero position did not fail") } - pos, err := bSeek.Seek(0, io.SeekStart) + pos, err := b.Seek(0, io.SeekStart) if err != nil { t.Errorf("seek err: %v", err) return @@ -298,12 +293,7 @@ func TestReader(t *testing.T) { if i != bl { t.Errorf("read length, expected %d, received %d", bl, i) } - bSeek, ok = b.(io.Seeker) - if !ok { - t.Errorf("seek interface missing") - return - } - _, err = bSeek.Seek(0, io.SeekStart) + _, err = b.Seek(0, io.SeekStart) if err != nil { t.Errorf("seek err: %v", err) return @@ -372,10 +362,12 @@ func TestOCI(t *testing.T) { t.Errorf("failed to unmarshal exBlob: %v", err) return } - tests := []struct { + tt := []struct { name string opts []Opts + fromJSON []byte wantRaw []byte + wantJSON []byte wantDesc types.Descriptor }{ { @@ -386,6 +378,16 @@ func TestOCI(t *testing.T) { }, wantDesc: exDesc, wantRaw: exBlob, + wantJSON: exBlob, + }, + { + name: "JSONMarshal", + opts: []Opts{ + WithDesc(exDesc), + }, + fromJSON: exBlob, + wantDesc: exDesc, + wantJSON: exBlob, }, { name: "Config with Default Desc", @@ -404,27 +406,43 @@ func TestOCI(t *testing.T) { }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - oc := NewOCIConfig(tt.opts...) + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + oc := NewOCIConfig(tc.opts...) - if tt.wantDesc.Digest != "" && tt.wantDesc.Digest != oc.GetDescriptor().Digest { - t.Errorf("digest, expected %s, received %s", tt.wantDesc.Digest, oc.GetDescriptor().Digest) + if len(tc.fromJSON) > 0 { + err := oc.UnmarshalJSON(tc.fromJSON) + if err != nil { + t.Errorf("failed to unmarshal json: %v", err) + } } - if tt.wantDesc.MediaType != "" && tt.wantDesc.MediaType != oc.GetDescriptor().MediaType { - t.Errorf("media type, expected %s, received %s", tt.wantDesc.MediaType, oc.GetDescriptor().MediaType) + if tc.wantDesc.Digest != "" && tc.wantDesc.Digest != oc.GetDescriptor().Digest { + t.Errorf("digest, expected %s, received %s", tc.wantDesc.Digest, oc.GetDescriptor().Digest) } - if tt.wantDesc.Size > 0 && tt.wantDesc.Size != oc.GetDescriptor().Size { - t.Errorf("size, expected %d, received %d", tt.wantDesc.Size, oc.GetDescriptor().Size) + if tc.wantDesc.MediaType != "" && tc.wantDesc.MediaType != oc.GetDescriptor().MediaType { + t.Errorf("media type, expected %s, received %s", tc.wantDesc.MediaType, oc.GetDescriptor().MediaType) } - if len(tt.wantRaw) > 0 { + if tc.wantDesc.Size > 0 && tc.wantDesc.Size != oc.GetDescriptor().Size { + t.Errorf("size, expected %d, received %d", tc.wantDesc.Size, oc.GetDescriptor().Size) + } + if len(tc.wantRaw) > 0 { raw, err := oc.RawBody() if err != nil { t.Errorf("config rawbody: %v", err) return } - if !bytes.Equal(tt.wantRaw, raw) { - t.Errorf("config bytes, expected %s, received %s", string(tt.wantRaw), string(raw)) + if !bytes.Equal(tc.wantRaw, raw) { + t.Errorf("config bytes, expected %s, received %s", string(tc.wantRaw), string(raw)) + } + } + if len(tc.wantJSON) > 0 { + ocJSON, err := oc.MarshalJSON() + if err != nil { + t.Errorf("json marshal: %v", err) + return + } + if !bytes.Equal(tc.wantJSON, ocJSON) { + t.Errorf("json marshal, expected %s, received %s", string(tc.wantJSON), string(ocJSON)) } } }) @@ -476,7 +494,7 @@ func TestTarReader(t *testing.T) { fh.Close() dig := digger.Digest() - tests := []struct { + tt := []struct { name string opts []Opts errClose bool @@ -507,14 +525,14 @@ func TestTarReader(t *testing.T) { }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { fh, err := os.Open(fileLayer) if err != nil { t.Errorf("failed to open test data: %v", err) return } - opts := append(tt.opts, WithReader(fh)) + opts := append(tc.opts, WithReader(fh)) btr := NewTarReader(opts...) tr, err := btr.GetTarReader() if err != nil { @@ -542,9 +560,9 @@ func TestTarReader(t *testing.T) { } } err = btr.Close() - if !tt.errClose && err != nil { + if !tc.errClose && err != nil { t.Errorf("failed to close tar reader: %v", err) - } else if tt.errClose && err == nil { + } else if tc.errClose && err == nil { t.Errorf("close did not fail") } }) @@ -552,7 +570,7 @@ func TestTarReader(t *testing.T) { } func TestReadFile(t *testing.T) { - tests := []struct { + tt := []struct { name string filename string content string @@ -600,20 +618,20 @@ func TestReadFile(t *testing.T) { return } blobDigest := digest.FromBytes(fileBytes) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { fh, err := os.Open(fileLayerWH) if err != nil { t.Errorf("failed to open test data: %v", err) return } btr := NewTarReader(WithReader(fh), WithDesc(types.Descriptor{Size: int64(len(fileBytes)), Digest: blobDigest, MediaType: types.MediaTypeOCI1Layer})) - th, rdr, err := btr.ReadFile(tt.filename) - if tt.expectErr != nil { + th, rdr, err := btr.ReadFile(tc.filename) + if tc.expectErr != nil { if err == nil { t.Errorf("ReadFile did not fail") - } else if !errors.Is(err, tt.expectErr) && err.Error() != tt.expectErr.Error() { - t.Errorf("unexpected error, expected %v, received %v", tt.expectErr, err) + } else if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() { + t.Errorf("unexpected error, expected %v, received %v", tc.expectErr, err) } err = btr.Close() if err != nil { @@ -640,8 +658,8 @@ func TestReadFile(t *testing.T) { if err != nil { t.Errorf("failed reading file: %v", err) } - if tt.content != string(content) { - t.Errorf("file content mismatch: expected %s, received %s", tt.content, string(content)) + if tc.content != string(content) { + t.Errorf("file content mismatch: expected %s, received %s", tc.content, string(content)) } err = btr.Close() if err != nil { diff --git a/types/blob/common.go b/types/blob/common.go index b60d108..6101b40 100644 --- a/types/blob/common.go +++ b/types/blob/common.go @@ -12,21 +12,11 @@ import ( "github.com/regclient/regclient/types/ref" ) -// Common interface is provided by all Blob implementations -type Common interface { - GetDescriptor() types.Descriptor - Response() *http.Response - RawHeaders() http.Header +// Common was previously an interface. A type alias is provided for upgrades. +type Common = *BCommon - // Deprecated: Digest should be replaced by GetDescriptor().Digest - Digest() digest.Digest - // Deprecated: Length should be replaced by GetDescriptor().Size - Length() int64 - // Deprecated: MediaType should be replaced by GetDescriptor().MediaType - MediaType() string -} - -type common struct { +// BCommon is a common struct for all blobs which includes various shared methods. +type BCommon struct { r ref.Ref desc types.Descriptor blobSet bool @@ -34,32 +24,38 @@ type common struct { resp *http.Response } -// GetDescriptor returns the descriptor associated with the blob -func (b *common) GetDescriptor() types.Descriptor { - return b.desc +// GetDescriptor returns the descriptor associated with the blob. +func (c *BCommon) GetDescriptor() types.Descriptor { + return c.desc } -// Digest returns the provided or calculated digest of the blob -func (b *common) Digest() digest.Digest { - return b.desc.Digest +// Digest returns the provided or calculated digest of the blob. +// +// Deprecated: Digest should be replaced by GetDescriptor().Digest. +func (c *BCommon) Digest() digest.Digest { + return c.desc.Digest } -// Length returns the provided or calculated length of the blob -func (b *common) Length() int64 { - return b.desc.Size +// Length returns the provided or calculated length of the blob. +// +// Deprecated: Length should be replaced by GetDescriptor().Size. +func (c *BCommon) Length() int64 { + return c.desc.Size } -// MediaType returns the Content-Type header received from the registry -func (b *common) MediaType() string { - return b.desc.MediaType +// MediaType returns the Content-Type header received from the registry. +// +// Deprecated: MediaType should be replaced by GetDescriptor().MediaType. +func (c *BCommon) MediaType() string { + return c.desc.MediaType } -// RawHeaders returns the headers received from the registry -func (b *common) RawHeaders() http.Header { - return b.rawHeader +// RawHeaders returns the headers received from the registry. +func (c *BCommon) RawHeaders() http.Header { + return c.rawHeader } -// Response returns the response associated with the blob -func (b *common) Response() *http.Response { - return b.resp +// Response returns the response associated with the blob. +func (c *BCommon) Response() *http.Response { + return c.resp } diff --git a/types/blob/ociconfig.go b/types/blob/ociconfig.go index 21b1362..ed5b043 100644 --- a/types/blob/ociconfig.go +++ b/types/blob/ociconfig.go @@ -13,23 +13,19 @@ import ( v1 "github.com/regclient/regclient/types/oci/v1" ) -// OCIConfig wraps an OCI Config struct extracted from a Blob -type OCIConfig interface { - Blob - GetConfig() v1.Image - SetConfig(v1.Image) -} +// OCIConfig was previously an interface. A type alias is provided for upgrading. +type OCIConfig = *BOCIConfig -// ociConfig includes an OCI Config struct extracted from a Blob -// Image is included as an anonymous field to facilitate json and templating calls transparently -type ociConfig struct { - common +// BOCIConfig includes an OCI Image Config struct that may be extracted from or pushed to a blob. +type BOCIConfig struct { + BCommon rawBody []byte - v1.Image + image v1.Image } -// NewOCIConfig creates a new BlobOCIConfig from an OCI Image -func NewOCIConfig(opts ...Opts) OCIConfig { +// NewOCIConfig creates a new BOCIConfig. +// When created from an existing blob, a BOCIConfig will be created using BReader.ToOCIConfig(). +func NewOCIConfig(opts ...Opts) *BOCIConfig { bc := blobConfig{} for _, opt := range opts { opt(&bc) @@ -56,49 +52,76 @@ func NewOCIConfig(opts ...Opts) OCIConfig { bc.desc.MediaType = types.MediaTypeOCI1ImageConfig } } - - c := common{ - desc: bc.desc, - r: bc.r, - rawHeader: bc.header, - resp: bc.resp, - } - b := ociConfig{ - common: c, + b := BOCIConfig{ + BCommon: BCommon{ + desc: bc.desc, + r: bc.r, + rawHeader: bc.header, + resp: bc.resp, + }, rawBody: bc.rawBody, } if bc.image != nil { - b.Image = *bc.image + b.image = *bc.image b.blobSet = true } return &b } -// GetConfig returns OCI config -func (b *ociConfig) GetConfig() v1.Image { - return b.Image +// GetConfig returns OCI config. +func (oc *BOCIConfig) GetConfig() v1.Image { + return oc.image } -// RawBody returns the original body from the request -func (b *ociConfig) RawBody() ([]byte, error) { +// RawBody returns the original body from the request. +func (oc *BOCIConfig) RawBody() ([]byte, error) { var err error - if !b.blobSet { + if !oc.blobSet { return []byte{}, fmt.Errorf("Blob is not defined") } - if len(b.rawBody) == 0 { - b.rawBody, err = json.Marshal(b.Image) + if len(oc.rawBody) == 0 { + oc.rawBody, err = json.Marshal(oc.image) } - return b.rawBody, err + return oc.rawBody, err } -// SetConfig updates the config, including raw body and descriptor -func (b *ociConfig) SetConfig(c v1.Image) { - b.Image = c - b.rawBody, _ = json.Marshal(b.Image) - if b.desc.MediaType == "" { - b.desc.MediaType = types.MediaTypeOCI1ImageConfig +// SetConfig updates the config, including raw body and descriptor. +func (oc *BOCIConfig) SetConfig(image v1.Image) { + oc.image = image + oc.rawBody, _ = json.Marshal(oc.image) + if oc.desc.MediaType == "" { + oc.desc.MediaType = types.MediaTypeOCI1ImageConfig } - b.desc.Digest = digest.FromBytes(b.rawBody) - b.desc.Size = int64(len(b.rawBody)) - b.blobSet = true + oc.desc.Digest = digest.FromBytes(oc.rawBody) + oc.desc.Size = int64(len(oc.rawBody)) + oc.blobSet = true +} + +// MarshalJSON passes through the marshalling to the underlying image if rawBody is not available. +func (oc *BOCIConfig) MarshalJSON() ([]byte, error) { + if !oc.blobSet { + return []byte{}, fmt.Errorf("Blob is not defined") + } + if len(oc.rawBody) > 0 { + return oc.rawBody, nil + } + return json.Marshal(oc.image) +} + +// UnmarshalJSON extracts json content and populates the content. +func (oc *BOCIConfig) UnmarshalJSON(data []byte) error { + image := v1.Image{} + err := json.Unmarshal(data, &image) + if err != nil { + return err + } + oc.rawBody = make([]byte, len(data)) + copy(oc.rawBody, data) + if oc.desc.MediaType == "" { + oc.desc.MediaType = types.MediaTypeOCI1ImageConfig + } + oc.desc.Digest = digest.FromBytes(oc.rawBody) + oc.desc.Size = int64(len(oc.rawBody)) + oc.blobSet = true + return nil } diff --git a/types/blob/reader.go b/types/blob/reader.go index 3a46bd3..96bc387 100644 --- a/types/blob/reader.go +++ b/types/blob/reader.go @@ -14,25 +14,20 @@ import ( "github.com/regclient/regclient/types" ) -// Reader is an unprocessed Blob with an available ReadCloser for reading the Blob -type Reader interface { - Blob - io.ReadSeekCloser - ToOCIConfig() (OCIConfig, error) - ToTarReader() (TarReader, error) -} +// Reader was previously an interface. A type alias is provided for upgrading. +type Reader = *BReader -// reader is the internal struct implementing BlobReader -type reader struct { - common +// BReader is used to read blobs. +type BReader struct { + BCommon readBytes int64 reader io.Reader origRdr io.Reader digester digest.Digester } -// NewReader creates a new reader -func NewReader(opts ...Opts) Reader { +// NewReader creates a new BReader. +func NewReader(opts ...Opts) *BReader { bc := blobConfig{} for _, opt := range opts { opt(&bc) @@ -59,14 +54,13 @@ func NewReader(opts ...Opts) Reader { bc.desc.Digest, _ = digest.Parse(bc.header.Get("Docker-Content-Digest")) } } - c := common{ - r: bc.r, - desc: bc.desc, - rawHeader: bc.header, - resp: bc.resp, - } - br := reader{ - common: c, + br := BReader{ + BCommon: BCommon{ + r: bc.r, + desc: bc.desc, + rawHeader: bc.header, + resp: bc.resp, + }, origRdr: bc.rdr, } if bc.rdr != nil { @@ -84,119 +78,121 @@ func NewReader(opts ...Opts) Reader { return &br } -func (b *reader) Close() error { - if b.origRdr == nil { +// Close attempts to close the reader and populates/validates the digest. +func (r *BReader) Close() error { + if r.origRdr == nil { return nil } // attempt to close if available in original reader - bc, ok := b.origRdr.(io.Closer) + bc, ok := r.origRdr.(io.Closer) if !ok { return nil } return bc.Close() } -// RawBody returns the original body from the request -func (b *reader) RawBody() ([]byte, error) { - return io.ReadAll(b) +// RawBody returns the original body from the request. +func (r *BReader) RawBody() ([]byte, error) { + return io.ReadAll(r) } -// Read passes through the read operation while computing the digest and tracking the size -func (b *reader) Read(p []byte) (int, error) { - if b.reader == nil { +// Read passes through the read operation while computing the digest and tracking the size. +func (r *BReader) Read(p []byte) (int, error) { + if r.reader == nil { return 0, fmt.Errorf("blob has no reader: %w", io.ErrUnexpectedEOF) } - size, err := b.reader.Read(p) - b.readBytes = b.readBytes + int64(size) + size, err := r.reader.Read(p) + r.readBytes = r.readBytes + int64(size) if err == io.EOF { // check/save size - if b.desc.Size == 0 { - b.desc.Size = b.readBytes - } else if b.readBytes < b.desc.Size { - err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrShortRead, b.desc.Size, b.readBytes, err) - } else if b.readBytes > b.desc.Size { - err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrSizeLimitExceeded, b.desc.Size, b.readBytes, err) + if r.desc.Size == 0 { + r.desc.Size = r.readBytes + } else if r.readBytes < r.desc.Size { + err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrShortRead, r.desc.Size, r.readBytes, err) + } else if r.readBytes > r.desc.Size { + err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrSizeLimitExceeded, r.desc.Size, r.readBytes, err) } // check/save digest - if b.desc.Digest == "" { - b.desc.Digest = b.digester.Digest() - } else if b.desc.Digest != b.digester.Digest() { - err = fmt.Errorf("%w [expected %s, calculated %s]: %v", types.ErrDigestMismatch, b.desc.Digest.String(), b.digester.Digest().String(), err) + if r.desc.Digest == "" { + r.desc.Digest = r.digester.Digest() + } else if r.desc.Digest != r.digester.Digest() { + err = fmt.Errorf("%w [expected %s, calculated %s]: %v", types.ErrDigestMismatch, r.desc.Digest.String(), r.digester.Digest().String(), err) } } return size, err } // Seek passes through the seek operation, reseting or invalidating the digest -func (b *reader) Seek(offset int64, whence int) (int64, error) { +func (r *BReader) Seek(offset int64, whence int) (int64, error) { if offset == 0 && whence == io.SeekCurrent { - return b.readBytes, nil + return r.readBytes, nil } // cannot do an arbitrary seek and still digest without a lot more complication if offset != 0 || whence != io.SeekStart { - return b.readBytes, fmt.Errorf("unable to seek to arbitrary position") + return r.readBytes, fmt.Errorf("unable to seek to arbitrary position") } - rdrSeek, ok := b.origRdr.(io.Seeker) + rdrSeek, ok := r.origRdr.(io.Seeker) if !ok { - return b.readBytes, fmt.Errorf("Seek unsupported") + return r.readBytes, fmt.Errorf("Seek unsupported") } o, err := rdrSeek.Seek(offset, whence) if err != nil || o != 0 { - return b.readBytes, err + return r.readBytes, err } // reset internal offset and digest calculation - rdr := b.origRdr - if b.desc.Size > 0 { + rdr := r.origRdr + if r.desc.Size > 0 { rdr = &limitread.LimitRead{ Reader: rdr, - Limit: b.desc.Size, + Limit: r.desc.Size, } } digester := digest.Canonical.Digester() - b.reader = io.TeeReader(rdr, digester.Hash()) - b.digester = digester - b.readBytes = 0 + r.reader = io.TeeReader(rdr, digester.Hash()) + r.digester = digester + r.readBytes = 0 return 0, nil } -// ToOCIConfig converts a blobReader to a BlobOCIConfig -func (b *reader) ToOCIConfig() (OCIConfig, error) { - if !b.blobSet { +// ToOCIConfig converts a BReader to a BOCIConfig. +func (r *BReader) ToOCIConfig() (*BOCIConfig, error) { + if !r.blobSet { return nil, fmt.Errorf("blob is not defined") } - if b.readBytes != 0 { + if r.readBytes != 0 { return nil, fmt.Errorf("unable to convert after read has been performed") } - blobBody, err := io.ReadAll(b) - errC := b.Close() + blobBody, err := io.ReadAll(r) + errC := r.Close() if err != nil { - return nil, fmt.Errorf("error reading image config for %s: %w", b.r.CommonName(), err) + return nil, fmt.Errorf("error reading image config for %s: %w", r.r.CommonName(), err) } if errC != nil { return nil, fmt.Errorf("error closing blob reader: %w", err) } return NewOCIConfig( - WithDesc(b.desc), - WithHeader(b.rawHeader), + WithDesc(r.desc), + WithHeader(r.rawHeader), WithRawBody(blobBody), - WithRef(b.r), - WithResp(b.resp), + WithRef(r.r), + WithResp(r.resp), ), nil } -func (b *reader) ToTarReader() (TarReader, error) { - if !b.blobSet { +// ToTarReader converts a BReader to a BTarReader +func (r *BReader) ToTarReader() (*BTarReader, error) { + if !r.blobSet { return nil, fmt.Errorf("blob is not defined") } - if b.readBytes != 0 { + if r.readBytes != 0 { return nil, fmt.Errorf("unable to convert after read has been performed") } return NewTarReader( - WithDesc(b.desc), - WithHeader(b.rawHeader), - WithRef(b.r), - WithResp(b.resp), - WithReader(b.reader), + WithDesc(r.desc), + WithHeader(r.rawHeader), + WithRef(r.r), + WithResp(r.resp), + WithReader(r.reader), ), nil } diff --git a/types/blob/tar.go b/types/blob/tar.go index d66ddd4..a1c6d6e 100644 --- a/types/blob/tar.go +++ b/types/blob/tar.go @@ -14,36 +14,32 @@ import ( "github.com/regclient/regclient/types" ) -// TarReader reads or writes to a blob with tar contents and optional compression -type TarReader interface { - Blob - io.Closer - GetTarReader() (*tar.Reader, error) - ReadFile(filename string) (*tar.Header, io.Reader, error) -} +// TarReader was previously an interface. A type alias is provided for upgrading. +type TarReader = *BTarReader -type tarReader struct { - common +// BTarReader is used to read individual files from an image layer. +type BTarReader struct { + BCommon origRdr io.Reader reader io.Reader digester digest.Digester tr *tar.Reader } -// NewTarReader creates a TarReader -func NewTarReader(opts ...Opts) TarReader { +// NewTarReader creates a BTarReader. +// Typically a BTarReader will be created using BReader.ToTarReader(). +func NewTarReader(opts ...Opts) *BTarReader { bc := blobConfig{} for _, opt := range opts { opt(&bc) } - c := common{ - desc: bc.desc, - r: bc.r, - rawHeader: bc.header, - resp: bc.resp, - } - tr := tarReader{ - common: c, + tr := BTarReader{ + BCommon: BCommon{ + desc: bc.desc, + r: bc.r, + rawHeader: bc.header, + resp: bc.resp, + }, origRdr: bc.rdr, } if bc.rdr != nil { @@ -61,8 +57,8 @@ func NewTarReader(opts ...Opts) TarReader { return &tr } -// Close attempts to close the reader and populates/validates the digest -func (tr *tarReader) Close() error { +// Close attempts to close the reader and populates/validates the digest. +func (tr *BTarReader) Close() error { // attempt to close if available in original reader if trc, ok := tr.origRdr.(io.Closer); ok && trc != nil { return trc.Close() @@ -70,8 +66,8 @@ func (tr *tarReader) Close() error { return nil } -// GetTarReader returns the tar.Reader for the blob -func (tr *tarReader) GetTarReader() (*tar.Reader, error) { +// GetTarReader returns the tar.Reader for the blob. +func (tr *BTarReader) GetTarReader() (*tar.Reader, error) { if tr.reader == nil { return nil, fmt.Errorf("blob has no reader defined") } @@ -85,8 +81,8 @@ func (tr *tarReader) GetTarReader() (*tar.Reader, error) { return tr.tr, nil } -// RawBody returns the original body from the request -func (tr *tarReader) RawBody() ([]byte, error) { +// RawBody returns the original body from the request. +func (tr *BTarReader) RawBody() ([]byte, error) { if !tr.blobSet { return []byte{}, fmt.Errorf("Blob is not defined") } @@ -109,8 +105,8 @@ func (tr *tarReader) RawBody() ([]byte, error) { return b, err } -// ReadFile parses the tar to find a file -func (tr *tarReader) ReadFile(filename string) (*tar.Header, io.Reader, error) { +// ReadFile parses the tar to find a file. +func (tr *BTarReader) ReadFile(filename string) (*tar.Header, io.Reader, error) { if strings.HasPrefix(filename, ".wh.") { return nil, nil, fmt.Errorf(".wh. prefix is reserved for whiteout files") }