feat: detect when full license text has been provided and preserve as separate field (#3450)

* feat: add full text field to syft license struct
---------
Signed-off-by: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
Christopher Angelo Phillips
2025-05-01 15:00:46 -04:00
committed by GitHub
parent 4999de4114
commit 94e63eb367
23 changed files with 3282 additions and 426 deletions
+30 -4
View File
@@ -25,9 +25,15 @@ var _ sort.Interface = (*Licenses)(nil)
// in order to distinguish if packages should be kept separate
// this is different for licenses since we're only looking for evidence
// of where a license was declared/concluded for a given package
// If a license is given as it's full text in the metadata rather than it's value or SPDX expression
// The FullText field is used to represent this data
// A Concluded License type is the license the SBOM creator believes governs the package (human crafted or altered SBOM)
// The Declared License is what the authors of a project believe govern the package. This is the default type syft declares.
type License struct {
Value string
SPDXExpression string
Value string
FullText string
Type license.Type
URLs []string `hash:"ignore"`
Locations file.LocationSet `hash:"ignore"`
@@ -68,8 +74,16 @@ func NewLicense(value string) License {
}
func NewLicenseFromType(value string, t license.Type) License {
var spdxExpression string
if value != "" {
var (
spdxExpression string
fullText string
)
// Check parsed value for newline character to see if it's the full license text
// License: <HERE IS THE FULL TEXT> <Expressions>
// DO we want to also submit file name when determining fulltext
if strings.Contains(strings.TrimSpace(value), "\n") {
fullText = value
} else {
var err error
spdxExpression, err = license.ParseExpression(value)
if err != nil {
@@ -77,6 +91,14 @@ func NewLicenseFromType(value string, t license.Type) License {
}
}
if fullText != "" {
return License{
FullText: fullText,
Type: t,
Locations: file.NewLocationSet(),
}
}
return License{
Value: value,
SPDXExpression: spdxExpression,
@@ -99,7 +121,7 @@ func NewLicensesFromLocation(location file.Location, values ...string) (licenses
}
licenses = append(licenses, NewLicenseFromLocations(v, location))
}
return
return licenses
}
func NewLicenseFromLocations(value string, locations ...file.Location) License {
@@ -157,6 +179,10 @@ func NewLicenseFromFields(value, url string, location *file.Location) License {
return l
}
func (s License) Empty() bool {
return s.Value == "" && s.SPDXExpression == "" && s.FullText == ""
}
// Merge two licenses into a new license object. If the merge is not possible due to unmergeable fields
// (e.g. different values for Value, SPDXExpression, Type, or any non-collection type) an error is returned.
// TODO: this is a bit of a hack to not infinitely recurse when hashing a license
+9 -8
View File
@@ -50,15 +50,16 @@ func (s *LicenseSet) Add(licenses ...License) {
s.set = make(map[artifact.ID]License)
}
for _, l := range licenses {
// we only want to add licenses that have a value
// we only want to add licenses that are not empty
if l.Empty() {
continue
}
// note, this check should be moved to the license constructor in the future
if l.Value != "" {
if id, merged, err := s.addToExisting(l); err == nil && !merged {
// doesn't exist, add it
s.set[id] = l
} else if err != nil {
log.Trace("license set failed to add license %#v: %+v", l, err)
}
if id, merged, err := s.addToExisting(l); err == nil && !merged {
// doesn't exist, add it
s.set[id] = l
} else if err != nil {
log.WithFields("error", err, "license", l).Trace("failed to add license to license set")
}
}
}
+27 -2
View File
@@ -13,7 +13,6 @@ import (
)
func Test_Hash(t *testing.T) {
loc1 := file.NewLocation("place!")
loc1.FileSystemID = "fs1"
loc2 := file.NewLocation("place!")
@@ -227,6 +226,33 @@ func TestLicense_Merge(t *testing.T) {
}
}
func TestFullText(t *testing.T) {
fullText := `I am a license with full text
my authors put new line characters in metadata for labeling a license`
tests := []struct {
name string
value string
want License
}{
{
name: "Full Text field is populated with the correct full text",
value: fullText,
want: License{
Value: "",
Type: license.Declared,
FullText: fullText,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewLicense(tt.value)
assert.Equal(t, tt.want, got)
})
}
}
func TestLicenseConstructors(t *testing.T) {
type input struct {
value string
@@ -244,7 +270,6 @@ func TestLicenseConstructors(t *testing.T) {
urls: []string{
`
http://user-agent-utils.googlecode.com/svn/trunk/UserAgentUtils/LICENSE.txt
`},
},
expected: License{