diff --git a/helper/plugin/grpc_provisioner.go b/helper/plugin/grpc_provisioner.go index 14494e462..088e94e4a 100644 --- a/helper/plugin/grpc_provisioner.go +++ b/helper/plugin/grpc_provisioner.go @@ -2,6 +2,8 @@ package plugin import ( "log" + "strings" + "unicode/utf8" "github.com/hashicorp/terraform/helper/schema" proto "github.com/hashicorp/terraform/internal/tfplugin5" @@ -90,7 +92,7 @@ type uiOutput struct { func (o uiOutput) Output(s string) { err := o.srv.Send(&proto.ProvisionResource_Response{ - Output: s, + Output: toValidUTF8(s, string(utf8.RuneError)), }) if err != nil { log.Printf("[ERROR] %s", err) @@ -145,3 +147,55 @@ func (s *GRPCProvisionerServer) Stop(_ context.Context, req *proto.Stop_Request) return resp, nil } + +// FIXME: backported from go1.13 strings package, remove once terraform is +// using go >= 1.13 +// ToValidUTF8 returns a copy of the string s with each run of invalid UTF-8 byte sequences +// replaced by the replacement string, which may be empty. +func toValidUTF8(s, replacement string) string { + var b strings.Builder + + for i, c := range s { + if c != utf8.RuneError { + continue + } + + _, wid := utf8.DecodeRuneInString(s[i:]) + if wid == 1 { + b.Grow(len(s) + len(replacement)) + b.WriteString(s[:i]) + s = s[i:] + break + } + } + + // Fast path for unchanged input + if b.Cap() == 0 { // didn't call b.Grow above + return s + } + + invalid := false // previous byte was from an invalid UTF-8 sequence + for i := 0; i < len(s); { + c := s[i] + if c < utf8.RuneSelf { + i++ + invalid = false + b.WriteByte(c) + continue + } + _, wid := utf8.DecodeRuneInString(s[i:]) + if wid == 1 { + i++ + if !invalid { + invalid = true + b.WriteString(replacement) + } + continue + } + invalid = false + b.WriteString(s[i : i+wid]) + i += wid + } + + return b.String() +} diff --git a/helper/plugin/grpc_provisioner_test.go b/helper/plugin/grpc_provisioner_test.go index c64045ab4..9b38daf4a 100644 --- a/helper/plugin/grpc_provisioner_test.go +++ b/helper/plugin/grpc_provisioner_test.go @@ -1,5 +1,82 @@ package plugin -import proto "github.com/hashicorp/terraform/internal/tfplugin5" +import ( + "testing" + "unicode/utf8" + + "github.com/golang/mock/gomock" + "github.com/hashicorp/terraform/helper/schema" + proto "github.com/hashicorp/terraform/internal/tfplugin5" + mockproto "github.com/hashicorp/terraform/plugin/mock_proto" + "github.com/hashicorp/terraform/terraform" + context "golang.org/x/net/context" +) var _ proto.ProvisionerServer = (*GRPCProvisionerServer)(nil) + +type validUTF8Matcher string + +func (m validUTF8Matcher) Matches(x interface{}) bool { + resp := x.(*proto.ProvisionResource_Response) + return utf8.Valid([]byte(resp.Output)) +} + +func (m validUTF8Matcher) String() string { + return string(m) +} + +func mockProvisionerServer(t *testing.T, c *gomock.Controller) *mockproto.MockProvisioner_ProvisionResourceServer { + server := mockproto.NewMockProvisioner_ProvisionResourceServer(c) + + server.EXPECT().Send( + validUTF8Matcher("check for valid utf8"), + ).Return(nil) + + return server +} + +// ensure that a provsioner cannot return invalid utf8 which isn't allowed in +// the grpc protocol. +func TestProvisionerInvalidUTF8(t *testing.T) { + p := &schema.Provisioner{ + ConnSchema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeInt, + Optional: true, + }, + }, + + ApplyFunc: func(ctx context.Context) error { + out := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) + out.Output("invalid \xc3\x28\n") + return nil + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + srv := mockProvisionerServer(t, ctrl) + cfg := &proto.DynamicValue{ + Msgpack: []byte("\x81\xa3foo\x01"), + } + conn := &proto.DynamicValue{ + Msgpack: []byte("\x81\xa3foo\xa4host"), + } + provisionerServer := NewGRPCProvisionerServerShim(p) + req := &proto.ProvisionResource_Request{ + Config: cfg, + Connection: conn, + } + + if err := provisionerServer.ProvisionResource(req, srv); err != nil { + t.Fatal(err) + } +}