1// Copyright 2014 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package blueprint 16 17import ( 18 "bytes" 19 "fmt" 20 "io" 21 "slices" 22 "strings" 23) 24 25const eof = -1 26 27var ( 28 defaultEscaper = strings.NewReplacer( 29 "\n", "$\n") 30 inputEscaper = strings.NewReplacer( 31 "\n", "$\n", 32 " ", "$ ") 33 outputEscaper = strings.NewReplacer( 34 "\n", "$\n", 35 " ", "$ ", 36 ":", "$:") 37) 38 39// ninjaString contains the parsed result of a string that can contain references to variables (e.g. $cflags) that will 40// be propagated to the build.ninja file. For literal strings with no variable references, the variables field will be 41// nil. For strings with variable references str contains the original, unparsed string, and variables contains a 42// pointer to a list of references, each with a span of bytes they should replace and a Variable interface. 43type ninjaString struct { 44 str string 45 variables *[]variableReference 46} 47 48// variableReference contains information about a single reference to a variable (e.g. $cflags) inside a parsed 49// ninjaString. start and end are int32 to reduce memory usage. A nil variable is a special case of an inserted '$' 50// at the beginning of the string to handle leading whitespace that must not be stripped by ninja. 51type variableReference struct { 52 // start is the offset of the '$' character from the beginning of the unparsed string. 53 start int32 54 55 // end is the offset of the character _after_ the final character of the variable name (or '}' if using the 56 //'${}' syntax) 57 end int32 58 59 variable Variable 60} 61 62type scope interface { 63 LookupVariable(name string) (Variable, error) 64 IsRuleVisible(rule Rule) bool 65 IsPoolVisible(pool Pool) bool 66} 67 68func simpleNinjaString(str string) *ninjaString { 69 return &ninjaString{str: str} 70} 71 72type parseState struct { 73 scope scope 74 str string 75 varStart int 76 varNameStart int 77 result *ninjaString 78} 79 80func (ps *parseState) pushVariable(start, end int, v Variable) { 81 if ps.result.variables == nil { 82 ps.result.variables = &[]variableReference{{start: int32(start), end: int32(end), variable: v}} 83 } else { 84 *ps.result.variables = append(*ps.result.variables, variableReference{start: int32(start), end: int32(end), variable: v}) 85 } 86} 87 88type stateFunc func(*parseState, int, rune) (stateFunc, error) 89 90// parseNinjaString parses an unescaped ninja string (i.e. all $<something> 91// occurrences are expected to be variables or $$) and returns a *ninjaString 92// that contains the original string and a list of the referenced variables. 93func parseNinjaString(scope scope, str string) (*ninjaString, error) { 94 ninjaString, str, err := parseNinjaOrSimpleString(scope, str) 95 if err != nil { 96 return nil, err 97 } 98 if ninjaString != nil { 99 return ninjaString, nil 100 } 101 return simpleNinjaString(str), nil 102} 103 104// parseNinjaOrSimpleString parses an unescaped ninja string (i.e. all $<something> 105// occurrences are expected to be variables or $$) and returns either a *ninjaString 106// if the string contains ninja variable references, or the original string and nil 107// for the *ninjaString if it doesn't. 108func parseNinjaOrSimpleString(scope scope, str string) (*ninjaString, string, error) { 109 // naively pre-allocate slice by counting $ signs 110 n := strings.Count(str, "$") 111 if n == 0 { 112 if len(str) > 0 && str[0] == ' ' { 113 str = "$" + str 114 } 115 return nil, str, nil 116 } 117 variableReferences := make([]variableReference, 0, n) 118 result := &ninjaString{ 119 str: str, 120 variables: &variableReferences, 121 } 122 123 parseState := &parseState{ 124 scope: scope, 125 str: str, 126 result: result, 127 } 128 129 state := parseFirstRuneState 130 var err error 131 for i := 0; i < len(str); i++ { 132 r := rune(str[i]) 133 state, err = state(parseState, i, r) 134 if err != nil { 135 return nil, "", fmt.Errorf("error parsing ninja string %q: %s", str, err) 136 } 137 } 138 139 _, err = state(parseState, len(parseState.str), eof) 140 if err != nil { 141 return nil, "", err 142 } 143 144 // All the '$' characters counted initially could have been "$$" escapes, leaving no 145 // variable references. Deallocate the variables slice if so. 146 if len(*result.variables) == 0 { 147 result.variables = nil 148 } 149 150 return result, "", nil 151} 152 153func parseFirstRuneState(state *parseState, i int, r rune) (stateFunc, error) { 154 if r == ' ' { 155 state.pushVariable(0, 1, nil) 156 } 157 return parseStringState(state, i, r) 158} 159 160func parseStringState(state *parseState, i int, r rune) (stateFunc, error) { 161 switch { 162 case r == '$': 163 state.varStart = i 164 return parseDollarStartState, nil 165 166 case r == eof: 167 return nil, nil 168 169 default: 170 return parseStringState, nil 171 } 172} 173 174func parseDollarStartState(state *parseState, i int, r rune) (stateFunc, error) { 175 switch { 176 case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', 177 r >= '0' && r <= '9', r == '_', r == '-': 178 // The beginning of a of the variable name. 179 state.varNameStart = i 180 return parseDollarState, nil 181 182 case r == '$': 183 // Just a "$$". Go back to parseStringState. 184 return parseStringState, nil 185 186 case r == '{': 187 // This is a bracketted variable name (e.g. "${blah.blah}"). 188 state.varNameStart = i + 1 189 return parseBracketsState, nil 190 191 case r == eof: 192 return nil, fmt.Errorf("unexpected end of string after '$'") 193 194 default: 195 // This was some arbitrary character following a dollar sign, 196 // which is not allowed. 197 return nil, fmt.Errorf("invalid character after '$' at byte "+ 198 "offset %d", i) 199 } 200} 201 202func parseDollarState(state *parseState, i int, r rune) (stateFunc, error) { 203 switch { 204 case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', 205 r >= '0' && r <= '9', r == '_', r == '-': 206 // A part of the variable name. Keep going. 207 return parseDollarState, nil 208 } 209 210 // The variable name has ended, output what we have. 211 v, err := state.scope.LookupVariable(state.str[state.varNameStart:i]) 212 if err != nil { 213 return nil, err 214 } 215 216 state.pushVariable(state.varStart, i, v) 217 218 switch { 219 case r == '$': 220 // A dollar after the variable name (e.g. "$blah$"). Start a new one. 221 state.varStart = i 222 return parseDollarStartState, nil 223 224 case r == eof: 225 return nil, nil 226 227 default: 228 return parseStringState, nil 229 } 230} 231 232func parseBracketsState(state *parseState, i int, r rune) (stateFunc, error) { 233 switch { 234 case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', 235 r >= '0' && r <= '9', r == '_', r == '-', r == '.': 236 // A part of the variable name. Keep going. 237 return parseBracketsState, nil 238 239 case r == '}': 240 if state.varNameStart == i { 241 // The brackets were immediately closed. That's no good. 242 return nil, fmt.Errorf("empty variable name at byte offset %d", 243 i) 244 } 245 246 // This is the end of the variable name. 247 v, err := state.scope.LookupVariable(state.str[state.varNameStart:i]) 248 if err != nil { 249 return nil, err 250 } 251 252 state.pushVariable(state.varStart, i+1, v) 253 return parseStringState, nil 254 255 case r == eof: 256 return nil, fmt.Errorf("unexpected end of string in variable name") 257 258 default: 259 // This character isn't allowed in a variable name. 260 return nil, fmt.Errorf("invalid character in variable name at "+ 261 "byte offset %d", i) 262 } 263} 264 265// parseNinjaStrings converts a list of strings to *ninjaStrings by finding the references 266// to ninja variables contained in the strings. 267func parseNinjaStrings(scope scope, strs []string) ([]*ninjaString, 268 error) { 269 270 if len(strs) == 0 { 271 return nil, nil 272 } 273 result := make([]*ninjaString, len(strs)) 274 for i, str := range strs { 275 ninjaStr, err := parseNinjaString(scope, str) 276 if err != nil { 277 return nil, fmt.Errorf("error parsing element %d: %s", i, err) 278 } 279 result[i] = ninjaStr 280 } 281 return result, nil 282} 283 284// parseNinjaOrSimpleStrings splits a list of strings into *ninjaStrings if they have ninja 285// variable references or a list of strings if they don't. If none of the input strings contain 286// ninja variable references (a very common case) then it returns the unmodified input slice as 287// the output slice. 288func parseNinjaOrSimpleStrings(scope scope, strs []string) ([]*ninjaString, []string, error) { 289 if len(strs) == 0 { 290 return nil, strs, nil 291 } 292 293 // allSimpleStrings is true until the first time a string with ninja variable references is found. 294 allSimpleStrings := true 295 var simpleStrings []string 296 var ninjaStrings []*ninjaString 297 298 for i, str := range strs { 299 ninjaStr, simpleStr, err := parseNinjaOrSimpleString(scope, str) 300 if err != nil { 301 return nil, nil, fmt.Errorf("error parsing element %d: %s", i, err) 302 } else if ninjaStr != nil { 303 ninjaStrings = append(ninjaStrings, ninjaStr) 304 if allSimpleStrings && i > 0 { 305 // If all previous strings had no ninja variable references then they weren't copied into 306 // simpleStrings to avoid allocating it if the input slice is reused as the output. Allocate 307 // simpleStrings and copy all the previous strings into it. 308 simpleStrings = make([]string, i, len(strs)) 309 copy(simpleStrings, strs[:i]) 310 } 311 allSimpleStrings = false 312 } else { 313 if !allSimpleStrings { 314 // Only copy into the output slice if at least one string with ninja variable references 315 // was found. Skipped strings will be copied the first time a string with ninja variable 316 // is found. 317 simpleStrings = append(simpleStrings, simpleStr) 318 } 319 } 320 } 321 if allSimpleStrings { 322 // None of the input strings had ninja variable references, return the input slice as the output. 323 return nil, strs, nil 324 } 325 return ninjaStrings, simpleStrings, nil 326} 327 328func (n *ninjaString) Value(nameTracker *nameTracker) string { 329 if n.variables == nil || len(*n.variables) == 0 { 330 return defaultEscaper.Replace(n.str) 331 } 332 str := &strings.Builder{} 333 n.ValueWithEscaper(str, nameTracker, defaultEscaper) 334 return str.String() 335} 336 337func (n *ninjaString) ValueWithEscaper(w io.StringWriter, nameTracker *nameTracker, escaper *strings.Replacer) { 338 339 if n.variables == nil || len(*n.variables) == 0 { 340 w.WriteString(escaper.Replace(n.str)) 341 return 342 } 343 344 i := 0 345 for _, v := range *n.variables { 346 w.WriteString(escaper.Replace(n.str[i:v.start])) 347 if v.variable == nil { 348 w.WriteString("$ ") 349 } else { 350 w.WriteString("${") 351 w.WriteString(nameTracker.Variable(v.variable)) 352 w.WriteString("}") 353 } 354 i = int(v.end) 355 } 356 w.WriteString(escaper.Replace(n.str[i:len(n.str)])) 357} 358 359func (n *ninjaString) Eval(variables map[Variable]*ninjaString) (string, error) { 360 if n.variables == nil || len(*n.variables) == 0 { 361 return n.str, nil 362 } 363 364 w := &strings.Builder{} 365 i := 0 366 for _, v := range *n.variables { 367 w.WriteString(n.str[i:v.start]) 368 if v.variable == nil { 369 w.WriteString(" ") 370 } else { 371 variable, ok := variables[v.variable] 372 if !ok { 373 return "", fmt.Errorf("no such global variable: %s", v.variable) 374 } 375 value, err := variable.Eval(variables) 376 if err != nil { 377 return "", err 378 } 379 w.WriteString(value) 380 } 381 i = int(v.end) 382 } 383 w.WriteString(n.str[i:len(n.str)]) 384 return w.String(), nil 385} 386 387func (n *ninjaString) Variables() []Variable { 388 if n.variables == nil || len(*n.variables) == 0 { 389 return nil 390 } 391 392 variables := make([]Variable, 0, len(*n.variables)) 393 for _, v := range *n.variables { 394 if v.variable != nil { 395 variables = append(variables, v.variable) 396 } 397 } 398 return variables 399} 400 401func validateNinjaName(name string) error { 402 for i, r := range name { 403 valid := (r >= 'a' && r <= 'z') || 404 (r >= 'A' && r <= 'Z') || 405 (r >= '0' && r <= '9') || 406 (r == '_') || 407 (r == '-') || 408 (r == '.') 409 if !valid { 410 411 return fmt.Errorf("%q contains an invalid Ninja name character "+ 412 "%q at byte offset %d", name, r, i) 413 } 414 } 415 return nil 416} 417 418func toNinjaName(name string) string { 419 ret := bytes.Buffer{} 420 ret.Grow(len(name)) 421 for _, r := range name { 422 valid := (r >= 'a' && r <= 'z') || 423 (r >= 'A' && r <= 'Z') || 424 (r >= '0' && r <= '9') || 425 (r == '_') || 426 (r == '-') || 427 (r == '.') 428 if valid { 429 ret.WriteRune(r) 430 } else { 431 // TODO(jeffrygaston): do escaping so that toNinjaName won't ever output duplicate 432 // names for two different input names 433 ret.WriteRune('_') 434 } 435 } 436 437 return ret.String() 438} 439 440var builtinRuleArgs = []string{"out", "in"} 441 442func validateArgName(argName string) error { 443 err := validateNinjaName(argName) 444 if err != nil { 445 return err 446 } 447 448 // We only allow globals within the rule's package to be used as rule 449 // arguments. A global in another package can always be mirrored into 450 // the rule's package by defining a new variable, so this doesn't limit 451 // what's possible. This limitation prevents situations where a Build 452 // invocation in another package must use the rule-defining package's 453 // import name for a 3rd package in order to set the rule's arguments. 454 if strings.ContainsRune(argName, '.') { 455 return fmt.Errorf("%q contains a '.' character", argName) 456 } 457 458 if argName == "tags" { 459 return fmt.Errorf("\"tags\" is a reserved argument name") 460 } 461 462 for _, builtin := range builtinRuleArgs { 463 if argName == builtin { 464 return fmt.Errorf("%q conflicts with Ninja built-in", argName) 465 } 466 } 467 468 return nil 469} 470 471func validateArgNames(argNames []string) error { 472 for _, argName := range argNames { 473 err := validateArgName(argName) 474 if err != nil { 475 return err 476 } 477 } 478 479 return nil 480} 481 482func ninjaStringsEqual(a, b *ninjaString) bool { 483 return a.str == b.str && 484 (a.variables == nil) == (b.variables == nil) && 485 (a.variables == nil || 486 slices.Equal(*a.variables, *b.variables)) 487} 488