feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -4,8 +4,13 @@
package composer
import (
"archive/tar"
"archive/zip"
"compress/bzip2"
"compress/gzip"
"errors"
"io"
"io/fs"
"path"
"regexp"
"strings"
@@ -29,8 +34,10 @@ var (
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
// Package represents a Composer package
type Package struct {
// PackageInfo represents Composer package info
type PackageInfo struct {
Filename string
Name string
Version string
Type string
@@ -44,7 +51,7 @@ type Metadata struct {
Description string `json:"description,omitempty"`
Readme string `json:"readme,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Comments Comments `json:"_comments,omitempty"`
Comments Comments `json:"_comment,omitempty"`
Homepage string `json:"homepage,omitempty"`
License Licenses `json:"license,omitempty"`
Authors []Author `json:"authors,omitempty"`
@@ -75,7 +82,7 @@ func (l *Licenses) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &values); err != nil {
return err
}
*l = Licenses(values)
*l = values
}
return nil
}
@@ -97,7 +104,7 @@ func (c *Comments) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &values); err != nil {
return err
}
*c = Comments(values)
*c = values
}
return nil
}
@@ -111,39 +118,121 @@ type Author struct {
var nameMatch = regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`)
// ParsePackage parses the metadata of a Composer package file
func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
archive, err := zip.NewReader(r, size)
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 {
if strings.Count(file.Name, "/") > 1 {
continue
}
if strings.HasSuffix(strings.ToLower(file.Name), "composer.json") {
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 ParseComposerFile(archive, path.Dir(file.Name), f)
return util.ReadWithLimit(f, limit)
}
}
return nil, ErrMissingComposerFile
return nil, fs.ErrNotExist
}
// ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package
func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Package, error) {
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.NewDecoder(r).Decode(&cj); err != nil {
if err := json.Unmarshal(dataComposerJSON, &cj); err != nil {
return nil, err
}
@@ -151,6 +240,9 @@ func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Pa
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
@@ -168,17 +260,23 @@ func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Pa
if cj.Readme == "" {
cj.Readme = "README.md"
}
f, err := archive.Open(path.Join(pathPrefix, cj.Readme))
if err == nil {
// 10kb limit for readme content
buf, _ := io.ReadAll(io.LimitReader(f, 10*1024))
cj.Readme = string(buf)
_ = f.Close()
} else {
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)
}
return &Package{
// 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,
@@ -4,14 +4,19 @@
package composer
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"io"
"strings"
"testing"
"code.gitea.io/gitea/modules/json"
"github.com/dsnet/compress/bzip2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
@@ -26,8 +31,10 @@ const (
license = "MIT"
)
const composerContent = `{
func buildComposerContent(version string) string {
return `{
"name": "` + name + `",
"version": "` + version + `",
"description": "` + description + `",
"type": "` + packageType + `",
"license": "` + license + `",
@@ -44,8 +51,9 @@ const composerContent = `{
"require": {
"php": ">=7.2 || ^8.0"
},
"_comments": "` + comments + `"
"_comment": "` + comments + `"
}`
}
func TestLicenseUnmarshal(t *testing.T) {
var l Licenses
@@ -73,16 +81,34 @@ func TestParsePackage(t *testing.T) {
archive := zip.NewWriter(&buf)
for name, content := range files {
w, _ := archive.Create(name)
w.Write([]byte(content))
_, _ = w.Write([]byte(content))
}
archive.Close()
_ = 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), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrMissingComposerFile)
})
@@ -90,7 +116,7 @@ func TestParsePackage(t *testing.T) {
t.Run("MissingComposerFileInRoot", func(t *testing.T) {
data := createArchive(map[string]string{"sub/sub/composer.json": ""})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrMissingComposerFile)
})
@@ -98,7 +124,7 @@ func TestParsePackage(t *testing.T) {
t.Run("InvalidComposerFile", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": ""})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.Error(t, err)
})
@@ -106,7 +132,7 @@ func TestParsePackage(t *testing.T) {
t.Run("InvalidPackageName", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": "{}"})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidName)
})
@@ -114,7 +140,7 @@ func TestParsePackage(t *testing.T) {
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), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidVersion)
})
@@ -122,22 +148,21 @@ func TestParsePackage(t *testing.T) {
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), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.NoError(t, err)
assert.NotNil(t, cp)
assert.Empty(t, cp.Metadata.Readme)
})
t.Run("Valid", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": composerContent, "README.md": readme})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
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.Empty(t, cp.Version)
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)
@@ -149,5 +174,25 @@ func TestParsePackage(t *testing.T) {
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")
})
}
@@ -103,7 +103,9 @@ func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) {
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
var image oci.Image
if err := json.NewDecoder(r).Decode(&image); err != nil {
// 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
}
@@ -22,6 +22,8 @@ func TestParseImageConfig(t *testing.T) {
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))
@@ -36,8 +36,8 @@ func (s *ContentStore) ShouldServeDirect() bool {
return setting.Packages.Storage.ServeDirect()
}
func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams url.Values) (*url.URL, error) {
return s.store.URL(KeyToRelativePath(key), filename, method, reqParams)
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
@@ -34,7 +34,7 @@ var (
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]+){1,3}\z`)
versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+)+\z`)
authorReplacePattern = regexp.MustCompile(`[\[\(].+?[\]\)]`)
)
@@ -128,13 +128,22 @@ func TestParseDescription(t *testing.T) {
})
t.Run("InvalidVersion", func(t *testing.T) {
for _, version := range []string{"1", "1 0", "1.2.3.4.5", "1-2-3-4-5", "1.", "1.0.", "1-", "1-0-"} {
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)
@@ -140,7 +140,7 @@ type nuspecPackage struct {
func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
archive, err := zip.NewReader(r, size)
if err != nil {
return nil, err
return nil, util.NewInvalidArgumentErrorf("unable to parse package meta: %v", err)
}
for _, file := range archive.File {
@@ -42,7 +42,7 @@ func (l PortablePdbList) Close() {
func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) {
archive, err := zip.NewReader(r, size)
if err != nil {
return nil, err
return nil, util.NewInvalidArgumentErrorf("unable to extract portable pdb: %v", err)
}
var pdbs PortablePdbList
@@ -142,8 +142,8 @@ func ParseDebugHeaderID(r io.ReadSeeker) (string, error) {
if _, err := r.Read(b); err != nil {
return "", err
}
if i := bytes.IndexByte(b, 0); i != -1 {
buf.Write(b[:i])
if before, _, ok := bytes.Cut(b, []byte{0}); ok {
buf.Write(before)
return buf.String(), nil
}
buf.Write(b)
@@ -46,10 +46,11 @@ type Package struct {
}
type VersionMetadata struct {
License string `json:"license,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
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 {
@@ -296,3 +297,43 @@ func getChangelogs(h *rpmutils.RpmHeader) []*Changelog {
}
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"`
}
@@ -9,6 +9,7 @@ import (
"io"
"regexp"
"strings"
"sync"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
@@ -25,7 +26,9 @@ var (
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
var versionMatcher = regexp.MustCompile(`\A[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z`)
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 {
@@ -128,7 +131,7 @@ func (r requirement) AsVersionRequirement() []VersionRequirement {
continue
}
version, ok := versionInt.(string)
if !ok || version == "0" {
if !ok || (version == "0" && restriction == ">=") {
continue
}
@@ -176,7 +179,7 @@ func parseMetadataFile(r io.Reader) (*Package, error) {
return nil, ErrInvalidName
}
if !versionMatcher.MatchString(spec.Version.Version) {
if !versionMatcher().MatchString(spec.Version.Version) {
return nil, ErrInvalidVersion
}
@@ -4,42 +4,30 @@
package rubygems
import (
"archive/tar"
"bytes"
"encoding/base64"
"io"
"testing"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestParsePackageMetaData(t *testing.T) {
createArchive := func(filename string, content []byte) io.Reader {
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
hdr := &tar.Header{
Name: filename,
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write(content)
tw.Close()
return &buf
}
t.Run("MissingMetadataFile", func(t *testing.T) {
data := createArchive("dummy.txt", []byte{0})
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) {
content, _ := base64.StdEncoding.DecodeString("H4sICHC/I2EEAG1ldGFkYXRhAAEeAOH/bmFtZTogZwp2ZXJzaW9uOgogIHZlcnNpb246IDEKWw35Tx4AAAA=")
data := createArchive("metadata.gz", content)
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)
@@ -47,17 +35,86 @@ func TestParsePackageMetaData(t *testing.T) {
}
func TestParseMetadataFile(t *testing.T) {
content, _ := base64.StdEncoding.DecodeString(`H4sIAMe7I2ECA9VVTW/UMBC9+1eYXvaUbJpSQBZUHJAqDlwK4kCFIseZzZrGH9iTqisEv52Js9nd
0KqggiqRXWnX45n3ZuZ5nCzL+JPQ15ulq7+AQnEORoj3HpReaSVRO8usNCB4qxEku4YQySbuCPo4
bjHOd07HeZGfMt9JXLlgBB9imOxx7UIULOPnCZMMLsDXXgeiYbW2jQ6C0y9TELBSa6kJ6/IzaySS
R1mUx1nxIitPeFGI9M2L6eGfWAMebANWaUgktzN9M3lsKNmxutBb1AYyCibbNhsDFu+q9GK/Tc4z
d2IcLBl9js5eHaXFsLyvXeNz0LQyL/YoLx8EsiCMBZlx46k6sS2PDD5AgA5kJPNKdhH2elWzOv7n
uv9Q9Aau/6ngP84elvNpXh5oRVlB5/yW7BH0+qu0G4gqaI/JdEHBFBS5l+pKtsARIjIwUnfj8Le0
+TrdJLl2DG5A9SjrjgZ1mG+4QbAD+G4ZZBUap6qVnnzGf6Rwp+vliBRqtnYGPBEKvkb0USyXE8mS
dVoR6hj07u0HZgAl3SRS8G/fmXcRK20jyq6rDMSYQFgidamqkXbbuspLXE/0k7GphtKqe67GuRC/
yjAbmt9LsOMp8xMamFkSQ38fP5EFjdz8LA4do2C69VvqWXAJgrPbKZb58/xZXrKoW6ttW13Bhvzi
4ftn7/yUxd4YGcglvTmmY8aGY3ZwRn4CqcWcidUGAAA=`)
rp, err := parseMetadataFile(bytes.NewReader(content))
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)
@@ -84,5 +141,5 @@ yjAbmt9LsOMp8xMamFkSQ38fP5EFjdz8LA4do2C69VvqWXAJgrPbKZb58/xZXrKoW6ttW13Bhvzi
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, "5.2", rp.Metadata.DevelopmentDependencies[0].Version[0].Version)
assert.Equal(t, "0", rp.Metadata.DevelopmentDependencies[0].Version[0].Version)
}
@@ -47,6 +47,7 @@ type Metadata struct {
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"`
}
@@ -67,7 +68,8 @@ type SoftwareSourceCode struct {
Keywords []string `json:"keywords,omitempty"`
CodeRepository string `json:"codeRepository,omitempty"`
License string `json:"license,omitempty"`
Author Person `json:"author"`
LicenseURL string `json:"licenseURL,omitempty"`
Author *Person `json:"author,omitempty"`
ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
RepositoryURLs []string `json:"repositoryURLs,omitempty"`
}
@@ -181,26 +183,31 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
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
author := Person{
Name: ssc.Author.Name,
GivenName: ssc.Author.GivenName,
MiddleName: ssc.Author.MiddleName,
FamilyName: ssc.Author.FamilyName,
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
}
// 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
}
@@ -4,11 +4,12 @@
package swift
import (
"archive/zip"
"bytes"
"strings"
"testing"
"code.gitea.io/gitea/modules/test"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
@@ -18,36 +19,24 @@ const (
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) {
createArchive := func(files map[string][]byte) *bytes.Reader {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for filename, content := range files {
w, _ := zw.Create(filename)
w.Write(content)
}
zw.Close()
return bytes.NewReader(buf.Bytes())
}
t.Run("MissingManifestFile", func(t *testing.T) {
data := createArchive(map[string][]byte{"dummy.txt": {}})
p, err := ParsePackage(data, data.Size(), nil)
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 := createArchive(map[string][]byte{
"Package.swift": make([]byte, maxManifestFileSize+1),
data := test.WriteZipArchive(map[string]string{
"Package.swift": strings.Repeat("a", maxManifestFileSize+1),
})
p, err := ParsePackage(data, data.Size(), nil)
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrManifestFileTooLarge)
})
@@ -56,12 +45,12 @@ func TestParsePackage(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 := createArchive(map[string][]byte{
"Package.swift": []byte(content1),
"Package@swift-5.5.swift": []byte(content2),
data := test.WriteZipArchive(map[string]string{
"Package.swift": content1,
"Package@swift-5.5.swift": content2,
})
p, err := ParsePackage(data, data.Size(), nil)
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
assert.NotNil(t, p)
assert.NoError(t, err)
@@ -77,14 +66,13 @@ func TestParsePackage(t *testing.T) {
})
t.Run("WithMetadata", func(t *testing.T) {
data := createArchive(map[string][]byte{
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
p, err := ParsePackage(
data,
data.Size(),
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
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)
@@ -97,6 +85,7 @@ func TestParsePackage(t *testing.T) {
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)
@@ -104,14 +93,13 @@ func TestParsePackage(t *testing.T) {
})
t.Run("WithExplicitNameField", func(t *testing.T) {
data := createArchive(map[string][]byte{
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
authorName := "John Doe"
p, err := ParsePackage(
data,
data.Size(),
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)
@@ -122,15 +110,30 @@ func TestParsePackage(t *testing.T) {
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 := createArchive(map[string][]byte{
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
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(
data,
data.Size(),
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{"author":{"givenName":"John","middleName":"Q","familyName":"Doe"}}`),
)
assert.NotNil(t, p)
@@ -0,0 +1,100 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"context"
"errors"
"io"
"time"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/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
}
@@ -0,0 +1,38 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"io"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/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
}