feat: vendor gitea 1.16.2
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user