mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-26 20:11:02 +00:00 
			
		
		
		
	- Massive replacement of changing `code.gitea.io/gitea` to `forgejo.org`. - Resolves forgejo/discussions#258 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7337 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Reviewed-by: Michael Kriese <michael.kriese@gmx.de> Reviewed-by: Beowulf <beowulf@beocode.eu> Reviewed-by: Panagiotis "Ivory" Vasilopoulos <git@n0toose.net> Co-authored-by: Gusted <postmaster@gusted.xyz> Co-committed-by: Gusted <postmaster@gusted.xyz>
		
			
				
	
	
		
			242 lines
		
	
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			242 lines
		
	
	
	
		
			5.2 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"
 | |
| 
 | |
| 	"forgejo.org/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 {
 | |
| 	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)
 | |
| 	case "License":
 | |
| 		p.Metadata.License = value
 | |
| 	case "Author":
 | |
| 		p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""))
 | |
| 	case "Depends":
 | |
| 		p.Metadata.Depends = splitAndTrim(value)
 | |
| 	case "Imports":
 | |
| 		p.Metadata.Imports = splitAndTrim(value)
 | |
| 	case "Suggests":
 | |
| 		p.Metadata.Suggests = splitAndTrim(value)
 | |
| 	case "LinkingTo":
 | |
| 		p.Metadata.LinkingTo = splitAndTrim(value)
 | |
| 	case "NeedsCompilation":
 | |
| 		p.Metadata.NeedsCompilation = value == "yes"
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func splitAndTrim(s string) []string {
 | |
| 	items := strings.Split(s, ", ")
 | |
| 	for i := range items {
 | |
| 		items[i] = strings.TrimSpace(items[i])
 | |
| 	}
 | |
| 	return items
 | |
| }
 |