code.gitea.io/gitea@v1.19.3/modules/packages/nuget/metadata.go (about)

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