code.gitea.io/gitea@v1.22.3/modules/packages/composer/metadata.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package composer
     5  
     6  import (
     7  	"archive/zip"
     8  	"io"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/modules/json"
    13  	"code.gitea.io/gitea/modules/util"
    14  	"code.gitea.io/gitea/modules/validation"
    15  
    16  	"github.com/hashicorp/go-version"
    17  )
    18  
    19  // TypeProperty is the name of the property for Composer package types
    20  const TypeProperty = "composer.type"
    21  
    22  var (
    23  	// ErrMissingComposerFile indicates a missing composer.json file
    24  	ErrMissingComposerFile = util.NewInvalidArgumentErrorf("composer.json file is missing")
    25  	// ErrInvalidName indicates an invalid package name
    26  	ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
    27  	// ErrInvalidVersion indicates an invalid package version
    28  	ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
    29  )
    30  
    31  // Package represents a Composer package
    32  type Package struct {
    33  	Name     string
    34  	Version  string
    35  	Type     string
    36  	Metadata *Metadata
    37  }
    38  
    39  // Metadata represents the metadata of a Composer package
    40  type Metadata struct {
    41  	Description string            `json:"description,omitempty"`
    42  	Keywords    []string          `json:"keywords,omitempty"`
    43  	Homepage    string            `json:"homepage,omitempty"`
    44  	License     Licenses          `json:"license,omitempty"`
    45  	Authors     []Author          `json:"authors,omitempty"`
    46  	Bin         []string          `json:"bin,omitempty"`
    47  	Autoload    map[string]any    `json:"autoload,omitempty"`
    48  	AutoloadDev map[string]any    `json:"autoload-dev,omitempty"`
    49  	Extra       map[string]any    `json:"extra,omitempty"`
    50  	Require     map[string]string `json:"require,omitempty"`
    51  	RequireDev  map[string]string `json:"require-dev,omitempty"`
    52  	Suggest     map[string]string `json:"suggest,omitempty"`
    53  	Provide     map[string]string `json:"provide,omitempty"`
    54  }
    55  
    56  // Licenses represents the licenses of a Composer package
    57  type Licenses []string
    58  
    59  // UnmarshalJSON reads from a string or array
    60  func (l *Licenses) UnmarshalJSON(data []byte) error {
    61  	switch data[0] {
    62  	case '"':
    63  		var value string
    64  		if err := json.Unmarshal(data, &value); err != nil {
    65  			return err
    66  		}
    67  		*l = Licenses{value}
    68  	case '[':
    69  		values := make([]string, 0, 5)
    70  		if err := json.Unmarshal(data, &values); err != nil {
    71  			return err
    72  		}
    73  		*l = Licenses(values)
    74  	}
    75  	return nil
    76  }
    77  
    78  // Author represents an author
    79  type Author struct {
    80  	Name     string `json:"name,omitempty"`
    81  	Email    string `json:"email,omitempty"`
    82  	Homepage string `json:"homepage,omitempty"`
    83  }
    84  
    85  var nameMatch = regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`)
    86  
    87  // ParsePackage parses the metadata of a Composer package file
    88  func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
    89  	archive, err := zip.NewReader(r, size)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	for _, file := range archive.File {
    95  		if strings.Count(file.Name, "/") > 1 {
    96  			continue
    97  		}
    98  		if strings.HasSuffix(strings.ToLower(file.Name), "composer.json") {
    99  			f, err := archive.Open(file.Name)
   100  			if err != nil {
   101  				return nil, err
   102  			}
   103  			defer f.Close()
   104  
   105  			return ParseComposerFile(f)
   106  		}
   107  	}
   108  	return nil, ErrMissingComposerFile
   109  }
   110  
   111  // ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package
   112  func ParseComposerFile(r io.Reader) (*Package, error) {
   113  	var cj struct {
   114  		Name    string `json:"name"`
   115  		Version string `json:"version"`
   116  		Type    string `json:"type"`
   117  		Metadata
   118  	}
   119  	if err := json.NewDecoder(r).Decode(&cj); err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	if !nameMatch.MatchString(cj.Name) {
   124  		return nil, ErrInvalidName
   125  	}
   126  
   127  	if cj.Version != "" {
   128  		if _, err := version.NewSemver(cj.Version); err != nil {
   129  			return nil, ErrInvalidVersion
   130  		}
   131  	}
   132  
   133  	if !validation.IsValidURL(cj.Homepage) {
   134  		cj.Homepage = ""
   135  	}
   136  
   137  	if cj.Type == "" {
   138  		cj.Type = "library"
   139  	}
   140  
   141  	return &Package{
   142  		Name:     cj.Name,
   143  		Version:  cj.Version,
   144  		Type:     cj.Type,
   145  		Metadata: &cj.Metadata,
   146  	}, nil
   147  }