From dd136858f1ea40ad3c94191d647487fa4f31926c Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 18 Oct 2024 20:33:49 +0200 Subject: Adding upstream version 9.0.0. Signed-off-by: Daniel Baumann --- modules/packages/pub/metadata.go | 153 ++++++++++++++++++++++++++++++++++ modules/packages/pub/metadata_test.go | 136 ++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 modules/packages/pub/metadata.go create mode 100644 modules/packages/pub/metadata_test.go (limited to 'modules/packages/pub') diff --git a/modules/packages/pub/metadata.go b/modules/packages/pub/metadata.go new file mode 100644 index 0000000..afb464e --- /dev/null +++ b/modules/packages/pub/metadata.go @@ -0,0 +1,153 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pub + +import ( + "archive/tar" + "compress/gzip" + "io" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "github.com/hashicorp/go-version" + "gopkg.in/yaml.v3" +) + +var ( + ErrMissingPubspecFile = util.NewInvalidArgumentErrorf("Pubspec file is missing") + ErrPubspecFileTooLarge = util.NewInvalidArgumentErrorf("Pubspec file is too large") + ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") +) + +var namePattern = regexp.MustCompile(`\A[a-zA-Z_][a-zA-Z0-9_]*\z`) + +// https://github.com/dart-lang/pub-dev/blob/4d582302a8d10152a5cd6129f65bf4f4dbca239d/pkg/pub_package_reader/lib/pub_package_reader.dart#L143 +const maxPubspecFileSize = 128 * 1024 + +// Package represents a Pub package +type Package struct { + Name string + Version string + Metadata *Metadata +} + +// Metadata represents the metadata of a Pub package +type Metadata struct { + Description string `json:"description,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + DocumentationURL string `json:"documentation_url,omitempty"` + Readme string `json:"readme,omitempty"` + Pubspec any `json:"pubspec"` +} + +type pubspecPackage struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + Description string `yaml:"description"` + Homepage string `yaml:"homepage"` + Repository string `yaml:"repository"` + Documentation string `yaml:"documentation"` +} + +// ParsePackage parses the Pub package file +func ParsePackage(r io.Reader) (*Package, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + var p *Package + var readme string + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + if hd.Name == "pubspec.yaml" { + if hd.Size > maxPubspecFileSize { + return nil, ErrPubspecFileTooLarge + } + p, err = ParsePubspecMetadata(tr) + if err != nil { + return nil, err + } + } else if strings.ToLower(hd.Name) == "readme.md" { + data, err := io.ReadAll(tr) + if err != nil { + return nil, err + } + readme = string(data) + } + } + + if p == nil { + return nil, ErrMissingPubspecFile + } + + p.Metadata.Readme = readme + + return p, nil +} + +// ParsePubspecMetadata parses a Pubspec file to retrieve the metadata of a Pub package +func ParsePubspecMetadata(r io.Reader) (*Package, error) { + buf, err := io.ReadAll(io.LimitReader(r, maxPubspecFileSize)) + if err != nil { + return nil, err + } + + var p pubspecPackage + if err := yaml.Unmarshal(buf, &p); err != nil { + return nil, err + } + + if !namePattern.MatchString(p.Name) { + return nil, ErrInvalidName + } + + v, err := version.NewSemver(p.Version) + if err != nil { + return nil, ErrInvalidVersion + } + + if !validation.IsValidURL(p.Homepage) { + p.Homepage = "" + } + if !validation.IsValidURL(p.Repository) { + p.Repository = "" + } + + var pubspec any + if err := yaml.Unmarshal(buf, &pubspec); err != nil { + return nil, err + } + + return &Package{ + Name: p.Name, + Version: v.String(), + Metadata: &Metadata{ + Description: p.Description, + ProjectURL: p.Homepage, + RepositoryURL: p.Repository, + DocumentationURL: p.Documentation, + Pubspec: pubspec, + }, + }, nil +} diff --git a/modules/packages/pub/metadata_test.go b/modules/packages/pub/metadata_test.go new file mode 100644 index 0000000..5ed083b --- /dev/null +++ b/modules/packages/pub/metadata_test.go @@ -0,0 +1,136 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pub + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + description = "Package Description" + projectURL = "https://gitea.com" + repositoryURL = "https://gitea.com/gitea/gitea" + documentationURL = "https://docs.gitea.com" +) + +const pubspecContent = `name: ` + packageName + ` +version: ` + packageVersion + ` +description: ` + description + ` +homepage: ` + projectURL + ` +repository: ` + repositoryURL + ` +documentation: ` + documentationURL + ` + +environment: + sdk: '>=2.16.0 <3.0.0' + +dependencies: + flutter: + sdk: flutter + path: '>=1.8.0 <3.0.0' + +dev_dependencies: + http: '>=0.13.0'` + +func TestParsePackage(t *testing.T) { + createArchive := func(files map[string][]byte) io.Reader { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + tw := tar.NewWriter(zw) + for filename, content := range files { + hdr := &tar.Header{ + Name: filename, + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write(content) + } + tw.Close() + zw.Close() + return &buf + } + + t.Run("MissingPubspecFile", func(t *testing.T) { + data := createArchive(map[string][]byte{"dummy.txt": {}}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + require.ErrorIs(t, err, ErrMissingPubspecFile) + }) + + t.Run("PubspecFileTooLarge", func(t *testing.T) { + data := createArchive(map[string][]byte{"pubspec.yaml": make([]byte, 200*1024)}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + require.ErrorIs(t, err, ErrPubspecFileTooLarge) + }) + + t.Run("InvalidPubspecFile", func(t *testing.T) { + data := createArchive(map[string][]byte{"pubspec.yaml": {}}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + require.Error(t, err) + }) + + t.Run("Valid", func(t *testing.T) { + data := createArchive(map[string][]byte{"pubspec.yaml": []byte(pubspecContent)}) + + pp, err := ParsePackage(data) + require.NoError(t, err) + assert.NotNil(t, pp) + assert.Empty(t, pp.Metadata.Readme) + }) + + t.Run("ValidWithReadme", func(t *testing.T) { + data := createArchive(map[string][]byte{"pubspec.yaml": []byte(pubspecContent), "README.md": []byte("readme")}) + + pp, err := ParsePackage(data) + require.NoError(t, err) + assert.NotNil(t, pp) + assert.Equal(t, "readme", pp.Metadata.Readme) + }) +} + +func TestParsePubspecMetadata(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + for _, name := range []string{"123abc", "ab-cd"} { + pp, err := ParsePubspecMetadata(strings.NewReader(`name: ` + name)) + assert.Nil(t, pp) + require.ErrorIs(t, err, ErrInvalidName) + } + }) + + t.Run("InvalidVersion", func(t *testing.T) { + pp, err := ParsePubspecMetadata(strings.NewReader(`name: dummy +version: invalid`)) + assert.Nil(t, pp) + require.ErrorIs(t, err, ErrInvalidVersion) + }) + + t.Run("Valid", func(t *testing.T) { + pp, err := ParsePubspecMetadata(strings.NewReader(pubspecContent)) + require.NoError(t, err) + assert.NotNil(t, pp) + + assert.Equal(t, packageName, pp.Name) + assert.Equal(t, packageVersion, pp.Version) + assert.Equal(t, description, pp.Metadata.Description) + assert.Equal(t, projectURL, pp.Metadata.ProjectURL) + assert.Equal(t, repositoryURL, pp.Metadata.RepositoryURL) + assert.Equal(t, documentationURL, pp.Metadata.DocumentationURL) + assert.NotNil(t, pp.Metadata.Pubspec) + }) +} -- cgit v1.2.3