anchore-oss-update-bot 5b58ec96b7
chore(deps): update Go version (#4773)
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>
2026-04-15 10:01:39 -04:00

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
}