mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-05-14 05:52:43 +00:00
Add CRAN package registry (#22331)
This PR adds a [CRAN](https://cran.r-project.org/) package registry. 
This commit is contained in:
parent
ec2a01d1e2
commit
cdb088cec2
23 changed files with 1212 additions and 2 deletions
244
modules/packages/cran/metadata.go
Normal file
244
modules/packages/cran/metadata.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
// 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue