mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 15:40:07 -06:00
addrs: Format string instance keys in an HCL-compatible way
So far we've only ever needed to re-parse address strings that happen not to contain instance keys and so we've gotten away with our serialization of these not being quite right, but given how liberally we've expected to be able to use address strings from this package for wire format interchange it seems likely that this is going to surprise us eventually. Now we'll use an escaping scheme compatible with HCL's parser rather than Go's parser, and so we can safely rely on hclsyntax.ParseTraversal as part of reversing this operation to transform an address string back into an address equivalent to the value it was created from.
This commit is contained in:
parent
aeefde7428
commit
de8eef1da5
@ -2,6 +2,8 @@ package addrs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
@ -72,9 +74,10 @@ func (k StringKey) instanceKeySigil() {
|
||||
}
|
||||
|
||||
func (k StringKey) String() string {
|
||||
// FIXME: This isn't _quite_ right because Go's quoted string syntax is
|
||||
// slightly different than HCL's, but we'll accept it for now.
|
||||
return fmt.Sprintf("[%q]", string(k))
|
||||
// We use HCL's quoting syntax here so that we can in principle parse
|
||||
// an address constructed by this package as if it were an HCL
|
||||
// traversal, even if the string contains HCL's own metacharacters.
|
||||
return fmt.Sprintf("[%s]", toHCLQuotedString(string(k)))
|
||||
}
|
||||
|
||||
func (k StringKey) Value() cty.Value {
|
||||
@ -133,3 +136,56 @@ const (
|
||||
IntKeyType InstanceKeyType = 'I'
|
||||
StringKeyType InstanceKeyType = 'S'
|
||||
)
|
||||
|
||||
// toHCLQuotedString is a helper which formats the given string in a way that
|
||||
// HCL's expression parser would treat as a quoted string template.
|
||||
//
|
||||
// This includes:
|
||||
// - Adding quote marks at the start and the end.
|
||||
// - Using backslash escapes as needed for characters that cannot be represented directly.
|
||||
// - Escaping anything that would be treated as a template interpolation or control sequence.
|
||||
func toHCLQuotedString(s string) string {
|
||||
// This is an adaptation of a similar function inside the hclwrite package,
|
||||
// inlined here because hclwrite's version generates HCL tokens but we
|
||||
// only need normal strings.
|
||||
if len(s) == 0 {
|
||||
return `""`
|
||||
}
|
||||
var buf strings.Builder
|
||||
buf.WriteByte('"')
|
||||
for i, r := range s {
|
||||
switch r {
|
||||
case '\n':
|
||||
buf.WriteString(`\n`)
|
||||
case '\r':
|
||||
buf.WriteString(`\r`)
|
||||
case '\t':
|
||||
buf.WriteString(`\t`)
|
||||
case '"':
|
||||
buf.WriteString(`\"`)
|
||||
case '\\':
|
||||
buf.WriteString(`\\`)
|
||||
case '$', '%':
|
||||
buf.WriteRune(r)
|
||||
remain := s[i+1:]
|
||||
if len(remain) > 0 && remain[0] == '{' {
|
||||
// Double up our template introducer symbol to escape it.
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
default:
|
||||
if !unicode.IsPrint(r) {
|
||||
var fmted string
|
||||
if r < 65536 {
|
||||
fmted = fmt.Sprintf("\\u%04x", r)
|
||||
} else {
|
||||
fmted = fmt.Sprintf("\\U%08x", r)
|
||||
}
|
||||
buf.WriteString(fmted)
|
||||
} else {
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteByte('"')
|
||||
return buf.String()
|
||||
}
|
||||
|
75
internal/addrs/instance_key_test.go
Normal file
75
internal/addrs/instance_key_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package addrs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInstanceKeyString(t *testing.T) {
|
||||
tests := []struct {
|
||||
Key InstanceKey
|
||||
Want string
|
||||
}{
|
||||
{
|
||||
IntKey(0),
|
||||
`[0]`,
|
||||
},
|
||||
{
|
||||
IntKey(5),
|
||||
`[5]`,
|
||||
},
|
||||
{
|
||||
StringKey(""),
|
||||
`[""]`,
|
||||
},
|
||||
{
|
||||
StringKey("hi"),
|
||||
`["hi"]`,
|
||||
},
|
||||
{
|
||||
StringKey("0"),
|
||||
`["0"]`, // intentionally distinct from IntKey(0)
|
||||
},
|
||||
{
|
||||
// Quotes must be escaped
|
||||
StringKey(`"`),
|
||||
`["\""]`,
|
||||
},
|
||||
{
|
||||
// Escape sequences must themselves be escaped
|
||||
StringKey(`\r\n`),
|
||||
`["\\r\\n"]`,
|
||||
},
|
||||
{
|
||||
// Template interpolation sequences "${" must be escaped.
|
||||
StringKey(`${hello}`),
|
||||
`["$${hello}"]`,
|
||||
},
|
||||
{
|
||||
// Template control sequences "%{" must be escaped.
|
||||
StringKey(`%{ for something in something }%{ endfor }`),
|
||||
`["%%{ for something in something }%%{ endfor }"]`,
|
||||
},
|
||||
{
|
||||
// Dollar signs that aren't followed by { are not interpolation sequences
|
||||
StringKey(`$hello`),
|
||||
`["$hello"]`,
|
||||
},
|
||||
{
|
||||
// Percent signs that aren't followed by { are not control sequences
|
||||
StringKey(`%hello`),
|
||||
`["%hello"]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testName := fmt.Sprintf("%#v", test.Key)
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
got := test.Key.String()
|
||||
want := test.Want
|
||||
if got != want {
|
||||
t.Errorf("wrong result\nreciever: %s\ngot: %s\nwant: %s", testName, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user