summaryrefslogtreecommitdiffstats
path: root/modules/packages/pub/metadata.go
blob: afb464e462057ba2e7aae2fea06c32863e03206a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
}