1// Copyright 2024 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 bpmodify 16 17import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "slices" 22 "strings" 23 24 "github.com/google/blueprint/parser" 25) 26 27// NewBlueprint returns a Blueprint for the given file contents that allows making modifications. 28func NewBlueprint(filename string, data []byte) (*Blueprint, error) { 29 r := bytes.NewReader(data) 30 file, errs := parser.Parse(filename, r) 31 if len(errs) > 0 { 32 return nil, errors.Join(errs...) 33 } 34 35 return &Blueprint{ 36 data: data, 37 bpFile: file, 38 }, nil 39} 40 41type Blueprint struct { 42 data []byte 43 bpFile *parser.File 44 modified bool 45} 46 47// Bytes returns a copy of the current, possibly modified contents of the Blueprint as a byte slice. 48func (bp *Blueprint) Bytes() ([]byte, error) { 49 if bp.modified { 50 data, err := parser.Print(bp.bpFile) 51 if err != nil { 52 return nil, err 53 } 54 return data, nil 55 } 56 return slices.Clone(bp.data), nil 57} 58 59// String returns the current, possibly modified contents of the Blueprint as a string. 60func (bp *Blueprint) String() string { 61 data, err := bp.Bytes() 62 if err != nil { 63 return err.Error() 64 } 65 return string(data) 66} 67 68// Modified returns true if any of the calls on the Blueprint caused the contents to be modified. 69func (bp *Blueprint) Modified() bool { 70 return bp.modified 71} 72 73// ModulesByName returns a ModuleSet that contains all modules with the given list of names. 74// Requesting a module that does not exist is not an error. 75func (bp *Blueprint) ModulesByName(names ...string) *ModuleSet { 76 moduleSet := &ModuleSet{ 77 bp: bp, 78 } 79 for _, def := range bp.bpFile.Defs { 80 module, ok := def.(*parser.Module) 81 if !ok { 82 continue 83 } 84 85 for _, prop := range module.Properties { 86 if prop.Name == "name" { 87 if stringValue, ok := prop.Value.(*parser.String); ok && slices.Contains(names, stringValue.Value) { 88 moduleSet.modules = append(moduleSet.modules, module) 89 } 90 } 91 } 92 } 93 94 return moduleSet 95} 96 97// AllModules returns a ModuleSet that contains all modules in the Blueprint. 98func (bp *Blueprint) AllModules() *ModuleSet { 99 moduleSet := &ModuleSet{ 100 bp: bp, 101 } 102 for _, def := range bp.bpFile.Defs { 103 module, ok := def.(*parser.Module) 104 if !ok { 105 continue 106 } 107 108 moduleSet.modules = append(moduleSet.modules, module) 109 } 110 111 return moduleSet 112} 113 114// A ModuleSet represents a set of modules in a Blueprint, and can be used to make modifications 115// the modules. 116type ModuleSet struct { 117 bp *Blueprint 118 modules []*parser.Module 119} 120 121// GetProperty returns a PropertySet that contains all properties with the given list of names 122// in all modules in the ModuleSet. Requesting properties that do not exist is not an error. 123// It returns an error for a malformed property name, or if the requested property is nested 124// in a property that is not a map. 125func (ms *ModuleSet) GetProperty(properties ...string) (*PropertySet, error) { 126 propertySet := &PropertySet{ 127 bp: ms.bp, 128 } 129 130 targetProperties, err := parseQualifiedProperties(properties) 131 if err != nil { 132 return nil, err 133 } 134 135 for _, targetProperty := range targetProperties { 136 for _, module := range ms.modules { 137 prop, _, err := getRecursiveProperty(module, targetProperty) 138 if err != nil { 139 return nil, err 140 } else if prop == nil { 141 continue 142 } 143 propertySet.properties = append(propertySet.properties, &property{ 144 property: prop, 145 module: module, 146 name: targetProperty, 147 }) 148 } 149 } 150 151 return propertySet, nil 152} 153 154// GetOrCreateProperty returns a PropertySet that contains all properties with the given list of names 155// in all modules in the ModuleSet, creating empty placeholder properties if they don't exist. 156// It returns an error for a malformed property name, or if the requested property is nested 157// in a property that is not a map. 158func (ms *ModuleSet) GetOrCreateProperty(typ Type, properties ...string) (*PropertySet, error) { 159 propertySet := &PropertySet{ 160 bp: ms.bp, 161 } 162 163 targetProperties, err := parseQualifiedProperties(properties) 164 if err != nil { 165 return nil, err 166 } 167 168 for _, targetProperty := range targetProperties { 169 for _, module := range ms.modules { 170 prop, _, err := getRecursiveProperty(module, targetProperty) 171 if err != nil { 172 return nil, err 173 } else if prop == nil { 174 prop, err = createRecursiveProperty(module, targetProperty, parser.ZeroExpression(parser.Type(typ))) 175 if err != nil { 176 return nil, err 177 } 178 ms.bp.modified = true 179 } else { 180 if prop.Value.Type() != parser.Type(typ) { 181 return nil, fmt.Errorf("unexpected type found in property %q, wanted %s, found %s", 182 targetProperty.String(), typ, prop.Value.Type()) 183 } 184 } 185 propertySet.properties = append(propertySet.properties, &property{ 186 property: prop, 187 module: module, 188 name: targetProperty, 189 }) 190 } 191 } 192 193 return propertySet, nil 194} 195 196// RemoveProperty removes the given list of properties from all modules in the ModuleSet. 197// It returns an error for a malformed property name, or if the requested property is nested 198// in a property that is not a map. Removing a property that does not exist is not an error. 199func (ms *ModuleSet) RemoveProperty(properties ...string) error { 200 targetProperties, err := parseQualifiedProperties(properties) 201 if err != nil { 202 return err 203 } 204 205 for _, targetProperty := range targetProperties { 206 for _, module := range ms.modules { 207 prop, parent, err := getRecursiveProperty(module, targetProperty) 208 if err != nil { 209 return err 210 } else if prop != nil { 211 parent.RemoveProperty(prop.Name) 212 ms.bp.modified = true 213 } 214 } 215 } 216 return nil 217} 218 219// MoveProperty moves the given list of properties to a new parent property. 220// It returns an error for a malformed property name, or if the requested property is nested 221// in a property that is not a map. Moving a property that does not exist is not an error. 222func (ms *ModuleSet) MoveProperty(newParent string, properties ...string) error { 223 targetProperties, err := parseQualifiedProperties(properties) 224 if err != nil { 225 return err 226 } 227 228 for _, targetProperty := range targetProperties { 229 for _, module := range ms.modules { 230 prop, parent, err := getRecursiveProperty(module, targetProperty) 231 if err != nil { 232 return err 233 } else if prop != nil { 234 parent.MovePropertyContents(prop.Name, newParent) 235 ms.bp.modified = true 236 } 237 } 238 } 239 return nil 240} 241 242// PropertySet represents a set of properties in a set of modules. 243type PropertySet struct { 244 bp *Blueprint 245 properties []*property 246 sortLists bool 247} 248 249type property struct { 250 property *parser.Property 251 module *parser.Module 252 name *qualifiedProperty 253} 254 255// SortListsWhenModifying causes any future modifications to lists in the PropertySet to sort 256// the lists. Otherwise, lists are only sorted if they appear to be sorted before modification. 257func (ps *PropertySet) SortListsWhenModifying() { 258 ps.sortLists = true 259} 260 261// SetString sets all properties in the PropertySet to the given string. It returns an error 262// if any of the properties are not strings. 263func (ps *PropertySet) SetString(s string) error { 264 var errs []error 265 for _, prop := range ps.properties { 266 value := prop.property.Value 267 str, ok := value.(*parser.String) 268 if !ok { 269 errs = append(errs, fmt.Errorf("expected property %s in module %s to be string, found %s", 270 prop.name, prop.module.Name(), value.Type().String())) 271 continue 272 } 273 if str.Value != s { 274 str.Value = s 275 ps.bp.modified = true 276 } 277 } 278 279 return errors.Join(errs...) 280} 281 282// SetBool sets all properties in the PropertySet to the given boolean. It returns an error 283// if any of the properties are not booleans. 284func (ps *PropertySet) SetBool(b bool) error { 285 var errs []error 286 for _, prop := range ps.properties { 287 value := prop.property.Value 288 res, ok := value.(*parser.Bool) 289 if !ok { 290 errs = append(errs, fmt.Errorf("expected property %s in module %s to be bool, found %s", 291 prop.name, prop.module.Name(), value.Type().String())) 292 continue 293 } 294 if res.Value != b { 295 res.Value = b 296 ps.bp.modified = true 297 } 298 } 299 return errors.Join(errs...) 300} 301 302// AddStringToList adds the given strings to all properties in the PropertySet. It returns an error 303// if any of the properties are not lists of strings. 304func (ps *PropertySet) AddStringToList(strs ...string) error { 305 var errs []error 306 for _, prop := range ps.properties { 307 value := prop.property.Value 308 list, ok := value.(*parser.List) 309 if !ok { 310 errs = append(errs, fmt.Errorf("expected property %s in module %s to be list, found %s", 311 prop.name, prop.module.Name(), value.Type())) 312 continue 313 } 314 wasSorted := parser.ListIsSorted(list) 315 modified := false 316 for _, s := range strs { 317 m := parser.AddStringToList(list, s) 318 modified = modified || m 319 } 320 if modified { 321 ps.bp.modified = true 322 if wasSorted || ps.sortLists { 323 parser.SortList(ps.bp.bpFile, list) 324 } 325 } 326 } 327 328 return errors.Join(errs...) 329} 330 331// RemoveStringFromList removes the given strings to all properties in the PropertySet if they are present. 332// It returns an error if any of the properties are not lists of strings. 333func (ps *PropertySet) RemoveStringFromList(strs ...string) error { 334 var errs []error 335 for _, prop := range ps.properties { 336 value := prop.property.Value 337 list, ok := value.(*parser.List) 338 if !ok { 339 errs = append(errs, fmt.Errorf("expected property %s in module %s to be list, found %s", 340 prop.name, prop.module.Name(), value.Type())) 341 continue 342 } 343 wasSorted := parser.ListIsSorted(list) 344 modified := false 345 for _, s := range strs { 346 m := parser.RemoveStringFromList(list, s) 347 modified = modified || m 348 } 349 if modified { 350 ps.bp.modified = true 351 if wasSorted || ps.sortLists { 352 parser.SortList(ps.bp.bpFile, list) 353 } 354 } 355 } 356 357 return errors.Join(errs...) 358} 359 360// AddLiteral adds the given literal blueprint snippet to all properties in the PropertySet if they are present. 361// It returns an error if any of the properties are not lists. 362func (ps *PropertySet) AddLiteral(s string) error { 363 var errs []error 364 for _, prop := range ps.properties { 365 value := prop.property.Value 366 if ps.sortLists { 367 return fmt.Errorf("sorting not supported when adding a literal") 368 } 369 list, ok := value.(*parser.List) 370 if !ok { 371 errs = append(errs, fmt.Errorf("expected property %s in module %s to be list, found %s", 372 prop.name, prop.module.Name(), value.Type().String())) 373 continue 374 } 375 value, parseErrs := parser.ParseExpression(strings.NewReader(s)) 376 if len(parseErrs) > 0 { 377 errs = append(errs, parseErrs...) 378 continue 379 } 380 list.Values = append(list.Values, value) 381 ps.bp.modified = true 382 } 383 384 return errors.Join(errs...) 385} 386 387// ReplaceStrings applies replacements to all properties in the PropertySet. It replaces all instances 388// of the strings in the keys of the given map with their corresponding values. It returns an error 389// if any of the properties are not lists of strings. 390func (ps *PropertySet) ReplaceStrings(replacements map[string]string) error { 391 var errs []error 392 for _, prop := range ps.properties { 393 value := prop.property.Value 394 if list, ok := value.(*parser.List); ok { 395 modified := parser.ReplaceStringsInList(list, replacements) 396 if modified { 397 ps.bp.modified = true 398 } 399 } else if str, ok := value.(*parser.String); ok { 400 oldVal := str.Value 401 replacementValue := replacements[oldVal] 402 if replacementValue != "" { 403 str.Value = replacementValue 404 ps.bp.modified = true 405 } 406 } else { 407 errs = append(errs, fmt.Errorf("expected property %s in module %s to be a list or string, found %s", 408 prop.name, prop.module.Name(), value.Type().String())) 409 } 410 } 411 return errors.Join(errs...) 412} 413 414func getRecursiveProperty(module *parser.Module, property *qualifiedProperty) (prop *parser.Property, 415 parent *parser.Map, err error) { 416 417 parent, err = traverseToQualifiedPropertyParent(module, property, false) 418 if err != nil { 419 return nil, nil, err 420 } 421 if parent == nil { 422 return nil, nil, nil 423 } 424 if prop, found := parent.GetProperty(property.name()); found { 425 return prop, parent, nil 426 } 427 428 return nil, nil, nil 429} 430 431func createRecursiveProperty(module *parser.Module, property *qualifiedProperty, 432 value parser.Expression) (prop *parser.Property, err error) { 433 parent, err := traverseToQualifiedPropertyParent(module, property, true) 434 if err != nil { 435 return nil, err 436 } 437 if _, found := parent.GetProperty(property.name()); found { 438 return nil, fmt.Errorf("property %q already exists", property.String()) 439 } 440 441 prop = &parser.Property{Name: property.name(), Value: value} 442 parent.Properties = append(parent.Properties, prop) 443 return prop, nil 444} 445 446func traverseToQualifiedPropertyParent(module *parser.Module, property *qualifiedProperty, 447 create bool) (parent *parser.Map, err error) { 448 m := &module.Map 449 for i, prefix := range property.prefixes() { 450 if prop, found := m.GetProperty(prefix); found { 451 if mm, ok := prop.Value.(*parser.Map); ok { 452 m = mm 453 } else { 454 // We've found a property in the AST and such property is not of type *parser.Map 455 return nil, fmt.Errorf("Expected property %q to be a map, found %s", 456 strings.Join(property.prefixes()[:i+1], "."), prop.Value.Type()) 457 } 458 } else if create { 459 mm := &parser.Map{} 460 m.Properties = append(m.Properties, &parser.Property{Name: prefix, Value: mm}) 461 m = mm 462 } else { 463 return nil, nil 464 } 465 } 466 return m, nil 467} 468 469type qualifiedProperty struct { 470 parts []string 471} 472 473func (p *qualifiedProperty) name() string { 474 return p.parts[len(p.parts)-1] 475} 476func (p *qualifiedProperty) prefixes() []string { 477 return p.parts[:len(p.parts)-1] 478} 479func (p *qualifiedProperty) String() string { 480 return strings.Join(p.parts, ".") 481} 482 483func parseQualifiedProperty(s string) (*qualifiedProperty, error) { 484 parts := strings.Split(s, ".") 485 if len(parts) == 0 { 486 return nil, fmt.Errorf("%q is not a valid property name", s) 487 } 488 for _, part := range parts { 489 if part == "" { 490 return nil, fmt.Errorf("%q is not a valid property name", s) 491 } 492 } 493 prop := qualifiedProperty{parts} 494 return &prop, nil 495 496} 497 498func parseQualifiedProperties(properties []string) ([]*qualifiedProperty, error) { 499 var qualifiedProperties []*qualifiedProperty 500 var errs []error 501 for _, property := range properties { 502 qualifiedProperty, err := parseQualifiedProperty(property) 503 if err != nil { 504 errs = append(errs, err) 505 } 506 qualifiedProperties = append(qualifiedProperties, qualifiedProperty) 507 } 508 if len(errs) > 0 { 509 return nil, errors.Join(errs...) 510 } 511 return qualifiedProperties, nil 512} 513 514type Type parser.Type 515 516var ( 517 List = Type(parser.ListType) 518 String = Type(parser.StringType) 519 Bool = Type(parser.BoolType) 520 Int64 = Type(parser.Int64Type) 521 Map = Type(parser.MapType) 522) 523 524func (t Type) String() string { 525 return parser.Type(t).String() 526} 527