|  | 
|  | 1 | +// Copyright 2023 The Gitea Authors. All rights reserved. | 
|  | 2 | +// SPDX-License-Identifier: MIT | 
|  | 3 | + | 
|  | 4 | +package arch | 
|  | 5 | + | 
|  | 6 | +import ( | 
|  | 7 | +	"archive/tar" | 
|  | 8 | +	"bufio" | 
|  | 9 | +	"bytes" | 
|  | 10 | +	"compress/gzip" | 
|  | 11 | +	"io" | 
|  | 12 | +	"regexp" | 
|  | 13 | +	"strconv" | 
|  | 14 | +	"strings" | 
|  | 15 | + | 
|  | 16 | +	"code.gitea.io/gitea/modules/util" | 
|  | 17 | +	"code.gitea.io/gitea/modules/validation" | 
|  | 18 | + | 
|  | 19 | +	"github.com/klauspost/compress/zstd" | 
|  | 20 | +	"github.com/ulikunitz/xz" | 
|  | 21 | +) | 
|  | 22 | + | 
|  | 23 | +const ( | 
|  | 24 | +	PropertyRepository   = "arch.repository" | 
|  | 25 | +	PropertyArchitecture = "arch.architecture" | 
|  | 26 | +	PropertySignature    = "arch.signature" | 
|  | 27 | +	PropertyMetadata     = "arch.metadata" | 
|  | 28 | + | 
|  | 29 | +	SettingKeyPrivate = "arch.key.private" | 
|  | 30 | +	SettingKeyPublic  = "arch.key.public" | 
|  | 31 | + | 
|  | 32 | +	RepositoryPackage = "_arch" | 
|  | 33 | +	RepositoryVersion = "_repository" | 
|  | 34 | + | 
|  | 35 | +	AnyArch = "any" | 
|  | 36 | +) | 
|  | 37 | + | 
|  | 38 | +var ( | 
|  | 39 | +	ErrMissingPKGINFOFile  = util.NewInvalidArgumentErrorf(".PKGINFO file is missing") | 
|  | 40 | +	ErrUnsupportedFormat   = util.NewInvalidArgumentErrorf("unsupported package container format") | 
|  | 41 | +	ErrInvalidName         = util.NewInvalidArgumentErrorf("package name is invalid") | 
|  | 42 | +	ErrInvalidVersion      = util.NewInvalidArgumentErrorf("package version is invalid") | 
|  | 43 | +	ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") | 
|  | 44 | + | 
|  | 45 | +	// https://man.archlinux.org/man/PKGBUILD.5 | 
|  | 46 | +	namePattern    = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`) | 
|  | 47 | +	versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) | 
|  | 48 | +) | 
|  | 49 | + | 
|  | 50 | +type Package struct { | 
|  | 51 | +	Name                     string | 
|  | 52 | +	Version                  string | 
|  | 53 | +	VersionMetadata          VersionMetadata | 
|  | 54 | +	FileMetadata             FileMetadata | 
|  | 55 | +	FileCompressionExtension string | 
|  | 56 | +} | 
|  | 57 | + | 
|  | 58 | +type VersionMetadata struct { | 
|  | 59 | +	Description string   `json:"description,omitempty"` | 
|  | 60 | +	ProjectURL  string   `json:"project_url,omitempty"` | 
|  | 61 | +	Licenses    []string `json:"licenses,omitempty"` | 
|  | 62 | +} | 
|  | 63 | + | 
|  | 64 | +type FileMetadata struct { | 
|  | 65 | +	Architecture  string   `json:"architecture"` | 
|  | 66 | +	Base          string   `json:"base,omitempty"` | 
|  | 67 | +	InstalledSize int64    `json:"installed_size,omitempty"` | 
|  | 68 | +	BuildDate     int64    `json:"build_date,omitempty"` | 
|  | 69 | +	Packager      string   `json:"packager,omitempty"` | 
|  | 70 | +	Groups        []string `json:"groups,omitempty"` | 
|  | 71 | +	Provides      []string `json:"provides,omitempty"` | 
|  | 72 | +	Depends       []string `json:"depends,omitempty"` | 
|  | 73 | +	OptDepends    []string `json:"opt_depends,omitempty"` | 
|  | 74 | +	MakeDepends   []string `json:"make_depends,omitempty"` | 
|  | 75 | +	CheckDepends  []string `json:"check_depends,omitempty"` | 
|  | 76 | +	XData         []string `json:"xdata,omitempty"` | 
|  | 77 | +	Backup        []string `json:"backup,omitempty"` | 
|  | 78 | +	Files         []string `json:"files,omitempty"` | 
|  | 79 | +} | 
|  | 80 | + | 
|  | 81 | +// ParsePackage parses an Arch package file | 
|  | 82 | +func ParsePackage(r io.Reader) (*Package, error) { | 
|  | 83 | +	header := make([]byte, 10) | 
|  | 84 | +	n, err := util.ReadAtMost(r, header) | 
|  | 85 | +	if err != nil { | 
|  | 86 | +		return nil, err | 
|  | 87 | +	} | 
|  | 88 | + | 
|  | 89 | +	r = io.MultiReader(bytes.NewReader(header[:n]), r) | 
|  | 90 | + | 
|  | 91 | +	var inner io.Reader | 
|  | 92 | +	var compressionType string | 
|  | 93 | +	if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst | 
|  | 94 | +		zr, err := zstd.NewReader(r) | 
|  | 95 | +		if err != nil { | 
|  | 96 | +			return nil, err | 
|  | 97 | +		} | 
|  | 98 | +		defer zr.Close() | 
|  | 99 | + | 
|  | 100 | +		inner = zr | 
|  | 101 | +		compressionType = "zst" | 
|  | 102 | +	} else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz | 
|  | 103 | +		xzr, err := xz.NewReader(r) | 
|  | 104 | +		if err != nil { | 
|  | 105 | +			return nil, err | 
|  | 106 | +		} | 
|  | 107 | + | 
|  | 108 | +		inner = xzr | 
|  | 109 | +		compressionType = "xz" | 
|  | 110 | +	} else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz | 
|  | 111 | +		gzr, err := gzip.NewReader(r) | 
|  | 112 | +		if err != nil { | 
|  | 113 | +			return nil, err | 
|  | 114 | +		} | 
|  | 115 | +		defer gzr.Close() | 
|  | 116 | + | 
|  | 117 | +		inner = gzr | 
|  | 118 | +		compressionType = "gz" | 
|  | 119 | +	} else { | 
|  | 120 | +		return nil, ErrUnsupportedFormat | 
|  | 121 | +	} | 
|  | 122 | + | 
|  | 123 | +	var p *Package | 
|  | 124 | +	files := make([]string, 0, 10) | 
|  | 125 | + | 
|  | 126 | +	tr := tar.NewReader(inner) | 
|  | 127 | +	for { | 
|  | 128 | +		hd, err := tr.Next() | 
|  | 129 | +		if err == io.EOF { | 
|  | 130 | +			break | 
|  | 131 | +		} | 
|  | 132 | +		if err != nil { | 
|  | 133 | +			return nil, err | 
|  | 134 | +		} | 
|  | 135 | + | 
|  | 136 | +		if hd.Typeflag != tar.TypeReg { | 
|  | 137 | +			continue | 
|  | 138 | +		} | 
|  | 139 | + | 
|  | 140 | +		filename := hd.FileInfo().Name() | 
|  | 141 | +		if filename == ".PKGINFO" { | 
|  | 142 | +			p, err = ParsePackageInfo(tr) | 
|  | 143 | +			if err != nil { | 
|  | 144 | +				return nil, err | 
|  | 145 | +			} | 
|  | 146 | +		} else if !strings.HasPrefix(filename, ".") { | 
|  | 147 | +			files = append(files, hd.Name) | 
|  | 148 | +		} | 
|  | 149 | +	} | 
|  | 150 | + | 
|  | 151 | +	if p == nil { | 
|  | 152 | +		return nil, ErrMissingPKGINFOFile | 
|  | 153 | +	} | 
|  | 154 | + | 
|  | 155 | +	p.FileMetadata.Files = files | 
|  | 156 | +	p.FileCompressionExtension = compressionType | 
|  | 157 | + | 
|  | 158 | +	return p, nil | 
|  | 159 | +} | 
|  | 160 | + | 
|  | 161 | +// ParsePackageInfo parses a .PKGINFO file to retrieve the metadata | 
|  | 162 | +// https://man.archlinux.org/man/PKGBUILD.5 | 
|  | 163 | +// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_package.c#L161 | 
|  | 164 | +func ParsePackageInfo(r io.Reader) (*Package, error) { | 
|  | 165 | +	p := &Package{} | 
|  | 166 | + | 
|  | 167 | +	s := bufio.NewScanner(r) | 
|  | 168 | +	for s.Scan() { | 
|  | 169 | +		line := s.Text() | 
|  | 170 | + | 
|  | 171 | +		if strings.HasPrefix(line, "#") { | 
|  | 172 | +			continue | 
|  | 173 | +		} | 
|  | 174 | + | 
|  | 175 | +		i := strings.IndexRune(line, '=') | 
|  | 176 | +		if i == -1 { | 
|  | 177 | +			continue | 
|  | 178 | +		} | 
|  | 179 | + | 
|  | 180 | +		key := strings.TrimSpace(line[:i]) | 
|  | 181 | +		value := strings.TrimSpace(line[i+1:]) | 
|  | 182 | + | 
|  | 183 | +		switch key { | 
|  | 184 | +		case "pkgname": | 
|  | 185 | +			p.Name = value | 
|  | 186 | +		case "pkgbase": | 
|  | 187 | +			p.FileMetadata.Base = value | 
|  | 188 | +		case "pkgver": | 
|  | 189 | +			p.Version = value | 
|  | 190 | +		case "pkgdesc": | 
|  | 191 | +			p.VersionMetadata.Description = value | 
|  | 192 | +		case "url": | 
|  | 193 | +			p.VersionMetadata.ProjectURL = value | 
|  | 194 | +		case "packager": | 
|  | 195 | +			p.FileMetadata.Packager = value | 
|  | 196 | +		case "arch": | 
|  | 197 | +			p.FileMetadata.Architecture = value | 
|  | 198 | +		case "license": | 
|  | 199 | +			p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value) | 
|  | 200 | +		case "provides": | 
|  | 201 | +			p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) | 
|  | 202 | +		case "depend": | 
|  | 203 | +			p.FileMetadata.Depends = append(p.FileMetadata.Depends, value) | 
|  | 204 | +		case "optdepend": | 
|  | 205 | +			p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value) | 
|  | 206 | +		case "makedepend": | 
|  | 207 | +			p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value) | 
|  | 208 | +		case "checkdepend": | 
|  | 209 | +			p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value) | 
|  | 210 | +		case "backup": | 
|  | 211 | +			p.FileMetadata.Backup = append(p.FileMetadata.Backup, value) | 
|  | 212 | +		case "group": | 
|  | 213 | +			p.FileMetadata.Groups = append(p.FileMetadata.Groups, value) | 
|  | 214 | +		case "builddate": | 
|  | 215 | +			date, err := strconv.ParseInt(value, 10, 64) | 
|  | 216 | +			if err != nil { | 
|  | 217 | +				return nil, err | 
|  | 218 | +			} | 
|  | 219 | +			p.FileMetadata.BuildDate = date | 
|  | 220 | +		case "size": | 
|  | 221 | +			size, err := strconv.ParseInt(value, 10, 64) | 
|  | 222 | +			if err != nil { | 
|  | 223 | +				return nil, err | 
|  | 224 | +			} | 
|  | 225 | +			p.FileMetadata.InstalledSize = size | 
|  | 226 | +		case "xdata": | 
|  | 227 | +			p.FileMetadata.XData = append(p.FileMetadata.XData, value) | 
|  | 228 | +		} | 
|  | 229 | +	} | 
|  | 230 | +	if err := s.Err(); err != nil { | 
|  | 231 | +		return nil, err | 
|  | 232 | +	} | 
|  | 233 | + | 
|  | 234 | +	if !namePattern.MatchString(p.Name) { | 
|  | 235 | +		return nil, ErrInvalidName | 
|  | 236 | +	} | 
|  | 237 | +	if !versionPattern.MatchString(p.Version) { | 
|  | 238 | +		return nil, ErrInvalidVersion | 
|  | 239 | +	} | 
|  | 240 | +	if p.FileMetadata.Architecture == "" { | 
|  | 241 | +		return nil, ErrInvalidArchitecture | 
|  | 242 | +	} | 
|  | 243 | + | 
|  | 244 | +	if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { | 
|  | 245 | +		p.VersionMetadata.ProjectURL = "" | 
|  | 246 | +	} | 
|  | 247 | + | 
|  | 248 | +	return p, nil | 
|  | 249 | +} | 
0 commit comments