code.gitea.io/gitea@v1.19.3/modules/packages/rubygems/metadata.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package rubygems
     5  
     6  import (
     7  	"archive/tar"
     8  	"compress/gzip"
     9  	"io"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"code.gitea.io/gitea/modules/util"
    14  	"code.gitea.io/gitea/modules/validation"
    15  
    16  	"gopkg.in/yaml.v3"
    17  )
    18  
    19  var (
    20  	// ErrMissingMetadataFile indicates a missing metadata.gz file
    21  	ErrMissingMetadataFile = util.NewInvalidArgumentErrorf("metadata.gz file is missing")
    22  	// ErrInvalidName indicates an invalid id in the metadata.gz file
    23  	ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
    24  	// ErrInvalidVersion indicates an invalid version in the metadata.gz file
    25  	ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
    26  )
    27  
    28  var versionMatcher = regexp.MustCompile(`\A[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z`)
    29  
    30  // Package represents a RubyGems package
    31  type Package struct {
    32  	Name     string
    33  	Version  string
    34  	Metadata *Metadata
    35  }
    36  
    37  // Metadata represents the metadata of a RubyGems package
    38  type Metadata struct {
    39  	Platform                string               `json:"platform,omitempty"`
    40  	Description             string               `json:"description,omitempty"`
    41  	Summary                 string               `json:"summary,omitempty"`
    42  	Authors                 []string             `json:"authors,omitempty"`
    43  	Licenses                []string             `json:"licenses,omitempty"`
    44  	RequiredRubyVersion     []VersionRequirement `json:"required_ruby_version,omitempty"`
    45  	RequiredRubygemsVersion []VersionRequirement `json:"required_rubygems_version,omitempty"`
    46  	ProjectURL              string               `json:"project_url,omitempty"`
    47  	RuntimeDependencies     []Dependency         `json:"runtime_dependencies,omitempty"`
    48  	DevelopmentDependencies []Dependency         `json:"development_dependencies,omitempty"`
    49  }
    50  
    51  // VersionRequirement represents a version restriction
    52  type VersionRequirement struct {
    53  	Restriction string `json:"restriction"`
    54  	Version     string `json:"version"`
    55  }
    56  
    57  // Dependency represents a dependency of a RubyGems package
    58  type Dependency struct {
    59  	Name    string               `json:"name"`
    60  	Version []VersionRequirement `json:"version"`
    61  }
    62  
    63  type gemspec struct {
    64  	Name    string `yaml:"name"`
    65  	Version struct {
    66  		Version string `yaml:"version"`
    67  	} `yaml:"version"`
    68  	Platform     string        `yaml:"platform"`
    69  	Authors      []string      `yaml:"authors"`
    70  	Autorequire  interface{}   `yaml:"autorequire"`
    71  	Bindir       string        `yaml:"bindir"`
    72  	CertChain    []interface{} `yaml:"cert_chain"`
    73  	Date         string        `yaml:"date"`
    74  	Dependencies []struct {
    75  		Name                string      `yaml:"name"`
    76  		Requirement         requirement `yaml:"requirement"`
    77  		Type                string      `yaml:"type"`
    78  		Prerelease          bool        `yaml:"prerelease"`
    79  		VersionRequirements requirement `yaml:"version_requirements"`
    80  	} `yaml:"dependencies"`
    81  	Description    string        `yaml:"description"`
    82  	Executables    []string      `yaml:"executables"`
    83  	Extensions     []interface{} `yaml:"extensions"`
    84  	ExtraRdocFiles []string      `yaml:"extra_rdoc_files"`
    85  	Files          []string      `yaml:"files"`
    86  	Homepage       string        `yaml:"homepage"`
    87  	Licenses       []string      `yaml:"licenses"`
    88  	Metadata       struct {
    89  		BugTrackerURI    string `yaml:"bug_tracker_uri"`
    90  		ChangelogURI     string `yaml:"changelog_uri"`
    91  		DocumentationURI string `yaml:"documentation_uri"`
    92  		SourceCodeURI    string `yaml:"source_code_uri"`
    93  	} `yaml:"metadata"`
    94  	PostInstallMessage      interface{}   `yaml:"post_install_message"`
    95  	RdocOptions             []interface{} `yaml:"rdoc_options"`
    96  	RequirePaths            []string      `yaml:"require_paths"`
    97  	RequiredRubyVersion     requirement   `yaml:"required_ruby_version"`
    98  	RequiredRubygemsVersion requirement   `yaml:"required_rubygems_version"`
    99  	Requirements            []interface{} `yaml:"requirements"`
   100  	RubygemsVersion         string        `yaml:"rubygems_version"`
   101  	SigningKey              interface{}   `yaml:"signing_key"`
   102  	SpecificationVersion    int           `yaml:"specification_version"`
   103  	Summary                 string        `yaml:"summary"`
   104  	TestFiles               []interface{} `yaml:"test_files"`
   105  }
   106  
   107  type requirement struct {
   108  	Requirements [][]interface{} `yaml:"requirements"`
   109  }
   110  
   111  // AsVersionRequirement converts into []VersionRequirement
   112  func (r requirement) AsVersionRequirement() []VersionRequirement {
   113  	requirements := make([]VersionRequirement, 0, len(r.Requirements))
   114  	for _, req := range r.Requirements {
   115  		if len(req) != 2 {
   116  			continue
   117  		}
   118  		restriction, ok := req[0].(string)
   119  		if !ok {
   120  			continue
   121  		}
   122  		vm, ok := req[1].(map[string]interface{})
   123  		if !ok {
   124  			continue
   125  		}
   126  		versionInt, ok := vm["version"]
   127  		if !ok {
   128  			continue
   129  		}
   130  		version, ok := versionInt.(string)
   131  		if !ok || version == "0" {
   132  			continue
   133  		}
   134  
   135  		requirements = append(requirements, VersionRequirement{
   136  			Restriction: restriction,
   137  			Version:     version,
   138  		})
   139  	}
   140  	return requirements
   141  }
   142  
   143  // ParsePackageMetaData parses the metadata of a Gem package file
   144  func ParsePackageMetaData(r io.Reader) (*Package, error) {
   145  	archive := tar.NewReader(r)
   146  	for {
   147  		hdr, err := archive.Next()
   148  		if err == io.EOF {
   149  			break
   150  		}
   151  		if err != nil {
   152  			return nil, err
   153  		}
   154  
   155  		if hdr.Name == "metadata.gz" {
   156  			return parseMetadataFile(archive)
   157  		}
   158  	}
   159  
   160  	return nil, ErrMissingMetadataFile
   161  }
   162  
   163  func parseMetadataFile(r io.Reader) (*Package, error) {
   164  	zr, err := gzip.NewReader(r)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	defer zr.Close()
   169  
   170  	var spec gemspec
   171  	if err := yaml.NewDecoder(zr).Decode(&spec); err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	if len(spec.Name) == 0 || strings.Contains(spec.Name, "/") {
   176  		return nil, ErrInvalidName
   177  	}
   178  
   179  	if !versionMatcher.MatchString(spec.Version.Version) {
   180  		return nil, ErrInvalidVersion
   181  	}
   182  
   183  	if !validation.IsValidURL(spec.Homepage) {
   184  		spec.Homepage = ""
   185  	}
   186  	if !validation.IsValidURL(spec.Metadata.SourceCodeURI) {
   187  		spec.Metadata.SourceCodeURI = ""
   188  	}
   189  
   190  	m := &Metadata{
   191  		Platform:                spec.Platform,
   192  		Description:             spec.Description,
   193  		Summary:                 spec.Summary,
   194  		Authors:                 spec.Authors,
   195  		Licenses:                spec.Licenses,
   196  		ProjectURL:              spec.Homepage,
   197  		RequiredRubyVersion:     spec.RequiredRubyVersion.AsVersionRequirement(),
   198  		RequiredRubygemsVersion: spec.RequiredRubygemsVersion.AsVersionRequirement(),
   199  		DevelopmentDependencies: make([]Dependency, 0, 5),
   200  		RuntimeDependencies:     make([]Dependency, 0, 5),
   201  	}
   202  
   203  	for _, gemdep := range spec.Dependencies {
   204  		dep := Dependency{
   205  			Name:    gemdep.Name,
   206  			Version: gemdep.Requirement.AsVersionRequirement(),
   207  		}
   208  		if gemdep.Type == ":runtime" {
   209  			m.RuntimeDependencies = append(m.RuntimeDependencies, dep)
   210  		} else {
   211  			m.DevelopmentDependencies = append(m.DevelopmentDependencies, dep)
   212  		}
   213  	}
   214  
   215  	return &Package{
   216  		Name:     spec.Name,
   217  		Version:  spec.Version.Version,
   218  		Metadata: m,
   219  	}, nil
   220  }