// 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() }