github.com/google/osv-scalibr@v0.4.1/inventory/osvecosystem/parsed.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 osvecosystem provides the Parsed type which represents an OSV ecosystem string.
    16  package osvecosystem
    17  
    18  import (
    19  	"encoding/json"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/ossf/osv-schema/bindings/go/osvconstants"
    24  )
    25  
    26  // Parsed represents an ecosystem-with-suffix string as defined by the [spec], parsed into
    27  // a structured format.
    28  //
    29  // The suffix is optional and is separated from the ecosystem by a colon.
    30  //
    31  // For example, "Debian:7" would be parsed into Parsed{Ecosystem: constants.EcosystemDebian, Suffix: "7"}
    32  //
    33  // [spec]: https://ossf.github.io/osv-schema/
    34  //
    35  //nolint:recvcheck
    36  type Parsed struct {
    37  	Ecosystem osvconstants.Ecosystem
    38  	Suffix    string
    39  }
    40  
    41  // IsEmpty returns true if the Ecosystem struct is empty.
    42  func (p Parsed) IsEmpty() bool {
    43  	return p.Ecosystem == ""
    44  }
    45  
    46  // Equal returns true if the two Parsed structs are equal.
    47  func (p Parsed) Equal(other Parsed) bool {
    48  	// only care about the minor version if both ecosystems have one
    49  	// otherwise we just assume that they're the same and move on
    50  	if p.Suffix != "" && other.Suffix != "" {
    51  		return p.Ecosystem == other.Ecosystem && p.Suffix == other.Suffix
    52  	}
    53  
    54  	return p.Ecosystem == other.Ecosystem
    55  }
    56  
    57  func (p Parsed) String() string {
    58  	str := string(p.Ecosystem)
    59  
    60  	if p.Suffix != "" {
    61  		str += ":" + p.Suffix
    62  	}
    63  
    64  	return str
    65  }
    66  
    67  // UnmarshalJSON handles unmarshalls a JSON string into a Parsed struct.
    68  //
    69  // This method implements the json.Unmarshaler interface.
    70  func (p *Parsed) UnmarshalJSON(data []byte) error {
    71  	var str string
    72  	err := json.Unmarshal(data, &str)
    73  
    74  	if err != nil {
    75  		return err
    76  	}
    77  
    78  	*p, err = Parse(str)
    79  
    80  	return err
    81  }
    82  
    83  // MarshalJSON handles marshals a Parsed struct into a JSON string.
    84  //
    85  // This method implements the json.Marshaler interface.
    86  func (p Parsed) MarshalJSON() ([]byte, error) {
    87  	return []byte(`"` + p.String() + `"`), nil
    88  }
    89  
    90  // GetValidity checks if the ecosystem is a valid OSV ecosystem value. Returns nil if valid, error otherwise.
    91  func (p Parsed) GetValidity() error {
    92  	if p.IsEmpty() {
    93  		return nil
    94  	}
    95  
    96  	// Missing ecosystems here would be caught by the "exhaustive" linter
    97  	switch p.Ecosystem {
    98  	case osvconstants.EcosystemAlmaLinux,
    99  		osvconstants.EcosystemAlpaquita,
   100  		osvconstants.EcosystemAlpine,
   101  		osvconstants.EcosystemAndroid,
   102  		osvconstants.EcosystemBellSoftHardenedContainers,
   103  		osvconstants.EcosystemBioconductor,
   104  		osvconstants.EcosystemBitnami,
   105  		osvconstants.EcosystemChainguard,
   106  		osvconstants.EcosystemConanCenter,
   107  		osvconstants.EcosystemCRAN,
   108  		osvconstants.EcosystemCratesIO,
   109  		osvconstants.EcosystemDebian,
   110  		osvconstants.EcosystemGHC,
   111  		osvconstants.EcosystemGitHubActions,
   112  		osvconstants.EcosystemGo,
   113  		osvconstants.EcosystemHackage,
   114  		osvconstants.EcosystemHex,
   115  		osvconstants.EcosystemKubernetes,
   116  		osvconstants.EcosystemLinux,
   117  		osvconstants.EcosystemMageia,
   118  		osvconstants.EcosystemMaven,
   119  		osvconstants.EcosystemMinimOS,
   120  		osvconstants.EcosystemNPM,
   121  		osvconstants.EcosystemNuGet,
   122  		osvconstants.EcosystemOpenEuler,
   123  		osvconstants.EcosystemOpenSUSE,
   124  		osvconstants.EcosystemOSSFuzz,
   125  		osvconstants.EcosystemPackagist,
   126  		osvconstants.EcosystemPhotonOS,
   127  		osvconstants.EcosystemPub,
   128  		osvconstants.EcosystemPyPI,
   129  		osvconstants.EcosystemRedHat,
   130  		osvconstants.EcosystemRockyLinux,
   131  		osvconstants.EcosystemRubyGems,
   132  		osvconstants.EcosystemSUSE,
   133  		osvconstants.EcosystemSwiftURL,
   134  		osvconstants.EcosystemUbuntu,
   135  		osvconstants.EcosystemWolfi:
   136  
   137  	default:
   138  		return fmt.Errorf("base ecosystem does not exist in osvschema: %q", p.Ecosystem)
   139  	}
   140  
   141  	return nil
   142  }
   143  
   144  // MustParse parses a string into a constants.Ecosystem and an optional suffix specified with a ":"
   145  // Panics if there is an invalid ecosystem
   146  func MustParse(str string) Parsed {
   147  	parsed, err := Parse(str)
   148  	if err != nil {
   149  		panic("Failed MustParse: " + err.Error())
   150  	}
   151  
   152  	return parsed
   153  }
   154  
   155  // Parse parses a string into a constants.Ecosystem and an optional suffix specified with a ":"
   156  func Parse(str string) (Parsed, error) {
   157  	// Special case to return an empty ecosystem if str is empty
   158  	// This is not considered an error.
   159  	if str == "" {
   160  		return Parsed{}, nil
   161  	}
   162  
   163  	// We will also add a check for whether the ecosystem is valid to have a suffix here.
   164  	// And return an error if not.
   165  	ecosystem, suffix, _ := strings.Cut(str, ":")
   166  
   167  	result := Parsed{osvconstants.Ecosystem(ecosystem), suffix}
   168  
   169  	return result, result.GetValidity()
   170  }
   171  
   172  // FromEcosystem creates a Parsed struct from an osvschema.Ecosystem.
   173  func FromEcosystem(ecosystem osvconstants.Ecosystem) Parsed {
   174  	return Parsed{Ecosystem: ecosystem}
   175  }