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 }