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