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") }