diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..07eb590 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,19 @@ +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + - name: Install Protoc + run: | + sudo apt install -y protobuf-compiler + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 + - name: Test + run: make test + - name: Build + run: make build diff --git a/Makefile b/Makefile index 755bd29..8724135 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ build: - go build -o protodump cmd/main.go + go build -o protodump cmd/protodump/main.go + +fmt: + go fmt ./... test: go test -v ./... diff --git a/README.md b/README.md index c7bad10..842e98f 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ go build -o protodump cmd/protodump/main.go ./protodump -file -output ``` -This was thrown together in a day and still has bugs :) - ## Getting in touch Feel free to contact me on twitter: https://twitter.com/arkadiyt diff --git a/cmd/protodump/main.go b/cmd/protodump/main.go index bd318eb..4d8995a 100644 --- a/cmd/protodump/main.go +++ b/cmd/protodump/main.go @@ -1,7 +1,7 @@ package main import ( - "flag" + "flag" "fmt" "log" "os" diff --git a/pkg/protodump/fixtures/test.proto b/pkg/protodump/fixtures/test.proto new file mode 100644 index 0000000..62fc14b --- /dev/null +++ b/pkg/protodump/fixtures/test.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package helloworld; + +option go_package = "./;helloworld"; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc StreamMethodServer (StreamRequest) returns (stream StreamReply) {} +} + +message HelloRequest { + message Test { + string blah = 1; + } + + enum NestedEnum { + BLAH = 0; + } + + string name = 1; + helloworld.HelloRequest.Test blah = 2; + helloworld.HelloRequest.NestedEnum blahblah = 3; + helloworld.HelloReply blaha = 4; +} + +message HelloReply { + helloworld.HelloRequest.Test blah = 2; + helloworld.HelloRequest.NestedEnum blahblah = 3; + string message = 1; +} + +message StreamRequest { + string message = 1; +} + +message StreamReply { + string message = 1; +} + diff --git a/pkg/protodump/proto.go b/pkg/protodump/proto.go index f4a2b87..987df33 100644 --- a/pkg/protodump/proto.go +++ b/pkg/protodump/proto.go @@ -2,6 +2,7 @@ package protodump import ( "fmt" + "reflect" "strconv" "strings" @@ -204,6 +205,60 @@ func (pd *ProtoDefinition) writeImport(fileImport protoreflect.FileImport) { pd.write("\";\n") } +func (pd *ProtoDefinition) writeStringFileOptions(name string, value string) { + pd.write("option ") + pd.write(name) + pd.write(" = \"") + pd.write(value) + pd.write("\";\n") +} + +func (pd *ProtoDefinition) writeBoolFileOptions(name string, value bool) { + pd.writeStringFileOptions(name, strconv.FormatBool(value)) +} + +func (pd *ProtoDefinition) writeFileOptions() { + optionDefinitions := []struct { + OptionName string + FieldName string + }{ + {"java_package", "JavaPackage"}, + {"java_outer_classname", "JavaOuterClassname"}, + {"java_multiple_files", "JavaMultipleFiles"}, + {"java_string_check_utf8", "JavaStringCheckUtf8"}, + // TODO OptimizeMode: https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto#L384 + {"go_package", "GoPackage"}, + // TODO generic services: https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto#L403 + // TODO deprecated: https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto#L412 + {"cc_enable_arenas", "CcEnableArenas"}, + {"objc_class_prefix", "ObjcClassPrefix"}, + {"csharp_namespace", "CsharpNamespace"}, + {"swift_prefix", "SwiftPrefix"}, + {"php_class_prefix", "PhpClassPrefix"}, + {"php_namespace", "PhpNamespace"}, + {"php_metadata_namespace", "PhpMetadataNamespace"}, + {"ruby_package", "RubyPackage"}, + } + options := reflect.ValueOf(pd.pb.GetOptions()).Elem() + printedOption := false + for _, option := range optionDefinitions { + elemPtr := options.FieldByName(option.FieldName) + if !elemPtr.IsNil() { + elem := elemPtr.Elem() + if elem.Kind() == reflect.String { + pd.writeStringFileOptions(option.OptionName, elem.String()) + } else if elem.Kind() == reflect.Bool { + pd.writeBoolFileOptions(option.OptionName, elem.Bool()) + } + printedOption = true + } + } + + if printedOption { + pd.write("\n") + } +} + func (pd *ProtoDefinition) writeFileDescriptor() { pd.write("syntax = \"") pd.write(pd.descriptor.Syntax().String()) @@ -213,11 +268,7 @@ func (pd *ProtoDefinition) writeFileDescriptor() { pd.write(string(pd.descriptor.Package().Name())) pd.write(";\n\n") - // TODO need to handle FileOptions - filepb := protodesc.ToFileDescriptorProto(pd.descriptor) - options := filepb.Options - if options != nil { - } + pd.writeFileOptions() for i := 0; i < pd.descriptor.Imports().Len(); i++ { pd.writeImport(pd.descriptor.Imports().Get(i)) diff --git a/pkg/protodump/proto_test.go b/pkg/protodump/proto_test.go index 054ed96..49c767d 100644 --- a/pkg/protodump/proto_test.go +++ b/pkg/protodump/proto_test.go @@ -1,18 +1,78 @@ package protodump import ( + "bytes" + "encoding/hex" + "fmt" "os" + "os/exec" + "path" + "regexp" + "strings" "testing" "github.com/stretchr/testify/assert" ) +const FIXTURES = "fixtures" + +var regex = regexp.MustCompile("(?m)var .* = \\[\\]byte{([^}]*)}") + +func convertProtoToFileDescriptor(filePath string) ([]byte, error) { + dir, err := os.MkdirTemp("", "") + if err != nil { + return nil, err + } + defer os.RemoveAll(dir) + + err = exec.Command("protoc", fmt.Sprintf("--go_out=%s", dir), filePath).Run() + if err != nil { + return nil, err + } + + fileName := fmt.Sprintf("%s.pb.go", strings.TrimSuffix(path.Base(filePath), ".proto")) + data, err := os.ReadFile(path.Join(dir, fileName)) + if err != nil { + return nil, err + } + + match := regex.FindSubmatch(data) + if len(match) != 2 { + return nil, fmt.Errorf("Could not find file descriptor bytes in %s", fileName) + } + + var hexString strings.Builder + for _, char := range strings.ToLower(string(bytes.ReplaceAll(match[1], []byte("0x"), []byte{}))) { + r := rune(char) + if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') { + hexString.WriteRune(r) + } + } + + result, err := hex.DecodeString(hexString.String()) + if err != nil { + return nil, err + } + return result, nil +} + func TestDefinitions(t *testing.T) { - files, err := os.ReadDir("fixtures") + files, err := os.ReadDir(FIXTURES) assert.NoError(t, err) for _, file := range files { t.Run(file.Name(), func(t *testing.T) { + filePath := path.Join(FIXTURES, file.Name()) + descriptor, err := convertProtoToFileDescriptor(filePath) + assert.NoError(t, err) + + expected, err := os.ReadFile(filePath) + assert.NoError(t, err) + + actual, err := NewFromBytes(descriptor) + assert.NoError(t, err) + + assert.Equal(t, string(expected), actual.String()) }) } }