mirror of
https://github.com/anchore/syft.git
synced 2026-05-20 04:05:24 +02:00
Signed-off-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com> Co-authored-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com>
249 lines
6.8 KiB
Go
249 lines
6.8 KiB
Go
package gentoo
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/scylladb/go-set/strset"
|
|
|
|
"github.com/anchore/syft/internal"
|
|
"github.com/anchore/syft/internal/log"
|
|
"github.com/anchore/syft/syft/file"
|
|
)
|
|
|
|
// the licenses files seems to conform to a custom format that is common to gentoo packages.
|
|
// see more details:
|
|
// - https://www.gentoo.org/glep/glep-0023.html#id9
|
|
// - https://devmanual.gentoo.org/general-concepts/licenses/index.html
|
|
//
|
|
// in short, the format is:
|
|
//
|
|
// mandatory-license
|
|
// || ( choosable-licence1 chooseable-license-2 )
|
|
// useflag? ( optional-component-license )
|
|
//
|
|
// "License names may contain [a-zA-Z0-9] (english alphanumeric characters), _ (underscore), - (hyphen), .
|
|
// (dot) and + (plus sign). They must not begin with a hyphen, a dot or a plus sign."
|
|
//
|
|
// this does not conform to SPDX license expressions, which would be a great enhancement in the future.
|
|
|
|
// extractLicenses attempts to parse the license field into a valid SPDX license expression
|
|
func extractLicenses(resolver file.Resolver, closestLocation *file.Location, reader io.Reader) (string, string) {
|
|
findings := strset.New()
|
|
contentsWriter := bytes.Buffer{}
|
|
scanner := bufio.NewScanner(io.TeeReader(reader, &contentsWriter))
|
|
scanner.Split(bufio.ScanWords)
|
|
var (
|
|
mandatoryLicenses, conditionalLicenses, useflagLicenses []string
|
|
usesGroups bool
|
|
pipe bool
|
|
useflag bool
|
|
)
|
|
|
|
for scanner.Scan() {
|
|
token := scanner.Text()
|
|
if token == "||" {
|
|
pipe = true
|
|
continue
|
|
}
|
|
// useflag
|
|
if strings.Contains(token, "?") {
|
|
useflag = true
|
|
continue
|
|
}
|
|
if !strings.ContainsAny(token, "()|?") {
|
|
switch {
|
|
case useflag:
|
|
useflagLicenses = append(useflagLicenses, token)
|
|
case pipe:
|
|
conditionalLicenses = append(conditionalLicenses, token)
|
|
default:
|
|
mandatoryLicenses = append(mandatoryLicenses, token)
|
|
}
|
|
if strings.HasPrefix(token, "@") {
|
|
usesGroups = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var licenseGroups map[string][]string
|
|
if usesGroups {
|
|
licenseGroups = readLicenseGroups(resolver, closestLocation)
|
|
}
|
|
mandatoryLicenses = replaceLicenseGroups(mandatoryLicenses, licenseGroups)
|
|
conditionalLicenses = replaceLicenseGroups(conditionalLicenses, licenseGroups)
|
|
findings.Add(mandatoryLicenses...)
|
|
findings.Add(conditionalLicenses...)
|
|
findings.Add(useflagLicenses...)
|
|
|
|
var mandatoryStatement, conditionalStatement string
|
|
|
|
// attempt to build valid SPDX license expression
|
|
if len(mandatoryLicenses) > 0 {
|
|
mandatoryStatement = strings.Join(mandatoryLicenses, " AND ")
|
|
}
|
|
if len(conditionalLicenses) > 0 {
|
|
conditionalStatement = strings.Join(conditionalLicenses, " OR ")
|
|
}
|
|
|
|
contents := strings.TrimSpace(contentsWriter.String())
|
|
|
|
if mandatoryStatement != "" && conditionalStatement != "" {
|
|
return contents, mandatoryStatement + " AND (" + conditionalStatement + ")"
|
|
}
|
|
|
|
if mandatoryStatement != "" {
|
|
return contents, mandatoryStatement
|
|
}
|
|
|
|
if conditionalStatement != "" {
|
|
return contents, conditionalStatement
|
|
}
|
|
|
|
return contents, ""
|
|
}
|
|
|
|
func readLicenseGroups(resolver file.Resolver, closestLocation *file.Location) map[string][]string {
|
|
if resolver == nil || closestLocation == nil {
|
|
return nil
|
|
}
|
|
var licenseGroups map[string][]string
|
|
groupLocation := resolver.RelativeFileByPath(*closestLocation, "/etc/portage/license_groups")
|
|
if groupLocation == nil {
|
|
return nil
|
|
}
|
|
|
|
groupReader, err := resolver.FileContentsByLocation(*groupLocation)
|
|
defer internal.CloseAndLogError(groupReader, groupLocation.RealPath)
|
|
if err != nil {
|
|
log.WithFields("path", groupLocation.RealPath, "error", err).Debug("failed to fetch portage LICENSE")
|
|
return nil
|
|
}
|
|
|
|
if groupReader == nil {
|
|
return nil
|
|
}
|
|
|
|
licenseGroups, err = parseLicenseGroups(groupReader)
|
|
if err != nil {
|
|
log.WithFields("path", groupLocation.RealPath, "error", err).Debug("failed to parse portage LICENSE")
|
|
}
|
|
|
|
return licenseGroups
|
|
}
|
|
|
|
func replaceLicenseGroups(licenses []string, groups map[string][]string) []string {
|
|
if groups == nil {
|
|
return licenses
|
|
}
|
|
|
|
result := make([]string, 0, len(licenses))
|
|
for _, license := range licenses {
|
|
if after, ok := strings.CutPrefix(license, "@"); ok {
|
|
// this is a license group...
|
|
name := after
|
|
if expandedLicenses, ok := groups[name]; ok {
|
|
result = append(result, expandedLicenses...)
|
|
} else {
|
|
// unable to expand, use the original license group value (including the '@')
|
|
result = append(result, license)
|
|
}
|
|
} else {
|
|
// this is a license...
|
|
result = append(result, license)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parseLicenseGroups(reader io.Reader) (map[string][]string, error) {
|
|
result := make(map[string][]string)
|
|
rawGroups := make(map[string][]string)
|
|
|
|
scanner := bufio.NewScanner(reader)
|
|
|
|
// first collect all raw groups
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
// skip empty lines and comments
|
|
continue
|
|
}
|
|
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 {
|
|
return nil, fmt.Errorf("invalid line format: %s", line)
|
|
}
|
|
|
|
groupName := parts[0]
|
|
licenses := parts[1:]
|
|
|
|
rawGroups[groupName] = licenses
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// next process each group to expand nested references
|
|
for groupName, licenses := range rawGroups {
|
|
expanded, err := expandLicenses(groupName, licenses, rawGroups, make(map[string]bool))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[groupName] = expanded
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// expandLicenses handles the recursive expansion of license groups, 'visited' is used to detect cycles. We are always
|
|
// in terms of slices instead of sets to ensure original ordering is preserved.
|
|
func expandLicenses(currentGroup string, licenses []string, rawGroups map[string][]string, visited map[string]bool) ([]string, error) {
|
|
if visited[currentGroup] {
|
|
return nil, fmt.Errorf("cycle detected in license group definitions for group: %s", currentGroup)
|
|
}
|
|
|
|
visited[currentGroup] = true
|
|
|
|
result := make([]string, 0)
|
|
|
|
for _, item := range licenses {
|
|
if strings.HasPrefix(item, "@") {
|
|
// this is a reference to another group
|
|
refGroupName := item[1:] // remove '@' prefix
|
|
|
|
refLicenses, exists := rawGroups[refGroupName]
|
|
if !exists {
|
|
return nil, fmt.Errorf("referenced group not found: %s", refGroupName)
|
|
}
|
|
|
|
newVisited := make(map[string]bool)
|
|
for k, v := range visited {
|
|
newVisited[k] = v
|
|
}
|
|
|
|
expanded, err := expandLicenses(refGroupName, refLicenses, rawGroups, newVisited)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, license := range expanded {
|
|
if !slices.Contains(result, license) {
|
|
result = append(result, license)
|
|
}
|
|
}
|
|
} else if !slices.Contains(result, item) {
|
|
// ...this is a regular license
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|