Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 45deca7f authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "compliance package policy and resolves"

parents f645c504 9ee7d03e
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -24,6 +24,14 @@ bootstrap_go_package {
        "condition.go",
        "conditionset.go",
        "graph.go",
        "policy/policy.go",
        "policy/resolve.go",
        "policy/resolvenotices.go",
        "policy/resolveshare.go",
        "policy/resolveprivacy.go",
        "policy/shareprivacyconflicts.go",
        "policy/shipped.go",
        "policy/walk.go",
        "readgraph.go",
        "resolution.go",
        "resolutionset.go",
@@ -32,6 +40,14 @@ bootstrap_go_package {
        "condition_test.go",
        "conditionset_test.go",
        "readgraph_test.go",
        "policy/policy_test.go",
        "policy/resolve_test.go",
        "policy/resolvenotices_test.go",
        "policy/resolveshare_test.go",
        "policy/resolveprivacy_test.go",
        "policy/shareprivacyconflicts_test.go",
        "policy/shipped_test.go",
        "policy/walk_test.go",
        "resolutionset_test.go",
        "test_util.go",
    ],
+238 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compliance

import (
	"regexp"
	"strings"
)

var (
	// ImpliesUnencumbered lists the condition names representing an author attempt to disclaim copyright.
	ImpliesUnencumbered = ConditionNames{"unencumbered"}

	// ImpliesPermissive lists the condition names representing copyrighted but "licensed without policy requirements".
	ImpliesPermissive = ConditionNames{"permissive"}

	// ImpliesNotice lists the condition names implying a notice or attribution policy.
	ImpliesNotice = ConditionNames{"unencumbered", "permissive", "notice", "reciprocal", "restricted", "proprietary", "by_exception_only"}

	// ImpliesReciprocal lists the condition names implying a local source-sharing policy.
	ImpliesReciprocal = ConditionNames{"reciprocal"}

	// Restricted lists the condition names implying an infectious source-sharing policy.
	ImpliesRestricted = ConditionNames{"restricted"}

	// ImpliesProprietary lists the condition names implying a confidentiality policy.
	ImpliesProprietary = ConditionNames{"proprietary"}

	// ImpliesByExceptionOnly lists the condition names implying a policy for "license review and approval before use".
	ImpliesByExceptionOnly = ConditionNames{"proprietary", "by_exception_only"}

	// ImpliesPrivate lists the condition names implying a source-code privacy policy.
	ImpliesPrivate = ConditionNames{"proprietary"}

	// ImpliesShared lists the condition names implying a source-code sharing policy.
	ImpliesShared = ConditionNames{"reciprocal", "restricted"}
)

var (
	anyLgpl      = regexp.MustCompile(`^SPDX-license-identifier-LGPL.*`)
	versionedGpl = regexp.MustCompile(`^SPDX-license-identifier-GPL-\p{N}.*`)
	genericGpl   = regexp.MustCompile(`^SPDX-license-identifier-GPL$`)
	ccBySa       = regexp.MustCompile(`^SPDX-license-identifier-CC-BY.*-SA.*`)
)

// Resolution happens in two passes:
//
// 1. A bottom-up traversal propagates license conditions up to targets from
// dendencies as needed.
//
// 2. For each condition of interest, a top-down traversal adjusts the attached
// conditions pushing restricted down from targets into linked dependencies.
//
// The behavior of the 2 passes gets controlled by the 2 functions below.
//
// The first function controls what happens during the bottom-up traversal. In
// general conditions flow up through static links but not other dependencies;
// except, restricted sometimes flows up through dynamic links.
//
// In general, too, the originating target gets acted on to resolve the
// condition (e.g. providing notice), but again restricted is special in that
// it requires acting on (i.e. sharing source of) both the originating module
// and the target using the module.
//
// The latter function controls what happens during the top-down traversal. In
// general, only restricted conditions flow down at all, and only through
// static links.
//
// Not all restricted licenses are create equal. Some have special rules or
// exceptions. e.g. LGPL or "with classpath excption".

// depActionsApplicableToTarget returns the actions which propagate up an
// edge from dependency to target.
//
// This function sets the policy for the bottom-up traversal and how conditions
// flow up the graph from dependencies to targets.
//
// If a pure aggregation is built into a derivative work that is not a pure
// aggregation, per policy it ceases to be a pure aggregation in the context of
// that derivative work. The `treatAsAggregate` parameter will be false for
// non-aggregates and for aggregates in non-aggregate contexts.
func depActionsApplicableToTarget(e TargetEdge, depActions actionSet, treatAsAggregate bool) actionSet {
	result := make(actionSet)
	if edgeIsDerivation(e) {
		result.addSet(depActions)
		for _, cs := range depActions.byName(ImpliesRestricted) {
			result.add(e.Target(), cs)
		}
		return result
	}
	if !edgeIsDynamicLink(e) {
		return result
	}

	restricted := depActions.byName(ImpliesRestricted)
	for actsOn, cs := range restricted {
		for _, lc := range cs.AsList() {
			hasGpl := false
			hasLgpl := false
			hasClasspath := false
			hasGeneric := false
			hasOther := false
			for _, kind := range lc.origin.LicenseKinds() {
				if strings.HasSuffix(kind, "-with-classpath-exception") {
					hasClasspath = true
				} else if anyLgpl.MatchString(kind) {
					hasLgpl = true
				} else if versionedGpl.MatchString(kind) {
					hasGpl = true
				} else if genericGpl.MatchString(kind) {
					hasGeneric = true
				} else if kind == "legacy_restricted" || ccBySa.MatchString(kind) {
					hasOther = true
				}
			}
			if hasOther || hasGpl {
				result.addCondition(actsOn, lc)
				result.addCondition(e.Target(), lc)
				continue
			}
			if hasClasspath && !edgeNodesAreIndependentModules(e) {
				result.addCondition(actsOn, lc)
				result.addCondition(e.Target(), lc)
				continue
			}
			if hasLgpl || hasClasspath {
				continue
			}
			if !hasGeneric {
				continue
			}
			result.addCondition(actsOn, lc)
			result.addCondition(e.Target(), lc)
		}
	}
	return result
}

// targetConditionsApplicableToDep returns the conditions which propagate down
// an edge from target to dependency.
//
// This function sets the policy for the top-down traversal and how conditions
// flow down the graph from targets to dependencies.
//
// If a pure aggregation is built into a derivative work that is not a pure
// aggregation, per policy it ceases to be a pure aggregation in the context of
// that derivative work. The `treatAsAggregate` parameter will be false for
// non-aggregates and for aggregates in non-aggregate contexts.
func targetConditionsApplicableToDep(e TargetEdge, targetConditions *LicenseConditionSet, treatAsAggregate bool) *LicenseConditionSet {
	result := targetConditions.Copy()

	// reverse direction -- none of these apply to things depended-on, only to targets depending-on.
	result.RemoveAllByName(ConditionNames{"unencumbered", "permissive", "notice", "reciprocal", "proprietary", "by_exception_only"})

	if !edgeIsDerivation(e) && !edgeIsDynamicLink(e) {
		// target is not a derivative work of dependency and is not linked to dependency
		result.RemoveAllByName(ImpliesRestricted)
		return result
	}
	if treatAsAggregate {
		// If the author of a pure aggregate licenses it restricted, apply restricted to immediate dependencies.
		// Otherwise, restricted does not propagate back down to dependencies.
		restricted := result.ByName(ImpliesRestricted).AsList()
		for _, lc := range restricted {
			if lc.origin.name != e.e.target {
				result.Remove(lc)
			}
		}
		return result
	}
	if edgeIsDerivation(e) {
		return result
	}
	restricted := result.ByName(ImpliesRestricted).AsList()
	for _, lc := range restricted {
		hasGpl := false
		hasLgpl := false
		hasClasspath := false
		hasGeneric := false
		hasOther := false
		for _, kind := range lc.origin.LicenseKinds() {
			if strings.HasSuffix(kind, "-with-classpath-exception") {
				hasClasspath = true
			} else if anyLgpl.MatchString(kind) {
				hasLgpl = true
			} else if versionedGpl.MatchString(kind) {
				hasGpl = true
			} else if genericGpl.MatchString(kind) {
				hasGeneric = true
			} else if kind == "legacy_restricted" || ccBySa.MatchString(kind) {
				hasOther = true
			}
		}
		if hasOther || hasGpl {
			continue
		}
		if hasClasspath && !edgeNodesAreIndependentModules(e) {
			continue
		}
		if hasGeneric && !hasLgpl && !hasClasspath {
			continue
		}
		result.Remove(lc)
	}
	return result
}

// edgeIsDynamicLink returns true for edges representing shared libraries
// linked dynamically at runtime.
func edgeIsDynamicLink(e TargetEdge) bool {
	return e.e.annotations.HasAnnotation("dynamic")
}

// edgeIsDerivation returns true for edges where the target is a derivative
// work of dependency.
func edgeIsDerivation(e TargetEdge) bool {
	isDynamic := e.e.annotations.HasAnnotation("dynamic")
	isToolchain := e.e.annotations.HasAnnotation("toolchain")
	return !isDynamic && !isToolchain
}

// edgeNodesAreIndependentModules returns true for edges where the target and
// dependency are independent modules.
func edgeNodesAreIndependentModules(e TargetEdge) bool {
	return e.Target().PackageName() != e.Dependency().PackageName()
}
+300 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compliance

import (
	"bytes"
	"fmt"
	"sort"
	"strings"
	"testing"
)

func TestPolicy_edgeConditions(t *testing.T) {
	tests := []struct {
		name                     string
		edge                     annotated
		treatAsAggregate         bool
		otherCondition           string
		expectedDepActions       []string
		expectedTargetConditions []string
	}{
		{
			name:                     "firstparty",
			edge:                     annotated{"apacheBin.meta_lic", "apacheLib.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"apacheLib.meta_lic:apacheLib.meta_lic:notice"},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "notice",
			edge:                     annotated{"mitBin.meta_lic", "mitLib.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"mitLib.meta_lic:mitLib.meta_lic:notice"},
			expectedTargetConditions: []string{},
		},
		{
			name: "fponlgpl",
			edge: annotated{"apacheBin.meta_lic", "lgplLib.meta_lic", []string{"static"}},
			expectedDepActions: []string{
				"apacheBin.meta_lic:lgplLib.meta_lic:restricted",
				"lgplLib.meta_lic:lgplLib.meta_lic:restricted",
			},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "fponlgpldynamic",
			edge:                     annotated{"apacheBin.meta_lic", "lgplLib.meta_lic", []string{"dynamic"}},
			expectedDepActions:       []string{},
			expectedTargetConditions: []string{},
		},
		{
			name: "fpongpl",
			edge: annotated{"apacheBin.meta_lic", "gplLib.meta_lic", []string{"static"}},
			expectedDepActions: []string{
				"apacheBin.meta_lic:gplLib.meta_lic:restricted",
				"gplLib.meta_lic:gplLib.meta_lic:restricted",
			},
			expectedTargetConditions: []string{},
		},
		{
			name: "fpongpldynamic",
			edge: annotated{"apacheBin.meta_lic", "gplLib.meta_lic", []string{"dynamic"}},
			expectedDepActions: []string{
				"apacheBin.meta_lic:gplLib.meta_lic:restricted",
				"gplLib.meta_lic:gplLib.meta_lic:restricted",
			},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "independentmodule",
			edge:                     annotated{"apacheBin.meta_lic", "gplWithClasspathException.meta_lic", []string{"dynamic"}},
			expectedDepActions:       []string{},
			expectedTargetConditions: []string{},
		},
		{
			name: "independentmodulestatic",
			edge: annotated{"apacheBin.meta_lic", "gplWithClasspathException.meta_lic", []string{"static"}},
			expectedDepActions: []string{
				"apacheBin.meta_lic:gplWithClasspathException.meta_lic:restricted",
				"gplWithClasspathException.meta_lic:gplWithClasspathException.meta_lic:restricted",
			},
			expectedTargetConditions: []string{},
		},
		{
			name: "dependentmodule",
			edge: annotated{"dependentModule.meta_lic", "gplWithClasspathException.meta_lic", []string{"dynamic"}},
			expectedDepActions: []string{
				"dependentModule.meta_lic:gplWithClasspathException.meta_lic:restricted",
				"gplWithClasspathException.meta_lic:gplWithClasspathException.meta_lic:restricted",
			},
			expectedTargetConditions: []string{},
		},

		{
			name:                     "lgplonfp",
			edge:                     annotated{"lgplBin.meta_lic", "apacheLib.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"apacheLib.meta_lic:apacheLib.meta_lic:notice"},
			expectedTargetConditions: []string{"lgplBin.meta_lic:restricted"},
		},
		{
			name:                     "lgplonfpdynamic",
			edge:                     annotated{"lgplBin.meta_lic", "apacheLib.meta_lic", []string{"dynamic"}},
			expectedDepActions:       []string{},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "gplonfp",
			edge:                     annotated{"gplBin.meta_lic", "apacheLib.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"apacheLib.meta_lic:apacheLib.meta_lic:notice"},
			expectedTargetConditions: []string{"gplBin.meta_lic:restricted"},
		},
		{
			name:                     "gplcontainer",
			edge:                     annotated{"gplContainer.meta_lic", "apacheLib.meta_lic", []string{"static"}},
			treatAsAggregate:         true,
			expectedDepActions:       []string{"apacheLib.meta_lic:apacheLib.meta_lic:notice"},
			expectedTargetConditions: []string{"gplContainer.meta_lic:restricted"},
		},
		{
			name:             "gploncontainer",
			edge:             annotated{"apacheContainer.meta_lic", "apacheLib.meta_lic", []string{"static"}},
			treatAsAggregate: true,
			otherCondition:   "gplLib.meta_lic:restricted",
			expectedDepActions: []string{
				"apacheContainer.meta_lic:gplLib.meta_lic:restricted",
				"apacheLib.meta_lic:apacheLib.meta_lic:notice",
				"apacheLib.meta_lic:gplLib.meta_lic:restricted",
				"gplLib.meta_lic:gplLib.meta_lic:restricted",
			},
			expectedTargetConditions: []string{},
		},
		{
			name:             "gplonbin",
			edge:             annotated{"apacheBin.meta_lic", "apacheLib.meta_lic", []string{"static"}},
			treatAsAggregate: false,
			otherCondition:   "gplLib.meta_lic:restricted",
			expectedDepActions: []string{
				"apacheBin.meta_lic:gplLib.meta_lic:restricted",
				"apacheLib.meta_lic:apacheLib.meta_lic:notice",
				"apacheLib.meta_lic:gplLib.meta_lic:restricted",
				"gplLib.meta_lic:gplLib.meta_lic:restricted",
			},
			expectedTargetConditions: []string{"gplLib.meta_lic:restricted"},
		},
		{
			name:                     "gplonfpdynamic",
			edge:                     annotated{"gplBin.meta_lic", "apacheLib.meta_lic", []string{"dynamic"}},
			expectedDepActions:       []string{},
			expectedTargetConditions: []string{"gplBin.meta_lic:restricted"},
		},
		{
			name:                     "independentmodulereverse",
			edge:                     annotated{"gplWithClasspathException.meta_lic", "apacheBin.meta_lic", []string{"dynamic"}},
			expectedDepActions:       []string{},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "independentmodulereversestatic",
			edge:                     annotated{"gplWithClasspathException.meta_lic", "apacheBin.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"apacheBin.meta_lic:apacheBin.meta_lic:notice"},
			expectedTargetConditions: []string{"gplWithClasspathException.meta_lic:restricted"},
		},
		{
			name:                     "dependentmodulereverse",
			edge:                     annotated{"gplWithClasspathException.meta_lic", "dependentModule.meta_lic", []string{"dynamic"}},
			expectedDepActions:       []string{},
			expectedTargetConditions: []string{"gplWithClasspathException.meta_lic:restricted"},
		},
		{
			name: "ponr",
			edge: annotated{"proprietary.meta_lic", "gplLib.meta_lic", []string{"static"}},
			expectedDepActions: []string{
				"proprietary.meta_lic:gplLib.meta_lic:restricted",
				"gplLib.meta_lic:gplLib.meta_lic:restricted",
			},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "ronp",
			edge:                     annotated{"gplBin.meta_lic", "proprietary.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"proprietary.meta_lic:proprietary.meta_lic:proprietary"},
			expectedTargetConditions: []string{"gplBin.meta_lic:restricted"},
		},
		{
			name:                     "noticeonb_e_o",
			edge:                     annotated{"mitBin.meta_lic", "by_exception.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"by_exception.meta_lic:by_exception.meta_lic:by_exception_only"},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "b_e_oonnotice",
			edge:                     annotated{"by_exception.meta_lic", "mitLib.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"mitLib.meta_lic:mitLib.meta_lic:notice"},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "noticeonrecip",
			edge:                     annotated{"mitBin.meta_lic", "mplLib.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"mplLib.meta_lic:mplLib.meta_lic:reciprocal"},
			expectedTargetConditions: []string{},
		},
		{
			name:                     "reciponnotice",
			edge:                     annotated{"mplBin.meta_lic", "mitLib.meta_lic", []string{"static"}},
			expectedDepActions:       []string{"mitLib.meta_lic:mitLib.meta_lic:notice"},
			expectedTargetConditions: []string{},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fs := make(testFS)
			stderr := &bytes.Buffer{}
			target := meta[tt.edge.target] + fmt.Sprintf("deps: {\n  file: \"%s\"\n", tt.edge.dep)
			for _, ann := range tt.edge.annotations {
				target += fmt.Sprintf("  annotations: \"%s\"\n", ann)
			}
			fs[tt.edge.target] = []byte(target + "}\n")
			fs[tt.edge.dep] = []byte(meta[tt.edge.dep])
			lg, err := ReadLicenseGraph(&fs, stderr, []string{tt.edge.target})
			if err != nil {
				t.Errorf("unexpected error reading graph: %w", err)
				return
			}
			// simulate a condition inherited from another edge/dependency.
			otherTarget := ""
			otherCondition := ""
			if len(tt.otherCondition) > 0 {
				fields := strings.Split(tt.otherCondition, ":")
				otherTarget = fields[0]
				otherCondition = fields[1]
				// other target must exist in graph
				lg.targets[otherTarget] = &TargetNode{name: otherTarget}
				lg.targets[otherTarget].proto.LicenseConditions = append(lg.targets[otherTarget].proto.LicenseConditions, otherCondition)
			}
			if tt.expectedDepActions != nil {
				depActions := make(actionSet)
				depActions[lg.targets[tt.edge.dep]] = lg.targets[tt.edge.dep].LicenseConditions()
				if otherTarget != "" {
					// simulate a sub-dependency's condition having already propagated up to dep and about to go to target
					otherCs := lg.targets[otherTarget].LicenseConditions()
					depActions[lg.targets[tt.edge.dep]].AddSet(otherCs)
					depActions[lg.targets[otherTarget]] = otherCs
				}
				asActual := depActionsApplicableToTarget(lg.Edges()[0], depActions, tt.treatAsAggregate)
				asExpected := make(actionSet)
				for _, triple := range tt.expectedDepActions {
					fields := strings.Split(triple, ":")
					actsOn := lg.targets[fields[0]]
					origin := lg.targets[fields[1]]
					expectedConditions := newLicenseConditionSet()
					expectedConditions.add(origin, fields[2:]...)
					if _, ok := asExpected[actsOn]; ok {
						asExpected[actsOn].AddSet(expectedConditions)
					} else {
						asExpected[actsOn] = expectedConditions
					}
				}

				checkSameActions(lg, asActual, asExpected, t)
			}
			if tt.expectedTargetConditions != nil {
				targetConditions := lg.TargetNode(tt.edge.target).LicenseConditions()
				if otherTarget != "" {
					targetConditions.add(lg.targets[otherTarget], otherCondition)
				}
				cs := targetConditionsApplicableToDep(
					lg.Edges()[0],
					targetConditions,
					tt.treatAsAggregate)
				actual := make([]string, 0, cs.Count())
				for _, lc := range cs.AsList() {
					actual = append(actual, lc.asString(":"))
				}
				sort.Strings(actual)
				sort.Strings(tt.expectedTargetConditions)
				if len(actual) != len(tt.expectedTargetConditions) {
					t.Errorf("unexpected number of target conditions: got %v with %d conditions, want %v with %d conditions",
						actual, len(actual), tt.expectedTargetConditions, len(tt.expectedTargetConditions))
				} else {
					for i := 0; i < len(actual); i++ {
						if actual[i] != tt.expectedTargetConditions[i] {
							t.Errorf("unexpected target condition at element %d: got %q, want %q",
								i, actual[i], tt.expectedTargetConditions[i])
						}
					}
				}
			}
		})
	}
}
+217 −0

File added.

Preview size limit exceeded, changes collapsed.

+755 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading