diff options
Diffstat (limited to 'modules/packages')
-rw-r--r-- | modules/packages/debian/metadata.go | 218 | ||||
-rw-r--r-- | modules/packages/debian/metadata_test.go | 171 | ||||
-rw-r--r-- | modules/packages/hashed_buffer.go | 22 | ||||
-rw-r--r-- | modules/packages/hashed_buffer_test.go | 2 | ||||
-rw-r--r-- | modules/packages/nuget/symbol_extractor.go | 2 |
5 files changed, 408 insertions, 7 deletions
diff --git a/modules/packages/debian/metadata.go b/modules/packages/debian/metadata.go new file mode 100644 index 0000000000..dee524c8ff --- /dev/null +++ b/modules/packages/debian/metadata.go @@ -0,0 +1,218 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package debian + +import ( + "archive/tar" + "bufio" + "compress/gzip" + "io" + "net/mail" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "github.com/blakesmith/ar" + "github.com/klauspost/compress/zstd" + "github.com/ulikunitz/xz" +) + +const ( + PropertyDistribution = "debian.distribution" + PropertyComponent = "debian.component" + PropertyArchitecture = "debian.architecture" + PropertyControl = "debian.control" + PropertyRepositoryIncludeInRelease = "debian.repository.include_in_release" + + SettingKeyPrivate = "debian.key.private" + SettingKeyPublic = "debian.key.public" + + RepositoryPackage = "_debian" + RepositoryVersion = "_repository" + + controlTar = "control.tar" +) + +var ( + ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing") + ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm") + ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") + ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") + + // https://www.debian.org/doc/debian-policy/ch-controlfields.html#source + namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) + // https://www.debian.org/doc/debian-policy/ch-controlfields.html#version + versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) +) + +type Package struct { + Name string + Version string + Architecture string + Control string + Metadata *Metadata +} + +type Metadata struct { + Maintainer string `json:"maintainer,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Description string `json:"description,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` +} + +// ParsePackage parses the Debian package file +// https://manpages.debian.org/bullseye/dpkg-dev/deb.5.en.html +func ParsePackage(r io.Reader) (*Package, error) { + arr := ar.NewReader(r) + + for { + hd, err := arr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if strings.HasPrefix(hd.Name, controlTar) { + var inner io.Reader + switch hd.Name[len(controlTar):] { + case "": + inner = arr + case ".gz": + gzr, err := gzip.NewReader(arr) + if err != nil { + return nil, err + } + defer gzr.Close() + + inner = gzr + case ".xz": + xzr, err := xz.NewReader(arr) + if err != nil { + return nil, err + } + + inner = xzr + case ".zst": + zr, err := zstd.NewReader(arr) + if err != nil { + return nil, err + } + defer zr.Close() + + inner = zr + default: + return nil, ErrUnsupportedCompression + } + + tr := tar.NewReader(inner) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + if hd.FileInfo().Name() == "control" { + return ParseControlFile(tr) + } + } + } + } + + return nil, ErrMissingControlFile +} + +// ParseControlFile parses a Debian control file to retrieve the metadata +func ParseControlFile(r io.Reader) (*Package, error) { + p := &Package{ + Metadata: &Metadata{}, + } + + key := "" + var depends strings.Builder + var control strings.Builder + + s := bufio.NewScanner(io.TeeReader(r, &control)) + for s.Scan() { + line := s.Text() + + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + + if line[0] == ' ' || line[0] == '\t' { + switch key { + case "Description": + p.Metadata.Description += line + case "Depends": + depends.WriteString(trimmed) + } + } else { + parts := strings.SplitN(trimmed, ":", 2) + if len(parts) < 2 { + continue + } + + key = parts[0] + value := strings.TrimSpace(parts[1]) + switch key { + case "Package": + if !namePattern.MatchString(value) { + return nil, ErrInvalidName + } + p.Name = value + case "Version": + if !versionPattern.MatchString(value) { + return nil, ErrInvalidVersion + } + p.Version = value + case "Architecture": + if value == "" { + return nil, ErrInvalidArchitecture + } + p.Architecture = value + case "Maintainer": + a, err := mail.ParseAddress(value) + if err != nil || a.Name == "" { + p.Metadata.Maintainer = value + } else { + p.Metadata.Maintainer = a.Name + } + case "Description": + p.Metadata.Description = value + case "Depends": + depends.WriteString(value) + case "Homepage": + if validation.IsValidURL(value) { + p.Metadata.ProjectURL = value + } + } + } + } + if err := s.Err(); err != nil { + return nil, err + } + + dependencies := strings.Split(depends.String(), ",") + for i := range dependencies { + dependencies[i] = strings.TrimSpace(dependencies[i]) + } + p.Metadata.Dependencies = dependencies + + p.Control = control.String() + + return p, nil +} diff --git a/modules/packages/debian/metadata_test.go b/modules/packages/debian/metadata_test.go new file mode 100644 index 0000000000..69fd51ea79 --- /dev/null +++ b/modules/packages/debian/metadata_test.go @@ -0,0 +1,171 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package debian + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "testing" + + "github.com/blakesmith/ar" + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" +) + +const ( + packageName = "gitea" + packageVersion = "0:1.0.1-te~st" + packageArchitecture = "amd64" + packageAuthor = "KN4CK3R" + description = "Description with multiple lines." + projectURL = "https://gitea.io" +) + +func TestParsePackage(t *testing.T) { + createArchive := func(files map[string][]byte) io.Reader { + var buf bytes.Buffer + aw := ar.NewWriter(&buf) + aw.WriteGlobalHeader() + for filename, content := range files { + hdr := &ar.Header{ + Name: filename, + Mode: 0o600, + Size: int64(len(content)), + } + aw.WriteHeader(hdr) + aw.Write(content) + } + return &buf + } + + t.Run("MissingControlFile", func(t *testing.T) { + data := createArchive(map[string][]byte{"dummy.txt": {}}) + + p, err := ParsePackage(data) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrMissingControlFile) + }) + + t.Run("Compression", func(t *testing.T) { + t.Run("Unsupported", func(t *testing.T) { + data := createArchive(map[string][]byte{"control.tar.foo": {}}) + + p, err := ParsePackage(data) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrUnsupportedCompression) + }) + + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + tw.WriteHeader(&tar.Header{ + Name: "control", + Mode: 0o600, + Size: 50, + }) + tw.Write([]byte("Package: gitea\nVersion: 1.0.0\nArchitecture: amd64\n")) + tw.Close() + + t.Run("None", func(t *testing.T) { + data := createArchive(map[string][]byte{"control.tar": buf.Bytes()}) + + p, err := ParsePackage(data) + assert.NotNil(t, p) + assert.NoError(t, err) + assert.Equal(t, "gitea", p.Name) + }) + + t.Run("gz", func(t *testing.T) { + var zbuf bytes.Buffer + zw := gzip.NewWriter(&zbuf) + zw.Write(buf.Bytes()) + zw.Close() + + data := createArchive(map[string][]byte{"control.tar.gz": zbuf.Bytes()}) + + p, err := ParsePackage(data) + assert.NotNil(t, p) + assert.NoError(t, err) + assert.Equal(t, "gitea", p.Name) + }) + + t.Run("xz", func(t *testing.T) { + var xbuf bytes.Buffer + xw, _ := xz.NewWriter(&xbuf) + xw.Write(buf.Bytes()) + xw.Close() + + data := createArchive(map[string][]byte{"control.tar.xz": xbuf.Bytes()}) + + p, err := ParsePackage(data) + assert.NotNil(t, p) + assert.NoError(t, err) + assert.Equal(t, "gitea", p.Name) + }) + + t.Run("zst", func(t *testing.T) { + var zbuf bytes.Buffer + zw, _ := zstd.NewWriter(&zbuf) + zw.Write(buf.Bytes()) + zw.Close() + + data := createArchive(map[string][]byte{"control.tar.zst": zbuf.Bytes()}) + + p, err := ParsePackage(data) + assert.NotNil(t, p) + assert.NoError(t, err) + assert.Equal(t, "gitea", p.Name) + }) + }) +} + +func TestParseControlFile(t *testing.T) { + buildContent := func(name, version, architecture string) *bytes.Buffer { + var buf bytes.Buffer + buf.WriteString("Package: " + name + "\nVersion: " + version + "\nArchitecture: " + architecture + "\nMaintainer: " + packageAuthor + " <kn4ck3r@gitea.io>\nHomepage: " + projectURL + "\nDepends: a,\n b\nDescription: Description\n with multiple\n lines.") + return &buf + } + + t.Run("InvalidName", func(t *testing.T) { + for _, name := range []string{"", "-cd"} { + p, err := ParseControlFile(buildContent(name, packageVersion, packageArchitecture)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) + } + }) + + t.Run("InvalidVersion", func(t *testing.T) { + for _, version := range []string{"", "1-", ":1.0", "1_0"} { + p, err := ParseControlFile(buildContent(packageName, version, packageArchitecture)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + } + }) + + t.Run("InvalidArchitecture", func(t *testing.T) { + p, err := ParseControlFile(buildContent(packageName, packageVersion, "")) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidArchitecture) + }) + + t.Run("Valid", func(t *testing.T) { + content := buildContent(packageName, packageVersion, packageArchitecture) + full := content.String() + + p, err := ParseControlFile(content) + assert.NoError(t, err) + assert.NotNil(t, p) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, packageArchitecture, p.Architecture) + assert.Equal(t, description, p.Metadata.Description) + assert.Equal(t, projectURL, p.Metadata.ProjectURL) + assert.Equal(t, packageAuthor, p.Metadata.Maintainer) + assert.Equal(t, []string{"a", "b"}, p.Metadata.Dependencies) + assert.Equal(t, full, p.Control) + }) +} diff --git a/modules/packages/hashed_buffer.go b/modules/packages/hashed_buffer.go index ef00a45057..017ddf1c8f 100644 --- a/modules/packages/hashed_buffer.go +++ b/modules/packages/hashed_buffer.go @@ -25,8 +25,15 @@ type HashedBuffer struct { combinedWriter io.Writer } -// NewHashedBuffer creates a hashed buffer with a specific maximum memory size -func NewHashedBuffer(maxMemorySize int) (*HashedBuffer, error) { +const DefaultMemorySize = 32 * 1024 * 1024 + +// NewHashedBuffer creates a hashed buffer with the default memory size +func NewHashedBuffer() (*HashedBuffer, error) { + return NewHashedBufferWithSize(DefaultMemorySize) +} + +// NewHashedBuffer creates a hashed buffer with a specific memory size +func NewHashedBufferWithSize(maxMemorySize int) (*HashedBuffer, error) { b, err := filebuffer.New(maxMemorySize) if err != nil { return nil, err @@ -43,9 +50,14 @@ func NewHashedBuffer(maxMemorySize int) (*HashedBuffer, error) { }, nil } -// CreateHashedBufferFromReader creates a hashed buffer and copies the provided reader data into it. -func CreateHashedBufferFromReader(r io.Reader, maxMemorySize int) (*HashedBuffer, error) { - b, err := NewHashedBuffer(maxMemorySize) +// CreateHashedBufferFromReader creates a hashed buffer with the default memory size and copies the provided reader data into it. +func CreateHashedBufferFromReader(r io.Reader) (*HashedBuffer, error) { + return CreateHashedBufferFromReaderWithSize(r, DefaultMemorySize) +} + +// CreateHashedBufferFromReaderWithSize creates a hashed buffer and copies the provided reader data into it. +func CreateHashedBufferFromReaderWithSize(r io.Reader, maxMemorySize int) (*HashedBuffer, error) { + b, err := NewHashedBufferWithSize(maxMemorySize) if err != nil { return nil, err } diff --git a/modules/packages/hashed_buffer_test.go b/modules/packages/hashed_buffer_test.go index e907aa0605..564e782f18 100644 --- a/modules/packages/hashed_buffer_test.go +++ b/modules/packages/hashed_buffer_test.go @@ -26,7 +26,7 @@ func TestHashedBuffer(t *testing.T) { } for _, c := range cases { - buf, err := CreateHashedBufferFromReader(strings.NewReader(c.Data), c.MaxMemorySize) + buf, err := CreateHashedBufferFromReaderWithSize(strings.NewReader(c.Data), c.MaxMemorySize) assert.NoError(t, err) assert.EqualValues(t, len(c.Data), buf.Size()) diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index b709eac4c1..81bf0371a0 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -63,7 +63,7 @@ func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { return err } - buf, err := packages.CreateHashedBufferFromReader(f, 32*1024*1024) + buf, err := packages.CreateHashedBufferFromReader(f) f.Close() |