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