// Copyright 2014 Google Inc. All rights reserved. // // 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 blueprint import ( "bytes" "fmt" "io" "strings" ) const eof = -1 var ( defaultEscaper = strings.NewReplacer( "\n", "$\n") inputEscaper = strings.NewReplacer( "\n", "$\n", " ", "$ ") outputEscaper = strings.NewReplacer( "\n", "$\n", " ", "$ ", ":", "$:") ) type ninjaString interface { Value(pkgNames map[*packageContext]string) string ValueWithEscaper(w io.StringWriter, pkgNames map[*packageContext]string, escaper *strings.Replacer) Eval(variables map[Variable]ninjaString) (string, error) Variables() []Variable } type varNinjaString struct { strings []string variables []Variable } type literalNinjaString string type scope interface { LookupVariable(name string) (Variable, error) IsRuleVisible(rule Rule) bool IsPoolVisible(pool Pool) bool } func simpleNinjaString(str string) ninjaString { return literalNinjaString(str) } type parseState struct { scope scope str string pendingStr string stringStart int varStart int result *varNinjaString } func (ps *parseState) pushVariable(v Variable) { if len(ps.result.variables) == len(ps.result.strings) { // Last push was a variable, we need a blank string separator ps.result.strings = append(ps.result.strings, "") } if ps.pendingStr != "" { panic("oops, pushed variable with pending string") } ps.result.variables = append(ps.result.variables, v) } func (ps *parseState) pushString(s string) { if len(ps.result.strings) != len(ps.result.variables) { panic("oops, pushed string after string") } ps.result.strings = append(ps.result.strings, ps.pendingStr+s) ps.pendingStr = "" } type stateFunc func(*parseState, int, rune) (stateFunc, error) // parseNinjaString parses an unescaped ninja string (i.e. all $ // occurrences are expected to be variables or $$) and returns a list of the // variable names that the string references. func parseNinjaString(scope scope, str string) (ninjaString, error) { // naively pre-allocate slices by counting $ signs n := strings.Count(str, "$") if n == 0 { if strings.HasPrefix(str, " ") { str = "$" + str } return literalNinjaString(str), nil } result := &varNinjaString{ strings: make([]string, 0, n+1), variables: make([]Variable, 0, n), } parseState := &parseState{ scope: scope, str: str, result: result, } state := parseFirstRuneState var err error for i := 0; i < len(str); i++ { r := rune(str[i]) state, err = state(parseState, i, r) if err != nil { return nil, fmt.Errorf("error parsing ninja string %q: %s", str, err) } } _, err = state(parseState, len(parseState.str), eof) if err != nil { return nil, err } return result, nil } func parseFirstRuneState(state *parseState, i int, r rune) (stateFunc, error) { if r == ' ' { state.pendingStr += "$" } return parseStringState(state, i, r) } func parseStringState(state *parseState, i int, r rune) (stateFunc, error) { switch { case r == '$': state.varStart = i + 1 return parseDollarStartState, nil case r == eof: state.pushString(state.str[state.stringStart:i]) return nil, nil default: return parseStringState, nil } } func parseDollarStartState(state *parseState, i int, r rune) (stateFunc, error) { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-': // The beginning of a of the variable name. Output the string and // keep going. state.pushString(state.str[state.stringStart : i-1]) return parseDollarState, nil case r == '$': // Just a "$$". Go back to parseStringState without changing // state.stringStart. return parseStringState, nil case r == '{': // This is a bracketted variable name (e.g. "${blah.blah}"). Output // the string and keep going. state.pushString(state.str[state.stringStart : i-1]) state.varStart = i + 1 return parseBracketsState, nil case r == eof: return nil, fmt.Errorf("unexpected end of string after '$'") default: // This was some arbitrary character following a dollar sign, // which is not allowed. return nil, fmt.Errorf("invalid character after '$' at byte "+ "offset %d", i) } } func parseDollarState(state *parseState, i int, r rune) (stateFunc, error) { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-': // A part of the variable name. Keep going. return parseDollarState, nil case r == '$': // A dollar after the variable name (e.g. "$blah$"). Output the // variable we have and start a new one. v, err := state.scope.LookupVariable(state.str[state.varStart:i]) if err != nil { return nil, err } state.pushVariable(v) state.varStart = i + 1 state.stringStart = i return parseDollarStartState, nil case r == eof: // This is the end of the variable name. v, err := state.scope.LookupVariable(state.str[state.varStart:i]) if err != nil { return nil, err } state.pushVariable(v) // We always end with a string, even if it's an empty one. state.pushString("") return nil, nil default: // We've just gone past the end of the variable name, so record what // we have. v, err := state.scope.LookupVariable(state.str[state.varStart:i]) if err != nil { return nil, err } state.pushVariable(v) state.stringStart = i return parseStringState, nil } } func parseBracketsState(state *parseState, i int, r rune) (stateFunc, error) { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-', r == '.': // A part of the variable name. Keep going. return parseBracketsState, nil case r == '}': if state.varStart == i { // The brackets were immediately closed. That's no good. return nil, fmt.Errorf("empty variable name at byte offset %d", i) } // This is the end of the variable name. v, err := state.scope.LookupVariable(state.str[state.varStart:i]) if err != nil { return nil, err } state.pushVariable(v) state.stringStart = i + 1 return parseStringState, nil case r == eof: return nil, fmt.Errorf("unexpected end of string in variable name") default: // This character isn't allowed in a variable name. return nil, fmt.Errorf("invalid character in variable name at "+ "byte offset %d", i) } } func parseNinjaStrings(scope scope, strs []string) ([]ninjaString, error) { if len(strs) == 0 { return nil, nil } result := make([]ninjaString, len(strs)) for i, str := range strs { ninjaStr, err := parseNinjaString(scope, str) if err != nil { return nil, fmt.Errorf("error parsing element %d: %s", i, err) } result[i] = ninjaStr } return result, nil } func (n varNinjaString) Value(pkgNames map[*packageContext]string) string { if len(n.strings) == 1 { return defaultEscaper.Replace(n.strings[0]) } str := &strings.Builder{} n.ValueWithEscaper(str, pkgNames, defaultEscaper) return str.String() } func (n varNinjaString) ValueWithEscaper(w io.StringWriter, pkgNames map[*packageContext]string, escaper *strings.Replacer) { w.WriteString(escaper.Replace(n.strings[0])) for i, v := range n.variables { w.WriteString("${") w.WriteString(v.fullName(pkgNames)) w.WriteString("}") w.WriteString(escaper.Replace(n.strings[i+1])) } } func (n varNinjaString) Eval(variables map[Variable]ninjaString) (string, error) { str := n.strings[0] for i, v := range n.variables { variable, ok := variables[v] if !ok { return "", fmt.Errorf("no such global variable: %s", v) } value, err := variable.Eval(variables) if err != nil { return "", err } str += value + n.strings[i+1] } return str, nil } func (n varNinjaString) Variables() []Variable { return n.variables } func (l literalNinjaString) Value(_ map[*packageContext]string) string { return defaultEscaper.Replace(string(l)) } func (l literalNinjaString) ValueWithEscaper(w io.StringWriter, _ map[*packageContext]string, escaper *strings.Replacer) { w.WriteString(escaper.Replace(string(l))) } func (l literalNinjaString) Eval(variables map[Variable]ninjaString) (string, error) { return string(l), nil } func (l literalNinjaString) Variables() []Variable { return nil } func validateNinjaName(name string) error { for i, r := range name { valid := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || (r == '_') || (r == '-') || (r == '.') if !valid { return fmt.Errorf("%q contains an invalid Ninja name character "+ "%q at byte offset %d", name, r, i) } } return nil } func toNinjaName(name string) string { ret := bytes.Buffer{} ret.Grow(len(name)) for _, r := range name { valid := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || (r == '_') || (r == '-') || (r == '.') if valid { ret.WriteRune(r) } else { // TODO(jeffrygaston): do escaping so that toNinjaName won't ever output duplicate // names for two different input names ret.WriteRune('_') } } return ret.String() } var builtinRuleArgs = []string{"out", "in"} func validateArgName(argName string) error { err := validateNinjaName(argName) if err != nil { return err } // We only allow globals within the rule's package to be used as rule // arguments. A global in another package can always be mirrored into // the rule's package by defining a new variable, so this doesn't limit // what's possible. This limitation prevents situations where a Build // invocation in another package must use the rule-defining package's // import name for a 3rd package in order to set the rule's arguments. if strings.ContainsRune(argName, '.') { return fmt.Errorf("%q contains a '.' character", argName) } for _, builtin := range builtinRuleArgs { if argName == builtin { return fmt.Errorf("%q conflicts with Ninja built-in", argName) } } return nil } func validateArgNames(argNames []string) error { for _, argName := range argNames { err := validateArgName(argName) if err != nil { return err } } return nil }