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 }