Repository: ktr0731/evans
Pull request #631 (2023-02-06 16:30:15)

Accept bytes as base64-encoded string by default
As per https://github.com/ktr0731/evans/pull/611#issuecomment-1418606068, this keeps thing symmetrical with the output, and how grpcurl handles it as well.

The old quoted literal behaviour is still accessible via `--bytes-as-quoted-literals`.

codecov[bot]: # [Codecov](https://codecov.io/gh/ktr0731/evans/pull/631?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ktr) Report
> Merging [#631](https://codecov.io/gh/ktr0731/evans/pull/631?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ktr) (b8d48fd) into [master](https://codecov.io/gh/ktr0731/evans/commit/5f3b4c54fbf8a5fe686c796b3844276078d2dc7c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ktr) (5f3b4c5) will **increase** coverage by `0.11%`.
> The diff coverage is `89.47%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #631      +/-   ##
==========================================
+ Coverage   80.01%   80.12%   +0.11%     
==========================================
  Files          57       57              
  Lines        3858     3855       -3     
==========================================
+ Hits         3087     3089       +2     
+ Misses        542      538       -4     
+ Partials      229      228       -1     
```

</details>
flokli: @ktr0731 I updated the PR. It now attempts to fall back to the old encoding in case neither `--bytes-as-base64` nor `--bytes-as-quoted-literals` was passed explicitly, as proposed in https://github.com/ktr0731/evans/pull/631#discussion_r1111330296.

The tests have been extended to ensure the old behaviour is preserved, base64 without explicitly specifying the parameter, and behaviour with `--bytes-as-base64` or `--bytes-as-quoted-literals` works.

PTAL.

Patch:
diff --git a/README.md b/README.md
index 7f3ff34..1f09092 100644
--- a/README.md
+++ b/README.md
@@ -263,27 +263,41 @@ When <kbd>CTRL-D</kbd> is entered, inputting will be aborted.
 ```
 
 ### Bytes type fields
-You can use byte literal and Unicode literal.
+You can pass bytes as a base64-encoded string.
 
 ```
 > call UnaryBytes
+data (TYPE_BYTES) => SGVsbG8gV29ybGQh
+{
+  "message": "received: (bytes) 48 65 6c 6c 6f 20 57 6f 72 6c 64 21, (string) Hello World!"
+}
+```
+
+⚠️  Warning: Previously, bytes were passed as a (quoted) byte literal or Unicode
+literal string.
+
+While evans currenty still attempts to fall back to that encoding if decoding
+as base64 fails, it's discouraged, and might be dropped.
+
+Please either encode bytes as base64, or pass `--bytes-as-quoted-literals`
+explicitly:
+
+```
+> call UnaryBytes --bytes-as-quoted-literals
 data (TYPE_BYTES) => \x46\x6f\x6f
 {
   "message": "received: (bytes) 46 6f 6f, (string) Foo"
 }
 
-> call UnaryBytes
+> call UnaryBytes --bytes-as-quoted-literals
 data (TYPE_BYTES) => \u65e5\u672c\u8a9e
 {
   "message": "received: (bytes) e6 97 a5 e6 9c ac e8 aa 9e, (string) 日本語"
 }
 ```
 
-Or add the flag `--bytes-as-base64` to pass bytes as a base64-encoded string
-```
-> call UnaryBytes --bytes-as-base64
-data (TYPE_BYTES) => SGVsbG8gV29ybGQh
-```
+You can also add the flag `--bytes-as-base64` to explicitly disable the
+fallback behaviour.
 
 Or add the flag `--bytes-from-file` to read bytes from the provided relative path
 ```
diff --git a/fill/filler.go b/fill/filler.go
index e0a96b9..9b5c9be 100644
--- a/fill/filler.go
+++ b/fill/filler.go
@@ -24,6 +24,8 @@ type InteractiveFillerOpts struct {
 	DigManually,
 	// BytesAsBase64 is true, Fill will interpret input as base64-encoded string
 	BytesAsBase64,
+	// BytesAsQuotedLiterals is true, Fill will interpret input as a string of (quoted) byte literal or Unicode
+	BytesAsQuotedLiterals,
 	// BytesFromFile is true, Fill will read the contents of the file from the provided relative path.
 	BytesFromFile,
 	// AddRepeatedManually is true, Fill asks whether to add a repeated field value
diff --git a/fill/proto/interactive_filler.go b/fill/proto/interactive_filler.go
index 126bf4b..ea176bf 100644
--- a/fill/proto/interactive_filler.go
+++ b/fill/proto/interactive_filler.go
@@ -12,6 +12,7 @@ import (
 	"github.com/jhump/protoreflect/desc/builder"
 	"github.com/jhump/protoreflect/dynamic"
 	"github.com/ktr0731/evans/fill"
+	"github.com/ktr0731/evans/logger"
 	"github.com/ktr0731/evans/prompt"
 	"github.com/pkg/errors"
 	"google.golang.org/protobuf/types/descriptorpb"
@@ -196,26 +197,60 @@ func (r *resolver) resolveField(f *desc.FieldDescriptor) error {
 		case descriptorpb.FieldDescriptorProto_TYPE_STRING:
 			converter = func(v string) (interface{}, error) { return v, nil }
 
+		// For bytes, if neither BytesAsBase64 nor BytesAsQuotedLiterals is explicitly set,
+		// try to decode as base64 first, and if that fails, fall back trying to parse
+		// as quoted literals (logging a warning).
+		//
+		// This is to preserve backwards compatibility, as we used to be accept quoted
+		// literals, but want to switch to base64.
+		//
+		// If either BytesAsBase64 or BytesAsQuotedLiterals is set, only parse it in that format,
+		// and if BytesFromFile is set, read it from file.
+		//
 		// Use strconv.Unquote to interpret byte literals and Unicode literals.
 		// For example, a user inputs `\x6f\x67\x69\x73\x6f`,
 		// His expects "ogiso" in string, but backslashes in the input are not interpreted as an escape sequence.
 		// So, we need to call strconv.Unquote to interpret backslashes as an escape sequence.
 		case descriptorpb.FieldDescriptorProto_TYPE_BYTES:
 			converter = func(v string) (interface{}, error) {
-				if r.opts.BytesAsBase64 {
+				if r.opts.BytesFromFile {
+					b, err := os.ReadFile(v)
+					if err != nil {
+						return nil, err
+					}
+					return b, nil
+
+				} else if r.opts.BytesAsBase64 {
 					b, err := base64.StdEncoding.DecodeString(v)
-					if err == nil {
-						return b, nil
+					if err != nil {
+						return nil, err
 					}
-				} else if r.opts.BytesFromFile {
-					b, err := os.ReadFile(v)
-					if err == nil {
-						return b, nil
+					return b, nil
+				} else if r.opts.BytesAsQuotedLiterals {
+					v, err := strconv.Unquote(`"` + v + `"`)
+
+					if err != nil {
+						return nil, err
 					}
+					return []byte(v), nil
 				}
 
-				v, err := strconv.Unquote(`"` + v + `"`)
-				return []byte(v), err
+				// try to decode as base64
+				b, err := base64.StdEncoding.DecodeString(v)
+				if err != nil {
+					// failed, try to parse as quoted literal
+					v, err2 := strconv.Unquote(`"` + v + `"`)
+					if err2 != nil {
+						// failed to parse as this too, assume user intended to input base64, propagate
+						// that error
+						return nil, err
+					}
+					// log a warning and return the decoded literal string
+					logger.Println(`warning: entering bytes as quoted literal is deprecated. Use --bytes-as-quoted-literals or base64 encoding"`)
+					return []byte(v), nil
+				}
+				// succeeded decoding as base64, return
+				return b, nil
 			}
 
 		default:
diff --git a/repl/commands.go b/repl/commands.go
index a203ab9..2109a96 100644
--- a/repl/commands.go
+++ b/repl/commands.go
@@ -155,7 +155,7 @@ func (c *showCommand) Run(w io.Writer, args []string) error {
 }
 
 type callCommand struct {
-	enrich, digManually, bytesAsBase64, bytesFromFile, emitDefaults, repeatCall, addRepeatedManually bool
+	enrich, digManually, bytesAsBase64, bytesAsQuotedLiterals, bytesFromFile, emitDefaults, repeatCall, addRepeatedManually bool
 }
 
 func (c *callCommand) FlagSet() (*pflag.FlagSet, bool) {
@@ -163,7 +163,8 @@ func (c *callCommand) FlagSet() (*pflag.FlagSet, bool) {
 	fs.Usage = func() {} // Disable help output when an error occurred.
 	fs.BoolVar(&c.enrich, "enrich", false, "enrich response output includes header, message, trailer and status")
 	fs.BoolVar(&c.digManually, "dig-manually", false, "prompt asks whether to dig down if it encountered to a message field")
-	fs.BoolVar(&c.bytesAsBase64, "bytes-as-base64", false, "interpret TYPE_BYTES input as base64-encoded string (mutually exclusive with --bytes-from-file)")
+	fs.BoolVar(&c.bytesAsBase64, "bytes-as-base64", false, "explicitly interpret TYPE_BYTES input as base64-encoded string (mutually exclusive with --bytes-from-file and --bytes-as-quoted-literals)")
+	fs.BoolVar(&c.bytesAsQuotedLiterals, "bytes-as-quoted-literals", false, "interpret TYPE_BYTES input as a string of (quoted) byte literal or Unicode (mutually exclusive with --bytes-from-file and --bytes-as-base64)")
 	fs.BoolVar(&c.bytesFromFile, "bytes-from-file", false, "interpret TYPE_BYTES input as a relative path to a file (mutually exclusive with --bytes-as-base64)")
 	fs.BoolVar(&c.emitDefaults, "emit-defaults", false, "render fields with default values")
 	fs.BoolVarP(&c.repeatCall, "repeat", "r", false, "repeat previous unary or server streaming request (if exists)")
@@ -200,15 +201,18 @@ func (c *callCommand) Run(w io.Writer, args []string) error {
 		},
 	)
 
-	// Ensure bytesAsBase64 and bytesFromFile are not both set
+	// Ensure only one of bytesAsBase64, bytesAsQuotedLiterals and bytesFromFile are not both set
 	// pflag doesn't suppport mutually exclusive flags (https://github.com/spf13/pflag/issues/270)
-	if c.bytesAsBase64 && c.bytesFromFile {
-		return errors.New("only one of --bytes-as-base64 or --bytes-from-file can be specified")
+	if c.bytesFromFile && (c.bytesAsBase64 || c.bytesAsQuotedLiterals) {
+		return errors.New("only one of --bytes-from-file or --bytes-as-* can be specified")
+	}
+	if c.bytesAsBase64 && c.bytesAsQuotedLiterals {
+		return errors.New("only one of --bytes-as-base64 or --bytes-as-quoted-literals can be specified")
 	}
 
 	// here we create the request context
 	// we also add the call command flags here
-	err := usecase.CallRPCInteractively(context.Background(), w, args[0], c.digManually, c.bytesAsBase64, c.bytesFromFile, c.repeatCall, c.addRepeatedManually)
+	err := usecase.CallRPCInteractively(context.Background(), w, args[0], c.digManually, c.bytesAsBase64, c.bytesAsQuotedLiterals, c.bytesFromFile, c.repeatCall, c.addRepeatedManually)
 	if errors.Is(err, io.EOF) {
 		return errors.New("inputting canceled")
 	}
diff --git a/usecase/call_rpc.go b/usecase/call_rpc.go
index 48e795e..ebce079 100644
--- a/usecase/call_rpc.go
+++ b/usecase/call_rpc.go
@@ -478,18 +478,19 @@ func (f *interactiveFiller) Fill(v interface{}) error {
 	return f.fillFunc(v)
 }
 
-func CallRPCInteractively(ctx context.Context, w io.Writer, rpcName string, digManually, bytesAsBase64, bytesFromFile, rerunPrevious, addRepeatedManually bool) error {
-	return dm.CallRPCInteractively(ctx, w, rpcName, digManually, bytesAsBase64, bytesFromFile, rerunPrevious, addRepeatedManually)
+func CallRPCInteractively(ctx context.Context, w io.Writer, rpcName string, digManually, bytesAsBase64, bytesAsQuotedLiterals, bytesFromFile, rerunPrevious, addRepeatedManually bool) error {
+	return dm.CallRPCInteractively(ctx, w, rpcName, digManually, bytesAsBase64, bytesAsQuotedLiterals, bytesFromFile, rerunPrevious, addRepeatedManually)
 }
 
-func (m *dependencyManager) CallRPCInteractively(ctx context.Context, w io.Writer, rpcName string, digManually, bytesAsBase64, bytesFromFile, rerunPrevious, addRepeatedManually bool) error {
+func (m *dependencyManager) CallRPCInteractively(ctx context.Context, w io.Writer, rpcName string, digManually, bytesAsBase64, bytesAsQuotedLiterals, bytesFromFile, rerunPrevious, addRepeatedManually bool) error {
 	return m.CallRPC(ctx, w, rpcName, rerunPrevious, &interactiveFiller{
 		fillFunc: func(v interface{}) error {
 			return m.interactiveFiller.Fill(v, fill.InteractiveFillerOpts{
-				DigManually:         digManually,
-				BytesAsBase64:       bytesAsBase64,
-				BytesFromFile:       bytesFromFile,
-				AddRepeatedManually: addRepeatedManually,
+				DigManually:           digManually,
+				BytesAsBase64:         bytesAsBase64,
+				BytesAsQuotedLiterals: bytesAsQuotedLiterals,
+				BytesFromFile:         bytesFromFile,
+				AddRepeatedManually:   addRepeatedManually,
 			})
 		},
 	})


Test patch:
diff --git a/e2e/old_repl_test.go b/e2e/old_repl_test.go
index 5d4066d..aa78408 100644
--- a/e2e/old_repl_test.go
+++ b/e2e/old_repl_test.go
@@ -121,9 +121,21 @@ func TestE2E_OldREPL(t *testing.T) {
 			input: []interface{}{"call UnaryEnum", 0},
 		},
 		"call UnaryBytes": {
+			args:  "testdata/test.proto",
+			input: []interface{}{"call UnaryBytes", "44KE44Gv44KK5L+644Gu6Z2S5pil44Op44OW44Kz44Oh44Gv44G+44Gh44GM44Gj44Gm44GE44KL44CC"},
+		},
+		"call UnaryBytes (fallback)": {
 			args:  "testdata/test.proto",
 			input: []interface{}{"call UnaryBytes", "\\u3084\\u306f\\u308a\\u4ffa\\u306e\\u9752\\u6625\\u30e9\\u30d6\\u30b3\\u30e1\\u306f\\u307e\\u3061\\u304c\\u3063\\u3066\\u3044\\u308b\\u3002"},
 		},
+		"call UnaryBytes --bytes-as-base64": {
+			args:  "testdata/test.proto",
+			input: []interface{}{"call UnaryBytes --bytes-as-base64", "44KE44Gv44KK5L+644Gu6Z2S5pil44Op44OW44Kz44Oh44Gv44G+44Gh44GM44Gj44Gm44GE44KL44CC"},
+		},
+		"call UnaryBytes --bytes-as-quoted-literals": {
+			args:  "testdata/test.proto",
+			input: []interface{}{"call UnaryBytes --bytes-as-quoted-literals", "\\u3084\\u306f\\u308a\\u4ffa\\u306e\\u9752\\u6625\\u30e9\\u30d6\\u30b3\\u30e1\\u306f\\u307e\\u3061\\u304c\\u3063\\u3066\\u3044\\u308b\\u3002"},
+		},
 		"call UnaryRepeatedEnum": {
 			args:  "testdata/test.proto",
 			input: []interface{}{"call UnaryRepeatedEnum", 0, 0, 1, io.EOF},
diff --git a/e2e/repl_test.go b/e2e/repl_test.go
index 79e6e13..83c0dac 100644
--- a/e2e/repl_test.go
+++ b/e2e/repl_test.go
@@ -128,9 +128,21 @@ func TestE2E_REPL(t *testing.T) {
 			input:       []interface{}{"call UnaryEnum", 0},
 		},
 		"call UnaryBytes": {
+			commonFlags: "--proto testdata/test.proto",
+			input:       []interface{}{"call UnaryBytes", "44KE44Gv44KK5L+644Gu6Z2S5pil44Op44OW44Kz44Oh44Gv44G+44Gh44GM44Gj44Gm44GE44KL44CC"},
+		},
+		"call UnaryBytes (fallback)": {
 			commonFlags: "--proto testdata/test.proto",
 			input:       []interface{}{"call UnaryBytes", "\\u3084\\u306f\\u308a\\u4ffa\\u306e\\u9752\\u6625\\u30e9\\u30d6\\u30b3\\u30e1\\u306f\\u307e\\u3061\\u304c\\u3063\\u3066\\u3044\\u308b\\u3002"},
 		},
+		"call UnaryBytes --bytes-as-base64": {
+			commonFlags: "--proto testdata/test.proto",
+			input:       []interface{}{"call UnaryBytes --bytes-as-base64", "44KE44Gv44KK5L+644Gu6Z2S5pil44Op44OW44Kz44Oh44Gv44G+44Gh44GM44Gj44Gm44GE44KL44CC"},
+		},
+		"call UnaryBytes --bytes-as-quoted-literals": {
+			commonFlags: "--proto testdata/test.proto",
+			input:       []interface{}{"call UnaryBytes --bytes-as-quoted-literals", "\\u3084\\u306f\\u308a\\u4ffa\\u306e\\u9752\\u6625\\u30e9\\u30d6\\u30b3\\u30e1\\u306f\\u307e\\u3061\\u304c\\u3063\\u3066\\u3044\\u308b\\u3002"},
+		},
 		"call UnaryRepeatedEnum": {
 			commonFlags: "--proto testdata/test.proto",
 			input:       []interface{}{"call UnaryRepeatedEnum", 0, 0, 1, io.EOF},
diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_(fallback).golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_(fallback).golden
new file mode 100644
index 0000000..62e67d9
--- /dev/null
+++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_(fallback).golden
@@ -0,0 +1,4 @@
+{
+  "message": "received: (bytes) e3 82 84 e3 81 af e3 82 8a e4 bf ba e3 81 ae e9 9d 92 e6 98 a5 e3 83 a9 e3 83 96 e3 82 b3 e3 83 a1 e3 81 af e3 81 be e3 81 a1 e3 81 8c e3 81 a3 e3 81 a6 e3 81 84 e3 82 8b e3 80 82, (string) やはり俺の青春ラブコメはまちがっている。"
+}
+
diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_--bytes-as-base64.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_--bytes-as-base64.golden
new file mode 100644
index 0000000..62e67d9
--- /dev/null
+++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_--bytes-as-base64.golden
@@ -0,0 +1,4 @@
+{
+  "message": "received: (bytes) e3 82 84 e3 81 af e3 82 8a e4 bf ba e3 81 ae e9 9d 92 e6 98 a5 e3 83 a9 e3 83 96 e3 82 b3 e3 83 a1 e3 81 af e3 81 be e3 81 a1 e3 81 8c e3 81 a3 e3 81 a6 e3 81 84 e3 82 8b e3 80 82, (string) やはり俺の青春ラブコメはまちがっている。"
+}
+
diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_--bytes-as-quoted-literals.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_--bytes-as-quoted-literals.golden
new file mode 100644
index 0000000..62e67d9
--- /dev/null
+++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes_--bytes-as-quoted-literals.golden
@@ -0,0 +1,4 @@
+{
+  "message": "received: (bytes) e3 82 84 e3 81 af e3 82 8a e4 bf ba e3 81 ae e9 9d 92 e6 98 a5 e3 83 a9 e3 83 96 e3 82 b3 e3 83 a1 e3 81 af e3 81 be e3 81 a1 e3 81 8c e3 81 a3 e3 81 a6 e3 81 84 e3 82 8b e3 80 82, (string) やはり俺の青春ラブコメはまちがっている。"
+}
+
diff --git a/e2e/testdata/fixtures/teste2e_repl-call_--help.golden b/e2e/testdata/fixtures/teste2e_repl-call_--help.golden
index e9b83e1..233a977 100644
--- a/e2e/testdata/fixtures/teste2e_repl-call_--help.golden
+++ b/e2e/testdata/fixtures/teste2e_repl-call_--help.golden
@@ -1,11 +1,12 @@
 usage: call <method name>
 
 Options:
-      --add-repeated-manually   prompt asks whether to add a value if it encountered to a repeated field
-      --bytes-as-base64         interpret TYPE_BYTES input as base64-encoded string (mutually exclusive with --bytes-from-file)
-      --bytes-from-file         interpret TYPE_BYTES input as a relative path to a file (mutually exclusive with --bytes-as-base64)
-      --dig-manually            prompt asks whether to dig down if it encountered to a message field
-      --emit-defaults           render fields with default values
-      --enrich                  enrich response output includes header, message, trailer and status
-  -r, --repeat                  repeat previous unary or server streaming request (if exists)
+      --add-repeated-manually      prompt asks whether to add a value if it encountered to a repeated field
+      --bytes-as-base64            explicitly interpret TYPE_BYTES input as base64-encoded string (mutually exclusive with --bytes-from-file and --bytes-as-quoted-literals)
+      --bytes-as-quoted-literals   interpret TYPE_BYTES input as a string of (quoted) byte literal or Unicode (mutually exclusive with --bytes-from-file and --bytes-as-base64)
+      --bytes-from-file            interpret TYPE_BYTES input as a relative path to a file (mutually exclusive with --bytes-as-base64)
+      --dig-manually               prompt asks whether to dig down if it encountered to a message field
+      --emit-defaults              render fields with default values
+      --enrich                     enrich response output includes header, message, trailer and status
+  -r, --repeat                     repeat previous unary or server streaming request (if exists)
 
diff --git a/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_(fallback).golden b/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_(fallback).golden
new file mode 100644
index 0000000..62e67d9
--- /dev/null
+++ b/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_(fallback).golden
@@ -0,0 +1,4 @@
+{
+  "message": "received: (bytes) e3 82 84 e3 81 af e3 82 8a e4 bf ba e3 81 ae e9 9d 92 e6 98 a5 e3 83 a9 e3 83 96 e3 82 b3 e3 83 a1 e3 81 af e3 81 be e3 81 a1 e3 81 8c e3 81 a3 e3 81 a6 e3 81 84 e3 82 8b e3 80 82, (string) やはり俺の青春ラブコメはまちがっている。"
+}
+
diff --git a/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_--bytes-as-base64.golden b/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_--bytes-as-base64.golden
new file mode 100644
index 0000000..62e67d9
--- /dev/null
+++ b/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_--bytes-as-base64.golden
@@ -0,0 +1,4 @@
+{
+  "message": "received: (bytes) e3 82 84 e3 81 af e3 82 8a e4 bf ba e3 81 ae e9 9d 92 e6 98 a5 e3 83 a9 e3 83 96 e3 82 b3 e3 83 a1 e3 81 af e3 81 be e3 81 a1 e3 81 8c e3 81 a3 e3 81 a6 e3 81 84 e3 82 8b e3 80 82, (string) やはり俺の青春ラブコメはまちがっている。"
+}
+
diff --git a/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_--bytes-as-quoted-literals.golden b/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_--bytes-as-quoted-literals.golden
new file mode 100644
index 0000000..62e67d9
--- /dev/null
+++ b/e2e/testdata/fixtures/teste2e_repl-call_unarybytes_--bytes-as-quoted-literals.golden
@@ -0,0 +1,4 @@
+{
+  "message": "received: (bytes) e3 82 84 e3 81 af e3 82 8a e4 bf ba e3 81 ae e9 9d 92 e6 98 a5 e3 83 a9 e3 83 96 e3 82 b3 e3 83 a1 e3 81 af e3 81 be e3 81 a1 e3 81 8c e3 81 a3 e3 81 a6 e3 81 84 e3 82 8b e3 80 82, (string) やはり俺の青春ラブコメはまちがっている。"
+}
+
diff --git a/fill/proto/interactive_filler_test.go b/fill/proto/interactive_filler_test.go
index c5072c7..f3367d8 100644
--- a/fill/proto/interactive_filler_test.go
+++ b/fill/proto/interactive_filler_test.go
@@ -76,8 +76,6 @@ func TestInteractiveFiller(t *testing.T) {
 	b.AddField(builder.NewField("o", builder.FieldTypeBool()))
 	b.AddField(builder.NewField("p", builder.FieldTypeString()))
 	b.AddField(builder.NewField("q", builder.FieldTypeBytes()))
-	b.AddField(builder.NewField("r", builder.FieldTypeBytes()))
-	b.AddField(builder.NewField("s", builder.FieldTypeBytes()))
 	m, err := b.Build()
 	if err != nil {
 		t.Fatalf("Build should not return an error, but got '%s'", err)
@@ -87,23 +85,21 @@ func TestInteractiveFiller(t *testing.T) {
 	p := &stubPrompt{
 		t: t,
 		input: []string{
-			"1.1",          // c
-			"1.2",          // d
-			"1",            // e
-			"2",            // f
-			"3",            // g
-			"4",            // h
-			"5",            // i
-			"6",            // j
-			"7",            // k
-			"8",            // l
-			"9",            // m
-			"10",           // n
-			"true",         // o
-			"foo",          // p
-			"bar",          // q
-			"\x62\x61\x7a", // r
-			"./proto.go",   // s
+			"1.1",        // c
+			"1.2",        // d
+			"1",          // e
+			"2",          // f
+			"3",          // g
+			"4",          // h
+			"5",          // i
+			"6",          // j
+			"7",          // k
+			"8",          // l
+			"9",          // m
+			"10",         // n
+			"true",       // o
+			"foo",        // p
+			"./proto.go", // q
 		},
 		selection: []int{
 			0, // a - yes
@@ -121,7 +117,7 @@ func TestInteractiveFiller(t *testing.T) {
 		return
 	}
 
-	const want = `{"a":[{}],"b":"enum2","c":1.1,"d":1.2,"e":"1","f":"2","g":"3","h":"4","i":"5","j":6,"k":7,"l":8,"m":9,"n":10,"o":true,"p":"foo","q":"YmFy","r":"YmF6","s":"Ly8gUGFja2FnZSBwcm90byBwcm92aWRlcyBhIGZpbGxlciBpbXBsZW1lbnRhdGlvbiBmb3IgUHJvdG9jb2wgQnVmZmVycy4KcGFja2FnZSBwcm90bwo="}`
+	const want = `{"a":[{}],"b":"enum2","c":1.1,"d":1.2,"e":"1","f":"2","g":"3","h":"4","i":"5","j":6,"k":7,"l":8,"m":9,"n":10,"o":true,"p":"foo","q":"Ly8gUGFja2FnZSBwcm90byBwcm92aWRlcyBhIGZpbGxlciBpbXBsZW1lbnRhdGlvbiBmb3IgUHJvdG9jb2wgQnVmZmVycy4KcGFja2FnZSBwcm90bwo="}`
 
 	marshaler := jsonpb.Marshaler{EmitDefaults: true}
 	got, err := marshaler.MarshalToString(msg)