Skip to content

Commit f2db392

Browse files
committed
fix
1 parent 0fb3be7 commit f2db392

File tree

3 files changed

+183
-45
lines changed

3 files changed

+183
-45
lines changed

modules/packages/composer/metadata.go

Lines changed: 120 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
package composer
55

66
import (
7+
"archive/tar"
78
"archive/zip"
9+
"compress/bzip2"
10+
"compress/gzip"
811
"io"
12+
"io/fs"
913
"path"
1014
"regexp"
1115
"strings"
@@ -15,6 +19,7 @@ import (
1519
"code.gitea.io/gitea/modules/validation"
1620

1721
"github.com/hashicorp/go-version"
22+
"github.com/pkg/errors"
1823
)
1924

2025
// TypeProperty is the name of the property for Composer package types
@@ -29,8 +34,10 @@ var (
2934
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
3035
)
3136

32-
// Package represents a Composer package
33-
type Package struct {
37+
// PackageInfo represents Composer package info
38+
type PackageInfo struct {
39+
Filename string
40+
3441
Name string
3542
Version string
3643
Type string
@@ -44,7 +51,7 @@ type Metadata struct {
4451
Description string `json:"description,omitempty"`
4552
Readme string `json:"readme,omitempty"`
4653
Keywords []string `json:"keywords,omitempty"`
47-
Comments Comments `json:"_comments,omitempty"`
54+
Comments Comments `json:"_comment,omitempty"`
4855
Homepage string `json:"homepage,omitempty"`
4956
License Licenses `json:"license,omitempty"`
5057
Authors []Author `json:"authors,omitempty"`
@@ -75,7 +82,7 @@ func (l *Licenses) UnmarshalJSON(data []byte) error {
7582
if err := json.Unmarshal(data, &values); err != nil {
7683
return err
7784
}
78-
*l = Licenses(values)
85+
*l = values
7986
}
8087
return nil
8188
}
@@ -97,7 +104,7 @@ func (c *Comments) UnmarshalJSON(data []byte) error {
97104
if err := json.Unmarshal(data, &values); err != nil {
98105
return err
99106
}
100-
*c = Comments(values)
107+
*c = values
101108
}
102109
return nil
103110
}
@@ -111,39 +118,121 @@ type Author struct {
111118

112119
var nameMatch = regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`)
113120

114-
// ParsePackage parses the metadata of a Composer package file
115-
func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
116-
archive, err := zip.NewReader(r, size)
121+
type ReadSeekAt interface {
122+
io.Reader
123+
io.ReaderAt
124+
io.Seeker
125+
Size() int64
126+
}
127+
128+
func readPackageFileZip(r ReadSeekAt, filename string, limit int) ([]byte, error) {
129+
archive, err := zip.NewReader(r, r.Size())
117130
if err != nil {
118131
return nil, err
119132
}
120133

121134
for _, file := range archive.File {
122-
if strings.Count(file.Name, "/") > 1 {
123-
continue
124-
}
125-
if strings.HasSuffix(strings.ToLower(file.Name), "composer.json") {
135+
filePath := path.Clean(file.Name)
136+
if util.AsciiEqualFold(filePath, filename) {
126137
f, err := archive.Open(file.Name)
127138
if err != nil {
128139
return nil, err
129140
}
130141
defer f.Close()
131142

132-
return ParseComposerFile(archive, path.Dir(file.Name), f)
143+
return util.ReadWithLimit(f, limit)
144+
}
145+
}
146+
return nil, fs.ErrNotExist
147+
}
148+
149+
func readPackageFileTar(r io.Reader, filename string, limit int) ([]byte, error) {
150+
tarReader := tar.NewReader(r)
151+
for {
152+
header, err := tarReader.Next()
153+
if err == io.EOF {
154+
break
155+
} else if err != nil {
156+
return nil, err
157+
}
158+
159+
filePath := path.Clean(header.Name)
160+
if util.AsciiEqualFold(filePath, filename) {
161+
return util.ReadWithLimit(tarReader, limit)
162+
}
163+
}
164+
return nil, fs.ErrNotExist
165+
}
166+
167+
const (
168+
pkgExtZip = ".zip"
169+
pkgExtTarGz = ".tar.gz"
170+
pkgExtTarBz2 = ".tar.bz2"
171+
)
172+
173+
func detectPackageExtName(r ReadSeekAt) (string, error) {
174+
headBytes := make([]byte, 4)
175+
_, err := r.ReadAt(headBytes, 0)
176+
if err != nil {
177+
return "", err
178+
}
179+
_, err = r.Seek(0, io.SeekStart)
180+
if err != nil {
181+
return "", err
182+
}
183+
switch {
184+
case headBytes[0] == 'P' && headBytes[1] == 'K':
185+
return pkgExtZip, nil
186+
case string(headBytes[:3]) == "BZh":
187+
return pkgExtTarBz2, nil
188+
case headBytes[0] == 0x1f && headBytes[1] == 0x8b:
189+
return pkgExtTarGz, nil
190+
}
191+
return "", util.NewInvalidArgumentErrorf("not a valid package file")
192+
}
193+
194+
func readPackageFile(pkgExt string, r ReadSeekAt, filename string, limit int) ([]byte, error) {
195+
_, err := r.Seek(0, io.SeekStart)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
switch pkgExt {
201+
case pkgExtZip:
202+
return readPackageFileZip(r, filename, limit)
203+
case pkgExtTarBz2:
204+
bzip2Reader := bzip2.NewReader(r)
205+
return readPackageFileTar(bzip2Reader, filename, limit)
206+
case pkgExtTarGz:
207+
gzReader, err := gzip.NewReader(r)
208+
if err != nil {
209+
return nil, err
133210
}
211+
return readPackageFileTar(gzReader, filename, limit)
134212
}
135-
return nil, ErrMissingComposerFile
213+
return nil, util.NewInvalidArgumentErrorf("not a valid package file")
136214
}
137215

138-
// ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package
139-
func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Package, error) {
216+
// ParsePackage parses the metadata of a Composer package file
217+
func ParsePackage(r ReadSeekAt) (*PackageInfo, error) {
218+
pkgExt, err := detectPackageExtName(r)
219+
if err != nil {
220+
return nil, err
221+
}
222+
dataComposerJSON, err := readPackageFile(pkgExt, r, "composer.json", 10*1024*1024)
223+
if errors.Is(err, fs.ErrNotExist) {
224+
return nil, ErrMissingComposerFile
225+
} else if err != nil {
226+
return nil, err
227+
}
228+
140229
var cj struct {
141230
Name string `json:"name"`
142231
Version string `json:"version"`
143232
Type string `json:"type"`
144233
Metadata
145234
}
146-
if err := json.NewDecoder(r).Decode(&cj); err != nil {
235+
if err := json.Unmarshal(dataComposerJSON, &cj); err != nil {
147236
return nil, err
148237
}
149238

@@ -168,17 +257,23 @@ func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Pa
168257
if cj.Readme == "" {
169258
cj.Readme = "README.md"
170259
}
171-
f, err := archive.Open(path.Join(pathPrefix, cj.Readme))
172-
if err == nil {
173-
// 10kb limit for readme content
174-
buf, _ := io.ReadAll(io.LimitReader(f, 10*1024))
175-
cj.Readme = string(buf)
176-
_ = f.Close()
177-
} else {
260+
dataReadmeMd, _ := readPackageFile(pkgExt, r, cj.Readme, 10*1024)
261+
262+
// FIXME: legacy problem, the "Readme" field is abused, it should always be the path to the readme file
263+
if len(dataReadmeMd) == 0 {
178264
cj.Readme = ""
265+
} else {
266+
cj.Readme = string(dataReadmeMd)
179267
}
180268

181-
return &Package{
269+
// FIXME: legacy format: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)), doesn't read good
270+
pkgFilename := strings.ReplaceAll(cj.Name, "/", "-")
271+
if cj.Version != "" {
272+
pkgFilename += "." + cj.Version
273+
}
274+
pkgFilename += pkgExt
275+
return &PackageInfo{
276+
Filename: pkgFilename,
182277
Name: cj.Name,
183278
Version: cj.Version,
184279
Type: cj.Type,

modules/packages/composer/metadata_test.go

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@
44
package composer
55

66
import (
7+
"archive/tar"
78
"archive/zip"
89
"bytes"
10+
"compress/gzip"
11+
"io"
912
"strings"
1013
"testing"
1114

1215
"code.gitea.io/gitea/modules/json"
1316

17+
"github.com/dsnet/compress/bzip2"
1418
"github.com/stretchr/testify/assert"
19+
"github.com/stretchr/testify/require"
1520
)
1621

1722
const (
@@ -26,8 +31,10 @@ const (
2631
license = "MIT"
2732
)
2833

29-
const composerContent = `{
34+
func buildComposerContent(version string) string {
35+
return `{
3036
"name": "` + name + `",
37+
"version": "` + version + `",
3138
"description": "` + description + `",
3239
"type": "` + packageType + `",
3340
"license": "` + license + `",
@@ -44,8 +51,9 @@ const composerContent = `{
4451
"require": {
4552
"php": ">=7.2 || ^8.0"
4653
},
47-
"_comments": "` + comments + `"
54+
"_comment": "` + comments + `"
4855
}`
56+
}
4957

5058
func TestLicenseUnmarshal(t *testing.T) {
5159
var l Licenses
@@ -73,71 +81,88 @@ func TestParsePackage(t *testing.T) {
7381
archive := zip.NewWriter(&buf)
7482
for name, content := range files {
7583
w, _ := archive.Create(name)
76-
w.Write([]byte(content))
84+
_, _ = w.Write([]byte(content))
85+
}
86+
_ = archive.Close()
87+
return buf.Bytes()
88+
}
89+
90+
createArchiveTar := func(comp func(io.Writer) io.WriteCloser, files map[string]string) []byte {
91+
var buf bytes.Buffer
92+
w := comp(&buf)
93+
archive := tar.NewWriter(w)
94+
for name, content := range files {
95+
hdr := &tar.Header{
96+
Name: name,
97+
Mode: 0o600,
98+
Size: int64(len(content)),
99+
}
100+
_ = archive.WriteHeader(hdr)
101+
_, _ = archive.Write([]byte(content))
77102
}
78-
archive.Close()
103+
_ = w.Close()
104+
_ = archive.Close()
79105
return buf.Bytes()
80106
}
81107

82108
t.Run("MissingComposerFile", func(t *testing.T) {
83109
data := createArchive(map[string]string{"dummy.txt": ""})
84110

85-
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
111+
cp, err := ParsePackage(bytes.NewReader(data))
86112
assert.Nil(t, cp)
87113
assert.ErrorIs(t, err, ErrMissingComposerFile)
88114
})
89115

90116
t.Run("MissingComposerFileInRoot", func(t *testing.T) {
91117
data := createArchive(map[string]string{"sub/sub/composer.json": ""})
92118

93-
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
119+
cp, err := ParsePackage(bytes.NewReader(data))
94120
assert.Nil(t, cp)
95121
assert.ErrorIs(t, err, ErrMissingComposerFile)
96122
})
97123

98124
t.Run("InvalidComposerFile", func(t *testing.T) {
99125
data := createArchive(map[string]string{"composer.json": ""})
100126

101-
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
127+
cp, err := ParsePackage(bytes.NewReader(data))
102128
assert.Nil(t, cp)
103129
assert.Error(t, err)
104130
})
105131

106132
t.Run("InvalidPackageName", func(t *testing.T) {
107133
data := createArchive(map[string]string{"composer.json": "{}"})
108134

109-
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
135+
cp, err := ParsePackage(bytes.NewReader(data))
110136
assert.Nil(t, cp)
111137
assert.ErrorIs(t, err, ErrInvalidName)
112138
})
113139

114140
t.Run("InvalidPackageVersion", func(t *testing.T) {
115141
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`})
116142

117-
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
143+
cp, err := ParsePackage(bytes.NewReader(data))
118144
assert.Nil(t, cp)
119145
assert.ErrorIs(t, err, ErrInvalidVersion)
120146
})
121147

122148
t.Run("InvalidReadmePath", func(t *testing.T) {
123149
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`})
124150

125-
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
151+
cp, err := ParsePackage(bytes.NewReader(data))
126152
assert.NoError(t, err)
127153
assert.NotNil(t, cp)
128154

129155
assert.Empty(t, cp.Metadata.Readme)
130156
})
131157

132-
t.Run("Valid", func(t *testing.T) {
133-
data := createArchive(map[string]string{"composer.json": composerContent, "README.md": readme})
134-
135-
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
136-
assert.NoError(t, err)
158+
assertValidPackage := func(t *testing.T, data []byte, version, filename string) {
159+
cp, err := ParsePackage(bytes.NewReader(data))
160+
require.NoError(t, err)
137161
assert.NotNil(t, cp)
138162

163+
assert.Equal(t, filename, cp.Filename)
139164
assert.Equal(t, name, cp.Name)
140-
assert.Empty(t, cp.Version)
165+
assert.Equal(t, version, cp.Version)
141166
assert.Equal(t, description, cp.Metadata.Description)
142167
assert.Equal(t, readme, cp.Metadata.Readme)
143168
assert.Len(t, cp.Metadata.Comments, 1)
@@ -149,5 +174,25 @@ func TestParsePackage(t *testing.T) {
149174
assert.Equal(t, packageType, cp.Type)
150175
assert.Len(t, cp.Metadata.License, 1)
151176
assert.Equal(t, license, cp.Metadata.License[0])
177+
}
178+
179+
t.Run("ValidZip", func(t *testing.T) {
180+
data := createArchive(map[string]string{"composer.json": buildComposerContent(""), "README.md": readme})
181+
assertValidPackage(t, data, "", "gitea-composer-package.zip")
182+
})
183+
184+
t.Run("ValidTarBz2", func(t *testing.T) {
185+
data := createArchiveTar(func(w io.Writer) io.WriteCloser {
186+
bz2Writer, _ := bzip2.NewWriter(w, nil)
187+
return bz2Writer
188+
}, map[string]string{"composer.json": buildComposerContent("1.0"), "README.md": readme})
189+
assertValidPackage(t, data, "1.0", "gitea-composer-package.1.0.tar.bz2")
190+
})
191+
192+
t.Run("ValidTarGz", func(t *testing.T) {
193+
data := createArchiveTar(func(w io.Writer) io.WriteCloser {
194+
return gzip.NewWriter(w)
195+
}, map[string]string{"composer.json": buildComposerContent(""), "README.md": readme})
196+
assertValidPackage(t, data, "", "gitea-composer-package.tar.gz")
152197
})
153198
}

0 commit comments

Comments
 (0)