github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/packages/rubygems/metadata.go (about)

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