github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/packages/composer/metadata.go (about)

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