diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index b8ef698d38..803b73c968 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -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: diff --git a/models/packages/package.go b/models/packages/package.go index 84e2fa7ee7..364cc2e7cc 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -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: diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go new file mode 100644 index 0000000000..fc66d288cc --- /dev/null +++ b/modules/packages/arch/metadata.go @@ -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() +} diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go new file mode 100644 index 0000000000..b084762fb6 --- /dev/null +++ b/modules/packages/arch/metadata_test.go @@ -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 +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 ", + 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 + +%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 ", + Arch: "x86_64", + }, + } + require.Equal(t, pkgdesc, md.Desc()) +} diff --git a/modules/setting/packages.go b/modules/setting/packages.go index 50263546ce..b3f50617d2 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -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") diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8737342448..d0a0dc0696 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 /etc/pacman.conf : +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 ~/.cargo/config.toml): cargo.install = To install the package using Cargo, run the following command: chef.registry = Setup this registry in your ~/.chef/config.rb file: diff --git a/public/assets/img/svg/gitea-arch.svg b/public/assets/img/svg/gitea-arch.svg new file mode 100644 index 0000000000..943a92c579 --- /dev/null +++ b/public/assets/img/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index f590947111..e13bd1e862 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -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) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go new file mode 100644 index 0000000000..a01b496d41 --- /dev/null +++ b/routers/api/packages/arch/arch.go @@ -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) + } +} diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 3ecc59a2ab..204efe6001 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -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]) diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go new file mode 100644 index 0000000000..acd002dcc8 --- /dev/null +++ b/services/packages/arch/repository.go @@ -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 +} diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go index 5aba730996..ab419a9a5a 100644 --- a/services/packages/cleanup/cleanup.go +++ b/services/packages/cleanup/cleanup.go @@ -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 diff --git a/services/packages/packages.go b/services/packages/packages.go index 8f688a74f4..a5b84506de 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -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: diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl new file mode 100644 index 0000000000..bcc24b585b --- /dev/null +++ b/templates/package/content/arch.tmpl @@ -0,0 +1,143 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} +

{{ctx.Locale.Tr "packages.installation"}}

+
+
+
+ +
+
wget -O sign.gpg 
+pacman-key --add sign.gpg
+pacman-key --lsign-key '{{$.SignMail}}'
+
+
+
+ +
+

+{{- 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 = 
+{{end -}}
+
+
+
+
+ +
+
pacman -Sy {{.PackageDescriptor.Package.LowerName}}
+
+
+
+ +
+
+
+ +

{{ctx.Locale.Tr "packages.arch.version.properties"}}

+
+ + + + + + + + {{if .PackageDescriptor.Metadata.Groups}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.Provides}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.Depends}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.OptDepends}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.MakeDepends}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.CheckDepends}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.Conflicts}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.Replaces}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.Backup}} + + + + + {{end}} + +
+
{{ctx.Locale.Tr "packages.arch.version.description"}}
+
{{.PackageDescriptor.Metadata.Description}}
+
{{ctx.Locale.Tr "packages.arch.version.groups"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.Groups ", "}}
+
{{ctx.Locale.Tr "packages.arch.version.provides"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.Provides ", "}}
+
{{ctx.Locale.Tr "packages.arch.version.depends"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.Depends ", "}}
+
{{ctx.Locale.Tr "packages.arch.version.optdepends"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.OptDepends ", "}}
+
{{ctx.Locale.Tr "packages.arch.version.makedepends"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.MakeDepends ", "}}
+
{{ctx.Locale.Tr "packages.arch.version.checkdepends"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.CheckDepends ", "}}
+
{{ctx.Locale.Tr "packages.arch.version.conflicts"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.Conflicts ", "}}
+
{{ctx.Locale.Tr "packages.arch.version.replaces"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.Replaces ", "}}
+
{{ctx.Locale.Tr "packages.arch.version.backup"}}
+
{{StringUtils.Join $.PackageDescriptor.Metadata.Backup ", "}}
+
+ +{{end}} diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl new file mode 100644 index 0000000000..822973eb7d --- /dev/null +++ b/templates/package/metadata/arch.tmpl @@ -0,0 +1,4 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} + {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} +{{end}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 1d87f4d3af..fe88e54317 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -19,6 +19,7 @@
{{template "package/content/alpine" .}} + {{template "package/content/arch" .}} {{template "package/content/cargo" .}} {{template "package/content/chef" .}} {{template "package/content/composer" .}} @@ -50,6 +51,7 @@
{{svg "octicon-calendar" 16 "tw-mr-2"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}
{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}
{{template "package/metadata/alpine" .}} + {{template "package/metadata/arch" .}} {{template "package/metadata/cargo" .}} {{template "package/metadata/chef" .}} {{template "package/metadata/composer" .}} diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go new file mode 100644 index 0000000000..6062a88ea0 --- /dev/null +++ b/tests/integration/api_packages_arch_test.go @@ -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) +} diff --git a/web_src/svg/gitea-arch.svg b/web_src/svg/gitea-arch.svg new file mode 100644 index 0000000000..ba8254d804 --- /dev/null +++ b/web_src/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file