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  }