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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. // https://man7.org/linux/man-pages/man5/deb-split.5.html#FORMAT
  69. // The file names might contain a trailing slash (since dpkg 1.15.6).
  70. switch strings.TrimSuffix(hd.Name[len(controlTar):], "/") {
  71. case "":
  72. inner = arr
  73. case ".gz":
  74. gzr, err := gzip.NewReader(arr)
  75. if err != nil {
  76. return nil, err
  77. }
  78. defer gzr.Close()
  79. inner = gzr
  80. case ".xz":
  81. xzr, err := xz.NewReader(arr)
  82. if err != nil {
  83. return nil, err
  84. }
  85. inner = xzr
  86. case ".zst":
  87. zr, err := zstd.NewReader(arr)
  88. if err != nil {
  89. return nil, err
  90. }
  91. defer zr.Close()
  92. inner = zr
  93. default:
  94. return nil, ErrUnsupportedCompression
  95. }
  96. tr := tar.NewReader(inner)
  97. for {
  98. hd, err := tr.Next()
  99. if err == io.EOF {
  100. break
  101. }
  102. if err != nil {
  103. return nil, err
  104. }
  105. if hd.Typeflag != tar.TypeReg {
  106. continue
  107. }
  108. if hd.FileInfo().Name() == "control" {
  109. return ParseControlFile(tr)
  110. }
  111. }
  112. }
  113. }
  114. return nil, ErrMissingControlFile
  115. }
  116. // ParseControlFile parses a Debian control file to retrieve the metadata
  117. func ParseControlFile(r io.Reader) (*Package, error) {
  118. p := &Package{
  119. Metadata: &Metadata{},
  120. }
  121. key := ""
  122. var depends strings.Builder
  123. var control strings.Builder
  124. s := bufio.NewScanner(io.TeeReader(r, &control))
  125. for s.Scan() {
  126. line := s.Text()
  127. trimmed := strings.TrimSpace(line)
  128. if trimmed == "" {
  129. continue
  130. }
  131. if line[0] == ' ' || line[0] == '\t' {
  132. switch key {
  133. case "Description":
  134. p.Metadata.Description += line
  135. case "Depends":
  136. depends.WriteString(trimmed)
  137. }
  138. } else {
  139. parts := strings.SplitN(trimmed, ":", 2)
  140. if len(parts) < 2 {
  141. continue
  142. }
  143. key = parts[0]
  144. value := strings.TrimSpace(parts[1])
  145. switch key {
  146. case "Package":
  147. p.Name = value
  148. case "Version":
  149. p.Version = value
  150. case "Architecture":
  151. p.Architecture = value
  152. case "Maintainer":
  153. a, err := mail.ParseAddress(value)
  154. if err != nil || a.Name == "" {
  155. p.Metadata.Maintainer = value
  156. } else {
  157. p.Metadata.Maintainer = a.Name
  158. }
  159. case "Description":
  160. p.Metadata.Description = value
  161. case "Depends":
  162. depends.WriteString(value)
  163. case "Homepage":
  164. if validation.IsValidURL(value) {
  165. p.Metadata.ProjectURL = value
  166. }
  167. }
  168. }
  169. }
  170. if err := s.Err(); err != nil {
  171. return nil, err
  172. }
  173. if !namePattern.MatchString(p.Name) {
  174. return nil, ErrInvalidName
  175. }
  176. if !versionPattern.MatchString(p.Version) {
  177. return nil, ErrInvalidVersion
  178. }
  179. if p.Architecture == "" {
  180. return nil, ErrInvalidArchitecture
  181. }
  182. dependencies := strings.Split(depends.String(), ",")
  183. for i := range dependencies {
  184. dependencies[i] = strings.TrimSpace(dependencies[i])
  185. }
  186. p.Metadata.Dependencies = dependencies
  187. p.Control = strings.TrimSpace(control.String())
  188. return p, nil
  189. }