207 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			207 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Go
		
	
	
	
| // Copyright 2021 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package nuget
 | |
| 
 | |
| import (
 | |
| 	"archive/zip"
 | |
| 	"bytes"
 | |
| 	"encoding/xml"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"path/filepath"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 
 | |
| 	"code.gitea.io/gitea/modules/util"
 | |
| 	"code.gitea.io/gitea/modules/validation"
 | |
| 
 | |
| 	"github.com/hashicorp/go-version"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	// ErrMissingNuspecFile indicates a missing Nuspec file
 | |
| 	ErrMissingNuspecFile = util.NewInvalidArgumentErrorf("Nuspec file is missing")
 | |
| 	// ErrNuspecFileTooLarge indicates a Nuspec file which is too large
 | |
| 	ErrNuspecFileTooLarge = util.NewInvalidArgumentErrorf("Nuspec file is too large")
 | |
| 	// ErrNuspecInvalidID indicates an invalid id in the Nuspec file
 | |
| 	ErrNuspecInvalidID = util.NewInvalidArgumentErrorf("Nuspec file contains an invalid id")
 | |
| 	// ErrNuspecInvalidVersion indicates an invalid version in the Nuspec file
 | |
| 	ErrNuspecInvalidVersion = util.NewInvalidArgumentErrorf("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"`
 | |
| 	RequireLicenseAcceptance bool                    `json:"require_license_acceptance"`
 | |
| 	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,
 | |
| 		RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
 | |
| 		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()
 | |
| }
 |