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 }