diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /modules/packages/alpine | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | modules/packages/alpine/metadata.go | 242 | ||||
-rw-r--r-- | modules/packages/alpine/metadata_test.go | 144 |
2 files changed, 386 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 +} diff --git a/modules/packages/alpine/metadata_test.go b/modules/packages/alpine/metadata_test.go new file mode 100644 index 0000000..8167b49 --- /dev/null +++ b/modules/packages/alpine/metadata_test.go @@ -0,0 +1,144 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package alpine + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + packageDescription = "Package Description" + packageProjectURL = "https://gitea.io" + packageMaintainer = "KN4CK3R <dummy@gitea.io>" +) + +func createPKGINFOContent(name, version string) []byte { + return []byte(`pkgname = ` + name + ` +pkgver = ` + version + ` +pkgdesc = ` + packageDescription + ` +url = ` + packageProjectURL + ` +# comment +builddate = 1678834800 +packager = Gitea <pack@ag.er> +size = 123456 +arch = aarch64 +origin = origin +commit = 1111e709613fbc979651b09ac2bc27c6591a9999 +maintainer = ` + packageMaintainer + ` +license = MIT +depend = common +install_if = value +depend = gitea +provides = common +provides = gitea`) +} + +func TestParsePackage(t *testing.T) { + createPackage := func(name string, content []byte) io.Reader { + names := []string{"first.stream", name} + contents := [][]byte{{0}, content} + + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + + for i := range names { + if i != 0 { + zw.Close() + zw.Reset(&buf) + } + + tw := tar.NewWriter(zw) + hdr := &tar.Header{ + Name: names[i], + Mode: 0o600, + Size: int64(len(contents[i])), + } + tw.WriteHeader(hdr) + tw.Write(contents[i]) + tw.Close() + } + + zw.Close() + + return &buf + } + + t.Run("MissingPKGINFOFile", func(t *testing.T) { + data := createPackage("dummy.txt", []byte{}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + require.ErrorIs(t, err, ErrMissingPKGINFOFile) + }) + + t.Run("InvalidPKGINFOFile", func(t *testing.T) { + data := createPackage(".PKGINFO", []byte{}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + require.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPackage(".PKGINFO", createPKGINFOContent(packageName, packageVersion)) + + p, err := ParsePackage(data) + require.NoError(t, err) + assert.NotNil(t, p) + + assert.Equal(t, "Q1SRYURM5+uQDqfHSwTnNIOIuuDVQ=", p.FileMetadata.Checksum) + }) +} + +func TestParsePackageInfo(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + data := createPKGINFOContent("", packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + require.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("InvalidVersion", func(t *testing.T) { + data := createPKGINFOContent(packageName, "") + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + require.ErrorIs(t, err, ErrInvalidVersion) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPKGINFOContent(packageName, packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + require.NoError(t, err) + assert.NotNil(t, p) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, packageDescription, p.VersionMetadata.Description) + assert.Equal(t, packageMaintainer, p.VersionMetadata.Maintainer) + assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL) + assert.Equal(t, "MIT", p.VersionMetadata.License) + assert.Empty(t, p.FileMetadata.Checksum) + assert.Equal(t, "Gitea <pack@ag.er>", p.FileMetadata.Packager) + assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate) + assert.EqualValues(t, 123456, p.FileMetadata.Size) + assert.Equal(t, "aarch64", p.FileMetadata.Architecture) + assert.Equal(t, "origin", p.FileMetadata.Origin) + assert.Equal(t, "1111e709613fbc979651b09ac2bc27c6591a9999", p.FileMetadata.CommitHash) + assert.Equal(t, "value", p.FileMetadata.InstallIf) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Dependencies) + }) +} |