package resource import ( "bytes" "errors" "fmt" "strconv" "strings" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/pulumi/pulumi/sdk/v3/go/common/util/logging" ) // PropertyPath represents a path to a nested property. The path may be composed of strings (which access properties // in ObjectProperty values) and integers (which access elements of ArrayProperty values). type PropertyPath []interface{} // ParsePropertyPath parses a property path into a PropertyPath value. // // A property path string is essentially a Javascript property access expression in which all elements are literals. // Valid property paths obey the following EBNF-ish grammar: // // propertyName := [a-zA-Z_$] { [a-zA-Z0-9_$] } // quotedPropertyName := '"' ( '\' '"' | [^"] ) { ( '\' '"' | [^"] ) } '"' // arrayIndex := { [0-9] } // // propertyIndex := '[' ( quotedPropertyName | arrayIndex ) ']' // rootProperty := ( propertyName | propertyIndex ) // propertyAccessor := ( ( '.' propertyName ) | propertyIndex ) // path := rootProperty { propertyAccessor } // // Examples of valid paths: // - root // - root.nested // - root["nested"] // - root.double.nest // - root["double"].nest // - root["double"]["nest"] // - root.array[0] // - root.array[100] // - root.array[0].nested // - root.array[0][1].nested // - root.nested.array[0].double[1] // - root["key with \"escaped\" quotes"] // - root["key with a ."] // - ["root key with \"escaped\" quotes"].nested // - ["root key with a ."][100] // - root.array[*].field // - root.array["*"].field func ParsePropertyPath(path string) (PropertyPath, error) { // We interpret the grammar above a little loosely in order to keep things simple. Specifically, we will accept // something close to the following: // pathElement := { '.' } [a-zA-Z_$][a-zA-Z0-9_$] // pathIndex := '[' ( [0-9]+ | '"' ('\' '"' | [^"] )+ '"' ']' // path := { pathElement | pathIndex } var elements []interface{} if len(path) > 0 && path[0] == '.' { return nil, errors.New("expected property path to start with a name or index") } for len(path) > 0 { switch path[0] { case '.': path = path[1:] if len(path) == 0 { return nil, errors.New("expected property path to end with a name or index") } if path[0] == '[' { // We tolerate a '.' followed by a '[', which is not strictly legal, but is common from old providers. logging.V(10).Infof("property path '%s' contains a '.' followed by a '['; this is not strictly legal", path) } case '[': // If the character following the '[' is a '"', parse a string key. var pathElement interface{} if len(path) > 1 && path[1] == '"' { var propertyKey []byte var i int for i = 2; ; { if i >= len(path) { return nil, errors.New("missing closing quote in property name") } else if path[i] == '"' { i++ break } else if path[i] == '\\' && i+1 < len(path) && path[i+1] == '"' { propertyKey = append(propertyKey, '"') i += 2 } else { propertyKey = append(propertyKey, path[i]) i++ } } if i >= len(path) || path[i] != ']' { return nil, errors.New("missing closing bracket in property access") } pathElement, path = string(propertyKey), path[i:] } else { // Look for a closing ']' rbracket := strings.IndexRune(path, ']') if rbracket == -1 { return nil, errors.New("missing closing bracket in array index") } segment := path[1:rbracket] if segment == "*" { pathElement, path = "*", path[rbracket:] } else { index, err := strconv.ParseInt(segment, 10, 0) if err != nil { return nil, fmt.Errorf("invalid array index: %w", err) } pathElement, path = int(index), path[rbracket:] } } elements, path = append(elements, pathElement), path[1:] default: for i := 0; ; i++ { if i == len(path) || path[i] == '.' || path[i] == '[' { elements, path = append(elements, path[:i]), path[i:] break } } } } return PropertyPath(elements), nil } // Get attempts to get the value located by the PropertyPath inside the given PropertyValue. If any component of the // path does not exist, this function will return (NullPropertyValue, false). func (p PropertyPath) Get(v PropertyValue) (PropertyValue, bool) { for _, key := range p { switch { case v.IsArray(): index, ok := key.(int) if !ok || index < 0 || index >= len(v.ArrayValue()) { return PropertyValue{}, false } v = v.ArrayValue()[index] case v.IsObject(): k, ok := key.(string) if !ok { return PropertyValue{}, false } v, ok = v.ObjectValue()[PropertyKey(k)] if !ok { return PropertyValue{}, false } default: return PropertyValue{}, false } } return v, true } // Set attempts to set the location inside a PropertyValue indicated by the PropertyPath to the given value. If any // component of the path besides the last component does not exist, this function will return false. func (p PropertyPath) Set(dest, v PropertyValue) bool { if len(p) == 0 { return false } dest, ok := p[:len(p)-1].Get(dest) if !ok { return false } key := p[len(p)-1] switch { case dest.IsArray(): index, ok := key.(int) if !ok || index < 0 || index >= len(dest.ArrayValue()) { return false } dest.ArrayValue()[index] = v case dest.IsObject(): k, ok := key.(string) if !ok { return false } dest.ObjectValue()[PropertyKey(k)] = v default: return false } return true } // Add sets the location inside a PropertyValue indicated by the PropertyPath to the given value. Any components // referred to by the path that do not exist will be created. If there is a mismatch between the type of an existing // component and a key that traverses that component, this function will return false. If the destination is a null // property value, this function will create and return a new property value. func (p PropertyPath) Add(dest, v PropertyValue) (PropertyValue, bool) { if len(p) == 0 { return PropertyValue{}, false } // set sets the destination referred to by the last element of the path to the given value. rv := dest set := func(v PropertyValue) { dest, rv = v, v } for _, key := range p { switch key := key.(type) { case int: // This key is an int, so we expect an array. switch { case dest.IsNull(): // If the destination array does not exist, create a new array with enough room to store the value at // the requested index. dest = NewArrayProperty(make([]PropertyValue, key+1)) set(dest) case dest.IsArray(): // If the destination array does exist, ensure that it is large enough to accommodate the requested // index. if arr := dest.ArrayValue(); key >= len(arr) { dest = NewArrayProperty(append(make([]PropertyValue, key+1-len(arr)), arr...)) set(dest) } default: return PropertyValue{}, false } destV := dest.ArrayValue() set = func(v PropertyValue) { destV[key] = v } dest = destV[key] case string: // This key is a string, so we expect an object. switch { case dest.IsNull(): // If the destination does not exist, create a new object. dest = NewObjectProperty(PropertyMap{}) set(dest) case dest.IsObject(): // OK default: return PropertyValue{}, false } destV := dest.ObjectValue() set = func(v PropertyValue) { destV[PropertyKey(key)] = v } dest = destV[PropertyKey(key)] default: return PropertyValue{}, false } } set(v) return rv, true } // Delete attempts to delete the value located by the PropertyPath inside the given PropertyValue. If any component // of the path does not exist, this function will return false. func (p PropertyPath) Delete(dest PropertyValue) bool { if len(p) == 0 { return false } dest, ok := p[:len(p)-1].Get(dest) if !ok { return false } key := p[len(p)-1] switch { case dest.IsArray(): index, ok := key.(int) if !ok || index < 0 || index >= len(dest.ArrayValue()) { return false } dest.ArrayValue()[index] = PropertyValue{} case dest.IsObject(): k, ok := key.(string) if !ok { return false } delete(dest.ObjectValue(), PropertyKey(k)) default: return false } return true } // Contains returns true if the receiver property path contains the other property path. // For example, the path `foo["bar"][1]` contains the path `foo.bar[1].baz`. The key `"*"` // is a wildcard which matches any string or int index at that same nesting level. So for example, // the path `foo.*.baz` contains `foo.bar.baz.bam`, and the path `*` contains any path. func (p PropertyPath) Contains(other PropertyPath) bool { if len(other) < len(p) { return false } for i := range p { pp := p[i] otherp := other[i] switch pp := pp.(type) { case int: if otherpi, ok := otherp.(int); !ok || otherpi != pp { return false } case string: if pp == "*" { continue } if otherps, ok := otherp.(string); !ok || otherps != pp { return false } default: // Invalid path, return false return false } } return true } // unwrapSecrets recursively unwraps any secrets from the given PropertyValue returning true if any secrets were // unwrapped. func unwrapSecrets(v PropertyValue) (PropertyValue, bool) { if v.IsSecret() { inner, _ := unwrapSecrets(v.SecretValue().Element) return inner, true } return v, false } func (p PropertyPath) reset(old, new PropertyValue, oldIsSecret, newIsSecret bool) bool { if len(p) == 0 { return false } // Unwrap any secrets from old & new, we can just go through them for this traversal. old, isSecret := unwrapSecrets(old) oldIsSecret = oldIsSecret || isSecret new, isSecret = unwrapSecrets(new) newIsSecret = newIsSecret || isSecret // If this is the last component we want to do the reset, else we want to search for the next component. key := p[0] switch key := key.(type) { case int: // An index < 0 is always a path error, even for empty arrays or objects if key < 0 { return false } // This is a leaf path element, so we want to reset the value at this index in new to the value at this index from old if len(p) == 1 { if !old.IsArray() && !new.IsArray() { // Neither old nor new are arrays, so we can't reset this index return true } else if !old.IsArray() || !new.IsArray() { // One of old or new is an array but the other isn't, so this is a path error return false } // If neither array contains this index then this is a _same_ and so ok, e.g. given old:[1, 2] and // new:[1] and a path of [3] we can return true because new at [3] is the same as old at [3], it // doesn't exist. if key >= len(old.ArrayValue()) && key >= len(new.ArrayValue()) { return true } // If one array has this index but the other doesn't this is a path failure because we can't // remove a location from an array. if key >= len(old.ArrayValue()) || key >= len(new.ArrayValue()) { return false } // Otherwise both arrays contain this index and we can reset the value of it in new to what is in // old. v := old.ArrayValue()[key] // If this was a secret value in old, but new isn't currently a secret context then we need to mark this // reset value as secret. if oldIsSecret && !newIsSecret { v = MakeSecret(v) } new.ArrayValue()[key] = v return true } if !old.IsArray() || !new.IsArray() { // At least one of old or new is not an array, so we can't keep searching along this path but // we only return an error if both are not arrays. return !old.IsArray() && !new.IsArray() } // If this index is out of bounds in either array then this is a path failure because we can't // continue the search of this path down each PropertyValue. if key >= len(old.ArrayValue()) || key >= len(new.ArrayValue()) { return false } old = old.ArrayValue()[key] new = new.ArrayValue()[key] return p[1:].reset(old, new, oldIsSecret, newIsSecret) case string: if key == "*" { if len(p) == 1 { if new.IsObject() { if old.IsObject() { for k := range old.ObjectValue() { v := old.ObjectValue()[k] // If this was a secret value in old, but new isn't currently a secret context then we need // to mark this reset value as secret. if oldIsSecret && !newIsSecret { v = MakeSecret(v) } new.ObjectValue()[k] = v } for k := range new.ObjectValue() { if _, has := old.ObjectValue()[k]; !has { delete(new.ObjectValue(), k) } } } return true } else if new.IsArray() { if old.IsArray() { for i := range old.ArrayValue() { v := old.ArrayValue()[i] // If this was a secret value in old, but new isn't currently a secret context then we need // to mark this reset value as secret. if oldIsSecret && !newIsSecret { v = MakeSecret(v) } new.ArrayValue()[i] = v } } return true } return false } if old.IsObject() && new.IsObject() { oldObject := old.ObjectValue() newObject := new.ObjectValue() for k := range oldObject { var hasOld, hasNew bool oldValue, hasOld := oldObject[k] newValue, hasNew := newObject[k] if !hasOld || !hasNew { return false } if !p[1:].reset(oldValue, newValue, oldIsSecret, newIsSecret) { return false } } return true } else if old.IsArray() && new.IsArray() { oldArray := old.ArrayValue() newArray := new.ArrayValue() for i := range oldArray { if !p[1:].reset(oldArray[i], newArray[i], oldIsSecret, newIsSecret) { return false } } return true } return false } else { pkey := PropertyKey(key) if len(p) == 1 { // This is the leaf path entry, so we want to reset this property in new to it's value in old. // Firstly if old doesn't have this key (either because it isn't an object or because it // doesn't have the property) then we want to delete this from new. var v PropertyValue var has bool if old.IsObject() { v, has = old.ObjectValue()[pkey] } if has { // If this path exists in old but new isn't an object than return a path error if !new.IsObject() { return false } // Else simply overwrite the value in new with the value from old, if this was a secret value in // old, but new isn't currently a secret context then we need to mark this reset value as secret. if oldIsSecret && !newIsSecret { v = MakeSecret(v) } new.ObjectValue()[pkey] = v } else { // If the path doesn't exist in old then we want to delete it from new, but if new isn't // an object then we can just do nothing we don't consider this a path error. e.g. given // old:{} and new:1 and a path of "a" we can return true because ["a"] in both is the // same (it doesn't exist). if new.IsObject() { delete(new.ObjectValue(), pkey) } } return true } if !old.IsObject() || !new.IsObject() { // At least one of old or new is not an object, so we can't keep searching along this path but // we only return an error if both are not objects. return !old.IsObject() && !new.IsObject() } new, hasNew := new.ObjectValue()[pkey] old, hasOld := old.ObjectValue()[pkey] if hasOld && !hasNew { // Old has this key but new doesn't, but we still searching for the leaf item to set so this // is a path error. return false } if !hasOld && !hasNew { // Neither value contain this path, so we're done. return true } return p[1:].reset(old, new, oldIsSecret, newIsSecret) } } contract.Failf("Invalid property path component type: %T", key) return true } // Reset attempts to reset the values located by the PropertyPath inside the given new PropertyMap to the // values from the same location in the old PropertyMap. Reset behaves likes Set in that it will not create // intermediate locations, it also won't create or delete array locations (because that would change the size // of the array). func (p PropertyPath) Reset(old, new PropertyMap) bool { return p.reset(NewObjectProperty(old), NewObjectProperty(new), false, false) } func requiresQuote(c rune) bool { return !(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_') } func (p PropertyPath) String() string { var buf bytes.Buffer for i, k := range p { switch k := k.(type) { case string: var keyBuf bytes.Buffer quoted := false for _, c := range k { if requiresQuote(c) { quoted = true if c == '"' { keyBuf.WriteByte('\\') } } keyBuf.WriteRune(c) } if !quoted { if i == 0 { fmt.Fprintf(&buf, "%s", keyBuf.String()) } else { fmt.Fprintf(&buf, ".%s", keyBuf.String()) } } else { fmt.Fprintf(&buf, `["%s"]`, keyBuf.String()) } case int: fmt.Fprintf(&buf, "[%d]", k) } } return buf.String() }