Skip to content

Commit

Permalink
convert: Fix panic: heterogeneous tuple with null
Browse files Browse the repository at this point in the history
Tuples with elements of different types can be converted to homogeneous
collections (sets or lists), so long as their elements are unifiable.
For example:

  list("a", "b")     // all elements have the same type
  list("a", 5)       // "a" and 5 can be unified to string
  list("a", 5, null) // null is a valid value for string

However, tuples with elements which are not unifiable cannot be
converted to homogeneous collections:

  list(["a"], "b")   // no common type for list(string) and string

This commit fixes a panic for this failure case, when the tuple contains
both non-unifiable types and a null value:

  list(["a"], "b", null) // should not panic

The null value was causing the unification process to result in a list
or set of dynamic type, which causes the conversion functions to pass
through the original value. This meant that in the final conversion
step, we would attempt to construct a list or set of different values,
which panics.
  • Loading branch information
alisdair authored and apparentlymart committed Jun 5, 2020
1 parent 07e6921 commit 4d5a7ef
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 7 deletions.
36 changes: 29 additions & 7 deletions cty/convert/conversion_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,34 +156,45 @@ func conversionCollectionToMap(ety cty.Type, conv conversion) conversion {
// given tuple type and return a set of the given element type.
//
// Will panic if the given tupleType isn't actually a tuple type.
func conversionTupleToSet(tupleType cty.Type, listEty cty.Type, unsafe bool) conversion {
func conversionTupleToSet(tupleType cty.Type, setEty cty.Type, unsafe bool) conversion {
tupleEtys := tupleType.TupleElementTypes()

if len(tupleEtys) == 0 {
// Empty tuple short-circuit
return func(val cty.Value, path cty.Path) (cty.Value, error) {
return cty.SetValEmpty(listEty), nil
return cty.SetValEmpty(setEty), nil
}
}

if listEty == cty.DynamicPseudoType {
if setEty == cty.DynamicPseudoType {
// This is a special case where the caller wants us to find
// a suitable single type that all elements can convert to, if
// possible.
listEty, _ = unify(tupleEtys, unsafe)
if listEty == cty.NilType {
setEty, _ = unify(tupleEtys, unsafe)
if setEty == cty.NilType {
return nil
}

// If the set element type after unification is still the dynamic
// type, the only way this can result in a valid set is if all values
// are of dynamic type
if setEty == cty.DynamicPseudoType {
for _, tupleEty := range tupleEtys {
if !tupleEty.Equals(cty.DynamicPseudoType) {
return nil
}
}
}
}

elemConvs := make([]conversion, len(tupleEtys))
for i, tupleEty := range tupleEtys {
if tupleEty.Equals(listEty) {
if tupleEty.Equals(setEty) {
// no conversion required
continue
}

elemConvs[i] = getConversion(tupleEty, listEty, unsafe)
elemConvs[i] = getConversion(tupleEty, setEty, unsafe)
if elemConvs[i] == nil {
// If any of our element conversions are impossible, then the our
// whole conversion is impossible.
Expand Down Expand Up @@ -244,6 +255,17 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
if listEty == cty.NilType {
return nil
}

// If the list element type after unification is still the dynamic
// type, the only way this can result in a valid list is if all values
// are of dynamic type
if listEty == cty.DynamicPseudoType {
for _, tupleEty := range tupleEtys {
if !tupleEty.Equals(cty.DynamicPseudoType) {
return nil
}
}
}
}

elemConvs := make([]conversion, len(tupleEtys))
Expand Down
89 changes: 89 additions & 0 deletions cty/convert/public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,95 @@ func TestConvert(t *testing.T) {
}),
WantError: false,
},
// https://github.com/hashicorp/terraform/issues/24377:
{
Value: cty.TupleVal([]cty.Value{
cty.ListVal([]cty.Value{cty.StringVal("a")}),
cty.StringVal("b"),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.Set(cty.DynamicPseudoType),
WantError: true,
},
{
Value: cty.TupleVal([]cty.Value{
cty.ListVal([]cty.Value{cty.StringVal("a")}),
cty.StringVal("b"),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.List(cty.DynamicPseudoType),
WantError: true,
},
{
Value: cty.TupleVal([]cty.Value{
cty.ListVal([]cty.Value{cty.StringVal("a")}),
cty.StringVal("b"),
}),
Type: cty.Set(cty.DynamicPseudoType),
WantError: true,
},
{
Value: cty.TupleVal([]cty.Value{
cty.ListVal([]cty.Value{cty.StringVal("a")}),
cty.StringVal("b"),
}),
Type: cty.List(cty.DynamicPseudoType),
WantError: true,
},
{
Value: cty.TupleVal([]cty.Value{
cty.StringVal("a"),
cty.NumberIntVal(9),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.Set(cty.DynamicPseudoType),
Want: cty.SetVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("9"),
cty.NullVal(cty.DynamicPseudoType),
}),
WantError: false,
},
{
Value: cty.TupleVal([]cty.Value{
cty.StringVal("a"),
cty.NumberIntVal(9),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.List(cty.DynamicPseudoType),
Want: cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("9"),
cty.NullVal(cty.DynamicPseudoType),
}),
WantError: false,
},
{
Value: cty.TupleVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.Set(cty.DynamicPseudoType),
Want: cty.SetVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
}),
WantError: false,
},
{
Value: cty.TupleVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.List(cty.DynamicPseudoType),
Want: cty.ListVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
}),
WantError: false,
},
}

for _, test := range tests {
Expand Down

0 comments on commit 4d5a7ef

Please sign in to comment.