aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2024-12-05 00:09:07 +0100
committerGitHub <noreply@github.com>2024-12-04 23:09:07 +0000
commit0c3c041c88afc66a5048c1d8cf1b29c8bbbb798f (patch)
tree57b7605040ce7b707f32e45bae443e068c90f664 /modules
parent5ab7aa700f4cafcb33d8ad77708d7419ad2480fa (diff)
downloadgitea-0c3c041c88afc66a5048c1d8cf1b29c8bbbb798f.tar.gz
gitea-0c3c041c88afc66a5048c1d8cf1b29c8bbbb798f.zip
Add Arch package registry (#32692)
Close #25037 Close #31037 This PR adds a Arch package registry usable with pacman. ![grafik](https://github.com/user-attachments/assets/81cdb0c2-02f9-4733-bee2-e48af6b45224) Rewrite of #25396 and #31037. You can follow [this tutorial](https://wiki.archlinux.org/title/Creating_packages) to build a package for testing. Docs PR: https://gitea.com/gitea/docs/pulls/111 Co-authored-by: [d1nch8g@ion.lc](mailto:d1nch8g@ion.lc) Co-authored-by: @ExplodingDragon --------- Co-authored-by: dancheg97 <dancheg97@fmnx.su> Co-authored-by: dragon <ExplodingFKL@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'modules')
-rw-r--r--modules/packages/arch/metadata.go249
-rw-r--r--modules/packages/arch/metadata_test.go157
-rw-r--r--modules/setting/packages.go2
3 files changed, 408 insertions, 0 deletions
diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go
new file mode 100644
index 0000000000..e1e79c60e0
--- /dev/null
+++ b/modules/packages/arch/metadata.go
@@ -0,0 +1,249 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+ "archive/tar"
+ "bufio"
+ "bytes"
+ "compress/gzip"
+ "io"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/validation"
+
+ "github.com/klauspost/compress/zstd"
+ "github.com/ulikunitz/xz"
+)
+
+const (
+ PropertyRepository = "arch.repository"
+ PropertyArchitecture = "arch.architecture"
+ PropertySignature = "arch.signature"
+ PropertyMetadata = "arch.metadata"
+
+ SettingKeyPrivate = "arch.key.private"
+ SettingKeyPublic = "arch.key.public"
+
+ RepositoryPackage = "_arch"
+ RepositoryVersion = "_repository"
+
+ AnyArch = "any"
+)
+
+var (
+ ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing")
+ ErrUnsupportedFormat = util.NewInvalidArgumentErrorf("unsupported package container format")
+ ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
+ ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
+ ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
+
+ // https://man.archlinux.org/man/PKGBUILD.5
+ namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
+ versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
+)
+
+type Package struct {
+ Name string
+ Version string
+ VersionMetadata VersionMetadata
+ FileMetadata FileMetadata
+ FileCompressionExtension string
+}
+
+type VersionMetadata struct {
+ Description string `json:"description,omitempty"`
+ ProjectURL string `json:"project_url,omitempty"`
+ Licenses []string `json:"licenses,omitempty"`
+}
+
+type FileMetadata struct {
+ Architecture string `json:"architecture"`
+ Base string `json:"base,omitempty"`
+ InstalledSize int64 `json:"installed_size,omitempty"`
+ BuildDate int64 `json:"build_date,omitempty"`
+ Packager string `json:"packager,omitempty"`
+ Groups []string `json:"groups,omitempty"`
+ Provides []string `json:"provides,omitempty"`
+ Depends []string `json:"depends,omitempty"`
+ OptDepends []string `json:"opt_depends,omitempty"`
+ MakeDepends []string `json:"make_depends,omitempty"`
+ CheckDepends []string `json:"check_depends,omitempty"`
+ XData []string `json:"xdata,omitempty"`
+ Backup []string `json:"backup,omitempty"`
+ Files []string `json:"files,omitempty"`
+}
+
+// ParsePackage parses an Arch package file
+func ParsePackage(r io.Reader) (*Package, error) {
+ header := make([]byte, 10)
+ n, err := util.ReadAtMost(r, header)
+ if err != nil {
+ return nil, err
+ }
+
+ r = io.MultiReader(bytes.NewReader(header[:n]), r)
+
+ var inner io.Reader
+ var compressionType string
+ if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst
+ zr, err := zstd.NewReader(r)
+ if err != nil {
+ return nil, err
+ }
+ defer zr.Close()
+
+ inner = zr
+ compressionType = "zst"
+ } else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz
+ xzr, err := xz.NewReader(r)
+ if err != nil {
+ return nil, err
+ }
+
+ inner = xzr
+ compressionType = "xz"
+ } else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz
+ gzr, err := gzip.NewReader(r)
+ if err != nil {
+ return nil, err
+ }
+ defer gzr.Close()
+
+ inner = gzr
+ compressionType = "gz"
+ } else {
+ return nil, ErrUnsupportedFormat
+ }
+
+ var p *Package
+ files := make([]string, 0, 10)
+
+ tr := tar.NewReader(inner)
+ for {
+ hd, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if hd.Typeflag != tar.TypeReg {
+ continue
+ }
+
+ filename := hd.FileInfo().Name()
+ if filename == ".PKGINFO" {
+ p, err = ParsePackageInfo(tr)
+ if err != nil {
+ return nil, err
+ }
+ } else if !strings.HasPrefix(filename, ".") {
+ files = append(files, hd.Name)
+ }
+ }
+
+ if p == nil {
+ return nil, ErrMissingPKGINFOFile
+ }
+
+ p.FileMetadata.Files = files
+ p.FileCompressionExtension = compressionType
+
+ return p, nil
+}
+
+// ParsePackageInfo parses a .PKGINFO file to retrieve the metadata
+// https://man.archlinux.org/man/PKGBUILD.5
+// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_package.c#L161
+func ParsePackageInfo(r io.Reader) (*Package, error) {
+ p := &Package{}
+
+ s := bufio.NewScanner(r)
+ for s.Scan() {
+ line := s.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 "pkgbase":
+ p.FileMetadata.Base = value
+ case "pkgver":
+ p.Version = value
+ case "pkgdesc":
+ p.VersionMetadata.Description = value
+ case "url":
+ p.VersionMetadata.ProjectURL = value
+ case "packager":
+ p.FileMetadata.Packager = value
+ case "arch":
+ p.FileMetadata.Architecture = value
+ case "license":
+ p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value)
+ case "provides":
+ p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
+ case "depend":
+ p.FileMetadata.Depends = append(p.FileMetadata.Depends, value)
+ case "optdepend":
+ p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value)
+ case "makedepend":
+ p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value)
+ case "checkdepend":
+ p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value)
+ case "backup":
+ p.FileMetadata.Backup = append(p.FileMetadata.Backup, value)
+ case "group":
+ p.FileMetadata.Groups = append(p.FileMetadata.Groups, value)
+ case "builddate":
+ date, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ p.FileMetadata.BuildDate = date
+ case "size":
+ size, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ p.FileMetadata.InstalledSize = size
+ case "xdata":
+ p.FileMetadata.XData = append(p.FileMetadata.XData, value)
+ }
+ }
+ if err := s.Err(); err != nil {
+ return nil, err
+ }
+
+ if !namePattern.MatchString(p.Name) {
+ return nil, ErrInvalidName
+ }
+ if !versionPattern.MatchString(p.Version) {
+ return nil, ErrInvalidVersion
+ }
+ if p.FileMetadata.Architecture == "" {
+ return nil, ErrInvalidArchitecture
+ }
+
+ if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
+ p.VersionMetadata.ProjectURL = ""
+ }
+
+ return p, nil
+}
diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go
new file mode 100644
index 0000000000..f611ef5e84
--- /dev/null
+++ b/modules/packages/arch/metadata_test.go
@@ -0,0 +1,157 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+ "archive/tar"
+ "bytes"
+ "compress/gzip"
+ "io"
+ "testing"
+
+ "github.com/klauspost/compress/zstd"
+ "github.com/stretchr/testify/assert"
+ "github.com/ulikunitz/xz"
+)
+
+const (
+ packageName = "gitea"
+ packageVersion = "1.0.1"
+ packageDescription = "Package Description"
+ packageProjectURL = "https://gitea.com"
+ packagePackager = "KN4CK3R <packager@gitea.com>"
+)
+
+func createPKGINFOContent(name, version string) []byte {
+ return []byte(`pkgname = ` + name + `
+pkgbase = ` + name + `
+pkgver = ` + version + `
+pkgdesc = ` + packageDescription + `
+url = ` + packageProjectURL + `
+# comment
+group=group
+builddate = 1678834800
+size = 123456
+arch = x86_64
+license = MIT
+packager = ` + packagePackager + `
+depend = common
+xdata = value
+depend = gitea
+provides = common
+provides = gitea
+optdepend = hex
+checkdepend = common
+makedepend = cmake
+backup = usr/bin/paket1`)
+}
+
+func TestParsePackage(t *testing.T) {
+ createPackage := func(compression string, files map[string][]byte) io.Reader {
+ var buf bytes.Buffer
+ var cw io.WriteCloser
+ switch compression {
+ case "zst":
+ cw, _ = zstd.NewWriter(&buf)
+ case "xz":
+ cw, _ = xz.NewWriter(&buf)
+ case "gz":
+ cw = gzip.NewWriter(&buf)
+ }
+ tw := tar.NewWriter(cw)
+
+ for name, content := range files {
+ hdr := &tar.Header{
+ Name: name,
+ Mode: 0o600,
+ Size: int64(len(content)),
+ }
+ tw.WriteHeader(hdr)
+ tw.Write(content)
+ }
+
+ tw.Close()
+ cw.Close()
+
+ return &buf
+ }
+
+ for _, c := range []string{"gz", "xz", "zst"} {
+ t.Run(c, func(t *testing.T) {
+ t.Run("MissingPKGINFOFile", func(t *testing.T) {
+ data := createPackage(c, map[string][]byte{"dummy.txt": {}})
+
+ pp, err := ParsePackage(data)
+ assert.Nil(t, pp)
+ assert.ErrorIs(t, err, ErrMissingPKGINFOFile)
+ })
+
+ t.Run("InvalidPKGINFOFile", func(t *testing.T) {
+ data := createPackage(c, map[string][]byte{".PKGINFO": {}})
+
+ pp, err := ParsePackage(data)
+ assert.Nil(t, pp)
+ assert.ErrorIs(t, err, ErrInvalidName)
+ })
+
+ t.Run("Valid", func(t *testing.T) {
+ data := createPackage(c, map[string][]byte{
+ ".PKGINFO": createPKGINFOContent(packageName, packageVersion),
+ "/test/dummy.txt": {},
+ })
+
+ p, err := ParsePackage(data)
+ assert.NoError(t, err)
+ assert.NotNil(t, p)
+
+ assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files)
+ })
+ })
+ }
+}
+
+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)
+ assert.ErrorIs(t, err, ErrInvalidName)
+ })
+
+ t.Run("InvalidVersion", func(t *testing.T) {
+ data := createPKGINFOContent(packageName, "")
+
+ p, err := ParsePackageInfo(bytes.NewReader(data))
+ assert.Nil(t, p)
+ assert.ErrorIs(t, err, ErrInvalidVersion)
+ })
+
+ t.Run("Valid", func(t *testing.T) {
+ data := createPKGINFOContent(packageName, packageVersion)
+
+ p, err := ParsePackageInfo(bytes.NewReader(data))
+ assert.NoError(t, err)
+ assert.NotNil(t, p)
+
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageName, p.FileMetadata.Base)
+ assert.Equal(t, packageVersion, p.Version)
+ assert.Equal(t, packageDescription, p.VersionMetadata.Description)
+ assert.Equal(t, packagePackager, p.FileMetadata.Packager)
+ assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL)
+ assert.ElementsMatch(t, []string{"MIT"}, p.VersionMetadata.Licenses)
+ assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate)
+ assert.EqualValues(t, 123456, p.FileMetadata.InstalledSize)
+ assert.Equal(t, "x86_64", p.FileMetadata.Architecture)
+ assert.ElementsMatch(t, []string{"value"}, p.FileMetadata.XData)
+ assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups)
+ assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides)
+ assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends)
+ assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends)
+ assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends)
+ assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends)
+ assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup)
+ })
+}
diff --git a/modules/setting/packages.go b/modules/setting/packages.go
index bc093e7ea6..3f618cfd64 100644
--- a/modules/setting/packages.go
+++ b/modules/setting/packages.go
@@ -22,6 +22,7 @@ var (
LimitTotalOwnerCount int64
LimitTotalOwnerSize int64
LimitSizeAlpine int64
+ LimitSizeArch int64
LimitSizeCargo int64
LimitSizeChef int64
LimitSizeComposer int64
@@ -79,6 +80,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE")
+ Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH")
Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO")
Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF")
Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")