Arch packages implementation (#4785)
This PR is from https://github.com/go-gitea/gitea/pull/31037 This PR was originally created by @d1nch8g , and the original source code comes from https://ion.lc/core/gitea. This PR adds a package registry for [Arch Linux](https://archlinux.org/) packages with support for package files, [signatures](https://wiki.archlinux.org/title/Pacman/Package_signing), and automatic [pacman-database](https://archlinux.org/pacman/repo-add.8.html) management. Features: 1. Push any ` tar.zst ` package and Gitea sign it. 2. Delete endpoint for specific package version and all related files 3. Supports trust levels with `SigLevel = Required`. 4. Package UI with instructions to connect to the new pacman database and visualised package metadata  You can follow [this tutorial](https://wiki.archlinux.org/title/Creating_packages) to build a *.pkg.tar.zst package for testing docs pr: https://codeberg.org/forgejo/docs/pulls/791 Co-authored-by: d1nch8g@ion.lc Co-authored-by: @KN4CK3R Co-authored-by: @mahlzahn Co-authored-by: @silverwind Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4785 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Exploding Dragon <explodingfkl@gmail.com> Co-committed-by: Exploding Dragon <explodingfkl@gmail.com>
This commit is contained in:
		
							parent
							
								
									22d3659803
								
							
						
					
					
						commit
						f17194ca91
					
				
					 18 changed files with 1896 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -13,6 +13,7 @@ import (
 | 
			
		|||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages/alpine"
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages/arch"
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages/cargo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages/chef"
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages/composer"
 | 
			
		||||
| 
						 | 
				
			
			@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
 | 
			
		|||
	switch p.Type {
 | 
			
		||||
	case TypeAlpine:
 | 
			
		||||
		metadata = &alpine.VersionMetadata{}
 | 
			
		||||
	case TypeArch:
 | 
			
		||||
		metadata = &arch.VersionMetadata{}
 | 
			
		||||
	case TypeCargo:
 | 
			
		||||
		metadata = &cargo.Metadata{}
 | 
			
		||||
	case TypeChef:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ type Type string
 | 
			
		|||
// List of supported packages
 | 
			
		||||
const (
 | 
			
		||||
	TypeAlpine    Type = "alpine"
 | 
			
		||||
	TypeArch      Type = "arch"
 | 
			
		||||
	TypeCargo     Type = "cargo"
 | 
			
		||||
	TypeChef      Type = "chef"
 | 
			
		||||
	TypeComposer  Type = "composer"
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +58,7 @@ const (
 | 
			
		|||
 | 
			
		||||
var TypeList = []Type{
 | 
			
		||||
	TypeAlpine,
 | 
			
		||||
	TypeArch,
 | 
			
		||||
	TypeCargo,
 | 
			
		||||
	TypeChef,
 | 
			
		||||
	TypeComposer,
 | 
			
		||||
| 
						 | 
				
			
			@ -84,6 +86,8 @@ func (pt Type) Name() string {
 | 
			
		|||
	switch pt {
 | 
			
		||||
	case TypeAlpine:
 | 
			
		||||
		return "Alpine"
 | 
			
		||||
	case TypeArch:
 | 
			
		||||
		return "Arch"
 | 
			
		||||
	case TypeCargo:
 | 
			
		||||
		return "Cargo"
 | 
			
		||||
	case TypeChef:
 | 
			
		||||
| 
						 | 
				
			
			@ -133,6 +137,8 @@ func (pt Type) SVGName() string {
 | 
			
		|||
	switch pt {
 | 
			
		||||
	case TypeAlpine:
 | 
			
		||||
		return "gitea-alpine"
 | 
			
		||||
	case TypeArch:
 | 
			
		||||
		return "gitea-arch"
 | 
			
		||||
	case TypeCargo:
 | 
			
		||||
		return "gitea-cargo"
 | 
			
		||||
	case TypeChef:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										316
									
								
								modules/packages/arch/metadata.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								modules/packages/arch/metadata.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,316 @@
 | 
			
		|||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package arch
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/validation"
 | 
			
		||||
 | 
			
		||||
	"github.com/mholt/archiver/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Arch Linux Packages
 | 
			
		||||
// https://man.archlinux.org/man/PKGBUILD.5
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	PropertyDescription  = "arch.description"
 | 
			
		||||
	PropertyArch         = "arch.architecture"
 | 
			
		||||
	PropertyDistribution = "arch.distribution"
 | 
			
		||||
 | 
			
		||||
	SettingKeyPrivate = "arch.key.private"
 | 
			
		||||
	SettingKeyPublic  = "arch.key.public"
 | 
			
		||||
 | 
			
		||||
	RepositoryPackage = "_arch"
 | 
			
		||||
	RepositoryVersion = "_repository"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	reName   = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$`)
 | 
			
		||||
	reVer    = regexp.MustCompile(`^[a-zA-Z0-9:_.+]+-+[0-9]+$`)
 | 
			
		||||
	reOptDep = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$|^[a-zA-Z0-9@._+-]+(:.*)`)
 | 
			
		||||
	rePkgVer = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$|^[a-zA-Z0-9@._+-]+(>.*)|^[a-zA-Z0-9@._+-]+(<.*)|^[a-zA-Z0-9@._+-]+(=.*)`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Package struct {
 | 
			
		||||
	Name            string `json:"name"`
 | 
			
		||||
	Version         string `json:"version"` // Includes version, release and epoch
 | 
			
		||||
	VersionMetadata VersionMetadata
 | 
			
		||||
	FileMetadata    FileMetadata
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Arch package metadata related to specific version.
 | 
			
		||||
// Version metadata the same across different architectures and distributions.
 | 
			
		||||
type VersionMetadata struct {
 | 
			
		||||
	Base         string   `json:"base"`
 | 
			
		||||
	Description  string   `json:"description"`
 | 
			
		||||
	ProjectURL   string   `json:"project_url"`
 | 
			
		||||
	Groups       []string `json:"groups,omitempty"`
 | 
			
		||||
	Provides     []string `json:"provides,omitempty"`
 | 
			
		||||
	License      []string `json:"license,omitempty"`
 | 
			
		||||
	Depends      []string `json:"depends,omitempty"`
 | 
			
		||||
	OptDepends   []string `json:"opt_depends,omitempty"`
 | 
			
		||||
	MakeDepends  []string `json:"make_depends,omitempty"`
 | 
			
		||||
	CheckDepends []string `json:"check_depends,omitempty"`
 | 
			
		||||
	Conflicts    []string `json:"conflicts,omitempty"`
 | 
			
		||||
	Replaces     []string `json:"replaces,omitempty"`
 | 
			
		||||
	Backup       []string `json:"backup,omitempty"`
 | 
			
		||||
	Xdata        []string `json:"xdata,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FileMetadata Metadata related to specific package file.
 | 
			
		||||
// This metadata might vary for different architecture and distribution.
 | 
			
		||||
type FileMetadata struct {
 | 
			
		||||
	CompressedSize int64  `json:"compressed_size"`
 | 
			
		||||
	InstalledSize  int64  `json:"installed_size"`
 | 
			
		||||
	MD5            string `json:"md5"`
 | 
			
		||||
	SHA256         string `json:"sha256"`
 | 
			
		||||
	BuildDate      int64  `json:"build_date"`
 | 
			
		||||
	Packager       string `json:"packager"`
 | 
			
		||||
	Arch           string `json:"arch"`
 | 
			
		||||
	PgpSigned      string `json:"pgp"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParsePackage Function that receives arch package archive data and returns it's metadata.
 | 
			
		||||
func ParsePackage(r *packages.HashedBuffer) (*Package, error) {
 | 
			
		||||
	md5, _, sha256, _ := r.Sums()
 | 
			
		||||
	_, err := r.Seek(0, io.SeekStart)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	zstd := archiver.NewTarZstd()
 | 
			
		||||
	err = zstd.Open(r, 0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer zstd.Close()
 | 
			
		||||
 | 
			
		||||
	var pkg *Package
 | 
			
		||||
	var mtree bool
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		f, err := zstd.Read()
 | 
			
		||||
		if err == io.EOF {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		defer f.Close()
 | 
			
		||||
 | 
			
		||||
		switch f.Name() {
 | 
			
		||||
		case ".PKGINFO":
 | 
			
		||||
			pkg, err = ParsePackageInfo(f)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		case ".MTREE":
 | 
			
		||||
			mtree = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pkg == nil {
 | 
			
		||||
		return nil, util.NewInvalidArgumentErrorf(".PKGINFO file not found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !mtree {
 | 
			
		||||
		return nil, util.NewInvalidArgumentErrorf(".MTREE file not found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pkg.FileMetadata.CompressedSize = r.Size()
 | 
			
		||||
	pkg.FileMetadata.MD5 = hex.EncodeToString(md5)
 | 
			
		||||
	pkg.FileMetadata.SHA256 = hex.EncodeToString(sha256)
 | 
			
		||||
 | 
			
		||||
	return pkg, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParsePackageInfo Function that accepts reader for .PKGINFO file from package archive,
 | 
			
		||||
// validates all field according to PKGBUILD spec and returns 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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		key, value, find := strings.Cut(line, "=")
 | 
			
		||||
		if !find {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		key = strings.TrimSpace(key)
 | 
			
		||||
		value = strings.TrimSpace(value)
 | 
			
		||||
		switch key {
 | 
			
		||||
		case "pkgname":
 | 
			
		||||
			p.Name = value
 | 
			
		||||
		case "pkgbase":
 | 
			
		||||
			p.VersionMetadata.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.Arch = value
 | 
			
		||||
		case "provides":
 | 
			
		||||
			p.VersionMetadata.Provides = append(p.VersionMetadata.Provides, value)
 | 
			
		||||
		case "license":
 | 
			
		||||
			p.VersionMetadata.License = append(p.VersionMetadata.License, value)
 | 
			
		||||
		case "depend":
 | 
			
		||||
			p.VersionMetadata.Depends = append(p.VersionMetadata.Depends, value)
 | 
			
		||||
		case "optdepend":
 | 
			
		||||
			p.VersionMetadata.OptDepends = append(p.VersionMetadata.OptDepends, value)
 | 
			
		||||
		case "makedepend":
 | 
			
		||||
			p.VersionMetadata.MakeDepends = append(p.VersionMetadata.MakeDepends, value)
 | 
			
		||||
		case "checkdepend":
 | 
			
		||||
			p.VersionMetadata.CheckDepends = append(p.VersionMetadata.CheckDepends, value)
 | 
			
		||||
		case "backup":
 | 
			
		||||
			p.VersionMetadata.Backup = append(p.VersionMetadata.Backup, value)
 | 
			
		||||
		case "group":
 | 
			
		||||
			p.VersionMetadata.Groups = append(p.VersionMetadata.Groups, value)
 | 
			
		||||
		case "conflict":
 | 
			
		||||
			p.VersionMetadata.Conflicts = append(p.VersionMetadata.Conflicts, value)
 | 
			
		||||
		case "replaces":
 | 
			
		||||
			p.VersionMetadata.Replaces = append(p.VersionMetadata.Replaces, value)
 | 
			
		||||
		case "xdata":
 | 
			
		||||
			p.VersionMetadata.Xdata = append(p.VersionMetadata.Xdata, value)
 | 
			
		||||
		case "builddate":
 | 
			
		||||
			bd, err := strconv.ParseInt(value, 10, 64)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			p.FileMetadata.BuildDate = bd
 | 
			
		||||
		case "size":
 | 
			
		||||
			is, err := strconv.ParseInt(value, 10, 64)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			p.FileMetadata.InstalledSize = is
 | 
			
		||||
		default:
 | 
			
		||||
			return nil, util.NewInvalidArgumentErrorf("property is not supported %s", key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return p, errors.Join(scanner.Err(), ValidatePackageSpec(p))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ValidatePackageSpec Arch package validation according to PKGBUILD specification.
 | 
			
		||||
func ValidatePackageSpec(p *Package) error {
 | 
			
		||||
	if !reName.MatchString(p.Name) {
 | 
			
		||||
		return util.NewInvalidArgumentErrorf("invalid package name")
 | 
			
		||||
	}
 | 
			
		||||
	if !reName.MatchString(p.VersionMetadata.Base) {
 | 
			
		||||
		return util.NewInvalidArgumentErrorf("invalid package base")
 | 
			
		||||
	}
 | 
			
		||||
	if !reVer.MatchString(p.Version) {
 | 
			
		||||
		return util.NewInvalidArgumentErrorf("invalid package version")
 | 
			
		||||
	}
 | 
			
		||||
	if p.FileMetadata.Arch == "" {
 | 
			
		||||
		return util.NewInvalidArgumentErrorf("architecture should be specified")
 | 
			
		||||
	}
 | 
			
		||||
	if p.VersionMetadata.ProjectURL != "" {
 | 
			
		||||
		if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid project URL")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, cd := range p.VersionMetadata.CheckDepends {
 | 
			
		||||
		if !rePkgVer.MatchString(cd) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid check dependency: " + cd)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, d := range p.VersionMetadata.Depends {
 | 
			
		||||
		if !rePkgVer.MatchString(d) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid dependency: " + d)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, md := range p.VersionMetadata.MakeDepends {
 | 
			
		||||
		if !rePkgVer.MatchString(md) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid make dependency: " + md)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, p := range p.VersionMetadata.Provides {
 | 
			
		||||
		if !rePkgVer.MatchString(p) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid provides: " + p)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, p := range p.VersionMetadata.Conflicts {
 | 
			
		||||
		if !rePkgVer.MatchString(p) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid conflicts: " + p)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, p := range p.VersionMetadata.Replaces {
 | 
			
		||||
		if !rePkgVer.MatchString(p) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid replaces: " + p)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, p := range p.VersionMetadata.Replaces {
 | 
			
		||||
		if !rePkgVer.MatchString(p) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid xdata: " + p)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, od := range p.VersionMetadata.OptDepends {
 | 
			
		||||
		if !reOptDep.MatchString(od) {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("invalid optional dependency: " + od)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, bf := range p.VersionMetadata.Backup {
 | 
			
		||||
		if strings.HasPrefix(bf, "/") {
 | 
			
		||||
			return util.NewInvalidArgumentErrorf("backup file contains leading forward slash")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Desc Create pacman package description file.
 | 
			
		||||
func (p *Package) Desc() string {
 | 
			
		||||
	entries := []string{
 | 
			
		||||
		"FILENAME", fmt.Sprintf("%s-%s-%s.pkg.tar.zst", p.Name, p.Version, p.FileMetadata.Arch),
 | 
			
		||||
		"NAME", p.Name,
 | 
			
		||||
		"BASE", p.VersionMetadata.Base,
 | 
			
		||||
		"VERSION", p.Version,
 | 
			
		||||
		"DESC", p.VersionMetadata.Description,
 | 
			
		||||
		"GROUPS", strings.Join(p.VersionMetadata.Groups, "\n"),
 | 
			
		||||
		"CSIZE", fmt.Sprintf("%d", p.FileMetadata.CompressedSize),
 | 
			
		||||
		"ISIZE", fmt.Sprintf("%d", p.FileMetadata.InstalledSize),
 | 
			
		||||
		"MD5SUM", p.FileMetadata.MD5,
 | 
			
		||||
		"SHA256SUM", p.FileMetadata.SHA256,
 | 
			
		||||
		"PGPSIG", p.FileMetadata.PgpSigned,
 | 
			
		||||
		"URL", p.VersionMetadata.ProjectURL,
 | 
			
		||||
		"LICENSE", strings.Join(p.VersionMetadata.License, "\n"),
 | 
			
		||||
		"ARCH", p.FileMetadata.Arch,
 | 
			
		||||
		"BUILDDATE", fmt.Sprintf("%d", p.FileMetadata.BuildDate),
 | 
			
		||||
		"PACKAGER", p.FileMetadata.Packager,
 | 
			
		||||
		"REPLACES", strings.Join(p.VersionMetadata.Replaces, "\n"),
 | 
			
		||||
		"CONFLICTS", strings.Join(p.VersionMetadata.Conflicts, "\n"),
 | 
			
		||||
		"PROVIDES", strings.Join(p.VersionMetadata.Provides, "\n"),
 | 
			
		||||
		"DEPENDS", strings.Join(p.VersionMetadata.Depends, "\n"),
 | 
			
		||||
		"OPTDEPENDS", strings.Join(p.VersionMetadata.OptDepends, "\n"),
 | 
			
		||||
		"MAKEDEPENDS", strings.Join(p.VersionMetadata.MakeDepends, "\n"),
 | 
			
		||||
		"CHECKDEPENDS", strings.Join(p.VersionMetadata.CheckDepends, "\n"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
	for i := 0; i < len(entries); i += 2 {
 | 
			
		||||
		if entries[i+1] != "" {
 | 
			
		||||
			_, _ = fmt.Fprintf(&buf, "%%%s%%\n%s\n\n", entries[i], entries[i+1])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return buf.String()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										445
									
								
								modules/packages/arch/metadata_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								modules/packages/arch/metadata_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,445 @@
 | 
			
		|||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package arch
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"testing/fstest"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages"
 | 
			
		||||
 | 
			
		||||
	"github.com/mholt/archiver/v3"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParsePackage(t *testing.T) {
 | 
			
		||||
	// Minimal PKGINFO contents and test FS
 | 
			
		||||
	const PKGINFO = `pkgname = a
 | 
			
		||||
pkgbase = b
 | 
			
		||||
pkgver = 1-2
 | 
			
		||||
arch = x86_64
 | 
			
		||||
`
 | 
			
		||||
	fs := fstest.MapFS{
 | 
			
		||||
		"pkginfo": &fstest.MapFile{
 | 
			
		||||
			Data:    []byte(PKGINFO),
 | 
			
		||||
			Mode:    os.ModePerm,
 | 
			
		||||
			ModTime: time.Now(),
 | 
			
		||||
		},
 | 
			
		||||
		"mtree": &fstest.MapFile{
 | 
			
		||||
			Data:    []byte("data"),
 | 
			
		||||
			Mode:    os.ModePerm,
 | 
			
		||||
			ModTime: time.Now(),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Test .PKGINFO file
 | 
			
		||||
	pinf, err := fs.Stat("pkginfo")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	pfile, err := fs.Open("pkginfo")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// Test .MTREE file
 | 
			
		||||
	minf, err := fs.Stat("mtree")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	mfile, err := fs.Open("mtree")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	t.Run("normal archive", func(t *testing.T) {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
 | 
			
		||||
		archive := archiver.NewTarZstd()
 | 
			
		||||
		archive.Create(&buf)
 | 
			
		||||
 | 
			
		||||
		err = archive.Write(archiver.File{
 | 
			
		||||
			FileInfo: archiver.FileInfo{
 | 
			
		||||
				FileInfo:   pinf,
 | 
			
		||||
				CustomName: parcname,
 | 
			
		||||
			},
 | 
			
		||||
			ReadCloser: pfile,
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, errors.Join(pfile.Close(), err))
 | 
			
		||||
 | 
			
		||||
		err = archive.Write(archiver.File{
 | 
			
		||||
			FileInfo: archiver.FileInfo{
 | 
			
		||||
				FileInfo:   minf,
 | 
			
		||||
				CustomName: marcname,
 | 
			
		||||
			},
 | 
			
		||||
			ReadCloser: mfile,
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, errors.Join(mfile.Close(), archive.Close(), err))
 | 
			
		||||
 | 
			
		||||
		reader, err := packages.CreateHashedBufferFromReader(&buf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		defer reader.Close()
 | 
			
		||||
		_, err = ParsePackage(reader)
 | 
			
		||||
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("missing .PKGINFO", func(t *testing.T) {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
 | 
			
		||||
		archive := archiver.NewTarZstd()
 | 
			
		||||
		archive.Create(&buf)
 | 
			
		||||
		require.NoError(t, archive.Close())
 | 
			
		||||
 | 
			
		||||
		reader, err := packages.CreateHashedBufferFromReader(&buf)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		defer reader.Close()
 | 
			
		||||
		_, err = ParsePackage(reader)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), ".PKGINFO file not found")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("missing .MTREE", func(t *testing.T) {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
 | 
			
		||||
		pfile, err := fs.Open("pkginfo")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		archive := archiver.NewTarZstd()
 | 
			
		||||
		archive.Create(&buf)
 | 
			
		||||
 | 
			
		||||
		err = archive.Write(archiver.File{
 | 
			
		||||
			FileInfo: archiver.FileInfo{
 | 
			
		||||
				FileInfo:   pinf,
 | 
			
		||||
				CustomName: parcname,
 | 
			
		||||
			},
 | 
			
		||||
			ReadCloser: pfile,
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, errors.Join(pfile.Close(), archive.Close(), err))
 | 
			
		||||
		reader, err := packages.CreateHashedBufferFromReader(&buf)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		defer reader.Close()
 | 
			
		||||
		_, err = ParsePackage(reader)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), ".MTREE file not found")
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParsePackageInfo(t *testing.T) {
 | 
			
		||||
	const PKGINFO = `# Generated by makepkg 6.0.2
 | 
			
		||||
# using fakeroot version 1.31
 | 
			
		||||
pkgname = a
 | 
			
		||||
pkgbase = b
 | 
			
		||||
pkgver = 1-2
 | 
			
		||||
pkgdesc = comment
 | 
			
		||||
url = https://example.com/
 | 
			
		||||
group = group
 | 
			
		||||
builddate = 3
 | 
			
		||||
packager = Name Surname <login@example.com>
 | 
			
		||||
size = 5
 | 
			
		||||
arch = x86_64
 | 
			
		||||
license = BSD
 | 
			
		||||
provides = pvd
 | 
			
		||||
depend = smth
 | 
			
		||||
optdepend = hex
 | 
			
		||||
checkdepend = ola
 | 
			
		||||
makedepend = cmake
 | 
			
		||||
backup = usr/bin/paket1
 | 
			
		||||
`
 | 
			
		||||
	p, err := ParsePackageInfo(strings.NewReader(PKGINFO))
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.Equal(t, Package{
 | 
			
		||||
		Name:    "a",
 | 
			
		||||
		Version: "1-2",
 | 
			
		||||
		VersionMetadata: VersionMetadata{
 | 
			
		||||
			Base:         "b",
 | 
			
		||||
			Description:  "comment",
 | 
			
		||||
			ProjectURL:   "https://example.com/",
 | 
			
		||||
			Groups:       []string{"group"},
 | 
			
		||||
			Provides:     []string{"pvd"},
 | 
			
		||||
			License:      []string{"BSD"},
 | 
			
		||||
			Depends:      []string{"smth"},
 | 
			
		||||
			OptDepends:   []string{"hex"},
 | 
			
		||||
			MakeDepends:  []string{"cmake"},
 | 
			
		||||
			CheckDepends: []string{"ola"},
 | 
			
		||||
			Backup:       []string{"usr/bin/paket1"},
 | 
			
		||||
		},
 | 
			
		||||
		FileMetadata: FileMetadata{
 | 
			
		||||
			InstalledSize: 5,
 | 
			
		||||
			BuildDate:     3,
 | 
			
		||||
			Packager:      "Name Surname <login@example.com>",
 | 
			
		||||
			Arch:          "x86_64",
 | 
			
		||||
		},
 | 
			
		||||
	}, *p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestValidatePackageSpec(t *testing.T) {
 | 
			
		||||
	newpkg := func() Package {
 | 
			
		||||
		return Package{
 | 
			
		||||
			Name:    "abc",
 | 
			
		||||
			Version: "1-1",
 | 
			
		||||
			VersionMetadata: VersionMetadata{
 | 
			
		||||
				Base:         "ghx",
 | 
			
		||||
				Description:  "whoami",
 | 
			
		||||
				ProjectURL:   "https://example.com/",
 | 
			
		||||
				Groups:       []string{"gnome"},
 | 
			
		||||
				Provides:     []string{"abc", "def"},
 | 
			
		||||
				License:      []string{"GPL"},
 | 
			
		||||
				Depends:      []string{"go", "gpg=1", "curl>=3", "git<=7"},
 | 
			
		||||
				OptDepends:   []string{"git: something", "make"},
 | 
			
		||||
				MakeDepends:  []string{"chrom"},
 | 
			
		||||
				CheckDepends: []string{"bariy"},
 | 
			
		||||
				Backup:       []string{"etc/pacman.d/filo"},
 | 
			
		||||
			},
 | 
			
		||||
			FileMetadata: FileMetadata{
 | 
			
		||||
				CompressedSize: 1,
 | 
			
		||||
				InstalledSize:  2,
 | 
			
		||||
				SHA256:         "def",
 | 
			
		||||
				BuildDate:      3,
 | 
			
		||||
				Packager:       "smon",
 | 
			
		||||
				Arch:           "x86_64",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("valid package", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid package name", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.Name = "!$%@^!*&()"
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid package name")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid package base", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.Base = "!$%@^!*&()"
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid package base")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid package version", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.Base = "una-luna?"
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid package base")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid package version", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.Version = "una-luna"
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid package version")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("missing architecture", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.FileMetadata.Arch = ""
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "architecture should be specified")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid URL", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.ProjectURL = "http%%$#"
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid project URL")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid check dependency", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.CheckDepends = []string{"Err^_^"}
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid check dependency")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid dependency", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.Depends = []string{"^^abc"}
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid dependency")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid make dependency", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.MakeDepends = []string{"^m^"}
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid make dependency")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid provides", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.Provides = []string{"^m^"}
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid provides")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid optional dependency", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.OptDepends = []string{"^m^:MM"}
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid optional dependency")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("invalid optional dependency", func(t *testing.T) {
 | 
			
		||||
		p := newpkg()
 | 
			
		||||
		p.VersionMetadata.Backup = []string{"/ola/cola"}
 | 
			
		||||
 | 
			
		||||
		err := ValidatePackageSpec(&p)
 | 
			
		||||
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "backup file contains leading forward slash")
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDescString(t *testing.T) {
 | 
			
		||||
	const pkgdesc = `%FILENAME%
 | 
			
		||||
zstd-1.5.5-1-x86_64.pkg.tar.zst
 | 
			
		||||
 | 
			
		||||
%NAME%
 | 
			
		||||
zstd
 | 
			
		||||
 | 
			
		||||
%BASE%
 | 
			
		||||
zstd
 | 
			
		||||
 | 
			
		||||
%VERSION%
 | 
			
		||||
1.5.5-1
 | 
			
		||||
 | 
			
		||||
%DESC%
 | 
			
		||||
Zstandard - Fast real-time compression algorithm
 | 
			
		||||
 | 
			
		||||
%GROUPS%
 | 
			
		||||
dummy1
 | 
			
		||||
dummy2
 | 
			
		||||
 | 
			
		||||
%CSIZE%
 | 
			
		||||
401
 | 
			
		||||
 | 
			
		||||
%ISIZE%
 | 
			
		||||
1500453
 | 
			
		||||
 | 
			
		||||
%MD5SUM%
 | 
			
		||||
5016660ef3d9aa148a7b72a08d3df1b2
 | 
			
		||||
 | 
			
		||||
%SHA256SUM%
 | 
			
		||||
9fa4ede47e35f5971e4f26ecadcbfb66ab79f1d638317ac80334a3362dedbabd
 | 
			
		||||
 | 
			
		||||
%URL%
 | 
			
		||||
https://facebook.github.io/zstd/
 | 
			
		||||
 | 
			
		||||
%LICENSE%
 | 
			
		||||
BSD
 | 
			
		||||
GPL2
 | 
			
		||||
 | 
			
		||||
%ARCH%
 | 
			
		||||
x86_64
 | 
			
		||||
 | 
			
		||||
%BUILDDATE%
 | 
			
		||||
1681646714
 | 
			
		||||
 | 
			
		||||
%PACKAGER%
 | 
			
		||||
Jelle van der Waa <jelle@archlinux.org>
 | 
			
		||||
 | 
			
		||||
%PROVIDES%
 | 
			
		||||
libzstd.so=1-64
 | 
			
		||||
 | 
			
		||||
%DEPENDS%
 | 
			
		||||
glibc
 | 
			
		||||
gcc-libs
 | 
			
		||||
zlib
 | 
			
		||||
xz
 | 
			
		||||
lz4
 | 
			
		||||
 | 
			
		||||
%OPTDEPENDS%
 | 
			
		||||
dummy3
 | 
			
		||||
dummy4
 | 
			
		||||
 | 
			
		||||
%MAKEDEPENDS%
 | 
			
		||||
cmake
 | 
			
		||||
gtest
 | 
			
		||||
ninja
 | 
			
		||||
 | 
			
		||||
%CHECKDEPENDS%
 | 
			
		||||
dummy5
 | 
			
		||||
dummy6
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
	md := &Package{
 | 
			
		||||
		Name:    "zstd",
 | 
			
		||||
		Version: "1.5.5-1",
 | 
			
		||||
		VersionMetadata: VersionMetadata{
 | 
			
		||||
			Base:         "zstd",
 | 
			
		||||
			Description:  "Zstandard - Fast real-time compression algorithm",
 | 
			
		||||
			ProjectURL:   "https://facebook.github.io/zstd/",
 | 
			
		||||
			Groups:       []string{"dummy1", "dummy2"},
 | 
			
		||||
			Provides:     []string{"libzstd.so=1-64"},
 | 
			
		||||
			License:      []string{"BSD", "GPL2"},
 | 
			
		||||
			Depends:      []string{"glibc", "gcc-libs", "zlib", "xz", "lz4"},
 | 
			
		||||
			OptDepends:   []string{"dummy3", "dummy4"},
 | 
			
		||||
			MakeDepends:  []string{"cmake", "gtest", "ninja"},
 | 
			
		||||
			CheckDepends: []string{"dummy5", "dummy6"},
 | 
			
		||||
		},
 | 
			
		||||
		FileMetadata: FileMetadata{
 | 
			
		||||
			CompressedSize: 401,
 | 
			
		||||
			InstalledSize:  1500453,
 | 
			
		||||
			MD5:            "5016660ef3d9aa148a7b72a08d3df1b2",
 | 
			
		||||
			SHA256:         "9fa4ede47e35f5971e4f26ecadcbfb66ab79f1d638317ac80334a3362dedbabd",
 | 
			
		||||
			BuildDate:      1681646714,
 | 
			
		||||
			Packager:       "Jelle van der Waa <jelle@archlinux.org>",
 | 
			
		||||
			Arch:           "x86_64",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	require.Equal(t, pkgdesc, md.Desc())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ var (
 | 
			
		|||
		LimitTotalOwnerCount  int64
 | 
			
		||||
		LimitTotalOwnerSize   int64
 | 
			
		||||
		LimitSizeAlpine       int64
 | 
			
		||||
		LimitSizeArch         int64
 | 
			
		||||
		LimitSizeCargo        int64
 | 
			
		||||
		LimitSizeChef         int64
 | 
			
		||||
		LimitSizeComposer     int64
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +84,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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3611,6 +3611,22 @@ alpine.repository = Repository Info
 | 
			
		|||
alpine.repository.branches = Branches
 | 
			
		||||
alpine.repository.repositories = Repositories
 | 
			
		||||
alpine.repository.architectures = Architectures
 | 
			
		||||
arch.pacman.helper.gpg = Add trust certificate for pacman:
 | 
			
		||||
arch.pacman.repo.multi = %s has the same version in different distributions.
 | 
			
		||||
arch.pacman.repo.multi.item = Configuration for %s
 | 
			
		||||
arch.pacman.conf = Add server with related distribution and architecture to <code>/etc/pacman.conf</code> :
 | 
			
		||||
arch.pacman.sync = Sync package with pacman:
 | 
			
		||||
arch.version.properties = Version Properties
 | 
			
		||||
arch.version.description = Description
 | 
			
		||||
arch.version.provides = Provides
 | 
			
		||||
arch.version.groups = Group
 | 
			
		||||
arch.version.depends = Depends
 | 
			
		||||
arch.version.optdepends = Optional depends
 | 
			
		||||
arch.version.makedepends = Make depends
 | 
			
		||||
arch.version.checkdepends = Check depends
 | 
			
		||||
arch.version.conflicts = Conflicts
 | 
			
		||||
arch.version.replaces = Replaces
 | 
			
		||||
arch.version.backup = Backup
 | 
			
		||||
cargo.registry = Setup this registry in the Cargo configuration file (for example <code>~/.cargo/config.toml</code>):
 | 
			
		||||
cargo.install = To install the package using Cargo, run the following command:
 | 
			
		||||
chef.registry = Setup this registry in your <code>~/.chef/config.rb</code> file:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								public/assets/img/svg/gitea-arch.svg
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/assets/img/svg/gitea-arch.svg
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg gitea-arch" width="16" height="16" aria-hidden="true"><path fill="#1793d1" d="M256 72c-14 35-23 57-39 91 10 11 22 23 41 36-21-8-35-17-45-26-21 43-53 103-117 220 50-30 90-48 127-55-2-7-3-14-3-22v-1c1-33 18-58 38-56 20 1 36 29 35 62l-2 17c36 7 75 26 125 54l-27-50c-13-10-27-23-55-38 19 5 33 11 44 17-86-159-93-180-122-250z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 402 B  | 
| 
						 | 
				
			
			@ -15,6 +15,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/alpine"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/arch"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/cargo"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/chef"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/composer"
 | 
			
		||||
| 
						 | 
				
			
			@ -137,6 +138,17 @@ func CommonRoutes() *web.Route {
 | 
			
		|||
				})
 | 
			
		||||
			})
 | 
			
		||||
		}, reqPackageAccess(perm.AccessModeRead))
 | 
			
		||||
		r.Group("/arch", func() {
 | 
			
		||||
			r.Group("/repository.key", func() {
 | 
			
		||||
				r.Head("", arch.GetRepositoryKey)
 | 
			
		||||
				r.Get("", arch.GetRepositoryKey)
 | 
			
		||||
			})
 | 
			
		||||
			r.Group("/{distro}", func() {
 | 
			
		||||
				r.Put("", reqPackageAccess(perm.AccessModeWrite), arch.PushPackage)
 | 
			
		||||
				r.Get("/{arch}/{file}", arch.GetPackageOrDB)
 | 
			
		||||
				r.Delete("/{package}/{version}", reqPackageAccess(perm.AccessModeWrite), arch.RemovePackage)
 | 
			
		||||
			})
 | 
			
		||||
		}, reqPackageAccess(perm.AccessModeRead))
 | 
			
		||||
		r.Group("/cargo", func() {
 | 
			
		||||
			r.Group("/api/v1/crates", func() {
 | 
			
		||||
				r.Get("", cargo.SearchPackages)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										248
									
								
								routers/api/packages/arch/arch.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								routers/api/packages/arch/arch.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,248 @@
 | 
			
		|||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package arch
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	arch_module "code.gitea.io/gitea/modules/packages/arch"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/helper"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
	arch_service "code.gitea.io/gitea/services/packages/arch"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func apiError(ctx *context.Context, status int, obj any) {
 | 
			
		||||
	helper.LogAndProcessError(ctx, status, obj, func(message string) {
 | 
			
		||||
		ctx.PlainText(status, message)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetRepositoryKey(ctx *context.Context) {
 | 
			
		||||
	_, pub, err := arch_service.GetOrCreateKeyPair(ctx, ctx.Package.Owner.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
 | 
			
		||||
		ContentType: "application/pgp-keys",
 | 
			
		||||
		Filename:    "repository.key",
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PushPackage(ctx *context.Context) {
 | 
			
		||||
	distro := ctx.Params("distro")
 | 
			
		||||
 | 
			
		||||
	upload, needToClose, err := ctx.UploadStream()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if needToClose {
 | 
			
		||||
		defer upload.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf, err := packages_module.CreateHashedBufferFromReader(upload)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer buf.Close()
 | 
			
		||||
 | 
			
		||||
	p, err := arch_module.ParsePackage(buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = buf.Seek(0, io.SeekStart)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	sign, err := arch_service.NewFileSign(ctx, ctx.Package.Owner.ID, buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer sign.Close()
 | 
			
		||||
	_, err = buf.Seek(0, io.SeekStart)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// update gpg sign
 | 
			
		||||
	pgp, err := io.ReadAll(sign)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	p.FileMetadata.PgpSigned = base64.StdEncoding.EncodeToString(pgp)
 | 
			
		||||
	_, err = sign.Seek(0, io.SeekStart)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	properties := map[string]string{
 | 
			
		||||
		arch_module.PropertyDescription:  p.Desc(),
 | 
			
		||||
		arch_module.PropertyArch:         p.FileMetadata.Arch,
 | 
			
		||||
		arch_module.PropertyDistribution: distro,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	version, _, err := packages_service.CreatePackageOrAddFileToExisting(
 | 
			
		||||
		ctx,
 | 
			
		||||
		&packages_service.PackageCreationInfo{
 | 
			
		||||
			PackageInfo: packages_service.PackageInfo{
 | 
			
		||||
				Owner:       ctx.Package.Owner,
 | 
			
		||||
				PackageType: packages_model.TypeArch,
 | 
			
		||||
				Name:        p.Name,
 | 
			
		||||
				Version:     p.Version,
 | 
			
		||||
			},
 | 
			
		||||
			Creator:  ctx.Doer,
 | 
			
		||||
			Metadata: p.VersionMetadata,
 | 
			
		||||
		},
 | 
			
		||||
		&packages_service.PackageFileCreationInfo{
 | 
			
		||||
			PackageFileInfo: packages_service.PackageFileInfo{
 | 
			
		||||
				Filename:     fmt.Sprintf("%s-%s-%s.pkg.tar.zst", p.Name, p.Version, p.FileMetadata.Arch),
 | 
			
		||||
				CompositeKey: distro,
 | 
			
		||||
			},
 | 
			
		||||
			OverwriteExisting: false,
 | 
			
		||||
			IsLead:            true,
 | 
			
		||||
			Creator:           ctx.ContextUser,
 | 
			
		||||
			Data:              buf,
 | 
			
		||||
			Properties:        properties,
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		switch {
 | 
			
		||||
		case errors.Is(err, packages_model.ErrDuplicatePackageVersion), errors.Is(err, packages_model.ErrDuplicatePackageFile):
 | 
			
		||||
			apiError(ctx, http.StatusConflict, err)
 | 
			
		||||
		case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
 | 
			
		||||
			apiError(ctx, http.StatusForbidden, err)
 | 
			
		||||
		default:
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// add sign file
 | 
			
		||||
	_, err = packages_service.AddFileToPackageVersionInternal(ctx, version, &packages_service.PackageFileCreationInfo{
 | 
			
		||||
		PackageFileInfo: packages_service.PackageFileInfo{
 | 
			
		||||
			CompositeKey: distro,
 | 
			
		||||
			Filename:     fmt.Sprintf("%s-%s-%s.pkg.tar.zst.sig", p.Name, p.Version, p.FileMetadata.Arch),
 | 
			
		||||
		},
 | 
			
		||||
		OverwriteExisting: true,
 | 
			
		||||
		IsLead:            false,
 | 
			
		||||
		Creator:           ctx.Doer,
 | 
			
		||||
		Data:              sign,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
	}
 | 
			
		||||
	if err = arch_service.BuildPacmanDB(ctx, ctx.Package.Owner.ID, distro, p.FileMetadata.Arch); err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Status(http.StatusCreated)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetPackageOrDB(ctx *context.Context) {
 | 
			
		||||
	var (
 | 
			
		||||
		file   = ctx.Params("file")
 | 
			
		||||
		distro = ctx.Params("distro")
 | 
			
		||||
		arch   = ctx.Params("arch")
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if strings.HasSuffix(file, ".pkg.tar.zst") || strings.HasSuffix(file, ".pkg.tar.zst.sig") {
 | 
			
		||||
		pkg, err := arch_service.GetPackageFile(ctx, distro, file, ctx.Package.Owner.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
				apiError(ctx, http.StatusNotFound, err)
 | 
			
		||||
			} else {
 | 
			
		||||
				apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ctx.ServeContent(pkg, &context.ServeHeaderOptions{
 | 
			
		||||
			Filename: file,
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.HasSuffix(file, ".db.tar.gz") ||
 | 
			
		||||
		strings.HasSuffix(file, ".db") ||
 | 
			
		||||
		strings.HasSuffix(file, ".db.tar.gz.sig") ||
 | 
			
		||||
		strings.HasSuffix(file, ".db.sig") {
 | 
			
		||||
		pkg, err := arch_service.GetPackageDBFile(ctx, distro, arch, ctx.Package.Owner.ID,
 | 
			
		||||
			strings.HasSuffix(file, ".sig"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
				apiError(ctx, http.StatusNotFound, err)
 | 
			
		||||
			} else {
 | 
			
		||||
				apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.ServeContent(pkg, &context.ServeHeaderOptions{
 | 
			
		||||
			Filename: file,
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Status(http.StatusNotFound)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RemovePackage(ctx *context.Context) {
 | 
			
		||||
	var (
 | 
			
		||||
		distro = ctx.Params("distro")
 | 
			
		||||
		pkg    = ctx.Params("package")
 | 
			
		||||
		ver    = ctx.Params("version")
 | 
			
		||||
	)
 | 
			
		||||
	pv, err := packages_model.GetVersionByNameAndVersion(
 | 
			
		||||
		ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg, ver,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
			apiError(ctx, http.StatusNotFound, err)
 | 
			
		||||
		} else {
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	files, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	deleted := false
 | 
			
		||||
	for _, file := range files {
 | 
			
		||||
		if file.CompositeKey == distro {
 | 
			
		||||
			deleted = true
 | 
			
		||||
			err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx, ctx.ContextUser, file)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if deleted {
 | 
			
		||||
		err = arch_service.BuildCustomRepositoryFiles(ctx, ctx.Package.Owner.ID, distro)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Status(http.StatusNoContent)
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.Error(http.StatusNotFound)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
package user
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +19,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
 | 
			
		||||
	arch_model "code.gitea.io/gitea/modules/packages/arch"
 | 
			
		||||
	debian_module "code.gitea.io/gitea/modules/packages/debian"
 | 
			
		||||
	rpm_module "code.gitea.io/gitea/modules/packages/rpm"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
| 
						 | 
				
			
			@ -200,6 +202,19 @@ func ViewPackageVersion(ctx *context.Context) {
 | 
			
		|||
		ctx.Data["Branches"] = util.Sorted(branches.Values())
 | 
			
		||||
		ctx.Data["Repositories"] = util.Sorted(repositories.Values())
 | 
			
		||||
		ctx.Data["Architectures"] = util.Sorted(architectures.Values())
 | 
			
		||||
	case packages_model.TypeArch:
 | 
			
		||||
		ctx.Data["RegistryHost"] = setting.Packages.RegistryHost
 | 
			
		||||
		ctx.Data["SignMail"] = fmt.Sprintf("%s@noreply.%s", ctx.Package.Owner.Name, setting.Packages.RegistryHost)
 | 
			
		||||
		groups := make(container.Set[string])
 | 
			
		||||
		for _, f := range pd.Files {
 | 
			
		||||
			for _, pp := range f.Properties {
 | 
			
		||||
				switch pp.Name {
 | 
			
		||||
				case arch_model.PropertyDistribution:
 | 
			
		||||
					groups.Add(pp.Value)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Data["Groups"] = util.Sorted(groups.Values())
 | 
			
		||||
	case packages_model.TypeDebian:
 | 
			
		||||
		distributions := make(container.Set[string])
 | 
			
		||||
		components := make(container.Set[string])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										348
									
								
								services/packages/arch/repository.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										348
									
								
								services/packages/arch/repository.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,348 @@
 | 
			
		|||
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package arch
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"archive/tar"
 | 
			
		||||
	"compress/gzip"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	arch_module "code.gitea.io/gitea/modules/packages/arch"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
 | 
			
		||||
	"github.com/ProtonMail/go-crypto/openpgp"
 | 
			
		||||
	"github.com/ProtonMail/go-crypto/openpgp/armor"
 | 
			
		||||
	"github.com/ProtonMail/go-crypto/openpgp/packet"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) {
 | 
			
		||||
	return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeArch, arch_module.RepositoryPackage, arch_module.RepositoryVersion)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
 | 
			
		||||
	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// remove old db files
 | 
			
		||||
	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, pf := range pfs {
 | 
			
		||||
		if strings.HasSuffix(pf.Name, ".db") {
 | 
			
		||||
			arch := strings.TrimSuffix(strings.TrimPrefix(pf.Name, fmt.Sprintf("%s-", pf.CompositeKey)), ".db")
 | 
			
		||||
			if err := BuildPacmanDB(ctx, ownerID, pf.CompositeKey, arch); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BuildCustomRepositoryFiles(ctx context.Context, ownerID int64, disco string) error {
 | 
			
		||||
	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// remove old db files
 | 
			
		||||
	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, pf := range pfs {
 | 
			
		||||
		if strings.HasSuffix(pf.Name, ".db") && pf.CompositeKey == disco {
 | 
			
		||||
			arch := strings.TrimSuffix(strings.TrimPrefix(pf.Name, fmt.Sprintf("%s-", pf.CompositeKey)), ".db")
 | 
			
		||||
			if err := BuildPacmanDB(ctx, ownerID, pf.CompositeKey, arch); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFileSign(ctx context.Context, ownerID int64, input io.Reader) (*packages_module.HashedBuffer, error) {
 | 
			
		||||
	// If no signature is specified, it will be generated by Gitea.
 | 
			
		||||
	priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	block, err := armor.Decode(strings.NewReader(priv))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	pkgSig, err := packages_module.NewHashedBuffer()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer pkgSig.Close()
 | 
			
		||||
	if err := openpgp.DetachSign(pkgSig, e, input, nil); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return pkgSig, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BuildPacmanDB Create db signature cache
 | 
			
		||||
func BuildPacmanDB(ctx context.Context, ownerID int64, distro, arch string) error {
 | 
			
		||||
	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// remove old db files
 | 
			
		||||
	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, pf := range pfs {
 | 
			
		||||
		if pf.CompositeKey == distro && strings.HasPrefix(pf.Name, fmt.Sprintf("%s-%s", distro, arch)) {
 | 
			
		||||
			// remove distro and arch
 | 
			
		||||
			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	db, err := flushDB(ctx, ownerID, distro, arch)
 | 
			
		||||
	if errors.Is(err, io.EOF) {
 | 
			
		||||
		return nil
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer db.Close()
 | 
			
		||||
	// Create db signature cache
 | 
			
		||||
	_, err = db.Seek(0, io.SeekStart)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	sig, err := NewFileSign(ctx, ownerID, db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer sig.Close()
 | 
			
		||||
	_, err = db.Seek(0, io.SeekStart)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for name, data := range map[string]*packages_module.HashedBuffer{
 | 
			
		||||
		fmt.Sprintf("%s-%s.db", distro, arch):     db,
 | 
			
		||||
		fmt.Sprintf("%s-%s.db.sig", distro, arch): sig,
 | 
			
		||||
	} {
 | 
			
		||||
		_, err = packages_service.AddFileToPackageVersionInternal(ctx, pv, &packages_service.PackageFileCreationInfo{
 | 
			
		||||
			PackageFileInfo: packages_service.PackageFileInfo{
 | 
			
		||||
				Filename:     name,
 | 
			
		||||
				CompositeKey: distro,
 | 
			
		||||
			},
 | 
			
		||||
			Creator:           user_model.NewGhostUser(),
 | 
			
		||||
			Data:              data,
 | 
			
		||||
			IsLead:            false,
 | 
			
		||||
			OverwriteExisting: true,
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func flushDB(ctx context.Context, ownerID int64, distro, arch string) (*packages_module.HashedBuffer, error) {
 | 
			
		||||
	pkgs, err := packages_model.GetPackagesByType(ctx, ownerID, packages_model.TypeArch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if len(pkgs) == 0 {
 | 
			
		||||
		return nil, io.EOF
 | 
			
		||||
	}
 | 
			
		||||
	db, err := packages_module.NewHashedBuffer()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	gw := gzip.NewWriter(db)
 | 
			
		||||
	tw := tar.NewWriter(gw)
 | 
			
		||||
	count := 0
 | 
			
		||||
	for _, pkg := range pkgs {
 | 
			
		||||
		versions, err := packages_model.GetVersionsByPackageName(
 | 
			
		||||
			ctx, ownerID, packages_model.TypeArch, pkg.Name,
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err)
 | 
			
		||||
		}
 | 
			
		||||
		sort.Slice(versions, func(i, j int) bool {
 | 
			
		||||
			return versions[i].CreatedUnix > versions[j].CreatedUnix
 | 
			
		||||
		})
 | 
			
		||||
		for _, ver := range versions {
 | 
			
		||||
			file := fmt.Sprintf("%s-%s-%s.pkg.tar.zst", pkg.Name, ver.Version, arch)
 | 
			
		||||
			pf, err := packages_model.GetFileForVersionByName(ctx, ver.ID, file, distro)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// add any arch package
 | 
			
		||||
				file = fmt.Sprintf("%s-%s-any.pkg.tar.zst", pkg.Name, ver.Version)
 | 
			
		||||
				pf, err = packages_model.GetFileForVersionByName(ctx, ver.ID, file, distro)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			pps, err := packages_model.GetPropertiesByName(
 | 
			
		||||
				ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyDescription,
 | 
			
		||||
			)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err)
 | 
			
		||||
			}
 | 
			
		||||
			if len(pps) >= 1 {
 | 
			
		||||
				meta := []byte(pps[0].Value)
 | 
			
		||||
				header := &tar.Header{
 | 
			
		||||
					Name: pkg.Name + "-" + ver.Version + "/desc",
 | 
			
		||||
					Size: int64(len(meta)),
 | 
			
		||||
					Mode: int64(os.ModePerm),
 | 
			
		||||
				}
 | 
			
		||||
				if err = tw.WriteHeader(header); err != nil {
 | 
			
		||||
					return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err)
 | 
			
		||||
				}
 | 
			
		||||
				if _, err := tw.Write(meta); err != nil {
 | 
			
		||||
					return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err)
 | 
			
		||||
				}
 | 
			
		||||
				count++
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	defer gw.Close()
 | 
			
		||||
	defer tw.Close()
 | 
			
		||||
	if count == 0 {
 | 
			
		||||
		return nil, errors.Join(db.Close(), io.EOF)
 | 
			
		||||
	}
 | 
			
		||||
	return db, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPackageFile Get data related to provided filename and distribution, for package files
 | 
			
		||||
// update download counter.
 | 
			
		||||
func GetPackageFile(ctx context.Context, distro, file string, ownerID int64) (io.ReadSeekCloser, error) {
 | 
			
		||||
	pf, err := getPackageFile(ctx, distro, file, ownerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filestream, _, _, err := packages_service.GetPackageFileStream(ctx, pf)
 | 
			
		||||
	return filestream, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Ejects parameters required to get package file property from file name.
 | 
			
		||||
func getPackageFile(ctx context.Context, distro, file string, ownerID int64) (*packages_model.PackageFile, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		splt    = strings.Split(file, "-")
 | 
			
		||||
		pkgname = strings.Join(splt[0:len(splt)-3], "-")
 | 
			
		||||
		vername = splt[len(splt)-3] + "-" + splt[len(splt)-2]
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	version, err := packages_model.GetVersionByNameAndVersion(ctx, ownerID, packages_model.TypeArch, pkgname, vername)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pkgfile, err := packages_model.GetFileForVersionByName(ctx, version.ID, file, distro)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return pkgfile, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetPackageDBFile(ctx context.Context, distro, arch string, ownerID int64, signFile bool) (io.ReadSeekCloser, error) {
 | 
			
		||||
	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	fileName := fmt.Sprintf("%s-%s.db", distro, arch)
 | 
			
		||||
	if signFile {
 | 
			
		||||
		fileName = fmt.Sprintf("%s-%s.db.sig", distro, arch)
 | 
			
		||||
	}
 | 
			
		||||
	file, err := packages_model.GetFileForVersionByName(ctx, pv.ID, fileName, distro)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	filestream, _, _, err := packages_service.GetPackageFileStream(ctx, file)
 | 
			
		||||
	return filestream, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files
 | 
			
		||||
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
 | 
			
		||||
	priv, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPrivate)
 | 
			
		||||
	if err != nil && !errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pub, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPublic)
 | 
			
		||||
	if err != nil && !errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if priv == "" || pub == "" {
 | 
			
		||||
		user, err := user_model.GetUserByID(ctx, ownerID)
 | 
			
		||||
		if err != nil && !errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
			return "", "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		priv, pub, err = generateKeypair(user.Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPrivate, priv); err != nil {
 | 
			
		||||
			return "", "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPublic, pub); err != nil {
 | 
			
		||||
			return "", "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return priv, pub, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func generateKeypair(owner string) (string, string, error) {
 | 
			
		||||
	e, err := openpgp.NewEntity(
 | 
			
		||||
		owner,
 | 
			
		||||
		"Arch Package signature only",
 | 
			
		||||
		fmt.Sprintf("%s@noreply.%s", owner, setting.Packages.RegistryHost), &packet.Config{
 | 
			
		||||
			RSABits: 4096,
 | 
			
		||||
		})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var priv strings.Builder
 | 
			
		||||
	var pub strings.Builder
 | 
			
		||||
 | 
			
		||||
	w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
	if err := e.SerializePrivate(w, nil); err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
	w.Close()
 | 
			
		||||
 | 
			
		||||
	w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
	if err := e.Serialize(w); err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
	w.Close()
 | 
			
		||||
 | 
			
		||||
	return priv.String(), pub.String(), nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ import (
 | 
			
		|||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
	alpine_service "code.gitea.io/gitea/services/packages/alpine"
 | 
			
		||||
	arch_service "code.gitea.io/gitea/services/packages/arch"
 | 
			
		||||
	cargo_service "code.gitea.io/gitea/services/packages/cargo"
 | 
			
		||||
	container_service "code.gitea.io/gitea/services/packages/container"
 | 
			
		||||
	debian_service "code.gitea.io/gitea/services/packages/debian"
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +133,10 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
 | 
			
		|||
				if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
 | 
			
		||||
					return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
 | 
			
		||||
				}
 | 
			
		||||
			} else if pcr.Type == packages_model.TypeArch {
 | 
			
		||||
				if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
 | 
			
		||||
					return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -359,6 +359,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
 | 
			
		|||
	switch packageType {
 | 
			
		||||
	case packages_model.TypeAlpine:
 | 
			
		||||
		typeSpecificSize = setting.Packages.LimitSizeAlpine
 | 
			
		||||
	case packages_model.TypeArch:
 | 
			
		||||
		typeSpecificSize = setting.Packages.LimitSizeArch
 | 
			
		||||
	case packages_model.TypeCargo:
 | 
			
		||||
		typeSpecificSize = setting.Packages.LimitSizeCargo
 | 
			
		||||
	case packages_model.TypeChef:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										143
									
								
								templates/package/content/arch.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								templates/package/content/arch.tmpl
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,143 @@
 | 
			
		|||
{{if eq .PackageDescriptor.Package.Type "arch"}}
 | 
			
		||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.installation"}}</h4>
 | 
			
		||||
<div class="ui attached segment">
 | 
			
		||||
	<div class="ui form">
 | 
			
		||||
		<div class="field">
 | 
			
		||||
			<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.arch.pacman.helper.gpg"}}</label>
 | 
			
		||||
			<div class="markup">
 | 
			
		||||
				<pre class="code-block"><code>wget -O sign.gpg <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/arch/repository.key"></origin-url>
 | 
			
		||||
pacman-key --add sign.gpg
 | 
			
		||||
pacman-key --lsign-key '{{$.SignMail}}'</code></pre>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="field">
 | 
			
		||||
			<label>{{svg "octicon-gear"}} {{ctx.Locale.Tr "packages.arch.pacman.conf"}}</label>
 | 
			
		||||
			<div class="markup">
 | 
			
		||||
				<pre
 | 
			
		||||
					class="code-block"><code>
 | 
			
		||||
{{- if gt (len $.Groups) 1 -}}
 | 
			
		||||
# {{ctx.Locale.Tr "packages.arch.pacman.repo.multi"  $.PackageDescriptor.Package.LowerName}}
 | 
			
		||||
 | 
			
		||||
{{end -}}
 | 
			
		||||
{{- $GroupSize := (len .Groups) -}}
 | 
			
		||||
{{-  range $i,$v :=  .Groups -}}
 | 
			
		||||
{{- if gt $i 0}}
 | 
			
		||||
{{end -}}{{- if gt $GroupSize 1 -}}
 | 
			
		||||
# {{ctx.Locale.Tr "packages.arch.pacman.repo.multi.item" .}}
 | 
			
		||||
{{end -}}
 | 
			
		||||
[{{$.PackageDescriptor.Owner.LowerName}}.{{$.RegistryHost}}]
 | 
			
		||||
SigLevel = Required
 | 
			
		||||
Server = <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/arch/{{.}}/$arch"></origin-url>
 | 
			
		||||
{{end -}}
 | 
			
		||||
</code></pre>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="field">
 | 
			
		||||
			<label>{{svg "octicon-sync"}} {{ctx.Locale.Tr "packages.arch.pacman.sync"}}</label>
 | 
			
		||||
			<div class="markup">
 | 
			
		||||
				<pre class="code-block"><code>pacman -Sy {{.PackageDescriptor.Package.LowerName}}</code></pre>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="field">
 | 
			
		||||
			<label>{{ctx.Locale.Tr "packages.registry.documentation" "Arch"
 | 
			
		||||
				"https://forgejo.org/docs/latest/user/packages/arch/"}}</label>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.arch.version.properties"}}</h4>
 | 
			
		||||
<div class="ui attached segment">
 | 
			
		||||
	<table class="ui very basic compact table">
 | 
			
		||||
		<tbody>
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.description"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{.PackageDescriptor.Metadata.Description}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.Groups}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.groups"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Groups ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.Provides}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.provides"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Provides ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.Depends}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.depends"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Depends ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.OptDepends}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.optdepends"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.OptDepends ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.MakeDepends}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.makedepends"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.MakeDepends ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.CheckDepends}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.checkdepends"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.CheckDepends ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.Conflicts}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.conflicts"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Conflicts ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.Replaces}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.replaces"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Replaces ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			{{if .PackageDescriptor.Metadata.Backup}}
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td class="collapsing">
 | 
			
		||||
					<h5>{{ctx.Locale.Tr "packages.arch.version.backup"}}</h5>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Backup ", "}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			{{end}}
 | 
			
		||||
		</tbody>
 | 
			
		||||
	</table>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{{end}}
 | 
			
		||||
							
								
								
									
										4
									
								
								templates/package/metadata/arch.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								templates/package/metadata/arch.tmpl
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
{{if eq .PackageDescriptor.Package.Type "arch"}}
 | 
			
		||||
	{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}}
 | 
			
		||||
	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +19,7 @@
 | 
			
		|||
		<div class="issue-content">
 | 
			
		||||
			<div class="issue-content-left">
 | 
			
		||||
				{{template "package/content/alpine" .}}
 | 
			
		||||
				{{template "package/content/arch" .}}
 | 
			
		||||
				{{template "package/content/cargo" .}}
 | 
			
		||||
				{{template "package/content/chef" .}}
 | 
			
		||||
				{{template "package/content/composer" .}}
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +51,7 @@
 | 
			
		|||
					<div class="item">{{svg "octicon-calendar" 16 "tw-mr-2"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}</div>
 | 
			
		||||
					<div class="item">{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
 | 
			
		||||
					{{template "package/metadata/alpine" .}}
 | 
			
		||||
					{{template "package/metadata/arch" .}}
 | 
			
		||||
					{{template "package/metadata/cargo" .}}
 | 
			
		||||
					{{template "package/metadata/chef" .}}
 | 
			
		||||
					{{template "package/metadata/composer" .}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										327
									
								
								tests/integration/api_packages_arch_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								tests/integration/api_packages_arch_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,327 @@
 | 
			
		|||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package integration
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"archive/tar"
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"compress/gzip"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"testing/fstest"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/packages"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	arch_model "code.gitea.io/gitea/modules/packages/arch"
 | 
			
		||||
	"code.gitea.io/gitea/tests"
 | 
			
		||||
 | 
			
		||||
	"github.com/ProtonMail/go-crypto/openpgp/armor"
 | 
			
		||||
	"github.com/ProtonMail/go-crypto/openpgp/packet"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestPackageArch(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
	unPack := func(s string) []byte {
 | 
			
		||||
		data, _ := base64.StdEncoding.DecodeString(strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(s), "\n", ""), "\r", ""))
 | 
			
		||||
		return data
 | 
			
		||||
	}
 | 
			
		||||
	rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name)
 | 
			
		||||
 | 
			
		||||
	pkgs := map[string][]byte{
 | 
			
		||||
		"any": unPack(`
 | 
			
		||||
KLUv/QBYXRMABmOHSbCWag6dY6d8VNtVR3rpBnWdBbkDAxM38Dj3XG3FK01TCKlWtMV9QpskYdsm
 | 
			
		||||
e6fh5gWqM8edeurYNESoIUz/RmtyQy68HVrBj1p+AIoAYABFSJh4jcDyWNQgHIKIuNgIll64S4oY
 | 
			
		||||
FFIUk6vJQBMIIl2iYtIysqKWVYMCYvXDpAKTMzVGwZTUWhbciFCglIMH1QMbEtjHpohSi8XRYwPr
 | 
			
		||||
AwACSy/fzxO1FobizlP7sFgHcpx90Pus94Edjcc9GOustbD3PBprLUxH50IGC1sfw31c7LOfT4Qe
 | 
			
		||||
nh0KP1uKywwdPrRYmuyIkWBHRlcLfeBIDpKKqw44N0K2nNAfFW5grHRfSShyVgaEIZwIVVmFGL7O
 | 
			
		||||
88XDE5whJm4NkwA91dRoPBCcrgqozKSyah1QygsWkCshAaYrvbHCFdUTJCOgBpeUTMuJJ6+SRtcj
 | 
			
		||||
wIRua8mGJyg7qWoqJQq9z/4+DU1rHrEO8f6QZ3HUu3IM7GY37u+jeWjUu45637yN+qj338cdi0Uc
 | 
			
		||||
y0a9a+e5//1cYnPUu37dxr15khzNQ9/PE80aC/1okjz9mGo3bqP5Ue+scflGshdzx2g28061k2PW
 | 
			
		||||
uKwzjmV/XzTzzmKdcfz3eRbJoRPddcaP/n4PSZqQeYa1PDtPQzOHJK0amfjvz0IUV/v38xHJK/rz
 | 
			
		||||
JtFpalPD30drDWi7Bl8NB3J/P3csijQyldWZ8gy3TNslLsozMw74DhoAXoAfnE8xydUUHPZ3hML4
 | 
			
		||||
2zVDGiEXSGYRx4BKQDcDJA5S9Ca25FRgPtSWSowZJpJTYAR9WCPHUDgACm6+hBecGDPNClpwHZ2A
 | 
			
		||||
EQ==
 | 
			
		||||
`),
 | 
			
		||||
		"x86_64": unPack(`
 | 
			
		||||
KLUv/QBYnRMAFmOJS7BUbg7Un8q21hxCopsOMn6UGTzJRbHI753uOeMdxZ+V7ajoETVxl9CSBCR5
 | 
			
		||||
2a3K1vr1gwyp9gCTH422bRNxHEg7Z0z9HV4rH/DGFn8AjABjAFQ2oaUVMRRGViVoqmxAVKuoKQVM
 | 
			
		||||
NJRwTDl9NcHCClliWjTpWin6sRUZsXSipWlAipQnleThRgFF5QTAzpth0UPFkhQeJRnYOaqSScEC
 | 
			
		||||
djCPDwE8pQTfVXW9F7bmznX3YTNZDeP7IHgxDazNQhp+UDa798KeRgvvvbCamgsYdL461TfvcmlY
 | 
			
		||||
djFowWYH5yaH5ztZcemh4omAkm7iQIWvGypNIXJQNgc7DVuHjx06I4MZGTIkeEBIOIL0OxcvnGps
 | 
			
		||||
0TwxycqKYESrwwQYEDKI2F0hNXH1/PCQ2BS4Ykki48EAaflAbRHxYrRQbdAZ4oXVAMGCkYOXkBRb
 | 
			
		||||
NkwjNCoIF07ByTlyfJhmoHQtCbFYDN+941783KqzusznmPePXJPluS1+cL/74Rd/1UHluW15blFv
 | 
			
		||||
ol6e+8XPPZNDPN/Kc9vOdX/xNZrT8twWnH34U9Xkqw76rqqrPjPQl6nJde9i74e/8Mtz6zOjT3R7
 | 
			
		||||
Uve8BrabpT4zanE83158MtVbkxbH84vPNWkGqeu2OF704vfRzAGl6mhRtXPdmOrRzFla+BO+DL34
 | 
			
		||||
uHHN9r74usjkduX5VEhNz9TnxV9trSabvYAwuIZffN0zSeZM3c3GUHX8dG6jeUgHGgBbgB9cUDHJ
 | 
			
		||||
1RR09teBwvjbNUMaIRdIZhHHgEpANwMkDpL0JsbkVFA+0JZKjBkmklNgBH1YI8dQOAAKbr6EF5wY
 | 
			
		||||
M80KWnAdnYAR
 | 
			
		||||
`),
 | 
			
		||||
		"aarch64": unPack(`
 | 
			
		||||
KLUv/QBYdRQAVuSMS7BUbg7Un8q21hxCopsOMn6UGTzJRbHI753uOeMdxZ+V7ajoEbUkUXbXhXW/
 | 
			
		||||
7FanWzv7B/EcMxhodFqyZkUcB9LOGVN/h9MqG7zFFmoAaQB8AEFrvpXntn3V/cXXaE7Lc9uP5uFP
 | 
			
		||||
VXPl+ue7qnJ9Zp8vU3PVvYu9HvbAL8+tz4y+0O1J3TPXqbZ5l3+lapk5ee+L577qXvdf+Atn+P69
 | 
			
		||||
4Qz8QhpYw4/xd78Q3/v6Wg28974u1Ojc2ODseAGpHs2crYG4kef84uNGnu198fWQuVq+8ymQmp5p
 | 
			
		||||
z4vPbRjOaBC+FxziF1/3TJI5U3ezMlQdPZ3baA7SMhnMunvHvfg5rrO6zOeY94+rJstzW/zgetfD
 | 
			
		||||
Lz7XP+W5bXluUW+hXp77xc89kwFRTF1PrKxAFpgXT7ZWhjzYjpRIStGyNCAGBYM6AnGrkKKCAmAH
 | 
			
		||||
k3HBI8VyBBYdGdApmoqJYQE62EeIADCkBF1VOW0WYnz/+y6ufTMaDQ2GDDme7Wapz4xa3JpvLz6Z
 | 
			
		||||
6q1Ji1vzi79q0vxR+ba4dejF76OZ80nV0aJqX3VjKCsuP1g0EWDSURyw0JVDZWlEzsnmYLdh8wDS
 | 
			
		||||
I2dkIEMjxsSOiAlJjH4HIwbTjayZJidXVxKQYH2gICOCBhK7KqMlLZ4gMCU1BapYlsTAXnywepyy
 | 
			
		||||
jMBmtEhxyCnCZdUAwYKxAxeRFVk4TCL0aYgWjt3kHTg9SjVStppI2YCSWshUEFGdmJmyCVGpnqIU
 | 
			
		||||
KNlA0hEjIOACGSLqYpXAD5SSNVT2MJRJwREAF4FRHPBlCJMSNwFguGAWDJBg+KIArkIJGNtCydUL
 | 
			
		||||
TuN1oBh/+zKkEblAsgjGqVgUwKLP+UOMOGCpAhICtg6ncFJH`),
 | 
			
		||||
		"other": unPack(`
 | 
			
		||||
KLUv/QBYbRMABuOHS9BSNQdQ56F+xNFoV3CijY54JYt3VqV1iUU3xmj00y2pyBOCuokbhDYpvNsj
 | 
			
		||||
ZJeCxqH+nQFpMf4Wa92okaZoF4eH6HsXXCBo+qy3Fn4AigBgAEaYrLCQEuAom6YbHyuKZAFYksqi
 | 
			
		||||
sSOFiRs0WDmlACk0CnpnaAeKiCS3BlwVkViJEbDS43lFNbLkZEmGhc305Nn4AMLGiUkBDiMTG5Vz
 | 
			
		||||
q4ZISjCofEfR1NpXijvP2X95Hu1e+zLalc0+mjeT3Z/FPGvt62WymbX2dXMDIYKDLjjP8n03RrPf
 | 
			
		||||
A1vOApwGOh2MgE2LpgZrgXLDF2CUJ15idG2J8GCSgcc2ZVRgA8+RHD0k2VJjg6mRUgGGhBWEyEcz
 | 
			
		||||
5EePLhUeWlYhoFCKONxUiBiIUiQeDIqiQwkjLiyqnF5eGs6a2gGRapbU9JRyuXAlPemYajlJojJd
 | 
			
		||||
GBBJjo5GxFRkITOAvLhSCr2TDz4uzdU8Yh3i/SHP4qh3vTG2s9198NP8M+pdR73BvIP6qPeDjzsW
 | 
			
		||||
gTi+jXrXWOe5P/jZxOeod/287v6JljzNP99RNM0a+/x4ljz3LNV2t5v9qHfW2Pyg24u54zSfObWX
 | 
			
		||||
Y9bYrCTHtwdfPPPOYiU5fvB5FssfNN2V5EIPfg9LnM+JhtVEO8+FZw5LXA068YNPhimu9sHPQiWv
 | 
			
		||||
qc6fE9BTnxIe/LTKatab+WYu7T74uWNRxJW5W5Ux0bDLuG1ioCwjg4DvGgBcgB8cUDHJ1RQ89neE
 | 
			
		||||
wvjbNUMiIZdo5hbHgEpANwMkDnL0Jr7kVFg+0pZKjBkmklNgBH1YI8dQOAAKbr6EF5wYM80KWnAd
 | 
			
		||||
nYAR`),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("RepositoryKey", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
		req := NewRequest(t, "GET", rootURL+"/repository.key")
 | 
			
		||||
		resp := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		require.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
 | 
			
		||||
		require.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Upload", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
		req := NewRequestWithBody(t, "PUT", rootURL+"/default", bytes.NewReader(pkgs["any"]))
 | 
			
		||||
		MakeRequest(t, req, http.StatusUnauthorized)
 | 
			
		||||
 | 
			
		||||
		req = NewRequestWithBody(t, "PUT", rootURL+"/default", bytes.NewReader(pkgs["any"])).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Len(t, pvs, 1)
 | 
			
		||||
 | 
			
		||||
		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Nil(t, pd.SemVer)
 | 
			
		||||
		require.IsType(t, &arch_model.VersionMetadata{}, pd.Metadata)
 | 
			
		||||
		require.Equal(t, "test", pd.Package.Name)
 | 
			
		||||
		require.Equal(t, "1.0.0-1", pd.Version.Version)
 | 
			
		||||
 | 
			
		||||
		pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Len(t, pfs, 2) // zst and zst.sig
 | 
			
		||||
		require.True(t, pfs[0].IsLead)
 | 
			
		||||
 | 
			
		||||
		pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, int64(len(pkgs["any"])), pb.Size)
 | 
			
		||||
 | 
			
		||||
		req = NewRequestWithBody(t, "PUT", rootURL+"/default", bytes.NewReader(pkgs["any"])).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusConflict)
 | 
			
		||||
		req = NewRequestWithBody(t, "PUT", rootURL+"/default", bytes.NewReader(pkgs["x86_64"])).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
		req = NewRequestWithBody(t, "PUT", rootURL+"/other", bytes.NewReader(pkgs["any"])).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
		req = NewRequestWithBody(t, "PUT", rootURL+"/other", bytes.NewReader(pkgs["aarch64"])).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
		req = NewRequestWithBody(t, "PUT", rootURL+"/base", bytes.NewReader(pkgs["other"])).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
		req = NewRequestWithBody(t, "PUT", rootURL+"/base", bytes.NewReader(pkgs["x86_64"])).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
		req = NewRequestWithBody(t, "PUT", rootURL+"/base", bytes.NewReader(pkgs["aarch64"])).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Download", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		req := NewRequest(t, "GET", rootURL+"/default/x86_64/test-1.0.0-1-x86_64.pkg.tar.zst")
 | 
			
		||||
		resp := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
		require.Equal(t, pkgs["x86_64"], resp.Body.Bytes())
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/default/x86_64/test-1.0.0-1-any.pkg.tar.zst")
 | 
			
		||||
		resp = MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
		require.Equal(t, pkgs["any"], resp.Body.Bytes())
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/default/x86_64/test-1.0.0-1-aarch64.pkg.tar.zst")
 | 
			
		||||
		MakeRequest(t, req, http.StatusNotFound)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/other/x86_64/test-1.0.0-1-x86_64.pkg.tar.zst")
 | 
			
		||||
		MakeRequest(t, req, http.StatusNotFound)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/other/x86_64/test-1.0.0-1-any.pkg.tar.zst")
 | 
			
		||||
		resp = MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
		require.Equal(t, pkgs["any"], resp.Body.Bytes())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("SignVerify", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		req := NewRequest(t, "GET", rootURL+"/repository.key")
 | 
			
		||||
		respPub := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/other/x86_64/test-1.0.0-1-any.pkg.tar.zst")
 | 
			
		||||
		respPkg := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/other/x86_64/test-1.0.0-1-any.pkg.tar.zst.sig")
 | 
			
		||||
		respSig := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		if err := gpgVerify(respPub.Body.Bytes(), respSig.Body.Bytes(), respPkg.Body.Bytes()); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Repository", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		req := NewRequest(t, "GET", rootURL+"/repository.key")
 | 
			
		||||
		respPub := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/base/x86_64/base.db")
 | 
			
		||||
		respPkg := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/base/x86_64/base.db.sig")
 | 
			
		||||
		respSig := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		if err := gpgVerify(respPub.Body.Bytes(), respSig.Body.Bytes(), respPkg.Body.Bytes()); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		files, err := listGzipFiles(respPkg.Body.Bytes())
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Len(t, files, 2)
 | 
			
		||||
		for s, d := range files {
 | 
			
		||||
			name := getProperty(string(d.Data), "NAME")
 | 
			
		||||
			ver := getProperty(string(d.Data), "VERSION")
 | 
			
		||||
			require.Equal(t, name+"-"+ver+"/desc", s)
 | 
			
		||||
			fn := getProperty(string(d.Data), "FILENAME")
 | 
			
		||||
			pgp := getProperty(string(d.Data), "PGPSIG")
 | 
			
		||||
			req = NewRequest(t, "GET", rootURL+"/base/x86_64/"+fn+".sig")
 | 
			
		||||
			respSig := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
			decodeString, err := base64.StdEncoding.DecodeString(pgp)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
			require.Equal(t, respSig.Body.Bytes(), decodeString)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Delete", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		req := NewRequestWithBody(t, "DELETE", rootURL+"/base/notfound/1.0.0-1", nil).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusNotFound)
 | 
			
		||||
 | 
			
		||||
		req = NewRequestWithBody(t, "DELETE", rootURL+"/base/test/1.0.0-1", nil).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusNoContent)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/base/x86_64/base.db")
 | 
			
		||||
		respPkg := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
		files, err := listGzipFiles(respPkg.Body.Bytes())
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Len(t, files, 1)
 | 
			
		||||
 | 
			
		||||
		req = NewRequestWithBody(t, "DELETE", rootURL+"/base/test2/1.0.0-1", nil).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
		MakeRequest(t, req, http.StatusNoContent)
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/base/x86_64/base.db")
 | 
			
		||||
		MakeRequest(t, req, http.StatusNotFound)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", rootURL+"/default/x86_64/base.db")
 | 
			
		||||
		respPkg = MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
		files, err = listGzipFiles(respPkg.Body.Bytes())
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Len(t, files, 1)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getProperty(data, key string) string {
 | 
			
		||||
	r := bufio.NewReader(strings.NewReader(data))
 | 
			
		||||
	for {
 | 
			
		||||
		line, _, err := r.ReadLine()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return ""
 | 
			
		||||
		}
 | 
			
		||||
		if strings.Contains(string(line), "%"+key+"%") {
 | 
			
		||||
			readLine, _, _ := r.ReadLine()
 | 
			
		||||
			return string(readLine)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func listGzipFiles(data []byte) (fstest.MapFS, error) {
 | 
			
		||||
	reader, err := gzip.NewReader(bytes.NewBuffer(data))
 | 
			
		||||
	defer reader.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	tarRead := tar.NewReader(reader)
 | 
			
		||||
	files := make(fstest.MapFS)
 | 
			
		||||
	for {
 | 
			
		||||
		cur, err := tarRead.Next()
 | 
			
		||||
		if err == io.EOF {
 | 
			
		||||
			break
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if cur.Typeflag != tar.TypeReg {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		data, err := io.ReadAll(tarRead)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		files[cur.Name] = &fstest.MapFile{Data: data}
 | 
			
		||||
	}
 | 
			
		||||
	return files, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func gpgVerify(pub, sig, data []byte) error {
 | 
			
		||||
	sigPack, err := packet.Read(bytes.NewBuffer(sig))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	signature, ok := sigPack.(*packet.Signature)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("invalid sign key")
 | 
			
		||||
	}
 | 
			
		||||
	pubBlock, err := armor.Decode(bytes.NewReader(pub))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	pack, err := packet.Read(pubBlock.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	publicKey, ok := pack.(*packet.PublicKey)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("invalid public key")
 | 
			
		||||
	}
 | 
			
		||||
	hash := signature.Hash.New()
 | 
			
		||||
	_, err = hash.Write(data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return publicKey.VerifySignature(hash, signature)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								web_src/svg/gitea-arch.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web_src/svg/gitea-arch.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#1793d1" d="M256 72c-14 35-23 57-39 91 10 11 22 23 41 36-21-8-35-17-45-26-21 43-53 103-117 220 50-30 90-48 127-55-2-7-3-14-3-22v-1c1-33 18-58 38-56 20 1 36 29 35 62l-2 17c36 7 75 26 125 54l-27-50c-13-10-27-23-55-38 19 5 33 11 44 17-86-159-93-180-122-250z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 337 B  | 
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue