code.gitea.io/gitea@v1.19.3/modules/packages/conda/metadata.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package conda
     5  
     6  import (
     7  	"archive/tar"
     8  	"archive/zip"
     9  	"compress/bzip2"
    10  	"io"
    11  	"strings"
    12  
    13  	"code.gitea.io/gitea/modules/json"
    14  	"code.gitea.io/gitea/modules/util"
    15  	"code.gitea.io/gitea/modules/validation"
    16  
    17  	"github.com/klauspost/compress/zstd"
    18  )
    19  
    20  var (
    21  	ErrInvalidStructure = util.SilentWrap{Message: "package structure is invalid", Err: util.ErrInvalidArgument}
    22  	ErrInvalidName      = util.SilentWrap{Message: "package name is invalid", Err: util.ErrInvalidArgument}
    23  	ErrInvalidVersion   = util.SilentWrap{Message: "package version is invalid", Err: util.ErrInvalidArgument}
    24  )
    25  
    26  const (
    27  	PropertyName     = "conda.name"
    28  	PropertyChannel  = "conda.channel"
    29  	PropertySubdir   = "conda.subdir"
    30  	PropertyMetadata = "conda.metdata"
    31  )
    32  
    33  // Package represents a Conda package
    34  type Package struct {
    35  	Name            string
    36  	Version         string
    37  	Subdir          string
    38  	VersionMetadata *VersionMetadata
    39  	FileMetadata    *FileMetadata
    40  }
    41  
    42  // VersionMetadata represents the metadata of a Conda package
    43  type VersionMetadata struct {
    44  	Description      string `json:"description,omitempty"`
    45  	Summary          string `json:"summary,omitempty"`
    46  	ProjectURL       string `json:"project_url,omitempty"`
    47  	RepositoryURL    string `json:"repository_url,omitempty"`
    48  	DocumentationURL string `json:"documentation_url,omitempty"`
    49  	License          string `json:"license,omitempty"`
    50  	LicenseFamily    string `json:"license_family,omitempty"`
    51  }
    52  
    53  // FileMetadata represents the metadata of a Conda package file
    54  type FileMetadata struct {
    55  	IsCondaPackage bool     `json:"is_conda"`
    56  	Architecture   string   `json:"architecture,omitempty"`
    57  	NoArch         string   `json:"noarch,omitempty"`
    58  	Build          string   `json:"build,omitempty"`
    59  	BuildNumber    int64    `json:"build_number,omitempty"`
    60  	Dependencies   []string `json:"dependencies,omitempty"`
    61  	Platform       string   `json:"platform,omitempty"`
    62  	Timestamp      int64    `json:"timestamp,omitempty"`
    63  }
    64  
    65  type index struct {
    66  	Name          string   `json:"name"`
    67  	Version       string   `json:"version"`
    68  	Architecture  string   `json:"arch"`
    69  	NoArch        string   `json:"noarch"`
    70  	Build         string   `json:"build"`
    71  	BuildNumber   int64    `json:"build_number"`
    72  	Dependencies  []string `json:"depends"`
    73  	License       string   `json:"license"`
    74  	LicenseFamily string   `json:"license_family"`
    75  	Platform      string   `json:"platform"`
    76  	Subdir        string   `json:"subdir"`
    77  	Timestamp     int64    `json:"timestamp"`
    78  }
    79  
    80  type about struct {
    81  	Description      string `json:"description"`
    82  	Summary          string `json:"summary"`
    83  	ProjectURL       string `json:"home"`
    84  	RepositoryURL    string `json:"dev_url"`
    85  	DocumentationURL string `json:"doc_url"`
    86  }
    87  
    88  type ReaderAndReaderAt interface {
    89  	io.Reader
    90  	io.ReaderAt
    91  }
    92  
    93  // ParsePackageBZ2 parses the Conda package file compressed with bzip2
    94  func ParsePackageBZ2(r io.Reader) (*Package, error) {
    95  	gzr := bzip2.NewReader(r)
    96  
    97  	return parsePackageTar(gzr)
    98  }
    99  
   100  // ParsePackageConda parses the Conda package file compressed with zip and zstd
   101  func ParsePackageConda(r io.ReaderAt, size int64) (*Package, error) {
   102  	zr, err := zip.NewReader(r, size)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	for _, file := range zr.File {
   108  		if strings.HasPrefix(file.Name, "info-") && strings.HasSuffix(file.Name, ".tar.zst") {
   109  			f, err := zr.Open(file.Name)
   110  			if err != nil {
   111  				return nil, err
   112  			}
   113  			defer f.Close()
   114  
   115  			dec, err := zstd.NewReader(f)
   116  			if err != nil {
   117  				return nil, err
   118  			}
   119  			defer dec.Close()
   120  
   121  			p, err := parsePackageTar(dec)
   122  			if p != nil {
   123  				p.FileMetadata.IsCondaPackage = true
   124  			}
   125  			return p, err
   126  		}
   127  	}
   128  
   129  	return nil, ErrInvalidStructure
   130  }
   131  
   132  func parsePackageTar(r io.Reader) (*Package, error) {
   133  	var i *index
   134  	var a *about
   135  
   136  	tr := tar.NewReader(r)
   137  	for {
   138  		hdr, err := tr.Next()
   139  		if err == io.EOF {
   140  			break
   141  		}
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  
   146  		if hdr.Typeflag != tar.TypeReg {
   147  			continue
   148  		}
   149  
   150  		if hdr.Name == "info/index.json" {
   151  			if err := json.NewDecoder(tr).Decode(&i); err != nil {
   152  				return nil, err
   153  			}
   154  
   155  			if !checkName(i.Name) {
   156  				return nil, ErrInvalidName
   157  			}
   158  
   159  			if !checkVersion(i.Version) {
   160  				return nil, ErrInvalidVersion
   161  			}
   162  
   163  			if a != nil {
   164  				break // stop loop if both files were found
   165  			}
   166  		} else if hdr.Name == "info/about.json" {
   167  			if err := json.NewDecoder(tr).Decode(&a); err != nil {
   168  				return nil, err
   169  			}
   170  
   171  			if !validation.IsValidURL(a.ProjectURL) {
   172  				a.ProjectURL = ""
   173  			}
   174  			if !validation.IsValidURL(a.RepositoryURL) {
   175  				a.RepositoryURL = ""
   176  			}
   177  			if !validation.IsValidURL(a.DocumentationURL) {
   178  				a.DocumentationURL = ""
   179  			}
   180  
   181  			if i != nil {
   182  				break // stop loop if both files were found
   183  			}
   184  		}
   185  	}
   186  
   187  	if i == nil {
   188  		return nil, ErrInvalidStructure
   189  	}
   190  	if a == nil {
   191  		a = &about{}
   192  	}
   193  
   194  	return &Package{
   195  		Name:    i.Name,
   196  		Version: i.Version,
   197  		Subdir:  i.Subdir,
   198  		VersionMetadata: &VersionMetadata{
   199  			License:          i.License,
   200  			LicenseFamily:    i.LicenseFamily,
   201  			Description:      a.Description,
   202  			Summary:          a.Summary,
   203  			ProjectURL:       a.ProjectURL,
   204  			RepositoryURL:    a.RepositoryURL,
   205  			DocumentationURL: a.DocumentationURL,
   206  		},
   207  		FileMetadata: &FileMetadata{
   208  			Architecture: i.Architecture,
   209  			NoArch:       i.NoArch,
   210  			Build:        i.Build,
   211  			BuildNumber:  i.BuildNumber,
   212  			Dependencies: i.Dependencies,
   213  			Platform:     i.Platform,
   214  			Timestamp:    i.Timestamp,
   215  		},
   216  	}, nil
   217  }
   218  
   219  // https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1393
   220  func checkName(name string) bool {
   221  	if name == "" {
   222  		return false
   223  	}
   224  	if name != strings.ToLower(name) {
   225  		return false
   226  	}
   227  	return !checkBadCharacters(name, "!")
   228  }
   229  
   230  // https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1403
   231  func checkVersion(version string) bool {
   232  	if version == "" {
   233  		return false
   234  	}
   235  	return !checkBadCharacters(version, "-")
   236  }
   237  
   238  func checkBadCharacters(s, additional string) bool {
   239  	if strings.ContainsAny(s, "=@#$%^&*:;\"'\\|<>?/ ") {
   240  		return true
   241  	}
   242  	return strings.ContainsAny(s, additional)
   243  }