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 }