aboutsummaryrefslogtreecommitdiffstats
path: root/modules/packages/rubygems
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2022-03-30 10:42:47 +0200
committerGitHub <noreply@github.com>2022-03-30 16:42:47 +0800
commit1d332342db6d5bd4e1552d8d46720bf1b948c26b (patch)
treeca0c8931e5da85e71037ed43d7a90826ba708d9d /modules/packages/rubygems
parent2bce1ea9862c70ebb69963e65bb84dcad6ebb31c (diff)
downloadgitea-1d332342db6d5bd4e1552d8d46720bf1b948c26b.tar.gz
gitea-1d332342db6d5bd4e1552d8d46720bf1b948c26b.zip
Add Package Registry (#16510)
* Added package store settings. * Added models. * Added generic package registry. * Added tests. * Added NuGet package registry. * Moved service index to api file. * Added NPM package registry. * Added Maven package registry. * Added PyPI package registry. * Summary is deprecated. * Changed npm name. * Sanitize project url. * Allow only scoped packages. * Added user interface. * Changed method name. * Added missing migration file. * Set page info. * Added documentation. * Added documentation links. * Fixed wrong error message. * Lint template files. * Fixed merge errors. * Fixed unit test storage path. * Switch to json module. * Added suggestions. * Added package webhook. * Add package api. * Fixed swagger file. * Fixed enum and comments. * Fixed NuGet pagination. * Print test names. * Added api tests. * Fixed access level. * Fix User unmarshal. * Added RubyGems package registry. * Fix lint. * Implemented io.Writer. * Added support for sha256/sha512 checksum files. * Improved maven-metadata.xml support. * Added support for symbol package uploads. * Added tests. * Added overview docs. * Added npm dependencies and keywords. * Added no-packages information. * Display file size. * Display asset count. * Fixed filter alignment. * Added package icons. * Formatted instructions. * Allow anonymous package downloads. * Fixed comments. * Fixed postgres test. * Moved file. * Moved models to models/packages. * Use correct error response format per client. * Use simpler search form. * Fixed IsProd. * Restructured data model. * Prevent empty filename. * Fix swagger. * Implemented user/org registry. * Implemented UI. * Use GetUserByIDCtx. * Use table for dependencies. * make svg * Added support for unscoped npm packages. * Add support for npm dist tags. * Added tests for npm tags. * Unlink packages if repository gets deleted. * Prevent user/org delete if a packages exist. * Use package unlink in repository service. * Added support for composer packages. * Restructured package docs. * Added missing tests. * Fixed generic content page. * Fixed docs. * Fixed swagger. * Added missing type. * Fixed ambiguous column. * Organize content store by sha256 hash. * Added admin package management. * Added support for sorting. * Add support for multiple identical versions/files. * Added missing repository unlink. * Added file properties. * make fmt * lint * Added Conan package registry. * Updated docs. * Unify package names. * Added swagger enum. * Use longer TEXT column type. * Removed version composite key. * Merged package and container registry. * Removed index. * Use dedicated package router. * Moved files to new location. * Updated docs. * Fixed JOIN order. * Fixed GROUP BY statement. * Fixed GROUP BY #2. * Added symbol server support. * Added more tests. * Set NOT NULL. * Added setting to disable package registries. * Moved auth into service. * refactor * Use ctx everywhere. * Added package cleanup task. * Changed packages path. * Added container registry. * Refactoring * Updated comparison. * Fix swagger. * Fixed table order. * Use token auth for npm routes. * Enabled ReverseProxy auth. * Added packages link for orgs. * Fixed anonymous org access. * Enable copy button for setup instructions. * Merge error * Added suggestions. * Fixed merge. * Handle "generic". * Added link for TODO. * Added suggestions. * Changed temporary buffer filename. * Added suggestions. * Apply suggestions from code review Co-authored-by: Thomas Boerger <thomas@webhippie.de> * Update docs/content/doc/packages/nuget.en-us.md Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Thomas Boerger <thomas@webhippie.de>
Diffstat (limited to 'modules/packages/rubygems')
-rw-r--r--modules/packages/rubygems/marshal.go311
-rw-r--r--modules/packages/rubygems/marshal_test.go99
-rw-r--r--modules/packages/rubygems/metadata.go222
-rw-r--r--modules/packages/rubygems/metadata_test.go89
4 files changed, 721 insertions, 0 deletions
diff --git a/modules/packages/rubygems/marshal.go b/modules/packages/rubygems/marshal.go
new file mode 100644
index 0000000000..2c45042fa8
--- /dev/null
+++ b/modules/packages/rubygems/marshal.go
@@ -0,0 +1,311 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package rubygems
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "io"
+ "reflect"
+)
+
+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 = errors.New("Type is unsupported")
+ // ErrInvalidIntRange indicates an invalid number range
+ ErrInvalidIntRange = errors.New("Number is not in valid range")
+)
+
+// RubyUserMarshal is a Ruby object that has a marshal_load function.
+type RubyUserMarshal struct {
+ Name string
+ Value interface{}
+}
+
+// RubyUserDef is a Ruby object that has a _load function.
+type RubyUserDef struct {
+ Name string
+ Value interface{}
+}
+
+// RubyObject is a default Ruby object.
+type RubyObject struct {
+ Name string
+ Member map[string]interface{}
+}
+
+// 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 interface{}) 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 interface{}) error {
+ if v == nil {
+ return e.marshalNil()
+ }
+
+ val := reflect.ValueOf(v)
+ typ := reflect.TypeOf(v)
+
+ if typ.Kind() == reflect.Ptr {
+ 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 len int
+ if 122 < i && i <= 0xff {
+ len = 1
+ } else if 0xff < i && i <= 0xffff {
+ len = 2
+ } else if 0xffff < i && i <= 0xffffff {
+ len = 3
+ } else if 0xffffff < i && i <= 0x3fffffff {
+ len = 4
+ } else if -0x100 <= i && i < -123 {
+ len = -1
+ } else if -0x10000 <= i && i < -0x100 {
+ len = -2
+ } else if -0x1000000 <= i && i < -0x100000 {
+ len = -3
+ } else if -0x40000000 <= i && i < -0x1000000 {
+ len = -4
+ } else {
+ return ErrInvalidIntRange
+ }
+
+ if err := e.w.WriteByte(byte(len)); err != nil {
+ return err
+ }
+ if len < 0 {
+ len = -len
+ }
+
+ for c := 0; c < len; 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
+ }
+
+ len := arr.Len()
+
+ if err := e.marshalIntInternal(int64(len)); err != nil {
+ return err
+ }
+
+ for i := 0; i < len; i++ {
+ 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
+}
diff --git a/modules/packages/rubygems/marshal_test.go b/modules/packages/rubygems/marshal_test.go
new file mode 100644
index 0000000000..e5963ebcd6
--- /dev/null
+++ b/modules/packages/rubygems/marshal_test.go
@@ -0,0 +1,99 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package rubygems
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMinimalEncoder(t *testing.T) {
+ cases := []struct {
+ Value interface{}
+ 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]interface{}{
+ "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)
+ }
+}
diff --git a/modules/packages/rubygems/metadata.go b/modules/packages/rubygems/metadata.go
new file mode 100644
index 0000000000..942f205fc3
--- /dev/null
+++ b/modules/packages/rubygems/metadata.go
@@ -0,0 +1,222 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package rubygems
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "errors"
+ "io"
+ "regexp"
+ "strings"
+
+ "code.gitea.io/gitea/modules/validation"
+
+ "gopkg.in/yaml.v2"
+)
+
+var (
+ // ErrMissingMetadataFile indicates a missing metadata.gz file
+ ErrMissingMetadataFile = errors.New("Metadata file is missing")
+ // ErrInvalidName indicates an invalid id in the metadata.gz file
+ ErrInvalidName = errors.New("Metadata file contains an invalid name")
+ // ErrInvalidVersion indicates an invalid version in the metadata.gz file
+ ErrInvalidVersion = errors.New("Metadata file contains an invalid version")
+)
+
+var versionMatcher = 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 interface{} `yaml:"autorequire"`
+ Bindir string `yaml:"bindir"`
+ CertChain []interface{} `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"`
+ Email string `yaml:"email"`
+ Executables []string `yaml:"executables"`
+ Extensions []interface{} `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 interface{} `yaml:"post_install_message"`
+ RdocOptions []interface{} `yaml:"rdoc_options"`
+ RequirePaths []string `yaml:"require_paths"`
+ RequiredRubyVersion requirement `yaml:"required_ruby_version"`
+ RequiredRubygemsVersion requirement `yaml:"required_rubygems_version"`
+ Requirements []interface{} `yaml:"requirements"`
+ RubygemsVersion string `yaml:"rubygems_version"`
+ SigningKey interface{} `yaml:"signing_key"`
+ SpecificationVersion int `yaml:"specification_version"`
+ Summary string `yaml:"summary"`
+ TestFiles []interface{} `yaml:"test_files"`
+}
+
+type requirement struct {
+ Requirements [][]interface{} `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[interface{}]interface{})
+ if !ok {
+ continue
+ }
+ versionInt, ok := vm["version"]
+ if !ok {
+ continue
+ }
+ version, ok := versionInt.(string)
+ if !ok || version == "0" {
+ 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
+}
diff --git a/modules/packages/rubygems/metadata_test.go b/modules/packages/rubygems/metadata_test.go
new file mode 100644
index 0000000000..dbefa9c236
--- /dev/null
+++ b/modules/packages/rubygems/metadata_test.go
@@ -0,0 +1,89 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package rubygems
+
+import (
+ "archive/tar"
+ "bytes"
+ "encoding/base64"
+ "io"
+ "testing"
+
+ "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})
+
+ 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)
+
+ rp, err := ParsePackageMetaData(data)
+ assert.NoError(t, err)
+ assert.NotNil(t, rp)
+ })
+}
+
+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))
+ 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, "5.2", rp.Metadata.DevelopmentDependencies[0].Version[0].Version)
+}