|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- // 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"
-
- controlTar = "control.tar"
- )
-
- var (
- ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing")
- ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm")
- 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, controlTar) {
- var inner io.Reader
- switch hd.Name[len(controlTar):] {
- 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
- }
|