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

     1  // Package pkgconfig implements a scanner that finds pkg-config files.
     2  //
     3  // Pkg-config is a widely-used package for finding linker and compiler flags on
     4  // Linux systems.
     5  package pkgconfig
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"io/fs"
    15  	"os"
    16  	"path/filepath"
    17  	"runtime/trace"
    18  
    19  	"github.com/quay/zlog"
    20  
    21  	"github.com/quay/claircore"
    22  	"github.com/quay/claircore/indexer"
    23  )
    24  
    25  var _ indexer.PackageScanner = (*Scanner)(nil)
    26  
    27  const (
    28  	pkgName    = `pkgconfig`
    29  	pkgVersion = `0.0.1`
    30  	pkgKind    = `package`
    31  )
    32  
    33  // Scanner finds pkg-config files in layers.
    34  type Scanner struct{}
    35  
    36  // Name implements scanner.VersionedScanner.
    37  func (*Scanner) Name() string { return pkgName }
    38  
    39  // Version implements scanner.VersionedScanner.
    40  func (*Scanner) Version() string { return pkgVersion }
    41  
    42  // Kind implements scanner.VersionedScanner.
    43  func (*Scanner) Kind() string { return pkgKind }
    44  
    45  // Scan attempts to find and enumerate pkg-config files.
    46  func (ps *Scanner) Scan(ctx context.Context, layer *claircore.Layer) ([]*claircore.Package, error) {
    47  	if err := ctx.Err(); err != nil {
    48  		return nil, err
    49  	}
    50  	defer trace.StartRegion(ctx, "Scanner.Scan").End()
    51  	trace.Log(ctx, "layer", layer.Hash.String())
    52  	ctx = zlog.ContextWithValues(ctx,
    53  		"component", "scanner/pkgconfig/Scanner.Scan",
    54  		"version", ps.Version(),
    55  		"layer", layer.Hash.String())
    56  	zlog.Debug(ctx).Msg("start")
    57  	defer zlog.Debug(ctx).Msg("done")
    58  
    59  	sys, err := layer.FS()
    60  	if err != nil {
    61  		return nil, fmt.Errorf("pkgconfig: opening layer failed: %w", err)
    62  	}
    63  
    64  	var ret []*claircore.Package
    65  	err = fs.WalkDir(sys, ".", func(p string, d fs.DirEntry, err error) error {
    66  		switch {
    67  		case err != nil:
    68  			return err
    69  		case d.IsDir():
    70  			return nil
    71  		case filepath.Ext(d.Name()) != ".pc":
    72  			return nil
    73  		}
    74  		zlog.Debug(ctx).
    75  			Str("path", p).
    76  			Msg("found possible pkg-config file")
    77  		f, err := sys.Open(p)
    78  		if err != nil {
    79  			return err
    80  		}
    81  		var pc pc
    82  		err = pc.Scan(f)
    83  		f.Close()
    84  		switch {
    85  		case errors.Is(nil, err):
    86  		case errors.Is(errInvalid, err): // skip
    87  			zlog.Info(ctx).
    88  				Str("path", p).
    89  				Msg("invalid pkg-config file")
    90  			return nil
    91  		default:
    92  			return err
    93  		}
    94  		ret = append(ret, &claircore.Package{
    95  			Name:           pc.Name,
    96  			Version:        pc.Version,
    97  			PackageDB:      filepath.Dir(p),
    98  			RepositoryHint: pc.URL,
    99  		})
   100  		return nil
   101  	})
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	return ret, nil
   106  }
   107  
   108  /*
   109  Below implements a subset of a pkg-config file scanner.
   110  
   111  In theory, we could just look for the Name and Version fields, extract the
   112  value, and be on our way. But the C source
   113  (https://cgit.freedesktop.org/pkg-config/tree/parse.c) makes sure to run all
   114  values through trim_and_sub, so we should do the same.
   115  */
   116  
   117  type pc struct {
   118  	Name    string
   119  	Version string
   120  	URL     string
   121  }
   122  
   123  func (pc *pc) Done() bool {
   124  	return pc.Name != "" &&
   125  		pc.Version != "" &&
   126  		pc.URL != ""
   127  }
   128  
   129  func (pc *pc) Err() bool {
   130  	return pc.Name != "" && pc.Version != ""
   131  }
   132  
   133  var errInvalid = errors.New("")
   134  
   135  func (pc *pc) Scan(r io.Reader) error {
   136  	vs := make(map[string]string)
   137  	expand := func(k string) string { return vs[k] }
   138  	s := bufio.NewScanner(r)
   139  
   140  	for s.Scan() && !pc.Done() {
   141  		b := s.Bytes()
   142  		i := bytes.IndexAny(b, ":=")
   143  		if i == -1 {
   144  			continue
   145  		}
   146  		tag := string(bytes.TrimSpace(b[:i]))
   147  		val := string(bytes.TrimSpace(b[i+1:]))
   148  		switch b[i] {
   149  		case '=': // Variable assignment
   150  			if _, exists := vs[tag]; exists {
   151  				return fmt.Errorf("duplicate variable assignment: %q", tag)
   152  			}
   153  			val = os.Expand(val, expand)
   154  			vs[tag] = val
   155  		case ':': // Key-Value
   156  			switch tag {
   157  			case "Name":
   158  				pc.Name = os.Expand(val, expand)
   159  			case "Version":
   160  				pc.Version = os.Expand(val, expand)
   161  			case "URL":
   162  				pc.URL = os.Expand(val, expand)
   163  			default: // skip
   164  			}
   165  		default:
   166  			panic("unreachable")
   167  		}
   168  	}
   169  	if err := s.Err(); err != nil {
   170  		return err
   171  	}
   172  	if !pc.Err() {
   173  		return errInvalid
   174  	}
   175  	return nil
   176  }