mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Add Package Registry (#16510)
* Added package store settings. * Added models. * Added generic package registry. * Added tests. * Added NuGet package registry. * Moved service index to api file. * Added NPM package registry. * Added Maven package registry. * Added PyPI package registry. * Summary is deprecated. * Changed npm name. * Sanitize project url. * Allow only scoped packages. * Added user interface. * Changed method name. * Added missing migration file. * Set page info. * Added documentation. * Added documentation links. * Fixed wrong error message. * Lint template files. * Fixed merge errors. * Fixed unit test storage path. * Switch to json module. * Added suggestions. * Added package webhook. * Add package api. * Fixed swagger file. * Fixed enum and comments. * Fixed NuGet pagination. * Print test names. * Added api tests. * Fixed access level. * Fix User unmarshal. * Added RubyGems package registry. * Fix lint. * Implemented io.Writer. * Added support for sha256/sha512 checksum files. * Improved maven-metadata.xml support. * Added support for symbol package uploads. * Added tests. * Added overview docs. * Added npm dependencies and keywords. * Added no-packages information. * Display file size. * Display asset count. * Fixed filter alignment. * Added package icons. * Formatted instructions. * Allow anonymous package downloads. * Fixed comments. * Fixed postgres test. * Moved file. * Moved models to models/packages. * Use correct error response format per client. * Use simpler search form. * Fixed IsProd. * Restructured data model. * Prevent empty filename. * Fix swagger. * Implemented user/org registry. * Implemented UI. * Use GetUserByIDCtx. * Use table for dependencies. * make svg * Added support for unscoped npm packages. * Add support for npm dist tags. * Added tests for npm tags. * Unlink packages if repository gets deleted. * Prevent user/org delete if a packages exist. * Use package unlink in repository service. * Added support for composer packages. * Restructured package docs. * Added missing tests. * Fixed generic content page. * Fixed docs. * Fixed swagger. * Added missing type. * Fixed ambiguous column. * Organize content store by sha256 hash. * Added admin package management. * Added support for sorting. * Add support for multiple identical versions/files. * Added missing repository unlink. * Added file properties. * make fmt * lint * Added Conan package registry. * Updated docs. * Unify package names. * Added swagger enum. * Use longer TEXT column type. * Removed version composite key. * Merged package and container registry. * Removed index. * Use dedicated package router. * Moved files to new location. * Updated docs. * Fixed JOIN order. * Fixed GROUP BY statement. * Fixed GROUP BY #2. * Added symbol server support. * Added more tests. * Set NOT NULL. * Added setting to disable package registries. * Moved auth into service. * refactor * Use ctx everywhere. * Added package cleanup task. * Changed packages path. * Added container registry. * Refactoring * Updated comparison. * Fix swagger. * Fixed table order. * Use token auth for npm routes. * Enabled ReverseProxy auth. * Added packages link for orgs. * Fixed anonymous org access. * Enable copy button for setup instructions. * Merge error * Added suggestions. * Fixed merge. * Handle "generic". * Added link for TODO. * Added suggestions. * Changed temporary buffer filename. * Added suggestions. * Apply suggestions from code review Co-authored-by: Thomas Boerger <thomas@webhippie.de> * Update docs/content/doc/packages/nuget.en-us.md Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Thomas Boerger <thomas@webhippie.de>
This commit is contained in:
		
							
								
								
									
										187
									
								
								modules/packages/nuget/metadata.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								modules/packages/nuget/metadata.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
// 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"
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"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:     v.String(),
 | 
			
		||||
		Metadata:    m,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										163
									
								
								modules/packages/nuget/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								modules/packages/nuget/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
// 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"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	id                = "System.Gitea"
 | 
			
		||||
	semver            = "1.0.1"
 | 
			
		||||
	authors           = "Gitea Authors"
 | 
			
		||||
	projectURL        = "https://gitea.io"
 | 
			
		||||
	description       = "Package Description"
 | 
			
		||||
	releaseNotes      = "Package Release Notes"
 | 
			
		||||
	repositoryURL     = "https://gitea.io/gitea/gitea"
 | 
			
		||||
	targetFramework   = ".NETStandard2.1"
 | 
			
		||||
	dependencyID      = "System.Text.Json"
 | 
			
		||||
	dependencyVersion = "5.0.0"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const nuspecContent = `<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 | 
			
		||||
  <metadata>
 | 
			
		||||
    <id>` + id + `</id>
 | 
			
		||||
    <version>` + semver + `</version>
 | 
			
		||||
    <authors>` + authors + `</authors>
 | 
			
		||||
    <requireLicenseAcceptance>true</requireLicenseAcceptance>
 | 
			
		||||
    <projectUrl>` + projectURL + `</projectUrl>
 | 
			
		||||
    <description>` + description + `</description>
 | 
			
		||||
    <releaseNotes>` + releaseNotes + `</releaseNotes>
 | 
			
		||||
    <repository url="` + repositoryURL + `" />
 | 
			
		||||
    <dependencies>
 | 
			
		||||
      <group targetFramework="` + targetFramework + `">
 | 
			
		||||
        <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" />
 | 
			
		||||
      </group>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
  </metadata>
 | 
			
		||||
</package>`
 | 
			
		||||
 | 
			
		||||
const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 | 
			
		||||
  <metadata>
 | 
			
		||||
    <id>` + id + `</id>
 | 
			
		||||
    <version>` + semver + `</version>
 | 
			
		||||
    <description>` + description + `</description>
 | 
			
		||||
    <packageTypes>
 | 
			
		||||
      <packageType name="SymbolsPackage" />
 | 
			
		||||
    </packageTypes>
 | 
			
		||||
    <dependencies>
 | 
			
		||||
      <group targetFramework="` + targetFramework + `" />
 | 
			
		||||
    </dependencies>
 | 
			
		||||
  </metadata>
 | 
			
		||||
</package>`
 | 
			
		||||
 | 
			
		||||
func TestParsePackageMetaData(t *testing.T) {
 | 
			
		||||
	createArchive := func(name, content string) []byte {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
		archive := zip.NewWriter(&buf)
 | 
			
		||||
		w, _ := archive.Create(name)
 | 
			
		||||
		w.Write([]byte(content))
 | 
			
		||||
		archive.Close()
 | 
			
		||||
		return buf.Bytes()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("MissingNuspecFile", func(t *testing.T) {
 | 
			
		||||
		data := createArchive("dummy.txt", "")
 | 
			
		||||
 | 
			
		||||
		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 | 
			
		||||
		assert.Nil(t, np)
 | 
			
		||||
		assert.ErrorIs(t, err, ErrMissingNuspecFile)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
 | 
			
		||||
		data := createArchive("sub/package.nuspec", "")
 | 
			
		||||
 | 
			
		||||
		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 | 
			
		||||
		assert.Nil(t, np)
 | 
			
		||||
		assert.ErrorIs(t, err, ErrMissingNuspecFile)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("InvalidNuspecFile", func(t *testing.T) {
 | 
			
		||||
		data := createArchive("package.nuspec", "")
 | 
			
		||||
 | 
			
		||||
		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 | 
			
		||||
		assert.Nil(t, np)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("InvalidPackageId", func(t *testing.T) {
 | 
			
		||||
		data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
		<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 | 
			
		||||
		  <metadata></metadata>
 | 
			
		||||
		</package>`)
 | 
			
		||||
 | 
			
		||||
		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 | 
			
		||||
		assert.Nil(t, np)
 | 
			
		||||
		assert.ErrorIs(t, err, ErrNuspecInvalidID)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("InvalidPackageVersion", func(t *testing.T) {
 | 
			
		||||
		data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
		<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 | 
			
		||||
		  <metadata>
 | 
			
		||||
			<id>`+id+`</id>
 | 
			
		||||
		  </metadata>
 | 
			
		||||
		</package>`)
 | 
			
		||||
 | 
			
		||||
		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 | 
			
		||||
		assert.Nil(t, np)
 | 
			
		||||
		assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Valid", func(t *testing.T) {
 | 
			
		||||
		data := createArchive("package.nuspec", nuspecContent)
 | 
			
		||||
 | 
			
		||||
		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, np)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParseNuspecMetaData(t *testing.T) {
 | 
			
		||||
	t.Run("Dependency Package", func(t *testing.T) {
 | 
			
		||||
		np, err := ParseNuspecMetaData(strings.NewReader(nuspecContent))
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, np)
 | 
			
		||||
		assert.Equal(t, DependencyPackage, np.PackageType)
 | 
			
		||||
 | 
			
		||||
		assert.Equal(t, id, np.ID)
 | 
			
		||||
		assert.Equal(t, semver, np.Version)
 | 
			
		||||
		assert.Equal(t, authors, np.Metadata.Authors)
 | 
			
		||||
		assert.Equal(t, projectURL, np.Metadata.ProjectURL)
 | 
			
		||||
		assert.Equal(t, description, np.Metadata.Description)
 | 
			
		||||
		assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
 | 
			
		||||
		assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
 | 
			
		||||
		assert.Len(t, np.Metadata.Dependencies, 1)
 | 
			
		||||
		assert.Contains(t, np.Metadata.Dependencies, targetFramework)
 | 
			
		||||
		deps := np.Metadata.Dependencies[targetFramework]
 | 
			
		||||
		assert.Len(t, deps, 1)
 | 
			
		||||
		assert.Equal(t, dependencyID, deps[0].ID)
 | 
			
		||||
		assert.Equal(t, dependencyVersion, deps[0].Version)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Symbols Package", func(t *testing.T) {
 | 
			
		||||
		np, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent))
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, np)
 | 
			
		||||
		assert.Equal(t, SymbolsPackage, np.PackageType)
 | 
			
		||||
 | 
			
		||||
		assert.Equal(t, id, np.ID)
 | 
			
		||||
		assert.Equal(t, semver, np.Version)
 | 
			
		||||
		assert.Equal(t, description, np.Metadata.Description)
 | 
			
		||||
		assert.Empty(t, np.Metadata.Dependencies)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										187
									
								
								modules/packages/nuget/symbol_extractor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								modules/packages/nuget/symbol_extractor.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
// Copyright 2022 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/binary"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrMissingPdbFiles       = errors.New("Package does not contain PDB files")
 | 
			
		||||
	ErrInvalidFiles          = errors.New("Package contains invalid files")
 | 
			
		||||
	ErrInvalidPdbMagicNumber = errors.New("Invalid Portable PDB magic number")
 | 
			
		||||
	ErrMissingPdbStream      = errors.New("Missing PDB stream")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PortablePdb struct {
 | 
			
		||||
	Name    string
 | 
			
		||||
	ID      string
 | 
			
		||||
	Content *packages.HashedBuffer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PortablePdbList []*PortablePdb
 | 
			
		||||
 | 
			
		||||
func (l PortablePdbList) Close() {
 | 
			
		||||
	for _, pdb := range l {
 | 
			
		||||
		pdb.Content.Close()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ExtractPortablePdb extracts PDB files from a .snupkg file
 | 
			
		||||
func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) {
 | 
			
		||||
	archive, err := zip.NewReader(r, size)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var pdbs PortablePdbList
 | 
			
		||||
 | 
			
		||||
	err = func() error {
 | 
			
		||||
		for _, file := range archive.File {
 | 
			
		||||
			if strings.HasSuffix(file.Name, "/") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			ext := strings.ToLower(filepath.Ext(file.Name))
 | 
			
		||||
 | 
			
		||||
			switch ext {
 | 
			
		||||
			case ".nuspec", ".xml", ".psmdcp", ".rels", ".p7s":
 | 
			
		||||
				continue
 | 
			
		||||
			case ".pdb":
 | 
			
		||||
				f, err := archive.Open(file.Name)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				buf, err := packages.CreateHashedBufferFromReader(f, 32*1024*1024)
 | 
			
		||||
 | 
			
		||||
				f.Close()
 | 
			
		||||
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				id, err := ParseDebugHeaderID(buf)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					buf.Close()
 | 
			
		||||
					return fmt.Errorf("Invalid PDB file: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if _, err := buf.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
					buf.Close()
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				pdbs = append(pdbs, &PortablePdb{
 | 
			
		||||
					Name:    path.Base(file.Name),
 | 
			
		||||
					ID:      id,
 | 
			
		||||
					Content: buf,
 | 
			
		||||
				})
 | 
			
		||||
			default:
 | 
			
		||||
				return ErrInvalidFiles
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		pdbs.Close()
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(pdbs) == 0 {
 | 
			
		||||
		return nil, ErrMissingPdbFiles
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return pdbs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseDebugHeaderID TODO
 | 
			
		||||
func ParseDebugHeaderID(r io.ReadSeeker) (string, error) {
 | 
			
		||||
	var magic uint32
 | 
			
		||||
	if err := binary.Read(r, binary.LittleEndian, &magic); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if magic != 0x424A5342 {
 | 
			
		||||
		return "", ErrInvalidPdbMagicNumber
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := r.Seek(8, io.SeekCurrent); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var versionStringSize int32
 | 
			
		||||
	if err := binary.Read(r, binary.LittleEndian, &versionStringSize); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := r.Seek(int64(versionStringSize), io.SeekCurrent); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := r.Seek(2, io.SeekCurrent); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var streamCount int16
 | 
			
		||||
	if err := binary.Read(r, binary.LittleEndian, &streamCount); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	read4ByteAlignedString := func(r io.Reader) (string, error) {
 | 
			
		||||
		b := make([]byte, 4)
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
		for {
 | 
			
		||||
			if _, err := r.Read(b); err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
			if i := bytes.IndexByte(b, 0); i != -1 {
 | 
			
		||||
				buf.Write(b[:i])
 | 
			
		||||
				return buf.String(), nil
 | 
			
		||||
			}
 | 
			
		||||
			buf.Write(b)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < int(streamCount); i++ {
 | 
			
		||||
		var offset uint32
 | 
			
		||||
		if err := binary.Read(r, binary.LittleEndian, &offset); err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := r.Seek(4, io.SeekCurrent); err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		name, err := read4ByteAlignedString(r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if name == "#Pdb" {
 | 
			
		||||
			if _, err := r.Seek(int64(offset), io.SeekStart); err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			b := make([]byte, 16)
 | 
			
		||||
			if _, err := r.Read(b); err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			data1 := binary.LittleEndian.Uint32(b[0:4])
 | 
			
		||||
			data2 := binary.LittleEndian.Uint16(b[4:6])
 | 
			
		||||
			data3 := binary.LittleEndian.Uint16(b[6:8])
 | 
			
		||||
			data4 := b[8:16]
 | 
			
		||||
 | 
			
		||||
			return fmt.Sprintf("%08x%04x%04x%04x%012x", data1, data2, data3, data4[:2], data4[2:]), nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "", ErrMissingPdbStream
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								modules/packages/nuget/symbol_extractor_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								modules/packages/nuget/symbol_extractor_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
// Copyright 2022 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/base64"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const pdbContent = `QlNKQgEAAQAAAAAADAAAAFBEQiB2MS4wAAAAAAAABgB8AAAAWAAAACNQZGIAAAAA1AAAAAgBAAAj
 | 
			
		||||
fgAA3AEAAAQAAAAjU3RyaW5ncwAAAADgAQAABAAAACNVUwDkAQAAMAAAACNHVUlEAAAAFAIAACgB
 | 
			
		||||
AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`
 | 
			
		||||
 | 
			
		||||
func TestExtractPortablePdb(t *testing.T) {
 | 
			
		||||
	createArchive := func(name string, content []byte) []byte {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
		archive := zip.NewWriter(&buf)
 | 
			
		||||
		w, _ := archive.Create(name)
 | 
			
		||||
		w.Write(content)
 | 
			
		||||
		archive.Close()
 | 
			
		||||
		return buf.Bytes()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("MissingPdbFiles", func(t *testing.T) {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
		zip.NewWriter(&buf).Close()
 | 
			
		||||
 | 
			
		||||
		pdbs, err := ExtractPortablePdb(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
 | 
			
		||||
		assert.ErrorIs(t, err, ErrMissingPdbFiles)
 | 
			
		||||
		assert.Empty(t, pdbs)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("InvalidFiles", func(t *testing.T) {
 | 
			
		||||
		data := createArchive("sub/test.bin", []byte{})
 | 
			
		||||
 | 
			
		||||
		pdbs, err := ExtractPortablePdb(bytes.NewReader(data), int64(len(data)))
 | 
			
		||||
		assert.ErrorIs(t, err, ErrInvalidFiles)
 | 
			
		||||
		assert.Empty(t, pdbs)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Valid", func(t *testing.T) {
 | 
			
		||||
		b, _ := base64.StdEncoding.DecodeString(pdbContent)
 | 
			
		||||
		data := createArchive("test.pdb", b)
 | 
			
		||||
 | 
			
		||||
		pdbs, err := ExtractPortablePdb(bytes.NewReader(data), int64(len(data)))
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Len(t, pdbs, 1)
 | 
			
		||||
		assert.Equal(t, "test.pdb", pdbs[0].Name)
 | 
			
		||||
		assert.Equal(t, "d910bb6948bd4c6cb40155bcf52c3c94", pdbs[0].ID)
 | 
			
		||||
		pdbs.Close()
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParseDebugHeaderID(t *testing.T) {
 | 
			
		||||
	t.Run("InvalidPdbMagicNumber", func(t *testing.T) {
 | 
			
		||||
		id, err := ParseDebugHeaderID(bytes.NewReader([]byte{0, 0, 0, 0}))
 | 
			
		||||
		assert.ErrorIs(t, err, ErrInvalidPdbMagicNumber)
 | 
			
		||||
		assert.Empty(t, id)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("MissingPdbStream", func(t *testing.T) {
 | 
			
		||||
		b, _ := base64.StdEncoding.DecodeString(`QlNKQgEAAQAAAAAADAAAAFBEQiB2MS4wAAAAAAAAAQB8AAAAWAAAACNVUwA=`)
 | 
			
		||||
 | 
			
		||||
		id, err := ParseDebugHeaderID(bytes.NewReader(b))
 | 
			
		||||
		assert.ErrorIs(t, err, ErrMissingPdbStream)
 | 
			
		||||
		assert.Empty(t, id)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Valid", func(t *testing.T) {
 | 
			
		||||
		b, _ := base64.StdEncoding.DecodeString(pdbContent)
 | 
			
		||||
 | 
			
		||||
		id, err := ParseDebugHeaderID(bytes.NewReader(b))
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, "d910bb6948bd4c6cb40155bcf52c3c94", id)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user