summaryrefslogtreecommitdiffstats
path: root/modules/packages/alpine/metadata.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/packages/alpine/metadata.go242
1 files changed, 242 insertions, 0 deletions
diff --git a/modules/packages/alpine/metadata.go b/modules/packages/alpine/metadata.go
new file mode 100644
index 0000000..582c426
--- /dev/null
+++ b/modules/packages/alpine/metadata.go
@@ -0,0 +1,242 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package alpine
+
+import (
+ "archive/tar"
+ "bufio"
+ "compress/gzip"
+ "crypto/sha1"
+ "encoding/base64"
+ "io"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/validation"
+)
+
+var (
+ ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf("PKGINFO file is missing")
+ ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
+ ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
+)
+
+const (
+ PropertyMetadata = "alpine.metadata"
+ PropertyBranch = "alpine.branch"
+ PropertyRepository = "alpine.repository"
+ PropertyArchitecture = "alpine.architecture"
+
+ SettingKeyPrivate = "alpine.key.private"
+ SettingKeyPublic = "alpine.key.public"
+
+ RepositoryPackage = "_alpine"
+ RepositoryVersion = "_repository"
+)
+
+// https://wiki.alpinelinux.org/wiki/Apk_spec
+
+// Package represents an Alpine package
+type Package struct {
+ Name string
+ Version string
+ VersionMetadata VersionMetadata
+ FileMetadata FileMetadata
+}
+
+// Metadata of an Alpine package
+type VersionMetadata struct {
+ Description string `json:"description,omitempty"`
+ License string `json:"license,omitempty"`
+ ProjectURL string `json:"project_url,omitempty"`
+ Maintainer string `json:"maintainer,omitempty"`
+}
+
+type FileMetadata struct {
+ Checksum string `json:"checksum"`
+ Packager string `json:"packager,omitempty"`
+ BuildDate int64 `json:"build_date,omitempty"`
+ Size int64 `json:"size,omitempty"`
+ Architecture string `json:"architecture,omitempty"`
+ Origin string `json:"origin,omitempty"`
+ CommitHash string `json:"commit_hash,omitempty"`
+ InstallIf string `json:"install_if,omitempty"`
+ Provides []string `json:"provides,omitempty"`
+ Dependencies []string `json:"dependencies,omitempty"`
+ ProviderPriority int64 `json:"provider_priority,omitempty"`
+}
+
+// ParsePackage parses the Alpine package file
+func ParsePackage(r io.Reader) (*Package, error) {
+ // Alpine packages are concated .tar.gz streams. Usually the first stream contains the package metadata.
+
+ br := bufio.NewReader(r) // needed for gzip Multistream
+
+ h := sha1.New()
+
+ gzr, err := gzip.NewReader(&teeByteReader{br, h})
+ if err != nil {
+ return nil, err
+ }
+ defer gzr.Close()
+
+ for {
+ gzr.Multistream(false)
+
+ tr := tar.NewReader(gzr)
+ for {
+ hd, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if hd.Name == ".PKGINFO" {
+ p, err := ParsePackageInfo(tr)
+ if err != nil {
+ return nil, err
+ }
+
+ // drain the reader
+ for {
+ if _, err := tr.Next(); err != nil {
+ break
+ }
+ }
+
+ p.FileMetadata.Checksum = "Q1" + base64.StdEncoding.EncodeToString(h.Sum(nil))
+
+ return p, nil
+ }
+ }
+
+ h = sha1.New()
+
+ err = gzr.Reset(&teeByteReader{br, h})
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return nil, ErrMissingPKGINFOFile
+}
+
+// ParsePackageInfo parses a PKGINFO file to retrieve the metadata of an Alpine package
+func ParsePackageInfo(r io.Reader) (*Package, error) {
+ p := &Package{}
+
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if strings.HasPrefix(line, "#") {
+ continue
+ }
+
+ i := strings.IndexRune(line, '=')
+ if i == -1 {
+ continue
+ }
+
+ key := strings.TrimSpace(line[:i])
+ value := strings.TrimSpace(line[i+1:])
+
+ switch key {
+ case "pkgname":
+ p.Name = value
+ case "pkgver":
+ p.Version = value
+ case "pkgdesc":
+ p.VersionMetadata.Description = value
+ case "url":
+ p.VersionMetadata.ProjectURL = value
+ case "builddate":
+ n, err := strconv.ParseInt(value, 10, 64)
+ if err == nil {
+ p.FileMetadata.BuildDate = n
+ }
+ case "size":
+ n, err := strconv.ParseInt(value, 10, 64)
+ if err == nil {
+ p.FileMetadata.Size = n
+ }
+ case "arch":
+ p.FileMetadata.Architecture = value
+ case "origin":
+ p.FileMetadata.Origin = value
+ case "commit":
+ p.FileMetadata.CommitHash = value
+ case "maintainer":
+ p.VersionMetadata.Maintainer = value
+ case "packager":
+ p.FileMetadata.Packager = value
+ case "license":
+ p.VersionMetadata.License = value
+ case "install_if":
+ p.FileMetadata.InstallIf = value
+ case "provides":
+ if value != "" {
+ p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
+ }
+ case "depend":
+ if value != "" {
+ p.FileMetadata.Dependencies = append(p.FileMetadata.Dependencies, value)
+ }
+ case "provider_priority":
+ n, err := strconv.ParseInt(value, 10, 64)
+ if err == nil {
+ p.FileMetadata.ProviderPriority = n
+ }
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ if p.Name == "" {
+ return nil, ErrInvalidName
+ }
+
+ if p.Version == "" {
+ return nil, ErrInvalidVersion
+ }
+
+ if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
+ p.VersionMetadata.ProjectURL = ""
+ }
+
+ return p, nil
+}
+
+// Same as io.TeeReader but implements io.ByteReader
+type teeByteReader struct {
+ r *bufio.Reader
+ w io.Writer
+}
+
+func (t *teeByteReader) Read(p []byte) (int, error) {
+ n, err := t.r.Read(p)
+ if n > 0 {
+ if n, err := t.w.Write(p[:n]); err != nil {
+ return n, err
+ }
+ }
+ return n, err
+}
+
+func (t *teeByteReader) ReadByte() (byte, error) {
+ b, err := t.r.ReadByte()
+ if err == nil {
+ if _, err := t.w.Write([]byte{b}); err != nil {
+ return 0, err
+ }
+ }
+ return b, err
+}