code.gitea.io/gitea@v1.19.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  	Autoload    map[string]interface{} `json:"autoload,omitempty"`
    47  	AutoloadDev map[string]interface{} `json:"autoload-dev,omitempty"`
    48  	Extra       map[string]interface{} `json:"extra,omitempty"`
    49  	Require     map[string]string      `json:"require,omitempty"`
    50  	RequireDev  map[string]string      `json:"require-dev,omitempty"`
    51  	Suggest     map[string]string      `json:"suggest,omitempty"`
    52  	Provide     map[string]string      `json:"provide,omitempty"`
    53  }
    54  
    55  // Licenses represents the licenses of a Composer package
    56  type Licenses []string
    57  
    58  // UnmarshalJSON reads from a string or array
    59  func (l *Licenses) UnmarshalJSON(data []byte) error {
    60  	switch data[0] {
    61  	case '"':
    62  		var value string
    63  		if err := json.Unmarshal(data, &value); err != nil {
    64  			return err
    65  		}
    66  		*l = Licenses{value}
    67  	case '[':
    68  		values := make([]string, 0, 5)
    69  		if err := json.Unmarshal(data, &values); err != nil {
    70  			return err
    71  		}
    72  		*l = Licenses(values)
    73  	}
    74  	return nil
    75  }
    76  
    77  // Author represents an author
    78  type Author struct {
    79  	Name     string `json:"name,omitempty"`
    80  	Email    string `json:"email,omitempty"`
    81  	Homepage string `json:"homepage,omitempty"`
    82  }
    83  
    84  var nameMatch = regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`)
    85  
    86  // ParsePackage parses the metadata of a Composer package file
    87  func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
    88  	archive, err := zip.NewReader(r, size)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	for _, file := range archive.File {
    94  		if strings.Count(file.Name, "/") > 1 {
    95  			continue
    96  		}
    97  		if strings.HasSuffix(strings.ToLower(file.Name), "composer.json") {
    98  			f, err := archive.Open(file.Name)
    99  			if err != nil {
   100  				return nil, err
   101  			}
   102  			defer f.Close()
   103  
   104  			return ParseComposerFile(f)
   105  		}
   106  	}
   107  	return nil, ErrMissingComposerFile
   108  }
   109  
   110  // ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package
   111  func ParseComposerFile(r io.Reader) (*Package, error) {
   112  	var cj struct {
   113  		Name    string `json:"name"`
   114  		Version string `json:"version"`
   115  		Type    string `json:"type"`
   116  		Metadata
   117  	}
   118  	if err := json.NewDecoder(r).Decode(&cj); err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	if !nameMatch.MatchString(cj.Name) {
   123  		return nil, ErrInvalidName
   124  	}
   125  
   126  	if cj.Version != "" {
   127  		if _, err := version.NewSemver(cj.Version); err != nil {
   128  			return nil, ErrInvalidVersion
   129  		}
   130  	}
   131  
   132  	if !validation.IsValidURL(cj.Homepage) {
   133  		cj.Homepage = ""
   134  	}
   135  
   136  	if cj.Type == "" {
   137  		cj.Type = "library"
   138  	}
   139  
   140  	return &Package{
   141  		Name:     cj.Name,
   142  		Version:  cj.Version,
   143  		Type:     cj.Type,
   144  		Metadata: &cj.Metadata,
   145  	}, nil
   146  }