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