code.gitea.io/gitea@v1.22.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  	NuspecContent *bytes.Buffer
    56  }
    57  
    58  // Metadata represents the metadata of a Nuget package
    59  type Metadata struct {
    60  	Description              string                  `json:"description,omitempty"`
    61  	ReleaseNotes             string                  `json:"release_notes,omitempty"`
    62  	Readme                   string                  `json:"readme,omitempty"`
    63  	Authors                  string                  `json:"authors,omitempty"`
    64  	ProjectURL               string                  `json:"project_url,omitempty"`
    65  	RepositoryURL            string                  `json:"repository_url,omitempty"`
    66  	RequireLicenseAcceptance bool                    `json:"require_license_acceptance"`
    67  	Dependencies             map[string][]Dependency `json:"dependencies,omitempty"`
    68  }
    69  
    70  // Dependency represents a dependency of a Nuget package
    71  type Dependency struct {
    72  	ID      string `json:"id"`
    73  	Version string `json:"version"`
    74  }
    75  
    76  // https://learn.microsoft.com/en-us/nuget/reference/nuspec
    77  type nuspecPackage struct {
    78  	Metadata struct {
    79  		ID                       string `xml:"id"`
    80  		Version                  string `xml:"version"`
    81  		Authors                  string `xml:"authors"`
    82  		RequireLicenseAcceptance bool   `xml:"requireLicenseAcceptance"`
    83  		ProjectURL               string `xml:"projectUrl"`
    84  		Description              string `xml:"description"`
    85  		ReleaseNotes             string `xml:"releaseNotes"`
    86  		Readme                   string `xml:"readme"`
    87  		PackageTypes             struct {
    88  			PackageType []struct {
    89  				Name string `xml:"name,attr"`
    90  			} `xml:"packageType"`
    91  		} `xml:"packageTypes"`
    92  		Repository struct {
    93  			URL string `xml:"url,attr"`
    94  		} `xml:"repository"`
    95  		Dependencies struct {
    96  			Dependency []struct {
    97  				ID      string `xml:"id,attr"`
    98  				Version string `xml:"version,attr"`
    99  				Exclude string `xml:"exclude,attr"`
   100  			} `xml:"dependency"`
   101  			Group []struct {
   102  				TargetFramework string `xml:"targetFramework,attr"`
   103  				Dependency      []struct {
   104  					ID      string `xml:"id,attr"`
   105  					Version string `xml:"version,attr"`
   106  					Exclude string `xml:"exclude,attr"`
   107  				} `xml:"dependency"`
   108  			} `xml:"group"`
   109  		} `xml:"dependencies"`
   110  	} `xml:"metadata"`
   111  }
   112  
   113  // ParsePackageMetaData parses the metadata of a Nuget package file
   114  func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
   115  	archive, err := zip.NewReader(r, size)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	for _, file := range archive.File {
   121  		if filepath.Dir(file.Name) != "." {
   122  			continue
   123  		}
   124  		if strings.HasSuffix(strings.ToLower(file.Name), ".nuspec") {
   125  			if file.UncompressedSize64 > maxNuspecFileSize {
   126  				return nil, ErrNuspecFileTooLarge
   127  			}
   128  			f, err := archive.Open(file.Name)
   129  			if err != nil {
   130  				return nil, err
   131  			}
   132  			defer f.Close()
   133  
   134  			return ParseNuspecMetaData(archive, f)
   135  		}
   136  	}
   137  	return nil, ErrMissingNuspecFile
   138  }
   139  
   140  // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
   141  func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
   142  	var nuspecBuf bytes.Buffer
   143  	var p nuspecPackage
   144  	if err := xml.NewDecoder(io.TeeReader(r, &nuspecBuf)).Decode(&p); err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	if !idmatch.MatchString(p.Metadata.ID) {
   149  		return nil, ErrNuspecInvalidID
   150  	}
   151  
   152  	v, err := version.NewSemver(p.Metadata.Version)
   153  	if err != nil {
   154  		return nil, ErrNuspecInvalidVersion
   155  	}
   156  
   157  	if !validation.IsValidURL(p.Metadata.ProjectURL) {
   158  		p.Metadata.ProjectURL = ""
   159  	}
   160  
   161  	packageType := DependencyPackage
   162  	for _, pt := range p.Metadata.PackageTypes.PackageType {
   163  		if pt.Name == "SymbolsPackage" {
   164  			packageType = SymbolsPackage
   165  			break
   166  		}
   167  	}
   168  
   169  	m := &Metadata{
   170  		Description:              p.Metadata.Description,
   171  		ReleaseNotes:             p.Metadata.ReleaseNotes,
   172  		Authors:                  p.Metadata.Authors,
   173  		ProjectURL:               p.Metadata.ProjectURL,
   174  		RepositoryURL:            p.Metadata.Repository.URL,
   175  		RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
   176  		Dependencies:             make(map[string][]Dependency),
   177  	}
   178  
   179  	if p.Metadata.Readme != "" {
   180  		f, err := archive.Open(p.Metadata.Readme)
   181  		if err == nil {
   182  			buf, _ := io.ReadAll(f)
   183  			m.Readme = string(buf)
   184  			_ = f.Close()
   185  		}
   186  	}
   187  
   188  	if len(p.Metadata.Dependencies.Dependency) > 0 {
   189  		deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
   190  		for _, dep := range p.Metadata.Dependencies.Dependency {
   191  			if dep.ID == "" || dep.Version == "" {
   192  				continue
   193  			}
   194  			deps = append(deps, Dependency{
   195  				ID:      dep.ID,
   196  				Version: dep.Version,
   197  			})
   198  		}
   199  		m.Dependencies[""] = deps
   200  	}
   201  	for _, group := range p.Metadata.Dependencies.Group {
   202  		deps := make([]Dependency, 0, len(group.Dependency))
   203  		for _, dep := range group.Dependency {
   204  			if dep.ID == "" || dep.Version == "" {
   205  				continue
   206  			}
   207  			deps = append(deps, Dependency{
   208  				ID:      dep.ID,
   209  				Version: dep.Version,
   210  			})
   211  		}
   212  		if len(deps) > 0 {
   213  			m.Dependencies[group.TargetFramework] = deps
   214  		}
   215  	}
   216  	return &Package{
   217  		PackageType:   packageType,
   218  		ID:            p.Metadata.ID,
   219  		Version:       toNormalizedVersion(v),
   220  		Metadata:      m,
   221  		NuspecContent: &nuspecBuf,
   222  	}, nil
   223  }
   224  
   225  // https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers
   226  // https://github.com/NuGet/NuGet.Client/blob/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Versioning/VersionFormatter.cs#L121
   227  func toNormalizedVersion(v *version.Version) string {
   228  	var buf bytes.Buffer
   229  	segments := v.Segments64()
   230  	fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2])
   231  	if len(segments) > 3 && segments[3] > 0 {
   232  		fmt.Fprintf(&buf, ".%d", segments[3])
   233  	}
   234  	pre := v.Prerelease()
   235  	if pre != "" {
   236  		fmt.Fprint(&buf, "-", pre)
   237  	}
   238  	return buf.String()
   239  }