// Copyright 2021 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package nuget import ( "archive/zip" "bytes" "encoding/xml" "errors" "fmt" "io" "path/filepath" "regexp" "strings" "code.gitea.io/gitea/modules/validation" "github.com/hashicorp/go-version" ) var ( // ErrMissingNuspecFile indicates a missing Nuspec file ErrMissingNuspecFile = errors.New("Nuspec file is missing") // ErrNuspecFileTooLarge indicates a Nuspec file which is too large ErrNuspecFileTooLarge = errors.New("Nuspec file is too large") // ErrNuspecInvalidID indicates an invalid id in the Nuspec file ErrNuspecInvalidID = errors.New("Nuspec file contains an invalid id") // ErrNuspecInvalidVersion indicates an invalid version in the Nuspec file ErrNuspecInvalidVersion = errors.New("Nuspec file contains an invalid version") ) // PackageType specifies the package type the metadata describes type PackageType int const ( // DependencyPackage represents a package (*.nupkg) DependencyPackage PackageType = iota + 1 // SymbolsPackage represents a symbol package (*.snupkg) SymbolsPackage PropertySymbolID = "nuget.symbol.id" ) var idmatch = regexp.MustCompile(`\A\w+(?:[.-]\w+)*\z`) const maxNuspecFileSize = 3 * 1024 * 1024 // Package represents a Nuget package type Package struct { PackageType PackageType ID string Version string Metadata *Metadata } // Metadata represents the metadata of a Nuget package type Metadata struct { Description string `json:"description,omitempty"` ReleaseNotes string `json:"release_notes,omitempty"` Authors string `json:"authors,omitempty"` ProjectURL string `json:"project_url,omitempty"` RepositoryURL string `json:"repository_url,omitempty"` Dependencies map[string][]Dependency `json:"dependencies,omitempty"` } // Dependency represents a dependency of a Nuget package type Dependency struct { ID string `json:"id"` Version string `json:"version"` } type nuspecPackage struct { Metadata struct { ID string `xml:"id"` Version string `xml:"version"` Authors string `xml:"authors"` RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` ProjectURL string `xml:"projectUrl"` Description string `xml:"description"` ReleaseNotes string `xml:"releaseNotes"` PackageTypes struct { PackageType []struct { Name string `xml:"name,attr"` } `xml:"packageType"` } `xml:"packageTypes"` Repository struct { URL string `xml:"url,attr"` } `xml:"repository"` Dependencies struct { Group []struct { TargetFramework string `xml:"targetFramework,attr"` Dependency []struct { ID string `xml:"id,attr"` Version string `xml:"version,attr"` Exclude string `xml:"exclude,attr"` } `xml:"dependency"` } `xml:"group"` } `xml:"dependencies"` } `xml:"metadata"` } // ParsePackageMetaData parses the metadata of a Nuget package file func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { archive, err := zip.NewReader(r, size) if err != nil { return nil, err } for _, file := range archive.File { if filepath.Dir(file.Name) != "." { continue } if strings.HasSuffix(strings.ToLower(file.Name), ".nuspec") { if file.UncompressedSize64 > maxNuspecFileSize { return nil, ErrNuspecFileTooLarge } f, err := archive.Open(file.Name) if err != nil { return nil, err } defer f.Close() return ParseNuspecMetaData(f) } } return nil, ErrMissingNuspecFile } // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package func ParseNuspecMetaData(r io.Reader) (*Package, error) { var p nuspecPackage if err := xml.NewDecoder(r).Decode(&p); err != nil { return nil, err } if !idmatch.MatchString(p.Metadata.ID) { return nil, ErrNuspecInvalidID } v, err := version.NewSemver(p.Metadata.Version) if err != nil { return nil, ErrNuspecInvalidVersion } if !validation.IsValidURL(p.Metadata.ProjectURL) { p.Metadata.ProjectURL = "" } packageType := DependencyPackage for _, pt := range p.Metadata.PackageTypes.PackageType { if pt.Name == "SymbolsPackage" { packageType = SymbolsPackage break } } m := &Metadata{ Description: p.Metadata.Description, ReleaseNotes: p.Metadata.ReleaseNotes, Authors: p.Metadata.Authors, ProjectURL: p.Metadata.ProjectURL, RepositoryURL: p.Metadata.Repository.URL, Dependencies: make(map[string][]Dependency), } for _, group := range p.Metadata.Dependencies.Group { deps := make([]Dependency, 0, len(group.Dependency)) for _, dep := range group.Dependency { if dep.ID == "" || dep.Version == "" { continue } deps = append(deps, Dependency{ ID: dep.ID, Version: dep.Version, }) } if len(deps) > 0 { m.Dependencies[group.TargetFramework] = deps } } return &Package{ PackageType: packageType, ID: p.Metadata.ID, Version: toNormalizedVersion(v), Metadata: m, }, nil } // https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers // https://github.com/NuGet/NuGet.Client/blob/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Versioning/VersionFormatter.cs#L121 func toNormalizedVersion(v *version.Version) string { var buf bytes.Buffer segments := v.Segments64() fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) if len(segments) > 3 && segments[3] > 0 { fmt.Fprintf(&buf, ".%d", segments[3]) } pre := v.Prerelease() if pre != "" { fmt.Fprint(&buf, "-", pre) } return buf.String() }