code.gitea.io/gitea@v1.22.3/modules/packages/alpine/metadata.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package alpine
     5  
     6  import (
     7  	"archive/tar"
     8  	"bufio"
     9  	"compress/gzip"
    10  	"crypto/sha1"
    11  	"encoding/base64"
    12  	"io"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"code.gitea.io/gitea/modules/util"
    17  	"code.gitea.io/gitea/modules/validation"
    18  )
    19  
    20  var (
    21  	ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf("PKGINFO file is missing")
    22  	ErrInvalidName        = util.NewInvalidArgumentErrorf("package name is invalid")
    23  	ErrInvalidVersion     = util.NewInvalidArgumentErrorf("package version is invalid")
    24  )
    25  
    26  const (
    27  	PropertyMetadata     = "alpine.metadata"
    28  	PropertyBranch       = "alpine.branch"
    29  	PropertyRepository   = "alpine.repository"
    30  	PropertyArchitecture = "alpine.architecture"
    31  
    32  	SettingKeyPrivate = "alpine.key.private"
    33  	SettingKeyPublic  = "alpine.key.public"
    34  
    35  	RepositoryPackage = "_alpine"
    36  	RepositoryVersion = "_repository"
    37  
    38  	NoArch = "noarch"
    39  )
    40  
    41  // https://wiki.alpinelinux.org/wiki/Apk_spec
    42  
    43  // Package represents an Alpine package
    44  type Package struct {
    45  	Name            string
    46  	Version         string
    47  	VersionMetadata VersionMetadata
    48  	FileMetadata    FileMetadata
    49  }
    50  
    51  // Metadata of an Alpine package
    52  type VersionMetadata struct {
    53  	Description string `json:"description,omitempty"`
    54  	License     string `json:"license,omitempty"`
    55  	ProjectURL  string `json:"project_url,omitempty"`
    56  	Maintainer  string `json:"maintainer,omitempty"`
    57  }
    58  
    59  type FileMetadata struct {
    60  	Checksum         string   `json:"checksum"`
    61  	Packager         string   `json:"packager,omitempty"`
    62  	BuildDate        int64    `json:"build_date,omitempty"`
    63  	Size             int64    `json:"size,omitempty"`
    64  	Architecture     string   `json:"architecture,omitempty"`
    65  	Origin           string   `json:"origin,omitempty"`
    66  	CommitHash       string   `json:"commit_hash,omitempty"`
    67  	InstallIf        string   `json:"install_if,omitempty"`
    68  	Provides         []string `json:"provides,omitempty"`
    69  	Dependencies     []string `json:"dependencies,omitempty"`
    70  	ProviderPriority int64    `json:"provider_priority,omitempty"`
    71  }
    72  
    73  // ParsePackage parses the Alpine package file
    74  func ParsePackage(r io.Reader) (*Package, error) {
    75  	// Alpine packages are concated .tar.gz streams. Usually the first stream contains the package metadata.
    76  
    77  	br := bufio.NewReader(r) // needed for gzip Multistream
    78  
    79  	h := sha1.New()
    80  
    81  	gzr, err := gzip.NewReader(&teeByteReader{br, h})
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	defer gzr.Close()
    86  
    87  	for {
    88  		gzr.Multistream(false)
    89  
    90  		tr := tar.NewReader(gzr)
    91  		for {
    92  			hd, err := tr.Next()
    93  			if err == io.EOF {
    94  				break
    95  			}
    96  			if err != nil {
    97  				return nil, err
    98  			}
    99  
   100  			if hd.Name == ".PKGINFO" {
   101  				p, err := ParsePackageInfo(tr)
   102  				if err != nil {
   103  					return nil, err
   104  				}
   105  
   106  				// drain the reader
   107  				for {
   108  					if _, err := tr.Next(); err != nil {
   109  						break
   110  					}
   111  				}
   112  
   113  				p.FileMetadata.Checksum = "Q1" + base64.StdEncoding.EncodeToString(h.Sum(nil))
   114  
   115  				return p, nil
   116  			}
   117  		}
   118  
   119  		h = sha1.New()
   120  
   121  		err = gzr.Reset(&teeByteReader{br, h})
   122  		if err == io.EOF {
   123  			break
   124  		}
   125  		if err != nil {
   126  			return nil, err
   127  		}
   128  	}
   129  
   130  	return nil, ErrMissingPKGINFOFile
   131  }
   132  
   133  // ParsePackageInfo parses a PKGINFO file to retrieve the metadata of an Alpine package
   134  func ParsePackageInfo(r io.Reader) (*Package, error) {
   135  	p := &Package{}
   136  
   137  	scanner := bufio.NewScanner(r)
   138  	for scanner.Scan() {
   139  		line := scanner.Text()
   140  
   141  		if strings.HasPrefix(line, "#") {
   142  			continue
   143  		}
   144  
   145  		i := strings.IndexRune(line, '=')
   146  		if i == -1 {
   147  			continue
   148  		}
   149  
   150  		key := strings.TrimSpace(line[:i])
   151  		value := strings.TrimSpace(line[i+1:])
   152  
   153  		switch key {
   154  		case "pkgname":
   155  			p.Name = value
   156  		case "pkgver":
   157  			p.Version = value
   158  		case "pkgdesc":
   159  			p.VersionMetadata.Description = value
   160  		case "url":
   161  			p.VersionMetadata.ProjectURL = value
   162  		case "builddate":
   163  			n, err := strconv.ParseInt(value, 10, 64)
   164  			if err == nil {
   165  				p.FileMetadata.BuildDate = n
   166  			}
   167  		case "size":
   168  			n, err := strconv.ParseInt(value, 10, 64)
   169  			if err == nil {
   170  				p.FileMetadata.Size = n
   171  			}
   172  		case "arch":
   173  			p.FileMetadata.Architecture = value
   174  		case "origin":
   175  			p.FileMetadata.Origin = value
   176  		case "commit":
   177  			p.FileMetadata.CommitHash = value
   178  		case "maintainer":
   179  			p.VersionMetadata.Maintainer = value
   180  		case "packager":
   181  			p.FileMetadata.Packager = value
   182  		case "license":
   183  			p.VersionMetadata.License = value
   184  		case "install_if":
   185  			p.FileMetadata.InstallIf = value
   186  		case "provides":
   187  			if value != "" {
   188  				p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
   189  			}
   190  		case "depend":
   191  			if value != "" {
   192  				p.FileMetadata.Dependencies = append(p.FileMetadata.Dependencies, value)
   193  			}
   194  		case "provider_priority":
   195  			n, err := strconv.ParseInt(value, 10, 64)
   196  			if err == nil {
   197  				p.FileMetadata.ProviderPriority = n
   198  			}
   199  		}
   200  	}
   201  	if err := scanner.Err(); err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	if p.Name == "" {
   206  		return nil, ErrInvalidName
   207  	}
   208  
   209  	if p.Version == "" {
   210  		return nil, ErrInvalidVersion
   211  	}
   212  
   213  	if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
   214  		p.VersionMetadata.ProjectURL = ""
   215  	}
   216  
   217  	return p, nil
   218  }
   219  
   220  // Same as io.TeeReader but implements io.ByteReader
   221  type teeByteReader struct {
   222  	r *bufio.Reader
   223  	w io.Writer
   224  }
   225  
   226  func (t *teeByteReader) Read(p []byte) (int, error) {
   227  	n, err := t.r.Read(p)
   228  	if n > 0 {
   229  		if n, err := t.w.Write(p[:n]); err != nil {
   230  			return n, err
   231  		}
   232  	}
   233  	return n, err
   234  }
   235  
   236  func (t *teeByteReader) ReadByte() (byte, error) {
   237  	b, err := t.r.ReadByte()
   238  	if err == nil {
   239  		if _, err := t.w.Write([]byte{b}); err != nil {
   240  			return 0, err
   241  		}
   242  	}
   243  	return b, err
   244  }