github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/packages/npm/creator.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 npm
     7  
     8  import (
     9  	"bytes"
    10  	"crypto/sha1"
    11  	"crypto/sha512"
    12  	"encoding/base64"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"regexp"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/gitbundle/modules/json"
    21  	"github.com/gitbundle/modules/validation"
    22  
    23  	"github.com/hashicorp/go-version"
    24  )
    25  
    26  var (
    27  	// ErrInvalidPackage indicates an invalid package
    28  	ErrInvalidPackage = errors.New("The package is invalid")
    29  	// ErrInvalidPackageName indicates an invalid name
    30  	ErrInvalidPackageName = errors.New("The package name is invalid")
    31  	// ErrInvalidPackageVersion indicates an invalid version
    32  	ErrInvalidPackageVersion = errors.New("The package version is invalid")
    33  	// ErrInvalidAttachment indicates a invalid attachment
    34  	ErrInvalidAttachment = errors.New("The package attachment is invalid")
    35  	// ErrInvalidIntegrity indicates an integrity validation error
    36  	ErrInvalidIntegrity = errors.New("Failed to validate integrity")
    37  )
    38  
    39  var nameMatch = regexp.MustCompile(`\A((@[^\s\/~'!\(\)\*]+?)[\/])?([^_.][^\s\/~'!\(\)\*]+)\z`)
    40  
    41  // Package represents a npm package
    42  type Package struct {
    43  	Name     string
    44  	Version  string
    45  	DistTags []string
    46  	Metadata Metadata
    47  	Filename string
    48  	Data     []byte
    49  }
    50  
    51  // PackageMetadata https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
    52  type PackageMetadata struct {
    53  	ID             string                             `json:"_id"`
    54  	Name           string                             `json:"name"`
    55  	Description    string                             `json:"description"`
    56  	DistTags       map[string]string                  `json:"dist-tags,omitempty"`
    57  	Versions       map[string]*PackageMetadataVersion `json:"versions"`
    58  	Readme         string                             `json:"readme,omitempty"`
    59  	Maintainers    []User                             `json:"maintainers,omitempty"`
    60  	Time           map[string]time.Time               `json:"time,omitempty"`
    61  	Homepage       string                             `json:"homepage,omitempty"`
    62  	Keywords       []string                           `json:"keywords,omitempty"`
    63  	Repository     Repository                         `json:"repository,omitempty"`
    64  	Author         User                               `json:"author"`
    65  	ReadmeFilename string                             `json:"readmeFilename,omitempty"`
    66  	Users          map[string]bool                    `json:"users,omitempty"`
    67  	License        string                             `json:"license,omitempty"`
    68  }
    69  
    70  // PackageMetadataVersion https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
    71  type PackageMetadataVersion struct {
    72  	ID                   string              `json:"_id"`
    73  	Name                 string              `json:"name"`
    74  	Version              string              `json:"version"`
    75  	Description          string              `json:"description"`
    76  	Author               User                `json:"author"`
    77  	Homepage             string              `json:"homepage,omitempty"`
    78  	License              string              `json:"license,omitempty"`
    79  	Repository           Repository          `json:"repository,omitempty"`
    80  	Keywords             []string            `json:"keywords,omitempty"`
    81  	Dependencies         map[string]string   `json:"dependencies,omitempty"`
    82  	DevDependencies      map[string]string   `json:"devDependencies,omitempty"`
    83  	PeerDependencies     map[string]string   `json:"peerDependencies,omitempty"`
    84  	OptionalDependencies map[string]string   `json:"optionalDependencies,omitempty"`
    85  	Readme               string              `json:"readme,omitempty"`
    86  	Dist                 PackageDistribution `json:"dist"`
    87  	Maintainers          []User              `json:"maintainers,omitempty"`
    88  }
    89  
    90  // PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
    91  type PackageDistribution struct {
    92  	Integrity    string `json:"integrity"`
    93  	Shasum       string `json:"shasum"`
    94  	Tarball      string `json:"tarball"`
    95  	FileCount    int    `json:"fileCount,omitempty"`
    96  	UnpackedSize int    `json:"unpackedSize,omitempty"`
    97  	NpmSignature string `json:"npm-signature,omitempty"`
    98  }
    99  
   100  // User https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
   101  type User struct {
   102  	Username string `json:"username,omitempty"`
   103  	Name     string `json:"name"`
   104  	Email    string `json:"email,omitempty"`
   105  	URL      string `json:"url,omitempty"`
   106  }
   107  
   108  // UnmarshalJSON is needed because User objects can be strings or objects
   109  func (u *User) UnmarshalJSON(data []byte) error {
   110  	switch data[0] {
   111  	case '"':
   112  		if err := json.Unmarshal(data, &u.Name); err != nil {
   113  			return err
   114  		}
   115  	case '{':
   116  		var tmp struct {
   117  			Username string `json:"username"`
   118  			Name     string `json:"name"`
   119  			Email    string `json:"email"`
   120  			URL      string `json:"url"`
   121  		}
   122  		if err := json.Unmarshal(data, &tmp); err != nil {
   123  			return err
   124  		}
   125  		u.Username = tmp.Username
   126  		u.Name = tmp.Name
   127  		u.Email = tmp.Email
   128  		u.URL = tmp.URL
   129  	}
   130  	return nil
   131  }
   132  
   133  // Repository https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
   134  type Repository struct {
   135  	Type string `json:"type"`
   136  	URL  string `json:"url"`
   137  }
   138  
   139  // PackageAttachment https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
   140  type PackageAttachment struct {
   141  	ContentType string `json:"content_type"`
   142  	Data        string `json:"data"`
   143  	Length      int    `json:"length"`
   144  }
   145  
   146  type packageUpload struct {
   147  	PackageMetadata
   148  	Attachments map[string]*PackageAttachment `json:"_attachments"`
   149  }
   150  
   151  // ParsePackage parses the content into a npm package
   152  func ParsePackage(r io.Reader) (*Package, error) {
   153  	var upload packageUpload
   154  	if err := json.NewDecoder(r).Decode(&upload); err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	for _, meta := range upload.Versions {
   159  		if !validateName(meta.Name) {
   160  			return nil, ErrInvalidPackageName
   161  		}
   162  
   163  		v, err := version.NewSemver(meta.Version)
   164  		if err != nil {
   165  			return nil, ErrInvalidPackageVersion
   166  		}
   167  
   168  		scope := ""
   169  		name := meta.Name
   170  		nameParts := strings.SplitN(meta.Name, "/", 2)
   171  		if len(nameParts) == 2 {
   172  			scope = nameParts[0]
   173  			name = nameParts[1]
   174  		}
   175  
   176  		if !validation.IsValidURL(meta.Homepage) {
   177  			meta.Homepage = ""
   178  		}
   179  
   180  		p := &Package{
   181  			Name:     meta.Name,
   182  			Version:  v.String(),
   183  			DistTags: make([]string, 0, 1),
   184  			Metadata: Metadata{
   185  				Scope:                   scope,
   186  				Name:                    name,
   187  				Description:             meta.Description,
   188  				Author:                  meta.Author.Name,
   189  				License:                 meta.License,
   190  				ProjectURL:              meta.Homepage,
   191  				Keywords:                meta.Keywords,
   192  				Dependencies:            meta.Dependencies,
   193  				DevelopmentDependencies: meta.DevDependencies,
   194  				PeerDependencies:        meta.PeerDependencies,
   195  				OptionalDependencies:    meta.OptionalDependencies,
   196  				Readme:                  meta.Readme,
   197  			},
   198  		}
   199  
   200  		for tag := range upload.DistTags {
   201  			p.DistTags = append(p.DistTags, tag)
   202  		}
   203  
   204  		p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version))
   205  
   206  		attachment := func() *PackageAttachment {
   207  			for _, a := range upload.Attachments {
   208  				return a
   209  			}
   210  			return nil
   211  		}()
   212  		if attachment == nil || len(attachment.Data) == 0 {
   213  			return nil, ErrInvalidAttachment
   214  		}
   215  
   216  		data, err := base64.StdEncoding.DecodeString(attachment.Data)
   217  		if err != nil {
   218  			return nil, ErrInvalidAttachment
   219  		}
   220  		p.Data = data
   221  
   222  		integrity := strings.SplitN(meta.Dist.Integrity, "-", 2)
   223  		if len(integrity) != 2 {
   224  			return nil, ErrInvalidIntegrity
   225  		}
   226  		integrityHash, err := base64.StdEncoding.DecodeString(integrity[1])
   227  		if err != nil {
   228  			return nil, ErrInvalidIntegrity
   229  		}
   230  		var hash []byte
   231  		switch integrity[0] {
   232  		case "sha1":
   233  			tmp := sha1.Sum(data)
   234  			hash = tmp[:]
   235  		case "sha512":
   236  			tmp := sha512.Sum512(data)
   237  			hash = tmp[:]
   238  		}
   239  		if !bytes.Equal(integrityHash, hash) {
   240  			return nil, ErrInvalidIntegrity
   241  		}
   242  
   243  		return p, nil
   244  	}
   245  
   246  	return nil, ErrInvalidPackage
   247  }
   248  
   249  func validateName(name string) bool {
   250  	if strings.TrimSpace(name) != name {
   251  		return false
   252  	}
   253  	if len(name) == 0 || len(name) > 214 {
   254  		return false
   255  	}
   256  	return nameMatch.MatchString(name)
   257  }