d1nch8g 1 week ago
parent
commit
ea2644ffe7
No account linked to committer's email address

+ 65
- 0
docs/content/usage/packages/arch.en-us.md View File

@@ -0,0 +1,65 @@
---
date: "2016-11-08T16:00:00+02:00"
title: "Arch Package Registry"
weight: 10
toc: true
draft: false
menu:
sidebar:
parent: "packages"
name: "Arch"
weight: 10
identifier: "arch"
---

# Arch package registry

Gitea has a Arch Linux package registry, which can act as a fully working [Arch linux mirror](https://wiki.archlinux.org/title/mirrors) and connected directly in `/etc/pacman.conf`. Gitea automatically creates pacman database for packages in user/organization space when a new Arch package is uploaded.

**Table of Contents**

{{< toc >}}

## Install packages

First, you need to update your pacman configuration, adding following lines:

```conf
[{owner}.{domain}]
SigLevel = Optional TrustAll
Server = https://{domain}/api/packages/{owner}/arch/{distribution}/{architecture}
```

Then, you can run pacman sync command (with -y flag to load connected database file), to install your package:

```sh
pacman -Sy package
```

## Upload packages

When uploading the package to gitea, you have to prepare package file with the `.pkg.tar.zst` extension and its `.pkg.tar.zst.sig` signature. You can use [curl](https://curl.se/) or any other HTTP client, Gitea supports multiple [authentication schemes](https://docs.gitea.com/usage/authentication). The upload command will create 3 files: package, signature and desc file for the pacman database (which will be created automatically on request).

The following command will upload arch package and related signature to gitea with basic authentification:

```sh
curl -X PUT \
https://{domain}/api/packages/{owner}/arch/push/{package-1-1-x86_64.pkg.tar.zst}/{archlinux}/$(xxd -p package-1-1-x86_64.pkg.tar.zst.sig | tr -d '\n') \
--user your_username:your_token_or_password \
--header "Content-Type: application/octet-stream" \
--data-binary '@/path/to/package/file/package-1-1-x86_64.pkg.tar.zst'
```

## Delete packages

The `DELETE` method will remove specific package version, and all package files related to that version:

```sh
curl -X DELETE \
https://{domain}/api/packages/{user}/arch/remove/{package}/{version} \
--user your_username:your_token_or_password
```

## Clients

Any `pacman` compatible package manager or AUR-helper can be used to install packages from gitea ([yay](https://github.com/Jguer/yay), [paru](https://github.com/Morganamilo/paru), [pikaur](https://github.com/actionless/pikaur), [aura](https://github.com/fosskers/aura)). Alternatively, you can try [pack](https://fmnx.su/core/pack) which supports full gitea API (install/push/remove). Also, any HTTP client can be used to execute get/push/remove operations ([curl](https://curl.se/), [postman](https://www.postman.com/), [thunder-client](https://www.thunderclient.com/)).

+ 1
- 0
docs/content/usage/packages/overview.en-us.md View File

@@ -24,6 +24,7 @@ The following package managers are currently supported:
| Name | Language | Package client |
| ---- | -------- | -------------- |
| [Alpine](usage/packages/alpine.md) | - | `apk` |
| [Arch](usage/packages/arch.md) | - | `pacman` |
| [Cargo](usage/packages/cargo.md) | Rust | `cargo` |
| [Chef](usage/packages/chef.md) | - | `knife` |
| [Composer](usage/packages/composer.md) | PHP | `composer` |

+ 3
- 0
models/packages/descriptor.go View File

@@ -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:

+ 6
- 0
models/packages/package.go View File

@@ -31,6 +31,7 @@ type Type string
// List of supported packages
const (
TypeAlpine Type = "alpine"
TypeArch Type = "arch"
TypeCargo Type = "cargo"
TypeChef Type = "chef"
TypeComposer Type = "composer"
@@ -55,6 +56,7 @@ const (

var TypeList = []Type{
TypeAlpine,
TypeArch,
TypeCargo,
TypeChef,
TypeComposer,
@@ -82,6 +84,8 @@ func (pt Type) Name() string {
switch pt {
case TypeAlpine:
return "Alpine"
case TypeArch:
return "Arch"
case TypeCargo:
return "Cargo"
case TypeChef:
@@ -131,6 +135,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:

+ 302
- 0
modules/packages/arch/metadata.go View File

@@ -0,0 +1,302 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package arch

import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"

"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"

"github.com/mholt/archiver/v3"
)

const (
PropertyDescription = "arch.description"
PropertySignature = "arch.signature"
)

var (
// https://man.archlinux.org/man/PKGBUILD.5
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"`
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"`
Backup []string `json:"backup,omitempty"`
}

// Metadata related to specific pakcage 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"`
}

// Function that receives arch package archive data and returns it's metadata.
func ParsePackage(r io.Reader, md5, sha256 []byte, size int64) (*Package, error) {
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 = size
pkg.FileMetadata.SHA256 = hex.EncodeToString(sha256)
pkg.FileMetadata.MD5 = hex.EncodeToString(md5)

return pkg, nil
}

// 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
}

i := strings.IndexRune(line, '=')
if i == -1 {
continue
}

key := strings.TrimSpace(line[:i])
value := strings.TrimSpace(line[i+1:])

switch key {
case "pkgname":
p.Name = value
case "pkgbase":
p.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 "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
}
}

return p, errors.Join(scanner.Err(), ValidatePackageSpec(p))
}

// 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 _, 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
}

// Create pacman package description file.
func (p *Package) Desc() string {
entries := [40]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,
"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,
"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 < 40; i += 2 {
if entries[i+1] != "" {
fmt.Fprintf(&buf, "%%%s%%\n%s\n\n", entries[i], entries[i+1])
}
}
return buf.String()
}

// Create pacman database archive based on provided package metadata structs.
func CreatePacmanDb(entries map[string][]byte) (*bytes.Buffer, error) {
var b bytes.Buffer

gw := gzip.NewWriter(&b)
tw := tar.NewWriter(gw)

for name, content := range entries {
header := &tar.Header{
Name: name,
Size: int64(len(content)),
Mode: int64(os.ModePerm),
}

if err := tw.WriteHeader(header); err != nil {
return nil, errors.Join(err, tw.Close(), gw.Close())
}

if _, err := tw.Write(content); err != nil {
return nil, errors.Join(err, tw.Close(), gw.Close())
}
}

return &b, errors.Join(tw.Close(), gw.Close())
}

+ 452
- 0
modules/packages/arch/metadata_test.go View File

@@ -0,0 +1,452 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package arch

import (
"bytes"
"encoding/base64"
"errors"
"io"
"os"
"strings"
"testing"
"testing/fstest"
"time"

"github.com/mholt/archiver/v3"
"github.com/stretchr/testify/assert"
)

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")
assert.NoError(t, err)

pfile, err := fs.Open("pkginfo")
assert.NoError(t, err)

parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO")
assert.NoError(t, err)

// Test .MTREE file
minf, err := fs.Stat("mtree")
assert.NoError(t, err)

mfile, err := fs.Open("mtree")
assert.NoError(t, err)

marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE")
assert.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,
})
assert.NoError(t, errors.Join(pfile.Close(), err))

err = archive.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: minf,
CustomName: marcname,
},
ReadCloser: mfile,
})
assert.NoError(t, errors.Join(mfile.Close(), archive.Close(), err))

_, err = ParsePackage(&buf, []byte{}, []byte{}, 0)

assert.NoError(t, err)
})

t.Run("missing .PKGINFO", func(t *testing.T) {
var buf bytes.Buffer

archive := archiver.NewTarZstd()
archive.Create(&buf)

assert.NoError(t, archive.Close())

_, err = ParsePackage(&buf, []byte{}, []byte{}, 0)

assert.Error(t, err)
assert.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")
assert.NoError(t, err)

archive := archiver.NewTarZstd()
archive.Create(&buf)

err = archive.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: pinf,
CustomName: parcname,
},
ReadCloser: pfile,
})
assert.NoError(t, errors.Join(pfile.Close(), archive.Close(), err))

_, err = ParsePackage(&buf, []byte{}, []byte{}, 0)

assert.Error(t, err)
assert.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))
assert.NoError(t, err)
assert.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,
MD5: "abc",
SHA256: "def",
BuildDate: 3,
Packager: "smon",
Arch: "x86_64",
},
}
}

t.Run("valid package", func(t *testing.T) {
p := newpkg()

err := ValidatePackageSpec(&p)

assert.NoError(t, err)
})

t.Run("invalid package name", func(t *testing.T) {
p := newpkg()
p.Name = "!$%@^!*&()"

err := ValidatePackageSpec(&p)

assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid package name")
})

t.Run("invalid package base", func(t *testing.T) {
p := newpkg()
p.VersionMetadata.Base = "!$%@^!*&()"

err := ValidatePackageSpec(&p)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid package version")
})

t.Run("missing architecture", func(t *testing.T) {
p := newpkg()
p.FileMetadata.Arch = ""

err := ValidatePackageSpec(&p)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.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)

assert.Error(t, err)
assert.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",
},
}
assert.Equal(t, pkgdesc, md.Desc())
}

func TestCreatePacmanDb(t *testing.T) {
const dbarchive = "H4sIAAAAAAAA/0rLzEnVS60oYaAhMDAwMDA3NwfTBgYG6LSBgYEpEtuAwcDQwMzUgEHBgJaOgoHS4pLEIgYDiu1C99wQASmlubmVA+2IUTAKRsEoGAV0B4AAAAD//2VF3KIACAAA"

db, err := CreatePacmanDb(map[string][]byte{
"file.ext": []byte("dummy"),
})
assert.NoError(t, err)

actual, err := io.ReadAll(db)
assert.NoError(t, err)

expected, err := base64.RawStdEncoding.DecodeString(dbarchive)
assert.NoError(t, err)

assert.Equal(t, expected, actual)
}

+ 2
- 0
modules/setting/packages.go View File

@@ -24,6 +24,7 @@ var (
LimitTotalOwnerCount int64
LimitTotalOwnerSize int64
LimitSizeAlpine int64
LimitSizeArch int64
LimitSizeCargo int64
LimitSizeChef int64
LimitSizeComposer int64
@@ -82,6 +83,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")

+ 10
- 0
options/locale/locale_en-US.ini View File

@@ -3448,6 +3448,16 @@ alpine.repository = Repository Info
alpine.repository.branches = Branches
alpine.repository.repositories = Repositories
alpine.repository.architectures = Architectures
arch.pacmanconf = Add server with related distribution and architecture to <code>/etc/pacman.conf</code>:
arch.pacmansync = Sync package with pacman:
arch.documentation = For more information on the arch mirrors, see %sthe documentation%s.
arch.properties = Package properties
arch.description = Description
arch.provides = Provides
arch.depends = Depends
arch.optdepends = Optional depends
arch.makedepends = Make depends
arch.checkdepends = Check depends
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
- 0
public/assets/img/svg/gitea-arch.svg View 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>

+ 7
- 0
routers/api/packages/api.go View File

@@ -14,6 +14,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"
@@ -121,6 +122,12 @@ func CommonRoutes() *web.Route {
})
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/arch", func() {
r.Put("/push/{filename}/{distro}", reqPackageAccess(perm.AccessModeWrite), arch.Push)
r.Put("/push/{filename}/{distro}/{sign}", reqPackageAccess(perm.AccessModeWrite), arch.Push)
r.Delete("/remove/{package}/{version}", reqPackageAccess(perm.AccessModeWrite), arch.Remove)
r.Get("/{distro}/{arch}/{file}", arch.Get)
})
r.Group("/cargo", func() {
r.Group("/api/v1/crates", func() {
r.Get("", cargo.SearchPackages)

+ 135
- 0
routers/api/packages/arch/arch.go View File

@@ -0,0 +1,135 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package arch

import (
"bytes"
"net/http"
"strings"

packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/packages/helper"
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)
})
}

// Push new package to arch package registry.
func Push(ctx *context.Context) {
var (
filename = ctx.Params("filename")
distro = ctx.Params("distro")
sign = ctx.Params("sign")
)

upload, close, err := ctx.UploadStream()
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if close {
defer upload.Close()
}

_, _, err = arch_service.UploadArchPackage(ctx, upload, filename, distro, sign)
if err != nil {
switch err {
case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusConflict, err)
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}

ctx.Status(http.StatusOK)
}

// Get file from arch package registry.
func Get(ctx *context.Context) {
var (
file = ctx.Params("file")
owner = ctx.Params("username")
distro = ctx.Params("distro")
arch = ctx.Params("arch")
)

if strings.HasSuffix(file, ".pkg.tar.zst") {
pkg, err := arch_service.GetPackageFile(ctx, distro, file)
if err != nil {
apiError(ctx, http.StatusNotFound, err)
return
}

ctx.ServeContent(pkg, &context.ServeHeaderOptions{
Filename: file,
})
return
}

if strings.HasSuffix(file, ".pkg.tar.zst.sig") {
sig, err := arch_service.GetPackageSignature(ctx, distro, file)
if err != nil {
apiError(ctx, http.StatusNotFound, err)
return
}

ctx.ServeContent(sig, &context.ServeHeaderOptions{
Filename: file,
})
return
}

if strings.HasSuffix(file, ".db.tar.gz") || strings.HasSuffix(file, ".db") {
db, err := arch_service.CreatePacmanDb(ctx, owner, arch, distro)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}

ctx.ServeContent(bytes.NewReader(db.Bytes()), &context.ServeHeaderOptions{
Filename: file,
})
return
}

ctx.Status(http.StatusNotFound)
}

// Remove specific package version, related files with properties.
func Remove(ctx *context.Context) {
var (
pkg = ctx.Params("package")
ver = ctx.Params("version")
)

version, err := packages_model.GetVersionByNameAndVersion(
ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg, ver,
)
if err != nil {
switch err {
case packages_model.ErrPackageNotExist:
apiError(ctx, http.StatusNotFound, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}

err = packages_service.RemovePackageVersion(ctx, ctx.Package.Owner, version)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}

ctx.Status(http.StatusOK)
}

+ 1
- 1
routers/web/user/package.go View File

@@ -177,7 +177,7 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.Data["PackageDescriptor"] = pd

switch pd.Package.Type {
case packages_model.TypeContainer:
case packages_model.TypeContainer, packages_model.TypeArch:
ctx.Data["RegistryHost"] = setting.Packages.RegistryHost
case packages_model.TypeAlpine:
branches := make(container.Set[string])

+ 133
- 0
services/packages/arch/service.go View File

@@ -0,0 +1,133 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package arch

import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"io"
"sort"
"strings"

packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
arch_module "code.gitea.io/gitea/modules/packages/arch"
packages_service "code.gitea.io/gitea/services/packages"
)

// Get data related to provided filename and distribution, for package files
// update download counter.
func GetPackageFile(ctx *context.Context, distro, file string) (io.ReadSeekCloser, error) {
pf, err := getPackageFile(ctx, distro, file)
if err != nil {
return nil, err
}

filestream, _, _, err := packages_service.GetPackageFileStream(ctx, pf)
return filestream, err
}

// This function will search for package signature and if present, will load it
// from package file properties, and return its byte reader.
func GetPackageSignature(ctx *context.Context, distro, file string) (*bytes.Reader, error) {
pf, err := getPackageFile(ctx, distro, strings.TrimSuffix(file, ".sig"))
if err != nil {
return nil, err
}

proprs, err := packages_model.GetProperties(ctx, packages_model.PropertyTypeFile, pf.ID)
if err != nil {
return nil, err
}

for _, pp := range proprs {
if pp.Name == arch_module.PropertySignature {
b, err := hex.DecodeString(pp.Value)
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}
}

return nil, errors.New("signature for requested package not found")
}

// Ejects parameters required to get package file property from file name.
func getPackageFile(ctx *context.Context, distro, file string) (*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, ctx.Package.Owner.ID, 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
}

// Finds all arch packages in user/organization scope, each package version
// starting from latest in descending order is checked to be compatible with
// requested combination of architecture and distribution. When/If the first
// compatible version is found, related desc file will be loaded from package
// properties and added to resulting .db.tar.gz archive.
func CreatePacmanDb(ctx *context.Context, owner, arch, distro string) (*bytes.Buffer, error) {
pkgs, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeArch)
if err != nil {
return nil, err
}

entries := make(map[string][]byte)

for _, pkg := range pkgs {
versions, err := packages_model.GetVersionsByPackageName(
ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg.Name,
)
if err != nil {
return nil, 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 {
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, err
}

if len(pps) >= 1 {
entries[pkg.Name+"-"+ver.Version+"/desc"] = []byte(pps[0].Value)
break
}
}
}

return arch_module.CreatePacmanDb(entries)
}

+ 83
- 0
services/packages/arch/upload.go View File

@@ -0,0 +1,83 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package arch

import (
"encoding/hex"
"errors"
"io"

packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
arch_module "code.gitea.io/gitea/modules/packages/arch"
packages_service "code.gitea.io/gitea/services/packages"
)

// UploadArchPackage adds an Arch Package to the registry.
// The first return value indictaes if the error is a user error.
func UploadArchPackage(ctx *context.Context, upload io.Reader, filename, distro, sign string) (bool, *packages_model.PackageVersion, error) {
buf, err := packages_module.CreateHashedBufferFromReader(upload)
if err != nil {
return false, nil, err
}
defer buf.Close()

md5, _, sha256, _ := buf.Sums()

p, err := arch_module.ParsePackage(buf, md5, sha256, buf.Size())
if err != nil {
return false, nil, err
}

_, err = buf.Seek(0, io.SeekStart)
if err != nil {
return false, nil, err
}

properties := map[string]string{
arch_module.PropertyDescription: p.Desc(),
}
if sign != "" {
_, err := hex.DecodeString(sign)
if err != nil {
return true, nil, errors.New("unable to decode package signature")
}
properties[arch_module.PropertySignature] = sign
}

ver, _, 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: filename,
CompositeKey: distro,
},
OverwriteExisting: true,
IsLead: true,
Creator: ctx.Doer,
Data: buf,
Properties: properties,
},
)
if err != nil {
switch err {
case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile, packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
return true, nil, err
default:
return false, nil, err
}
}

return false, ver, nil
}

+ 2
- 0
services/packages/packages.go View File

@@ -355,6 +355,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:

+ 70
- 0
templates/package/content/arch.tmpl View File

@@ -0,0 +1,70 @@
{{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-gear"}} {{ctx.Locale.Tr "packages.arch.pacmanconf" | Safe}}</label>
<div class="markup"><pre class="code-block"><code>[{{.PackageDescriptor.Owner.LowerName}}.{{.RegistryHost}}]
SigLevel = Optional TrustAll
Server = <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/arch/$distribution/$arch"></gitea-origin-url></code></pre></div>
</div>

<div class="field">
<label>{{svg "octicon-sync"}} {{ctx.Locale.Tr "packages.arch.pacmansync"}}</label>
<div class="markup"><pre class="code-block"><code>pacman -Sy {{.PackageDescriptor.Package.LowerName}}</code></pre></div>
</div>

<div class="field">
{{ctx.Locale.Tr "packages.arch.documentation" (printf `<a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/usage/packages/arch/">`) (printf `</a>`) | Safe}}
</div>
</div>
</div>

<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.arch.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.description"}}</h5></td>
<td>{{.PackageDescriptor.Metadata.Description}}</td>
</tr>

{{if .PackageDescriptor.Metadata.Provides}}
<tr>
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.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.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.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.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.checkdepends"}}</h5></td>
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.CheckDepends ", "}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}

+ 4
- 0
templates/package/metadata/arch.tmpl View 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}}

+ 2
- 0
templates/package/view.tmpl View File

@@ -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" .}}

+ 454
- 0
tests/integration/api_packages_arch_test.go View File

@@ -0,0 +1,454 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package integration

import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"os"
"testing"
"testing/fstest"
"time"

"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"
"code.gitea.io/gitea/modules/packages/arch"
"code.gitea.io/gitea/tests"

"github.com/mholt/archiver/v3"
"github.com/minio/sha256-simd"
"github.com/stretchr/testify/assert"
)

func TestPackageArch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

var (
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})

pushBatch = []*TestArchPackage{
BuildArchPackage(t, "git", "1-1", "x86_64"),
BuildArchPackage(t, "git", "2-1", "x86_64"),
BuildArchPackage(t, "git", "1-1", "i686"),
BuildArchPackage(t, "adwaita", "1-1", "any"),
BuildArchPackage(t, "adwaita", "2-1", "any"),
}

removeBatch = []*TestArchPackage{
BuildArchPackage(t, "curl", "1-1", "x86_64"),
BuildArchPackage(t, "curl", "2-1", "x86_64"),
BuildArchPackage(t, "dock", "1-1", "any"),
BuildArchPackage(t, "dock", "2-1", "any"),
}

firstDatabaseBatch = []*TestArchPackage{
BuildArchPackage(t, "pacman", "1-1", "x86_64"),
BuildArchPackage(t, "pacman", "1-1", "i686"),
BuildArchPackage(t, "htop", "1-1", "x86_64"),
BuildArchPackage(t, "htop", "1-1", "i686"),
BuildArchPackage(t, "dash", "1-1", "any"),
}

secondDatabaseBatch = []*TestArchPackage{
BuildArchPackage(t, "pacman", "2-1", "x86_64"),
BuildArchPackage(t, "htop", "2-1", "i686"),
BuildArchPackage(t, "dash", "2-1", "any"),
}

PacmanDBx86 = BuildPacmanDb(t,
secondDatabaseBatch[0].Pkg,
firstDatabaseBatch[2].Pkg,
secondDatabaseBatch[2].Pkg,
)

PacmanDBi686 = BuildPacmanDb(t,
firstDatabaseBatch[0].Pkg,
secondDatabaseBatch[1].Pkg,
secondDatabaseBatch[2].Pkg,
)

signdata = []byte{1, 2, 3, 4}
)

t.Run("PushWithSignature", func(t *testing.T) {
for _, p := range pushBatch {
t.Run(p.File, func(t *testing.T) {
defer tests.PrintCurrentTest(t)()

url := fmt.Sprintf(
"/api/packages/%s/arch/push/%s/archlinux/%s",
user.Name, p.File, hex.EncodeToString(signdata),
)

req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data))
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)

pv, err := packages.GetVersionByNameAndVersion(
db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver,
)
assert.NoError(t, err)

pf, err := packages.GetFileForVersionByName(
db.DefaultContext, pv.ID, p.File, "archlinux",
)
assert.NoError(t, err)
assert.NotNil(t, pf)

pps, err := packages.GetPropertiesByName(
db.DefaultContext, packages.PropertyTypeFile,
pf.ID, arch.PropertySignature,
)
assert.NoError(t, err)
assert.Len(t, pps, 1)
})
}
})

t.Run("PushWithoutSignature", func(t *testing.T) {
for _, p := range pushBatch {
t.Run(p.File, func(t *testing.T) {
defer tests.PrintCurrentTest(t)()

url := fmt.Sprintf(
"/api/packages/%s/arch/push/%s/parabola",
user.Name, p.File,
)

req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data))
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)

pv, err := packages.GetVersionByNameAndVersion(
db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver,
)
assert.NoError(t, err)

pf, err := packages.GetFileForVersionByName(
db.DefaultContext, pv.ID, p.File, "parabola",
)
assert.NoError(t, err)
assert.NotNil(t, pf)
})
}
})

t.Run("GetPackage", func(t *testing.T) {
for _, p := range pushBatch {
t.Run(p.File, func(t *testing.T) {
defer tests.PrintCurrentTest(t)()

url := fmt.Sprintf(
"/api/packages/%s/arch/push/%s/artix/%s",
user.Name, p.File, hex.EncodeToString(signdata),
)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data))
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)

url = fmt.Sprintf(
"/api/packages/%s/arch/artix/%s/%s",
user.Name, p.Arch, p.File,
)
req = NewRequest(t, "GET", url)
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, p.Data, resp.Body.Bytes())
})
}
})

t.Run("GetSignature", func(t *testing.T) {
for _, p := range pushBatch {
t.Run(p.File, func(t *testing.T) {
defer tests.PrintCurrentTest(t)()

url := fmt.Sprintf(
"/api/packages/%s/arch/push/%s/arco/%s",
user.Name, p.File, hex.EncodeToString(signdata),
)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data))
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)

url = fmt.Sprintf(
"/api/packages/%s/arch/arco/%s/%s.sig",
user.Name, p.Arch, p.File,
)
req = NewRequest(t, "GET", url)
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, signdata, resp.Body.Bytes())
})
}
})

t.Run("Remove", func(t *testing.T) {
for _, p := range removeBatch {
t.Run(p.File, func(t *testing.T) {
defer tests.PrintCurrentTest(t)()

url := fmt.Sprintf(
"/api/packages/%s/arch/push/%s/manjaro/%s",
user.Name, p.File, hex.EncodeToString(signdata),
)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data))
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)

url = fmt.Sprintf(
"/api/packages/%s/arch/remove/%s/%s",
user.Name, p.Name, p.Ver,
)
req = NewRequest(t, "DELETE", url)
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)

_, err := packages.GetVersionByNameAndVersion(
db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver,
)
assert.ErrorIs(t, err, packages.ErrPackageNotExist)
})
}
})

t.Run("PacmanDatabase", func(t *testing.T) {
prepareDatabasePackages := func(t *testing.T) {
for _, p := range firstDatabaseBatch {
url := fmt.Sprintf(
"/api/packages/%s/arch/push/%s/ion/%s",
user.Name, p.File, hex.EncodeToString(signdata),
)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data))
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)
}

// While creating pacman database, package versions are sorted by
// UnixTime, second delay is required to ensure that newer package
// version creation time differs from older packages.
time.Sleep(time.Second)

for _, p := range secondDatabaseBatch {
url := fmt.Sprintf(
"/api/packages/%s/arch/push/%s/ion/%s",
user.Name, p.File, hex.EncodeToString(signdata),
)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data))
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)
}
}

t.Run("x86_64", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()

prepareDatabasePackages(t)

url := fmt.Sprintf(
"/api/packages/%s/arch/ion/x86_64/user.db.tar.gz", user.Name,
)
req := NewRequest(t, "GET", url)
resp := MakeRequest(t, req, http.StatusOK)

CompareTarGzEntries(t, PacmanDBx86, resp.Body.Bytes())
})

t.Run("i686", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()

prepareDatabasePackages(t)

url := fmt.Sprintf(
"/api/packages/%s/arch/ion/i686/user.db", user.Name,
)
req := NewRequest(t, "GET", url)
resp := MakeRequest(t, req, http.StatusOK)

CompareTarGzEntries(t, PacmanDBi686, resp.Body.Bytes())
})
})
}

type TestArchPackage struct {
Pkg arch.Package
Data []byte
File string
Name string
Ver string
Arch string
}

func BuildArchPackage(t *testing.T, name, ver, architecture string) *TestArchPackage {
fs := fstest.MapFS{
"pkginfo": &fstest.MapFile{
Data: []byte(fmt.Sprintf(
"pkgname = %s\npkgbase = %s\npkgver = %s\narch = %s\n",
name, name, ver, architecture,
)),
Mode: os.ModePerm,
ModTime: time.Now(),
},
"mtree": &fstest.MapFile{
Data: []byte("test"),
Mode: os.ModePerm,
ModTime: time.Now(),
},
}

pinf, err := fs.Stat("pkginfo")
assert.NoError(t, err)

pfile, err := fs.Open("pkginfo")
assert.NoError(t, err)

parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO")
assert.NoError(t, err)

minf, err := fs.Stat("mtree")
assert.NoError(t, err)

mfile, err := fs.Open("mtree")
assert.NoError(t, err)

marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE")
assert.NoError(t, err)

var buf bytes.Buffer

archive := archiver.NewTarZstd()
archive.Create(&buf)

err = archive.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: pinf,
CustomName: parcname,
},
ReadCloser: pfile,
})
assert.NoError(t, errors.Join(pfile.Close(), err))

err = archive.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: minf,
CustomName: marcname,
},
ReadCloser: mfile,
})
assert.NoError(t, errors.Join(mfile.Close(), archive.Close(), err))

md5, sha256, size := archPkgParams(buf.Bytes())

return &TestArchPackage{
Data: buf.Bytes(),
Name: name,
Ver: ver,
Arch: architecture,
File: fmt.Sprintf("%s-%s-%s.pkg.tar.zst", name, ver, architecture),
Pkg: arch.Package{
Name: name,
Version: ver,
VersionMetadata: arch.VersionMetadata{
Base: name,
},
FileMetadata: arch.FileMetadata{
CompressedSize: size,
MD5: hex.EncodeToString(md5),
SHA256: hex.EncodeToString(sha256),
Arch: architecture,
},
},
}
}

func archPkgParams(b []byte) ([]byte, []byte, int64) {
md5 := md5.New()
sha256 := sha256.New()
c := counter{bytes.NewReader(b), 0}

br := bufio.NewReader(io.TeeReader(&c, io.MultiWriter(md5, sha256)))

io.ReadAll(br)
return md5.Sum(nil), sha256.Sum(nil), int64(c.n)
}

type counter struct {
io.Reader
n int
}

func (w *counter) Read(p []byte) (int, error) {
n, err := w.Reader.Read(p)
w.n += n
return n, err
}

func BuildPacmanDb(t *testing.T, pkgs ...arch.Package) []byte {
entries := map[string][]byte{}
for _, p := range pkgs {
entries[fmt.Sprintf("%s-%s/desc", p.Name, p.Version)] = []byte(p.Desc())
}
b, err := arch.CreatePacmanDb(entries)
if err != nil {
assert.NoError(t, err)
return nil
}
return b.Bytes()
}

func CompareTarGzEntries(t *testing.T, expected, actual []byte) {
fgz, err := gzip.NewReader(bytes.NewReader(expected))
if err != nil {
assert.NoError(t, err)
return
}
ftar := tar.NewReader(fgz)

validatemap := map[string]struct{}{}

for {
h, err := ftar.Next()
if err != nil {
break
}

validatemap[h.Name] = struct{}{}
}

sgz, err := gzip.NewReader(bytes.NewReader(actual))
if err != nil {
assert.NoError(t, err)
return
}
star := tar.NewReader(sgz)

for {
h, err := star.Next()
if err != nil {
break
}

_, ok := validatemap[h.Name]
if !ok {
assert.Fail(t, "Unexpected entry in archive: "+h.Name)
}
delete(validatemap, h.Name)
}

if len(validatemap) == 0 {
return
}

for e := range validatemap {
assert.Fail(t, "Entry not found in archive: "+e)
}
}

+ 1
- 0
web_src/svg/gitea-arch.svg View 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>

Loading…
Cancel
Save