code.gitea.io/gitea@v1.19.3/modules/packages/conda/metadata.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package conda 5 6 import ( 7 "archive/tar" 8 "archive/zip" 9 "compress/bzip2" 10 "io" 11 "strings" 12 13 "code.gitea.io/gitea/modules/json" 14 "code.gitea.io/gitea/modules/util" 15 "code.gitea.io/gitea/modules/validation" 16 17 "github.com/klauspost/compress/zstd" 18 ) 19 20 var ( 21 ErrInvalidStructure = util.SilentWrap{Message: "package structure is invalid", Err: util.ErrInvalidArgument} 22 ErrInvalidName = util.SilentWrap{Message: "package name is invalid", Err: util.ErrInvalidArgument} 23 ErrInvalidVersion = util.SilentWrap{Message: "package version is invalid", Err: util.ErrInvalidArgument} 24 ) 25 26 const ( 27 PropertyName = "conda.name" 28 PropertyChannel = "conda.channel" 29 PropertySubdir = "conda.subdir" 30 PropertyMetadata = "conda.metdata" 31 ) 32 33 // Package represents a Conda package 34 type Package struct { 35 Name string 36 Version string 37 Subdir string 38 VersionMetadata *VersionMetadata 39 FileMetadata *FileMetadata 40 } 41 42 // VersionMetadata represents the metadata of a Conda package 43 type VersionMetadata struct { 44 Description string `json:"description,omitempty"` 45 Summary string `json:"summary,omitempty"` 46 ProjectURL string `json:"project_url,omitempty"` 47 RepositoryURL string `json:"repository_url,omitempty"` 48 DocumentationURL string `json:"documentation_url,omitempty"` 49 License string `json:"license,omitempty"` 50 LicenseFamily string `json:"license_family,omitempty"` 51 } 52 53 // FileMetadata represents the metadata of a Conda package file 54 type FileMetadata struct { 55 IsCondaPackage bool `json:"is_conda"` 56 Architecture string `json:"architecture,omitempty"` 57 NoArch string `json:"noarch,omitempty"` 58 Build string `json:"build,omitempty"` 59 BuildNumber int64 `json:"build_number,omitempty"` 60 Dependencies []string `json:"dependencies,omitempty"` 61 Platform string `json:"platform,omitempty"` 62 Timestamp int64 `json:"timestamp,omitempty"` 63 } 64 65 type index struct { 66 Name string `json:"name"` 67 Version string `json:"version"` 68 Architecture string `json:"arch"` 69 NoArch string `json:"noarch"` 70 Build string `json:"build"` 71 BuildNumber int64 `json:"build_number"` 72 Dependencies []string `json:"depends"` 73 License string `json:"license"` 74 LicenseFamily string `json:"license_family"` 75 Platform string `json:"platform"` 76 Subdir string `json:"subdir"` 77 Timestamp int64 `json:"timestamp"` 78 } 79 80 type about struct { 81 Description string `json:"description"` 82 Summary string `json:"summary"` 83 ProjectURL string `json:"home"` 84 RepositoryURL string `json:"dev_url"` 85 DocumentationURL string `json:"doc_url"` 86 } 87 88 type ReaderAndReaderAt interface { 89 io.Reader 90 io.ReaderAt 91 } 92 93 // ParsePackageBZ2 parses the Conda package file compressed with bzip2 94 func ParsePackageBZ2(r io.Reader) (*Package, error) { 95 gzr := bzip2.NewReader(r) 96 97 return parsePackageTar(gzr) 98 } 99 100 // ParsePackageConda parses the Conda package file compressed with zip and zstd 101 func ParsePackageConda(r io.ReaderAt, size int64) (*Package, error) { 102 zr, err := zip.NewReader(r, size) 103 if err != nil { 104 return nil, err 105 } 106 107 for _, file := range zr.File { 108 if strings.HasPrefix(file.Name, "info-") && strings.HasSuffix(file.Name, ".tar.zst") { 109 f, err := zr.Open(file.Name) 110 if err != nil { 111 return nil, err 112 } 113 defer f.Close() 114 115 dec, err := zstd.NewReader(f) 116 if err != nil { 117 return nil, err 118 } 119 defer dec.Close() 120 121 p, err := parsePackageTar(dec) 122 if p != nil { 123 p.FileMetadata.IsCondaPackage = true 124 } 125 return p, err 126 } 127 } 128 129 return nil, ErrInvalidStructure 130 } 131 132 func parsePackageTar(r io.Reader) (*Package, error) { 133 var i *index 134 var a *about 135 136 tr := tar.NewReader(r) 137 for { 138 hdr, err := tr.Next() 139 if err == io.EOF { 140 break 141 } 142 if err != nil { 143 return nil, err 144 } 145 146 if hdr.Typeflag != tar.TypeReg { 147 continue 148 } 149 150 if hdr.Name == "info/index.json" { 151 if err := json.NewDecoder(tr).Decode(&i); err != nil { 152 return nil, err 153 } 154 155 if !checkName(i.Name) { 156 return nil, ErrInvalidName 157 } 158 159 if !checkVersion(i.Version) { 160 return nil, ErrInvalidVersion 161 } 162 163 if a != nil { 164 break // stop loop if both files were found 165 } 166 } else if hdr.Name == "info/about.json" { 167 if err := json.NewDecoder(tr).Decode(&a); err != nil { 168 return nil, err 169 } 170 171 if !validation.IsValidURL(a.ProjectURL) { 172 a.ProjectURL = "" 173 } 174 if !validation.IsValidURL(a.RepositoryURL) { 175 a.RepositoryURL = "" 176 } 177 if !validation.IsValidURL(a.DocumentationURL) { 178 a.DocumentationURL = "" 179 } 180 181 if i != nil { 182 break // stop loop if both files were found 183 } 184 } 185 } 186 187 if i == nil { 188 return nil, ErrInvalidStructure 189 } 190 if a == nil { 191 a = &about{} 192 } 193 194 return &Package{ 195 Name: i.Name, 196 Version: i.Version, 197 Subdir: i.Subdir, 198 VersionMetadata: &VersionMetadata{ 199 License: i.License, 200 LicenseFamily: i.LicenseFamily, 201 Description: a.Description, 202 Summary: a.Summary, 203 ProjectURL: a.ProjectURL, 204 RepositoryURL: a.RepositoryURL, 205 DocumentationURL: a.DocumentationURL, 206 }, 207 FileMetadata: &FileMetadata{ 208 Architecture: i.Architecture, 209 NoArch: i.NoArch, 210 Build: i.Build, 211 BuildNumber: i.BuildNumber, 212 Dependencies: i.Dependencies, 213 Platform: i.Platform, 214 Timestamp: i.Timestamp, 215 }, 216 }, nil 217 } 218 219 // https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1393 220 func checkName(name string) bool { 221 if name == "" { 222 return false 223 } 224 if name != strings.ToLower(name) { 225 return false 226 } 227 return !checkBadCharacters(name, "!") 228 } 229 230 // https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1403 231 func checkVersion(version string) bool { 232 if version == "" { 233 return false 234 } 235 return !checkBadCharacters(version, "-") 236 } 237 238 func checkBadCharacters(s, additional string) bool { 239 if strings.ContainsAny(s, "=@#$%^&*:;\"'\\|<>?/ ") { 240 return true 241 } 242 return strings.ContainsAny(s, additional) 243 }