mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-03 21:08:25 +00:00 
			
		
		
		
	This PR adds an Alpine package registry. You can follow [this tutorial](https://wiki.alpinelinux.org/wiki/Creating_an_Alpine_package) to build a *.apk package for testing. This functionality is similar to the Debian registry (#22854) and therefore shares some methods. I marked this PR as blocked because it should be merged after #22854.  --------- Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Giteabot <teabot@gitea.io>
		
			
				
	
	
		
			237 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			237 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package alpine
 | 
						|
 | 
						|
import (
 | 
						|
	"archive/tar"
 | 
						|
	"bufio"
 | 
						|
	"compress/gzip"
 | 
						|
	"crypto/sha1"
 | 
						|
	"encoding/base64"
 | 
						|
	"io"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/modules/util"
 | 
						|
	"code.gitea.io/gitea/modules/validation"
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf("PKGINFO file is missing")
 | 
						|
	ErrInvalidName        = util.NewInvalidArgumentErrorf("package name is invalid")
 | 
						|
	ErrInvalidVersion     = util.NewInvalidArgumentErrorf("package version is invalid")
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	PropertyMetadata     = "alpine.metadata"
 | 
						|
	PropertyBranch       = "alpine.branch"
 | 
						|
	PropertyRepository   = "alpine.repository"
 | 
						|
	PropertyArchitecture = "alpine.architecture"
 | 
						|
 | 
						|
	SettingKeyPrivate = "alpine.key.private"
 | 
						|
	SettingKeyPublic  = "alpine.key.public"
 | 
						|
 | 
						|
	RepositoryPackage = "_alpine"
 | 
						|
	RepositoryVersion = "_repository"
 | 
						|
)
 | 
						|
 | 
						|
// https://wiki.alpinelinux.org/wiki/Apk_spec
 | 
						|
 | 
						|
// Package represents an Alpine package
 | 
						|
type Package struct {
 | 
						|
	Name            string
 | 
						|
	Version         string
 | 
						|
	VersionMetadata VersionMetadata
 | 
						|
	FileMetadata    FileMetadata
 | 
						|
}
 | 
						|
 | 
						|
// Metadata of an Alpine package
 | 
						|
type VersionMetadata struct {
 | 
						|
	Description string `json:"description,omitempty"`
 | 
						|
	License     string `json:"license,omitempty"`
 | 
						|
	ProjectURL  string `json:"project_url,omitempty"`
 | 
						|
	Maintainer  string `json:"maintainer,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
type FileMetadata struct {
 | 
						|
	Checksum     string   `json:"checksum"`
 | 
						|
	Packager     string   `json:"packager,omitempty"`
 | 
						|
	BuildDate    int64    `json:"build_date,omitempty"`
 | 
						|
	Size         int64    `json:"size,omitempty"`
 | 
						|
	Architecture string   `json:"architecture,omitempty"`
 | 
						|
	Origin       string   `json:"origin,omitempty"`
 | 
						|
	CommitHash   string   `json:"commit_hash,omitempty"`
 | 
						|
	InstallIf    string   `json:"install_if,omitempty"`
 | 
						|
	Provides     []string `json:"provides,omitempty"`
 | 
						|
	Dependencies []string `json:"dependencies,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
// ParsePackage parses the Alpine package file
 | 
						|
func ParsePackage(r io.Reader) (*Package, error) {
 | 
						|
	// Alpine packages are concated .tar.gz streams. Usually the first stream contains the package metadata.
 | 
						|
 | 
						|
	br := bufio.NewReader(r) // needed for gzip Multistream
 | 
						|
 | 
						|
	h := sha1.New()
 | 
						|
 | 
						|
	gzr, err := gzip.NewReader(&teeByteReader{br, h})
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer gzr.Close()
 | 
						|
 | 
						|
	for {
 | 
						|
		gzr.Multistream(false)
 | 
						|
 | 
						|
		tr := tar.NewReader(gzr)
 | 
						|
		for {
 | 
						|
			hd, err := tr.Next()
 | 
						|
			if err == io.EOF {
 | 
						|
				break
 | 
						|
			}
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
 | 
						|
			if hd.Name == ".PKGINFO" {
 | 
						|
				p, err := ParsePackageInfo(tr)
 | 
						|
				if err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
 | 
						|
				// drain the reader
 | 
						|
				for {
 | 
						|
					if _, err := tr.Next(); err != nil {
 | 
						|
						break
 | 
						|
					}
 | 
						|
				}
 | 
						|
 | 
						|
				p.FileMetadata.Checksum = "Q1" + base64.StdEncoding.EncodeToString(h.Sum(nil))
 | 
						|
 | 
						|
				return p, nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		h = sha1.New()
 | 
						|
 | 
						|
		err = gzr.Reset(&teeByteReader{br, h})
 | 
						|
		if err == io.EOF {
 | 
						|
			break
 | 
						|
		}
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil, ErrMissingPKGINFOFile
 | 
						|
}
 | 
						|
 | 
						|
// ParsePackageInfo parses a PKGINFO file to retrieve the metadata of an Alpine package
 | 
						|
func ParsePackageInfo(r io.Reader) (*Package, error) {
 | 
						|
	p := &Package{}
 | 
						|
 | 
						|
	scanner := bufio.NewScanner(r)
 | 
						|
	for scanner.Scan() {
 | 
						|
		line := scanner.Text()
 | 
						|
 | 
						|
		if strings.HasPrefix(line, "#") {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		i := strings.IndexRune(line, '=')
 | 
						|
		if i == -1 {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		key := strings.TrimSpace(line[:i])
 | 
						|
		value := strings.TrimSpace(line[i+1:])
 | 
						|
 | 
						|
		switch key {
 | 
						|
		case "pkgname":
 | 
						|
			p.Name = value
 | 
						|
		case "pkgver":
 | 
						|
			p.Version = value
 | 
						|
		case "pkgdesc":
 | 
						|
			p.VersionMetadata.Description = value
 | 
						|
		case "url":
 | 
						|
			p.VersionMetadata.ProjectURL = value
 | 
						|
		case "builddate":
 | 
						|
			n, err := strconv.ParseInt(value, 10, 64)
 | 
						|
			if err == nil {
 | 
						|
				p.FileMetadata.BuildDate = n
 | 
						|
			}
 | 
						|
		case "size":
 | 
						|
			n, err := strconv.ParseInt(value, 10, 64)
 | 
						|
			if err == nil {
 | 
						|
				p.FileMetadata.Size = n
 | 
						|
			}
 | 
						|
		case "arch":
 | 
						|
			p.FileMetadata.Architecture = value
 | 
						|
		case "origin":
 | 
						|
			p.FileMetadata.Origin = value
 | 
						|
		case "commit":
 | 
						|
			p.FileMetadata.CommitHash = value
 | 
						|
		case "maintainer":
 | 
						|
			p.VersionMetadata.Maintainer = value
 | 
						|
		case "packager":
 | 
						|
			p.FileMetadata.Packager = value
 | 
						|
		case "license":
 | 
						|
			p.VersionMetadata.License = value
 | 
						|
		case "install_if":
 | 
						|
			p.FileMetadata.InstallIf = value
 | 
						|
		case "provides":
 | 
						|
			if value != "" {
 | 
						|
				p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
 | 
						|
			}
 | 
						|
		case "depend":
 | 
						|
			if value != "" {
 | 
						|
				p.FileMetadata.Dependencies = append(p.FileMetadata.Dependencies, value)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if err := scanner.Err(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if p.Name == "" {
 | 
						|
		return nil, ErrInvalidName
 | 
						|
	}
 | 
						|
 | 
						|
	if p.Version == "" {
 | 
						|
		return nil, ErrInvalidVersion
 | 
						|
	}
 | 
						|
 | 
						|
	if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
 | 
						|
		p.VersionMetadata.ProjectURL = ""
 | 
						|
	}
 | 
						|
 | 
						|
	return p, nil
 | 
						|
}
 | 
						|
 | 
						|
// Same as io.TeeReader but implements io.ByteReader
 | 
						|
type teeByteReader struct {
 | 
						|
	r *bufio.Reader
 | 
						|
	w io.Writer
 | 
						|
}
 | 
						|
 | 
						|
func (t *teeByteReader) Read(p []byte) (int, error) {
 | 
						|
	n, err := t.r.Read(p)
 | 
						|
	if n > 0 {
 | 
						|
		if n, err := t.w.Write(p[:n]); err != nil {
 | 
						|
			return n, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return n, err
 | 
						|
}
 | 
						|
 | 
						|
func (t *teeByteReader) ReadByte() (byte, error) {
 | 
						|
	b, err := t.r.ReadByte()
 | 
						|
	if err == nil {
 | 
						|
		if _, err := t.w.Write([]byte{b}); err != nil {
 | 
						|
			return 0, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return b, err
 | 
						|
}
 |