You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

metadata.go 6.0KB

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