github.com/google/osv-scalibr@v0.4.1/purl/purl.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package purl provides functions to code and decode package url according to the spec: https://github.com/package-url/purl-spec
    16  // This package is a convenience wrapper and abstraction layer around an existing open source implementation.
    17  package purl
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/package-url/packageurl-go"
    24  )
    25  
    26  // These are the known purl types as defined in the spec. Some of these require
    27  // special treatment during parsing.
    28  // https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst
    29  const (
    30  	// TypeAlpm is a pkg:alpm purl.
    31  	TypeAlpm = "alpm"
    32  	// TypeApk is a pkg:apk purl.
    33  	TypeApk = "apk"
    34  	// TypeBitbucket is a pkg:bitbucket purl.
    35  	TypeBitbucket = "bitbucket"
    36  	// TypeBrew is a pkg:brew purl.
    37  	TypeBrew = "brew"
    38  	// TypeCocoapods is a pkg:cocoapods purl.
    39  	TypeCocoapods = "cocoapods"
    40  	// TypeCargo is a pkg:cargo purl.
    41  	TypeCargo = "cargo"
    42  	// TypeComposer is a pkg:composer purl.
    43  	TypeComposer = "composer"
    44  	// TypeConan is a pkg:conan purl.
    45  	TypeConan = "conan"
    46  	// TypeConda is a pkg:conda purl.
    47  	TypeConda = "conda"
    48  	// TypeCOS is the pkg:cos purl
    49  	TypeCOS = "cos"
    50  	// TypeCran is a pkg:cran purl.
    51  	TypeCran = "cran"
    52  	// TypeDebian is a pkg:deb purl.
    53  	TypeDebian = "deb"
    54  	// TypeDocker is a pkg:docker purl.
    55  	TypeDocker = "docker"
    56  	// TypeK8s is a pkg:k8s purl.
    57  	TypeK8s = "k8s"
    58  	// TypeFlatpak is a pkg:flatpak purl.
    59  	TypeFlatpak = "flatpak"
    60  	// TypeGem is a pkg:gem purl.
    61  	TypeGem = "gem"
    62  	// TypeGeneric is a pkg:generic purl.
    63  	TypeGeneric = "generic"
    64  	// TypeGithub is a pkg:github purl.
    65  	TypeGithub = "github"
    66  	// TypeGolang is a pkg:golang purl.
    67  	TypeGolang = "golang"
    68  	// TypeHackage is a pkg:hackage purl.
    69  	TypeHackage = "hackage"
    70  	// TypeHaskell is a pkg:haskell purl.
    71  	TypeHaskell = "haskell"
    72  	// TypeMacApps is a pkg:macapps purl.
    73  	TypeMacApps = "macapps"
    74  	// TypeHex is a pkg:hex purl.
    75  	TypeHex = "hex"
    76  	// TypeMaven is a pkg:maven purl.
    77  	TypeMaven = "maven"
    78  	// TypeNix is a pkg:nix purl.
    79  	TypeNix = "nix"
    80  	// TypeNPM is a pkg:npm purl.
    81  	TypeNPM = "npm"
    82  	// TypePacman is a pkg:pacman purl.
    83  	TypePacman = "pacman"
    84  	// TypeNuget is a pkg:nuget purl.
    85  	TypeNuget = "nuget"
    86  	// TypeOCI is a pkg:oci purl
    87  	TypeOCI = "oci"
    88  	// TypeOpkg is a pkg:opkg purl.
    89  	TypeOpkg = "opkg"
    90  	// TypePub is a pkg:pub purl.
    91  	TypePub = "pub"
    92  	// TypePortage is a pkg:portage purl.
    93  	TypePortage = "portage"
    94  	// TypePyPi is a pkg:pypi purl.
    95  	TypePyPi = "pypi"
    96  	// TypeRPM is a pkg:rpm purl.
    97  	TypeRPM = "rpm"
    98  	// TypeSnap is a pkg:snap purl.
    99  	TypeSnap = "snap"
   100  	// TypeSwift is pkg:swift purl
   101  	TypeSwift = "swift"
   102  	// TypeGooget is pkg:googet purl
   103  	TypeGooget = "googet"
   104  	// TypeWordpress is pkg:wordpress purl
   105  	TypeWordpress = "wordpress"
   106  	// TypeAsdf is pkg:asdf purl
   107  	TypeAsdf = "asdf"
   108  	// TypeMacports is pkg:macports purl
   109  	TypeMacports = "macports"
   110  	// TypeWinget is pkg:winget purl
   111  	TypeWinget = "winget"
   112  	// TypeNim is pkg:nim purl
   113  	TypeNim = "nim"
   114  	// TypeLua is pkg:lua purl
   115  	TypeLua = "lua"
   116  )
   117  
   118  // PackageURL is the struct representation of the parts that make a package url.
   119  type PackageURL struct {
   120  	Type       string
   121  	Namespace  string
   122  	Name       string
   123  	Version    string
   124  	Qualifiers Qualifiers
   125  	Subpath    string
   126  }
   127  
   128  // Qualifier represents a single key=value qualifier in the package url.
   129  type Qualifier packageurl.Qualifier
   130  
   131  // Qualifiers is a slice of key=value pairs, with order preserved as it appears
   132  // in the package URL.
   133  type Qualifiers packageurl.Qualifiers
   134  
   135  // QualifiersFromMap constructs a Qualifiers slice from a string map. To get a
   136  // deterministic qualifier order (despite maps not providing any iteration order
   137  // guarantees) the returned Qualifiers are sorted in increasing order of key.
   138  func QualifiersFromMap(mm map[string]string) Qualifiers {
   139  	for key, value := range mm {
   140  		// Empty value strings are invalid qualifiers according to the purl spec
   141  		// so we filter them out.
   142  		if value == "" {
   143  			delete(mm, key)
   144  		}
   145  	}
   146  	return Qualifiers(packageurl.QualifiersFromMap(mm))
   147  }
   148  
   149  func (p PackageURL) String() string {
   150  	purl := packageurl.PackageURL{
   151  		Type:       p.Type,
   152  		Namespace:  p.Namespace,
   153  		Name:       p.Name,
   154  		Version:    p.Version,
   155  		Qualifiers: packageurl.Qualifiers(p.Qualifiers),
   156  		Subpath:    p.Subpath,
   157  	}
   158  	return (&purl).String()
   159  }
   160  
   161  // FromString parses a valid package url string into a PackageURL structure.
   162  func FromString(purl string) (PackageURL, error) {
   163  	p, err := packageurl.FromString(purl)
   164  	if err != nil {
   165  		return PackageURL{}, fmt.Errorf("failed to decode PURL string %q: %w", purl, err)
   166  	}
   167  	if !validType(p.Type) {
   168  		return PackageURL{}, fmt.Errorf("invalid PURL type %q", p.Type)
   169  	}
   170  	return PackageURL{
   171  		Type:       p.Type,
   172  		Namespace:  p.Namespace,
   173  		Name:       p.Name,
   174  		Version:    p.Version,
   175  		Qualifiers: Qualifiers(p.Qualifiers),
   176  		Subpath:    p.Subpath,
   177  	}, nil
   178  }
   179  
   180  func validType(t string) bool {
   181  	types := map[string]bool{
   182  		TypeAlpm:      true,
   183  		TypeApk:       true,
   184  		TypeBitbucket: true,
   185  		TypeBrew:      true,
   186  		TypeCargo:     true,
   187  		TypeCocoapods: true,
   188  		TypeComposer:  true,
   189  		TypeConan:     true,
   190  		TypeConda:     true,
   191  		TypeCOS:       true,
   192  		TypeCran:      true,
   193  		TypeDebian:    true,
   194  		TypePacman:    true,
   195  		TypeDocker:    true,
   196  		TypeFlatpak:   true,
   197  		TypeGem:       true,
   198  		TypeGeneric:   true,
   199  		TypeGithub:    true,
   200  		TypeGolang:    true,
   201  		TypeHackage:   true,
   202  		TypeHaskell:   true,
   203  		TypeNim:       true,
   204  		TypeLua:       true,
   205  		TypeHex:       true,
   206  		TypeMacApps:   true,
   207  		TypeMaven:     true,
   208  		TypeNix:       true,
   209  		TypeNPM:       true,
   210  		TypeNuget:     true,
   211  		TypeOCI:       true,
   212  		TypeOpkg:      true,
   213  		TypePub:       true,
   214  		TypePortage:   true,
   215  		TypePyPi:      true,
   216  		TypeRPM:       true,
   217  		TypeSwift:     true,
   218  		TypeGooget:    true,
   219  		TypeWordpress: true,
   220  		TypeAsdf:      true,
   221  		TypeMacports:  true,
   222  		TypeWinget:    true,
   223  	}
   224  
   225  	// purl type is case-insensitive, canonical form is lower-case
   226  	t = strings.ToLower(t)
   227  	_, ok := types[t]
   228  	return ok
   229  }
   230  
   231  // Qualifier names.
   232  const (
   233  	Distro              = "distro"
   234  	Epoch               = "epoch"
   235  	Arch                = "arch"
   236  	Origin              = "origin"
   237  	Source              = "source"
   238  	SourceVersion       = "sourceversion"
   239  	SourceRPM           = "sourcerpm"
   240  	BuildNumber         = "buildnumber"
   241  	PackageDependencies = "packagedependencies"
   242  	Classifier          = "classifier" // Maven specific qualifier
   243  	Type                = "type"       // Maven specific qualifier
   244  )