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.  Part of #20751. Revised copy of #22854. --------- Co-authored-by: Brian Hong <brian@hongs.me> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Giteabot <teabot@gitea.io>
		
			
				
	
	
		
			219 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			219 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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
 | |
| }
 |