// Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package conda import ( "archive/tar" "archive/zip" "compress/bzip2" "io" "strings" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" "github.com/klauspost/compress/zstd" ) var ( ErrInvalidStructure = util.SilentWrap{Message: "package structure is invalid", Err: util.ErrInvalidArgument} ErrInvalidName = util.SilentWrap{Message: "package name is invalid", Err: util.ErrInvalidArgument} ErrInvalidVersion = util.SilentWrap{Message: "package version is invalid", Err: util.ErrInvalidArgument} ) const ( PropertyName = "conda.name" PropertyChannel = "conda.channel" PropertySubdir = "conda.subdir" PropertyMetadata = "conda.metdata" ) // Package represents a Conda package type Package struct { Name string Version string Subdir string VersionMetadata *VersionMetadata FileMetadata *FileMetadata } // VersionMetadata represents the metadata of a Conda package type VersionMetadata struct { Description string `json:"description,omitempty"` Summary string `json:"summary,omitempty"` ProjectURL string `json:"project_url,omitempty"` RepositoryURL string `json:"repository_url,omitempty"` DocumentationURL string `json:"documentation_url,omitempty"` License string `json:"license,omitempty"` LicenseFamily string `json:"license_family,omitempty"` } // FileMetadata represents the metadata of a Conda package file type FileMetadata struct { IsCondaPackage bool `json:"is_conda"` Architecture string `json:"architecture,omitempty"` NoArch string `json:"noarch,omitempty"` Build string `json:"build,omitempty"` BuildNumber int64 `json:"build_number,omitempty"` Dependencies []string `json:"dependencies,omitempty"` Platform string `json:"platform,omitempty"` Timestamp int64 `json:"timestamp,omitempty"` } type index struct { Name string `json:"name"` Version string `json:"version"` Architecture string `json:"arch"` NoArch string `json:"noarch"` Build string `json:"build"` BuildNumber int64 `json:"build_number"` Dependencies []string `json:"depends"` License string `json:"license"` LicenseFamily string `json:"license_family"` Platform string `json:"platform"` Subdir string `json:"subdir"` Timestamp int64 `json:"timestamp"` } type about struct { Description string `json:"description"` Summary string `json:"summary"` ProjectURL string `json:"home"` RepositoryURL string `json:"dev_url"` DocumentationURL string `json:"doc_url"` } type ReaderAndReaderAt interface { io.Reader io.ReaderAt } // ParsePackageBZ2 parses the Conda package file compressed with bzip2 func ParsePackageBZ2(r io.Reader) (*Package, error) { gzr := bzip2.NewReader(r) return parsePackageTar(gzr) } // ParsePackageConda parses the Conda package file compressed with zip and zstd func ParsePackageConda(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.HasPrefix(file.Name, "info-") && strings.HasSuffix(file.Name, ".tar.zst") { f, err := zr.Open(file.Name) if err != nil { return nil, err } defer f.Close() dec, err := zstd.NewReader(f) if err != nil { return nil, err } defer dec.Close() p, err := parsePackageTar(dec) if p != nil { p.FileMetadata.IsCondaPackage = true } return p, err } } return nil, ErrInvalidStructure } func parsePackageTar(r io.Reader) (*Package, error) { var i *index var a *about tr := tar.NewReader(r) for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { return nil, err } if hdr.Typeflag != tar.TypeReg { continue } if hdr.Name == "info/index.json" { if err := json.NewDecoder(tr).Decode(&i); err != nil { return nil, err } if !checkName(i.Name) { return nil, ErrInvalidName } if !checkVersion(i.Version) { return nil, ErrInvalidVersion } if a != nil { break // stop loop if both files were found } } else if hdr.Name == "info/about.json" { if err := json.NewDecoder(tr).Decode(&a); err != nil { return nil, err } if !validation.IsValidURL(a.ProjectURL) { a.ProjectURL = "" } if !validation.IsValidURL(a.RepositoryURL) { a.RepositoryURL = "" } if !validation.IsValidURL(a.DocumentationURL) { a.DocumentationURL = "" } if i != nil { break // stop loop if both files were found } } } if i == nil { return nil, ErrInvalidStructure } if a == nil { a = &about{} } return &Package{ Name: i.Name, Version: i.Version, Subdir: i.Subdir, VersionMetadata: &VersionMetadata{ License: i.License, LicenseFamily: i.LicenseFamily, Description: a.Description, Summary: a.Summary, ProjectURL: a.ProjectURL, RepositoryURL: a.RepositoryURL, DocumentationURL: a.DocumentationURL, }, FileMetadata: &FileMetadata{ Architecture: i.Architecture, NoArch: i.NoArch, Build: i.Build, BuildNumber: i.BuildNumber, Dependencies: i.Dependencies, Platform: i.Platform, Timestamp: i.Timestamp, }, }, nil } // https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1393 func checkName(name string) bool { if name == "" { return false } if name != strings.ToLower(name) { return false } return !checkBadCharacters(name, "!") } // https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1403 func checkVersion(version string) bool { if version == "" { return false } return !checkBadCharacters(version, "-") } func checkBadCharacters(s, additional string) bool { if strings.ContainsAny(s, "=@#$%^&*:;\"'\\|<>?/ ") { return true } return strings.ContainsAny(s, additional) }