diff --git a/example/proto2_gogo_test.go b/example/proto2_gogo_test.go index b472235..b61ee1c 100644 --- a/example/proto2_gogo_test.go +++ b/example/proto2_gogo_test.go @@ -148,6 +148,23 @@ func TestProto2GogoMarshalJSON(t *testing.T) { }) } +func TestProto2GogoMarshalText(t *testing.T) { + msg := createTestProto2GogoMessage() + // replace the current date/time with a known value for reproducible output + now := time.Date(2000, time.January, 1, 1, 2, 3, 0, time.UTC) + msg.Timestamp = proto.Uint64(uint64(now.Unix())) + // NOTE: the prototext format is explicitly documented as not stable + // - this string matches gogo/protobuf@v1.3.2 + // - if this test breaks after updating gogo/protobuf, then update the expected string + // accordingly + expected := "eventID: \"test-event\"\nsourceID: \"test-source\"\ntimestamp: 946688523\neventType: EVENT_TYPE_ONE\ndata: \"\"\n[crowdstrike.csproto.example.proto2.gogo.TestEvent.eventExt]: <\n name: \"test\"\n info: \"\"\n labels: \"one\"\n labels: \"two\"\n labels: \"three\"\n embedded: <\n ID: 42\n stuff: \"some stuff\"\n favoriteNumbers: 42\n favoriteNumbers: 1138\n >\n jedi: true\n nested: <\n details: \"these are some nested details\"\n >\n>\n" + + s, err := csproto.MarshalText(msg) + + assert.NoError(t, err) + assert.Equal(t, expected, s) +} + func createTestProto2GogoMessage() *gogo.BaseEvent { now := uint64(time.Now().UTC().Unix()) et := gogo.EventType_EVENT_TYPE_ONE diff --git a/example/proto2_googlev1_test.go b/example/proto2_googlev1_test.go index 1d4e9f0..c2591f3 100644 --- a/example/proto2_googlev1_test.go +++ b/example/proto2_googlev1_test.go @@ -157,6 +157,27 @@ func TestProto2GoogleV1MarshalJSON(t *testing.T) { }) } +func TestProto2GoogleV1MarshalText(t *testing.T) { + msg := createTestProto2GoogleV1Message() + // replace the current date/time with a known value for reproducible output + now := time.Date(2000, time.January, 1, 1, 2, 3, 0, time.UTC) + msg.Timestamp = proto.Uint64(uint64(now.Unix())) + // NOTE: the prototext format is explicitly documented as not stable + // - this string matches github.com/golang/protobuf@v1.5.2 + // - if this test breaks after updating golang/protobuf, then update the expected string + // accordingly + expected := "eventID: \"test-event\"\nsourceID: \"test-source\"\ntimestamp: 946688523\neventType: EVENT_TYPE_ONE\ndata: \"\"\n[crowdstrike.csproto.example.proto2.googlev1.TestEvent.eventExt]: {\n name: \"test\"\n info: \"\"\n labels: \"one\"\n labels: \"two\"\n labels: \"three\"\n embedded: {\n ID: 42\n stuff: \"some stuff\"\n favoriteNumbers: 42\n favoriteNumbers: 1138\n }\n jedi: true\n nested: {\n details: \"these are some nested details\"\n }\n}\n" + + s, err := csproto.MarshalText(msg) + // replace ": " with ": " to undo the Google library's intentional randomization of the output :( + // see: https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/internal/encoding/text/encode.go#L226 + // https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/internal/encoding/text/encode.go#L238 + s = strings.ReplaceAll(s, ": ", ": ") + + assert.NoError(t, err) + assert.Equal(t, expected, s) +} + func createTestProto2GoogleV1Message() *googlev1.BaseEvent { now := uint64(time.Now().UTC().Unix()) et := googlev1.EventType_EVENT_TYPE_ONE diff --git a/example/proto2_googlev2_test.go b/example/proto2_googlev2_test.go index 2f57aa4..8766a06 100644 --- a/example/proto2_googlev2_test.go +++ b/example/proto2_googlev2_test.go @@ -157,6 +157,27 @@ func TestProto2GoogleV2MarshalJSON(t *testing.T) { }) } +func TestProto2GoogleV2MarshalText(t *testing.T) { + msg := createTestProto2GoogleV2Message() + // replace the current date/time with a known value for reproducible output + now := time.Date(2000, time.January, 1, 1, 2, 3, 0, time.UTC) + msg.Timestamp = proto.Uint64(uint64(now.Unix())) + // NOTE: the prototext format is explicitly documented as not stable + // - this string matches google.golang.org/protobuf@v1.28.1 + // - if this test breaks after updating google.golang.org/protobuf, then update the expected string + // accordingly + expected := "eventID: \"test-event\"\nsourceID: \"test-source\"\ntimestamp: 946688523\neventType: EVENT_TYPE_ONE\ndata: \"\"\n[crowdstrike.csproto.example.proto2.googlev2.TestEvent.eventExt]: {\n name: \"test\"\n info: \"\"\n labels: \"one\"\n labels: \"two\"\n labels: \"three\"\n embedded: {\n ID: 42\n stuff: \"some stuff\"\n favoriteNumbers: 42\n favoriteNumbers: 1138\n }\n jedi: true\n nested: {\n details: \"these are some nested details\"\n }\n}\n" + + s, err := csproto.MarshalText(msg) + // replace ": " with ": " to undo the Google library's intentional randomization of the output :( + // see: https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/internal/encoding/text/encode.go#L226 + // https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/internal/encoding/text/encode.go#L238 + s = strings.ReplaceAll(s, ": ", ": ") + + assert.NoError(t, err) + assert.Equal(t, expected, s) +} + func createTestProto2GoogleV2Message() *googlev2.BaseEvent { now := uint64(time.Now().UTC().Unix()) et := googlev2.EventType_EVENT_TYPE_ONE diff --git a/example/proto3_gogo_test.go b/example/proto3_gogo_test.go index b230809..2526a03 100644 --- a/example/proto3_gogo_test.go +++ b/example/proto3_gogo_test.go @@ -3,6 +3,7 @@ package example_test import ( "fmt" "testing" + "time" "github.com/gogo/protobuf/proto" "github.com/gogo/protobuf/types" @@ -129,6 +130,23 @@ func TestProto3GogoMarshalJSON(t *testing.T) { }) } +func TestProto3GogoMarshalText(t *testing.T) { + msg := createTestProto3GogoMessage() + // replace the current date/time with a known value for reproducible output + now := time.Date(2000, time.January, 1, 1, 2, 3, 0, time.UTC) + msg.Ts, _ = types.TimestampProto(now) + // NOTE: the prototext format is explicitly documented as not stable + // - this string matches gogo/protobuf@v1.3.2 + // - if this test breaks after updating gogo/protobuf, then update the expected string + // accordingly + expected := "name: \"test\"\nlabels: \"one\"\nlabels: \"two\"\nlabels: \"three\"\nembedded: <\n ID: 42\n stuff: \"some stuff\"\n favoriteNumbers: 42\n favoriteNumbers: 1138\n>\njedi: true\nnested: <\n details: \"these are some nested details\"\n>\nts: <\n seconds: 946688523\n>\n" + + s, err := csproto.MarshalText(msg) + + assert.NoError(t, err) + assert.Equal(t, expected, s) +} + func createTestProto3GogoMessage() *gogo.TestEvent { event := gogo.TestEvent{ Name: "test", diff --git a/example/proto3_googlev1_test.go b/example/proto3_googlev1_test.go index 0bd9823..141fd94 100644 --- a/example/proto3_googlev1_test.go +++ b/example/proto3_googlev1_test.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "testing" + "time" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" @@ -133,6 +134,27 @@ func TestProto3GoogleV1MarshalJSON(t *testing.T) { }) } +func TestProto3GoogleV1MarshalText(t *testing.T) { + msg := createTestProto3GoogleV1Message() + // replace the current date/time with a known value for reproducible output + ts := time.Date(2000, time.January, 1, 1, 2, 3, 0, time.UTC) + msg.Ts = timestamppb.New(ts) + // NOTE: the prototext format is explicitly documented as not stable + // - this string matches github.com/golang/protobuf@v1.5.2 + // - if this test breaks after updating golang/protobuf, then update the expected string + // accordingly + expected := "name: \"test\"\nlabels: \"one\"\nlabels: \"two\"\nlabels: \"three\"\nembedded: {\n ID: 42\n stuff: \"some stuff\"\n favoriteNumbers: 42\n favoriteNumbers: 1138\n}\njedi: true\nnested: {\n details: \"these are some nested details\"\n}\nts: {\n seconds: 946688523\n}\n" + + s, err := csproto.MarshalText(msg) + // replace ": " with ": " to undo the Google library's intentional randomization of the output :( + // see: https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/internal/encoding/text/encode.go#L226 + // https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/internal/encoding/text/encode.go#L238 + s = strings.ReplaceAll(s, ": ", ": ") + + assert.NoError(t, err) + assert.Equal(t, expected, s) +} + func createTestProto3GoogleV1Message() *googlev1.TestEvent { event := googlev1.TestEvent{ Name: "test", diff --git a/example/proto3_googlev2_test.go b/example/proto3_googlev2_test.go index 64ccda7..7e7141b 100644 --- a/example/proto3_googlev2_test.go +++ b/example/proto3_googlev2_test.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/proto" @@ -133,6 +134,27 @@ func TestProto3GoogleV2MarshalJSON(t *testing.T) { }) } +func TestProto3GoogleV2MarshalText(t *testing.T) { + msg := createTestProto3GoogleV2Message() + // replace the current date/time with a known value for reproducible output + ts := time.Date(2000, time.January, 1, 1, 2, 3, 0, time.UTC) + msg.Ts = timestamppb.New(ts) + // NOTE: the prototext format is explicitly documented as not stable + // - this string matches google.golang.org/protobuf@v1.28.1 + // - if this test breaks after updating google.golang.org/protobuf, then update the expected string + // accordingly + expected := "name: \"test\"\nlabels: \"one\"\nlabels: \"two\"\nlabels: \"three\"\nembedded: {\n ID: 42\n stuff: \"some stuff\"\n favoriteNumbers: 42\n favoriteNumbers: 1138\n}\njedi: true\nnested: {\n details: \"these are some nested details\"\n}\nts: {\n seconds: 946688523\n}\n" + + s, err := csproto.MarshalText(msg) + // replace ": " with ": " to undo the Google library's intentional randomization of the output :( + // see: https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/internal/encoding/text/encode.go#L226 + // https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/internal/encoding/text/encode.go#L238 + s = strings.ReplaceAll(s, ": ", ": ") + + assert.NoError(t, err) + assert.Equal(t, expected, s) +} + func createTestProto3GoogleV2Message() *googlev2.TestEvent { event := googlev2.TestEvent{ Name: "test", diff --git a/marshal_text.go b/marshal_text.go new file mode 100644 index 0000000..e7d82e6 --- /dev/null +++ b/marshal_text.go @@ -0,0 +1,33 @@ +package csproto + +import ( + "encoding" + "fmt" + + gogo "github.com/gogo/protobuf/proto" + googlev1 "github.com/golang/protobuf/proto" //nolint: staticcheck // we're using this deprecated package intentionally" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" +) + +// MarshalText converts the specified message to prototext string format +func MarshalText(msg interface{}) (string, error) { + if tm, ok := msg.(encoding.TextMarshaler); ok { + res, err := tm.MarshalText() + if err != nil { + return "", err + } + return string(res), nil + } + + switch MsgType(msg) { + case MessageTypeGoogle: + return prototext.Format(msg.(proto.Message)), nil + case MessageTypeGoogleV1: + return googlev1.MarshalTextString(msg.(googlev1.Message)), nil + case MessageTypeGogo: + return gogo.MarshalTextString(msg.(gogo.Message)), nil + default: + return "", fmt.Errorf("unsupported message type: %T", msg) + } +}