github.com/kubri/kubri@v0.5.1-0.20240317001612-bda2aaef967e/integrations/yum/repo.go (about) 1 package yum 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "context" 7 "crypto/sha256" 8 "encoding/hex" 9 "encoding/xml" 10 "errors" 11 "io/fs" 12 "os" 13 "path/filepath" 14 "strconv" 15 "strings" 16 "time" 17 18 "github.com/cavaliergopher/rpm" 19 20 "github.com/kubri/kubri/pkg/crypto/pgp" 21 "github.com/kubri/kubri/target" 22 ) 23 24 type repo struct { 25 primary *MetaData 26 filelists *FileLists 27 other *Other 28 29 dir string 30 files []string 31 } 32 33 func openRepo(ctx context.Context, t target.Target) (*repo, error) { 34 dir, err := os.MkdirTemp("", "") 35 if err != nil { 36 return nil, err 37 } 38 39 res := &repo{dir: dir} 40 41 var r RepoMD 42 if err := readXML(ctx, t, "repodata/repomd.xml", &r); err != nil { 43 if errors.Is(err, fs.ErrNotExist) { 44 res.primary = &MetaData{} 45 res.filelists = &FileLists{} 46 res.other = &Other{} 47 return res, nil 48 } 49 return nil, err 50 } 51 52 for _, v := range r.Data { 53 var r any 54 switch v.Type { 55 case "primary": 56 r = &res.primary 57 case "filelists": 58 r = &res.filelists 59 case "other": 60 r = &res.other 61 default: 62 continue 63 } 64 if err := readXML(ctx, t, v.Location.HREF, r); err != nil { 65 return nil, err 66 } 67 res.files = append(res.files, v.Location.HREF) 68 } 69 70 if res.primary == nil || res.filelists == nil || res.other == nil { 71 return nil, errors.New("invalid repomd.xml") 72 } 73 74 return res, nil 75 } 76 77 //nolint:funlen 78 func (r *repo) Add(b []byte) error { 79 h, err := rpm.Read(bytes.NewReader(b)) 80 if err != nil { 81 return err 82 } 83 84 id := h.String() 85 checksum := sha256.Sum256(b) // TODO: add checksum 86 start, end := h.HeaderRange() 87 files := h.Files() 88 89 p := Package{ 90 Type: "rpm", 91 Name: h.Name(), 92 Arch: h.Architecture(), 93 Version: Version{ 94 Ver: h.Version(), 95 Rel: h.Release(), 96 Epoch: strconv.Itoa(h.Epoch()), 97 }, 98 Checksum: Checksum{ 99 Type: "sha256", 100 PkgID: "YES", 101 Value: hex.EncodeToString(checksum[:]), 102 }, 103 Summary: h.Summary(), 104 Description: h.Description(), 105 Packager: h.Packager(), 106 URL: h.URL(), 107 Time: Time{ 108 File: timeNow(), 109 Build: int(h.BuildTime().Unix()), 110 }, 111 Size: Size{ 112 Package: len(b), 113 Archive: int(h.ArchiveSize()), 114 Installed: int(h.Size()), 115 }, 116 Location: Location{ 117 HREF: "Packages/" + id[0:1] + "/" + id + ".rpm", 118 }, 119 Format: Format{ 120 License: h.License(), 121 Vendor: h.Vendor(), 122 Group: h.Groups()[0], 123 BuildHost: h.BuildHost(), 124 SourceRPM: h.SourceRPM(), 125 HeaderRange: HeaderRange{Start: start, End: end}, 126 Provides: getEntries(h.Provides()), 127 Obsoletes: getEntries(h.Obsoletes()), 128 Requires: getEntries(h.Requires()), 129 Conflicts: getEntries(h.Conflicts()), 130 Files: filterPackageFiles(files), 131 }, 132 } 133 134 r.primary.Package = append(r.primary.Package, p) 135 136 r.filelists.Package = append(r.filelists.Package, FileListsPackage{ 137 Name: p.Name, 138 PkgID: p.Checksum.Value, 139 Arch: p.Arch, 140 Version: p.Version, 141 Files: convertPackageFiles(files), 142 }) 143 144 r.other.Package = append(r.other.Package, OtherPackage{ 145 Name: p.Name, 146 PkgID: p.Checksum.Value, 147 Arch: p.Arch, 148 Version: p.Version, 149 }) 150 151 return writeFile(filepath.Join(r.dir, p.Location.HREF), b) 152 } 153 154 //nolint:funlen 155 func (r *repo) Write(pgpKey *pgp.PrivateKey) error { 156 md := &RepoMD{} 157 158 data := map[string]any{ 159 "primary": r.primary, 160 "filelists": r.filelists, 161 "other": r.other, 162 } 163 164 for _, name := range []string{"primary", "filelists", "other"} { 165 raw, err := xmlMarshal(data[name]) 166 if err != nil { 167 return err 168 } 169 170 gz, err := compress(raw) 171 if err != nil { 172 return err 173 } 174 175 var d Data 176 d.Type = name 177 d.Checksum = getChecksum(gz) 178 d.OpenChecksum = getChecksum(raw) 179 d.Location.HREF = "repodata/" + d.Checksum.Value + "-" + name + ".xml.gz" 180 d.Timestamp = timeNow() 181 d.Size = len(gz) 182 d.OpenSize = len(raw) 183 184 err = writeFile(filepath.Join(r.dir, d.Location.HREF), gz) 185 if err != nil { 186 return err 187 } 188 189 md.Data = append(md.Data, d) 190 } 191 192 md.Revision = timeNow() 193 filename := filepath.Join(r.dir, "repodata/repomd.xml") 194 195 b, err := xmlMarshal(md) 196 if err != nil { 197 return err 198 } 199 if err = writeFile(filename, b); err != nil { 200 return err 201 } 202 203 if pgpKey != nil { 204 key, err := pgp.MarshalPublicKey(pgp.Public(pgpKey)) 205 if err != nil { 206 return err 207 } 208 if err = writeFile(filename+".key", key); err != nil { 209 return err 210 } 211 212 sig, err := pgp.Sign(pgpKey, b) 213 if err != nil { 214 return err 215 } 216 if err = writeFile(filename+".asc", sig); err != nil { 217 return err 218 } 219 } 220 221 return nil 222 } 223 224 func readXML(ctx context.Context, t target.Target, path string, res any) error { 225 rd, err := t.NewReader(ctx, path) 226 if err != nil { 227 return err 228 } 229 defer rd.Close() 230 231 r := rd 232 233 if strings.HasSuffix(path, ".gz") { 234 r, err = gzip.NewReader(rd) 235 if err != nil { 236 return err 237 } 238 defer r.Close() 239 } 240 241 return xml.NewDecoder(r).Decode(res) 242 } 243 244 func filterPackageFiles(files []rpm.FileInfo) []string { 245 var res []string 246 for _, f := range files { 247 if !f.IsDir() && strings.HasPrefix(f.Name(), "/etc/") || strings.Contains(f.Name(), "/bin/") { 248 res = append(res, f.Name()) 249 } 250 } 251 return res 252 } 253 254 func convertPackageFiles(files []rpm.FileInfo) []File { 255 res := make([]File, len(files)) 256 for i, f := range files { 257 var typ string 258 if f.IsDir() { 259 typ = "dir" 260 } 261 res[i] = File{Type: typ, Path: f.Name()} 262 } 263 return res 264 } 265 266 func getEntries(d []rpm.Dependency) *Entries { 267 if len(d) == 0 { 268 return nil 269 } 270 271 entries := make([]Entry, 0, len(d)) 272 273 for _, d := range d { 274 e := Entry{ 275 Name: d.Name(), 276 Ver: d.Version(), 277 Rel: d.Release(), 278 } 279 if e.Ver != "" { 280 e.Epoch = strconv.Itoa(d.Epoch()) 281 } 282 entries = append(entries, e) 283 } 284 285 return &Entries{entries} 286 } 287 288 func getChecksum(b []byte) Checksum { 289 sum := sha256.Sum256(b) 290 return Checksum{ 291 Type: "sha256", 292 Value: hex.EncodeToString(sum[:]), 293 } 294 } 295 296 func compress(p []byte) ([]byte, error) { 297 var b bytes.Buffer 298 w := gzip.NewWriter(&b) 299 if _, err := w.Write(p); err != nil { 300 return nil, err 301 } 302 if err := w.Close(); err != nil { 303 return nil, err 304 } 305 return b.Bytes(), nil 306 } 307 308 //nolint:gochecknoglobals 309 var replacer = strings.NewReplacer("></version>", "/>", "></time>", "/>", "></size>", "/>", 310 "></location>", "/>", "></rpm:entry>", "/>", "></rpm:header-range>", "/>") 311 312 func xmlMarshal(v any) ([]byte, error) { 313 w := bytes.NewBufferString(xml.Header) 314 b, err := xml.MarshalIndent(v, "", "\t") 315 if err != nil { 316 return nil, err 317 } 318 if _, err = replacer.WriteString(w, string(b)); err != nil { 319 return nil, err 320 } 321 return w.Bytes(), nil 322 } 323 324 func writeFile(path string, data []byte) error { 325 if err := os.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil { 326 return err 327 } 328 return os.WriteFile(path, data, 0o600) 329 } 330 331 //nolint:gochecknoglobals 332 var timeNow = func() int { return int(time.Now().Unix()) }