wfn/matching.go (297 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates.
//
// 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 wfn
import (
"fmt"
)
// Possible values of Relation type
const (
Disjoint Relation = iota
Subset
Equal
Superset
)
// HasWildcard returns true if attribute has a wildcard symbol in it
func HasWildcard(s string) bool {
for n, r := range s {
if r != '*' && r != '?' {
continue
}
quoted := false
for i := n - 1; i >= 0; i-- {
if s[i] != '\\' {
break
}
quoted = !quoted
}
if !quoted {
return true
}
}
return false
}
// Relation describes four possible set relations of wfns attribute-value
type Relation int
// String return human readable representation of Relation value
func (r Relation) String() string {
switch r {
case Disjoint:
return "DISJOINT"
case Subset:
return "SUBSET"
case Equal:
return "EQUAL"
case Superset:
return "SUPERSET"
default:
return fmt.Sprintf("Undefined value %d", r)
}
}
// Comparison is the result of CPE name matching
type Comparison struct {
Part Relation
Vendor Relation
Product Relation
Version Relation
Update Relation
Edition Relation
Language Relation
SWEdition Relation
TargetSW Relation
TargetHW Relation
Other Relation
}
// IsDisjoint returns true if the result CPE name matching is disjoint
func (c Comparison) IsDisjoint() bool {
switch {
case c.Part == Disjoint:
return true
case c.Vendor == Disjoint:
return true
case c.Product == Disjoint:
return true
case c.Version == Disjoint:
return true
case c.Update == Disjoint:
return true
case c.Edition == Disjoint:
return true
case c.Language == Disjoint:
return true
case c.SWEdition == Disjoint:
return true
case c.TargetSW == Disjoint:
return true
case c.TargetHW == Disjoint:
return true
case c.Other == Disjoint:
return true
default:
return false
}
}
// IsEqual returns true if the result CPE name matching is equal
func (c Comparison) IsEqual() bool {
switch {
case c.Part != Equal:
return false
case c.Vendor != Equal:
return false
case c.Product != Equal:
return false
case c.Version != Equal:
return false
case c.Update != Equal:
return false
case c.Edition != Equal:
return false
case c.Language != Equal:
return false
case c.SWEdition != Equal:
return false
case c.TargetSW != Equal:
return false
case c.TargetHW != Equal:
return false
case c.Other != Equal:
return false
default:
return true
}
}
// IsSubset returns true if the result CPE name matching is a subset relation
func (c Comparison) IsSubset() bool {
switch {
case c.Part != Equal && c.Part != Subset:
return false
case c.Vendor != Equal && c.Vendor != Subset:
return false
case c.Product != Equal && c.Product != Subset:
return false
case c.Version != Equal && c.Version != Subset:
return false
case c.Update != Equal && c.Update != Subset:
return false
case c.Edition != Equal && c.Edition != Subset:
return false
case c.Language != Equal && c.Language != Subset:
return false
case c.SWEdition != Equal && c.SWEdition != Subset:
return false
case c.TargetSW != Equal && c.TargetSW != Subset:
return false
case c.TargetHW != Equal && c.TargetHW != Subset:
return false
case c.Other != Equal && c.Other != Subset:
return false
default:
return true
}
}
// IsSuperset returns true if the result CPE name matching is a superset relation
func (c Comparison) IsSuperset() bool {
switch {
case c.Part != Equal && c.Part != Superset:
return false
case c.Vendor != Equal && c.Vendor != Superset:
return false
case c.Product != Equal && c.Product != Superset:
return false
case c.Version != Equal && c.Version != Superset:
return false
case c.Update != Equal && c.Update != Superset:
return false
case c.Edition != Equal && c.Edition != Superset:
return false
case c.Language != Equal && c.Language != Superset:
return false
case c.SWEdition != Equal && c.SWEdition != Superset:
return false
case c.TargetSW != Equal && c.TargetSW != Superset:
return false
case c.TargetHW != Equal && c.TargetHW != Superset:
return false
case c.Other != Equal && c.Other != Superset:
return false
default:
return true
}
}
// Relation returns relation between matched CPE names
func (c Comparison) Relation() Relation {
if c.IsSubset() {
return Subset
}
if c.IsEqual() {
return Equal
}
if c.IsSuperset() {
return Superset
}
return Disjoint
}
// Compare performs comparison of each attribute-value (A-V) of the wfns
// as per Name Matching Specification v.2.3 and returns the set relation between
// source and target attribute-values.
// The table below illustrates a set of source and target A-Vs and the resulting set of attribute
// comparison relations.
// +--------------------------------------------+------------------------------------+
// | Attribute Relation Set | Name Comparison Relation |
// +--------------------------------------------+------------------------------------+
// | any attribute relation is != | CPE name relation is DISJOINT (!=) |
// | all attribute relations are == | CPE name relation is EQAL (==) |
// | all attribute relations are Subset or == | CPE name relation is Subset |
// | all attribute relations are Superset or == | CPE name relation is Superset |
// +--------------------------------------------+------------------------------------+
func Compare(src, tgt *Attributes) (Comparison, error) {
var result Comparison
var err error
if result.Part, err = CompareAttr(src.Part, tgt.Part); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.Part, tgt.Part, err)
}
if result.Vendor, err = CompareAttr(src.Vendor, tgt.Vendor); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.Vendor, tgt.Vendor, err)
}
if result.Product, err = CompareAttr(src.Product, tgt.Product); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.Product, tgt.Product, err)
}
if result.Version, err = CompareAttr(src.Version, tgt.Version); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.Version, tgt.Version, err)
}
if result.Update, err = CompareAttr(src.Update, tgt.Update); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.Update, tgt.Update, err)
}
if result.Edition, err = CompareAttr(src.Edition, tgt.Edition); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.Edition, tgt.Edition, err)
}
if result.Language, err = CompareAttr(src.Language, tgt.Language); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.Language, tgt.Language, err)
}
if result.SWEdition, err = CompareAttr(src.SWEdition, tgt.SWEdition); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.SWEdition, tgt.SWEdition, err)
}
if result.TargetSW, err = CompareAttr(src.TargetSW, tgt.TargetSW); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.TargetSW, tgt.TargetSW, err)
}
if result.TargetHW, err = CompareAttr(src.TargetHW, tgt.TargetHW); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.TargetHW, tgt.TargetHW, err)
}
if result.Other, err = CompareAttr(src.Other, tgt.Other); err != nil {
return result, fmt.Errorf("failed to compare wfns %q to %q: %v", src.Other, tgt.Other, err)
}
return result, nil
}
// Match returns false if the src and tgt attributes are disjoint.
// Undefined relations between attributes (see CompareAttr) are considered to be disjoint,
// except when source attribute matches target attribute byte-by-byte.
func Match(src, tgt *Attributes) bool {
if src == nil || tgt == nil {
return false
}
return matchAttr(src.Part, tgt.Part) && matchAttr(src.Vendor, tgt.Vendor) &&
matchAttr(src.Product, tgt.Product) && matchAttr(src.Version, tgt.Version) &&
matchAttr(src.Update, tgt.Update) && matchAttr(src.Edition, tgt.Edition) &&
matchAttr(src.Language, tgt.Language) && matchAttr(src.SWEdition, tgt.SWEdition) &&
matchAttr(src.TargetHW, tgt.TargetHW) && matchAttr(src.TargetSW, tgt.TargetSW) &&
matchAttr(src.Other, tgt.Other)
}
// CompareAttr calculates a relation between a pair of wfn attribute values.
// Accordingly to standard, string matching must be insensitive to lexical case,
// target A-V must not have wildcards.
// The table below defines possible set relations for each comparison
// ANY and NA are logical values as defined per [CPE23-N:5.3.1]
// i and k are wildcard-free attribute-value strings that are not identical, e.g. i is "foo" and k is "bar"
// m + w is attribute-value string containing a legal combination of unquoted question mark or asterisk wildcards
// at the beginning and/or the end of the string, e.g. "*b??"
// Enumeration of
// Attribute Comparison Set Relations
// +------------+------------+--------------+
// | Source A-V | Target A-V | Relation |
// +------------+------------+--------------+
// | ANY | ANY | == |
// | ANY | NA | Superset |
// | ANY | i | Superset |
// | ANY | m + w | undef |
// | NA | ANY | Subset |
// | NA | NA | == |
// | NA | i | != |
// | NA | m + w | undefined |
// | i | i | == |
// | i | k | != |
// | i | m + w | undefined |
// | i | NA | != |
// | i | ANY | Subset |
// | m1 + w | m2 | Subset or != |
// | m + w | ANY | Subset |
// | m1 + w | NA | != |
// | m1 + w | m2 + w | undefined |
// +----------------------------------------+
func CompareAttr(src, tgt string) (Relation, error) {
if src != NA && src != Any && HasWildcard(tgt) {
return Disjoint, fmt.Errorf("target attribute value cannot contain wildcard")
}
if src == tgt {
return Equal, nil
}
if src == Any {
return Superset, nil
}
if tgt == Any {
return Subset, nil
}
if src == NA || tgt == NA {
return Disjoint, nil
}
return matchStr(src, tgt), nil
}
// matchAttr returns true if relation between src and tgt is one of Equal, Subset or Superset.
// It returns false on undefined relations, except when src == tgt byte-by-byte.
// This is crude but fast(-er) version of CompareAttr.
func matchAttr(src, tgt string) bool {
switch {
case src == Any || tgt == Any || src == tgt:
return true
case src == NA || tgt == NA || HasWildcard(tgt):
return false
default:
return matchStr(src, tgt) != Disjoint
}
}
func matchStr(s, t string) Relation {
escaped := false
matchesAs := Equal
idx := 0
for ; idx < len(t); idx++ {
if idx >= len(s) {
return Disjoint
}
if !escaped && s[idx] == '*' {
if idx == len(s)-1 {
return Superset
}
for i := idx; i < len(t); i++ {
if matchStr(s[idx+1:], t[i:]) != Disjoint {
return Superset
}
}
return Disjoint
}
if (escaped || s[idx] != '?') && s[idx] != t[idx] {
return Disjoint
} else if !escaped && s[idx] == '?' {
matchesAs = Superset
}
if s[idx] == '\\' {
escaped = !escaped
} else {
escaped = false
}
}
for ; idx < len(s); idx++ {
if s[idx] != '*' {
return Disjoint
}
}
if len(s) > len(t) {
return Superset
}
return matchesAs
}