mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
243 lines
6.0 KiB
Go
243 lines
6.0 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package conda
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"compress/bzip2"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/util"
|
|
"github.com/hanzoai/git/modules/validation"
|
|
"github.com/hanzoai/git/modules/zstd"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidStructure = util.NewInvalidArgumentErrorf("package structure is invalid")
|
|
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
|
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
|
)
|
|
|
|
const (
|
|
PropertyName = "conda.name"
|
|
PropertyChannel = "conda.channel"
|
|
PropertySubdir = "conda.subdir"
|
|
PropertyMetadata = "conda.metadata"
|
|
)
|
|
|
|
// 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)
|
|
}
|