blob: ab93032d88ea50ae3e1af19e5b5eac11a4cca01c [file] [log] [blame]
package desc
import (
"fmt"
"path/filepath"
"reflect"
"strings"
"sync"
"github.com/golang/protobuf/proto"
dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
)
var (
globalImportPathConf map[string]string
globalImportPathMu sync.RWMutex
)
// RegisterImportPath registers an alternate import path for a given registered
// proto file path. For more details on why alternate import paths may need to
// be configured, see ImportResolver.
//
// This method panics if provided invalid input. An empty importPath is invalid.
// An un-registered registerPath is also invalid. For example, if an attempt is
// made to register the import path "foo/bar.proto" as "bar.proto", but there is
// no "bar.proto" registered in the Go protobuf runtime, this method will panic.
// This method also panics if an attempt is made to register the same import
// path more than once.
//
// This function works globally, applying to all descriptors loaded by this
// package. If you instead want more granular support for handling alternate
// import paths -- such as for a single invocation of a function in this
// package or when the alternate path is only used from one file (so you don't
// want the alternate path used when loading every other file), use an
// ImportResolver instead.
func RegisterImportPath(registerPath, importPath string) {
if len(importPath) == 0 {
panic("import path cannot be empty")
}
desc := proto.FileDescriptor(registerPath)
if len(desc) == 0 {
panic(fmt.Sprintf("path %q is not a registered proto file", registerPath))
}
globalImportPathMu.Lock()
defer globalImportPathMu.Unlock()
if reg := globalImportPathConf[importPath]; reg != "" {
panic(fmt.Sprintf("import path %q already registered for %s", importPath, reg))
}
if globalImportPathConf == nil {
globalImportPathConf = map[string]string{}
}
globalImportPathConf[importPath] = registerPath
}
// ResolveImport resolves the given import path. If it has been registered as an
// alternate via RegisterImportPath, the registered path is returned. Otherwise,
// the given import path is returned unchanged.
func ResolveImport(importPath string) string {
importPath = clean(importPath)
globalImportPathMu.RLock()
defer globalImportPathMu.RUnlock()
reg := globalImportPathConf[importPath]
if reg == "" {
return importPath
}
return reg
}
// ImportResolver lets you work-around linking issues that are caused by
// mismatches between how a particular proto source file is registered in the Go
// protobuf runtime and how that same file is imported by other files. The file
// is registered using the same relative path given to protoc when the file is
// compiled (i.e. when Go code is generated). So if any file tries to import
// that source file, but using a different relative path, then a link error will
// occur when this package tries to load a descriptor for the importing file.
//
// For example, let's say we have two proto source files: "foo/bar.proto" and
// "fubar/baz.proto". The latter imports the former using a line like so:
// import "foo/bar.proto";
// However, when protoc is invoked, the command-line args looks like so:
// protoc -Ifoo/ --go_out=foo/ bar.proto
// protoc -I./ -Ifubar/ --go_out=fubar/ baz.proto
// Because the path given to protoc is just "bar.proto" and "baz.proto", this is
// how they are registered in the Go protobuf runtime. So, when loading the
// descriptor for "fubar/baz.proto", we'll see an import path of "foo/bar.proto"
// but will find no file registered with that path:
// fd, err := desc.LoadFileDescriptor("baz.proto")
// // err will be non-nil, complaining that there is no such file
// // found named "foo/bar.proto"
//
// This can be remedied by registering alternate import paths using an
// ImportResolver. Continuing with the example above, the code below would fix
// any link issue:
// var r desc.ImportResolver
// r.RegisterImportPath("bar.proto", "foo/bar.proto")
// fd, err := r.LoadFileDescriptor("baz.proto")
// // err will be nil; descriptor successfully loaded!
//
// If there are files that are *always* imported using a different relative
// path then how they are registered, consider using the global
// RegisterImportPath function, so you don't have to use an ImportResolver for
// every file that imports it.
type ImportResolver struct {
children map[string]*ImportResolver
importPaths map[string]string
// By default, an ImportResolver will fallback to consulting any paths
// registered via the top-level RegisterImportPath function. Setting this
// field to true will cause the ImportResolver to skip that fallback and
// only examine its own locally registered paths.
SkipFallbackRules bool
}
// ResolveImport resolves the given import path in the context of the given
// source file. If a matching alternate has been registered with this resolver
// via a call to RegisterImportPath or RegisterImportPathFrom, then the
// registered path is returned. Otherwise, the given import path is returned
// unchanged.
func (r *ImportResolver) ResolveImport(source, importPath string) string {
if r != nil {
res := r.resolveImport(clean(source), clean(importPath))
if res != "" {
return res
}
if r.SkipFallbackRules {
return importPath
}
}
return ResolveImport(importPath)
}
func (r *ImportResolver) resolveImport(source, importPath string) string {
if source == "" {
return r.importPaths[importPath]
}
var car, cdr string
idx := strings.IndexRune(source, filepath.Separator)
if idx < 0 {
car, cdr = source, ""
} else {
car, cdr = source[:idx], source[idx+1:]
}
ch := r.children[car]
if ch != nil {
if reg := ch.resolveImport(cdr, importPath); reg != "" {
return reg
}
}
return r.importPaths[importPath]
}
// RegisterImportPath registers an alternate import path for a given registered
// proto file path with this resolver. Any appearance of the given import path
// when linking files will instead try to link the given registered path. If the
// registered path cannot be located, then linking will fallback to the actual
// imported path.
//
// This method will panic if given an empty path or if the same import path is
// registered more than once.
//
// To constrain the contexts where the given import path is to be re-written,
// use RegisterImportPathFrom instead.
func (r *ImportResolver) RegisterImportPath(registerPath, importPath string) {
r.RegisterImportPathFrom(registerPath, importPath, "")
}
// RegisterImportPathFrom registers an alternate import path for a given
// registered proto file path with this resolver, but only for imports in the
// specified source context.
//
// The source context can be the name of a folder or a proto source file. Any
// appearance of the given import path in that context will instead try to link
// the given registered path. To be in context, the file that is being linked
// (i.e. the one whose import statement is being resolved) must be the same
// relative path of the source context or be a sub-path (i.e. a descendant of
// the source folder).
//
// If the registered path cannot be located, then linking will fallback to the
// actual imported path.
//
// This method will panic if given an empty path. The source context, on the
// other hand, is allowed to be blank. A blank source matches all files. This
// method also panics if the same import path is registered in the same source
// context more than once.
func (r *ImportResolver) RegisterImportPathFrom(registerPath, importPath, source string) {
importPath = clean(importPath)
if len(importPath) == 0 {
panic("import path cannot be empty")
}
registerPath = clean(registerPath)
if len(registerPath) == 0 {
panic("registered path cannot be empty")
}
r.registerImportPathFrom(registerPath, importPath, clean(source))
}
func (r *ImportResolver) registerImportPathFrom(registerPath, importPath, source string) {
if source == "" {
if r.importPaths == nil {
r.importPaths = map[string]string{}
} else if reg := r.importPaths[importPath]; reg != "" {
panic(fmt.Sprintf("already registered import path %q as %q", importPath, registerPath))
}
r.importPaths[importPath] = registerPath
return
}
var car, cdr string
idx := strings.IndexRune(source, filepath.Separator)
if idx < 0 {
car, cdr = source, ""
} else {
car, cdr = source[:idx], source[idx+1:]
}
ch := r.children[car]
if ch == nil {
if r.children == nil {
r.children = map[string]*ImportResolver{}
}
ch = &ImportResolver{}
r.children[car] = ch
}
ch.registerImportPathFrom(registerPath, importPath, cdr)
}
// LoadFileDescriptor is the same as the package function of the same name, but
// any alternate paths configured in this resolver are used when linking the
// given descriptor proto.
func (r *ImportResolver) LoadFileDescriptor(filePath string) (*FileDescriptor, error) {
return loadFileDescriptor(filePath, r)
}
// LoadMessageDescriptor is the same as the package function of the same name,
// but any alternate paths configured in this resolver are used when linking
// files for the returned descriptor.
func (r *ImportResolver) LoadMessageDescriptor(msgName string) (*MessageDescriptor, error) {
return loadMessageDescriptor(msgName, r)
}
// LoadMessageDescriptorForMessage is the same as the package function of the
// same name, but any alternate paths configured in this resolver are used when
// linking files for the returned descriptor.
func (r *ImportResolver) LoadMessageDescriptorForMessage(msg proto.Message) (*MessageDescriptor, error) {
return loadMessageDescriptorForMessage(msg, r)
}
// LoadMessageDescriptorForType is the same as the package function of the same
// name, but any alternate paths configured in this resolver are used when
// linking files for the returned descriptor.
func (r *ImportResolver) LoadMessageDescriptorForType(msgType reflect.Type) (*MessageDescriptor, error) {
return loadMessageDescriptorForType(msgType, r)
}
// LoadEnumDescriptorForEnum is the same as the package function of the same
// name, but any alternate paths configured in this resolver are used when
// linking files for the returned descriptor.
func (r *ImportResolver) LoadEnumDescriptorForEnum(enum protoEnum) (*EnumDescriptor, error) {
return loadEnumDescriptorForEnum(enum, r)
}
// LoadEnumDescriptorForType is the same as the package function of the same
// name, but any alternate paths configured in this resolver are used when
// linking files for the returned descriptor.
func (r *ImportResolver) LoadEnumDescriptorForType(enumType reflect.Type) (*EnumDescriptor, error) {
return loadEnumDescriptorForType(enumType, r)
}
// LoadFieldDescriptorForExtension is the same as the package function of the
// same name, but any alternate paths configured in this resolver are used when
// linking files for the returned descriptor.
func (r *ImportResolver) LoadFieldDescriptorForExtension(ext *proto.ExtensionDesc) (*FieldDescriptor, error) {
return loadFieldDescriptorForExtension(ext, r)
}
// CreateFileDescriptor is the same as the package function of the same name,
// but any alternate paths configured in this resolver are used when linking the
// given descriptor proto.
func (r *ImportResolver) CreateFileDescriptor(fdp *dpb.FileDescriptorProto, deps ...*FileDescriptor) (*FileDescriptor, error) {
return createFileDescriptor(fdp, deps, r)
}
// CreateFileDescriptors is the same as the package function of the same name,
// but any alternate paths configured in this resolver are used when linking the
// given descriptor protos.
func (r *ImportResolver) CreateFileDescriptors(fds []*dpb.FileDescriptorProto) (map[string]*FileDescriptor, error) {
return createFileDescriptors(fds, r)
}
// CreateFileDescriptorFromSet is the same as the package function of the same
// name, but any alternate paths configured in this resolver are used when
// linking the descriptor protos in the given set.
func (r *ImportResolver) CreateFileDescriptorFromSet(fds *dpb.FileDescriptorSet) (*FileDescriptor, error) {
return createFileDescriptorFromSet(fds, r)
}
// CreateFileDescriptorsFromSet is the same as the package function of the same
// name, but any alternate paths configured in this resolver are used when
// linking the descriptor protos in the given set.
func (r *ImportResolver) CreateFileDescriptorsFromSet(fds *dpb.FileDescriptorSet) (map[string]*FileDescriptor, error) {
return createFileDescriptorsFromSet(fds, r)
}
const dotPrefix = "." + string(filepath.Separator)
func clean(path string) string {
if path == "" {
return ""
}
path = filepath.Clean(path)
if path == "." {
return ""
}
return strings.TrimPrefix(path, dotPrefix)
}