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

     1  // Package rpm provides an [indexer.PackageScanner] for the rpm package manager.
     2  package rpm
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path"
    11  	"runtime/trace"
    12  
    13  	"github.com/quay/zlog"
    14  
    15  	"github.com/quay/claircore"
    16  	"github.com/quay/claircore/indexer"
    17  	"github.com/quay/claircore/rpm/bdb"
    18  	"github.com/quay/claircore/rpm/ndb"
    19  	"github.com/quay/claircore/rpm/sqlite"
    20  )
    21  
    22  const (
    23  	pkgName    = "rpm"
    24  	pkgKind    = "package"
    25  	pkgVersion = "10"
    26  )
    27  
    28  var (
    29  	_ indexer.VersionedScanner = (*Scanner)(nil)
    30  	_ indexer.PackageScanner   = (*Scanner)(nil)
    31  )
    32  
    33  // Scanner implements the scanner.PackageScanner interface.
    34  //
    35  // This looks for directories that look like rpm databases and examines the
    36  // files it finds there.
    37  //
    38  // The zero value is ready to use.
    39  type Scanner struct{}
    40  
    41  // Name implements scanner.VersionedScanner.
    42  func (*Scanner) Name() string { return pkgName }
    43  
    44  // Version implements scanner.VersionedScanner.
    45  func (*Scanner) Version() string { return pkgVersion }
    46  
    47  // Kind implements scanner.VersionedScanner.
    48  func (*Scanner) Kind() string { return pkgKind }
    49  
    50  // Scan attempts to find rpm databases within the layer and enumerate the
    51  // packages there.
    52  //
    53  // A return of (nil, nil) is expected if there's no rpm database.
    54  func (ps *Scanner) Scan(ctx context.Context, layer *claircore.Layer) ([]*claircore.Package, error) {
    55  	if err := ctx.Err(); err != nil {
    56  		return nil, err
    57  	}
    58  	defer trace.StartRegion(ctx, "Scanner.Scan").End()
    59  	trace.Log(ctx, "layer", layer.Hash.String())
    60  	ctx = zlog.ContextWithValues(ctx,
    61  		"component", "rpm/Scanner.Scan",
    62  		"version", ps.Version(),
    63  		"layer", layer.Hash.String())
    64  	zlog.Debug(ctx).Msg("start")
    65  	defer zlog.Debug(ctx).Msg("done")
    66  
    67  	sys, err := layer.FS()
    68  	if err != nil {
    69  		return nil, fmt.Errorf("rpm: unable to open layer: %w", err)
    70  	}
    71  
    72  	found := make([]foundDB, 0)
    73  	if err := fs.WalkDir(sys, ".", findDBs(ctx, &found, sys)); err != nil {
    74  		return nil, fmt.Errorf("rpm: error walking fs: %w", err)
    75  	}
    76  	if len(found) == 0 {
    77  		return nil, nil
    78  	}
    79  
    80  	zlog.Debug(ctx).Int("count", len(found)).Msg("found possible databases")
    81  
    82  	var pkgs []*claircore.Package
    83  	done := map[string]struct{}{}
    84  	for _, db := range found {
    85  		ctx := zlog.ContextWithValues(ctx, "db", db.String())
    86  		zlog.Debug(ctx).Msg("examining database")
    87  		if _, ok := done[db.Path]; ok {
    88  			zlog.Debug(ctx).Msg("already seen, skipping")
    89  			continue
    90  		}
    91  		done[db.Path] = struct{}{}
    92  
    93  		var nat nativeDB // see native_db.go:/nativeDB
    94  		switch db.Kind {
    95  		case kindSQLite:
    96  			r, err := sys.Open(path.Join(db.Path, `rpmdb.sqlite`))
    97  			if err != nil {
    98  				return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)
    99  			}
   100  			defer func() {
   101  				if err := r.Close(); err != nil {
   102  					zlog.Warn(ctx).Err(err).Msg("unable to close sqlite db")
   103  				}
   104  			}()
   105  			f, err := os.CreateTemp(os.TempDir(), `rpmdb.sqlite.*`)
   106  			if err != nil {
   107  				return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)
   108  			}
   109  			defer func() {
   110  				if err := os.Remove(f.Name()); err != nil {
   111  					zlog.Error(ctx).Err(err).Msg("unable to unlink sqlite db")
   112  				}
   113  				if err := f.Close(); err != nil {
   114  					zlog.Warn(ctx).Err(err).Msg("unable to close sqlite db")
   115  				}
   116  			}()
   117  			zlog.Debug(ctx).Str("file", f.Name()).Msg("copying sqlite db out of FS")
   118  			if _, err := io.Copy(f, r); err != nil {
   119  				return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)
   120  			}
   121  			if err := f.Sync(); err != nil {
   122  				return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)
   123  			}
   124  			sdb, err := sqlite.Open(f.Name())
   125  			if err != nil {
   126  				return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)
   127  			}
   128  			defer sdb.Close()
   129  			nat = sdb
   130  		case kindBDB:
   131  			f, err := sys.Open(path.Join(db.Path, `Packages`))
   132  			if err != nil {
   133  				return nil, fmt.Errorf("rpm: error reading bdb db: %w", err)
   134  			}
   135  			defer f.Close()
   136  			r, done, err := mkAt(ctx, db.Kind, f)
   137  			if err != nil {
   138  				return nil, fmt.Errorf("rpm: error reading bdb db: %w", err)
   139  			}
   140  			defer done()
   141  			var bpdb bdb.PackageDB
   142  			if err := bpdb.Parse(r); err != nil {
   143  				return nil, fmt.Errorf("rpm: error parsing bdb db: %w", err)
   144  			}
   145  			nat = &bpdb
   146  		case kindNDB:
   147  			f, err := sys.Open(path.Join(db.Path, `Packages.db`))
   148  			if err != nil {
   149  				return nil, fmt.Errorf("rpm: error reading ndb db: %w", err)
   150  			}
   151  			defer f.Close()
   152  			r, done, err := mkAt(ctx, db.Kind, f)
   153  			if err != nil {
   154  				return nil, fmt.Errorf("rpm: error reading ndb db: %w", err)
   155  			}
   156  			defer done()
   157  			var npdb ndb.PackageDB
   158  			if err := npdb.Parse(r); err != nil {
   159  				return nil, fmt.Errorf("rpm: error parsing ndb db: %w", err)
   160  			}
   161  			nat = &npdb
   162  		default:
   163  			panic("programmer error: bad kind: " + db.Kind.String())
   164  		}
   165  		if err := nat.Validate(ctx); err != nil {
   166  			zlog.Warn(ctx).
   167  				Err(err).
   168  				Msg("rpm: invalid native DB")
   169  			continue
   170  		}
   171  		ps, err := packagesFromDB(ctx, db.String(), nat)
   172  		if err != nil {
   173  			return nil, fmt.Errorf("rpm: error reading native db: %w", err)
   174  		}
   175  		pkgs = append(pkgs, ps...)
   176  	}
   177  
   178  	return pkgs, nil
   179  }
   180  
   181  func findDBs(ctx context.Context, out *[]foundDB, sys fs.FS) fs.WalkDirFunc {
   182  	return func(p string, d fs.DirEntry, err error) error {
   183  		if err != nil {
   184  			return err
   185  		}
   186  		if d.IsDir() {
   187  			return nil
   188  		}
   189  
   190  		dir, n := path.Split(p)
   191  		dir = path.Clean(dir)
   192  		switch n {
   193  		case `Packages`:
   194  			f, err := sys.Open(p)
   195  			if err != nil {
   196  				return err
   197  			}
   198  			ok := bdb.CheckMagic(ctx, f)
   199  			f.Close()
   200  			if !ok {
   201  				return nil
   202  			}
   203  			*out = append(*out, foundDB{
   204  				Path: dir,
   205  				Kind: kindBDB,
   206  			})
   207  		case `rpmdb.sqlite`:
   208  			*out = append(*out, foundDB{
   209  				Path: dir,
   210  				Kind: kindSQLite,
   211  			})
   212  		case `Packages.db`:
   213  			f, err := sys.Open(p)
   214  			if err != nil {
   215  				return err
   216  			}
   217  			ok := ndb.CheckMagic(ctx, f)
   218  			f.Close()
   219  			if !ok {
   220  				return nil
   221  			}
   222  			*out = append(*out, foundDB{
   223  				Path: dir,
   224  				Kind: kindNDB,
   225  			})
   226  		}
   227  		return nil
   228  	}
   229  }
   230  
   231  func mkAt(ctx context.Context, k dbKind, f fs.File) (io.ReaderAt, func(), error) {
   232  	if r, ok := f.(io.ReaderAt); ok {
   233  		return r, func() {}, nil
   234  	}
   235  	spool, err := os.CreateTemp(os.TempDir(), `Packages.`+k.String()+`.`)
   236  	if err != nil {
   237  		return nil, nil, fmt.Errorf("rpm: error spooling db: %w", err)
   238  	}
   239  	ctx = zlog.ContextWithValues(ctx, "file", spool.Name())
   240  	if err := os.Remove(spool.Name()); err != nil {
   241  		zlog.Error(ctx).Err(err).Msg("unable to remove spool; file leaked!")
   242  	}
   243  	zlog.Debug(ctx).
   244  		Msg("copying db out of fs.FS")
   245  	if _, err := io.Copy(spool, f); err != nil {
   246  		if err := spool.Close(); err != nil {
   247  			zlog.Warn(ctx).Err(err).Msg("unable to close spool")
   248  		}
   249  		return nil, nil, fmt.Errorf("rpm: error spooling db: %w", err)
   250  	}
   251  	return spool, closeSpool(ctx, spool), nil
   252  }
   253  
   254  func closeSpool(ctx context.Context, f *os.File) func() {
   255  	return func() {
   256  		if err := f.Close(); err != nil {
   257  			zlog.Warn(ctx).Err(err).Msg("unable to close spool")
   258  		}
   259  	}
   260  }
   261  
   262  type dbKind uint
   263  
   264  //go:generate -command stringer go run golang.org/x/tools/cmd/stringer
   265  //go:generate stringer -linecomment -type dbKind
   266  
   267  const (
   268  	_ dbKind = iota
   269  
   270  	kindBDB    // bdb
   271  	kindSQLite // sqlite
   272  	kindNDB    // ndb
   273  )
   274  
   275  type foundDB struct {
   276  	Path string
   277  	Kind dbKind
   278  }
   279  
   280  func (f foundDB) String() string {
   281  	return f.Kind.String() + ":" + f.Path
   282  }