初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
// 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"
"gitea.dev/modules/util"
"gitea.dev/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"
NoArch = "noarch"
)
// 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"`
ProviderPriority int64 `json:"provider_priority,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)
}
case "provider_priority":
n, err := strconv.ParseInt(value, 10, 64)
if err == nil {
p.FileMetadata.ProviderPriority = n
}
}
}
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
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package alpine
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
const (
packageName = "gitea"
packageVersion = "1.0.1"
packageDescription = "Package Description"
packageProjectURL = "https://gitea.io"
packageMaintainer = "KN4CK3R <dummy@gitea.io>"
)
func createPKGINFOContent(name, version string) []byte {
return []byte(`pkgname = ` + name + `
pkgver = ` + version + `
pkgdesc = ` + packageDescription + `
url = ` + packageProjectURL + `
# comment
builddate = 1678834800
packager = Gitea <pack@ag.er>
size = 123456
arch = aarch64
origin = origin
commit = 1111e709613fbc979651b09ac2bc27c6591a9999
maintainer = ` + packageMaintainer + `
license = MIT
depend = common
install_if = value
depend = gitea
provides = common
provides = gitea`)
}
func TestParsePackage(t *testing.T) {
createPackage := func(name string, content []byte) io.Reader {
names := []string{"first.stream", name}
contents := [][]byte{{0}, content}
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
for i := range names {
if i != 0 {
zw.Close()
zw.Reset(&buf)
}
tw := tar.NewWriter(zw)
hdr := &tar.Header{
Name: names[i],
Mode: 0o600,
Size: int64(len(contents[i])),
}
tw.WriteHeader(hdr)
tw.Write(contents[i])
tw.Close()
}
zw.Close()
return &buf
}
t.Run("MissingPKGINFOFile", func(t *testing.T) {
data := createPackage("dummy.txt", []byte{})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrMissingPKGINFOFile)
})
t.Run("InvalidPKGINFOFile", func(t *testing.T) {
data := createPackage(".PKGINFO", []byte{})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrInvalidName)
})
t.Run("Valid", func(t *testing.T) {
data := createPackage(".PKGINFO", createPKGINFOContent(packageName, packageVersion))
p, err := ParsePackage(data)
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, "Q1SRYURM5+uQDqfHSwTnNIOIuuDVQ=", p.FileMetadata.Checksum)
})
}
func TestParsePackageInfo(t *testing.T) {
t.Run("InvalidName", func(t *testing.T) {
data := createPKGINFOContent("", packageVersion)
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidName)
})
t.Run("InvalidVersion", func(t *testing.T) {
data := createPKGINFOContent(packageName, "")
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidVersion)
})
t.Run("Valid", func(t *testing.T) {
data := createPKGINFOContent(packageName, packageVersion)
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, packageDescription, p.VersionMetadata.Description)
assert.Equal(t, packageMaintainer, p.VersionMetadata.Maintainer)
assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL)
assert.Equal(t, "MIT", p.VersionMetadata.License)
assert.Empty(t, p.FileMetadata.Checksum)
assert.Equal(t, "Gitea <pack@ag.er>", p.FileMetadata.Packager)
assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate)
assert.EqualValues(t, 123456, p.FileMetadata.Size)
assert.Equal(t, "aarch64", p.FileMetadata.Architecture)
assert.Equal(t, "origin", p.FileMetadata.Origin)
assert.Equal(t, "1111e709613fbc979651b09ac2bc27c6591a9999", p.FileMetadata.CommitHash)
assert.Equal(t, "value", p.FileMetadata.InstallIf)
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides)
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Dependencies)
})
}
+256
View File
@@ -0,0 +1,256 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"io"
"regexp"
"strconv"
"strings"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"github.com/klauspost/compress/zstd"
"github.com/ulikunitz/xz"
)
const (
PropertyRepository = "arch.repository"
PropertyArchitecture = "arch.architecture"
PropertySignature = "arch.signature"
PropertyMetadata = "arch.metadata"
SettingKeyPrivate = "arch.key.private"
SettingKeyPublic = "arch.key.public"
RepositoryPackage = "_arch"
RepositoryVersion = "_repository"
AnyArch = "any"
)
var (
ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing")
ErrUnsupportedFormat = util.NewInvalidArgumentErrorf("unsupported package container format")
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
// https://man.archlinux.org/man/PKGBUILD.5
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
// (epoch:pkgver-pkgrel)
versionPattern = regexp.MustCompile(`\A(?:\d:)?[\w.+~]+(?:-[-\w.+~]+)?\z`)
)
type Package struct {
Name string
Version string
VersionMetadata VersionMetadata
FileMetadata FileMetadata
FileCompressionExtension string
}
type VersionMetadata struct {
Description string `json:"description,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Licenses []string `json:"licenses,omitempty"`
}
type FileMetadata struct {
Architecture string `json:"architecture"`
Base string `json:"base,omitempty"`
InstalledSize int64 `json:"installed_size,omitempty"`
BuildDate int64 `json:"build_date,omitempty"`
Packager string `json:"packager,omitempty"`
Groups []string `json:"groups,omitempty"`
Provides []string `json:"provides,omitempty"`
Replaces []string `json:"replaces,omitempty"`
Depends []string `json:"depends,omitempty"`
OptDepends []string `json:"opt_depends,omitempty"`
MakeDepends []string `json:"make_depends,omitempty"`
CheckDepends []string `json:"check_depends,omitempty"`
Conflicts []string `json:"conflicts,omitempty"`
XData []string `json:"xdata,omitempty"`
Backup []string `json:"backup,omitempty"`
Files []string `json:"files,omitempty"`
}
// ParsePackage parses an Arch package file
func ParsePackage(r io.Reader) (*Package, error) {
header := make([]byte, 10)
n, err := util.ReadAtMost(r, header)
if err != nil {
return nil, err
}
r = io.MultiReader(bytes.NewReader(header[:n]), r)
var inner io.Reader
var compressionType string
if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst
zr, err := zstd.NewReader(r)
if err != nil {
return nil, err
}
defer zr.Close()
inner = zr
compressionType = "zst"
} else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz
xzr, err := xz.NewReader(r)
if err != nil {
return nil, err
}
inner = xzr
compressionType = "xz"
} else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gzr.Close()
inner = gzr
compressionType = "gz"
} else {
return nil, ErrUnsupportedFormat
}
var p *Package
files := make([]string, 0, 10)
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
}
filename := hd.FileInfo().Name()
if filename == ".PKGINFO" {
p, err = ParsePackageInfo(tr)
if err != nil {
return nil, err
}
} else if !strings.HasPrefix(filename, ".") {
files = append(files, hd.Name)
}
}
if p == nil {
return nil, ErrMissingPKGINFOFile
}
p.FileMetadata.Files = files
p.FileCompressionExtension = compressionType
return p, nil
}
// ParsePackageInfo parses a .PKGINFO file to retrieve the metadata
// https://man.archlinux.org/man/PKGBUILD.5
// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_package.c#L161
func ParsePackageInfo(r io.Reader) (*Package, error) {
p := &Package{}
s := bufio.NewScanner(r)
for s.Scan() {
line := s.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 "pkgbase":
p.FileMetadata.Base = value
case "pkgver":
p.Version = value
case "pkgdesc":
p.VersionMetadata.Description = value
case "url":
p.VersionMetadata.ProjectURL = value
case "packager":
p.FileMetadata.Packager = value
case "arch":
p.FileMetadata.Architecture = value
case "license":
p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value)
case "provides":
p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
case "depend":
p.FileMetadata.Depends = append(p.FileMetadata.Depends, value)
case "replaces":
p.FileMetadata.Replaces = append(p.FileMetadata.Replaces, value)
case "optdepend":
p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value)
case "makedepend":
p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value)
case "checkdepend":
p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value)
case "conflict":
p.FileMetadata.Conflicts = append(p.FileMetadata.Conflicts, value)
case "backup":
p.FileMetadata.Backup = append(p.FileMetadata.Backup, value)
case "group":
p.FileMetadata.Groups = append(p.FileMetadata.Groups, value)
case "builddate":
date, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, err
}
p.FileMetadata.BuildDate = date
case "size":
size, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, err
}
p.FileMetadata.InstalledSize = size
case "xdata":
p.FileMetadata.XData = append(p.FileMetadata.XData, value)
}
}
if err := s.Err(); err != nil {
return nil, err
}
if !namePattern.MatchString(p.Name) {
return nil, ErrInvalidName
}
if !versionPattern.MatchString(p.Version) {
return nil, ErrInvalidVersion
}
if p.FileMetadata.Architecture == "" {
return nil, ErrInvalidArchitecture
}
if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
p.VersionMetadata.ProjectURL = ""
}
return p, nil
}
+169
View File
@@ -0,0 +1,169 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"testing"
"github.com/klauspost/compress/zstd"
"github.com/stretchr/testify/assert"
"github.com/ulikunitz/xz"
)
const (
packageName = "gitea"
packageVersion = "1.0.1"
packageDescription = "Package Description"
packageProjectURL = "https://gitea.com"
packagePackager = "KN4CK3R <packager@gitea.com>"
)
func createPKGINFOContent(name, version string) []byte {
return []byte(`pkgname = ` + name + `
pkgbase = ` + name + `
pkgver = ` + version + `
pkgdesc = ` + packageDescription + `
url = ` + packageProjectURL + `
# comment
group=group
builddate = 1678834800
size = 123456
arch = x86_64
license = MIT
packager = ` + packagePackager + `
depend = common
xdata = value
depend = gitea
provides = common
provides = gitea
optdepend = hex
replaces = gogs
checkdepend = common
makedepend = cmake
conflict = ninja
backup = usr/bin/paket1`)
}
func TestParsePackage(t *testing.T) {
createPackage := func(compression string, files map[string][]byte) io.Reader {
var buf bytes.Buffer
var cw io.WriteCloser
switch compression {
case "zst":
cw, _ = zstd.NewWriter(&buf)
case "xz":
cw, _ = xz.NewWriter(&buf)
case "gz":
cw = gzip.NewWriter(&buf)
}
tw := tar.NewWriter(cw)
for name, content := range files {
hdr := &tar.Header{
Name: name,
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write(content)
}
tw.Close()
cw.Close()
return &buf
}
for _, c := range []string{"gz", "xz", "zst"} {
t.Run(c, func(t *testing.T) {
t.Run("MissingPKGINFOFile", func(t *testing.T) {
data := createPackage(c, map[string][]byte{"dummy.txt": {}})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrMissingPKGINFOFile)
})
t.Run("InvalidPKGINFOFile", func(t *testing.T) {
data := createPackage(c, map[string][]byte{".PKGINFO": {}})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrInvalidName)
})
t.Run("Valid", func(t *testing.T) {
data := createPackage(c, map[string][]byte{
".PKGINFO": createPKGINFOContent(packageName, packageVersion),
"/test/dummy.txt": {},
})
p, err := ParsePackage(data)
assert.NoError(t, err)
assert.NotNil(t, p)
assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files)
})
})
}
}
func TestParsePackageInfo(t *testing.T) {
t.Run("InvalidName", func(t *testing.T) {
data := createPKGINFOContent("", packageVersion)
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidName)
})
t.Run("Regexp", func(t *testing.T) {
assert.Regexp(t, versionPattern, "1.2_3~4+5")
assert.Regexp(t, versionPattern, "1:2_3~4+5")
assert.NotRegexp(t, versionPattern, "a:1.0.0-1")
assert.NotRegexp(t, versionPattern, "0.0.1/1-1")
assert.NotRegexp(t, versionPattern, "1.0.0 -1")
})
t.Run("InvalidVersion", func(t *testing.T) {
data := createPKGINFOContent(packageName, "")
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidVersion)
})
t.Run("Valid", func(t *testing.T) {
data := createPKGINFOContent(packageName, packageVersion)
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageName, p.FileMetadata.Base)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, packageDescription, p.VersionMetadata.Description)
assert.Equal(t, packagePackager, p.FileMetadata.Packager)
assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL)
assert.ElementsMatch(t, []string{"MIT"}, p.VersionMetadata.Licenses)
assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate)
assert.EqualValues(t, 123456, p.FileMetadata.InstalledSize)
assert.Equal(t, "x86_64", p.FileMetadata.Architecture)
assert.ElementsMatch(t, []string{"value"}, p.FileMetadata.XData)
assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups)
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides)
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends)
assert.ElementsMatch(t, []string{"gogs"}, p.FileMetadata.Replaces)
assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends)
assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends)
assert.ElementsMatch(t, []string{"ninja"}, p.FileMetadata.Conflicts)
assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends)
assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup)
})
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cargo
import (
"encoding/binary"
"errors"
"io"
"regexp"
"gitea.dev/modules/json"
"gitea.dev/modules/validation"
"github.com/hashicorp/go-version"
)
const PropertyYanked = "cargo.yanked"
var (
ErrInvalidName = errors.New("package name is invalid")
ErrInvalidVersion = errors.New("package version is invalid")
)
// Package represents a Cargo package
type Package struct {
Name string
Version string
Metadata *Metadata
Content io.Reader
ContentSize int64
}
// Metadata represents the metadata of a Cargo package
type Metadata struct {
Dependencies []*Dependency `json:"dependencies,omitempty"`
Features map[string][]string `json:"features,omitempty"`
Authors []string `json:"authors,omitempty"`
Description string `json:"description,omitempty"`
DocumentationURL string `json:"documentation_url,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Readme string `json:"readme,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Categories []string `json:"categories,omitempty"`
License string `json:"license,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
Links string `json:"links,omitempty"`
}
type Dependency struct {
Name string `json:"name"`
Req string `json:"req"`
Features []string `json:"features"`
Optional bool `json:"optional"`
DefaultFeatures bool `json:"default_features"`
Target *string `json:"target"`
Kind string `json:"kind"`
Registry *string `json:"registry"`
Package *string `json:"package"`
}
var nameMatch = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9-_]{0,63}\z`)
// ParsePackage reads the metadata and content of a package
func ParsePackage(r io.Reader) (*Package, error) {
var size uint32
if err := binary.Read(r, binary.LittleEndian, &size); err != nil {
return nil, err
}
p, err := parsePackage(io.LimitReader(r, int64(size)))
if err != nil {
return nil, err
}
if err := binary.Read(r, binary.LittleEndian, &size); err != nil {
return nil, err
}
p.Content = io.LimitReader(r, int64(size))
p.ContentSize = int64(size)
return p, nil
}
func parsePackage(r io.Reader) (*Package, error) {
var meta struct {
Name string `json:"name"`
Vers string `json:"vers"`
Deps []struct {
Name string `json:"name"`
VersionReq string `json:"version_req"`
Features []string `json:"features"`
Optional bool `json:"optional"`
DefaultFeatures bool `json:"default_features"`
Target *string `json:"target"`
Kind string `json:"kind"`
Registry *string `json:"registry"`
ExplicitNameInToml string `json:"explicit_name_in_toml"`
} `json:"deps"`
Features map[string][]string `json:"features"`
Authors []string `json:"authors"`
Description string `json:"description"`
Documentation string `json:"documentation"`
Homepage string `json:"homepage"`
Readme string `json:"readme"`
ReadmeFile string `json:"readme_file"`
Keywords []string `json:"keywords"`
Categories []string `json:"categories"`
License string `json:"license"`
LicenseFile string `json:"license_file"`
Repository string `json:"repository"`
Links string `json:"links"`
}
if err := json.NewDecoder(r).Decode(&meta); err != nil {
return nil, err
}
if !nameMatch.MatchString(meta.Name) {
return nil, ErrInvalidName
}
if _, err := version.NewSemver(meta.Vers); err != nil {
return nil, ErrInvalidVersion
}
if !validation.IsValidURL(meta.Homepage) {
meta.Homepage = ""
}
if !validation.IsValidURL(meta.Documentation) {
meta.Documentation = ""
}
if !validation.IsValidURL(meta.Repository) {
meta.Repository = ""
}
dependencies := make([]*Dependency, 0, len(meta.Deps))
for _, dep := range meta.Deps {
// https://doc.rust-lang.org/cargo/reference/registry-web-api.html#publish
// It is a string of the new package name if the dependency is renamed, otherwise empty
name := dep.ExplicitNameInToml
pkg := &dep.Name
if name == "" {
name = dep.Name
pkg = nil
}
dependencies = append(dependencies, &Dependency{
Name: name,
Req: dep.VersionReq,
Features: dep.Features,
Optional: dep.Optional,
DefaultFeatures: dep.DefaultFeatures,
Target: dep.Target,
Kind: dep.Kind,
Registry: dep.Registry,
Package: pkg,
})
}
return &Package{
Name: meta.Name,
Version: meta.Vers,
Metadata: &Metadata{
Dependencies: dependencies,
Features: meta.Features,
Authors: meta.Authors,
Description: meta.Description,
DocumentationURL: meta.Documentation,
ProjectURL: meta.Homepage,
Readme: meta.Readme,
Keywords: meta.Keywords,
Categories: meta.Categories,
License: meta.License,
RepositoryURL: meta.Repository,
Links: meta.Links,
},
}, nil
}
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cargo
import (
"bytes"
"encoding/binary"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParsePackage(t *testing.T) {
const (
description = "Package Description"
author = "KN4CK3R"
homepage = "https://gitea.io/"
license = "MIT"
payload = "gitea test dummy payload" // a fake payload for test only
)
makeDefaultPackageMeta := func(name, version string) string {
return `{
"name":"` + name + `",
"vers":"` + version + `",
"description":"` + description + `",
"authors": ["` + author + `"],
"deps":[
{
"name":"dep",
"version_req":"1.0"
}
],
"homepage":"` + homepage + `",
"license":"` + license + `"
}`
}
createPackage := func(metadata string) io.Reader {
var buf bytes.Buffer
binary.Write(&buf, binary.LittleEndian, uint32(len(metadata)))
buf.WriteString(metadata)
binary.Write(&buf, binary.LittleEndian, uint32(len(payload)))
buf.WriteString(payload)
return &buf
}
t.Run("InvalidName", func(t *testing.T) {
for _, name := range []string{"", "0test", "-test", "_test", strings.Repeat("a", 65)} {
data := createPackage(makeDefaultPackageMeta(name, "1.0.0"))
cp, err := ParsePackage(data)
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidName)
}
})
t.Run("InvalidVersion", func(t *testing.T) {
for _, version := range []string{"", "1.", "-1.0", "1.0.0/1"} {
data := createPackage(makeDefaultPackageMeta("test", version))
cp, err := ParsePackage(data)
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidVersion)
}
})
t.Run("Valid", func(t *testing.T) {
data := createPackage(makeDefaultPackageMeta("test", "1.0.0"))
cp, err := ParsePackage(data)
assert.NotNil(t, cp)
assert.NoError(t, err)
assert.Equal(t, "test", cp.Name)
assert.Equal(t, "1.0.0", cp.Version)
assert.Equal(t, description, cp.Metadata.Description)
assert.Equal(t, []string{author}, cp.Metadata.Authors)
assert.Len(t, cp.Metadata.Dependencies, 1)
assert.Equal(t, "dep", cp.Metadata.Dependencies[0].Name)
assert.Nil(t, cp.Metadata.Dependencies[0].Package)
assert.Equal(t, homepage, cp.Metadata.ProjectURL)
assert.Equal(t, license, cp.Metadata.License)
content, _ := io.ReadAll(cp.Content)
assert.Equal(t, payload, string(content))
})
t.Run("Renamed", func(t *testing.T) {
data := createPackage(`{
"name":"test-pkg",
"vers":"1.0",
"description":"test-desc",
"authors": ["test-author"],
"deps":[
{
"name":"dep-renamed",
"explicit_name_in_toml":"dep-explicit",
"version_req":"1.0"
}
],
"homepage":"https://gitea.io/",
"license":"MIT"
}`)
cp, err := ParsePackage(data)
assert.NoError(t, err)
assert.Equal(t, "test-pkg", cp.Name)
assert.Equal(t, "https://gitea.io/", cp.Metadata.ProjectURL)
assert.Equal(t, "dep-explicit", cp.Metadata.Dependencies[0].Name)
assert.Equal(t, "dep-renamed", *cp.Metadata.Dependencies[0].Package)
})
}
+134
View File
@@ -0,0 +1,134 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package chef
import (
"archive/tar"
"compress/gzip"
"io"
"regexp"
"strings"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
)
const (
KeyBits = 4096
SettingPublicPem = "chef.public_pem"
)
var (
ErrMissingMetadataFile = util.NewInvalidArgumentErrorf("metadata.json file is missing")
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
namePattern = regexp.MustCompile(`\A\S+\z`)
versionPattern = regexp.MustCompile(`\A\d+\.\d+(?:\.\d+)?\z`)
)
// Package represents a Chef package
type Package struct {
Name string
Version string
Metadata *Metadata
}
// Metadata represents the metadata of a Chef package
type Metadata struct {
Description string `json:"description,omitempty"`
LongDescription string `json:"long_description,omitempty"`
Author string `json:"author,omitempty"`
License string `json:"license,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"`
}
type chefMetadata struct {
Name string `json:"name"`
Description string `json:"description"`
LongDescription string `json:"long_description"`
Maintainer string `json:"maintainer"`
MaintainerEmail string `json:"maintainer_email"`
License string `json:"license"`
Platforms map[string]string `json:"platforms"`
Dependencies map[string]string `json:"dependencies"`
Providing map[string]string `json:"providing"`
Recipes map[string]string `json:"recipes"`
Version string `json:"version"`
SourceURL string `json:"source_url"`
IssuesURL string `json:"issues_url"`
Privacy bool `json:"privacy"`
ChefVersions [][]string `json:"chef_versions"`
Gems [][]string `json:"gems"`
EagerLoadLibraries bool `json:"eager_load_libraries"`
}
// ParsePackage parses the Chef package file
func ParsePackage(r io.Reader) (*Package, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.Typeflag != tar.TypeReg {
continue
}
if strings.Count(hd.Name, "/") != 1 {
continue
}
if hd.FileInfo().Name() == "metadata.json" {
return ParseChefMetadata(tr)
}
}
return nil, ErrMissingMetadataFile
}
// ParseChefMetadata parses a metadata.json file to retrieve the metadata of a Chef package
func ParseChefMetadata(r io.Reader) (*Package, error) {
var cm chefMetadata
if err := json.NewDecoder(r).Decode(&cm); err != nil {
return nil, err
}
if !namePattern.MatchString(cm.Name) {
return nil, ErrInvalidName
}
if !versionPattern.MatchString(cm.Version) {
return nil, ErrInvalidVersion
}
if !validation.IsValidURL(cm.SourceURL) {
cm.SourceURL = ""
}
return &Package{
Name: cm.Name,
Version: cm.Version,
Metadata: &Metadata{
Description: cm.Description,
LongDescription: cm.LongDescription,
Author: cm.Maintainer,
License: cm.License,
RepositoryURL: cm.SourceURL,
Dependencies: cm.Dependencies,
},
}, nil
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package chef
import (
"archive/tar"
"bytes"
"compress/gzip"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
packageName = "gitea"
packageVersion = "1.0.1"
packageAuthor = "KN4CK3R"
packageDescription = "Package Description"
packageRepositoryURL = "https://gitea.io/gitea/gitea"
)
func TestParsePackage(t *testing.T) {
t.Run("MissingMetadataFile", func(t *testing.T) {
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
tw := tar.NewWriter(zw)
tw.Close()
zw.Close()
p, err := ParsePackage(&buf)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrMissingMetadataFile)
})
t.Run("Valid", func(t *testing.T) {
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
tw := tar.NewWriter(zw)
content := `{"name":"` + packageName + `","version":"` + packageVersion + `"}`
hdr := &tar.Header{
Name: packageName + "/metadata.json",
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write([]byte(content))
tw.Close()
zw.Close()
p, err := ParsePackage(&buf)
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.NotNil(t, p.Metadata)
})
}
func TestParseChefMetadata(t *testing.T) {
t.Run("InvalidName", func(t *testing.T) {
for _, name := range []string{" test", "test "} {
p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + name + `","version":"1.0.0"}`))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidName)
}
})
t.Run("InvalidVersion", func(t *testing.T) {
for _, version := range []string{"1", "1.2.3.4", "1.0.0 "} {
p, err := ParseChefMetadata(strings.NewReader(`{"name":"test","version":"` + version + `"}`))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidVersion)
}
})
t.Run("Valid", func(t *testing.T) {
p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + packageName + `","version":"` + packageVersion + `","description":"` + packageDescription + `","maintainer":"` + packageAuthor + `","source_url":"` + packageRepositoryURL + `"}`))
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, packageDescription, p.Metadata.Description)
assert.Equal(t, packageAuthor, p.Metadata.Author)
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
})
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package composer
import (
"archive/tar"
"archive/zip"
"compress/bzip2"
"compress/gzip"
"errors"
"io"
"io/fs"
"path"
"regexp"
"strings"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"github.com/hashicorp/go-version"
)
// TypeProperty is the name of the property for Composer package types
const TypeProperty = "composer.type"
var (
// ErrMissingComposerFile indicates a missing composer.json file
ErrMissingComposerFile = util.NewInvalidArgumentErrorf("composer.json file is missing")
// ErrInvalidName indicates an invalid package name
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
// ErrInvalidVersion indicates an invalid package version
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
// PackageInfo represents Composer package info
type PackageInfo struct {
Filename string
Name string
Version string
Type string
Metadata *Metadata
}
// https://getcomposer.org/doc/04-schema.md
// Metadata represents the metadata of a Composer package
type Metadata struct {
Description string `json:"description,omitempty"`
Readme string `json:"readme,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Comments Comments `json:"_comment,omitempty"`
Homepage string `json:"homepage,omitempty"`
License Licenses `json:"license,omitempty"`
Authors []Author `json:"authors,omitempty"`
Bin []string `json:"bin,omitempty"`
Autoload map[string]any `json:"autoload,omitempty"`
AutoloadDev map[string]any `json:"autoload-dev,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
Require map[string]string `json:"require,omitempty"`
RequireDev map[string]string `json:"require-dev,omitempty"`
Suggest map[string]string `json:"suggest,omitempty"`
Provide map[string]string `json:"provide,omitempty"`
}
// Licenses represents the licenses of a Composer package
type Licenses []string
// UnmarshalJSON reads from a string or array
func (l *Licenses) UnmarshalJSON(data []byte) error {
switch data[0] {
case '"':
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*l = Licenses{value}
case '[':
values := make([]string, 0, 5)
if err := json.Unmarshal(data, &values); err != nil {
return err
}
*l = values
}
return nil
}
// Comments represents the comments of a Composer package
type Comments []string
// UnmarshalJSON reads from a string or array
func (c *Comments) UnmarshalJSON(data []byte) error {
switch data[0] {
case '"':
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*c = Comments{value}
case '[':
values := make([]string, 0, 5)
if err := json.Unmarshal(data, &values); err != nil {
return err
}
*c = values
}
return nil
}
// Author represents an author
type Author struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Homepage string `json:"homepage,omitempty"`
}
var nameMatch = regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`)
type ReadSeekAt interface {
io.Reader
io.ReaderAt
io.Seeker
Size() int64
}
func readPackageFileZip(r ReadSeekAt, filename string, limit int) ([]byte, error) {
archive, err := zip.NewReader(r, r.Size())
if err != nil {
return nil, err
}
for _, file := range archive.File {
filePath := path.Clean(file.Name)
if util.AsciiEqualFold(filePath, filename) {
f, err := archive.Open(file.Name)
if err != nil {
return nil, err
}
defer f.Close()
return util.ReadWithLimit(f, limit)
}
}
return nil, fs.ErrNotExist
}
func readPackageFileTar(r io.Reader, filename string, limit int) ([]byte, error) {
tarReader := tar.NewReader(r)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
filePath := path.Clean(header.Name)
if util.AsciiEqualFold(filePath, filename) {
return util.ReadWithLimit(tarReader, limit)
}
}
return nil, fs.ErrNotExist
}
const (
pkgExtZip = ".zip"
pkgExtTarGz = ".tar.gz"
pkgExtTarBz2 = ".tar.bz2"
)
func detectPackageExtName(r ReadSeekAt) (string, error) {
headBytes := make([]byte, 4)
_, err := r.ReadAt(headBytes, 0)
if err != nil {
return "", err
}
_, err = r.Seek(0, io.SeekStart)
if err != nil {
return "", err
}
switch {
case headBytes[0] == 'P' && headBytes[1] == 'K':
return pkgExtZip, nil
case string(headBytes[:3]) == "BZh":
return pkgExtTarBz2, nil
case headBytes[0] == 0x1f && headBytes[1] == 0x8b:
return pkgExtTarGz, nil
}
return "", util.NewInvalidArgumentErrorf("not a valid package file")
}
func readPackageFile(pkgExt string, r ReadSeekAt, filename string, limit int) ([]byte, error) {
_, err := r.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
switch pkgExt {
case pkgExtZip:
return readPackageFileZip(r, filename, limit)
case pkgExtTarBz2:
bzip2Reader := bzip2.NewReader(r)
return readPackageFileTar(bzip2Reader, filename, limit)
case pkgExtTarGz:
gzReader, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
return readPackageFileTar(gzReader, filename, limit)
}
return nil, util.NewInvalidArgumentErrorf("not a valid package file")
}
// ParsePackage parses the metadata of a Composer package file
func ParsePackage(r ReadSeekAt, optVersion ...string) (*PackageInfo, error) {
pkgExt, err := detectPackageExtName(r)
if err != nil {
return nil, err
}
dataComposerJSON, err := readPackageFile(pkgExt, r, "composer.json", 10*1024*1024)
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrMissingComposerFile
} else if err != nil {
return nil, err
}
var cj struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Metadata
}
if err := json.Unmarshal(dataComposerJSON, &cj); err != nil {
return nil, err
}
if !nameMatch.MatchString(cj.Name) {
return nil, ErrInvalidName
}
if cj.Version == "" {
cj.Version = util.OptionalArg(optVersion)
}
if cj.Version != "" {
if _, err := version.NewSemver(cj.Version); err != nil {
return nil, ErrInvalidVersion
}
}
if !validation.IsValidURL(cj.Homepage) {
cj.Homepage = ""
}
if cj.Type == "" {
cj.Type = "library"
}
if cj.Readme == "" {
cj.Readme = "README.md"
}
dataReadmeMd, _ := readPackageFile(pkgExt, r, cj.Readme, 10*1024)
// FIXME: legacy problem, the "Readme" field is abused, it should always be the path to the readme file
if len(dataReadmeMd) == 0 {
cj.Readme = ""
} else {
cj.Readme = string(dataReadmeMd)
}
// FIXME: legacy format: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)), doesn't read good
pkgFilename := strings.ReplaceAll(cj.Name, "/", "-")
if cj.Version != "" {
pkgFilename += "." + cj.Version
}
pkgFilename += pkgExt
return &PackageInfo{
Filename: pkgFilename,
Name: cj.Name,
Version: cj.Version,
Type: cj.Type,
Metadata: &cj.Metadata,
}, nil
}
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package composer
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"io"
"strings"
"testing"
"gitea.dev/modules/json"
"github.com/dsnet/compress/bzip2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
name = "gitea/composer-package"
description = "Package Description"
readme = "Package Readme"
comments = "Package Comment"
packageType = "composer-plugin"
author = "Gitea Authors"
email = "no.reply@gitea.io"
homepage = "https://gitea.io"
license = "MIT"
)
func buildComposerContent(version string) string {
return `{
"name": "` + name + `",
"version": "` + version + `",
"description": "` + description + `",
"type": "` + packageType + `",
"license": "` + license + `",
"authors": [
{
"name": "` + author + `",
"email": "` + email + `"
}
],
"homepage": "` + homepage + `",
"autoload": {
"psr-4": {"Gitea\\ComposerPackage\\": "src/"}
},
"require": {
"php": ">=7.2 || ^8.0"
},
"_comment": "` + comments + `"
}`
}
func TestLicenseUnmarshal(t *testing.T) {
var l Licenses
assert.NoError(t, json.NewDecoder(strings.NewReader(`["MIT"]`)).Decode(&l))
assert.Len(t, l, 1)
assert.Equal(t, "MIT", l[0])
assert.NoError(t, json.NewDecoder(strings.NewReader(`"MIT"`)).Decode(&l))
assert.Len(t, l, 1)
assert.Equal(t, "MIT", l[0])
}
func TestCommentsUnmarshal(t *testing.T) {
var c Comments
assert.NoError(t, json.NewDecoder(strings.NewReader(`["comment"]`)).Decode(&c))
assert.Len(t, c, 1)
assert.Equal(t, "comment", c[0])
assert.NoError(t, json.NewDecoder(strings.NewReader(`"comment"`)).Decode(&c))
assert.Len(t, c, 1)
assert.Equal(t, "comment", c[0])
}
func TestParsePackage(t *testing.T) {
createArchive := func(files map[string]string) []byte {
var buf bytes.Buffer
archive := zip.NewWriter(&buf)
for name, content := range files {
w, _ := archive.Create(name)
_, _ = w.Write([]byte(content))
}
_ = archive.Close()
return buf.Bytes()
}
createArchiveTar := func(comp func(io.Writer) io.WriteCloser, files map[string]string) []byte {
var buf bytes.Buffer
w := comp(&buf)
archive := tar.NewWriter(w)
for name, content := range files {
hdr := &tar.Header{
Name: name,
Mode: 0o600,
Size: int64(len(content)),
}
_ = archive.WriteHeader(hdr)
_, _ = archive.Write([]byte(content))
}
_ = w.Close()
_ = archive.Close()
return buf.Bytes()
}
t.Run("MissingComposerFile", func(t *testing.T) {
data := createArchive(map[string]string{"dummy.txt": ""})
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrMissingComposerFile)
})
t.Run("MissingComposerFileInRoot", func(t *testing.T) {
data := createArchive(map[string]string{"sub/sub/composer.json": ""})
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrMissingComposerFile)
})
t.Run("InvalidComposerFile", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": ""})
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.Error(t, err)
})
t.Run("InvalidPackageName", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": "{}"})
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidName)
})
t.Run("InvalidPackageVersion", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`})
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidVersion)
})
t.Run("InvalidReadmePath", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`})
cp, err := ParsePackage(bytes.NewReader(data))
assert.NoError(t, err)
assert.NotNil(t, cp)
assert.Empty(t, cp.Metadata.Readme)
})
assertValidPackage := func(t *testing.T, data []byte, version, filename string) {
cp, err := ParsePackage(bytes.NewReader(data))
require.NoError(t, err)
assert.NotNil(t, cp)
assert.Equal(t, filename, cp.Filename)
assert.Equal(t, name, cp.Name)
assert.Equal(t, version, cp.Version)
assert.Equal(t, description, cp.Metadata.Description)
assert.Equal(t, readme, cp.Metadata.Readme)
assert.Len(t, cp.Metadata.Comments, 1)
assert.Equal(t, comments, cp.Metadata.Comments[0])
assert.Len(t, cp.Metadata.Authors, 1)
assert.Equal(t, author, cp.Metadata.Authors[0].Name)
assert.Equal(t, email, cp.Metadata.Authors[0].Email)
assert.Equal(t, homepage, cp.Metadata.Homepage)
assert.Equal(t, packageType, cp.Type)
assert.Len(t, cp.Metadata.License, 1)
assert.Equal(t, license, cp.Metadata.License[0])
}
t.Run("ValidZip", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": buildComposerContent(""), "README.md": readme})
assertValidPackage(t, data, "", "gitea-composer-package.zip")
})
t.Run("ValidTarBz2", func(t *testing.T) {
data := createArchiveTar(func(w io.Writer) io.WriteCloser {
bz2Writer, _ := bzip2.NewWriter(w, nil)
return bz2Writer
}, map[string]string{"composer.json": buildComposerContent("1.0"), "README.md": readme})
assertValidPackage(t, data, "1.0", "gitea-composer-package.1.0.tar.bz2")
})
t.Run("ValidTarGz", func(t *testing.T) {
data := createArchiveTar(func(w io.Writer) io.WriteCloser {
return gzip.NewWriter(w)
}, map[string]string{"composer.json": buildComposerContent(""), "README.md": readme})
assertValidPackage(t, data, "", "gitea-composer-package.tar.gz")
})
}
@@ -0,0 +1,67 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
"io"
"regexp"
"strings"
)
var (
patternAuthor = compilePattern("author")
patternHomepage = compilePattern("homepage")
patternURL = compilePattern("url")
patternLicense = compilePattern("license")
patternDescription = compilePattern("description")
patternTopics = regexp.MustCompile(`(?im)^\s*topics\s*=\s*\((.+)\)`)
patternTopicList = regexp.MustCompile(`\s*['"](.+?)['"]\s*,?`)
)
func compilePattern(name string) *regexp.Regexp {
return regexp.MustCompile(`(?im)^\s*` + name + `\s*=\s*['"\(](.+)['"\)]`)
}
func ParseConanfile(r io.Reader) (*Metadata, error) {
buf, err := io.ReadAll(io.LimitReader(r, 1<<20))
if err != nil {
return nil, err
}
metadata := &Metadata{}
m := patternAuthor.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.Author = string(m[1])
}
m = patternHomepage.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.ProjectURL = string(m[1])
}
m = patternURL.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.RepositoryURL = string(m[1])
}
m = patternLicense.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.License = strings.ReplaceAll(strings.ReplaceAll(string(m[1]), "'", ""), "\"", "")
}
m = patternDescription.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.Description = string(m[1])
}
m = patternTopics.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
m2 := patternTopicList.FindAllSubmatch(m[1], -1)
if len(m2) > 0 {
metadata.Keywords = make([]string, 0, len(m2))
for _, g := range m2 {
if len(g) > 1 {
metadata.Keywords = append(metadata.Keywords, string(g[1]))
}
}
}
}
return metadata, nil
}
@@ -0,0 +1,50 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
name = "ConanPackage"
version = "1.2"
license = "MIT"
author = "Gitea <info@gitea.io>"
homepage = "https://gitea.io/"
url = "https://gitea.com/"
description = "Description of ConanPackage"
topic1 = "gitea"
topic2 = "conan"
contentConanfile = `from conans import ConanFile, CMake, tools
class ConanPackageConan(ConanFile):
name = "` + name + `"
version = "` + version + `"
license = "` + license + `"
author = "` + author + `"
homepage = "` + homepage + `"
url = "` + url + `"
description = "` + description + `"
topics = ("` + topic1 + `", "` + topic2 + `")
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False], "fPIC": [True, False]}
default_options = {"shared": False, "fPIC": True}
generators = "cmake"
`
)
func TestParseConanfile(t *testing.T) {
metadata, err := ParseConanfile(strings.NewReader(contentConanfile))
assert.NoError(t, err)
assert.Equal(t, license, metadata.License)
assert.Equal(t, author, metadata.Author)
assert.Equal(t, homepage, metadata.ProjectURL)
assert.Equal(t, url, metadata.RepositoryURL)
assert.Equal(t, description, metadata.Description)
assert.Equal(t, []string{topic1, topic2}, metadata.Keywords)
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
"bufio"
"io"
"strings"
"gitea.dev/modules/util"
)
// Conaninfo represents infos of a Conan package
type Conaninfo struct {
Settings map[string]string `json:"settings"`
FullSettings map[string]string `json:"full_settings"`
Requires []string `json:"requires"`
FullRequires []string `json:"full_requires"`
Options map[string]string `json:"options"`
FullOptions map[string]string `json:"full_options"`
RecipeHash string `json:"recipe_hash"`
Environment map[string][]string `json:"environment"`
}
func ParseConaninfo(r io.Reader) (*Conaninfo, error) {
sections, err := readSections(io.LimitReader(r, 1<<20))
if err != nil {
return nil, err
}
info := &Conaninfo{}
for section, lines := range sections {
if len(lines) == 0 {
continue
}
switch section {
case "settings":
info.Settings = toMap(lines)
case "full_settings":
info.FullSettings = toMap(lines)
case "options":
info.Options = toMap(lines)
case "full_options":
info.FullOptions = toMap(lines)
case "requires":
info.Requires = lines
case "full_requires":
info.FullRequires = lines
case "recipe_hash":
info.RecipeHash = lines[0]
case "env":
info.Environment = toMapArray(lines)
}
}
return info, nil
}
func readSections(r io.Reader) (map[string][]string, error) {
sections := make(map[string][]string)
section := ""
lines := make([]string, 0, 5)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
if section != "" {
sections[section] = lines
}
section = line[1 : len(line)-1]
lines = make([]string, 0, 5)
continue
}
if section != "" {
if line != "" {
lines = append(lines, line)
}
continue
}
if line != "" {
return nil, util.NewInvalidArgumentErrorf("invalid conaninfo.txt")
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
if section != "" {
sections[section] = lines
}
return sections, nil
}
func toMap(lines []string) map[string]string {
result := make(map[string]string)
for _, line := range lines {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
continue
}
result[parts[0]] = parts[1]
}
return result
}
func toMapArray(lines []string) map[string][]string {
result := make(map[string][]string)
for _, line := range lines {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
continue
}
var items []string
if strings.HasPrefix(parts[1], "[") && strings.HasSuffix(parts[1], "]") {
items = strings.Split(parts[1], ",")
} else {
items = []string{parts[1]}
}
result[parts[0]] = items
}
return result
}
@@ -0,0 +1,84 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
settingsKey = "arch"
settingsValue = "x84_64"
optionsKey = "shared"
optionsValue = "False"
requires = "fmt/7.1.3"
hash = "74714915a51073acb548ca1ce29afbac"
envKey = "CC"
envValue = "gcc-10"
contentConaninfo = `[settings]
` + settingsKey + `=` + settingsValue + `
[requires]
` + requires + `
[options]
` + optionsKey + `=` + optionsValue + `
[full_settings]
` + settingsKey + `=` + settingsValue + `
[full_requires]
` + requires + `
[full_options]
` + optionsKey + `=` + optionsValue + `
[recipe_hash]
` + hash + `
[env]
` + envKey + `=` + envValue + `
`
)
func TestParseConaninfo(t *testing.T) {
info, err := ParseConaninfo(strings.NewReader(contentConaninfo))
assert.NotNil(t, info)
assert.NoError(t, err)
assert.Equal(
t,
map[string]string{
settingsKey: settingsValue,
},
info.Settings,
)
assert.Equal(t, info.Settings, info.FullSettings)
assert.Equal(
t,
map[string]string{
optionsKey: optionsValue,
},
info.Options,
)
assert.Equal(t, info.Options, info.FullOptions)
assert.Equal(
t,
[]string{requires},
info.Requires,
)
assert.Equal(t, info.Requires, info.FullRequires)
assert.Equal(t, hash, info.RecipeHash)
assert.Equal(
t,
map[string][]string{
envKey: {envValue},
},
info.Environment,
)
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
const (
PropertyRecipeUser = "conan.recipe.user"
PropertyRecipeChannel = "conan.recipe.channel"
PropertyRecipeRevision = "conan.recipe.revision"
PropertyPackageReference = "conan.package.reference"
PropertyPackageRevision = "conan.package.revision"
PropertyPackageInfo = "conan.package.info"
)
// Metadata represents the metadata of a Conan package
type Metadata struct {
Author string `json:"author,omitempty"`
License string `json:"license,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
Description string `json:"description,omitempty"`
Keywords []string `json:"keywords,omitempty"`
}
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
"fmt"
"regexp"
"strings"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
)
const (
// taken from https://github.com/conan-io/conan/blob/develop/conans/model/ref.py
minChars = 2
maxChars = 51
// DefaultRevision if no revision is specified
DefaultRevision = "0"
)
var (
namePattern = regexp.MustCompile(fmt.Sprintf(`^[a-zA-Z0-9_][a-zA-Z0-9_\+\.-]{%d,%d}$`, minChars-1, maxChars-1))
revisionPattern = regexp.MustCompile(fmt.Sprintf(`^[a-zA-Z0-9]{1,%d}$`, maxChars))
ErrValidation = util.NewInvalidArgumentErrorf("could not validate one or more reference fields")
)
// RecipeReference represents a recipe <Name>/<Version>@<User>/<Channel>#<Revision>
type RecipeReference struct {
Name string
Version string
User string
Channel string
Revision string
}
func NewRecipeReference(name, version, user, channel, revision string) (*RecipeReference, error) {
log.Trace("Conan Recipe: %s/%s(@%s/%s(#%s))", name, version, user, channel, revision)
if user == "_" {
user = ""
}
if channel == "_" {
channel = ""
}
if (user != "" && channel == "") || (user == "" && channel != "") {
return nil, ErrValidation
}
if !namePattern.MatchString(name) {
return nil, ErrValidation
}
v := strings.TrimSpace(version)
if v == "" {
return nil, ErrValidation
}
if user != "" && !namePattern.MatchString(user) {
return nil, ErrValidation
}
if channel != "" && !namePattern.MatchString(channel) {
return nil, ErrValidation
}
if revision != "" && !revisionPattern.MatchString(revision) {
return nil, ErrValidation
}
return &RecipeReference{name, v, user, channel, revision}, nil
}
func (r *RecipeReference) RevisionOrDefault() string {
if r.Revision == "" {
return DefaultRevision
}
return r.Revision
}
func (r *RecipeReference) String() string {
rev := ""
if r.Revision != "" {
rev = "#" + r.Revision
}
if r.User == "" || r.Channel == "" {
return fmt.Sprintf("%s/%s%s", r.Name, r.Version, rev)
}
return fmt.Sprintf("%s/%s@%s/%s%s", r.Name, r.Version, r.User, r.Channel, rev)
}
func (r *RecipeReference) LinkName() string {
user := r.User
if user == "" {
user = "_"
}
channel := r.Channel
if channel == "" {
channel = "_"
}
return fmt.Sprintf("%s/%s/%s/%s/%s", r.Name, r.Version, user, channel, r.RevisionOrDefault())
}
func (r *RecipeReference) WithRevision(revision string) *RecipeReference {
return &RecipeReference{r.Name, r.Version, r.User, r.Channel, revision}
}
// AsKey builds the additional key for the package file
func (r *RecipeReference) AsKey() string {
return fmt.Sprintf("%s|%s|%s", r.User, r.Channel, r.RevisionOrDefault())
}
// PackageReference represents a package of a recipe <Name>/<Version>@<User>/<Channel>#<Revision> <Reference>#<Revision>
type PackageReference struct {
Recipe *RecipeReference
Reference string
Revision string
}
func NewPackageReference(recipe *RecipeReference, reference, revision string) (*PackageReference, error) {
log.Trace("Conan Package: %v %s(#%s)", recipe, reference, revision)
if recipe == nil {
return nil, ErrValidation
}
if reference == "" || !revisionPattern.MatchString(reference) {
return nil, ErrValidation
}
if revision != "" && !revisionPattern.MatchString(revision) {
return nil, ErrValidation
}
return &PackageReference{recipe, reference, revision}, nil
}
func (r *PackageReference) RevisionOrDefault() string {
if r.Revision == "" {
return DefaultRevision
}
return r.Revision
}
func (r *PackageReference) LinkName() string {
return fmt.Sprintf("%s/%s", r.Reference, r.RevisionOrDefault())
}
func (r *PackageReference) WithRevision(revision string) *PackageReference {
return &PackageReference{r.Recipe, r.Reference, revision}
}
// AsKey builds the additional key for the package file
func (r *PackageReference) AsKey() string {
return fmt.Sprintf("%s|%s|%s|%s|%s", r.Recipe.User, r.Recipe.Channel, r.Recipe.RevisionOrDefault(), r.Reference, r.RevisionOrDefault())
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewRecipeReference(t *testing.T) {
cases := []struct {
Name string
Version string
User string
Channel string
Revision string
IsValid bool
}{
{"", "", "", "", "", false},
{"name", "", "", "", "", false},
{"", "1.0", "", "", "", false},
{"", "", "user", "", "", false},
{"", "", "", "channel", "", false},
{"", "", "", "", "0", false},
{"name", "1.0", "", "", "", true},
{"name", "1.0", "user", "", "", false},
{"name", "1.0", "", "channel", "", false},
{"name", "1.0", "user", "channel", "", true},
{"name", "1.0", "_", "", "", true},
{"name", "1.0", "", "_", "", true},
{"name", "1.0", "_", "_", "", true},
{"name", "1.0", "_", "_", "0", true},
{"name", "1.0", "", "", "0", true},
{"name", "1.0.0q", "", "", "0", true},
{"name", "1.0", "", "", "000000000000000000000000000000000000000000000000000000000000", false},
}
for i, c := range cases {
rref, err := NewRecipeReference(c.Name, c.Version, c.User, c.Channel, c.Revision)
if c.IsValid {
assert.NoError(t, err, "case %d, should be invalid", i)
assert.NotNil(t, rref, "case %d, should not be nil", i)
} else {
assert.Error(t, err, "case %d, should be valid", i)
}
}
}
func TestRecipeReferenceRevisionOrDefault(t *testing.T) {
rref, err := NewRecipeReference("name", "1.0", "", "", "")
assert.NoError(t, err)
assert.Equal(t, DefaultRevision, rref.RevisionOrDefault())
rref, err = NewRecipeReference("name", "1.0", "", "", DefaultRevision)
assert.NoError(t, err)
assert.Equal(t, DefaultRevision, rref.RevisionOrDefault())
rref, err = NewRecipeReference("name", "1.0", "", "", "Az09")
assert.NoError(t, err)
assert.Equal(t, "Az09", rref.RevisionOrDefault())
}
func TestRecipeReferenceString(t *testing.T) {
rref, err := NewRecipeReference("name", "1.0", "", "", "")
assert.NoError(t, err)
assert.Equal(t, "name/1.0", rref.String())
rref, err = NewRecipeReference("name", "1.0", "user", "channel", "")
assert.NoError(t, err)
assert.Equal(t, "name/1.0@user/channel", rref.String())
rref, err = NewRecipeReference("name", "1.0", "user", "channel", "Az09")
assert.NoError(t, err)
assert.Equal(t, "name/1.0@user/channel#Az09", rref.String())
}
func TestRecipeReferenceLinkName(t *testing.T) {
rref, err := NewRecipeReference("name", "1.0", "", "", "")
assert.NoError(t, err)
assert.Equal(t, "name/1.0/_/_/0", rref.LinkName())
rref, err = NewRecipeReference("name", "1.0", "user", "channel", "")
assert.NoError(t, err)
assert.Equal(t, "name/1.0/user/channel/0", rref.LinkName())
rref, err = NewRecipeReference("name", "1.0", "user", "channel", "Az09")
assert.NoError(t, err)
assert.Equal(t, "name/1.0/user/channel/Az09", rref.LinkName())
}
func TestNewPackageReference(t *testing.T) {
rref, _ := NewRecipeReference("name", "1.0", "", "", "")
cases := []struct {
Recipe *RecipeReference
Reference string
Revision string
IsValid bool
}{
{nil, "", "", false},
{rref, "", "", false},
{nil, "aZ09", "", false},
{rref, "aZ09", "", true},
{rref, "", "Az09", false},
{rref, "aZ09", "Az09", true},
}
for i, c := range cases {
pref, err := NewPackageReference(c.Recipe, c.Reference, c.Revision)
if c.IsValid {
assert.NoError(t, err, "case %d, should be invalid", i)
assert.NotNil(t, pref, "case %d, should not be nil", i)
} else {
assert.Error(t, err, "case %d, should be valid", i)
}
}
}
func TestPackageReferenceRevisionOrDefault(t *testing.T) {
rref, _ := NewRecipeReference("name", "1.0", "", "", "")
pref, err := NewPackageReference(rref, "ref", "")
assert.NoError(t, err)
assert.Equal(t, DefaultRevision, pref.RevisionOrDefault())
pref, err = NewPackageReference(rref, "ref", DefaultRevision)
assert.NoError(t, err)
assert.Equal(t, DefaultRevision, pref.RevisionOrDefault())
pref, err = NewPackageReference(rref, "ref", "Az09")
assert.NoError(t, err)
assert.Equal(t, "Az09", pref.RevisionOrDefault())
}
func TestPackageReferenceLinkName(t *testing.T) {
rref, _ := NewRecipeReference("name", "1.0", "", "", "")
pref, err := NewPackageReference(rref, "ref", "")
assert.NoError(t, err)
assert.Equal(t, "ref/0", pref.LinkName())
pref, err = NewPackageReference(rref, "ref", "Az09")
assert.NoError(t, err)
assert.Equal(t, "ref/Az09", pref.LinkName())
}
+242
View File
@@ -0,0 +1,242 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conda
import (
"archive/tar"
"archive/zip"
"compress/bzip2"
"io"
"strings"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"gitea.dev/modules/zstd"
)
var (
ErrInvalidStructure = util.NewInvalidArgumentErrorf("package structure is invalid")
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
const (
PropertyName = "conda.name"
PropertyChannel = "conda.channel"
PropertySubdir = "conda.subdir"
PropertyMetadata = "conda.metadata"
)
// Package represents a Conda package
type Package struct {
Name string
Version string
Subdir string
VersionMetadata *VersionMetadata
FileMetadata *FileMetadata
}
// VersionMetadata represents the metadata of a Conda package
type VersionMetadata struct {
Description string `json:"description,omitempty"`
Summary string `json:"summary,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
DocumentationURL string `json:"documentation_url,omitempty"`
License string `json:"license,omitempty"`
LicenseFamily string `json:"license_family,omitempty"`
}
// FileMetadata represents the metadata of a Conda package file
type FileMetadata struct {
IsCondaPackage bool `json:"is_conda"`
Architecture string `json:"architecture,omitempty"`
NoArch string `json:"noarch,omitempty"`
Build string `json:"build,omitempty"`
BuildNumber int64 `json:"build_number,omitempty"`
Dependencies []string `json:"dependencies,omitempty"`
Platform string `json:"platform,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
type index struct {
Name string `json:"name"`
Version string `json:"version"`
Architecture string `json:"arch"`
NoArch string `json:"noarch"`
Build string `json:"build"`
BuildNumber int64 `json:"build_number"`
Dependencies []string `json:"depends"`
License string `json:"license"`
LicenseFamily string `json:"license_family"`
Platform string `json:"platform"`
Subdir string `json:"subdir"`
Timestamp int64 `json:"timestamp"`
}
type about struct {
Description string `json:"description"`
Summary string `json:"summary"`
ProjectURL string `json:"home"`
RepositoryURL string `json:"dev_url"`
DocumentationURL string `json:"doc_url"`
}
type ReaderAndReaderAt interface {
io.Reader
io.ReaderAt
}
// ParsePackageBZ2 parses the Conda package file compressed with bzip2
func ParsePackageBZ2(r io.Reader) (*Package, error) {
gzr := bzip2.NewReader(r)
return parsePackageTar(gzr)
}
// ParsePackageConda parses the Conda package file compressed with zip and zstd
func ParsePackageConda(r io.ReaderAt, size int64) (*Package, error) {
zr, err := zip.NewReader(r, size)
if err != nil {
return nil, err
}
for _, file := range zr.File {
if strings.HasPrefix(file.Name, "info-") && strings.HasSuffix(file.Name, ".tar.zst") {
f, err := zr.Open(file.Name)
if err != nil {
return nil, err
}
defer f.Close()
dec, err := zstd.NewReader(f)
if err != nil {
return nil, err
}
defer dec.Close()
p, err := parsePackageTar(dec)
if p != nil {
p.FileMetadata.IsCondaPackage = true
}
return p, err
}
}
return nil, ErrInvalidStructure
}
func parsePackageTar(r io.Reader) (*Package, error) {
var i *index
var a *about
tr := tar.NewReader(r)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hdr.Typeflag != tar.TypeReg {
continue
}
if hdr.Name == "info/index.json" {
if err := json.NewDecoder(tr).Decode(&i); err != nil {
return nil, err
}
if !checkName(i.Name) {
return nil, ErrInvalidName
}
if !checkVersion(i.Version) {
return nil, ErrInvalidVersion
}
if a != nil {
break // stop loop if both files were found
}
} else if hdr.Name == "info/about.json" {
if err := json.NewDecoder(tr).Decode(&a); err != nil {
return nil, err
}
if !validation.IsValidURL(a.ProjectURL) {
a.ProjectURL = ""
}
if !validation.IsValidURL(a.RepositoryURL) {
a.RepositoryURL = ""
}
if !validation.IsValidURL(a.DocumentationURL) {
a.DocumentationURL = ""
}
if i != nil {
break // stop loop if both files were found
}
}
}
if i == nil {
return nil, ErrInvalidStructure
}
if a == nil {
a = &about{}
}
return &Package{
Name: i.Name,
Version: i.Version,
Subdir: i.Subdir,
VersionMetadata: &VersionMetadata{
License: i.License,
LicenseFamily: i.LicenseFamily,
Description: a.Description,
Summary: a.Summary,
ProjectURL: a.ProjectURL,
RepositoryURL: a.RepositoryURL,
DocumentationURL: a.DocumentationURL,
},
FileMetadata: &FileMetadata{
Architecture: i.Architecture,
NoArch: i.NoArch,
Build: i.Build,
BuildNumber: i.BuildNumber,
Dependencies: i.Dependencies,
Platform: i.Platform,
Timestamp: i.Timestamp,
},
}, nil
}
// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1393
func checkName(name string) bool {
if name == "" {
return false
}
if name != strings.ToLower(name) {
return false
}
return !checkBadCharacters(name, "!")
}
// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1403
func checkVersion(version string) bool {
if version == "" {
return false
}
return !checkBadCharacters(version, "-")
}
func checkBadCharacters(s, additional string) bool {
if strings.ContainsAny(s, "=@#$%^&*:;\"'\\|<>?/ ") {
return true
}
return strings.ContainsAny(s, additional)
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conda
import (
"archive/tar"
"archive/zip"
"bytes"
"io"
"testing"
"gitea.dev/modules/zstd"
"github.com/dsnet/compress/bzip2"
"github.com/stretchr/testify/assert"
)
const (
packageName = "gitea"
packageVersion = "1.0.1"
description = "Package Description"
projectURL = "https://gitea.com"
repositoryURL = "https://gitea.com/gitea/gitea"
documentationURL = "https://docs.gitea.com"
)
func TestParsePackage(t *testing.T) {
createArchive := func(files map[string][]byte) *bytes.Buffer {
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
for filename, content := range files {
hdr := &tar.Header{
Name: filename,
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write(content)
}
tw.Close()
return &buf
}
t.Run("MissingIndexFile", func(t *testing.T) {
buf := createArchive(map[string][]byte{"dummy.txt": {}})
p, err := parsePackageTar(buf)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidStructure)
})
t.Run("MissingAboutFile", func(t *testing.T) {
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"1.0"}`)})
p, err := parsePackageTar(buf)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, "name", p.Name)
assert.Equal(t, "1.0", p.Version)
assert.Empty(t, p.VersionMetadata.ProjectURL)
})
t.Run("InvalidName", func(t *testing.T) {
for _, name := range []string{"", "name!", "nAMe"} {
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"` + name + `","version":"1.0"}`)})
p, err := parsePackageTar(buf)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidName)
}
})
t.Run("InvalidVersion", func(t *testing.T) {
for _, version := range []string{"", "1.0-2"} {
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"` + version + `"}`)})
p, err := parsePackageTar(buf)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidVersion)
}
})
t.Run("Valid", func(t *testing.T) {
buf := createArchive(map[string][]byte{
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `","subdir":"linux-64"}`),
"info/about.json": []byte(`{"description":"` + description + `","dev_url":"` + repositoryURL + `","doc_url":"` + documentationURL + `","home":"` + projectURL + `"}`),
})
p, err := parsePackageTar(buf)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, "linux-64", p.Subdir)
assert.Equal(t, description, p.VersionMetadata.Description)
assert.Equal(t, projectURL, p.VersionMetadata.ProjectURL)
assert.Equal(t, repositoryURL, p.VersionMetadata.RepositoryURL)
assert.Equal(t, documentationURL, p.VersionMetadata.DocumentationURL)
})
t.Run(".tar.bz2", func(t *testing.T) {
tarArchive := createArchive(map[string][]byte{
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`),
})
var buf bytes.Buffer
bw, _ := bzip2.NewWriter(&buf, nil)
io.Copy(bw, tarArchive)
bw.Close()
br := bytes.NewReader(buf.Bytes())
p, err := ParsePackageBZ2(br)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.False(t, p.FileMetadata.IsCondaPackage)
})
t.Run(".conda", func(t *testing.T) {
tarArchive := createArchive(map[string][]byte{
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`),
})
var infoBuf bytes.Buffer
zsw, _ := zstd.NewWriter(&infoBuf)
io.Copy(zsw, tarArchive)
zsw.Close()
var buf bytes.Buffer
zpw := zip.NewWriter(&buf)
w, _ := zpw.Create("info-x.tar.zst")
w.Write(infoBuf.Bytes())
zpw.Close()
br := bytes.NewReader(buf.Bytes())
p, err := ParsePackageConda(br, int64(br.Len()))
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.True(t, p.FileMetadata.IsCondaPackage)
})
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
const (
ContentTypeDockerDistributionManifestV2 = "application/vnd.docker.distribution.manifest.v2+json"
ManifestFilename = "manifest.json"
UploadVersion = "_upload"
)
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package helm
// https://github.com/helm/helm/blob/main/pkg/chart/
const ConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
// Maintainer describes a Chart maintainer.
type Maintainer struct {
// Name is a user name or organization name
Name string `json:"name,omitempty"`
// Email is an optional email address to contact the named maintainer
Email string `json:"email,omitempty"`
// URL is an optional URL to an address for the named maintainer
URL string `json:"url,omitempty"`
}
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
type Metadata struct {
// The name of the chart. Required.
Name string `json:"name,omitempty"`
// The URL to a relevant project page, git repo, or contact person
Home string `json:"home,omitempty"`
// Source is the URL to the source code of this chart
Sources []string `json:"sources,omitempty"`
// A SemVer 2 conformant version string of the chart. Required.
Version string `json:"version,omitempty"`
// A one-sentence description of the chart
Description string `json:"description,omitempty"`
// A list of string keywords
Keywords []string `json:"keywords,omitempty"`
// A list of name and URL/email address combinations for the maintainer(s)
Maintainers []*Maintainer `json:"maintainers,omitempty"`
// The URL to an icon file.
Icon string `json:"icon,omitempty"`
// The API Version of this chart. Required.
APIVersion string `json:"apiVersion,omitempty"`
// The condition to check to enable chart
Condition string `json:"condition,omitempty"`
// The tags to check to enable chart
Tags string `json:"tags,omitempty"`
// The version of the application enclosed inside of this chart.
AppVersion string `json:"appVersion,omitempty"`
// Whether or not this chart is deprecated
Deprecated bool `json:"deprecated,omitempty"`
// Annotations are additional mappings uninterpreted by Helm,
// made available for inspection by other applications.
Annotations map[string]string `json:"annotations,omitempty"`
// KubeVersion is a SemVer constraint specifying the version of Kubernetes required.
KubeVersion string `json:"kubeVersion,omitempty"`
// Specifies the chart type: application or library
Type string `json:"type,omitempty"`
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"fmt"
"io"
"strings"
"gitea.dev/modules/json"
"gitea.dev/modules/packages/container/helm"
"gitea.dev/modules/validation"
oci "github.com/opencontainers/image-spec/specs-go/v1"
)
const (
PropertyRepository = "container.repository"
PropertyDigest = "container.digest"
PropertyMediaType = "container.mediatype"
PropertyManifestTagged = "container.manifest.tagged"
PropertyManifestReference = "container.manifest.reference"
DefaultPlatform = "linux/amd64"
labelLicenses = "org.opencontainers.image.licenses"
labelURL = "org.opencontainers.image.url"
labelSource = "org.opencontainers.image.source"
labelDocumentation = "org.opencontainers.image.documentation"
labelDescription = "org.opencontainers.image.description"
labelAuthors = "org.opencontainers.image.authors"
)
type ImageType string
const (
TypeOCI ImageType = "oci"
TypeHelm ImageType = "helm"
)
// Name gets the name of the image type
func (it ImageType) Name() string {
switch it {
case TypeHelm:
return "Helm Chart"
default:
return "OCI / Docker"
}
}
// Metadata represents the metadata of a Container package
type Metadata struct {
Type ImageType `json:"type"`
IsTagged bool `json:"is_tagged"`
Platform string `json:"platform,omitempty"`
Description string `json:"description,omitempty"`
Authors []string `json:"authors,omitempty"`
Licenses string `json:"license,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
DocumentationURL string `json:"documentation_url,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
ImageLayers []string `json:"layer_creation,omitempty"`
Manifests []*Manifest `json:"manifests,omitempty"`
}
type Manifest struct {
Platform string `json:"platform"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
func IsMediaTypeValid(mt string) bool {
return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.")
}
func IsMediaTypeImageManifest(mt string) bool {
return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json")
}
func IsMediaTypeImageIndex(mt string) bool {
return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json")
}
// ParseImageConfig parses the metadata of an image config
func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) {
if strings.EqualFold(mediaType, helm.ConfigMediaType) {
return parseHelmConfig(r)
}
// fallback to OCI Image Config
// FIXME: this fallback is not right, we should strictly check the media type in the future
metadata, err := parseOCIImageConfig(r)
if err != nil {
if !IsMediaTypeImageManifest(mediaType) {
return &Metadata{Platform: "unknown/unknown"}, nil
}
return nil, err
}
return metadata, nil
}
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
var image oci.Image
// FIXME: JSON-KEY-CASE: here seems a abuse of the case-insensitive decoding feature, spec is case-sensitive
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
if err := json.NewDecoderCaseInsensitive(r).Decode(&image); err != nil {
return nil, err
}
platform := DefaultPlatform
if image.OS != "" && image.Architecture != "" {
platform = fmt.Sprintf("%s/%s", image.OS, image.Architecture)
if image.Variant != "" {
platform = fmt.Sprintf("%s/%s", platform, image.Variant)
}
}
imageLayers := make([]string, 0, len(image.History))
for _, history := range image.History {
cmd := history.CreatedBy
if i := strings.Index(cmd, "#(nop) "); i != -1 {
cmd = strings.TrimSpace(cmd[i+7:])
}
if cmd != "" {
imageLayers = append(imageLayers, cmd)
}
}
metadata := &Metadata{
Type: TypeOCI,
Platform: platform,
Licenses: image.Config.Labels[labelLicenses],
ProjectURL: image.Config.Labels[labelURL],
RepositoryURL: image.Config.Labels[labelSource],
DocumentationURL: image.Config.Labels[labelDocumentation],
Description: image.Config.Labels[labelDescription],
Labels: image.Config.Labels,
ImageLayers: imageLayers,
}
if authors, ok := image.Config.Labels[labelAuthors]; ok {
metadata.Authors = []string{authors}
}
if !validation.IsValidURL(metadata.ProjectURL) {
metadata.ProjectURL = ""
}
if !validation.IsValidURL(metadata.RepositoryURL) {
metadata.RepositoryURL = ""
}
if !validation.IsValidURL(metadata.DocumentationURL) {
metadata.DocumentationURL = ""
}
return metadata, nil
}
func parseHelmConfig(r io.Reader) (*Metadata, error) {
var config helm.Metadata
if err := json.NewDecoder(r).Decode(&config); err != nil {
return nil, err
}
metadata := &Metadata{
Type: TypeHelm,
Description: config.Description,
ProjectURL: config.Home,
}
if len(config.Maintainers) > 0 {
authors := make([]string, 0, len(config.Maintainers))
for _, maintainer := range config.Maintainers {
authors = append(authors, maintainer.Name)
}
metadata.Authors = authors
}
if len(config.Sources) > 0 && validation.IsValidURL(config.Sources[0]) {
metadata.RepositoryURL = config.Sources[0]
}
if !validation.IsValidURL(metadata.ProjectURL) {
metadata.ProjectURL = ""
}
return metadata, nil
}
@@ -0,0 +1,68 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"strings"
"testing"
"gitea.dev/modules/packages/container/helm"
oci "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseImageConfig(t *testing.T) {
description := "Image Description"
author := "Gitea"
license := "MIT"
projectURL := "https://gitea.com"
repositoryURL := "https://gitea.com/gitea"
documentationURL := "https://docs.gitea.com"
// FIXME: JSON-KEY-CASE: the test case is not right, the config fields are capitalized in the spec
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}`
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI))
assert.NoError(t, err)
assert.Equal(t, TypeOCI, metadata.Type)
assert.Equal(t, description, metadata.Description)
assert.ElementsMatch(t, []string{author}, metadata.Authors)
assert.Equal(t, license, metadata.Licenses)
assert.Equal(t, projectURL, metadata.ProjectURL)
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
assert.Equal(t, documentationURL, metadata.DocumentationURL)
assert.ElementsMatch(t, []string{"do it 1", "do it 2"}, metadata.ImageLayers)
assert.Equal(
t,
map[string]string{
labelAuthors: author,
labelLicenses: license,
labelURL: projectURL,
labelSource: repositoryURL,
labelDocumentation: documentationURL,
labelDescription: description,
},
metadata.Labels,
)
assert.Empty(t, metadata.Manifests)
configHelm := `{"description":"` + description + `", "home": "` + projectURL + `", "sources": ["` + repositoryURL + `"], "maintainers":[{"name":"` + author + `"}]}`
metadata, err = ParseImageConfig(helm.ConfigMediaType, strings.NewReader(configHelm))
assert.NoError(t, err)
assert.Equal(t, TypeHelm, metadata.Type)
assert.Equal(t, description, metadata.Description)
assert.ElementsMatch(t, []string{author}, metadata.Authors)
assert.Equal(t, projectURL, metadata.ProjectURL)
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
metadata, err = ParseImageConfig("anything-unknown", strings.NewReader(""))
require.NoError(t, err)
assert.Equal(t, &Metadata{Platform: "unknown/unknown"}, metadata)
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"io"
"net/url"
"path"
"strings"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
"gitea.dev/modules/util"
)
// BlobHash256Key is the key to address a blob content
type BlobHash256Key string
// ContentStore is a wrapper around ObjectStorage
type ContentStore struct {
store storage.ObjectStorage
}
// NewContentStore creates the default package store
func NewContentStore() *ContentStore {
contentStore := &ContentStore{storage.Packages}
return contentStore
}
func (s *ContentStore) OpenBlob(key BlobHash256Key) (storage.Object, error) {
return s.store.Open(KeyToRelativePath(key))
}
func (s *ContentStore) ShouldServeDirect() bool {
return setting.Packages.Storage.ServeDirect()
}
func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams *storage.ServeDirectOptions) (*url.URL, error) {
return s.store.ServeDirectURL(KeyToRelativePath(key), filename, method, reqParams)
}
// FIXME: Workaround to be removed in v1.20
// https://github.com/go-gitea/gitea/issues/19586
func (s *ContentStore) Has(key BlobHash256Key) error {
_, err := s.store.Stat(KeyToRelativePath(key))
return err
}
// Save stores a package blob
func (s *ContentStore) Save(key BlobHash256Key, r io.Reader, size int64) error {
_, err := s.store.Save(KeyToRelativePath(key), r, size)
return err
}
// Delete deletes a package blob
func (s *ContentStore) Delete(key BlobHash256Key) error {
return s.store.Delete(KeyToRelativePath(key))
}
// KeyToRelativePath converts the sha256 key aabb000000... to aa/bb/aabb000000...
func KeyToRelativePath(key BlobHash256Key) string {
return path.Join(string(key)[0:2], string(key)[2:4], string(key))
}
// RelativePathToKey converts a relative path aa/bb/aabb000000... to the sha256 key aabb000000...
func RelativePathToKey(relativePath string) (BlobHash256Key, error) {
parts := strings.SplitN(relativePath, "/", 3)
if len(parts) != 3 || len(parts[0]) != 2 || len(parts[1]) != 2 || len(parts[2]) < 4 || parts[0]+parts[1] != parts[2][0:4] {
return "", util.ErrInvalidArgument
}
return BlobHash256Key(parts[2]), nil
}
+242
View File
@@ -0,0 +1,242 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cran
import (
"archive/tar"
"archive/zip"
"bufio"
"compress/gzip"
"io"
"path"
"regexp"
"strings"
"gitea.dev/modules/util"
)
const (
PropertyType = "cran.type"
PropertyPlatform = "cran.platform"
PropertyRVersion = "cran.rvserion"
TypeSource = "source"
TypeBinary = "binary"
)
var (
ErrMissingDescriptionFile = util.NewInvalidArgumentErrorf("DESCRIPTION file is missing")
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
var (
fieldPattern = regexp.MustCompile(`\A\S+:`)
namePattern = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9\.]*[a-zA-Z0-9]\z`)
versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+)+\z`)
authorReplacePattern = regexp.MustCompile(`[\[\(].+?[\]\)]`)
)
// Package represents a CRAN package
type Package struct {
Name string
Version string
FileExtension string
Metadata *Metadata
}
// Metadata represents the metadata of a CRAN package
type Metadata struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
ProjectURL []string `json:"project_url,omitempty"`
License string `json:"license,omitempty"`
Authors []string `json:"authors,omitempty"`
Depends []string `json:"depends,omitempty"`
Imports []string `json:"imports,omitempty"`
Suggests []string `json:"suggests,omitempty"`
LinkingTo []string `json:"linking_to,omitempty"`
NeedsCompilation bool `json:"needs_compilation"`
}
type ReaderReaderAt interface {
io.Reader
io.ReaderAt
}
// ParsePackage reads the package metadata from a CRAN package
// .zip and .tar.gz/.tgz files are supported.
func ParsePackage(r ReaderReaderAt, size int64) (*Package, error) {
magicBytes := make([]byte, 2)
if _, err := r.ReadAt(magicBytes, 0); err != nil {
return nil, err
}
if magicBytes[0] == 0x1F && magicBytes[1] == 0x8B {
return parsePackageTarGz(r)
}
return parsePackageZip(r, size)
}
func parsePackageTarGz(r io.Reader) (*Package, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.Typeflag != tar.TypeReg {
continue
}
if strings.Count(hd.Name, "/") > 1 {
continue
}
if path.Base(hd.Name) == "DESCRIPTION" {
p, err := ParseDescription(tr)
if p != nil {
p.FileExtension = ".tar.gz"
}
return p, err
}
}
return nil, ErrMissingDescriptionFile
}
func parsePackageZip(r io.ReaderAt, size int64) (*Package, error) {
zr, err := zip.NewReader(r, size)
if err != nil {
return nil, err
}
for _, file := range zr.File {
if strings.Count(file.Name, "/") > 1 {
continue
}
if path.Base(file.Name) == "DESCRIPTION" {
f, err := zr.Open(file.Name)
if err != nil {
return nil, err
}
defer f.Close()
p, err := ParseDescription(f)
if p != nil {
p.FileExtension = ".zip"
}
return p, err
}
}
return nil, ErrMissingDescriptionFile
}
// ParseDescription parses a DESCRIPTION file to retrieve the metadata of a CRAN package
func ParseDescription(r io.Reader) (*Package, error) {
p := &Package{
Metadata: &Metadata{},
}
scanner := bufio.NewScanner(r)
var b strings.Builder
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if !fieldPattern.MatchString(line) {
b.WriteRune(' ')
b.WriteString(line)
continue
}
if err := setField(p, b.String()); err != nil {
return nil, err
}
b.Reset()
b.WriteString(line)
}
if err := setField(p, b.String()); err != nil {
return nil, err
}
if err := scanner.Err(); err != nil {
return nil, err
}
return p, nil
}
func setField(p *Package, data string) error {
if data == "" {
return nil
}
parts := strings.SplitN(data, ":", 2)
if len(parts) != 2 {
return nil
}
name := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch name {
case "Package":
if !namePattern.MatchString(value) {
return ErrInvalidName
}
p.Name = value
case "Version":
if !versionPattern.MatchString(value) {
return ErrInvalidVersion
}
p.Version = value
case "Title":
p.Metadata.Title = value
case "Description":
p.Metadata.Description = value
case "URL":
p.Metadata.ProjectURL = splitAndTrim(value)
case "License":
p.Metadata.License = value
case "Author":
p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""))
case "Depends":
p.Metadata.Depends = splitAndTrim(value)
case "Imports":
p.Metadata.Imports = splitAndTrim(value)
case "Suggests":
p.Metadata.Suggests = splitAndTrim(value)
case "LinkingTo":
p.Metadata.LinkingTo = splitAndTrim(value)
case "NeedsCompilation":
p.Metadata.NeedsCompilation = value == "yes"
}
return nil
}
func splitAndTrim(s string) []string {
items := strings.Split(s, ", ")
for i := range items {
items[i] = strings.TrimSpace(items[i])
}
return items
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cran
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
const (
packageName = "gitea"
packageVersion = "1.0.1"
author = "KN4CK3R"
description = "Package Description"
projectURL = "https://gitea.io"
license = "GPL (>= 2)"
)
func createDescription(name, version string) *bytes.Buffer {
var buf bytes.Buffer
fmt.Fprintln(&buf, "Package:", name)
fmt.Fprintln(&buf, "Version:", version)
fmt.Fprintln(&buf, "Description:", "Package\n\n Description")
fmt.Fprintln(&buf, "URL:", projectURL)
fmt.Fprintln(&buf, "Imports: abc,\n123")
fmt.Fprintln(&buf, "NeedsCompilation: yes")
fmt.Fprintln(&buf, "License:", license)
fmt.Fprintln(&buf, "Author:", author)
return &buf
}
func TestParsePackage(t *testing.T) {
t.Run(".tar.gz", func(t *testing.T) {
createArchive := func(filename string, content []byte) *bytes.Reader {
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
hdr := &tar.Header{
Name: filename,
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write(content)
tw.Close()
gw.Close()
return bytes.NewReader(buf.Bytes())
}
t.Run("MissingDescriptionFile", func(t *testing.T) {
buf := createArchive(
"dummy.txt",
[]byte{},
)
p, err := ParsePackage(buf, buf.Size())
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrMissingDescriptionFile)
})
t.Run("Valid", func(t *testing.T) {
buf := createArchive(
"package/DESCRIPTION",
createDescription(packageName, packageVersion).Bytes(),
)
p, err := ParsePackage(buf, buf.Size())
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
})
})
t.Run(".zip", func(t *testing.T) {
createArchive := func(filename string, content []byte) *bytes.Reader {
var buf bytes.Buffer
archive := zip.NewWriter(&buf)
w, _ := archive.Create(filename)
w.Write(content)
archive.Close()
return bytes.NewReader(buf.Bytes())
}
t.Run("MissingDescriptionFile", func(t *testing.T) {
buf := createArchive(
"dummy.txt",
[]byte{},
)
p, err := ParsePackage(buf, buf.Size())
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrMissingDescriptionFile)
})
t.Run("Valid", func(t *testing.T) {
buf := createArchive(
"package/DESCRIPTION",
createDescription(packageName, packageVersion).Bytes(),
)
p, err := ParsePackage(buf, buf.Size())
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
})
})
}
func TestParseDescription(t *testing.T) {
t.Run("InvalidName", func(t *testing.T) {
for _, name := range []string{"123abc", "ab-cd", "ab cd", "ab/cd"} {
p, err := ParseDescription(createDescription(name, packageVersion))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidName)
}
})
t.Run("InvalidVersion", func(t *testing.T) {
for _, version := range []string{"1", "1 0", "1.", "1.0.", "1-", "1-0-"} {
p, err := ParseDescription(createDescription(packageName, version))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidVersion)
}
})
t.Run("ValidVersionManyComponents", func(t *testing.T) {
for _, version := range []string{"0.3.4.0.2", "1.2.3.4.5", "1-2-3-4-5"} {
p, err := ParseDescription(createDescription(packageName, version))
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, version, p.Version)
}
})
t.Run("Valid", func(t *testing.T) {
p, err := ParseDescription(createDescription(packageName, packageVersion))
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, description, p.Metadata.Description)
assert.ElementsMatch(t, []string{projectURL}, p.Metadata.ProjectURL)
assert.ElementsMatch(t, []string{author}, p.Metadata.Authors)
assert.Equal(t, license, p.Metadata.License)
assert.ElementsMatch(t, []string{"abc", "123"}, p.Metadata.Imports)
assert.True(t, p.Metadata.NeedsCompilation)
})
}
+221
View File
@@ -0,0 +1,221 @@
// 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"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"gitea.dev/modules/zstd"
"github.com/blakesmith/ar"
"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|[1-9][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
// https://man7.org/linux/man-pages/man5/deb-split.5.html#FORMAT
// The file names might contain a trailing slash (since dpkg 1.15.6).
switch strings.TrimSuffix(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":
p.Name = value
case "Version":
p.Version = value
case "Architecture":
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
}
if !namePattern.MatchString(p.Name) {
return nil, ErrInvalidName
}
if !versionPattern.MatchString(p.Version) {
return nil, ErrInvalidVersion
}
if p.Architecture == "" {
return nil, ErrInvalidArchitecture
}
dependencies := strings.Split(depends.String(), ",")
for i := range dependencies {
dependencies[i] = strings.TrimSpace(dependencies[i])
}
p.Metadata.Dependencies = dependencies
p.Control = strings.TrimSpace(control.String())
return p, nil
}
+187
View File
@@ -0,0 +1,187 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package debian
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"testing"
"gitea.dev/modules/util"
"gitea.dev/modules/zstd"
"github.com/blakesmith/ar"
"github.com/stretchr/testify/assert"
"github.com/ulikunitz/xz"
)
const (
packageName = "gitea"
packageVersion = "0:1.0.1-te~st"
packageArchitecture = "amd64"
packageAuthor = "KN4CK3R"
description = "Description with multiple lines."
projectURL = "https://gitea.io"
)
func TestParsePackage(t *testing.T) {
createArchive := func(files map[string][]byte) io.Reader {
var buf bytes.Buffer
aw := ar.NewWriter(&buf)
aw.WriteGlobalHeader()
for filename, content := range files {
hdr := &ar.Header{
Name: filename,
Mode: 0o600,
Size: int64(len(content)),
}
aw.WriteHeader(hdr)
aw.Write(content)
}
return &buf
}
t.Run("MissingControlFile", func(t *testing.T) {
data := createArchive(map[string][]byte{"dummy.txt": {}})
p, err := ParsePackage(data)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrMissingControlFile)
})
t.Run("Compression", func(t *testing.T) {
t.Run("Unsupported", func(t *testing.T) {
data := createArchive(map[string][]byte{"control.tar.foo": {}})
p, err := ParsePackage(data)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrUnsupportedCompression)
})
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
tw.WriteHeader(&tar.Header{
Name: "control",
Mode: 0o600,
Size: 50,
})
tw.Write([]byte("Package: gitea\nVersion: 1.0.0\nArchitecture: amd64\n"))
tw.Close()
cases := []struct {
Extension string
WriterFactory func(io.Writer) io.WriteCloser
}{
{
Extension: "",
WriterFactory: func(w io.Writer) io.WriteCloser {
return util.NopCloser{Writer: w}
},
},
{
Extension: ".gz",
WriterFactory: func(w io.Writer) io.WriteCloser {
return gzip.NewWriter(w)
},
},
{
Extension: ".xz",
WriterFactory: func(w io.Writer) io.WriteCloser {
xw, _ := xz.NewWriter(w)
return xw
},
},
{
Extension: ".zst",
WriterFactory: func(w io.Writer) io.WriteCloser {
zw, _ := zstd.NewWriter(w)
return zw
},
},
}
for _, c := range cases {
t.Run(c.Extension, func(t *testing.T) {
var cbuf bytes.Buffer
w := c.WriterFactory(&cbuf)
w.Write(buf.Bytes())
w.Close()
data := createArchive(map[string][]byte{"control.tar" + c.Extension: cbuf.Bytes()})
p, err := ParsePackage(data)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, "gitea", p.Name)
t.Run("TrailingSlash", func(t *testing.T) {
data := createArchive(map[string][]byte{"control.tar" + c.Extension + "/": cbuf.Bytes()})
p, err := ParsePackage(data)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, "gitea", p.Name)
})
})
}
})
}
func TestParseControlFile(t *testing.T) {
buildContent := func(name, version, architecture string) *bytes.Buffer {
var buf bytes.Buffer
buf.WriteString("Package: " + name + "\nVersion: " + version + "\nArchitecture: " + architecture + "\nMaintainer: " + packageAuthor + " <kn4ck3r@gitea.io>\nHomepage: " + projectURL + "\nDepends: a,\n b\nDescription: Description\n with multiple\n lines.")
return &buf
}
t.Run("InvalidName", func(t *testing.T) {
for _, name := range []string{"", "-cd"} {
p, err := ParseControlFile(buildContent(name, packageVersion, packageArchitecture))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidName)
}
})
t.Run("InvalidVersion", func(t *testing.T) {
for _, version := range []string{"", "1-", ":1.0", "1_0"} {
p, err := ParseControlFile(buildContent(packageName, version, packageArchitecture))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidVersion)
}
})
t.Run("InvalidArchitecture", func(t *testing.T) {
p, err := ParseControlFile(buildContent(packageName, packageVersion, ""))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidArchitecture)
})
t.Run("Valid", func(t *testing.T) {
content := buildContent(packageName, packageVersion, packageArchitecture)
full := content.String()
p, err := ParseControlFile(content)
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, packageArchitecture, p.Architecture)
assert.Equal(t, description, p.Metadata.Description)
assert.Equal(t, projectURL, p.Metadata.ProjectURL)
assert.Equal(t, packageAuthor, p.Metadata.Maintainer)
assert.Equal(t, []string{"a", "b"}, p.Metadata.Dependencies)
assert.Equal(t, full, p.Control)
})
t.Run("ValidVersions", func(t *testing.T) {
for _, version := range []string{"1.0", "0:1.2", "9:1.0", "10:1.0", "900:1a.2b-x-y_z~1+2"} {
p, err := ParseControlFile(buildContent("testpkg", version, "amd64"))
assert.NoError(t, err, "ParseControlFile with version %q", version)
assert.NotNil(t, p)
}
})
}
+93
View File
@@ -0,0 +1,93 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package goproxy
import (
"archive/zip"
"io"
"path"
"strings"
"gitea.dev/modules/util"
)
const (
PropertyGoMod = "go.mod"
maxGoModFileSize = 16 * 1024 * 1024 // https://go.dev/ref/mod#zip-path-size-constraints
)
var (
ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure")
ErrGoModFileTooLarge = util.NewInvalidArgumentErrorf("go.mod file is too large")
)
type Package struct {
Name string
Version string
GoMod string
}
// ParsePackage parses the Go package file
// https://go.dev/ref/mod#zip-files
func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
archive, err := zip.NewReader(r, size)
if err != nil {
return nil, err
}
var p *Package
for _, file := range archive.File {
nameAndVersion := path.Dir(file.Name)
parts := strings.SplitN(nameAndVersion, "@", 2)
if len(parts) != 2 {
continue
}
versionParts := strings.SplitN(parts[1], "/", 2)
if p == nil {
p = &Package{
Name: strings.TrimSuffix(nameAndVersion, "@"+parts[1]),
Version: versionParts[0],
}
}
if len(versionParts) > 1 {
// files are expected in the "root" folder
continue
}
if path.Base(file.Name) == "go.mod" {
if file.UncompressedSize64 > maxGoModFileSize {
return nil, ErrGoModFileTooLarge
}
f, err := archive.Open(file.Name)
if err != nil {
return nil, err
}
defer f.Close()
bytes, err := io.ReadAll(&io.LimitedReader{R: f, N: maxGoModFileSize})
if err != nil {
return nil, err
}
p.GoMod = string(bytes)
return p, nil
}
}
if p == nil {
return nil, ErrInvalidStructure
}
p.GoMod = "module " + p.Name
return p, nil
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package goproxy
import (
"archive/zip"
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
const (
packageName = "gitea.com/go-gitea/gitea"
packageVersion = "v0.0.1"
)
func TestParsePackage(t *testing.T) {
createArchive := func(files map[string][]byte) *bytes.Reader {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for name, content := range files {
w, _ := zw.Create(name)
w.Write(content)
}
zw.Close()
return bytes.NewReader(buf.Bytes())
}
t.Run("EmptyPackage", func(t *testing.T) {
data := createArchive(nil)
p, err := ParsePackage(data, int64(data.Len()))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidStructure)
})
t.Run("InvalidNameOrVersionStructure", func(t *testing.T) {
data := createArchive(map[string][]byte{
packageName + "/" + packageVersion + "/go.mod": {},
})
p, err := ParsePackage(data, int64(data.Len()))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidStructure)
})
t.Run("GoModFileInWrongDirectory", func(t *testing.T) {
data := createArchive(map[string][]byte{
packageName + "@" + packageVersion + "/subdir/go.mod": {},
})
p, err := ParsePackage(data, int64(data.Len()))
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, "module gitea.com/go-gitea/gitea", p.GoMod)
})
t.Run("Valid", func(t *testing.T) {
data := createArchive(map[string][]byte{
packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"),
packageName + "@" + packageVersion + "/go.mod": []byte("valid"),
})
p, err := ParsePackage(data, int64(data.Len()))
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, "valid", p.GoMod)
})
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"io"
"gitea.dev/modules/setting"
"gitea.dev/modules/util/filebuffer"
)
// HashedSizeReader provide methods to read, sum hashes and a Size method
type HashedSizeReader interface {
io.Reader
HashSummer
Size() int64
}
// HashedBuffer is buffer which calculates multiple checksums
type HashedBuffer struct {
*filebuffer.FileBackedBuffer
hash *MultiHasher
combinedWriter io.Writer
}
const DefaultMemorySize = 32 * 1024 * 1024
// NewHashedBuffer creates a hashed buffer with the default memory size
func NewHashedBuffer() (*HashedBuffer, error) {
return NewHashedBufferWithSize(DefaultMemorySize)
}
// NewHashedBufferWithSize creates a hashed buffer with a specific memory size
func NewHashedBufferWithSize(maxMemorySize int) (*HashedBuffer, error) {
tempDir, err := setting.AppDataTempDir("package-hashed-buffer").MkdirAllSub("")
if err != nil {
return nil, err
}
b := filebuffer.New(maxMemorySize, tempDir)
hash := NewMultiHasher()
combinedWriter := io.MultiWriter(b, hash)
return &HashedBuffer{
b,
hash,
combinedWriter,
}, nil
}
// CreateHashedBufferFromReader creates a hashed buffer with the default memory size and copies the provided reader data into it.
func CreateHashedBufferFromReader(r io.Reader) (*HashedBuffer, error) {
return CreateHashedBufferFromReaderWithSize(r, DefaultMemorySize)
}
// CreateHashedBufferFromReaderWithSize creates a hashed buffer and copies the provided reader data into it.
func CreateHashedBufferFromReaderWithSize(r io.Reader, maxMemorySize int) (*HashedBuffer, error) {
b, err := NewHashedBufferWithSize(maxMemorySize)
if err != nil {
return nil, err
}
_, err = io.Copy(b, r)
if err != nil {
return nil, err
}
return b, nil
}
// Write implements io.Writer
func (b *HashedBuffer) Write(p []byte) (int, error) {
return b.combinedWriter.Write(p)
}
// Sums gets the MD5, SHA1, SHA256 and SHA512 checksums of the data
func (b *HashedBuffer) Sums() (hashMD5, hashSHA1, hashSHA256, hashSHA512 []byte) {
return b.hash.Sums()
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"encoding/hex"
"io"
"strings"
"testing"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestHashedBuffer(t *testing.T) {
setting.AppDataPath = t.TempDir()
cases := []struct {
MaxMemorySize int
Data string
HashMD5 string
HashSHA1 string
HashSHA256 string
HashSHA512 string
}{
{5, "test", "098f6bcd4621d373cade4e832627b4f6", "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"},
{5, "testtest", "05a671c66aefea124cc08b76ea6d30bb", "51abb9636078defbf888d8457a7c76f85c8f114c", "37268335dd6931045bdcdf92623ff819a64244b53d0e746d438797349d4da578", "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc"},
}
for _, c := range cases {
buf, err := CreateHashedBufferFromReaderWithSize(strings.NewReader(c.Data), c.MaxMemorySize)
assert.NoError(t, err)
assert.EqualValues(t, len(c.Data), buf.Size())
data, err := io.ReadAll(buf)
assert.NoError(t, err)
assert.Equal(t, c.Data, string(data))
hashMD5, hashSHA1, hashSHA256, hashSHA512 := buf.Sums()
assert.Equal(t, c.HashMD5, hex.EncodeToString(hashMD5))
assert.Equal(t, c.HashSHA1, hex.EncodeToString(hashSHA1))
assert.Equal(t, c.HashSHA256, hex.EncodeToString(hashSHA256))
assert.Equal(t, c.HashSHA512, hex.EncodeToString(hashSHA512))
assert.NoError(t, buf.Close())
}
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package helm
import (
"archive/tar"
"compress/gzip"
"io"
"strings"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"github.com/hashicorp/go-version"
"go.yaml.in/yaml/v4"
)
var (
// ErrMissingChartFile indicates a missing Chart.yaml file
ErrMissingChartFile = util.NewInvalidArgumentErrorf("Chart.yaml file is missing")
// ErrInvalidName indicates an invalid package name
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
// ErrInvalidVersion indicates an invalid package version
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
// ErrInvalidChart indicates an invalid chart
ErrInvalidChart = util.NewInvalidArgumentErrorf("chart is invalid")
)
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
type Metadata struct {
APIVersion string `json:"api_version" yaml:"apiVersion"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
Name string `json:"name" yaml:"name"`
Version string `json:"version" yaml:"version"`
AppVersion string `json:"app_version,omitempty" yaml:"appVersion,omitempty"`
Home string `json:"home,omitempty" yaml:"home,omitempty"`
Sources []string `json:"sources,omitempty" yaml:"sources,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty"`
Maintainers []*Maintainer `json:"maintainers,omitempty" yaml:"maintainers,omitempty"`
Icon string `json:"icon,omitempty" yaml:"icon,omitempty"`
Condition string `json:"condition,omitempty" yaml:"condition,omitempty"`
Tags string `json:"tags,omitempty" yaml:"tags,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
KubeVersion string `json:"kube_version,omitempty" yaml:"kubeVersion,omitempty"`
Dependencies []*Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
}
type Maintainer struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Email string `json:"email,omitempty" yaml:"email,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
type Dependency struct {
Name string `json:"name" yaml:"name"`
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Repository string `json:"repository" yaml:"repository"`
Condition string `json:"condition,omitempty" yaml:"condition,omitempty"`
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
ImportValues []any `json:"import_values,omitempty" yaml:"import-values,omitempty"`
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
}
// ParseChartArchive parses the metadata of a Helm archive
func ParseChartArchive(r io.Reader) (*Metadata, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
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() == "Chart.yaml" {
if strings.Count(hd.Name, "/") != 1 {
continue
}
return ParseChartFile(tr)
}
}
return nil, ErrMissingChartFile
}
// ParseChartFile parses a Chart.yaml file to retrieve the metadata of a Helm chart
func ParseChartFile(r io.Reader) (*Metadata, error) {
var metadata *Metadata
if err := yaml.NewDecoder(r).Decode(&metadata); err != nil {
return nil, err
}
if metadata.APIVersion == "" {
return nil, ErrInvalidChart
}
if metadata.Type != "" && metadata.Type != "application" && metadata.Type != "library" {
return nil, ErrInvalidChart
}
if metadata.Name == "" {
return nil, ErrInvalidName
}
if _, err := version.NewSemver(metadata.Version); err != nil {
return nil, ErrInvalidVersion
}
if !validation.IsValidURL(metadata.Home) {
metadata.Home = ""
}
return metadata, nil
}
+111
View File
@@ -0,0 +1,111 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package maven
import (
"encoding/xml"
"io"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"golang.org/x/net/html/charset"
)
// Metadata represents the metadata of a Maven package
type Metadata struct {
GroupID string `json:"group_id,omitempty"`
ArtifactID string `json:"artifact_id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Licenses []string `json:"licenses,omitempty"`
Dependencies []*Dependency `json:"dependencies,omitempty"`
}
// Dependency represents a dependency of a Maven package
type Dependency struct {
GroupID string `json:"group_id,omitempty"`
ArtifactID string `json:"artifact_id,omitempty"`
Version string `json:"version,omitempty"`
}
type pomStruct struct {
XMLName xml.Name `xml:"project"`
Parent struct {
GroupID string `xml:"groupId"`
ArtifactID string `xml:"artifactId"`
Version string `xml:"version"`
} `xml:"parent"`
GroupID string `xml:"groupId"`
ArtifactID string `xml:"artifactId"`
Version string `xml:"version"`
Name string `xml:"name"`
Description string `xml:"description"`
URL string `xml:"url"`
Licenses []struct {
Name string `xml:"name"`
URL string `xml:"url"`
Distribution string `xml:"distribution"`
} `xml:"licenses>license"`
Dependencies []struct {
GroupID string `xml:"groupId"`
ArtifactID string `xml:"artifactId"`
Version string `xml:"version"`
Scope string `xml:"scope"`
} `xml:"dependencies>dependency"`
}
// ParsePackageMetaData parses the metadata of a pom file
func ParsePackageMetaData(r io.Reader) (*Metadata, error) {
var pom pomStruct
dec := xml.NewDecoder(r)
dec.CharsetReader = charset.NewReaderLabel
if err := dec.Decode(&pom); err != nil {
return nil, err
}
if !validation.IsValidURL(pom.URL) {
pom.URL = ""
}
licenses := make([]string, 0, len(pom.Licenses))
for _, l := range pom.Licenses {
if l.Name != "" {
licenses = append(licenses, l.Name)
}
}
dependencies := make([]*Dependency, 0, len(pom.Dependencies))
for _, d := range pom.Dependencies {
dependencies = append(dependencies, &Dependency{
GroupID: d.GroupID,
ArtifactID: d.ArtifactID,
Version: d.Version,
})
}
pomGroupID := pom.GroupID
if pomGroupID == "" {
// the current module could inherit parent: https://maven.apache.org/pom.html#Inheritance
pomGroupID = pom.Parent.GroupID
}
if pomGroupID == "" {
return nil, util.ErrInvalidArgument
}
return &Metadata{
GroupID: pomGroupID,
ArtifactID: pom.ArtifactID,
Name: pom.Name,
Description: pom.Description,
ProjectURL: pom.URL,
Licenses: licenses,
Dependencies: dependencies,
}, nil
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package maven
import (
"strings"
"testing"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/encoding/charmap"
)
const (
groupID = "org.gitea"
artifactID = "my-project"
version = "1.0.1"
name = "My Gitea Project"
description = "Package Description"
projectURL = "https://gitea.io"
license = "MIT"
dependencyGroupID = "org.gitea.core"
dependencyArtifactID = "git"
dependencyVersion = "5.0.0"
)
const pomContent = `<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>` + groupID + `</groupId>
<artifactId>` + artifactID + `</artifactId>
<version>` + version + `</version>
<name>` + name + `</name>
<description>` + description + `</description>
<url>` + projectURL + `</url>
<licenses>
<license>
<name>` + license + `</name>
</license>
</licenses>
<dependencies>
<dependency>
<groupId>` + dependencyGroupID + `</groupId>
<artifactId>` + dependencyArtifactID + `</artifactId>
<version>` + dependencyVersion + `</version>
</dependency>
</dependencies>
</project>`
func TestParsePackageMetaData(t *testing.T) {
t.Run("InvalidFile", func(t *testing.T) {
m, err := ParsePackageMetaData(strings.NewReader(""))
assert.Nil(t, m)
assert.Error(t, err)
})
t.Run("Valid", func(t *testing.T) {
m, err := ParsePackageMetaData(strings.NewReader(pomContent))
assert.NoError(t, err)
assert.NotNil(t, m)
assert.Equal(t, groupID, m.GroupID)
assert.Equal(t, artifactID, m.ArtifactID)
assert.Equal(t, name, m.Name)
assert.Equal(t, description, m.Description)
assert.Equal(t, projectURL, m.ProjectURL)
assert.Len(t, m.Licenses, 1)
assert.Equal(t, license, m.Licenses[0])
assert.Len(t, m.Dependencies, 1)
assert.Equal(t, dependencyGroupID, m.Dependencies[0].GroupID)
assert.Equal(t, dependencyArtifactID, m.Dependencies[0].ArtifactID)
assert.Equal(t, dependencyVersion, m.Dependencies[0].Version)
})
t.Run("Encoding", func(t *testing.T) {
// UTF-8 is default but the metadata could be encoded differently
pomContent8859_1, err := charmap.ISO8859_1.NewEncoder().String(
strings.ReplaceAll(
pomContent,
`<?xml version="1.0"?>`,
`<?xml version="1.0" encoding="ISO-8859-1"?>`,
),
)
assert.NoError(t, err)
m, err := ParsePackageMetaData(strings.NewReader(pomContent8859_1))
assert.NoError(t, err)
assert.NotNil(t, m)
})
t.Run("ParentInherit", func(t *testing.T) {
pom := `<?xml version="1.0"?>
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>submodule1</artifactId>
</project>
`
m, err := ParsePackageMetaData(strings.NewReader(pom))
require.NoError(t, err)
require.NotNil(t, m)
assert.Equal(t, "com.mycompany.app", m.GroupID)
assert.Equal(t, "submodule1", m.ArtifactID)
})
t.Run("ParentInherit", func(t *testing.T) {
pom := `<?xml version="1.0"?>
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId></artifactId>
</project>
`
_, err := ParsePackageMetaData(strings.NewReader(pom))
require.ErrorIs(t, err, util.ErrInvalidArgument)
})
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding"
"errors"
"hash"
"io"
)
const (
marshaledSizeMD5 = 92
marshaledSizeSHA1 = 96
marshaledSizeSHA256 = 108
marshaledSizeSHA512 = 204
marshaledSize = marshaledSizeMD5 + marshaledSizeSHA1 + marshaledSizeSHA256 + marshaledSizeSHA512
)
// HashSummer provide a Sums method
type HashSummer interface {
Sums() (hashMD5, hashSHA1, hashSHA256, hashSHA512 []byte)
}
// MultiHasher calculates multiple checksums
type MultiHasher struct {
md5 hash.Hash
sha1 hash.Hash
sha256 hash.Hash
sha512 hash.Hash
combinedWriter io.Writer
}
// NewMultiHasher creates a multi hasher
func NewMultiHasher() *MultiHasher {
md5 := md5.New()
sha1 := sha1.New()
sha256 := sha256.New()
sha512 := sha512.New()
combinedWriter := io.MultiWriter(md5, sha1, sha256, sha512)
return &MultiHasher{
md5,
sha1,
sha256,
sha512,
combinedWriter,
}
}
// MarshalBinary implements encoding.BinaryMarshaler
func (h *MultiHasher) MarshalBinary() ([]byte, error) {
md5Bytes, err := h.md5.(encoding.BinaryMarshaler).MarshalBinary()
if err != nil {
return nil, err
}
sha1Bytes, err := h.sha1.(encoding.BinaryMarshaler).MarshalBinary()
if err != nil {
return nil, err
}
sha256Bytes, err := h.sha256.(encoding.BinaryMarshaler).MarshalBinary()
if err != nil {
return nil, err
}
sha512Bytes, err := h.sha512.(encoding.BinaryMarshaler).MarshalBinary()
if err != nil {
return nil, err
}
b := make([]byte, 0, marshaledSize)
b = append(b, md5Bytes...)
b = append(b, sha1Bytes...)
b = append(b, sha256Bytes...)
b = append(b, sha512Bytes...)
return b, nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (h *MultiHasher) UnmarshalBinary(b []byte) error {
if len(b) != marshaledSize {
return errors.New("invalid hash state size")
}
if err := h.md5.(encoding.BinaryUnmarshaler).UnmarshalBinary(b[:marshaledSizeMD5]); err != nil {
return err
}
b = b[marshaledSizeMD5:]
if err := h.sha1.(encoding.BinaryUnmarshaler).UnmarshalBinary(b[:marshaledSizeSHA1]); err != nil {
return err
}
b = b[marshaledSizeSHA1:]
if err := h.sha256.(encoding.BinaryUnmarshaler).UnmarshalBinary(b[:marshaledSizeSHA256]); err != nil {
return err
}
b = b[marshaledSizeSHA256:]
return h.sha512.(encoding.BinaryUnmarshaler).UnmarshalBinary(b[:marshaledSizeSHA512])
}
// Write implements io.Writer
func (h *MultiHasher) Write(p []byte) (int, error) {
return h.combinedWriter.Write(p)
}
// Sums gets the MD5, SHA1, SHA256 and SHA512 checksums of the data
func (h *MultiHasher) Sums() (hashMD5, hashSHA1, hashSHA256, hashSHA512 []byte) {
hashMD5 = h.md5.Sum(nil)
hashSHA1 = h.sha1.Sum(nil)
hashSHA256 = h.sha256.Sum(nil)
hashSHA512 = h.sha512.Sum(nil)
return hashMD5, hashSHA1, hashSHA256, hashSHA512
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
)
const (
expectedMD5 = "e3bef03c5f3b7f6b3ab3e3053ed71e9c"
expectedSHA1 = "060b3b99f88e96085b4a68e095bc9e3d1d91e1bc"
expectedSHA256 = "6ccce4863b70f258d691f59609d31b4502e1ba5199942d3bc5d35d17a4ce771d"
expectedSHA512 = "7f70e439ba8c52025c1f06cdf6ae443c4b8ed2e90059cdb9bbbf8adf80846f185a24acca9245b128b226d61753b0d7ed46580a69c8999eeff3bc13a4d0bd816c"
)
func TestMultiHasherSums(t *testing.T) {
t.Run("Sums", func(t *testing.T) {
h := NewMultiHasher()
h.Write([]byte("gitea"))
hashMD5, hashSHA1, hashSHA256, hashSHA512 := h.Sums()
assert.Equal(t, expectedMD5, hex.EncodeToString(hashMD5))
assert.Equal(t, expectedSHA1, hex.EncodeToString(hashSHA1))
assert.Equal(t, expectedSHA256, hex.EncodeToString(hashSHA256))
assert.Equal(t, expectedSHA512, hex.EncodeToString(hashSHA512))
})
t.Run("State", func(t *testing.T) {
h := NewMultiHasher()
h.Write([]byte("git"))
state, err := h.MarshalBinary()
assert.NoError(t, err)
h2 := NewMultiHasher()
err = h2.UnmarshalBinary(state)
assert.NoError(t, err)
h2.Write([]byte("ea"))
hashMD5, hashSHA1, hashSHA256, hashSHA512 := h2.Sums()
assert.Equal(t, expectedMD5, hex.EncodeToString(hashMD5))
assert.Equal(t, expectedSHA1, hex.EncodeToString(hashSHA1))
assert.Equal(t, expectedSHA256, hex.EncodeToString(hashSHA256))
assert.Equal(t, expectedSHA512, hex.EncodeToString(hashSHA512))
})
}
+313
View File
@@ -0,0 +1,313 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package npm
import (
"bytes"
"crypto/sha1"
"crypto/sha512"
"encoding/base64"
"fmt"
"io"
"regexp"
"strings"
"time"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"github.com/hashicorp/go-version"
)
var (
// ErrInvalidPackage indicates an invalid package
ErrInvalidPackage = util.NewInvalidArgumentErrorf("package is invalid")
// ErrInvalidPackageName indicates an invalid name
ErrInvalidPackageName = util.NewInvalidArgumentErrorf("package name is invalid")
// ErrInvalidPackageVersion indicates an invalid version
ErrInvalidPackageVersion = util.NewInvalidArgumentErrorf("package version is invalid")
// ErrInvalidAttachment indicates a invalid attachment
ErrInvalidAttachment = util.NewInvalidArgumentErrorf("package attachment is invalid")
// ErrInvalidIntegrity indicates an integrity validation error
ErrInvalidIntegrity = util.NewInvalidArgumentErrorf("failed to validate integrity")
)
var nameMatch = regexp.MustCompile(`^(@[a-z0-9-][a-z0-9-._]*/)?[a-z0-9-][a-z0-9-._]*$`)
// Package represents a npm package
type Package struct {
Name string
Version string
DistTags []string
Metadata Metadata
Filename string
Data []byte
}
// PackageMetadata https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
type PackageMetadata struct {
ID string `json:"_id"`
Name string `json:"name"`
Description string `json:"description"`
DistTags map[string]string `json:"dist-tags,omitempty"`
Versions map[string]*PackageMetadataVersion `json:"versions"`
Readme string `json:"readme,omitempty"`
Maintainers []User `json:"maintainers,omitempty"`
Time map[string]time.Time `json:"time,omitempty"`
Homepage string `json:"homepage,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Repository Repository `json:"repository"`
Author User `json:"author"`
ReadmeFilename string `json:"readmeFilename,omitempty"`
Users map[string]bool `json:"users,omitempty"`
License License `json:"license,omitempty"`
}
type License string
func (l *License) UnmarshalJSON(data []byte) error {
switch data[0] {
case '"':
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*l = License(value)
case '{':
var values map[string]any
if err := json.Unmarshal(data, &values); err != nil {
return err
}
value, _ := values["type"].(string)
*l = License(value)
}
return nil
}
// PackageMetadataVersion documentation: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
// PackageMetadataVersion response: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
type PackageMetadataVersion struct {
ID string `json:"_id"`
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
Author User `json:"author"`
Homepage string `json:"homepage,omitempty"`
License License `json:"license,omitempty"`
Repository Repository `json:"repository"`
Keywords []string `json:"keywords,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"`
BundleDependencies []string `json:"bundleDependencies,omitempty"`
DevDependencies map[string]string `json:"devDependencies,omitempty"`
PeerDependencies map[string]string `json:"peerDependencies,omitempty"`
PeerDependenciesMeta map[string]any `json:"peerDependenciesMeta,omitempty"`
Bin map[string]string `json:"bin,omitempty"`
OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"`
Readme string `json:"readme,omitempty"`
Dist PackageDistribution `json:"dist"`
Maintainers []User `json:"maintainers,omitempty"`
}
// PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
type PackageDistribution struct {
Integrity string `json:"integrity"`
Shasum string `json:"shasum"`
Tarball string `json:"tarball"`
FileCount int `json:"fileCount,omitempty"`
UnpackedSize int `json:"unpackedSize,omitempty"`
NpmSignature string `json:"npm-signature,omitempty"`
}
type PackageSearch struct {
Objects []*PackageSearchObject `json:"objects"`
Total int64 `json:"total"`
}
type PackageSearchObject struct {
Package *PackageSearchPackage `json:"package"`
}
type PackageSearchPackage struct {
Scope string `json:"scope"`
Name string `json:"name"`
Version string `json:"version"`
Date time.Time `json:"date"`
Description string `json:"description"`
Author User `json:"author"`
Publisher User `json:"publisher"`
Maintainers []User `json:"maintainers"`
Keywords []string `json:"keywords,omitempty"`
Links *PackageSearchPackageLinks `json:"links"`
}
type PackageSearchPackageLinks struct {
Registry string `json:"npm"`
Homepage string `json:"homepage,omitempty"`
Repository string `json:"repository,omitempty"`
}
// User https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
type User struct {
Username string `json:"username,omitempty"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
URL string `json:"url,omitempty"`
}
// UnmarshalJSON is needed because User objects can be strings or objects
func (u *User) UnmarshalJSON(data []byte) error {
switch data[0] {
case '"':
if err := json.Unmarshal(data, &u.Name); err != nil {
return err
}
case '{':
var tmp struct {
Username string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
URL string `json:"url"`
}
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
u.Username = tmp.Username
u.Name = tmp.Name
u.Email = tmp.Email
u.URL = tmp.URL
}
return nil
}
// Repository https://docs.npmjs.com/cli/v11/configuring-npm/package-json#repository
type Repository struct {
Type string `json:"type"`
URL string `json:"url"`
Directory string `json:"directory,omitempty"`
}
// PackageAttachment https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
type PackageAttachment struct {
ContentType string `json:"content_type"`
Data string `json:"data"`
Length int `json:"length"`
}
type packageUpload struct {
PackageMetadata
Attachments map[string]*PackageAttachment `json:"_attachments"`
}
// ParsePackage parses the content into a npm package
func ParsePackage(r io.Reader) (*Package, error) {
var upload packageUpload
if err := json.NewDecoder(r).Decode(&upload); err != nil {
return nil, err
}
for _, meta := range upload.Versions {
if !validateName(meta.Name) {
return nil, ErrInvalidPackageName
}
v, err := version.NewSemver(meta.Version)
if err != nil {
return nil, ErrInvalidPackageVersion
}
scope := ""
name := meta.Name
nameParts := strings.SplitN(meta.Name, "/", 2)
if len(nameParts) == 2 {
scope = nameParts[0]
name = nameParts[1]
}
if !validation.IsValidURL(meta.Homepage) {
meta.Homepage = ""
}
p := &Package{
Name: meta.Name,
Version: v.String(),
DistTags: make([]string, 0, 1),
Metadata: Metadata{
Scope: scope,
Name: name,
Description: meta.Description,
Author: meta.Author.Name,
License: meta.License,
ProjectURL: meta.Homepage,
Keywords: meta.Keywords,
Dependencies: meta.Dependencies,
BundleDependencies: meta.BundleDependencies,
DevelopmentDependencies: meta.DevDependencies,
PeerDependencies: meta.PeerDependencies,
PeerDependenciesMeta: meta.PeerDependenciesMeta,
OptionalDependencies: meta.OptionalDependencies,
Bin: meta.Bin,
Readme: meta.Readme,
Repository: meta.Repository,
},
}
for tag := range upload.DistTags {
p.DistTags = append(p.DistTags, tag)
}
p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version))
attachment := func() *PackageAttachment {
for _, a := range upload.Attachments {
return a
}
return nil
}()
if attachment == nil || len(attachment.Data) == 0 {
return nil, ErrInvalidAttachment
}
data, err := base64.StdEncoding.DecodeString(attachment.Data)
if err != nil {
return nil, ErrInvalidAttachment
}
p.Data = data
integrity := strings.SplitN(meta.Dist.Integrity, "-", 2)
if len(integrity) != 2 {
return nil, ErrInvalidIntegrity
}
integrityHash, err := base64.StdEncoding.DecodeString(integrity[1])
if err != nil {
return nil, ErrInvalidIntegrity
}
var hash []byte
switch integrity[0] {
case "sha1":
tmp := sha1.Sum(data)
hash = tmp[:]
case "sha512":
tmp := sha512.Sum512(data)
hash = tmp[:]
}
if !bytes.Equal(integrityHash, hash) {
return nil, ErrInvalidIntegrity
}
return p, nil
}
return nil, ErrInvalidPackage
}
func validateName(name string) bool {
if strings.TrimSpace(name) != name {
return false
}
if len(name) == 0 || len(name) > 214 {
return false
}
return nameMatch.MatchString(name)
}
+329
View File
@@ -0,0 +1,329 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package npm
import (
"bytes"
"encoding/base64"
"fmt"
"strings"
"testing"
"gitea.dev/modules/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParsePackage(t *testing.T) {
packageScope := "@scope"
packageName := "test-package"
packageFullName := packageScope + "/" + packageName
packageVersion := "1.0.1-pre"
packageTag := "latest"
packageAuthor := "KN4CK3R"
packageBin := "gitea"
packageDescription := "Test Description"
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
integrity := "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg=="
repository := Repository{
Type: "gitea",
URL: "http://localhost:3000/gitea/test.git",
Directory: "packages/test-package",
}
t.Run("InvalidUpload", func(t *testing.T) {
p, err := ParsePackage(bytes.NewReader([]byte{0}))
assert.Nil(t, p)
assert.Error(t, err)
})
t.Run("InvalidUploadNoData", func(t *testing.T) {
b, _ := json.Marshal(packageUpload{})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackage)
})
t.Run("InvalidPackageName", func(t *testing.T) {
test := func(t *testing.T, name string) {
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: name,
Name: name,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: name,
},
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageName)
}
test(t, " test ")
test(t, " test")
test(t, "test ")
test(t, "te st")
test(t, "Test")
test(t, "_test")
test(t, ".test")
test(t, "^test")
test(t, "te^st")
test(t, "te|st")
test(t, "te)(st")
test(t, "te'st")
test(t, "te!st")
test(t, "te*st")
test(t, "te~st")
test(t, "invalid/scope")
test(t, "@invalid/_name")
test(t, "@invalid/.name")
})
t.Run("ValidPackageName", func(t *testing.T) {
test := func(t *testing.T, name string) {
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: name,
Name: name,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: name,
},
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
}
test(t, "test")
test(t, "@scope/name")
test(t, "@scope/q")
test(t, "q")
test(t, "@scope/package-name")
test(t, "@scope/package.name")
test(t, "@scope/package_name")
test(t, "123name")
test(t, "----")
test(t, packageFullName)
})
t.Run("InvalidPackageVersion", func(t *testing.T) {
version := "first-version"
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
version: {
Name: packageFullName,
Version: version,
},
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
})
t.Run("InvalidAttachment", func(t *testing.T) {
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
},
},
},
Attachments: map[string]*PackageAttachment{
"dummy.tgz": {},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidAttachment)
})
t.Run("InvalidData", func(t *testing.T) {
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
},
},
},
Attachments: map[string]*PackageAttachment{
filename: {
Data: "/",
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidAttachment)
})
t.Run("InvalidIntegrity", func(t *testing.T) {
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
Dist: PackageDistribution{
Integrity: "sha512-test==",
},
},
},
},
Attachments: map[string]*PackageAttachment{
filename: {
Data: data,
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidIntegrity)
})
t.Run("InvalidIntegrity2", func(t *testing.T) {
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
Dist: PackageDistribution{
Integrity: integrity,
},
},
},
},
Attachments: map[string]*PackageAttachment{
filename: {
Data: base64.StdEncoding.EncodeToString([]byte("data")),
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidIntegrity)
})
t.Run("Valid", func(t *testing.T) {
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
DistTags: map[string]string{
packageTag: packageVersion,
},
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
Description: packageDescription,
Author: User{Name: packageAuthor},
License: "MIT",
Homepage: "https://gitea.io/",
Readme: packageDescription,
Dependencies: map[string]string{
"package": "1.2.0",
},
Bin: map[string]string{
"bin": packageBin,
},
Dist: PackageDistribution{
Integrity: integrity,
},
Repository: repository,
},
},
},
Attachments: map[string]*PackageAttachment{
filename: {
Data: data,
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageFullName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, []string{packageTag}, p.DistTags)
assert.Equal(t, fmt.Sprintf("%s-%s.tgz", strings.Split(packageFullName, "/")[1], packageVersion), p.Filename)
b, _ = base64.StdEncoding.DecodeString(data)
assert.Equal(t, b, p.Data)
assert.Equal(t, packageName, p.Metadata.Name)
assert.Equal(t, packageScope, p.Metadata.Scope)
assert.Equal(t, packageDescription, p.Metadata.Description)
assert.Equal(t, packageDescription, p.Metadata.Readme)
assert.Equal(t, packageAuthor, p.Metadata.Author)
assert.Equal(t, packageBin, p.Metadata.Bin["bin"])
assert.Equal(t, "MIT", string(p.Metadata.License))
assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL)
assert.Contains(t, p.Metadata.Dependencies, "package")
assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"])
assert.Equal(t, repository.Type, p.Metadata.Repository.Type)
assert.Equal(t, repository.URL, p.Metadata.Repository.URL)
assert.Equal(t, repository.Directory, p.Metadata.Repository.Directory)
})
t.Run("ValidLicenseMap", func(t *testing.T) {
packageJSON := `{
"versions": {
"0.1.1": {
"name": "dev-null",
"version": "0.1.1",
"license": {
"type": "MIT"
},
"dist": {
"integrity": "sha256-"
}
}
},
"_attachments": {
"foo": {
"data": "AAAA"
}
}
}`
p, err := ParsePackage(strings.NewReader(packageJSON))
require.NoError(t, err)
require.Equal(t, "MIT", string(p.Metadata.License))
})
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package npm
// TagProperty is the name of the property for tag management
const TagProperty = "npm.tag"
// Metadata represents the metadata of a npm package
type Metadata struct {
Scope string `json:"scope,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Author string `json:"author,omitempty"`
License License `json:"license,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"`
BundleDependencies []string `json:"bundleDependencies,omitempty"`
DevelopmentDependencies map[string]string `json:"development_dependencies,omitempty"`
PeerDependencies map[string]string `json:"peer_dependencies,omitempty"`
PeerDependenciesMeta map[string]any `json:"peer_dependencies_meta,omitempty"`
OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"`
Bin map[string]string `json:"bin,omitempty"`
Readme string `json:"readme,omitempty"`
Repository Repository `json:"repository"`
}
+276
View File
@@ -0,0 +1,276 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package nuget
import (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"github.com/hashicorp/go-version"
)
var (
// ErrMissingNuspecFile indicates a missing Nuspec file
ErrMissingNuspecFile = util.NewInvalidArgumentErrorf("Nuspec file is missing")
// ErrNuspecFileTooLarge indicates a Nuspec file which is too large
ErrNuspecFileTooLarge = util.NewInvalidArgumentErrorf("Nuspec file is too large")
// ErrNuspecInvalidID indicates an invalid id in the Nuspec file
ErrNuspecInvalidID = util.NewInvalidArgumentErrorf("Nuspec file contains an invalid id")
// ErrNuspecInvalidVersion indicates an invalid version in the Nuspec file
ErrNuspecInvalidVersion = util.NewInvalidArgumentErrorf("Nuspec file contains an invalid version")
)
// PackageType specifies the package type the metadata describes
type PackageType int
const (
// DependencyPackage represents a package (*.nupkg)
DependencyPackage PackageType = iota + 1
// SymbolsPackage represents a symbol package (*.snupkg)
SymbolsPackage
PropertySymbolID = "nuget.symbol.id"
)
var idmatch = regexp.MustCompile(`\A\w+(?:[.-]\w+)*\z`)
const maxNuspecFileSize = 3 * 1024 * 1024
// Package represents a Nuget package
type Package struct {
PackageType PackageType
ID string
Version string
Metadata *Metadata
NuspecContent *bytes.Buffer
}
// Metadata represents the metadata of a Nuget package
type Metadata struct {
Authors string `json:"authors,omitempty"`
Copyright string `json:"copyright,omitempty"`
Description string `json:"description,omitempty"`
DevelopmentDependency bool `json:"development_dependency,omitempty"`
IconURL string `json:"icon_url,omitempty"`
Language string `json:"language,omitempty"`
LicenseURL string `json:"license_url,omitempty"`
MinClientVersion string `json:"min_client_version,omitempty"`
Owners string `json:"owners,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Readme string `json:"readme,omitempty"`
ReleaseNotes string `json:"release_notes,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
RequireLicenseAcceptance bool `json:"require_license_acceptance"`
Summary string `json:"summary,omitempty"`
Tags string `json:"tags,omitempty"`
Title string `json:"title,omitempty"`
Dependencies map[string][]Dependency `json:"dependencies,omitempty"`
}
// Dependency represents a dependency of a Nuget package
type Dependency struct {
ID string `json:"id"`
Version string `json:"version"`
}
// https://learn.microsoft.com/en-us/nuget/reference/nuspec
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Packaging/compiler/resources/nuspec.xsd
type nuspecPackage struct {
Metadata struct {
// required fields
Authors string `xml:"authors"`
Description string `xml:"description"`
ID string `xml:"id"`
Version string `xml:"version"`
// optional fields
Copyright string `xml:"copyright"`
DevelopmentDependency bool `xml:"developmentDependency"`
IconURL string `xml:"iconUrl"`
Language string `xml:"language"`
LicenseURL string `xml:"licenseUrl"`
MinClientVersion string `xml:"minClientVersion,attr"`
Owners string `xml:"owners"`
ProjectURL string `xml:"projectUrl"`
Readme string `xml:"readme"`
ReleaseNotes string `xml:"releaseNotes"`
RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
Summary string `xml:"summary"`
Tags string `xml:"tags"`
Title string `xml:"title"`
Dependencies struct {
Dependency []struct {
ID string `xml:"id,attr"`
Version string `xml:"version,attr"`
Exclude string `xml:"exclude,attr"`
} `xml:"dependency"`
Group []struct {
TargetFramework string `xml:"targetFramework,attr"`
Dependency []struct {
ID string `xml:"id,attr"`
Version string `xml:"version,attr"`
Exclude string `xml:"exclude,attr"`
} `xml:"dependency"`
} `xml:"group"`
} `xml:"dependencies"`
PackageTypes struct {
PackageType []struct {
Name string `xml:"name,attr"`
} `xml:"packageType"`
} `xml:"packageTypes"`
Repository struct {
URL string `xml:"url,attr"`
} `xml:"repository"`
} `xml:"metadata"`
}
// ParsePackageMetaData parses the metadata of a Nuget package file
func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
archive, err := zip.NewReader(r, size)
if err != nil {
return nil, util.NewInvalidArgumentErrorf("unable to parse package meta: %v", err)
}
for _, file := range archive.File {
if filepath.Dir(file.Name) != "." {
continue
}
if strings.HasSuffix(strings.ToLower(file.Name), ".nuspec") {
if file.UncompressedSize64 > maxNuspecFileSize {
return nil, ErrNuspecFileTooLarge
}
f, err := archive.Open(file.Name)
if err != nil {
return nil, err
}
defer f.Close()
return ParseNuspecMetaData(archive, f)
}
}
return nil, ErrMissingNuspecFile
}
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
var nuspecBuf bytes.Buffer
var p nuspecPackage
if err := xml.NewDecoder(io.TeeReader(r, &nuspecBuf)).Decode(&p); err != nil {
return nil, err
}
if !idmatch.MatchString(p.Metadata.ID) {
return nil, ErrNuspecInvalidID
}
v, err := version.NewSemver(p.Metadata.Version)
if err != nil {
return nil, ErrNuspecInvalidVersion
}
if !validation.IsValidURL(p.Metadata.ProjectURL) {
p.Metadata.ProjectURL = ""
}
packageType := DependencyPackage
for _, pt := range p.Metadata.PackageTypes.PackageType {
if pt.Name == "SymbolsPackage" {
packageType = SymbolsPackage
break
}
}
m := &Metadata{
Authors: p.Metadata.Authors,
Copyright: p.Metadata.Copyright,
Description: p.Metadata.Description,
DevelopmentDependency: p.Metadata.DevelopmentDependency,
IconURL: p.Metadata.IconURL,
Language: p.Metadata.Language,
LicenseURL: p.Metadata.LicenseURL,
MinClientVersion: p.Metadata.MinClientVersion,
Owners: p.Metadata.Owners,
ProjectURL: p.Metadata.ProjectURL,
ReleaseNotes: p.Metadata.ReleaseNotes,
RepositoryURL: p.Metadata.Repository.URL,
RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
Summary: p.Metadata.Summary,
Tags: p.Metadata.Tags,
Title: p.Metadata.Title,
Dependencies: make(map[string][]Dependency),
}
if p.Metadata.Readme != "" {
f, err := archive.Open(p.Metadata.Readme)
if err == nil {
buf, _ := util.ReadWithLimit(f, 1024*1024)
m.Readme = string(buf)
_ = f.Close()
}
}
if len(p.Metadata.Dependencies.Dependency) > 0 {
deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
for _, dep := range p.Metadata.Dependencies.Dependency {
if dep.ID == "" || dep.Version == "" {
continue
}
deps = append(deps, Dependency{
ID: dep.ID,
Version: dep.Version,
})
}
m.Dependencies[""] = deps
}
for _, group := range p.Metadata.Dependencies.Group {
deps := make([]Dependency, 0, len(group.Dependency))
for _, dep := range group.Dependency {
if dep.ID == "" || dep.Version == "" {
continue
}
deps = append(deps, Dependency{
ID: dep.ID,
Version: dep.Version,
})
}
if len(deps) > 0 {
m.Dependencies[group.TargetFramework] = deps
}
}
return &Package{
PackageType: packageType,
ID: p.Metadata.ID,
Version: toNormalizedVersion(v),
Metadata: m,
NuspecContent: &nuspecBuf,
}, nil
}
// https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers
// https://github.com/NuGet/NuGet.Client/blob/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Versioning/VersionFormatter.cs#L121
func toNormalizedVersion(v *version.Version) string {
var buf bytes.Buffer
segments := v.Segments64()
_, _ = fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2])
if len(segments) > 3 && segments[3] > 0 {
_, _ = fmt.Fprintf(&buf, ".%d", segments[3])
}
pre := v.Prerelease()
if pre != "" {
_, _ = fmt.Fprint(&buf, "-", pre)
}
return buf.String()
}
+217
View File
@@ -0,0 +1,217 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package nuget
import (
"archive/zip"
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
const (
authors = "Gitea Authors"
copyright = "Package Copyright"
dependencyID = "System.Text.Json"
dependencyVersion = "5.0.0"
developmentDependency = true
description = "Package Description"
iconURL = "https://gitea.io/favicon.png"
id = "System.Gitea"
language = "Package Language"
licenseURL = "https://gitea.io/license"
minClientVersion = "1.0.0.0"
owners = "Package Owners"
projectURL = "https://gitea.io"
readme = "Readme"
releaseNotes = "Package Release Notes"
repositoryURL = "https://gitea.io/gitea/gitea"
requireLicenseAcceptance = true
tags = "tag_1 tag_2 tag_3"
targetFramework = ".NETStandard2.1"
title = "Package Title"
versionStr = "1.0.1"
)
const nuspecContent = `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata minClientVersion="` + minClientVersion + `">
<authors>` + authors + `</authors>
<copyright>` + copyright + `</copyright>
<description>` + description + `</description>
<developmentDependency>true</developmentDependency>
<iconUrl>` + iconURL + `</iconUrl>
<id>` + id + `</id>
<language>` + language + `</language>
<licenseUrl>` + licenseURL + `</licenseUrl>
<owners>` + owners + `</owners>
<projectUrl>` + projectURL + `</projectUrl>
<readme>README.md</readme>
<releaseNotes>` + releaseNotes + `</releaseNotes>
<repository url="` + repositoryURL + `" />
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<tags>` + tags + `</tags>
<title>` + title + `</title>
<version>` + versionStr + `</version>
<dependencies>
<group targetFramework="` + targetFramework + `">
<dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
</package>`
const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>` + id + `</id>
<version>` + versionStr + `</version>
<description>` + description + `</description>
<packageTypes>
<packageType name="SymbolsPackage" />
</packageTypes>
<dependencies>
<group targetFramework="` + targetFramework + `" />
</dependencies>
</metadata>
</package>`
func TestParsePackageMetaData(t *testing.T) {
createArchive := func(files map[string]string) []byte {
var buf bytes.Buffer
archive := zip.NewWriter(&buf)
for name, content := range files {
w, _ := archive.Create(name)
w.Write([]byte(content))
}
archive.Close()
return buf.Bytes()
}
t.Run("MissingNuspecFile", func(t *testing.T) {
data := createArchive(map[string]string{"dummy.txt": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
assert.ErrorIs(t, err, ErrMissingNuspecFile)
})
t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
data := createArchive(map[string]string{"sub/package.nuspec": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
assert.ErrorIs(t, err, ErrMissingNuspecFile)
})
t.Run("InvalidNuspecFile", func(t *testing.T) {
data := createArchive(map[string]string{"package.nuspec": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
assert.Error(t, err)
})
t.Run("InvalidPackageId", func(t *testing.T) {
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata></metadata>
</package>`})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
assert.ErrorIs(t, err, ErrNuspecInvalidID)
})
t.Run("InvalidPackageVersion", func(t *testing.T) {
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>` + id + `</id>
</metadata>
</package>`})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
})
t.Run("MissingReadme", func(t *testing.T) {
data := createArchive(map[string]string{"package.nuspec": nuspecContent})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, np)
assert.Empty(t, np.Metadata.Readme)
})
t.Run("Dependency Package", func(t *testing.T) {
data := createArchive(map[string]string{
"package.nuspec": nuspecContent,
"README.md": readme,
})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, np)
assert.Equal(t, DependencyPackage, np.PackageType)
assert.Equal(t, authors, np.Metadata.Authors)
assert.Equal(t, description, np.Metadata.Description)
assert.Equal(t, id, np.ID)
assert.Equal(t, versionStr, np.Version)
assert.Equal(t, copyright, np.Metadata.Copyright)
assert.Equal(t, developmentDependency, np.Metadata.DevelopmentDependency)
assert.Equal(t, iconURL, np.Metadata.IconURL)
assert.Equal(t, language, np.Metadata.Language)
assert.Equal(t, licenseURL, np.Metadata.LicenseURL)
assert.Equal(t, minClientVersion, np.Metadata.MinClientVersion)
assert.Equal(t, owners, np.Metadata.Owners)
assert.Equal(t, projectURL, np.Metadata.ProjectURL)
assert.Equal(t, readme, np.Metadata.Readme)
assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
assert.Equal(t, requireLicenseAcceptance, np.Metadata.RequireLicenseAcceptance)
assert.Equal(t, tags, np.Metadata.Tags)
assert.Equal(t, title, np.Metadata.Title)
assert.Len(t, np.Metadata.Dependencies, 1)
assert.Contains(t, np.Metadata.Dependencies, targetFramework)
deps := np.Metadata.Dependencies[targetFramework]
assert.Len(t, deps, 1)
assert.Equal(t, dependencyID, deps[0].ID)
assert.Equal(t, dependencyVersion, deps[0].Version)
t.Run("NormalizedVersion", func(t *testing.T) {
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>test</id>
<version>1.04.5.2.5-rc.1+metadata</version>
</metadata>
</package>`})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, np)
assert.Equal(t, "1.4.5.2-rc.1", np.Version)
})
})
t.Run("Symbols Package", func(t *testing.T) {
data := createArchive(map[string]string{"package.nuspec": symbolsNuspecContent})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, np)
assert.Equal(t, SymbolsPackage, np.PackageType)
assert.Equal(t, id, np.ID)
assert.Equal(t, versionStr, np.Version)
assert.Equal(t, description, np.Metadata.Description)
assert.Empty(t, np.Metadata.Dependencies)
})
}
+186
View File
@@ -0,0 +1,186 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package nuget
import (
"archive/zip"
"bytes"
"encoding/binary"
"fmt"
"io"
"path"
"path/filepath"
"strings"
"gitea.dev/modules/packages"
"gitea.dev/modules/util"
)
var (
ErrMissingPdbFiles = util.NewInvalidArgumentErrorf("package does not contain PDB files")
ErrInvalidFiles = util.NewInvalidArgumentErrorf("package contains invalid files")
ErrInvalidPdbMagicNumber = util.NewInvalidArgumentErrorf("invalid Portable PDB magic number")
ErrMissingPdbStream = util.NewInvalidArgumentErrorf("missing PDB stream")
)
type PortablePdb struct {
Name string
ID string
Content *packages.HashedBuffer
}
type PortablePdbList []*PortablePdb
func (l PortablePdbList) Close() {
for _, pdb := range l {
_ = pdb.Content.Close()
}
}
// ExtractPortablePdb extracts PDB files from a .snupkg file
func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) {
archive, err := zip.NewReader(r, size)
if err != nil {
return nil, util.NewInvalidArgumentErrorf("unable to extract portable pdb: %v", err)
}
var pdbs PortablePdbList
err = func() error {
for _, file := range archive.File {
if strings.HasSuffix(file.Name, "/") {
continue
}
ext := strings.ToLower(filepath.Ext(file.Name))
switch ext {
case ".nuspec", ".xml", ".psmdcp", ".rels", ".p7s":
continue
case ".pdb":
f, err := archive.Open(file.Name)
if err != nil {
return err
}
buf, err := packages.CreateHashedBufferFromReader(f)
_ = f.Close()
if err != nil {
return err
}
id, err := ParseDebugHeaderID(buf)
if err != nil {
_ = buf.Close()
return fmt.Errorf("Invalid PDB file: %w", err)
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
_ = buf.Close()
return err
}
pdbs = append(pdbs, &PortablePdb{
Name: path.Base(file.Name),
ID: id,
Content: buf,
})
default:
return ErrInvalidFiles
}
}
return nil
}()
if err != nil {
pdbs.Close()
return nil, err
}
if len(pdbs) == 0 {
return nil, ErrMissingPdbFiles
}
return pdbs, nil
}
// ParseDebugHeaderID TODO
func ParseDebugHeaderID(r io.ReadSeeker) (string, error) {
var magic uint32
if err := binary.Read(r, binary.LittleEndian, &magic); err != nil {
return "", err
}
if magic != 0x424A5342 {
return "", ErrInvalidPdbMagicNumber
}
if _, err := r.Seek(8, io.SeekCurrent); err != nil {
return "", err
}
var versionStringSize int32
if err := binary.Read(r, binary.LittleEndian, &versionStringSize); err != nil {
return "", err
}
if _, err := r.Seek(int64(versionStringSize), io.SeekCurrent); err != nil {
return "", err
}
if _, err := r.Seek(2, io.SeekCurrent); err != nil {
return "", err
}
var streamCount int16
if err := binary.Read(r, binary.LittleEndian, &streamCount); err != nil {
return "", err
}
read4ByteAlignedString := func(r io.Reader) (string, error) {
b := make([]byte, 4)
var buf bytes.Buffer
for {
if _, err := r.Read(b); err != nil {
return "", err
}
if before, _, ok := bytes.Cut(b, []byte{0}); ok {
buf.Write(before)
return buf.String(), nil
}
buf.Write(b)
}
}
for i := 0; i < int(streamCount); i++ {
var offset uint32
if err := binary.Read(r, binary.LittleEndian, &offset); err != nil {
return "", err
}
if _, err := r.Seek(4, io.SeekCurrent); err != nil {
return "", err
}
name, err := read4ByteAlignedString(r)
if err != nil {
return "", err
}
if name == "#Pdb" {
if _, err := r.Seek(int64(offset), io.SeekStart); err != nil {
return "", err
}
b := make([]byte, 16)
if _, err := r.Read(b); err != nil {
return "", err
}
data1 := binary.LittleEndian.Uint32(b[0:4])
data2 := binary.LittleEndian.Uint16(b[4:6])
data3 := binary.LittleEndian.Uint16(b[6:8])
data4 := b[8:16]
return fmt.Sprintf("%08x%04x%04x%04x%012x", data1, data2, data3, data4[:2], data4[2:]), nil
}
}
return "", ErrMissingPdbStream
}
@@ -0,0 +1,84 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package nuget
import (
"archive/zip"
"bytes"
"encoding/base64"
"testing"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
)
const pdbContent = `QlNKQgEAAQAAAAAADAAAAFBEQiB2MS4wAAAAAAAABgB8AAAAWAAAACNQZGIAAAAA1AAAAAgBAAAj
fgAA3AEAAAQAAAAjU3RyaW5ncwAAAADgAQAABAAAACNVUwDkAQAAMAAAACNHVUlEAAAAFAIAACgB
AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`
func TestExtractPortablePdb(t *testing.T) {
setting.AppDataPath = t.TempDir()
createArchive := func(name string, content []byte) []byte {
var buf bytes.Buffer
archive := zip.NewWriter(&buf)
w, _ := archive.Create(name)
_, _ = w.Write(content)
_ = archive.Close()
return buf.Bytes()
}
t.Run("MissingPdbFiles", func(t *testing.T) {
var buf bytes.Buffer
_ = zip.NewWriter(&buf).Close()
pdbs, err := ExtractPortablePdb(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
assert.ErrorIs(t, err, ErrMissingPdbFiles)
assert.Empty(t, pdbs)
})
t.Run("InvalidFiles", func(t *testing.T) {
data := createArchive("sub/test.bin", []byte{})
pdbs, err := ExtractPortablePdb(bytes.NewReader(data), int64(len(data)))
assert.ErrorIs(t, err, ErrInvalidFiles)
assert.Empty(t, pdbs)
})
t.Run("Valid", func(t *testing.T) {
b, _ := base64.StdEncoding.DecodeString(pdbContent)
data := createArchive("test.pdb", b)
pdbs, err := ExtractPortablePdb(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.Len(t, pdbs, 1)
assert.Equal(t, "test.pdb", pdbs[0].Name)
assert.Equal(t, "d910bb6948bd4c6cb40155bcf52c3c94", pdbs[0].ID)
pdbs.Close()
})
}
func TestParseDebugHeaderID(t *testing.T) {
t.Run("InvalidPdbMagicNumber", func(t *testing.T) {
id, err := ParseDebugHeaderID(bytes.NewReader([]byte{0, 0, 0, 0}))
assert.ErrorIs(t, err, ErrInvalidPdbMagicNumber)
assert.Empty(t, id)
})
t.Run("MissingPdbStream", func(t *testing.T) {
b, _ := base64.StdEncoding.DecodeString(`QlNKQgEAAQAAAAAADAAAAFBEQiB2MS4wAAAAAAAAAQB8AAAAWAAAACNVUwA=`)
id, err := ParseDebugHeaderID(bytes.NewReader(b))
assert.ErrorIs(t, err, ErrMissingPdbStream)
assert.Empty(t, id)
})
t.Run("Valid", func(t *testing.T) {
b, _ := base64.StdEncoding.DecodeString(pdbContent)
id, err := ParseDebugHeaderID(bytes.NewReader(b))
assert.NoError(t, err)
assert.Equal(t, "d910bb6948bd4c6cb40155bcf52c3c94", id)
})
}
+153
View File
@@ -0,0 +1,153 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pub
import (
"archive/tar"
"compress/gzip"
"io"
"regexp"
"strings"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"github.com/hashicorp/go-version"
"go.yaml.in/yaml/v4"
)
var (
ErrMissingPubspecFile = util.NewInvalidArgumentErrorf("Pubspec file is missing")
ErrPubspecFileTooLarge = util.NewInvalidArgumentErrorf("Pubspec file is too large")
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
var namePattern = regexp.MustCompile(`\A[a-zA-Z_][a-zA-Z0-9_]*\z`)
// https://github.com/dart-lang/pub-dev/blob/4d582302a8d10152a5cd6129f65bf4f4dbca239d/pkg/pub_package_reader/lib/pub_package_reader.dart#L143
const maxPubspecFileSize = 128 * 1024
// Package represents a Pub package
type Package struct {
Name string
Version string
Metadata *Metadata
}
// Metadata represents the metadata of a Pub package
type Metadata struct {
Description string `json:"description,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
DocumentationURL string `json:"documentation_url,omitempty"`
Readme string `json:"readme,omitempty"`
Pubspec any `json:"pubspec"`
}
type pubspecPackage struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Description string `yaml:"description"`
Homepage string `yaml:"homepage"`
Repository string `yaml:"repository"`
Documentation string `yaml:"documentation"`
}
// ParsePackage parses the Pub package file
func ParsePackage(r io.Reader) (*Package, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gzr.Close()
var p *Package
var readme string
tr := tar.NewReader(gzr)
for {
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.Typeflag != tar.TypeReg {
continue
}
if hd.Name == "pubspec.yaml" {
if hd.Size > maxPubspecFileSize {
return nil, ErrPubspecFileTooLarge
}
p, err = ParsePubspecMetadata(tr)
if err != nil {
return nil, err
}
} else if strings.EqualFold(hd.Name, "readme.md") {
data, err := util.ReadWithLimit(tr, 1024*1024)
if err != nil {
return nil, err
}
readme = string(data)
}
}
if p == nil {
return nil, ErrMissingPubspecFile
}
p.Metadata.Readme = readme
return p, nil
}
// ParsePubspecMetadata parses a Pubspec file to retrieve the metadata of a Pub package
func ParsePubspecMetadata(r io.Reader) (*Package, error) {
buf, err := io.ReadAll(io.LimitReader(r, maxPubspecFileSize))
if err != nil {
return nil, err
}
var p pubspecPackage
if err := yaml.Unmarshal(buf, &p); err != nil {
return nil, err
}
if !namePattern.MatchString(p.Name) {
return nil, ErrInvalidName
}
v, err := version.NewSemver(p.Version)
if err != nil {
return nil, ErrInvalidVersion
}
if !validation.IsValidURL(p.Homepage) {
p.Homepage = ""
}
if !validation.IsValidURL(p.Repository) {
p.Repository = ""
}
var pubspec any
if err := yaml.Unmarshal(buf, &pubspec); err != nil {
return nil, err
}
return &Package{
Name: p.Name,
Version: v.String(),
Metadata: &Metadata{
Description: p.Description,
ProjectURL: p.Homepage,
RepositoryURL: p.Repository,
DocumentationURL: p.Documentation,
Pubspec: pubspec,
},
}, nil
}
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pub
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
packageName = "gitea"
packageVersion = "1.0.1"
description = "Package Description"
projectURL = "https://gitea.com"
repositoryURL = "https://gitea.com/gitea/gitea"
documentationURL = "https://docs.gitea.com"
)
const pubspecContent = `name: ` + packageName + `
version: ` + packageVersion + `
description: ` + description + `
homepage: ` + projectURL + `
repository: ` + repositoryURL + `
documentation: ` + documentationURL + `
environment:
sdk: '>=2.16.0 <3.0.0'
dependencies:
flutter:
sdk: flutter
path: '>=1.8.0 <3.0.0'
dev_dependencies:
http: '>=0.13.0'`
func TestParsePackage(t *testing.T) {
createArchive := func(files map[string][]byte) io.Reader {
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
tw := tar.NewWriter(zw)
for filename, content := range files {
hdr := &tar.Header{
Name: filename,
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write(content)
}
tw.Close()
zw.Close()
return &buf
}
t.Run("MissingPubspecFile", func(t *testing.T) {
data := createArchive(map[string][]byte{"dummy.txt": {}})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrMissingPubspecFile)
})
t.Run("PubspecFileTooLarge", func(t *testing.T) {
data := createArchive(map[string][]byte{"pubspec.yaml": make([]byte, 200*1024)})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrPubspecFileTooLarge)
})
t.Run("InvalidPubspecFile", func(t *testing.T) {
data := createArchive(map[string][]byte{"pubspec.yaml": {}})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.Error(t, err)
})
t.Run("Valid", func(t *testing.T) {
data := createArchive(map[string][]byte{"pubspec.yaml": []byte(pubspecContent)})
pp, err := ParsePackage(data)
assert.NoError(t, err)
assert.NotNil(t, pp)
assert.Empty(t, pp.Metadata.Readme)
})
t.Run("ValidWithReadme", func(t *testing.T) {
data := createArchive(map[string][]byte{"pubspec.yaml": []byte(pubspecContent), "README.md": []byte("readme")})
pp, err := ParsePackage(data)
assert.NoError(t, err)
assert.NotNil(t, pp)
assert.Equal(t, "readme", pp.Metadata.Readme)
})
}
func TestParsePubspecMetadata(t *testing.T) {
t.Run("InvalidName", func(t *testing.T) {
for _, name := range []string{"123abc", "ab-cd"} {
pp, err := ParsePubspecMetadata(strings.NewReader(`name: ` + name))
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrInvalidName)
}
})
t.Run("InvalidVersion", func(t *testing.T) {
pp, err := ParsePubspecMetadata(strings.NewReader(`name: dummy
version: invalid`))
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrInvalidVersion)
})
t.Run("Valid", func(t *testing.T) {
pp, err := ParsePubspecMetadata(strings.NewReader(pubspecContent))
assert.NoError(t, err)
assert.NotNil(t, pp)
assert.Equal(t, packageName, pp.Name)
assert.Equal(t, packageVersion, pp.Version)
assert.Equal(t, description, pp.Metadata.Description)
assert.Equal(t, projectURL, pp.Metadata.ProjectURL)
assert.Equal(t, repositoryURL, pp.Metadata.RepositoryURL)
assert.Equal(t, documentationURL, pp.Metadata.DocumentationURL)
assert.NotNil(t, pp.Metadata.Pubspec)
})
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pypi
// Metadata represents the metadata of a PyPI package
type Metadata struct {
Author string `json:"author,omitempty"`
Description string `json:"description,omitempty"`
LongDescription string `json:"long_description,omitempty"`
Summary string `json:"summary,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
License string `json:"license,omitempty"`
RequiresPython string `json:"requires_python,omitempty"`
}
+339
View File
@@ -0,0 +1,339 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rpm
import (
"fmt"
"io"
"strings"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/validation"
"github.com/sassoftware/go-rpmutils"
)
const (
PropertyMetadata = "rpm.metadata"
PropertyGroup = "rpm.group"
PropertyArchitecture = "rpm.architecture"
SettingKeyPrivate = "rpm.key.private"
SettingKeyPublic = "rpm.key.public"
RepositoryPackage = "_rpm"
RepositoryVersion = "_repository"
)
const (
// Can't use the syscall constants because they are not available for windows build.
sIFMT = 0xf000
sIFDIR = 0x4000
sIXUSR = 0x40
sIXGRP = 0x8
sIXOTH = 0x1
)
// https://rpm-software-management.github.io/rpm/manual/spec.html
// https://refspecs.linuxbase.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/pkgformat.html
type Package struct {
Name string
Version string
VersionMetadata *VersionMetadata
FileMetadata *FileMetadata
}
type VersionMetadata struct {
License string `json:"license,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
Updates []*Update `json:"updates,omitempty"`
}
type FileMetadata struct {
Architecture string `json:"architecture,omitempty"`
Epoch string `json:"epoch,omitempty"`
Version string `json:"version,omitempty"`
Release string `json:"release,omitempty"`
Vendor string `json:"vendor,omitempty"`
Group string `json:"group,omitempty"`
Packager string `json:"packager,omitempty"`
SourceRpm string `json:"source_rpm,omitempty"`
BuildHost string `json:"build_host,omitempty"`
BuildTime uint64 `json:"build_time,omitempty"`
FileTime uint64 `json:"file_time,omitempty"`
InstalledSize uint64 `json:"installed_size,omitempty"`
ArchiveSize uint64 `json:"archive_size,omitempty"`
Provides []*Entry `json:"provide,omitempty"`
Requires []*Entry `json:"require,omitempty"`
Conflicts []*Entry `json:"conflict,omitempty"`
Obsoletes []*Entry `json:"obsolete,omitempty"`
Files []*File `json:"files,omitempty"`
Changelogs []*Changelog `json:"changelogs,omitempty"`
}
type Entry struct {
Name string `json:"name" xml:"name,attr"`
Flags string `json:"flags,omitempty" xml:"flags,attr,omitempty"`
Version string `json:"version,omitempty" xml:"ver,attr,omitempty"`
Epoch string `json:"epoch,omitempty" xml:"epoch,attr,omitempty"`
Release string `json:"release,omitempty" xml:"rel,attr,omitempty"`
}
type File struct {
Path string `json:"path" xml:",chardata"`
Type string `json:"type,omitempty" xml:"type,attr,omitempty"`
IsExecutable bool `json:"is_executable" xml:"-"`
}
type Changelog struct {
Author string `json:"author,omitempty" xml:"author,attr"`
Date timeutil.TimeStamp `json:"date,omitempty" xml:"date,attr"`
Text string `json:"text,omitempty" xml:",chardata"`
}
// ParsePackage parses the RPM package file
func ParsePackage(r io.Reader) (*Package, error) {
rpm, err := rpmutils.ReadRpm(r)
if err != nil {
return nil, err
}
nevra, err := rpm.Header.GetNEVRA()
if err != nil {
return nil, err
}
version := fmt.Sprintf("%s-%s", nevra.Version, nevra.Release)
if nevra.Epoch != "" && nevra.Epoch != "0" {
version = fmt.Sprintf("%s-%s", nevra.Epoch, version)
}
p := &Package{
Name: nevra.Name,
Version: version,
VersionMetadata: &VersionMetadata{
Summary: getString(rpm.Header, rpmutils.SUMMARY),
Description: getString(rpm.Header, rpmutils.DESCRIPTION),
License: getString(rpm.Header, rpmutils.LICENSE),
ProjectURL: getString(rpm.Header, rpmutils.URL),
},
FileMetadata: &FileMetadata{
Architecture: nevra.Arch,
Epoch: nevra.Epoch,
Version: nevra.Version,
Release: nevra.Release,
Vendor: getString(rpm.Header, rpmutils.VENDOR),
Group: getString(rpm.Header, rpmutils.GROUP),
Packager: getString(rpm.Header, rpmutils.PACKAGER),
SourceRpm: getString(rpm.Header, rpmutils.SOURCERPM),
BuildHost: getString(rpm.Header, rpmutils.BUILDHOST),
BuildTime: getUInt64(rpm.Header, rpmutils.BUILDTIME),
FileTime: getUInt64(rpm.Header, rpmutils.FILEMTIMES),
InstalledSize: getUInt64(rpm.Header, rpmutils.SIZE),
ArchiveSize: getUInt64(rpm.Header, rpmutils.SIG_PAYLOADSIZE),
Provides: getEntries(rpm.Header, rpmutils.PROVIDENAME, rpmutils.PROVIDEVERSION, rpmutils.PROVIDEFLAGS),
Requires: getEntries(rpm.Header, rpmutils.REQUIRENAME, rpmutils.REQUIREVERSION, rpmutils.REQUIREFLAGS),
Conflicts: getEntries(rpm.Header, rpmutils.CONFLICTNAME, rpmutils.CONFLICTVERSION, rpmutils.CONFLICTFLAGS),
Obsoletes: getEntries(rpm.Header, rpmutils.OBSOLETENAME, rpmutils.OBSOLETEVERSION, rpmutils.OBSOLETEFLAGS),
Files: getFiles(rpm.Header),
Changelogs: getChangelogs(rpm.Header),
},
}
if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
p.VersionMetadata.ProjectURL = ""
}
return p, nil
}
func getString(h *rpmutils.RpmHeader, tag int) string {
values, err := h.GetStrings(tag)
if err != nil || len(values) < 1 {
return ""
}
return values[0]
}
func getUInt64(h *rpmutils.RpmHeader, tag int) uint64 {
values, err := h.GetUint64s(tag)
if err != nil || len(values) < 1 {
return 0
}
return values[0]
}
func getEntries(h *rpmutils.RpmHeader, namesTag, versionsTag, flagsTag int) []*Entry {
names, err := h.GetStrings(namesTag)
if err != nil || len(names) == 0 {
return nil
}
flags, err := h.GetUint64s(flagsTag)
if err != nil || len(flags) == 0 {
return nil
}
versions, err := h.GetStrings(versionsTag)
if err != nil || len(versions) == 0 {
return nil
}
if len(names) != len(flags) || len(names) != len(versions) {
return nil
}
entries := make([]*Entry, 0, len(names))
for i := range names {
e := &Entry{
Name: names[i],
}
flags := flags[i]
if (flags&rpmutils.RPMSENSE_GREATER) != 0 && (flags&rpmutils.RPMSENSE_EQUAL) != 0 {
e.Flags = "GE"
} else if (flags&rpmutils.RPMSENSE_LESS) != 0 && (flags&rpmutils.RPMSENSE_EQUAL) != 0 {
e.Flags = "LE"
} else if (flags & rpmutils.RPMSENSE_GREATER) != 0 {
e.Flags = "GT"
} else if (flags & rpmutils.RPMSENSE_LESS) != 0 {
e.Flags = "LT"
} else if (flags & rpmutils.RPMSENSE_EQUAL) != 0 {
e.Flags = "EQ"
}
version := versions[i]
if version != "" {
parts := strings.Split(version, "-")
versionParts := strings.Split(parts[0], ":")
if len(versionParts) == 2 {
e.Version = versionParts[1]
e.Epoch = versionParts[0]
} else {
e.Version = versionParts[0]
e.Epoch = "0"
}
if len(parts) > 1 {
e.Release = parts[1]
}
}
entries = append(entries, e)
}
return entries
}
func getFiles(h *rpmutils.RpmHeader) []*File {
baseNames, _ := h.GetStrings(rpmutils.BASENAMES)
dirNames, _ := h.GetStrings(rpmutils.DIRNAMES)
dirIndexes, _ := h.GetUint32s(rpmutils.DIRINDEXES)
fileFlags, _ := h.GetUint32s(rpmutils.FILEFLAGS)
fileModes, _ := h.GetUint32s(rpmutils.FILEMODES)
files := make([]*File, 0, len(baseNames))
for i := range baseNames {
if len(dirIndexes) <= i {
continue
}
dirIndex := dirIndexes[i]
if len(dirNames) <= int(dirIndex) {
continue
}
var fileType string
var isExecutable bool
if i < len(fileFlags) && (fileFlags[i]&rpmutils.RPMFILE_GHOST) != 0 {
fileType = "ghost"
} else if i < len(fileModes) {
if (fileModes[i] & sIFMT) == sIFDIR {
fileType = "dir"
} else {
mode := fileModes[i] & ^uint32(sIFMT)
isExecutable = (mode&sIXUSR) != 0 || (mode&sIXGRP) != 0 || (mode&sIXOTH) != 0
}
}
files = append(files, &File{
Path: dirNames[dirIndex] + baseNames[i],
Type: fileType,
IsExecutable: isExecutable,
})
}
return files
}
func getChangelogs(h *rpmutils.RpmHeader) []*Changelog {
texts, err := h.GetStrings(rpmutils.CHANGELOGTEXT)
if err != nil || len(texts) == 0 {
return nil
}
authors, err := h.GetStrings(rpmutils.CHANGELOGNAME)
if err != nil || len(authors) == 0 {
return nil
}
times, err := h.GetUint32s(rpmutils.CHANGELOGTIME)
if err != nil || len(times) == 0 {
return nil
}
if len(texts) != len(authors) || len(texts) != len(times) {
return nil
}
changelogs := make([]*Changelog, 0, len(texts))
for i := range texts {
changelogs = append(changelogs, &Changelog{
Author: authors[i],
Date: timeutil.TimeStamp(times[i]),
Text: texts[i],
})
}
return changelogs
}
type DateAttr struct {
Date string `xml:"date,attr" json:"date"`
}
type Update struct {
From string `xml:"from,attr" json:"from"`
Status string `xml:"status,attr" json:"status"`
Type string `xml:"type,attr" json:"type"`
Version string `xml:"version,attr" json:"version"`
ID string `xml:"id" json:"id"`
Title string `xml:"title" json:"title"`
Severity string `xml:"severity" json:"severity"`
Description string `xml:"description" json:"description"`
Issued *DateAttr `xml:"issued" json:"issued"`
Updated *DateAttr `xml:"updated" json:"updated"`
References []*Reference `xml:"references>reference" json:"references"`
PkgList []*Collection `xml:"pkglist>collection" json:"pkg_list"`
}
type Reference struct {
Href string `xml:"href,attr" json:"href"`
ID string `xml:"id,attr" json:"id"`
Title string `xml:"title,attr" json:"title"`
Type string `xml:"type,attr" json:"type"`
}
type Collection struct {
Short string `xml:"short,attr" json:"short"`
Packages []*UpdatePackage `xml:"package" json:"packages"`
}
type UpdatePackage struct {
Arch string `xml:"arch,attr" json:"arch"`
Name string `xml:"name,attr" json:"name"`
Release string `xml:"release,attr" json:"release"`
Src string `xml:"src,attr" json:"src"`
Version string `xml:"version,attr" json:"version"`
Filename string `xml:"filename" json:"filename"`
}
+163
View File
@@ -0,0 +1,163 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rpm
import (
"bytes"
"compress/gzip"
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParsePackage(t *testing.T) {
base64RpmPackageContent := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
VNwk2zd2PdvZ9Sxnd3Z3NllNsmF3o6congVFsWFHRWwIImIXfRER0QcRfPBJEXvvBQvWSfZTT0VQ
8TF/MuU33zcz3+zOJGEe73lyuQBRBWKWRzDrEddjuVAkxLMc+lsFUOWfm5bvvReAalWECg/TsivU
dyKa0U61aVnl6wj0Uxe4nc8F92hZiaYE8CO/P0r7/Quegr0c7M/AvoCaGZEIWNGUqMHrhhGROIUT
Zc7gOAOraoQzCNZ0WdU0HpEI5jiB4zlek3gT85wqCBomhomxoGCs8wImWMImbxqKgXVNUKKaqShR
STKVKK9glFUNcf2g+/t27xs16v5x/eyOKftVGlIhyiuvvPLKK6+88sorr7zyyiuvvPKCO5HPnz+v
pGVhhXsTsFVeSstuWR9anwU+Bk3Vch5wTwL3JkHg+8C1gR8A169wj1KdpobAj4HbAT+Be5VewE+h
fz/g52AvBX4N9vHAb4AnA7+F8ePAH8BuA38ELgf+BLzQ50oIeBlw0OdAOXAlP57AGuCsbwGtbgCu
DrwRuAb4bwau6T/PwFbgWsDXgWuD/y3gOmC/B1wI/Bi4AcT3Arih3z9YCNzI9w9m/YKUG4Nd9N9z
pSZgHwrcFPgccFt//OADGE+F/q+Ao+D/FrijzwV1gbv4/QvaAHcFDgF3B5aB+wB3Be7rz1dQCtwP
eDxwMcw3GbgU7AasdwzYE8DjwT4L/CeAvRx4IvBCYA3iWQds+FzpDjABfghsAj8BTgA/A/b8+StX
A84A1wKe5s9fuRB4JpzHZv55rL8a/Dv49vpn/PErR4BvQX8Z+Db4l2W5CH2/f0W5+1fEoeFDBzFp
rE/FMcK4mWQSOzN+aDOIqztW2rPsFKIyqh7sQERR42RVMSKihnzVHlQ8Ag0YLBYNEIajkhmuR5Io
7nlpt2M4nJs0ZNkoYaUyZahMlSfJImr1n1WjFVNCPCaTZgYNGdGL8YN2mX8WHfA/C7ViHJK0pxHG
SrkeTiSI4T+7ubf85yrzRCQRQ5EVxVAjvIBVRY/KRFAVReIkhfARSddNSceayQkGliIKb0q8RAxJ
5QWNVxHIsW3Pz369bw+5jh5y0klE9Znqm0dF57b0HbGy2A5lVUBTZZrqZjdUjYoprFmpsBtHP5d0
+ISltS2yk2mHuC4x+lgJMhgnidvuqy3b0suK0bm+tw3FMxI2zjm7/fA0MtQhplX2s7nYLZ2ZC0yg
CxJZDokhORTJlrlcCvG5OieGBERlVCs7CfuS6WzQ/T2j+9f92BWxTFEcp2IkYccYGp2LYySEfreq
irue4WRF5XkpKovw2wgpq2rZBI8bQZkzxEkiYaNwxnXCCVvHidzIiB3CM2yMYdNWmjDsaLovaE4c
x3a6mLaTxB7rEj3jWN4M2p7uwPaa1GfI8BHFfcZMKhkycnhR7y781/a+A4t7FpWWTupRUtKbegwZ
XMKwJinTSe70uhRcj55qNu3YHtE922Fdz7FTMTq9Q3TbMdiYrrPudMvT44S6u2miu138eC0tTN9D
2CFGHHtQsHHsGCRFDFbXuT9wx6mUTZfseydlkWZeJkW6xOgYjqXT+LA7I6XHaUx2xmUzqelWymA9
rCXI9+D1BHbjsITssqhBNysw0tOWjcpmIh6+aViYPfftw8ZSGfRVPUqKiosZj5R5qGmk/8AjjRbZ
d8b3vvngdPHx3HvMeCarIk7VVSwbgoZVkceEVyOmyUmGxBGNYDVKSFSOGlIkGqWnUZFkiY/wsmhK
Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
9tMDyaXb7OAlk5acuPn57ss9mw6Wym0m1Fq2cej7tUt2LL4/b8enXU2fndk+fvv57ndnt55/cQob
7tpp/pEjDS7cGPZ6BY430+7danDq6f42Nw49b9F7zp6BiKpJb9s5P0AYN2+L159cnrur636rx+v1
7ae1K28QbMMcqI8CqwIrgwg9nTOp8Oj9q81plUY7ZuwXN8Vvs8wbAAA=`
rpmPackageContent, err := base64.StdEncoding.DecodeString(base64RpmPackageContent)
assert.NoError(t, err)
zr, err := gzip.NewReader(bytes.NewReader(rpmPackageContent))
assert.NoError(t, err)
p, err := ParsePackage(zr)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, "gitea-test", p.Name)
assert.Equal(t, "1.0.2-1", p.Version)
assert.NotNil(t, p.VersionMetadata)
assert.NotNil(t, p.FileMetadata)
assert.Equal(t, "MIT", p.VersionMetadata.License)
assert.Equal(t, "https://gitea.io", p.VersionMetadata.ProjectURL)
assert.Equal(t, "RPM package summary", p.VersionMetadata.Summary)
assert.Equal(t, "RPM package description", p.VersionMetadata.Description)
assert.Equal(t, "x86_64", p.FileMetadata.Architecture)
assert.Equal(t, "0", p.FileMetadata.Epoch)
assert.Equal(t, "1.0.2", p.FileMetadata.Version)
assert.Equal(t, "1", p.FileMetadata.Release)
assert.Empty(t, p.FileMetadata.Vendor)
assert.Equal(t, "KN4CK3R", p.FileMetadata.Packager)
assert.Equal(t, "gitea-test-1.0.2-1.src.rpm", p.FileMetadata.SourceRpm)
assert.Equal(t, "e44b1687d04b", p.FileMetadata.BuildHost)
assert.EqualValues(t, 1678225964, p.FileMetadata.BuildTime)
assert.EqualValues(t, 1678225964, p.FileMetadata.FileTime)
assert.EqualValues(t, 13, p.FileMetadata.InstalledSize)
assert.EqualValues(t, 272, p.FileMetadata.ArchiveSize)
assert.Empty(t, p.FileMetadata.Conflicts)
assert.Empty(t, p.FileMetadata.Obsoletes)
assert.ElementsMatch(
t,
[]*Entry{
{
Name: "gitea-test",
Flags: "EQ",
Version: "1.0.2",
Epoch: "0",
Release: "1",
},
{
Name: "gitea-test(x86-64)",
Flags: "EQ",
Version: "1.0.2",
Epoch: "0",
Release: "1",
},
},
p.FileMetadata.Provides,
)
assert.ElementsMatch(
t,
[]*Entry{
{
Name: "/bin/sh",
},
{
Name: "/bin/sh",
},
{
Name: "/bin/sh",
},
{
Name: "rpmlib(CompressedFileNames)",
Flags: "LE",
Version: "3.0.4",
Epoch: "0",
Release: "1",
},
{
Name: "rpmlib(FileDigests)",
Flags: "LE",
Version: "4.6.0",
Epoch: "0",
Release: "1",
},
{
Name: "rpmlib(PayloadFilesHavePrefix)",
Flags: "LE",
Version: "4.0",
Epoch: "0",
Release: "1",
},
{
Name: "rpmlib(PayloadIsXz)",
Flags: "LE",
Version: "5.2",
Epoch: "0",
Release: "1",
},
},
p.FileMetadata.Requires,
)
assert.ElementsMatch(
t,
[]*File{
{
Path: "/usr/local/bin/hello",
IsExecutable: true,
},
},
p.FileMetadata.Files,
)
assert.ElementsMatch(
t,
[]*Changelog{
{
Author: "KN4CK3R <dummy@gitea.io>",
Date: 1678276800,
Text: "- Changelog message.",
},
},
p.FileMetadata.Changelogs,
)
}
+311
View File
@@ -0,0 +1,311 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rubygems
import (
"bufio"
"bytes"
"io"
"reflect"
"gitea.dev/modules/util"
)
const (
majorVersion = 4
minorVersion = 8
typeNil = '0'
typeTrue = 'T'
typeFalse = 'F'
typeFixnum = 'i'
typeString = '"'
typeSymbol = ':'
typeSymbolLink = ';'
typeArray = '['
typeIVar = 'I'
typeUserMarshal = 'U'
typeUserDef = 'u'
typeObject = 'o'
)
var (
// ErrUnsupportedType indicates an unsupported type
ErrUnsupportedType = util.NewInvalidArgumentErrorf("type is unsupported")
// ErrInvalidIntRange indicates an invalid number range
ErrInvalidIntRange = util.NewInvalidArgumentErrorf("number is not in valid range")
)
// RubyUserMarshal is a Ruby object that has a marshal_load function.
type RubyUserMarshal struct {
Name string
Value any
}
// RubyUserDef is a Ruby object that has a _load function.
type RubyUserDef struct {
Name string
Value any
}
// RubyObject is a default Ruby object.
type RubyObject struct {
Name string
Member map[string]any
}
// MarshalEncoder mimics Rubys Marshal class.
// Note: Only supports types used by the RubyGems package registry.
type MarshalEncoder struct {
w *bufio.Writer
symbols map[string]int
}
// NewMarshalEncoder creates a new MarshalEncoder
func NewMarshalEncoder(w io.Writer) *MarshalEncoder {
return &MarshalEncoder{
w: bufio.NewWriter(w),
symbols: map[string]int{},
}
}
// Encode encodes the given type
func (e *MarshalEncoder) Encode(v any) error {
if _, err := e.w.Write([]byte{majorVersion, minorVersion}); err != nil {
return err
}
if err := e.marshal(v); err != nil {
return err
}
return e.w.Flush()
}
func (e *MarshalEncoder) marshal(v any) error {
if v == nil {
return e.marshalNil()
}
val := reflect.ValueOf(v)
typ := reflect.TypeOf(v)
if typ.Kind() == reflect.Pointer {
val = val.Elem()
typ = typ.Elem()
}
switch typ.Kind() {
case reflect.Bool:
return e.marshalBool(val.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
return e.marshalInt(val.Int())
case reflect.String:
return e.marshalString(val.String())
case reflect.Slice, reflect.Array:
return e.marshalArray(val)
}
switch typ.Name() {
case "RubyUserMarshal":
return e.marshalUserMarshal(val.Interface().(RubyUserMarshal))
case "RubyUserDef":
return e.marshalUserDef(val.Interface().(RubyUserDef))
case "RubyObject":
return e.marshalObject(val.Interface().(RubyObject))
}
return ErrUnsupportedType
}
func (e *MarshalEncoder) marshalNil() error {
return e.w.WriteByte(typeNil)
}
func (e *MarshalEncoder) marshalBool(b bool) error {
if b {
return e.w.WriteByte(typeTrue)
}
return e.w.WriteByte(typeFalse)
}
func (e *MarshalEncoder) marshalInt(i int64) error {
if err := e.w.WriteByte(typeFixnum); err != nil {
return err
}
return e.marshalIntInternal(i)
}
func (e *MarshalEncoder) marshalIntInternal(i int64) error {
if i == 0 {
return e.w.WriteByte(0)
} else if 0 < i && i < 123 {
return e.w.WriteByte(byte(i + 5))
} else if -124 < i && i <= -1 {
return e.w.WriteByte(byte(i - 5))
}
var length int
if 122 < i && i <= 0xff {
length = 1
} else if 0xff < i && i <= 0xffff {
length = 2
} else if 0xffff < i && i <= 0xffffff {
length = 3
} else if 0xffffff < i && i <= 0x3fffffff {
length = 4
} else if -0x100 <= i && i < -123 {
length = -1
} else if -0x10000 <= i && i < -0x100 {
length = -2
} else if -0x1000000 <= i && i < -0x100000 {
length = -3
} else if -0x40000000 <= i && i < -0x1000000 {
length = -4
} else {
return ErrInvalidIntRange
}
if err := e.w.WriteByte(byte(length)); err != nil {
return err
}
if length < 0 {
length = -length
}
for c := 0; c < length; c++ {
if err := e.w.WriteByte(byte(i >> uint(8*c) & 0xff)); err != nil {
return err
}
}
return nil
}
func (e *MarshalEncoder) marshalString(str string) error {
if err := e.w.WriteByte(typeIVar); err != nil {
return err
}
if err := e.marshalRawString(str); err != nil {
return err
}
if err := e.marshalIntInternal(1); err != nil {
return err
}
if err := e.marshalSymbol("E"); err != nil {
return err
}
return e.marshalBool(true)
}
func (e *MarshalEncoder) marshalRawString(str string) error {
if err := e.w.WriteByte(typeString); err != nil {
return err
}
if err := e.marshalIntInternal(int64(len(str))); err != nil {
return err
}
_, err := e.w.WriteString(str)
return err
}
func (e *MarshalEncoder) marshalSymbol(str string) error {
if index, ok := e.symbols[str]; ok {
if err := e.w.WriteByte(typeSymbolLink); err != nil {
return err
}
return e.marshalIntInternal(int64(index))
}
e.symbols[str] = len(e.symbols)
if err := e.w.WriteByte(typeSymbol); err != nil {
return err
}
if err := e.marshalIntInternal(int64(len(str))); err != nil {
return err
}
_, err := e.w.WriteString(str)
return err
}
func (e *MarshalEncoder) marshalArray(arr reflect.Value) error {
if err := e.w.WriteByte(typeArray); err != nil {
return err
}
length := arr.Len()
if err := e.marshalIntInternal(int64(length)); err != nil {
return err
}
for i := range length {
if err := e.marshal(arr.Index(i).Interface()); err != nil {
return err
}
}
return nil
}
func (e *MarshalEncoder) marshalUserMarshal(userMarshal RubyUserMarshal) error {
if err := e.w.WriteByte(typeUserMarshal); err != nil {
return err
}
if err := e.marshalSymbol(userMarshal.Name); err != nil {
return err
}
return e.marshal(userMarshal.Value)
}
func (e *MarshalEncoder) marshalUserDef(userDef RubyUserDef) error {
var buf bytes.Buffer
if err := NewMarshalEncoder(&buf).Encode(userDef.Value); err != nil {
return err
}
if err := e.w.WriteByte(typeUserDef); err != nil {
return err
}
if err := e.marshalSymbol(userDef.Name); err != nil {
return err
}
if err := e.marshalIntInternal(int64(buf.Len())); err != nil {
return err
}
_, err := e.w.Write(buf.Bytes())
return err
}
func (e *MarshalEncoder) marshalObject(obj RubyObject) error {
if err := e.w.WriteByte(typeObject); err != nil {
return err
}
if err := e.marshalSymbol(obj.Name); err != nil {
return err
}
if err := e.marshalIntInternal(int64(len(obj.Member))); err != nil {
return err
}
for k, v := range obj.Member {
if err := e.marshalSymbol(k); err != nil {
return err
}
if err := e.marshal(v); err != nil {
return err
}
}
return nil
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rubygems
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMinimalEncoder(t *testing.T) {
cases := []struct {
Value any
Expected []byte
Error error
}{
{
Value: nil,
Expected: []byte{4, 8, 0x30},
},
{
Value: true,
Expected: []byte{4, 8, 'T'},
},
{
Value: false,
Expected: []byte{4, 8, 'F'},
},
{
Value: 0,
Expected: []byte{4, 8, 'i', 0},
},
{
Value: 1,
Expected: []byte{4, 8, 'i', 6},
},
{
Value: -1,
Expected: []byte{4, 8, 'i', 0xfa},
},
{
Value: 0x1fffffff,
Expected: []byte{4, 8, 'i', 4, 0xff, 0xff, 0xff, 0x1f},
},
{
Value: 0x41000000,
Error: ErrInvalidIntRange,
},
{
Value: "test",
Expected: []byte{4, 8, 'I', '"', 9, 't', 'e', 's', 't', 6, ':', 6, 'E', 'T'},
},
{
Value: []int{1, 2},
Expected: []byte{4, 8, '[', 7, 'i', 6, 'i', 7},
},
{
Value: &RubyUserMarshal{
Name: "Test",
Value: 4,
},
Expected: []byte{4, 8, 'U', ':', 9, 'T', 'e', 's', 't', 'i', 9},
},
{
Value: &RubyUserDef{
Name: "Test",
Value: 4,
},
Expected: []byte{4, 8, 'u', ':', 9, 'T', 'e', 's', 't', 9, 4, 8, 'i', 9},
},
{
Value: &RubyObject{
Name: "Test",
Member: map[string]any{
"test": 4,
},
},
Expected: []byte{4, 8, 'o', ':', 9, 'T', 'e', 's', 't', 6, ':', 9, 't', 'e', 's', 't', 'i', 9},
},
{
Value: &struct {
Name string
}{
"test",
},
Error: ErrUnsupportedType,
},
}
for i, c := range cases {
var b bytes.Buffer
err := NewMarshalEncoder(&b).Encode(c.Value)
assert.ErrorIs(t, err, c.Error)
assert.Equal(t, c.Expected, b.Bytes(), "case %d", i)
}
}
+223
View File
@@ -0,0 +1,223 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rubygems
import (
"archive/tar"
"compress/gzip"
"io"
"regexp"
"strings"
"sync"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"go.yaml.in/yaml/v4"
)
var (
// ErrMissingMetadataFile indicates a missing metadata.gz file
ErrMissingMetadataFile = util.NewInvalidArgumentErrorf("metadata.gz file is missing")
// ErrInvalidName indicates an invalid id in the metadata.gz file
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
// ErrInvalidVersion indicates an invalid version in the metadata.gz file
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
var versionMatcher = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`\A[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z`)
})
// Package represents a RubyGems package
type Package struct {
Name string
Version string
Metadata *Metadata
}
// Metadata represents the metadata of a RubyGems package
type Metadata struct {
Platform string `json:"platform,omitempty"`
Description string `json:"description,omitempty"`
Summary string `json:"summary,omitempty"`
Authors []string `json:"authors,omitempty"`
Licenses []string `json:"licenses,omitempty"`
RequiredRubyVersion []VersionRequirement `json:"required_ruby_version,omitempty"`
RequiredRubygemsVersion []VersionRequirement `json:"required_rubygems_version,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
RuntimeDependencies []Dependency `json:"runtime_dependencies,omitempty"`
DevelopmentDependencies []Dependency `json:"development_dependencies,omitempty"`
}
// VersionRequirement represents a version restriction
type VersionRequirement struct {
Restriction string `json:"restriction"`
Version string `json:"version"`
}
// Dependency represents a dependency of a RubyGems package
type Dependency struct {
Name string `json:"name"`
Version []VersionRequirement `json:"version"`
}
type gemspec struct {
Name string `yaml:"name"`
Version struct {
Version string `yaml:"version"`
} `yaml:"version"`
Platform string `yaml:"platform"`
Authors []string `yaml:"authors"`
Autorequire any `yaml:"autorequire"`
Bindir string `yaml:"bindir"`
CertChain []any `yaml:"cert_chain"`
Date string `yaml:"date"`
Dependencies []struct {
Name string `yaml:"name"`
Requirement requirement `yaml:"requirement"`
Type string `yaml:"type"`
Prerelease bool `yaml:"prerelease"`
VersionRequirements requirement `yaml:"version_requirements"`
} `yaml:"dependencies"`
Description string `yaml:"description"`
Executables []string `yaml:"executables"`
Extensions []any `yaml:"extensions"`
ExtraRdocFiles []string `yaml:"extra_rdoc_files"`
Files []string `yaml:"files"`
Homepage string `yaml:"homepage"`
Licenses []string `yaml:"licenses"`
Metadata struct {
BugTrackerURI string `yaml:"bug_tracker_uri"`
ChangelogURI string `yaml:"changelog_uri"`
DocumentationURI string `yaml:"documentation_uri"`
SourceCodeURI string `yaml:"source_code_uri"`
} `yaml:"metadata"`
PostInstallMessage any `yaml:"post_install_message"`
RdocOptions []any `yaml:"rdoc_options"`
RequirePaths []string `yaml:"require_paths"`
RequiredRubyVersion requirement `yaml:"required_ruby_version"`
RequiredRubygemsVersion requirement `yaml:"required_rubygems_version"`
Requirements []any `yaml:"requirements"`
RubygemsVersion string `yaml:"rubygems_version"`
SigningKey any `yaml:"signing_key"`
SpecificationVersion int `yaml:"specification_version"`
Summary string `yaml:"summary"`
TestFiles []any `yaml:"test_files"`
}
type requirement struct {
Requirements [][]any `yaml:"requirements"`
}
// AsVersionRequirement converts into []VersionRequirement
func (r requirement) AsVersionRequirement() []VersionRequirement {
requirements := make([]VersionRequirement, 0, len(r.Requirements))
for _, req := range r.Requirements {
if len(req) != 2 {
continue
}
restriction, ok := req[0].(string)
if !ok {
continue
}
vm, ok := req[1].(map[string]any)
if !ok {
continue
}
versionInt, ok := vm["version"]
if !ok {
continue
}
version, ok := versionInt.(string)
if !ok || (version == "0" && restriction == ">=") {
continue
}
requirements = append(requirements, VersionRequirement{
Restriction: restriction,
Version: version,
})
}
return requirements
}
// ParsePackageMetaData parses the metadata of a Gem package file
func ParsePackageMetaData(r io.Reader) (*Package, error) {
archive := tar.NewReader(r)
for {
hdr, err := archive.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hdr.Name == "metadata.gz" {
return parseMetadataFile(archive)
}
}
return nil, ErrMissingMetadataFile
}
func parseMetadataFile(r io.Reader) (*Package, error) {
zr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer zr.Close()
var spec gemspec
if err := yaml.NewDecoder(zr).Decode(&spec); err != nil {
return nil, err
}
if len(spec.Name) == 0 || strings.Contains(spec.Name, "/") {
return nil, ErrInvalidName
}
if !versionMatcher().MatchString(spec.Version.Version) {
return nil, ErrInvalidVersion
}
if !validation.IsValidURL(spec.Homepage) {
spec.Homepage = ""
}
if !validation.IsValidURL(spec.Metadata.SourceCodeURI) {
spec.Metadata.SourceCodeURI = ""
}
m := &Metadata{
Platform: spec.Platform,
Description: spec.Description,
Summary: spec.Summary,
Authors: spec.Authors,
Licenses: spec.Licenses,
ProjectURL: spec.Homepage,
RequiredRubyVersion: spec.RequiredRubyVersion.AsVersionRequirement(),
RequiredRubygemsVersion: spec.RequiredRubygemsVersion.AsVersionRequirement(),
DevelopmentDependencies: make([]Dependency, 0, 5),
RuntimeDependencies: make([]Dependency, 0, 5),
}
for _, gemdep := range spec.Dependencies {
dep := Dependency{
Name: gemdep.Name,
Version: gemdep.Requirement.AsVersionRequirement(),
}
if gemdep.Type == ":runtime" {
m.RuntimeDependencies = append(m.RuntimeDependencies, dep)
} else {
m.DevelopmentDependencies = append(m.DevelopmentDependencies, dep)
}
}
return &Package{
Name: spec.Name,
Version: spec.Version.Version,
Metadata: m,
}, nil
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rubygems
import (
"testing"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
func TestParsePackageMetaData(t *testing.T) {
t.Run("MissingMetadataFile", func(t *testing.T) {
data := test.WriteTarArchive(map[string]string{"dummy.txt": ""})
rp, err := ParsePackageMetaData(data)
assert.ErrorIs(t, err, ErrMissingMetadataFile)
assert.Nil(t, rp)
})
t.Run("Valid", func(t *testing.T) {
metadataContent := test.CompressGzip(`
name: g
version:
version: 1
`)
data := test.WriteTarArchive(map[string]string{
"metadata.gz": metadataContent.String(),
})
rp, err := ParsePackageMetaData(data)
assert.NoError(t, err)
assert.NotNil(t, rp)
})
}
func TestParseMetadataFile(t *testing.T) {
content := test.CompressGzip(`--- !ruby/object:Gem::Specification
name: gitea
version: !ruby/object:Gem::Version
version: 1.0.5
platform: ruby
authors:
- Gitea
autorequire:
bindir: bin
cert_chain: []
date: 2021-08-23 00:00:00.000000000 Z
dependencies:
- !ruby/object:Gem::Dependency
name: runtime-dep
requirement: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: 1.2.0
- - "<"
- !ruby/object:Gem::Version
version: '2.0'
type: :runtime
prerelease: false
version_requirements: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: 1.2.0
- - "<"
- !ruby/object:Gem::Version
version: '2.0'
- !ruby/object:Gem::Dependency
name: dev-dep
requirement: !ruby/object:Gem::Requirement
requirements:
- - "~>"
- !ruby/object:Gem::Version
version: '0'
type: :development
prerelease: false
version_requirements: !ruby/object:Gem::Requirement
requirements:
- - "~>"
- !ruby/object:Gem::Version
version: '5.2'
description: RubyGems package test
email: rubygems@gitea.io
executables: []
extensions: []
extra_rdoc_files: []
files:
- lib/gitea.rb
homepage: https://gitea.io/
licenses:
- MIT
metadata: {}
post_install_message:
rdoc_options: []
require_paths:
- lib
required_ruby_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: 2.3.0
required_rubygems_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: '0'
requirements: []
rubyforge_project:
rubygems_version: 2.7.6.2
signing_key:
specification_version: 4
summary: Gitea package
test_files: []
`)
rp, err := parseMetadataFile(content)
assert.NoError(t, err)
assert.NotNil(t, rp)
assert.Equal(t, "gitea", rp.Name)
assert.Equal(t, "1.0.5", rp.Version)
assert.Equal(t, "ruby", rp.Metadata.Platform)
assert.Equal(t, "Gitea package", rp.Metadata.Summary)
assert.Equal(t, "RubyGems package test", rp.Metadata.Description)
assert.Equal(t, []string{"Gitea"}, rp.Metadata.Authors)
assert.Equal(t, "https://gitea.io/", rp.Metadata.ProjectURL)
assert.Equal(t, []string{"MIT"}, rp.Metadata.Licenses)
assert.Empty(t, rp.Metadata.RequiredRubygemsVersion)
assert.Len(t, rp.Metadata.RequiredRubyVersion, 1)
assert.Equal(t, ">=", rp.Metadata.RequiredRubyVersion[0].Restriction)
assert.Equal(t, "2.3.0", rp.Metadata.RequiredRubyVersion[0].Version)
assert.Len(t, rp.Metadata.RuntimeDependencies, 1)
assert.Equal(t, "runtime-dep", rp.Metadata.RuntimeDependencies[0].Name)
assert.Len(t, rp.Metadata.RuntimeDependencies[0].Version, 2)
assert.Equal(t, ">=", rp.Metadata.RuntimeDependencies[0].Version[0].Restriction)
assert.Equal(t, "1.2.0", rp.Metadata.RuntimeDependencies[0].Version[0].Version)
assert.Equal(t, "<", rp.Metadata.RuntimeDependencies[0].Version[1].Restriction)
assert.Equal(t, "2.0", rp.Metadata.RuntimeDependencies[0].Version[1].Version)
assert.Len(t, rp.Metadata.DevelopmentDependencies, 1)
assert.Equal(t, "dev-dep", rp.Metadata.DevelopmentDependencies[0].Name)
assert.Len(t, rp.Metadata.DevelopmentDependencies[0].Version, 1)
assert.Equal(t, "~>", rp.Metadata.DevelopmentDependencies[0].Version[0].Restriction)
assert.Equal(t, "0", rp.Metadata.DevelopmentDependencies[0].Version[0].Version)
}
+228
View File
@@ -0,0 +1,228 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swift
import (
"archive/zip"
"fmt"
"io"
"path"
"regexp"
"strings"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"github.com/hashicorp/go-version"
)
var (
ErrMissingManifestFile = util.NewInvalidArgumentErrorf("Package.swift file is missing")
ErrManifestFileTooLarge = util.NewInvalidArgumentErrorf("Package.swift file is too large")
ErrInvalidManifestVersion = util.NewInvalidArgumentErrorf("manifest version is invalid")
manifestPattern = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`)
toolsVersionPattern = regexp.MustCompile(`\A// swift-tools-version:(\d+(?:\.\d+)?(?:\.\d+)?)`)
)
const (
maxManifestFileSize = 128 * 1024
PropertyScope = "swift.scope"
PropertyName = "swift.name"
PropertyRepositoryURL = "swift.repository_url"
)
// Package represents a Swift package
type Package struct {
RepositoryURLs []string
Metadata *Metadata
}
// Metadata represents the metadata of a Swift package
type Metadata struct {
Description string `json:"description,omitempty"`
Keywords []string `json:"keywords,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
License string `json:"license,omitempty"`
LicenseURL string `json:"license_url,omitempty"`
Author Person `json:"author"`
Manifests map[string]*Manifest `json:"manifests,omitempty"`
}
// Manifest represents a Package.swift file
type Manifest struct {
Content string `json:"content"`
ToolsVersion string `json:"tools_version,omitempty"`
}
// https://schema.org/SoftwareSourceCode
type SoftwareSourceCode struct {
Context []string `json:"@context"`
Type string `json:"@type"`
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
Keywords []string `json:"keywords,omitempty"`
CodeRepository string `json:"codeRepository,omitempty"`
License string `json:"license,omitempty"`
LicenseURL string `json:"licenseURL,omitempty"`
Author *Person `json:"author,omitempty"`
ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
RepositoryURLs []string `json:"repositoryURLs,omitempty"`
}
// https://schema.org/ProgrammingLanguage
type ProgrammingLanguage struct {
Type string `json:"@type"`
Name string `json:"name"`
URL string `json:"url"`
}
// https://schema.org/Person
type Person struct {
Type string `json:"@type,omitempty"`
Name string `json:"name,omitempty"` // inherited from https://schema.org/Thing
GivenName string `json:"givenName,omitempty"`
MiddleName string `json:"middleName,omitempty"`
FamilyName string `json:"familyName,omitempty"`
}
func (p Person) String() string {
var sb strings.Builder
if p.GivenName != "" {
sb.WriteString(p.GivenName)
}
if p.MiddleName != "" {
if sb.Len() > 0 {
sb.WriteRune(' ')
}
sb.WriteString(p.MiddleName)
}
if p.FamilyName != "" {
if sb.Len() > 0 {
sb.WriteRune(' ')
}
sb.WriteString(p.FamilyName)
}
return sb.String()
}
// ParsePackage parses the Swift package upload
func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
zr, err := zip.NewReader(sr, size)
if err != nil {
return nil, err
}
p := &Package{
Metadata: &Metadata{
Manifests: make(map[string]*Manifest),
},
}
for _, file := range zr.File {
manifestMatch := manifestPattern.FindStringSubmatch(path.Base(file.Name))
if len(manifestMatch) == 0 {
continue
}
if file.UncompressedSize64 > maxManifestFileSize {
return nil, ErrManifestFileTooLarge
}
f, err := zr.Open(file.Name)
if err != nil {
return nil, err
}
content, err := io.ReadAll(f)
if err := f.Close(); err != nil {
return nil, err
}
if err != nil {
return nil, err
}
swiftVersion := ""
if len(manifestMatch) == 2 && manifestMatch[1] != "" {
v, err := version.NewSemver(manifestMatch[1])
if err != nil {
return nil, ErrInvalidManifestVersion
}
swiftVersion = TrimmedVersionString(v)
}
manifest := &Manifest{
Content: string(content),
}
toolsMatch := toolsVersionPattern.FindStringSubmatch(manifest.Content)
if len(toolsMatch) == 2 {
v, err := version.NewSemver(toolsMatch[1])
if err != nil {
return nil, ErrInvalidManifestVersion
}
manifest.ToolsVersion = TrimmedVersionString(v)
}
p.Metadata.Manifests[swiftVersion] = manifest
}
if _, found := p.Metadata.Manifests[""]; !found {
return nil, ErrMissingManifestFile
}
if mr != nil {
var ssc *SoftwareSourceCode
if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
return nil, err
}
p.Metadata.Description = ssc.Description
p.Metadata.Keywords = ssc.Keywords
p.Metadata.License = ssc.License
p.Metadata.LicenseURL = ssc.LicenseURL
if ssc.Author != nil {
author := Person{
Name: ssc.Author.Name,
GivenName: ssc.Author.GivenName,
MiddleName: ssc.Author.MiddleName,
FamilyName: ssc.Author.FamilyName,
}
// If Name is not provided, generate it from individual name components
if author.Name == "" {
author.Name = author.String()
}
p.Metadata.Author = author
}
p.Metadata.RepositoryURL = ssc.CodeRepository
if !validation.IsValidURL(p.Metadata.RepositoryURL) {
p.Metadata.RepositoryURL = ""
}
if !validation.IsValidURL(p.Metadata.LicenseURL) {
p.Metadata.LicenseURL = ""
}
p.RepositoryURLs = ssc.RepositoryURLs
}
return p, nil
}
// TrimmedVersionString returns the version string without the patch segment if it is zero
func TrimmedVersionString(v *version.Version) string {
segments := v.Segments64()
var b strings.Builder
fmt.Fprintf(&b, "%d.%d", segments[0], segments[1])
if segments[2] != 0 {
fmt.Fprintf(&b, ".%d", segments[2])
}
return b.String()
}
+226
View File
@@ -0,0 +1,226 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swift
import (
"bytes"
"strings"
"testing"
"gitea.dev/modules/test"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
const (
packageName = "gitea"
packageVersion = "1.0.1"
packageDescription = "Package Description"
packageRepositoryURL = "https://gitea.io/gitea/gitea"
packageLicenseURL = "https://opensource.org/license/mit"
packageAuthor = "KN4CK3R"
packageLicense = "MIT"
)
func TestParsePackage(t *testing.T) {
t.Run("MissingManifestFile", func(t *testing.T) {
data := test.WriteZipArchive(map[string]string{"dummy.txt": ""})
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrMissingManifestFile)
})
t.Run("ManifestFileTooLarge", func(t *testing.T) {
data := test.WriteZipArchive(map[string]string{
"Package.swift": strings.Repeat("a", maxManifestFileSize+1),
})
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrManifestFileTooLarge)
})
t.Run("WithoutMetadata", func(t *testing.T) {
content1 := "// swift-tools-version:5.7\n//\n// Package.swift"
content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
data := test.WriteZipArchive(map[string]string{
"Package.swift": content1,
"Package@swift-5.5.swift": content2,
})
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.NotNil(t, p.Metadata)
assert.Empty(t, p.RepositoryURLs)
assert.Len(t, p.Metadata.Manifests, 2)
m := p.Metadata.Manifests[""]
assert.Equal(t, "5.7", m.ToolsVersion)
assert.Equal(t, content1, m.Content)
m = p.Metadata.Manifests["5.5"]
assert.Equal(t, "5.6", m.ToolsVersion)
assert.Equal(t, content2, m.Content)
})
t.Run("WithMetadata", func(t *testing.T) {
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
p, err := ParsePackage(
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","licenseURL":"`+packageLicenseURL+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.NotNil(t, p.Metadata)
assert.Len(t, p.Metadata.Manifests, 1)
m := p.Metadata.Manifests[""]
assert.Equal(t, "5.7", m.ToolsVersion)
assert.Equal(t, packageDescription, p.Metadata.Description)
assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
assert.Equal(t, packageLicense, p.Metadata.License)
assert.Equal(t, packageLicenseURL, p.Metadata.LicenseURL)
assert.Equal(t, packageAuthor, p.Metadata.Author.Name)
assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
assert.ElementsMatch(t, []string{packageRepositoryURL}, p.RepositoryURLs)
})
t.Run("WithExplicitNameField", func(t *testing.T) {
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
authorName := "John Doe"
p, err := ParsePackage(
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","author":{"name":"`+authorName+`","givenName":"John","familyName":"Doe"}}`),
)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, authorName, p.Metadata.Author.Name)
assert.Equal(t, "John", p.Metadata.Author.GivenName)
assert.Equal(t, "Doe", p.Metadata.Author.FamilyName)
})
t.Run("WithEmptyJSONMetadata", func(t *testing.T) {
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
p, err := ParsePackage(
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{}`),
)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.NotNil(t, p.Metadata)
assert.Empty(t, p.Metadata.Author.Name)
assert.Empty(t, p.RepositoryURLs)
})
t.Run("NameFieldGeneration", func(t *testing.T) {
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
// Test with only individual name components - Name should be auto-generated
p, err := ParsePackage(
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{"author":{"givenName":"John","middleName":"Q","familyName":"Doe"}}`),
)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, "John Q Doe", p.Metadata.Author.Name)
assert.Equal(t, "John", p.Metadata.Author.GivenName)
assert.Equal(t, "Q", p.Metadata.Author.MiddleName)
assert.Equal(t, "Doe", p.Metadata.Author.FamilyName)
})
}
func TestTrimmedVersionString(t *testing.T) {
cases := []struct {
Version *version.Version
Expected string
}{
{
Version: version.Must(version.NewVersion("1")),
Expected: "1.0",
},
{
Version: version.Must(version.NewVersion("1.0")),
Expected: "1.0",
},
{
Version: version.Must(version.NewVersion("1.0.0")),
Expected: "1.0",
},
{
Version: version.Must(version.NewVersion("1.0.1")),
Expected: "1.0.1",
},
{
Version: version.Must(version.NewVersion("1.0+meta")),
Expected: "1.0",
},
{
Version: version.Must(version.NewVersion("1.0.0+meta")),
Expected: "1.0",
},
{
Version: version.Must(version.NewVersion("1.0.1+meta")),
Expected: "1.0.1",
},
}
for _, c := range cases {
assert.Equal(t, c.Expected, TrimmedVersionString(c.Version))
}
}
func TestPersonNameString(t *testing.T) {
cases := []struct {
Name string
Person Person
Expected string
}{
{
Name: "GivenNameOnly",
Person: Person{GivenName: "John"},
Expected: "John",
},
{
Name: "GivenAndFamily",
Person: Person{GivenName: "John", FamilyName: "Doe"},
Expected: "John Doe",
},
{
Name: "FullName",
Person: Person{GivenName: "John", MiddleName: "Q", FamilyName: "Doe"},
Expected: "John Q Doe",
},
{
Name: "MiddleAndFamily",
Person: Person{MiddleName: "Q", FamilyName: "Doe"},
Expected: "Q Doe",
},
{
Name: "Empty",
Person: Person{},
Expected: "",
},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
assert.Equal(t, c.Expected, c.Person.String())
})
}
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"context"
"errors"
"io"
"time"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"xorm.io/builder"
)
const LockFile = "terraform.lock"
// LockInfo is the metadata for a terraform lock.
type LockInfo struct {
ID string `json:"ID"`
Operation string `json:"Operation"`
Info string `json:"Info"`
Who string `json:"Who"`
Version string `json:"Version"`
Created time.Time `json:"Created"`
Path string `json:"Path"`
}
func (l *LockInfo) IsLocked() bool {
return l.ID != ""
}
func ParseLockInfo(r io.Reader) (*LockInfo, error) {
var lock LockInfo
err := json.NewDecoder(r).Decode(&lock)
if err != nil {
return nil, err
}
// ID is required. Rest is less important.
if lock.ID == "" {
return nil, util.NewInvalidArgumentErrorf("terraform lock is missing an ID")
}
return &lock, nil
}
// GetLock returns the terraform lock for the given package.
// Lock is empty if no lock exists.
func GetLock(ctx context.Context, packageID int64) (LockInfo, error) {
var lock LockInfo
locks, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypePackage, packageID, LockFile)
if err != nil {
return lock, err
}
if len(locks) == 0 || locks[0].Value == "" {
return lock, nil
}
err = json.Unmarshal([]byte(locks[0].Value), &lock)
return lock, err
}
// SetLock sets the terraform lock for the given package.
func SetLock(ctx context.Context, packageID int64, lock *LockInfo) error {
jsonBytes, err := json.Marshal(lock)
if err != nil {
return err
}
return updateLock(ctx, packageID, string(jsonBytes), builder.Eq{"value": ""})
}
// RemoveLock removes the terraform lock for the given package.
func RemoveLock(ctx context.Context, packageID int64) error {
return updateLock(ctx, packageID, "", builder.Neq{"value": ""})
}
func updateLock(ctx context.Context, refID int64, value string, cond builder.Cond) error {
pp := packages_model.PackageProperty{RefType: packages_model.PropertyTypePackage, RefID: refID, Name: LockFile}
ok, err := db.GetEngine(ctx).Get(&pp)
if err != nil {
return err
}
if ok {
n, err := db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", packages_model.PropertyTypePackage, refID, LockFile).And(cond).Cols("value").Update(&packages_model.PackageProperty{Value: value})
if err != nil {
return err
}
if n == 0 {
return errors.New("failed to update lock state")
}
return nil
}
_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, refID, LockFile, value)
return err
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"io"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
)
// Note: this is a subset of the Terraform state file format as the full one has two forms.
// If needed, it can be expanded in the future.
type State struct {
Serial uint64 `json:"serial"`
Lineage string `json:"lineage"`
}
// ParseState parses the required parts of Terraform state file
func ParseState(r io.Reader) (*State, error) {
var state State
err := json.NewDecoder(r).Decode(&state)
if err != nil {
return nil, err
}
// Serial starts at 1; 0 means it wasn't set in the state file
if state.Serial == 0 {
return nil, util.NewInvalidArgumentErrorf("state serial is missing")
}
// Lineage should always be set
if state.Lineage == "" {
return nil, util.NewInvalidArgumentErrorf("state lineage is missing")
}
return &state, nil
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package vagrant
import (
"archive/tar"
"compress/gzip"
"io"
"strings"
"gitea.dev/modules/json"
"gitea.dev/modules/validation"
)
const (
PropertyProvider = "vagrant.provider"
)
// Metadata represents the metadata of a Vagrant package
type Metadata struct {
Author string `json:"author,omitempty"`
Description string `json:"description,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
}
// ParseMetadataFromBox parses the metadata of a box file
func ParseMetadataFromBox(r io.Reader) (*Metadata, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.Typeflag != tar.TypeReg {
continue
}
if hd.Name == "info.json" {
return ParseInfoFile(tr)
}
}
return &Metadata{}, nil
}
// ParseInfoFile parses a info.json file to retrieve the metadata of a Vagrant package
func ParseInfoFile(r io.Reader) (*Metadata, error) {
var values map[string]string
if err := json.NewDecoder(r).Decode(&values); err != nil {
return nil, err
}
m := &Metadata{}
// There is no defined format for this file, just try the common keys
for k, v := range values {
switch strings.ToLower(k) {
case "description":
fallthrough
case "short_description":
m.Description = v
case "website":
fallthrough
case "homepage":
fallthrough
case "url":
if validation.IsValidURL(v) {
m.ProjectURL = v
}
case "repository":
fallthrough
case "source":
if validation.IsValidURL(v) {
m.RepositoryURL = v
}
case "author":
fallthrough
case "authors":
m.Author = v
}
}
return m, nil
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package vagrant
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"testing"
"gitea.dev/modules/json"
"github.com/stretchr/testify/assert"
)
const (
author = "gitea"
description = "Package Description"
projectURL = "https://gitea.io"
repositoryURL = "https://gitea.io/gitea/gitea"
)
func TestParseMetadataFromBox(t *testing.T) {
createArchive := func(files map[string][]byte) io.Reader {
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
tw := tar.NewWriter(zw)
for filename, content := range files {
hdr := &tar.Header{
Name: filename,
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write(content)
}
tw.Close()
zw.Close()
return &buf
}
t.Run("MissingInfoFile", func(t *testing.T) {
data := createArchive(map[string][]byte{"dummy.txt": {}})
metadata, err := ParseMetadataFromBox(data)
assert.NotNil(t, metadata)
assert.NoError(t, err)
})
t.Run("Valid", func(t *testing.T) {
content, err := json.Marshal(map[string]string{
"description": description,
"author": author,
"website": projectURL,
"repository": repositoryURL,
})
assert.NoError(t, err)
data := createArchive(map[string][]byte{"info.json": content})
metadata, err := ParseMetadataFromBox(data)
assert.NotNil(t, metadata)
assert.NoError(t, err)
assert.Equal(t, author, metadata.Author)
assert.Equal(t, description, metadata.Description)
assert.Equal(t, projectURL, metadata.ProjectURL)
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
})
}
func TestParseInfoFile(t *testing.T) {
t.Run("UnknownKeys", func(t *testing.T) {
content, err := json.Marshal(map[string]string{
"package": "",
"dummy": "",
})
assert.NoError(t, err)
metadata, err := ParseInfoFile(bytes.NewReader(content))
assert.NotNil(t, metadata)
assert.NoError(t, err)
assert.Empty(t, metadata.Author)
assert.Empty(t, metadata.Description)
assert.Empty(t, metadata.ProjectURL)
assert.Empty(t, metadata.RepositoryURL)
})
t.Run("Valid", func(t *testing.T) {
content, err := json.Marshal(map[string]string{
"description": description,
"author": author,
"website": projectURL,
"repository": repositoryURL,
})
assert.NoError(t, err)
metadata, err := ParseInfoFile(bytes.NewReader(content))
assert.NotNil(t, metadata)
assert.NoError(t, err)
assert.Equal(t, author, metadata.Author)
assert.Equal(t, description, metadata.Description)
assert.Equal(t, projectURL, metadata.ProjectURL)
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
})
}