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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package nuget
  4. import (
  5. "archive/zip"
  6. "bytes"
  7. "encoding/xml"
  8. "fmt"
  9. "io"
  10. "path/filepath"
  11. "regexp"
  12. "strings"
  13. "code.gitea.io/gitea/modules/util"
  14. "code.gitea.io/gitea/modules/validation"
  15. "github.com/hashicorp/go-version"
  16. )
  17. var (
  18. // ErrMissingNuspecFile indicates a missing Nuspec file
  19. ErrMissingNuspecFile = util.NewInvalidArgumentErrorf("Nuspec file is missing")
  20. // ErrNuspecFileTooLarge indicates a Nuspec file which is too large
  21. ErrNuspecFileTooLarge = util.NewInvalidArgumentErrorf("Nuspec file is too large")
  22. // ErrNuspecInvalidID indicates an invalid id in the Nuspec file
  23. ErrNuspecInvalidID = util.NewInvalidArgumentErrorf("Nuspec file contains an invalid id")
  24. // ErrNuspecInvalidVersion indicates an invalid version in the Nuspec file
  25. ErrNuspecInvalidVersion = util.NewInvalidArgumentErrorf("Nuspec file contains an invalid version")
  26. )
  27. // PackageType specifies the package type the metadata describes
  28. type PackageType int
  29. const (
  30. // DependencyPackage represents a package (*.nupkg)
  31. DependencyPackage PackageType = iota + 1
  32. // SymbolsPackage represents a symbol package (*.snupkg)
  33. SymbolsPackage
  34. PropertySymbolID = "nuget.symbol.id"
  35. )
  36. var idmatch = regexp.MustCompile(`\A\w+(?:[.-]\w+)*\z`)
  37. const maxNuspecFileSize = 3 * 1024 * 1024
  38. // Package represents a Nuget package
  39. type Package struct {
  40. PackageType PackageType
  41. ID string
  42. Version string
  43. Metadata *Metadata
  44. }
  45. // Metadata represents the metadata of a Nuget package
  46. type Metadata struct {
  47. Description string `json:"description,omitempty"`
  48. ReleaseNotes string `json:"release_notes,omitempty"`
  49. Authors string `json:"authors,omitempty"`
  50. ProjectURL string `json:"project_url,omitempty"`
  51. RepositoryURL string `json:"repository_url,omitempty"`
  52. RequireLicenseAcceptance bool `json:"require_license_acceptance"`
  53. Dependencies map[string][]Dependency `json:"dependencies,omitempty"`
  54. }
  55. // Dependency represents a dependency of a Nuget package
  56. type Dependency struct {
  57. ID string `json:"id"`
  58. Version string `json:"version"`
  59. }
  60. type nuspecPackage struct {
  61. Metadata struct {
  62. ID string `xml:"id"`
  63. Version string `xml:"version"`
  64. Authors string `xml:"authors"`
  65. RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
  66. ProjectURL string `xml:"projectUrl"`
  67. Description string `xml:"description"`
  68. ReleaseNotes string `xml:"releaseNotes"`
  69. PackageTypes struct {
  70. PackageType []struct {
  71. Name string `xml:"name,attr"`
  72. } `xml:"packageType"`
  73. } `xml:"packageTypes"`
  74. Repository struct {
  75. URL string `xml:"url,attr"`
  76. } `xml:"repository"`
  77. Dependencies struct {
  78. Group []struct {
  79. TargetFramework string `xml:"targetFramework,attr"`
  80. Dependency []struct {
  81. ID string `xml:"id,attr"`
  82. Version string `xml:"version,attr"`
  83. Exclude string `xml:"exclude,attr"`
  84. } `xml:"dependency"`
  85. } `xml:"group"`
  86. } `xml:"dependencies"`
  87. } `xml:"metadata"`
  88. }
  89. // ParsePackageMetaData parses the metadata of a Nuget package file
  90. func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
  91. archive, err := zip.NewReader(r, size)
  92. if err != nil {
  93. return nil, err
  94. }
  95. for _, file := range archive.File {
  96. if filepath.Dir(file.Name) != "." {
  97. continue
  98. }
  99. if strings.HasSuffix(strings.ToLower(file.Name), ".nuspec") {
  100. if file.UncompressedSize64 > maxNuspecFileSize {
  101. return nil, ErrNuspecFileTooLarge
  102. }
  103. f, err := archive.Open(file.Name)
  104. if err != nil {
  105. return nil, err
  106. }
  107. defer f.Close()
  108. return ParseNuspecMetaData(f)
  109. }
  110. }
  111. return nil, ErrMissingNuspecFile
  112. }
  113. // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
  114. func ParseNuspecMetaData(r io.Reader) (*Package, error) {
  115. var p nuspecPackage
  116. if err := xml.NewDecoder(r).Decode(&p); err != nil {
  117. return nil, err
  118. }
  119. if !idmatch.MatchString(p.Metadata.ID) {
  120. return nil, ErrNuspecInvalidID
  121. }
  122. v, err := version.NewSemver(p.Metadata.Version)
  123. if err != nil {
  124. return nil, ErrNuspecInvalidVersion
  125. }
  126. if !validation.IsValidURL(p.Metadata.ProjectURL) {
  127. p.Metadata.ProjectURL = ""
  128. }
  129. packageType := DependencyPackage
  130. for _, pt := range p.Metadata.PackageTypes.PackageType {
  131. if pt.Name == "SymbolsPackage" {
  132. packageType = SymbolsPackage
  133. break
  134. }
  135. }
  136. m := &Metadata{
  137. Description: p.Metadata.Description,
  138. ReleaseNotes: p.Metadata.ReleaseNotes,
  139. Authors: p.Metadata.Authors,
  140. ProjectURL: p.Metadata.ProjectURL,
  141. RepositoryURL: p.Metadata.Repository.URL,
  142. RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
  143. Dependencies: make(map[string][]Dependency),
  144. }
  145. for _, group := range p.Metadata.Dependencies.Group {
  146. deps := make([]Dependency, 0, len(group.Dependency))
  147. for _, dep := range group.Dependency {
  148. if dep.ID == "" || dep.Version == "" {
  149. continue
  150. }
  151. deps = append(deps, Dependency{
  152. ID: dep.ID,
  153. Version: dep.Version,
  154. })
  155. }
  156. if len(deps) > 0 {
  157. m.Dependencies[group.TargetFramework] = deps
  158. }
  159. }
  160. return &Package{
  161. PackageType: packageType,
  162. ID: p.Metadata.ID,
  163. Version: toNormalizedVersion(v),
  164. Metadata: m,
  165. }, nil
  166. }
  167. // https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers
  168. // https://github.com/NuGet/NuGet.Client/blob/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Versioning/VersionFormatter.cs#L121
  169. func toNormalizedVersion(v *version.Version) string {
  170. var buf bytes.Buffer
  171. segments := v.Segments64()
  172. fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2])
  173. if len(segments) > 3 && segments[3] > 0 {
  174. fmt.Fprintf(&buf, ".%d", segments[3])
  175. }
  176. pre := v.Prerelease()
  177. if pre != "" {
  178. fmt.Fprint(&buf, "-", pre)
  179. }
  180. return buf.String()
  181. }