// 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", " ", "$ ", ":", "$:") ) // ninjaString contains the parsed result of a string that can contain references to variables (e.g. $cflags) that will // be propagated to the build.ninja file. For literal strings with no variable references, the variables field will be // nil. For strings with variable references str contains the original, unparsed string, and variables contains a // pointer to a list of references, each with a span of bytes they should replace and a Variable interface. type ninjaString struct { str string variables *[]variableReference } // variableReference contains information about a single reference to a variable (e.g. $cflags) inside a parsed // ninjaString. start and end are int32 to reduce memory usage. A nil variable is a special case of an inserted '$' // at the beginning of the string to handle leading whitespace that must not be stripped by ninja. type variableReference struct { // start is the offset of the '$' character from the beginning of the unparsed string. start int32 // end is the offset of the character _after_ the final character of the variable name (or '}' if using the //'${}' syntax) end int32 variable Variable } type scope interface { LookupVariable(name string) (Variable, error) IsRuleVisible(rule Rule) bool IsPoolVisible(pool Pool) bool } func simpleNinjaString(str string) *ninjaString { return &ninjaString{str: str} } type parseState struct { scope scope str string varStart int varNameStart int result *ninjaString } func (ps *parseState) pushVariable(start, end int, v Variable) { if ps.result.variables == nil { ps.result.variables = &[]variableReference{{start: int32(start), end: int32(end), variable: v}} } else { *ps.result.variables = append(*ps.result.variables, variableReference{start: int32(start), end: int32(end), variable: v}) } } 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 *ninjaString // that contains the original string and a list of the referenced variables. func parseNinjaString(scope scope, str string) (*ninjaString, error) { ninjaString, str, err := parseNinjaOrSimpleString(scope, str) if err != nil { return nil, err } if ninjaString != nil { return ninjaString, nil } return simpleNinjaString(str), nil } // parseNinjaOrSimpleString parses an unescaped ninja string (i.e. all $ // occurrences are expected to be variables or $$) and returns either a *ninjaString // if the string contains ninja variable references, or the original string and nil // for the *ninjaString if it doesn't. func parseNinjaOrSimpleString(scope scope, str string) (*ninjaString, string, error) { // naively pre-allocate slice by counting $ signs n := strings.Count(str, "$") if n == 0 { if len(str) > 0 && str[0] == ' ' { str = "$" + str } return nil, str, nil } variableReferences := make([]variableReference, 0, n) result := &ninjaString{ str: str, variables: &variableReferences, } 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 } // All the '$' characters counted initially could have been "$$" escapes, leaving no // variable references. Deallocate the variables slice if so. if len(*result.variables) == 0 { result.variables = nil } return result, "", nil } func parseFirstRuneState(state *parseState, i int, r rune) (stateFunc, error) { if r == ' ' { state.pushVariable(0, 1, nil) } return parseStringState(state, i, r) } func parseStringState(state *parseState, i int, r rune) (stateFunc, error) { switch { case r == '$': state.varStart = i return parseDollarStartState, nil case r == eof: 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. state.varNameStart = i return parseDollarState, nil case r == '$': // Just a "$$". Go back to parseStringState. return parseStringState, nil case r == '{': // This is a bracketted variable name (e.g. "${blah.blah}"). state.varNameStart = 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 } // The variable name has ended, output what we have. v, err := state.scope.LookupVariable(state.str[state.varNameStart:i]) if err != nil { return nil, err } state.pushVariable(state.varStart, i, v) switch { case r == '$': // A dollar after the variable name (e.g. "$blah$"). Start a new one. state.varStart = i return parseDollarStartState, nil case r == eof: return nil, nil default: 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.varNameStart == 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.varNameStart:i]) if err != nil { return nil, err } state.pushVariable(state.varStart, i+1, v) 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) } } // parseNinjaStrings converts a list of strings to *ninjaStrings by finding the references // to ninja variables contained in the strings. 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 } // parseNinjaOrSimpleStrings splits a list of strings into *ninjaStrings if they have ninja // variable references or a list of strings if they don't. If none of the input strings contain // ninja variable references (a very common case) then it returns the unmodified input slice as // the output slice. func parseNinjaOrSimpleStrings(scope scope, strs []string) ([]*ninjaString, []string, error) { if len(strs) == 0 { return nil, strs, nil } // allSimpleStrings is true until the first time a string with ninja variable references is found. allSimpleStrings := true var simpleStrings []string var ninjaStrings []*ninjaString for i, str := range strs { ninjaStr, simpleStr, err := parseNinjaOrSimpleString(scope, str) if err != nil { return nil, nil, fmt.Errorf("error parsing element %d: %s", i, err) } else if ninjaStr != nil { ninjaStrings = append(ninjaStrings, ninjaStr) if allSimpleStrings && i > 0 { // If all previous strings had no ninja variable references then they weren't copied into // simpleStrings to avoid allocating it if the input slice is reused as the output. Allocate // simpleStrings and copy all the previous strings into it. simpleStrings = make([]string, i, len(strs)) copy(simpleStrings, strs[:i]) } allSimpleStrings = false } else { if !allSimpleStrings { // Only copy into the output slice if at least one string with ninja variable references // was found. Skipped strings will be copied the first time a string with ninja variable // is found. simpleStrings = append(simpleStrings, simpleStr) } } } if allSimpleStrings { // None of the input strings had ninja variable references, return the input slice as the output. return nil, strs, nil } return ninjaStrings, simpleStrings, nil } func (n *ninjaString) Value(pkgNames map[*packageContext]string) string { if n.variables == nil || len(*n.variables) == 0 { return defaultEscaper.Replace(n.str) } str := &strings.Builder{} n.ValueWithEscaper(str, pkgNames, defaultEscaper) return str.String() } func (n *ninjaString) ValueWithEscaper(w io.StringWriter, pkgNames map[*packageContext]string, escaper *strings.Replacer) { if n.variables == nil || len(*n.variables) == 0 { w.WriteString(escaper.Replace(n.str)) return } i := 0 for _, v := range *n.variables { w.WriteString(escaper.Replace(n.str[i:v.start])) if v.variable == nil { w.WriteString("$ ") } else { w.WriteString("${") w.WriteString(v.variable.fullName(pkgNames)) w.WriteString("}") } i = int(v.end) } w.WriteString(escaper.Replace(n.str[i:len(n.str)])) } func (n *ninjaString) Eval(variables map[Variable]*ninjaString) (string, error) { if n.variables == nil || len(*n.variables) == 0 { return n.str, nil } w := &strings.Builder{} i := 0 for _, v := range *n.variables { w.WriteString(n.str[i:v.start]) if v.variable == nil { w.WriteString(" ") } else { variable, ok := variables[v.variable] if !ok { return "", fmt.Errorf("no such global variable: %s", v.variable) } value, err := variable.Eval(variables) if err != nil { return "", err } w.WriteString(value) } i = int(v.end) } w.WriteString(n.str[i:len(n.str)]) return w.String(), nil } func (n *ninjaString) Variables() []Variable { if n.variables == nil || len(*n.variables) == 0 { return nil } variables := make([]Variable, 0, len(*n.variables)) for _, v := range *n.variables { if v.variable != nil { variables = append(variables, v.variable) } } return variables } 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) } if argName == "tags" { return fmt.Errorf("\"tags\" is a reserved argument name") } 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 }