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

Commit dfd9be96 authored by Treehugger Robot's avatar Treehugger Robot Committed by Automerger Merge Worker
Browse files

Merge "compliance package policy and resolves" am: 45deca7f am: e7d26816

Original change: https://android-review.googlesource.com/c/platform/build/+/1870078

Change-Id: I9b279f3a0a47517440cca2beba7c028f7c9e02fb
parents ad7b35b7 e7d26816
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