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 }