github.com/quay/claircore@v1.5.28/layer.go (about)

     1  package claircore
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  
    14  	"github.com/quay/claircore/pkg/tarfs"
    15  )
    16  
    17  // LayerDescription is a description of a container layer. It should contain
    18  // enough information to fetch the layer.
    19  //
    20  // Unlike the [Layer] type, this type does not have any extra state or access to
    21  // the contents of the layer.
    22  type LayerDescription struct {
    23  	// Digest is a content addressable checksum uniquely identifying this layer.
    24  	Digest string
    25  	// URI is a URI that can be used to fetch layer contents.
    26  	URI string
    27  	// MediaType is the [OCI Layer media type] for this layer. Any [Indexer]
    28  	// instance will support the OCI-defined media types, and may support others
    29  	// based on its configuration.
    30  	//
    31  	// [OCI Layer media type]: https://github.com/opencontainers/image-spec/blob/main/layer.md
    32  	MediaType string
    33  	// Headers is additional request headers for fetching layer contents.
    34  	Headers map[string][]string
    35  }
    36  
    37  // Layer is an internal representation of a container image file system layer.
    38  // Layers are stacked on top of each other to create the final file system of
    39  // the container image.
    40  //
    41  // This type being in the external API of the
    42  // [github.com/quay/claircore/libindex.Libindex] type is a historical accident.
    43  //
    44  // Previously, it was OK to use Layer literals. This is no longer allowed and
    45  // the [Layer.Init] method must be called. Any methods besides [Layer.Init]
    46  // called on an uninitialized Layer will report errors and may panic.
    47  type Layer struct {
    48  	noCopy noCopy
    49  	// Final is used to implement the panicking Finalizer.
    50  	//
    51  	// Without a unique allocation, we cannot set a Finalizer (so, when filling
    52  	// in a Layer in a slice). A pointer to a zero-sized type (like a *struct{})
    53  	// is not unique. So this camps on a unique heap allocation made for the
    54  	// purpose of tracking the Closed state of this Layer.
    55  	final *string
    56  
    57  	// Hash is a content addressable hash uniquely identifying this layer.
    58  	// Libindex will treat layers with this same hash as identical.
    59  	Hash Digest `json:"hash"`
    60  	// URI is a URI that can be used to fetch layer contents.
    61  	//
    62  	// Deprecated: This is exported for historical reasons and may stop being
    63  	// populated in the future.
    64  	URI string `json:"uri"`
    65  	// Headers is additional request headers for fetching layer contents.
    66  	//
    67  	// Deprecated: This is exported for historical reasons and may stop being
    68  	// populated in the future.
    69  	Headers map[string][]string `json:"headers"`
    70  
    71  	cleanup []io.Closer
    72  	sys     fs.FS
    73  	rd      io.ReaderAt
    74  	closed  bool // Used to catch double-closes.
    75  	init    bool // Used to track initialization.
    76  }
    77  
    78  // Init initializes a Layer in-place. This is provided for flexibility when
    79  // constructing a slice of Layers.
    80  func (l *Layer) Init(ctx context.Context, desc *LayerDescription, r io.ReaderAt) error {
    81  	if l.init {
    82  		return fmt.Errorf("claircore: Init called on already initialized Layer")
    83  	}
    84  	var err error
    85  	l.Hash, err = ParseDigest(desc.Digest)
    86  	if err != nil {
    87  		return err
    88  	}
    89  	l.URI = desc.URI
    90  	l.Headers = desc.Headers
    91  	l.rd = r
    92  	defer func() {
    93  		if l.init {
    94  			return
    95  		}
    96  		for _, f := range l.cleanup {
    97  			f.Close()
    98  		}
    99  	}()
   100  
   101  	switch desc.MediaType {
   102  	case `application/vnd.oci.image.layer.v1.tar`,
   103  		`application/vnd.oci.image.layer.v1.tar+gzip`,
   104  		`application/vnd.oci.image.layer.v1.tar+zstd`,
   105  		`application/vnd.oci.image.layer.nondistributable.v1.tar`,
   106  		`application/vnd.oci.image.layer.nondistributable.v1.tar+gzip`,
   107  		`application/vnd.oci.image.layer.nondistributable.v1.tar+zstd`:
   108  		sys, err := tarfs.New(r)
   109  		switch {
   110  		case errors.Is(err, nil):
   111  		default:
   112  			return fmt.Errorf("claircore: layer %v: unable to create fs.FS: %w", desc.Digest, err)
   113  		}
   114  		l.sys = sys
   115  	default:
   116  		return fmt.Errorf("claircore: layer %v: unknown MediaType %q", desc.Digest, desc.MediaType)
   117  	}
   118  
   119  	_, file, line, _ := runtime.Caller(1)
   120  	fmsg := fmt.Sprintf("%s:%d: Layer not closed", file, line)
   121  	l.final = &fmsg
   122  	runtime.SetFinalizer(l.final, func(msg *string) { panic(*msg) })
   123  	l.init = true
   124  	return nil
   125  }
   126  
   127  // Close releases held resources by this Layer.
   128  //
   129  // Not calling Close may cause the program to panic.
   130  func (l *Layer) Close() error {
   131  	if !l.init {
   132  		return errors.New("claircore: Close: uninitialized Layer")
   133  	}
   134  	if l.closed {
   135  		_, file, line, _ := runtime.Caller(1)
   136  		panic(fmt.Sprintf("%s:%d: Layer closed twice", file, line))
   137  	}
   138  	runtime.SetFinalizer(l.final, nil)
   139  	l.closed = true
   140  	errs := make([]error, len(l.cleanup))
   141  	for i, c := range l.cleanup {
   142  		errs[i] = c.Close()
   143  	}
   144  	return errors.Join(errs...)
   145  }
   146  
   147  // SetLocal is a namespacing wart.
   148  //
   149  // Deprecated: This function unconditionally errors and does nothing. Use the
   150  // [Layer.Init] method instead.
   151  func (l *Layer) SetLocal(_ string) error {
   152  	// TODO(hank) Just wrap errors.ErrUnsupported when updating to go1.21
   153  	return errUnsupported
   154  }
   155  
   156  type unsupported struct{}
   157  
   158  var errUnsupported = &unsupported{}
   159  
   160  func (*unsupported) Error() string {
   161  	return "unsupported operation"
   162  }
   163  func (*unsupported) Is(tgt error) bool {
   164  	// Hack for forwards compatibility: In go1.21, [errors.ErrUnsupported] was
   165  	// added and ideally we'd just use that. However, we're supporting go1.20
   166  	// until it's out of upstream support. This hack will make constructions
   167  	// like:
   168  	//
   169  	//	errors.Is(err, errors.ErrUnsupported)
   170  	//
   171  	// work as soon as a main module is built with go1.21.
   172  	return tgt.Error() == "unsupported operation"
   173  }
   174  
   175  // Fetched reports whether the layer blob has been fetched locally.
   176  //
   177  // Deprecated: Layers should now only be constructed by the code that does the
   178  // fetching. That is, merely having a valid Layer indicates that the blob has
   179  // been fetched.
   180  func (l *Layer) Fetched() bool {
   181  	return l.init
   182  }
   183  
   184  // FS returns an [fs.FS] reading from an initialized layer.
   185  func (l *Layer) FS() (fs.FS, error) {
   186  	if !l.init {
   187  		return nil, errors.New("claircore: unable to return FS: uninitialized Layer")
   188  	}
   189  	return l.sys, nil
   190  }
   191  
   192  // Reader returns a [ReadAtCloser] of the layer.
   193  //
   194  // It should also implement [io.Seeker], and should be a tar stream.
   195  func (l *Layer) Reader() (ReadAtCloser, error) {
   196  	if !l.init {
   197  		return nil, errors.New("claircore: unable to return Reader: uninitialized Layer")
   198  	}
   199  	// Some hacks for making the returned ReadAtCloser implements as many
   200  	// interfaces as possible.
   201  	switch rd := l.rd.(type) {
   202  	case *os.File:
   203  		fi, err := rd.Stat()
   204  		if err != nil {
   205  			return nil, fmt.Errorf("claircore: unable to stat file: %w", err)
   206  		}
   207  		return &fileAdapter{
   208  			SectionReader: io.NewSectionReader(rd, 0, fi.Size()),
   209  			File:          rd,
   210  		}, nil
   211  	default:
   212  	}
   213  	// Doing this with no size breaks the "seek to the end trick".
   214  	//
   215  	// This could do additional interface testing to support the various sizing
   216  	// tricks we do elsewhere.
   217  	return &rac{io.NewSectionReader(l.rd, 0, -1)}, nil
   218  }
   219  
   220  // Rac implements [io.Closer] on an [io.SectionReader].
   221  type rac struct {
   222  	*io.SectionReader
   223  }
   224  
   225  // Close implements [io.Closer].
   226  func (*rac) Close() error {
   227  	return nil
   228  }
   229  
   230  // FileAdapter implements [ReadAtCloser] in such a way that most of the File's
   231  // methods are promoted, but the [io.ReaderAt] and [io.ReadSeeker] interfaces
   232  // are dispatched to the [io.SectionReader] so that there's an independent
   233  // cursor.
   234  type fileAdapter struct {
   235  	*io.SectionReader
   236  	*os.File
   237  }
   238  
   239  // Read implements [io.Reader].
   240  func (a *fileAdapter) Read(p []byte) (n int, err error) {
   241  	return a.SectionReader.Read(p)
   242  }
   243  
   244  // ReadAt implements [io.ReaderAt].
   245  func (a *fileAdapter) ReadAt(p []byte, off int64) (n int, err error) {
   246  	return a.SectionReader.ReadAt(p, off)
   247  }
   248  
   249  // Seek implements [io.Seeker].
   250  func (a *fileAdapter) Seek(offset int64, whence int) (int64, error) {
   251  	return a.SectionReader.Seek(offset, whence)
   252  }
   253  
   254  // Close implements [io.Closer].
   255  func (*fileAdapter) Close() error {
   256  	return nil
   257  }
   258  
   259  // ReadAtCloser is an [io.ReadCloser] and also an [io.ReaderAt].
   260  type ReadAtCloser interface {
   261  	io.ReadCloser
   262  	io.ReaderAt
   263  }
   264  
   265  // NormalizeIn is used to make sure paths are tar-root relative.
   266  func normalizeIn(in, p string) string {
   267  	p = filepath.Clean(p)
   268  	if !filepath.IsAbs(p) {
   269  		p = filepath.Join(in, p)
   270  	}
   271  	if filepath.IsAbs(p) {
   272  		p = p[1:]
   273  	}
   274  	return p
   275  }
   276  
   277  // ErrNotFound is returned by [Layer.Files] if none of the requested files are
   278  // found.
   279  //
   280  // Deprecated: The [Layer.Files] method is deprecated.
   281  var ErrNotFound = errors.New("claircore: unable to find any requested files")
   282  
   283  // Files retrieves specific files from the layer's tar archive.
   284  //
   285  // An error is returned only if none of the requested files are found.
   286  //
   287  // The returned map may contain more entries than the number of paths requested.
   288  // All entries in the map are keyed by paths that are relative to the tar-root.
   289  // For example, requesting paths of "/etc/os-release", "./etc/os-release", and
   290  // "etc/os-release" will all result in any found content being stored with the
   291  // key "etc/os-release".
   292  //
   293  // Deprecated: Callers should instead use [fs.WalkDir] with the [fs.FS] returned
   294  // by [Layer.FS].
   295  func (l *Layer) Files(paths ...string) (map[string]*bytes.Buffer, error) {
   296  	// Clean the input paths.
   297  	want := make(map[string]struct{})
   298  	for i, p := range paths {
   299  		p := normalizeIn("/", p)
   300  		paths[i] = p
   301  		want[p] = struct{}{}
   302  	}
   303  
   304  	f := make(map[string]*bytes.Buffer)
   305  	// Walk the fs. ReadFile will handle symlink resolution.
   306  	if err := fs.WalkDir(l.sys, ".", func(p string, d fs.DirEntry, err error) error {
   307  		switch {
   308  		case err != nil:
   309  			return err
   310  		case d.IsDir():
   311  			return nil
   312  		}
   313  		if _, ok := want[p]; !ok {
   314  			return nil
   315  		}
   316  		delete(want, p)
   317  		b, err := fs.ReadFile(l.sys, p)
   318  		if err != nil {
   319  			return err
   320  		}
   321  		f[p] = bytes.NewBuffer(b)
   322  		return nil
   323  	}); err != nil {
   324  		return nil, err
   325  	}
   326  
   327  	// If there's nothing in the "f" map, we didn't find anything.
   328  	if len(f) == 0 {
   329  		return nil, ErrNotFound
   330  	}
   331  	return f, nil
   332  }
   333  
   334  // NoCopy is a marker to get `go vet` to complain about copying.
   335  type noCopy struct{}
   336  
   337  func (noCopy) Lock()   {}
   338  func (noCopy) Unlock() {}