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 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package debian
  4. import (
  5. "archive/tar"
  6. "bufio"
  7. "compress/gzip"
  8. "io"
  9. "net/mail"
  10. "regexp"
  11. "strings"
  12. "code.gitea.io/gitea/modules/util"
  13. "code.gitea.io/gitea/modules/validation"
  14. "github.com/blakesmith/ar"
  15. "github.com/klauspost/compress/zstd"
  16. "github.com/ulikunitz/xz"
  17. )
  18. const (
  19. PropertyDistribution = "debian.distribution"
  20. PropertyComponent = "debian.component"
  21. PropertyArchitecture = "debian.architecture"
  22. PropertyControl = "debian.control"
  23. PropertyRepositoryIncludeInRelease = "debian.repository.include_in_release"
  24. SettingKeyPrivate = "debian.key.private"
  25. SettingKeyPublic = "debian.key.public"
  26. RepositoryPackage = "_debian"
  27. RepositoryVersion = "_repository"
  28. controlTar = "control.tar"
  29. )
  30. var (
  31. ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing")
  32. ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm")
  33. ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
  34. ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
  35. ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
  36. // https://www.debian.org/doc/debian-policy/ch-controlfields.html#source
  37. namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`)
  38. // https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
  39. versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
  40. )
  41. type Package struct {
  42. Name string
  43. Version string
  44. Architecture string
  45. Control string
  46. Metadata *Metadata
  47. }
  48. type Metadata struct {
  49. Maintainer string `json:"maintainer,omitempty"`
  50. ProjectURL string `json:"project_url,omitempty"`
  51. Description string `json:"description,omitempty"`
  52. Dependencies []string `json:"dependencies,omitempty"`
  53. }
  54. // ParsePackage parses the Debian package file
  55. // https://manpages.debian.org/bullseye/dpkg-dev/deb.5.en.html
  56. func ParsePackage(r io.Reader) (*Package, error) {
  57. arr := ar.NewReader(r)
  58. for {
  59. hd, err := arr.Next()
  60. if err == io.EOF {
  61. break
  62. }
  63. if err != nil {
  64. return nil, err
  65. }
  66. if strings.HasPrefix(hd.Name, controlTar) {
  67. var inner io.Reader
  68. switch hd.Name[len(controlTar):] {
  69. case "":
  70. inner = arr
  71. case ".gz":
  72. gzr, err := gzip.NewReader(arr)
  73. if err != nil {
  74. return nil, err
  75. }
  76. defer gzr.Close()
  77. inner = gzr
  78. case ".xz":
  79. xzr, err := xz.NewReader(arr)
  80. if err != nil {
  81. return nil, err
  82. }
  83. inner = xzr
  84. case ".zst":
  85. zr, err := zstd.NewReader(arr)
  86. if err != nil {
  87. return nil, err
  88. }
  89. defer zr.Close()
  90. inner = zr
  91. default:
  92. return nil, ErrUnsupportedCompression
  93. }
  94. tr := tar.NewReader(inner)
  95. for {
  96. hd, err := tr.Next()
  97. if err == io.EOF {
  98. break
  99. }
  100. if err != nil {
  101. return nil, err
  102. }
  103. if hd.Typeflag != tar.TypeReg {
  104. continue
  105. }
  106. if hd.FileInfo().Name() == "control" {
  107. return ParseControlFile(tr)
  108. }
  109. }
  110. }
  111. }
  112. return nil, ErrMissingControlFile
  113. }
  114. // ParseControlFile parses a Debian control file to retrieve the metadata
  115. func ParseControlFile(r io.Reader) (*Package, error) {
  116. p := &Package{
  117. Metadata: &Metadata{},
  118. }
  119. key := ""
  120. var depends strings.Builder
  121. var control strings.Builder
  122. s := bufio.NewScanner(io.TeeReader(r, &control))
  123. for s.Scan() {
  124. line := s.Text()
  125. trimmed := strings.TrimSpace(line)
  126. if trimmed == "" {
  127. continue
  128. }
  129. if line[0] == ' ' || line[0] == '\t' {
  130. switch key {
  131. case "Description":
  132. p.Metadata.Description += line
  133. case "Depends":
  134. depends.WriteString(trimmed)
  135. }
  136. } else {
  137. parts := strings.SplitN(trimmed, ":", 2)
  138. if len(parts) < 2 {
  139. continue
  140. }
  141. key = parts[0]
  142. value := strings.TrimSpace(parts[1])
  143. switch key {
  144. case "Package":
  145. if !namePattern.MatchString(value) {
  146. return nil, ErrInvalidName
  147. }
  148. p.Name = value
  149. case "Version":
  150. if !versionPattern.MatchString(value) {
  151. return nil, ErrInvalidVersion
  152. }
  153. p.Version = value
  154. case "Architecture":
  155. if value == "" {
  156. return nil, ErrInvalidArchitecture
  157. }
  158. p.Architecture = value
  159. case "Maintainer":
  160. a, err := mail.ParseAddress(value)
  161. if err != nil || a.Name == "" {
  162. p.Metadata.Maintainer = value
  163. } else {
  164. p.Metadata.Maintainer = a.Name
  165. }
  166. case "Description":
  167. p.Metadata.Description = value
  168. case "Depends":
  169. depends.WriteString(value)
  170. case "Homepage":
  171. if validation.IsValidURL(value) {
  172. p.Metadata.ProjectURL = value
  173. }
  174. }
  175. }
  176. }
  177. if err := s.Err(); err != nil {
  178. return nil, err
  179. }
  180. dependencies := strings.Split(depends.String(), ",")
  181. for i := range dependencies {
  182. dependencies[i] = strings.TrimSpace(dependencies[i])
  183. }
  184. p.Metadata.Dependencies = dependencies
  185. p.Control = control.String()
  186. return p, nil
  187. }