diff options
author | KN4CK3R <admin@oldschoolhack.me> | 2024-12-05 00:09:07 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-04 23:09:07 +0000 |
commit | 0c3c041c88afc66a5048c1d8cf1b29c8bbbb798f (patch) | |
tree | 57b7605040ce7b707f32e45bae443e068c90f664 /modules | |
parent | 5ab7aa700f4cafcb33d8ad77708d7419ad2480fa (diff) | |
download | gitea-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.go | 249 | ||||
-rw-r--r-- | modules/packages/arch/metadata_test.go | 157 | ||||
-rw-r--r-- | modules/setting/packages.go | 2 |
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") |