| // Copyright 2020 CUE Authors |
| // |
| // 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 load |
| |
| import ( |
| "crypto/rand" |
| "encoding/hex" |
| "os" |
| "os/user" |
| "runtime" |
| "strings" |
| "time" |
| |
| "cuelang.org/go/cue" |
| "cuelang.org/go/cue/ast" |
| "cuelang.org/go/cue/build" |
| "cuelang.org/go/cue/errors" |
| "cuelang.org/go/cue/parser" |
| "cuelang.org/go/cue/token" |
| "cuelang.org/go/internal" |
| "cuelang.org/go/internal/cli" |
| ) |
| |
| // A TagVar represents an injection variable. |
| type TagVar struct { |
| // Func returns an ast for a tag variable. It is only called once |
| // per evaluation of a configuration. |
| Func func() (ast.Expr, error) |
| |
| // Description documents this TagVar. |
| Description string |
| } |
| |
| const rfc3339 = "2006-01-02T15:04:05.999999999Z" |
| |
| // DefaultTagVars creates a new map with a set of supported injection variables. |
| func DefaultTagVars() map[string]TagVar { |
| return map[string]TagVar{ |
| "now": { |
| Func: func() (ast.Expr, error) { |
| return ast.NewString(time.Now().UTC().Format(rfc3339)), nil |
| }, |
| }, |
| "os": { |
| Func: func() (ast.Expr, error) { |
| return ast.NewString(runtime.GOOS), nil |
| }, |
| }, |
| "cwd": { |
| Func: func() (ast.Expr, error) { |
| return varToString(os.Getwd()) |
| }, |
| }, |
| "username": { |
| Func: func() (ast.Expr, error) { |
| u, err := user.Current() |
| return varToString(u.Username, err) |
| }, |
| }, |
| "hostname": { |
| Func: func() (ast.Expr, error) { |
| return varToString(os.Hostname()) |
| }, |
| }, |
| "rand": { |
| Func: func() (ast.Expr, error) { |
| var b [16]byte |
| _, err := rand.Read(b[:]) |
| if err != nil { |
| return nil, err |
| } |
| var hx [34]byte |
| hx[0] = '0' |
| hx[1] = 'x' |
| hex.Encode(hx[2:], b[:]) |
| return ast.NewLit(token.INT, string(hx[:])), nil |
| }, |
| }, |
| } |
| } |
| |
| func varToString(s string, err error) (ast.Expr, error) { |
| if err != nil { |
| return nil, err |
| } |
| x := ast.NewString(s) |
| return x, nil |
| } |
| |
| // A tag binds an identifier to a field to allow passing command-line values. |
| // |
| // A tag is of the form |
| // @tag(<name>,[type=(string|int|number|bool)][,short=<shorthand>+]) |
| // |
| // The name is mandatory and type defaults to string. Tags are set using the -t |
| // option on the command line. -t name=value will parse value for the type |
| // defined for name and set the field for which this tag was defined to this |
| // value. A tag may be associated with multiple fields. |
| // |
| // Tags also allow shorthands. If a shorthand bar is declared for a tag with |
| // name foo, then -t bar is identical to -t foo=bar. |
| // |
| // It is a deliberate choice to not allow other values to be associated with |
| // shorthands than the shorthand name itself. Doing so would create a powerful |
| // mechanism that would assign different values to different fields based on the |
| // same shorthand, duplicating functionality that is already available in CUE. |
| type tag struct { |
| key string |
| kind cue.Kind |
| shorthands []string |
| vars string // -T flag |
| hasReplacement bool |
| |
| field *ast.Field |
| } |
| |
| func parseTag(pos token.Pos, body string) (t *tag, err errors.Error) { |
| t = &tag{} |
| t.kind = cue.StringKind |
| |
| a := internal.ParseAttrBody(pos, body) |
| |
| t.key, _ = a.String(0) |
| if !ast.IsValidIdent(t.key) { |
| return t, errors.Newf(pos, "invalid identifier %q", t.key) |
| } |
| |
| if s, ok, _ := a.Lookup(1, "type"); ok { |
| switch s { |
| case "string": |
| case "int": |
| t.kind = cue.IntKind |
| case "number": |
| t.kind = cue.NumberKind |
| case "bool": |
| t.kind = cue.BoolKind |
| default: |
| return t, errors.Newf(pos, "invalid type %q", s) |
| } |
| } |
| |
| if s, ok, _ := a.Lookup(1, "short"); ok { |
| for _, s := range strings.Split(s, "|") { |
| if !ast.IsValidIdent(t.key) { |
| return t, errors.Newf(pos, "invalid identifier %q", s) |
| } |
| t.shorthands = append(t.shorthands, s) |
| } |
| } |
| |
| if s, ok, _ := a.Lookup(1, "var"); ok { |
| t.vars = s |
| } |
| |
| return t, nil |
| } |
| |
| func (t *tag) inject(value string, l *loader) errors.Error { |
| e, err := cli.ParseValue(token.NoPos, t.key, value, t.kind) |
| t.injectValue(e, l) |
| return err |
| } |
| |
| func (t *tag) injectValue(x ast.Expr, l *loader) { |
| injected := ast.NewBinExpr(token.AND, t.field.Value, x) |
| if l.replacements == nil { |
| l.replacements = map[ast.Node]ast.Node{} |
| } |
| l.replacements[t.field.Value] = injected |
| t.field.Value = injected |
| t.hasReplacement = true |
| } |
| |
| // findTags defines which fields may be associated with tags. |
| // |
| // TODO: should we limit the depth at which tags may occur? |
| func findTags(b *build.Instance) (tags []*tag, errs errors.Error) { |
| findInvalidTags := func(x ast.Node, msg string) { |
| ast.Walk(x, nil, func(n ast.Node) { |
| if f, ok := n.(*ast.Field); ok { |
| for _, a := range f.Attrs { |
| if key, _ := a.Split(); key == "tag" { |
| errs = errors.Append(errs, errors.Newf(a.Pos(), msg)) |
| // TODO: add position of x. |
| } |
| } |
| } |
| }) |
| } |
| for _, f := range b.Files { |
| ast.Walk(f, func(n ast.Node) bool { |
| switch x := n.(type) { |
| case *ast.ListLit: |
| findInvalidTags(n, "@tag not allowed within lists") |
| return false |
| |
| case *ast.Comprehension: |
| findInvalidTags(n, "@tag not allowed within comprehension") |
| return false |
| |
| case *ast.Field: |
| // TODO: allow optional fields? |
| _, _, err := ast.LabelName(x.Label) |
| if err != nil || x.Optional != token.NoPos { |
| findInvalidTags(n, "@tag not allowed within optional fields") |
| return false |
| } |
| |
| for _, a := range x.Attrs { |
| key, body := a.Split() |
| if key != "tag" { |
| continue |
| } |
| t, err := parseTag(a.Pos(), body) |
| if err != nil { |
| errs = errors.Append(errs, err) |
| continue |
| } |
| t.field = x |
| tags = append(tags, t) |
| } |
| } |
| return true |
| }, nil) |
| } |
| return tags, errs |
| } |
| |
| func injectTags(tags []string, l *loader) errors.Error { |
| // Parses command line args |
| for _, s := range tags { |
| p := strings.Index(s, "=") |
| found := l.buildTags[s] |
| if p > 0 { // key-value |
| for _, t := range l.tags { |
| if t.key == s[:p] { |
| found = true |
| if err := t.inject(s[p+1:], l); err != nil { |
| return err |
| } |
| } |
| } |
| if !found { |
| return errors.Newf(token.NoPos, "no tag for %q", s[:p]) |
| } |
| } else { // shorthand |
| for _, t := range l.tags { |
| for _, sh := range t.shorthands { |
| if sh == s { |
| found = true |
| if err := t.inject(s, l); err != nil { |
| return err |
| } |
| } |
| } |
| } |
| if !found { |
| return errors.Newf(token.NoPos, "tag %q not used in any file", s) |
| } |
| } |
| } |
| |
| if l.cfg.TagVars != nil { |
| vars := map[string]ast.Expr{} |
| |
| // Inject tag variables if the tag wasn't already set. |
| for _, t := range l.tags { |
| if t.hasReplacement || t.vars == "" { |
| continue |
| } |
| x, ok := vars[t.vars] |
| if !ok { |
| tv, ok := l.cfg.TagVars[t.vars] |
| if !ok { |
| return errors.Newf(token.NoPos, |
| "tag variable '%s' not found", t.vars) |
| } |
| tag, err := tv.Func() |
| if err != nil { |
| return errors.Wrapf(err, token.NoPos, |
| "error getting tag variable '%s'", t.vars) |
| } |
| x = tag |
| vars[t.vars] = tag |
| } |
| if x != nil { |
| t.injectValue(x, l) |
| } |
| } |
| } |
| return nil |
| } |
| |
| // shouldBuildFile determines whether a File should be included based on its |
| // attributes. |
| func shouldBuildFile(f *ast.File, fp *fileProcessor) errors.Error { |
| tags := fp.c.Tags |
| |
| a, errs := getBuildAttr(f) |
| if errs != nil { |
| return errs |
| } |
| if a == nil { |
| return nil |
| } |
| |
| _, body := a.Split() |
| |
| expr, err := parser.ParseExpr("", body) |
| if err != nil { |
| return errors.Promote(err, "") |
| } |
| |
| tagMap := map[string]bool{} |
| for _, t := range tags { |
| tagMap[t] = !strings.ContainsRune(t, '=') |
| } |
| |
| c := checker{tags: tagMap, loader: fp.c.loader} |
| include := c.shouldInclude(expr) |
| if c.err != nil { |
| return c.err |
| } |
| if !include { |
| return excludeError{errors.Newf(a.Pos(), "@if(%s) did not match", body)} |
| } |
| return nil |
| } |
| |
| func getBuildAttr(f *ast.File) (*ast.Attribute, errors.Error) { |
| var a *ast.Attribute |
| for _, d := range f.Decls { |
| switch x := d.(type) { |
| case *ast.Attribute: |
| key, _ := x.Split() |
| if key != "if" { |
| continue |
| } |
| if a != nil { |
| err := errors.Newf(d.Pos(), "multiple @if attributes") |
| err = errors.Append(err, |
| errors.Newf(a.Pos(), "previous declaration here")) |
| return nil, err |
| } |
| a = x |
| |
| case *ast.Package: |
| break |
| } |
| } |
| return a, nil |
| } |
| |
| type checker struct { |
| loader *loader |
| tags map[string]bool |
| err errors.Error |
| } |
| |
| func (c *checker) shouldInclude(expr ast.Expr) bool { |
| switch x := expr.(type) { |
| case *ast.Ident: |
| c.loader.buildTags[x.Name] = true |
| return c.tags[x.Name] |
| |
| case *ast.BinaryExpr: |
| switch x.Op { |
| case token.LAND: |
| return c.shouldInclude(x.X) && c.shouldInclude(x.Y) |
| |
| case token.LOR: |
| return c.shouldInclude(x.X) || c.shouldInclude(x.Y) |
| |
| default: |
| c.err = errors.Append(c.err, errors.Newf(token.NoPos, |
| "invalid operator %v", x.Op)) |
| return false |
| } |
| |
| case *ast.UnaryExpr: |
| if x.Op != token.NOT { |
| c.err = errors.Append(c.err, errors.Newf(token.NoPos, |
| "invalid operator %v", x.Op)) |
| } |
| return !c.shouldInclude(x.X) |
| |
| default: |
| c.err = errors.Append(c.err, errors.Newf(token.NoPos, |
| "invalid type %T in build attribute", expr)) |
| return false |
| } |
| } |