mirror of
https://codeberg.org/davrot/forgejo.git
synced 2025-05-17 23:00:02 +02:00
Add Debian package registry (#22854)
Co-authored-by: @awkwardbunny This PR adds a Debian package registry. You can follow [this tutorial](https://www.baeldung.com/linux/create-debian-package) to build a *.deb package for testing. Source packages are not supported at the moment and I did not find documentation of the architecture "all" and how these packages should be treated. --------- Co-authored-by: Brian Hong <brian@hongs.me> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
parent
bc4e06109d
commit
bf77e2163b
57 changed files with 1995 additions and 96 deletions
216
modules/packages/debian/metadata.go
Normal file
216
modules/packages/debian/metadata.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
"github.com/blakesmith/ar"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/ulikunitz/xz"
|
||||
)
|
||||
|
||||
const (
|
||||
PropertyDistribution = "debian.distribution"
|
||||
PropertyComponent = "debian.component"
|
||||
PropertyArchitecture = "debian.architecture"
|
||||
PropertyControl = "debian.control"
|
||||
PropertyRepositoryIncludeInRelease = "debian.repository.include_in_release"
|
||||
|
||||
SettingKeyPrivate = "debian.key.private"
|
||||
SettingKeyPublic = "debian.key.public"
|
||||
|
||||
RepositoryPackage = "_debian"
|
||||
RepositoryVersion = "_repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing")
|
||||
ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithmn")
|
||||
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||||
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||||
ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
|
||||
|
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#source
|
||||
namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`)
|
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
|
||||
versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
|
||||
)
|
||||
|
||||
type Package struct {
|
||||
Name string
|
||||
Version string
|
||||
Architecture string
|
||||
Control string
|
||||
Metadata *Metadata
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Maintainer string `json:"maintainer,omitempty"`
|
||||
ProjectURL string `json:"project_url,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
// ParsePackage parses the Debian package file
|
||||
// https://manpages.debian.org/bullseye/dpkg-dev/deb.5.en.html
|
||||
func ParsePackage(r io.Reader) (*Package, error) {
|
||||
arr := ar.NewReader(r)
|
||||
|
||||
for {
|
||||
hd, err := arr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(hd.Name, "control.tar") {
|
||||
var inner io.Reader
|
||||
switch hd.Name[11:] {
|
||||
case "":
|
||||
inner = arr
|
||||
case ".gz":
|
||||
gzr, err := gzip.NewReader(arr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
inner = gzr
|
||||
case ".xz":
|
||||
xzr, err := xz.NewReader(arr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inner = xzr
|
||||
case ".zst":
|
||||
zr, err := zstd.NewReader(arr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
inner = zr
|
||||
default:
|
||||
return nil, ErrUnsupportedCompression
|
||||
}
|
||||
|
||||
tr := tar.NewReader(inner)
|
||||
for {
|
||||
hd, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hd.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
|
||||
if hd.FileInfo().Name() == "control" {
|
||||
return ParseControlFile(tr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrMissingControlFile
|
||||
}
|
||||
|
||||
// ParseControlFile parses a Debian control file to retrieve the metadata
|
||||
func ParseControlFile(r io.Reader) (*Package, error) {
|
||||
p := &Package{
|
||||
Metadata: &Metadata{},
|
||||
}
|
||||
|
||||
key := ""
|
||||
var depends strings.Builder
|
||||
var control strings.Builder
|
||||
|
||||
s := bufio.NewScanner(io.TeeReader(r, &control))
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if line[0] == ' ' || line[0] == '\t' {
|
||||
switch key {
|
||||
case "Description":
|
||||
p.Metadata.Description += line
|
||||
case "Depends":
|
||||
depends.WriteString(trimmed)
|
||||
}
|
||||
} else {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key = parts[0]
|
||||
value := strings.TrimSpace(parts[1])
|
||||
switch key {
|
||||
case "Package":
|
||||
if !namePattern.MatchString(value) {
|
||||
return nil, ErrInvalidName
|
||||
}
|
||||
p.Name = value
|
||||
case "Version":
|
||||
if !versionPattern.MatchString(value) {
|
||||
return nil, ErrInvalidVersion
|
||||
}
|
||||
p.Version = value
|
||||
case "Architecture":
|
||||
if value == "" {
|
||||
return nil, ErrInvalidArchitecture
|
||||
}
|
||||
p.Architecture = value
|
||||
case "Maintainer":
|
||||
a, err := mail.ParseAddress(value)
|
||||
if err != nil || a.Name == "" {
|
||||
p.Metadata.Maintainer = value
|
||||
} else {
|
||||
p.Metadata.Maintainer = a.Name
|
||||
}
|
||||
case "Description":
|
||||
p.Metadata.Description = value
|
||||
case "Depends":
|
||||
depends.WriteString(value)
|
||||
case "Homepage":
|
||||
if validation.IsValidURL(value) {
|
||||
p.Metadata.ProjectURL = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dependencies := strings.Split(depends.String(), ",")
|
||||
for i := range dependencies {
|
||||
dependencies[i] = strings.TrimSpace(dependencies[i])
|
||||
}
|
||||
p.Metadata.Dependencies = dependencies
|
||||
|
||||
p.Control = control.String()
|
||||
|
||||
return p, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue