1
0
mirror of https://github.com/regclient/regclient.git synced 2025-07-29 09:01:11 +03:00

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 <git@bmitch.net>
This commit is contained in:
Brandon Mitchell
2023-09-26 20:18:19 -04:00
parent 934e52602a
commit cf47d837b7
6 changed files with 273 additions and 223 deletions

View File

@ -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 package blob
import ( import (
"io" "io"
"net/http" "net/http"
"github.com/opencontainers/go-digest"
"github.com/regclient/regclient/types" "github.com/regclient/regclient/types"
v1 "github.com/regclient/regclient/types/oci/v1" v1 "github.com/regclient/regclient/types/oci/v1"
"github.com/regclient/regclient/types/ref" "github.com/regclient/regclient/types/ref"
) )
// Blob interface is used for returning blobs // Blob interface is used for returning blobs.
type Blob interface { 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) 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 { type blobConfig struct {
@ -26,51 +46,52 @@ type blobConfig struct {
rawBody []byte rawBody []byte
} }
// Opts is used for options to create a new blob.
type Opts func(*blobConfig) 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 { func WithDesc(d types.Descriptor) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.desc = d 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 { func WithHeader(header http.Header) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.header = header 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 { func WithImage(image v1.Image) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.image = &image bc.image = &image
} }
} }
// WithRawBody defines the raw blob contents for OCIConfig // WithRawBody defines the raw blob contents for OCIConfig.
func WithRawBody(raw []byte) Opts { func WithRawBody(raw []byte) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.rawBody = raw 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 { func WithReader(rc io.Reader) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.rdr = rc 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 { func WithRef(r ref.Ref) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.r = r 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 { func WithResp(resp *http.Response) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.resp = resp bc.resp = resp

View File

@ -257,16 +257,11 @@ func TestReader(t *testing.T) {
if i != bl { if i != bl {
t.Errorf("read length, expected %d, received %d", bl, i) t.Errorf("read length, expected %d, received %d", bl, i)
} }
bSeek, ok := b.(io.Seeker) _, err = b.Seek(5, io.SeekStart)
if !ok {
t.Errorf("seek interface missing")
return
}
_, err = bSeek.Seek(5, io.SeekStart)
if err == nil { if err == nil {
t.Errorf("seek to non-zero position did not fail") 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 { if err != nil {
t.Errorf("seek err: %v", err) t.Errorf("seek err: %v", err)
return return
@ -298,12 +293,7 @@ func TestReader(t *testing.T) {
if i != bl { if i != bl {
t.Errorf("read length, expected %d, received %d", bl, i) t.Errorf("read length, expected %d, received %d", bl, i)
} }
bSeek, ok = b.(io.Seeker) _, err = b.Seek(0, io.SeekStart)
if !ok {
t.Errorf("seek interface missing")
return
}
_, err = bSeek.Seek(0, io.SeekStart)
if err != nil { if err != nil {
t.Errorf("seek err: %v", err) t.Errorf("seek err: %v", err)
return return
@ -372,10 +362,12 @@ func TestOCI(t *testing.T) {
t.Errorf("failed to unmarshal exBlob: %v", err) t.Errorf("failed to unmarshal exBlob: %v", err)
return return
} }
tests := []struct { tt := []struct {
name string name string
opts []Opts opts []Opts
fromJSON []byte
wantRaw []byte wantRaw []byte
wantJSON []byte
wantDesc types.Descriptor wantDesc types.Descriptor
}{ }{
{ {
@ -386,6 +378,16 @@ func TestOCI(t *testing.T) {
}, },
wantDesc: exDesc, wantDesc: exDesc,
wantRaw: exBlob, wantRaw: exBlob,
wantJSON: exBlob,
},
{
name: "JSONMarshal",
opts: []Opts{
WithDesc(exDesc),
},
fromJSON: exBlob,
wantDesc: exDesc,
wantJSON: exBlob,
}, },
{ {
name: "Config with Default Desc", name: "Config with Default Desc",
@ -404,27 +406,43 @@ func TestOCI(t *testing.T) {
}, },
} }
for _, tt := range tests { for _, tc := range tt {
t.Run(tt.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
oc := NewOCIConfig(tt.opts...) oc := NewOCIConfig(tc.opts...)
if tt.wantDesc.Digest != "" && tt.wantDesc.Digest != oc.GetDescriptor().Digest { if len(tc.fromJSON) > 0 {
t.Errorf("digest, expected %s, received %s", tt.wantDesc.Digest, oc.GetDescriptor().Digest) 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 { if tc.wantDesc.Digest != "" && tc.wantDesc.Digest != oc.GetDescriptor().Digest {
t.Errorf("media type, expected %s, received %s", tt.wantDesc.MediaType, oc.GetDescriptor().MediaType) t.Errorf("digest, expected %s, received %s", tc.wantDesc.Digest, oc.GetDescriptor().Digest)
} }
if tt.wantDesc.Size > 0 && tt.wantDesc.Size != oc.GetDescriptor().Size { if tc.wantDesc.MediaType != "" && tc.wantDesc.MediaType != oc.GetDescriptor().MediaType {
t.Errorf("size, expected %d, received %d", tt.wantDesc.Size, oc.GetDescriptor().Size) 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() raw, err := oc.RawBody()
if err != nil { if err != nil {
t.Errorf("config rawbody: %v", err) t.Errorf("config rawbody: %v", err)
return return
} }
if !bytes.Equal(tt.wantRaw, raw) { if !bytes.Equal(tc.wantRaw, raw) {
t.Errorf("config bytes, expected %s, received %s", string(tt.wantRaw), string(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() fh.Close()
dig := digger.Digest() dig := digger.Digest()
tests := []struct { tt := []struct {
name string name string
opts []Opts opts []Opts
errClose bool errClose bool
@ -507,14 +525,14 @@ func TestTarReader(t *testing.T) {
}, },
} }
for _, tt := range tests { for _, tc := range tt {
t.Run(tt.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fh, err := os.Open(fileLayer) fh, err := os.Open(fileLayer)
if err != nil { if err != nil {
t.Errorf("failed to open test data: %v", err) t.Errorf("failed to open test data: %v", err)
return return
} }
opts := append(tt.opts, WithReader(fh)) opts := append(tc.opts, WithReader(fh))
btr := NewTarReader(opts...) btr := NewTarReader(opts...)
tr, err := btr.GetTarReader() tr, err := btr.GetTarReader()
if err != nil { if err != nil {
@ -542,9 +560,9 @@ func TestTarReader(t *testing.T) {
} }
} }
err = btr.Close() err = btr.Close()
if !tt.errClose && err != nil { if !tc.errClose && err != nil {
t.Errorf("failed to close tar reader: %v", err) 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") t.Errorf("close did not fail")
} }
}) })
@ -552,7 +570,7 @@ func TestTarReader(t *testing.T) {
} }
func TestReadFile(t *testing.T) { func TestReadFile(t *testing.T) {
tests := []struct { tt := []struct {
name string name string
filename string filename string
content string content string
@ -600,20 +618,20 @@ func TestReadFile(t *testing.T) {
return return
} }
blobDigest := digest.FromBytes(fileBytes) blobDigest := digest.FromBytes(fileBytes)
for _, tt := range tests { for _, tc := range tt {
t.Run(tt.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fh, err := os.Open(fileLayerWH) fh, err := os.Open(fileLayerWH)
if err != nil { if err != nil {
t.Errorf("failed to open test data: %v", err) t.Errorf("failed to open test data: %v", err)
return return
} }
btr := NewTarReader(WithReader(fh), WithDesc(types.Descriptor{Size: int64(len(fileBytes)), Digest: blobDigest, MediaType: types.MediaTypeOCI1Layer})) btr := NewTarReader(WithReader(fh), WithDesc(types.Descriptor{Size: int64(len(fileBytes)), Digest: blobDigest, MediaType: types.MediaTypeOCI1Layer}))
th, rdr, err := btr.ReadFile(tt.filename) th, rdr, err := btr.ReadFile(tc.filename)
if tt.expectErr != nil { if tc.expectErr != nil {
if err == nil { if err == nil {
t.Errorf("ReadFile did not fail") t.Errorf("ReadFile did not fail")
} else if !errors.Is(err, tt.expectErr) && err.Error() != tt.expectErr.Error() { } else if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() {
t.Errorf("unexpected error, expected %v, received %v", tt.expectErr, err) t.Errorf("unexpected error, expected %v, received %v", tc.expectErr, err)
} }
err = btr.Close() err = btr.Close()
if err != nil { if err != nil {
@ -640,8 +658,8 @@ func TestReadFile(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("failed reading file: %v", err) t.Errorf("failed reading file: %v", err)
} }
if tt.content != string(content) { if tc.content != string(content) {
t.Errorf("file content mismatch: expected %s, received %s", tt.content, string(content)) t.Errorf("file content mismatch: expected %s, received %s", tc.content, string(content))
} }
err = btr.Close() err = btr.Close()
if err != nil { if err != nil {

View File

@ -12,21 +12,11 @@ import (
"github.com/regclient/regclient/types/ref" "github.com/regclient/regclient/types/ref"
) )
// Common interface is provided by all Blob implementations // Common was previously an interface. A type alias is provided for upgrades.
type Common interface { type Common = *BCommon
GetDescriptor() types.Descriptor
Response() *http.Response
RawHeaders() http.Header
// Deprecated: Digest should be replaced by GetDescriptor().Digest // BCommon is a common struct for all blobs which includes various shared methods.
Digest() digest.Digest type BCommon struct {
// Deprecated: Length should be replaced by GetDescriptor().Size
Length() int64
// Deprecated: MediaType should be replaced by GetDescriptor().MediaType
MediaType() string
}
type common struct {
r ref.Ref r ref.Ref
desc types.Descriptor desc types.Descriptor
blobSet bool blobSet bool
@ -34,32 +24,38 @@ type common struct {
resp *http.Response resp *http.Response
} }
// GetDescriptor returns the descriptor associated with the blob // GetDescriptor returns the descriptor associated with the blob.
func (b *common) GetDescriptor() types.Descriptor { func (c *BCommon) GetDescriptor() types.Descriptor {
return b.desc return c.desc
} }
// Digest returns the provided or calculated digest of the blob // Digest returns the provided or calculated digest of the blob.
func (b *common) Digest() digest.Digest { //
return b.desc.Digest // 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 // Length returns the provided or calculated length of the blob.
func (b *common) Length() int64 { //
return b.desc.Size // 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 // MediaType returns the Content-Type header received from the registry.
func (b *common) MediaType() string { //
return b.desc.MediaType // 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 // RawHeaders returns the headers received from the registry.
func (b *common) RawHeaders() http.Header { func (c *BCommon) RawHeaders() http.Header {
return b.rawHeader return c.rawHeader
} }
// Response returns the response associated with the blob // Response returns the response associated with the blob.
func (b *common) Response() *http.Response { func (c *BCommon) Response() *http.Response {
return b.resp return c.resp
} }

View File

@ -13,23 +13,19 @@ import (
v1 "github.com/regclient/regclient/types/oci/v1" v1 "github.com/regclient/regclient/types/oci/v1"
) )
// OCIConfig wraps an OCI Config struct extracted from a Blob // OCIConfig was previously an interface. A type alias is provided for upgrading.
type OCIConfig interface { type OCIConfig = *BOCIConfig
Blob
GetConfig() v1.Image
SetConfig(v1.Image)
}
// ociConfig includes an OCI Config struct extracted from a Blob // BOCIConfig includes an OCI Image Config struct that may be extracted from or pushed to a blob.
// Image is included as an anonymous field to facilitate json and templating calls transparently type BOCIConfig struct {
type ociConfig struct { BCommon
common
rawBody []byte rawBody []byte
v1.Image image v1.Image
} }
// NewOCIConfig creates a new BlobOCIConfig from an OCI Image // NewOCIConfig creates a new BOCIConfig.
func NewOCIConfig(opts ...Opts) OCIConfig { // When created from an existing blob, a BOCIConfig will be created using BReader.ToOCIConfig().
func NewOCIConfig(opts ...Opts) *BOCIConfig {
bc := blobConfig{} bc := blobConfig{}
for _, opt := range opts { for _, opt := range opts {
opt(&bc) opt(&bc)
@ -56,49 +52,76 @@ func NewOCIConfig(opts ...Opts) OCIConfig {
bc.desc.MediaType = types.MediaTypeOCI1ImageConfig bc.desc.MediaType = types.MediaTypeOCI1ImageConfig
} }
} }
b := BOCIConfig{
c := common{ BCommon: BCommon{
desc: bc.desc, desc: bc.desc,
r: bc.r, r: bc.r,
rawHeader: bc.header, rawHeader: bc.header,
resp: bc.resp, resp: bc.resp,
} },
b := ociConfig{
common: c,
rawBody: bc.rawBody, rawBody: bc.rawBody,
} }
if bc.image != nil { if bc.image != nil {
b.Image = *bc.image b.image = *bc.image
b.blobSet = true b.blobSet = true
} }
return &b return &b
} }
// GetConfig returns OCI config // GetConfig returns OCI config.
func (b *ociConfig) GetConfig() v1.Image { func (oc *BOCIConfig) GetConfig() v1.Image {
return b.Image return oc.image
} }
// RawBody returns the original body from the request // RawBody returns the original body from the request.
func (b *ociConfig) RawBody() ([]byte, error) { func (oc *BOCIConfig) RawBody() ([]byte, error) {
var err error var err error
if !b.blobSet { if !oc.blobSet {
return []byte{}, fmt.Errorf("Blob is not defined") return []byte{}, fmt.Errorf("Blob is not defined")
} }
if len(b.rawBody) == 0 { if len(oc.rawBody) == 0 {
b.rawBody, err = json.Marshal(b.Image) oc.rawBody, err = json.Marshal(oc.image)
} }
return b.rawBody, err return oc.rawBody, err
} }
// SetConfig updates the config, including raw body and descriptor // SetConfig updates the config, including raw body and descriptor.
func (b *ociConfig) SetConfig(c v1.Image) { func (oc *BOCIConfig) SetConfig(image v1.Image) {
b.Image = c oc.image = image
b.rawBody, _ = json.Marshal(b.Image) oc.rawBody, _ = json.Marshal(oc.image)
if b.desc.MediaType == "" { if oc.desc.MediaType == "" {
b.desc.MediaType = types.MediaTypeOCI1ImageConfig oc.desc.MediaType = types.MediaTypeOCI1ImageConfig
} }
b.desc.Digest = digest.FromBytes(b.rawBody) oc.desc.Digest = digest.FromBytes(oc.rawBody)
b.desc.Size = int64(len(b.rawBody)) oc.desc.Size = int64(len(oc.rawBody))
b.blobSet = true 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
} }

View File

@ -14,25 +14,20 @@ import (
"github.com/regclient/regclient/types" "github.com/regclient/regclient/types"
) )
// Reader is an unprocessed Blob with an available ReadCloser for reading the Blob // Reader was previously an interface. A type alias is provided for upgrading.
type Reader interface { type Reader = *BReader
Blob
io.ReadSeekCloser
ToOCIConfig() (OCIConfig, error)
ToTarReader() (TarReader, error)
}
// reader is the internal struct implementing BlobReader // BReader is used to read blobs.
type reader struct { type BReader struct {
common BCommon
readBytes int64 readBytes int64
reader io.Reader reader io.Reader
origRdr io.Reader origRdr io.Reader
digester digest.Digester digester digest.Digester
} }
// NewReader creates a new reader // NewReader creates a new BReader.
func NewReader(opts ...Opts) Reader { func NewReader(opts ...Opts) *BReader {
bc := blobConfig{} bc := blobConfig{}
for _, opt := range opts { for _, opt := range opts {
opt(&bc) opt(&bc)
@ -59,14 +54,13 @@ func NewReader(opts ...Opts) Reader {
bc.desc.Digest, _ = digest.Parse(bc.header.Get("Docker-Content-Digest")) bc.desc.Digest, _ = digest.Parse(bc.header.Get("Docker-Content-Digest"))
} }
} }
c := common{ br := BReader{
r: bc.r, BCommon: BCommon{
desc: bc.desc, r: bc.r,
rawHeader: bc.header, desc: bc.desc,
resp: bc.resp, rawHeader: bc.header,
} resp: bc.resp,
br := reader{ },
common: c,
origRdr: bc.rdr, origRdr: bc.rdr,
} }
if bc.rdr != nil { if bc.rdr != nil {
@ -84,119 +78,121 @@ func NewReader(opts ...Opts) Reader {
return &br return &br
} }
func (b *reader) Close() error { // Close attempts to close the reader and populates/validates the digest.
if b.origRdr == nil { func (r *BReader) Close() error {
if r.origRdr == nil {
return nil return nil
} }
// attempt to close if available in original reader // attempt to close if available in original reader
bc, ok := b.origRdr.(io.Closer) bc, ok := r.origRdr.(io.Closer)
if !ok { if !ok {
return nil return nil
} }
return bc.Close() return bc.Close()
} }
// RawBody returns the original body from the request // RawBody returns the original body from the request.
func (b *reader) RawBody() ([]byte, error) { func (r *BReader) RawBody() ([]byte, error) {
return io.ReadAll(b) return io.ReadAll(r)
} }
// Read passes through the read operation while computing the digest and tracking the size // Read passes through the read operation while computing the digest and tracking the size.
func (b *reader) Read(p []byte) (int, error) { func (r *BReader) Read(p []byte) (int, error) {
if b.reader == nil { if r.reader == nil {
return 0, fmt.Errorf("blob has no reader: %w", io.ErrUnexpectedEOF) return 0, fmt.Errorf("blob has no reader: %w", io.ErrUnexpectedEOF)
} }
size, err := b.reader.Read(p) size, err := r.reader.Read(p)
b.readBytes = b.readBytes + int64(size) r.readBytes = r.readBytes + int64(size)
if err == io.EOF { if err == io.EOF {
// check/save size // check/save size
if b.desc.Size == 0 { if r.desc.Size == 0 {
b.desc.Size = b.readBytes r.desc.Size = r.readBytes
} else if b.readBytes < b.desc.Size { } else if r.readBytes < r.desc.Size {
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrShortRead, b.desc.Size, b.readBytes, err) err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrShortRead, r.desc.Size, r.readBytes, err)
} else if b.readBytes > b.desc.Size { } else if r.readBytes > r.desc.Size {
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrSizeLimitExceeded, b.desc.Size, b.readBytes, err) err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrSizeLimitExceeded, r.desc.Size, r.readBytes, err)
} }
// check/save digest // check/save digest
if b.desc.Digest == "" { if r.desc.Digest == "" {
b.desc.Digest = b.digester.Digest() r.desc.Digest = r.digester.Digest()
} else if b.desc.Digest != b.digester.Digest() { } else if r.desc.Digest != r.digester.Digest() {
err = fmt.Errorf("%w [expected %s, calculated %s]: %v", types.ErrDigestMismatch, b.desc.Digest.String(), b.digester.Digest().String(), err) err = fmt.Errorf("%w [expected %s, calculated %s]: %v", types.ErrDigestMismatch, r.desc.Digest.String(), r.digester.Digest().String(), err)
} }
} }
return size, err return size, err
} }
// Seek passes through the seek operation, reseting or invalidating the digest // 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 { 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 // cannot do an arbitrary seek and still digest without a lot more complication
if offset != 0 || whence != io.SeekStart { 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 { if !ok {
return b.readBytes, fmt.Errorf("Seek unsupported") return r.readBytes, fmt.Errorf("Seek unsupported")
} }
o, err := rdrSeek.Seek(offset, whence) o, err := rdrSeek.Seek(offset, whence)
if err != nil || o != 0 { if err != nil || o != 0 {
return b.readBytes, err return r.readBytes, err
} }
// reset internal offset and digest calculation // reset internal offset and digest calculation
rdr := b.origRdr rdr := r.origRdr
if b.desc.Size > 0 { if r.desc.Size > 0 {
rdr = &limitread.LimitRead{ rdr = &limitread.LimitRead{
Reader: rdr, Reader: rdr,
Limit: b.desc.Size, Limit: r.desc.Size,
} }
} }
digester := digest.Canonical.Digester() digester := digest.Canonical.Digester()
b.reader = io.TeeReader(rdr, digester.Hash()) r.reader = io.TeeReader(rdr, digester.Hash())
b.digester = digester r.digester = digester
b.readBytes = 0 r.readBytes = 0
return 0, nil return 0, nil
} }
// ToOCIConfig converts a blobReader to a BlobOCIConfig // ToOCIConfig converts a BReader to a BOCIConfig.
func (b *reader) ToOCIConfig() (OCIConfig, error) { func (r *BReader) ToOCIConfig() (*BOCIConfig, error) {
if !b.blobSet { if !r.blobSet {
return nil, fmt.Errorf("blob is not defined") 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 nil, fmt.Errorf("unable to convert after read has been performed")
} }
blobBody, err := io.ReadAll(b) blobBody, err := io.ReadAll(r)
errC := b.Close() errC := r.Close()
if err != nil { 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 { if errC != nil {
return nil, fmt.Errorf("error closing blob reader: %w", err) return nil, fmt.Errorf("error closing blob reader: %w", err)
} }
return NewOCIConfig( return NewOCIConfig(
WithDesc(b.desc), WithDesc(r.desc),
WithHeader(b.rawHeader), WithHeader(r.rawHeader),
WithRawBody(blobBody), WithRawBody(blobBody),
WithRef(b.r), WithRef(r.r),
WithResp(b.resp), WithResp(r.resp),
), nil ), nil
} }
func (b *reader) ToTarReader() (TarReader, error) { // ToTarReader converts a BReader to a BTarReader
if !b.blobSet { func (r *BReader) ToTarReader() (*BTarReader, error) {
if !r.blobSet {
return nil, fmt.Errorf("blob is not defined") 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 nil, fmt.Errorf("unable to convert after read has been performed")
} }
return NewTarReader( return NewTarReader(
WithDesc(b.desc), WithDesc(r.desc),
WithHeader(b.rawHeader), WithHeader(r.rawHeader),
WithRef(b.r), WithRef(r.r),
WithResp(b.resp), WithResp(r.resp),
WithReader(b.reader), WithReader(r.reader),
), nil ), nil
} }

View File

@ -14,36 +14,32 @@ import (
"github.com/regclient/regclient/types" "github.com/regclient/regclient/types"
) )
// TarReader reads or writes to a blob with tar contents and optional compression // TarReader was previously an interface. A type alias is provided for upgrading.
type TarReader interface { type TarReader = *BTarReader
Blob
io.Closer
GetTarReader() (*tar.Reader, error)
ReadFile(filename string) (*tar.Header, io.Reader, error)
}
type tarReader struct { // BTarReader is used to read individual files from an image layer.
common type BTarReader struct {
BCommon
origRdr io.Reader origRdr io.Reader
reader io.Reader reader io.Reader
digester digest.Digester digester digest.Digester
tr *tar.Reader tr *tar.Reader
} }
// NewTarReader creates a TarReader // NewTarReader creates a BTarReader.
func NewTarReader(opts ...Opts) TarReader { // Typically a BTarReader will be created using BReader.ToTarReader().
func NewTarReader(opts ...Opts) *BTarReader {
bc := blobConfig{} bc := blobConfig{}
for _, opt := range opts { for _, opt := range opts {
opt(&bc) opt(&bc)
} }
c := common{ tr := BTarReader{
desc: bc.desc, BCommon: BCommon{
r: bc.r, desc: bc.desc,
rawHeader: bc.header, r: bc.r,
resp: bc.resp, rawHeader: bc.header,
} resp: bc.resp,
tr := tarReader{ },
common: c,
origRdr: bc.rdr, origRdr: bc.rdr,
} }
if bc.rdr != nil { if bc.rdr != nil {
@ -61,8 +57,8 @@ func NewTarReader(opts ...Opts) TarReader {
return &tr return &tr
} }
// Close attempts to close the reader and populates/validates the digest // Close attempts to close the reader and populates/validates the digest.
func (tr *tarReader) Close() error { func (tr *BTarReader) Close() error {
// attempt to close if available in original reader // attempt to close if available in original reader
if trc, ok := tr.origRdr.(io.Closer); ok && trc != nil { if trc, ok := tr.origRdr.(io.Closer); ok && trc != nil {
return trc.Close() return trc.Close()
@ -70,8 +66,8 @@ func (tr *tarReader) Close() error {
return nil return nil
} }
// GetTarReader returns the tar.Reader for the blob // GetTarReader returns the tar.Reader for the blob.
func (tr *tarReader) GetTarReader() (*tar.Reader, error) { func (tr *BTarReader) GetTarReader() (*tar.Reader, error) {
if tr.reader == nil { if tr.reader == nil {
return nil, fmt.Errorf("blob has no reader defined") return nil, fmt.Errorf("blob has no reader defined")
} }
@ -85,8 +81,8 @@ func (tr *tarReader) GetTarReader() (*tar.Reader, error) {
return tr.tr, nil return tr.tr, nil
} }
// RawBody returns the original body from the request // RawBody returns the original body from the request.
func (tr *tarReader) RawBody() ([]byte, error) { func (tr *BTarReader) RawBody() ([]byte, error) {
if !tr.blobSet { if !tr.blobSet {
return []byte{}, fmt.Errorf("Blob is not defined") return []byte{}, fmt.Errorf("Blob is not defined")
} }
@ -109,8 +105,8 @@ func (tr *tarReader) RawBody() ([]byte, error) {
return b, err return b, err
} }
// ReadFile parses the tar to find a file // ReadFile parses the tar to find a file.
func (tr *tarReader) ReadFile(filename string) (*tar.Header, io.Reader, error) { func (tr *BTarReader) ReadFile(filename string) (*tar.Header, io.Reader, error) {
if strings.HasPrefix(filename, ".wh.") { if strings.HasPrefix(filename, ".wh.") {
return nil, nil, fmt.Errorf(".wh. prefix is reserved for whiteout files") return nil, nil, fmt.Errorf(".wh. prefix is reserved for whiteout files")
} }