gitea源码

metadata.go 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package cran
  4. import (
  5. "archive/tar"
  6. "archive/zip"
  7. "bufio"
  8. "compress/gzip"
  9. "io"
  10. "path"
  11. "regexp"
  12. "strings"
  13. "code.gitea.io/gitea/modules/util"
  14. )
  15. const (
  16. PropertyType = "cran.type"
  17. PropertyPlatform = "cran.platform"
  18. PropertyRVersion = "cran.rvserion"
  19. TypeSource = "source"
  20. TypeBinary = "binary"
  21. )
  22. var (
  23. ErrMissingDescriptionFile = util.NewInvalidArgumentErrorf("DESCRIPTION file is missing")
  24. ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
  25. ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
  26. )
  27. var (
  28. fieldPattern = regexp.MustCompile(`\A\S+:`)
  29. namePattern = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9\.]*[a-zA-Z0-9]\z`)
  30. versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+){1,3}\z`)
  31. authorReplacePattern = regexp.MustCompile(`[\[\(].+?[\]\)]`)
  32. )
  33. // Package represents a CRAN package
  34. type Package struct {
  35. Name string
  36. Version string
  37. FileExtension string
  38. Metadata *Metadata
  39. }
  40. // Metadata represents the metadata of a CRAN package
  41. type Metadata struct {
  42. Title string `json:"title,omitempty"`
  43. Description string `json:"description,omitempty"`
  44. ProjectURL []string `json:"project_url,omitempty"`
  45. License string `json:"license,omitempty"`
  46. Authors []string `json:"authors,omitempty"`
  47. Depends []string `json:"depends,omitempty"`
  48. Imports []string `json:"imports,omitempty"`
  49. Suggests []string `json:"suggests,omitempty"`
  50. LinkingTo []string `json:"linking_to,omitempty"`
  51. NeedsCompilation bool `json:"needs_compilation"`
  52. }
  53. type ReaderReaderAt interface {
  54. io.Reader
  55. io.ReaderAt
  56. }
  57. // ParsePackage reads the package metadata from a CRAN package
  58. // .zip and .tar.gz/.tgz files are supported.
  59. func ParsePackage(r ReaderReaderAt, size int64) (*Package, error) {
  60. magicBytes := make([]byte, 2)
  61. if _, err := r.ReadAt(magicBytes, 0); err != nil {
  62. return nil, err
  63. }
  64. if magicBytes[0] == 0x1F && magicBytes[1] == 0x8B {
  65. return parsePackageTarGz(r)
  66. }
  67. return parsePackageZip(r, size)
  68. }
  69. func parsePackageTarGz(r io.Reader) (*Package, error) {
  70. gzr, err := gzip.NewReader(r)
  71. if err != nil {
  72. return nil, err
  73. }
  74. defer gzr.Close()
  75. tr := tar.NewReader(gzr)
  76. for {
  77. hd, err := tr.Next()
  78. if err == io.EOF {
  79. break
  80. }
  81. if err != nil {
  82. return nil, err
  83. }
  84. if hd.Typeflag != tar.TypeReg {
  85. continue
  86. }
  87. if strings.Count(hd.Name, "/") > 1 {
  88. continue
  89. }
  90. if path.Base(hd.Name) == "DESCRIPTION" {
  91. p, err := ParseDescription(tr)
  92. if p != nil {
  93. p.FileExtension = ".tar.gz"
  94. }
  95. return p, err
  96. }
  97. }
  98. return nil, ErrMissingDescriptionFile
  99. }
  100. func parsePackageZip(r io.ReaderAt, size int64) (*Package, error) {
  101. zr, err := zip.NewReader(r, size)
  102. if err != nil {
  103. return nil, err
  104. }
  105. for _, file := range zr.File {
  106. if strings.Count(file.Name, "/") > 1 {
  107. continue
  108. }
  109. if path.Base(file.Name) == "DESCRIPTION" {
  110. f, err := zr.Open(file.Name)
  111. if err != nil {
  112. return nil, err
  113. }
  114. defer f.Close()
  115. p, err := ParseDescription(f)
  116. if p != nil {
  117. p.FileExtension = ".zip"
  118. }
  119. return p, err
  120. }
  121. }
  122. return nil, ErrMissingDescriptionFile
  123. }
  124. // ParseDescription parses a DESCRIPTION file to retrieve the metadata of a CRAN package
  125. func ParseDescription(r io.Reader) (*Package, error) {
  126. p := &Package{
  127. Metadata: &Metadata{},
  128. }
  129. scanner := bufio.NewScanner(r)
  130. var b strings.Builder
  131. for scanner.Scan() {
  132. line := strings.TrimSpace(scanner.Text())
  133. if line == "" {
  134. continue
  135. }
  136. if !fieldPattern.MatchString(line) {
  137. b.WriteRune(' ')
  138. b.WriteString(line)
  139. continue
  140. }
  141. if err := setField(p, b.String()); err != nil {
  142. return nil, err
  143. }
  144. b.Reset()
  145. b.WriteString(line)
  146. }
  147. if err := setField(p, b.String()); err != nil {
  148. return nil, err
  149. }
  150. if err := scanner.Err(); err != nil {
  151. return nil, err
  152. }
  153. return p, nil
  154. }
  155. func setField(p *Package, data string) error {
  156. if data == "" {
  157. return nil
  158. }
  159. parts := strings.SplitN(data, ":", 2)
  160. if len(parts) != 2 {
  161. return nil
  162. }
  163. name := strings.TrimSpace(parts[0])
  164. value := strings.TrimSpace(parts[1])
  165. switch name {
  166. case "Package":
  167. if !namePattern.MatchString(value) {
  168. return ErrInvalidName
  169. }
  170. p.Name = value
  171. case "Version":
  172. if !versionPattern.MatchString(value) {
  173. return ErrInvalidVersion
  174. }
  175. p.Version = value
  176. case "Title":
  177. p.Metadata.Title = value
  178. case "Description":
  179. p.Metadata.Description = value
  180. case "URL":
  181. p.Metadata.ProjectURL = splitAndTrim(value)
  182. case "License":
  183. p.Metadata.License = value
  184. case "Author":
  185. p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""))
  186. case "Depends":
  187. p.Metadata.Depends = splitAndTrim(value)
  188. case "Imports":
  189. p.Metadata.Imports = splitAndTrim(value)
  190. case "Suggests":
  191. p.Metadata.Suggests = splitAndTrim(value)
  192. case "LinkingTo":
  193. p.Metadata.LinkingTo = splitAndTrim(value)
  194. case "NeedsCompilation":
  195. p.Metadata.NeedsCompilation = value == "yes"
  196. }
  197. return nil
  198. }
  199. func splitAndTrim(s string) []string {
  200. items := strings.Split(s, ", ")
  201. for i := range items {
  202. items[i] = strings.TrimSpace(items[i])
  203. }
  204. return items
  205. }