mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-27 12:16:10 +01:00
245 lines
5.3 KiB
Go
245 lines
5.3 KiB
Go
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||
|
// SPDX-License-Identifier: MIT
|
||
|
|
||
|
package cran
|
||
|
|
||
|
import (
|
||
|
"archive/tar"
|
||
|
"archive/zip"
|
||
|
"bufio"
|
||
|
"compress/gzip"
|
||
|
"io"
|
||
|
"path"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
|
||
|
"code.gitea.io/gitea/modules/util"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
PropertyType = "cran.type"
|
||
|
PropertyPlatform = "cran.platform"
|
||
|
PropertyRVersion = "cran.rvserion"
|
||
|
|
||
|
TypeSource = "source"
|
||
|
TypeBinary = "binary"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
ErrMissingDescriptionFile = util.NewInvalidArgumentErrorf("DESCRIPTION file is missing")
|
||
|
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||
|
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
fieldPattern = regexp.MustCompile(`\A\S+:`)
|
||
|
namePattern = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9\.]*[a-zA-Z0-9]\z`)
|
||
|
versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+){1,3}\z`)
|
||
|
authorReplacePattern = regexp.MustCompile(`[\[\(].+?[\]\)]`)
|
||
|
)
|
||
|
|
||
|
// Package represents a CRAN package
|
||
|
type Package struct {
|
||
|
Name string
|
||
|
Version string
|
||
|
FileExtension string
|
||
|
Metadata *Metadata
|
||
|
}
|
||
|
|
||
|
// Metadata represents the metadata of a CRAN package
|
||
|
type Metadata struct {
|
||
|
Title string `json:"title,omitempty"`
|
||
|
Description string `json:"description,omitempty"`
|
||
|
ProjectURL []string `json:"project_url,omitempty"`
|
||
|
License string `json:"license,omitempty"`
|
||
|
Authors []string `json:"authors,omitempty"`
|
||
|
Depends []string `json:"depends,omitempty"`
|
||
|
Imports []string `json:"imports,omitempty"`
|
||
|
Suggests []string `json:"suggests,omitempty"`
|
||
|
LinkingTo []string `json:"linking_to,omitempty"`
|
||
|
NeedsCompilation bool `json:"needs_compilation"`
|
||
|
}
|
||
|
|
||
|
type ReaderReaderAt interface {
|
||
|
io.Reader
|
||
|
io.ReaderAt
|
||
|
}
|
||
|
|
||
|
// ParsePackage reads the package metadata from a CRAN package
|
||
|
// .zip and .tar.gz/.tgz files are supported.
|
||
|
func ParsePackage(r ReaderReaderAt, size int64) (*Package, error) {
|
||
|
magicBytes := make([]byte, 2)
|
||
|
if _, err := r.ReadAt(magicBytes, 0); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if magicBytes[0] == 0x1F && magicBytes[1] == 0x8B {
|
||
|
return parsePackageTarGz(r)
|
||
|
}
|
||
|
return parsePackageZip(r, size)
|
||
|
}
|
||
|
|
||
|
func parsePackageTarGz(r io.Reader) (*Package, error) {
|
||
|
gzr, err := gzip.NewReader(r)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer gzr.Close()
|
||
|
|
||
|
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 strings.Count(hd.Name, "/") > 1 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if path.Base(hd.Name) == "DESCRIPTION" {
|
||
|
p, err := ParseDescription(tr)
|
||
|
if p != nil {
|
||
|
p.FileExtension = ".tar.gz"
|
||
|
}
|
||
|
return p, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil, ErrMissingDescriptionFile
|
||
|
}
|
||
|
|
||
|
func parsePackageZip(r io.ReaderAt, size int64) (*Package, error) {
|
||
|
zr, err := zip.NewReader(r, size)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
for _, file := range zr.File {
|
||
|
if strings.Count(file.Name, "/") > 1 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if path.Base(file.Name) == "DESCRIPTION" {
|
||
|
f, err := zr.Open(file.Name)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
p, err := ParseDescription(f)
|
||
|
if p != nil {
|
||
|
p.FileExtension = ".zip"
|
||
|
}
|
||
|
return p, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil, ErrMissingDescriptionFile
|
||
|
}
|
||
|
|
||
|
// ParseDescription parses a DESCRIPTION file to retrieve the metadata of a CRAN package
|
||
|
func ParseDescription(r io.Reader) (*Package, error) {
|
||
|
p := &Package{
|
||
|
Metadata: &Metadata{},
|
||
|
}
|
||
|
|
||
|
scanner := bufio.NewScanner(r)
|
||
|
|
||
|
var b strings.Builder
|
||
|
for scanner.Scan() {
|
||
|
line := strings.TrimSpace(scanner.Text())
|
||
|
if line == "" {
|
||
|
continue
|
||
|
}
|
||
|
if !fieldPattern.MatchString(line) {
|
||
|
b.WriteRune(' ')
|
||
|
b.WriteString(line)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if err := setField(p, b.String()); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
b.Reset()
|
||
|
b.WriteString(line)
|
||
|
}
|
||
|
|
||
|
if err := setField(p, b.String()); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if err := scanner.Err(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return p, nil
|
||
|
}
|
||
|
|
||
|
func setField(p *Package, data string) error {
|
||
|
const listDelimiter = ", "
|
||
|
|
||
|
if data == "" {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
parts := strings.SplitN(data, ":", 2)
|
||
|
if len(parts) != 2 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
name := strings.TrimSpace(parts[0])
|
||
|
value := strings.TrimSpace(parts[1])
|
||
|
|
||
|
switch name {
|
||
|
case "Package":
|
||
|
if !namePattern.MatchString(value) {
|
||
|
return ErrInvalidName
|
||
|
}
|
||
|
p.Name = value
|
||
|
case "Version":
|
||
|
if !versionPattern.MatchString(value) {
|
||
|
return ErrInvalidVersion
|
||
|
}
|
||
|
p.Version = value
|
||
|
case "Title":
|
||
|
p.Metadata.Title = value
|
||
|
case "Description":
|
||
|
p.Metadata.Description = value
|
||
|
case "URL":
|
||
|
p.Metadata.ProjectURL = splitAndTrim(value, listDelimiter)
|
||
|
case "License":
|
||
|
p.Metadata.License = value
|
||
|
case "Author":
|
||
|
p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""), listDelimiter)
|
||
|
case "Depends":
|
||
|
p.Metadata.Depends = splitAndTrim(value, listDelimiter)
|
||
|
case "Imports":
|
||
|
p.Metadata.Imports = splitAndTrim(value, listDelimiter)
|
||
|
case "Suggests":
|
||
|
p.Metadata.Suggests = splitAndTrim(value, listDelimiter)
|
||
|
case "LinkingTo":
|
||
|
p.Metadata.LinkingTo = splitAndTrim(value, listDelimiter)
|
||
|
case "NeedsCompilation":
|
||
|
p.Metadata.NeedsCompilation = value == "yes"
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func splitAndTrim(s, sep string) []string {
|
||
|
items := strings.Split(s, sep)
|
||
|
for i := range items {
|
||
|
items[i] = strings.TrimSpace(items[i])
|
||
|
}
|
||
|
return items
|
||
|
}
|