Repository: bufbuild/buf (go)

Issue:
Zero length encoding is considered invalid.
### GitHub repository with your minimal reproducible example (do not leave this field blank or fill out this field with "github.com/bufbuild/buf" or we will automatically close your issue, see the instructions above!)

https://github.com/daniel-wong-dfinity-org/foo.proto

### Commands

```shell
# I assume that buf 1.50.0 is installed and on your PATH.

echo -n '' | \
buf convert \
  foo.proto \
  --type=M \
  --from=-#format=binpb \
  --to=-#format=txtpb
```

### Output

```shell
Failure: --from: length of data read from "-#format=binpb" was zero
```

### Expected Output

None. Not even a trailing newline. Literally zero bytes.

### Anything else?

`buf convert` itself is able to produce 0 length encoded message in binpb format. Therefore, according to the tool itself, 0 length encoding is valid. For example:

```
$ echo '' | \   
buf convert \
  foo.proto \
  --type=M \
  --from=-#format=txtpb \
  --to=-#format=binpb | \
wc
      0       0       0
```

Pull request:
Allow zero length for binpb, txtpb, and yaml for `buf convert`
This removes the zero length check for `buf convert`, allowing empty
inputs for `binpb`, `txtpb`, and `yaml` formats. Empty is valid for these
formats. We return the unmarshal error for `json`, since null/empty is not
valid for json (we expect a json object).

Fixes #3663

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c0654782..b65552b6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
 
 ## [Unreleased]
 
-- No changes yet.
+- Fix `buf convert` to allow for zero length for `binpb`, `txtpb`, and `yaml` formats.
 
 ## [v1.50.1] - 2025-03-10
 
diff --git a/private/buf/bufctl/controller.go b/private/buf/bufctl/controller.go
index c57ba1066..f2a514970 100644
--- a/private/buf/bufctl/controller.go
+++ b/private/buf/bufctl/controller.go
@@ -725,9 +725,6 @@ func (c *controller) GetMessage(
 	if err != nil {
 		return nil, 0, err
 	}
-	if len(data) == 0 {
-		return nil, 0, fmt.Errorf("length of data read from %q was zero", messageInput)
-	}
 	message, err := bufreflect.NewMessage(ctx, schemaImage, typeName)
 	if err != nil {
 		return nil, 0, err
diff --git a/private/pkg/protoencoding/json_marshaler.go b/private/pkg/protoencoding/json_marshaler.go
index 0f5aa6864..a9d2c6270 100644
--- a/private/pkg/protoencoding/json_marshaler.go
+++ b/private/pkg/protoencoding/json_marshaler.go
@@ -15,6 +15,8 @@
 package protoencoding
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/proto"
 )
@@ -51,5 +53,9 @@ func (m *jsonMarshaler) Marshal(message proto.Message) ([]byte, error) {
 		EmitUnpopulated: m.emitUnpopulated,
 		Indent:          m.indent,
 	}
-	return options.Marshal(message)
+	data, err := options.Marshal(message)
+	if err != nil {
+		return nil, fmt.Errorf("json marshal: %w", err)
+	}
+	return data, err
 }
diff --git a/private/pkg/protoencoding/json_unmarshaler.go b/private/pkg/protoencoding/json_unmarshaler.go
index 3f15316db..6ade5603a 100644
--- a/private/pkg/protoencoding/json_unmarshaler.go
+++ b/private/pkg/protoencoding/json_unmarshaler.go
@@ -15,6 +15,8 @@
 package protoencoding
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/proto"
 )
@@ -42,5 +44,8 @@ func (m *jsonUnmarshaler) Unmarshal(data []byte, message proto.Message) error {
 		Resolver:       m.resolver,
 		DiscardUnknown: !m.disallowUnknown,
 	}
-	return options.Unmarshal(data, message)
+	if err := options.Unmarshal(data, message); err != nil {
+		return fmt.Errorf("json unmarshal: %w", err)
+	}
+	return nil
 }
diff --git a/private/pkg/protoencoding/txtpb_marshaler.go b/private/pkg/protoencoding/txtpb_marshaler.go
index 99ed36398..380d36340 100644
--- a/private/pkg/protoencoding/txtpb_marshaler.go
+++ b/private/pkg/protoencoding/txtpb_marshaler.go
@@ -15,6 +15,8 @@
 package protoencoding
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/proto"
 )
@@ -37,5 +39,9 @@ func (m *txtpbMarshaler) Marshal(message proto.Message) ([]byte, error) {
 		Resolver: m.resolver,
 		Indent:   "  ",
 	}
-	return options.Marshal(message)
+	data, err := options.Marshal(message)
+	if err != nil {
+		return nil, fmt.Errorf("txtpb marshal: %w", err)
+	}
+	return data, err
 }
diff --git a/private/pkg/protoencoding/txtpb_unmarshaler.go b/private/pkg/protoencoding/txtpb_unmarshaler.go
index 84a1fecba..b2565e223 100644
--- a/private/pkg/protoencoding/txtpb_unmarshaler.go
+++ b/private/pkg/protoencoding/txtpb_unmarshaler.go
@@ -15,6 +15,8 @@
 package protoencoding
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/proto"
 )
@@ -38,5 +40,8 @@ func (m *txtpbUnmarshaler) Unmarshal(data []byte, message proto.Message) error {
 		// TODO: make this an option
 		DiscardUnknown: true,
 	}
-	return options.Unmarshal(data, message)
+	if err := options.Unmarshal(data, message); err != nil {
+		return fmt.Errorf("txtpb unmarshal: %w", err)
+	}
+	return nil
 }
diff --git a/private/pkg/protoencoding/wire_marshaler.go b/private/pkg/protoencoding/wire_marshaler.go
index 3943ba438..20d7286ad 100644
--- a/private/pkg/protoencoding/wire_marshaler.go
+++ b/private/pkg/protoencoding/wire_marshaler.go
@@ -15,6 +15,8 @@
 package protoencoding
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
 )
 
@@ -28,5 +30,9 @@ func (m *wireMarshaler) Marshal(message proto.Message) ([]byte, error) {
 	options := proto.MarshalOptions{
 		Deterministic: true,
 	}
-	return options.Marshal(message)
+	data, err := options.Marshal(message)
+	if err != nil {
+		return nil, fmt.Errorf("wire marshal: %w", err)
+	}
+	return data, err
 }
diff --git a/private/pkg/protoencoding/wire_unmarshaler.go b/private/pkg/protoencoding/wire_unmarshaler.go
index 197b8d62c..c992d1b44 100644
--- a/private/pkg/protoencoding/wire_unmarshaler.go
+++ b/private/pkg/protoencoding/wire_unmarshaler.go
@@ -15,6 +15,8 @@
 package protoencoding
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
 )
 
@@ -35,5 +37,8 @@ func (m *wireUnmarshaler) Unmarshal(data []byte, message proto.Message) error {
 	options := proto.UnmarshalOptions{
 		Resolver: m.resolver,
 	}
-	return options.Unmarshal(data, message)
+	if err := options.Unmarshal(data, message); err != nil {
+		return fmt.Errorf("wire unmarshal: %w", err)
+	}
+	return nil
 }
diff --git a/private/pkg/protoencoding/yaml_marshaler.go b/private/pkg/protoencoding/yaml_marshaler.go
index c5820722d..d6d0aded8 100644
--- a/private/pkg/protoencoding/yaml_marshaler.go
+++ b/private/pkg/protoencoding/yaml_marshaler.go
@@ -15,6 +15,8 @@
 package protoencoding
 
 import (
+	"fmt"
+
 	"buf.build/go/protoyaml"
 	"google.golang.org/protobuf/proto"
 )
@@ -51,5 +53,9 @@ func (m *yamlMarshaler) Marshal(message proto.Message) ([]byte, error) {
 		UseEnumNumbers:  m.useEnumNumbers,
 		EmitUnpopulated: m.emitUnpopulated,
 	}
-	return options.Marshal(message)
+	data, err := options.Marshal(message)
+	if err != nil {
+		return nil, fmt.Errorf("yaml marshal: %w", err)
+	}
+	return data, err
 }
diff --git a/private/pkg/protoencoding/yaml_unmarshaler.go b/private/pkg/protoencoding/yaml_unmarshaler.go
index 0324937f1..4e0828c18 100644
--- a/private/pkg/protoencoding/yaml_unmarshaler.go
+++ b/private/pkg/protoencoding/yaml_unmarshaler.go
@@ -15,6 +15,8 @@
 package protoencoding
 
 import (
+	"fmt"
+
 	"buf.build/go/protoyaml"
 	"google.golang.org/protobuf/proto"
 )
@@ -44,5 +46,8 @@ func (m *yamlUnmarshaler) Unmarshal(data []byte, message proto.Message) error {
 		Validator: m.validator,
 		Path:      m.path,
 	}
-	return options.Unmarshal(data, message)
+	if err := options.Unmarshal(data, message); err != nil {
+		return fmt.Errorf("yaml unmarshal: %w", err)
+	}
+	return nil
 }


Test patch:
diff --git a/private/buf/cmd/buf/buf_test.go b/private/buf/cmd/buf/buf_test.go
index 698504aff..600b15943 100644
--- a/private/buf/cmd/buf/buf_test.go
+++ b/private/buf/cmd/buf/buf_test.go
@@ -3576,7 +3576,6 @@ func TestConvertWithImage(t *testing.T) {
 		"-o",
 		filepath.Join(tempDir, "image.binpb"),
 	)
-
 	t.Run("stdin input", func(t *testing.T) {
 		t.Parallel()
 		stdin, err := os.Open(filepath.Join(convertTestDataDir, "descriptor.plain.binpb"))
@@ -3596,18 +3595,69 @@ func TestConvertWithImage(t *testing.T) {
 		assert.JSONEq(t, `{"one":"55"}`, stdout.String())
 	})
 
-	t.Run("no stdin input", func(t *testing.T) {
+	t.Run("no stdin input from binpb", func(t *testing.T) {
+		t.Parallel()
+		testRun(
+			t,
+			0,
+			nil,
+			nil,
+			"convert",
+			filepath.Join(tempDir, "image.binpb"),
+			"--type",
+			"buf.Foo",
+			"--from=-#format=binpb",
+			"--to=-#format=txtpb",
+		)
+	})
+	t.Run("no stdin input from txtpb", func(t *testing.T) {
 		t.Parallel()
-		testRunStdoutStderrNoWarn(
+		testRun(
+			t,
+			0,
+			nil,
+			nil,
+			"convert",
+			filepath.Join(tempDir, "image.binpb"),
+			"--type",
+			"buf.Foo",
+			"--from=-#format=txtpb",
+			"--to=-#format=binpb",
+		)
+	})
+	t.Run("no stdin input from yaml", func(t *testing.T) {
+		t.Parallel()
+		testRun(
+			t,
+			0,
+			nil,
+			nil,
+			"convert",
+			filepath.Join(tempDir, "image.binpb"),
+			"--type",
+			"buf.Foo",
+			"--from=-#format=yaml",
+			"--to=-#format=binpb",
+		)
+	})
+	t.Run("no stdin input from json", func(t *testing.T) {
+		t.Parallel()
+		testRunStderrContainsNoWarn(
 			t,
 			nil,
 			1,
-			"",
-			`Failure: --from: length of data read from "-" was zero`,
+			[]string{
+				"Failure: --from:",
+				"json unmarshal:",
+				"proto:",
+				"syntax error (line 1:1): unexpected token",
+			},
 			"convert",
 			filepath.Join(tempDir, "image.binpb"),
 			"--type",
 			"buf.Foo",
+			"--from=-#format=json",
+			"--to=-#format=binpb",
 		)
 	})
 }