// Copyright 2018 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 containers defines types and functions for resolving qualified names within a namespace // or type provided to CEL. package containers import ( "fmt" "strings" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) var ( // DefaultContainer has an empty container name. DefaultContainer *Container = nil // Empty map to search for aliases when needed. noAliases = make(map[string]string) ) // NewContainer creates a new Container with the fully-qualified name. func NewContainer(opts ...ContainerOption) (*Container, error) { var c *Container var err error for _, opt := range opts { c, err = opt(c) if err != nil { return nil, err } } return c, nil } // Container holds a reference to an optional qualified container name and set of aliases. // // The program container can be used to simplify variable, function, and type specification within // CEL programs and behaves more or less like a C++ namespace. See ResolveCandidateNames for more // details. type Container struct { name string aliases map[string]string } // Extend creates a new Container with the existing settings and applies a series of // ContainerOptions to further configure the new container. func (c *Container) Extend(opts ...ContainerOption) (*Container, error) { if c == nil { return NewContainer(opts...) } // Copy the name and aliases of the existing container. ext := &Container{name: c.Name()} if len(c.aliasSet()) > 0 { aliasSet := make(map[string]string, len(c.aliasSet())) for k, v := range c.aliasSet() { aliasSet[k] = v } ext.aliases = aliasSet } // Apply the new options to the container. var err error for _, opt := range opts { ext, err = opt(ext) if err != nil { return nil, err } } return ext, nil } // Name returns the fully-qualified name of the container. // // The name may conceptually be a namespace, package, or type. func (c *Container) Name() string { if c == nil { return "" } return c.name } // ResolveCandidateNames returns the candidates name of namespaced identifiers in C++ resolution // order. // // Names which shadow other names are returned first. If a name includes a leading dot ('.'), // the name is treated as an absolute identifier which cannot be shadowed. // // Given a container name a.b.c.M.N and a type name R.s, this will deliver in order: // // a.b.c.M.N.R.s // a.b.c.M.R.s // a.b.c.R.s // a.b.R.s // a.R.s // R.s // // If aliases or abbreviations are configured for the container, then alias names will take // precedence over containerized names. func (c *Container) ResolveCandidateNames(name string) []string { if strings.HasPrefix(name, ".") { qn := name[1:] alias, isAlias := c.findAlias(qn) if isAlias { return []string{alias} } return []string{qn} } alias, isAlias := c.findAlias(name) if isAlias { return []string{alias} } if c.Name() == "" { return []string{name} } nextCont := c.Name() candidates := []string{nextCont + "." + name} for i := strings.LastIndex(nextCont, "."); i >= 0; i = strings.LastIndex(nextCont, ".") { nextCont = nextCont[:i] candidates = append(candidates, nextCont+"."+name) } return append(candidates, name) } // aliasSet returns the alias to fully-qualified name mapping stored in the container. func (c *Container) aliasSet() map[string]string { if c == nil || c.aliases == nil { return noAliases } return c.aliases } // findAlias takes a name as input and returns an alias expansion if one exists. // // If the name is qualified, the first component of the qualified name is checked against known // aliases. Any alias that is found in a qualified name is expanded in the result: // // alias: R -> my.alias.R // name: R.S.T // output: my.alias.R.S.T // // Note, the name must not have a leading dot. func (c *Container) findAlias(name string) (string, bool) { // If an alias exists for the name, ensure it is searched last. simple := name qualifier := "" dot := strings.Index(name, ".") if dot >= 0 { simple = name[0:dot] qualifier = name[dot:] } alias, found := c.aliasSet()[simple] if !found { return "", false } return alias + qualifier, true } // ContainerOption specifies a functional configuration option for a Container. // // Note, ContainerOption implementations must be able to handle nil container inputs. type ContainerOption func(*Container) (*Container, error) // Abbrevs configures a set of simple names as abbreviations for fully-qualified names. // // An abbreviation (abbrev for short) is a simple name that expands to a fully-qualified name. // Abbreviations can be useful when working with variables, functions, and especially types from // multiple namespaces: // // // CEL object construction // qual.pkg.version.ObjTypeName{ // field: alt.container.ver.FieldTypeName{value: ...} // } // // Only one the qualified names above may be used as the CEL container, so at least one of these // references must be a long qualified name within an otherwise short CEL program. Using the // following abbreviations, the program becomes much simpler: // // // CEL Go option // Abbrevs("qual.pkg.version.ObjTypeName", "alt.container.ver.FieldTypeName") // // Simplified Object construction // ObjTypeName{field: FieldTypeName{value: ...}} // // There are a few rules for the qualified names and the simple abbreviations generated from them: // - Qualified names must be dot-delimited, e.g. `package.subpkg.name`. // - The last element in the qualified name is the abbreviation. // - Abbreviations must not collide with each other. // - The abbreviation must not collide with unqualified names in use. // // Abbreviations are distinct from container-based references in the following important ways: // - Abbreviations must expand to a fully-qualified name. // - Expanded abbreviations do not participate in namespace resolution. // - Abbreviation expansion is done instead of the container search for a matching identifier. // - Containers follow C++ namespace resolution rules with searches from the most qualified name // to the least qualified name. // - Container references within the CEL program may be relative, and are resolved to fully // qualified names at either type-check time or program plan time, whichever comes first. // // If there is ever a case where an identifier could be in both the container and as an // abbreviation, the abbreviation wins as this will ensure that the meaning of a program is // preserved between compilations even as the container evolves. func Abbrevs(qualifiedNames ...string) ContainerOption { return func(c *Container) (*Container, error) { for _, qn := range qualifiedNames { ind := strings.LastIndex(qn, ".") if ind <= 0 || ind >= len(qn)-1 { return nil, fmt.Errorf( "invalid qualified name: %s, wanted name of the form 'qualified.name'", qn) } alias := qn[ind+1:] var err error c, err = aliasAs("abbreviation", qn, alias)(c) if err != nil { return nil, err } } return c, nil } } // Alias associates a fully-qualified name with a user-defined alias. // // In general, Abbrevs is preferred to Alias since the names generated from the Abbrevs option // are more easily traced back to source code. The Alias option is useful for propagating alias // configuration from one Container instance to another, and may also be useful for remapping // poorly chosen protobuf message / package names. // // Note: all of the rules that apply to Abbrevs also apply to Alias. func Alias(qualifiedName, alias string) ContainerOption { return aliasAs("alias", qualifiedName, alias) } func aliasAs(kind, qualifiedName, alias string) ContainerOption { return func(c *Container) (*Container, error) { if len(alias) == 0 || strings.Contains(alias, ".") { return nil, fmt.Errorf( "%s must be non-empty and simple (not qualified): %s=%s", kind, kind, alias) } if qualifiedName[0:1] == "." { return nil, fmt.Errorf("qualified name must not begin with a leading '.': %s", qualifiedName) } ind := strings.LastIndex(qualifiedName, ".") if ind <= 0 || ind == len(qualifiedName)-1 { return nil, fmt.Errorf("%s must refer to a valid qualified name: %s", kind, qualifiedName) } aliasRef, found := c.aliasSet()[alias] if found { return nil, fmt.Errorf( "%s collides with existing reference: name=%s, %s=%s, existing=%s", kind, qualifiedName, kind, alias, aliasRef) } if strings.HasPrefix(c.Name(), alias+".") || c.Name() == alias { return nil, fmt.Errorf( "%s collides with container name: name=%s, %s=%s, container=%s", kind, qualifiedName, kind, alias, c.Name()) } if c == nil { c = &Container{} } if c.aliases == nil { c.aliases = make(map[string]string) } c.aliases[alias] = qualifiedName return c, nil } } // Name sets the fully-qualified name of the Container. func Name(name string) ContainerOption { return func(c *Container) (*Container, error) { if len(name) > 0 && name[0:1] == "." { return nil, fmt.Errorf("container name must not contain a leading '.': %s", name) } if c.Name() == name { return c, nil } if c == nil { return &Container{name: name}, nil } c.name = name return c, nil } } // ToQualifiedName converts an expression AST into a qualified name if possible, with a boolean // 'found' value that indicates if the conversion is successful. func ToQualifiedName(e *exprpb.Expr) (string, bool) { switch e.GetExprKind().(type) { case *exprpb.Expr_IdentExpr: id := e.GetIdentExpr() return id.GetName(), true case *exprpb.Expr_SelectExpr: sel := e.GetSelectExpr() // Test only expressions are not valid as qualified names. if sel.GetTestOnly() { return "", false } if qual, found := ToQualifiedName(sel.GetOperand()); found { return qual + "." + sel.GetField(), true } } return "", false }