blob: 2561e56c0c6a710ada3e50206f97e5c79fe8830a [file] [log] [blame]
package protoparse
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
var errNoImportPathsForAbsoluteFilePath = errors.New("must specify at least one import path if any absolute file paths are given")
// ResolveFilenames tries to resolve fileNames into paths that are relative to
// directories in the given importPaths. The returned slice has the results in
// the same order as they are supplied in fileNames.
//
// The resulting names should be suitable for passing to Parser.ParseFiles.
//
// If no import paths are given and any file name is absolute, this returns an
// error. If no import paths are given and all file names are relative, this
// returns the original file names. If a file name is already relative to one
// of the given import paths, it will be unchanged in the returned slice. If a
// file name given is relative to the current working directory, it will be made
// relative to one of the given import paths; but if it cannot be made relative
// (due to no matching import path), an error will be returned.
func ResolveFilenames(importPaths []string, fileNames ...string) ([]string, error) {
if len(importPaths) == 0 {
if containsAbsFilePath(fileNames) {
// We have to do this as otherwise parseProtoFiles can result in duplicate symbols.
// For example, assume we import "foo/bar/bar.proto" in a file "/home/alice/dev/foo/bar/baz.proto"
// as we call ParseFiles("/home/alice/dev/foo/bar/bar.proto","/home/alice/dev/foo/bar/baz.proto")
// with "/home/alice/dev" as our current directory. Due to the recursive nature of parseProtoFiles,
// it will discover the import "foo/bar/bar.proto" in the input file, and call parse on this,
// adding "foo/bar/bar.proto" to the parsed results, as well as "/home/alice/dev/foo/bar/bar.proto"
// from the input file list. This will result in a
// 'duplicate symbol SYMBOL: already defined as field in "/home/alice/dev/foo/bar/bar.proto'
// error being returned from ParseFiles.
return nil, errNoImportPathsForAbsoluteFilePath
}
return fileNames, nil
}
absImportPaths, err := absoluteFilePaths(importPaths)
if err != nil {
return nil, err
}
resolvedFileNames := make([]string, 0, len(fileNames))
for _, fileName := range fileNames {
resolvedFileName, err := resolveFilename(absImportPaths, fileName)
if err != nil {
return nil, err
}
resolvedFileNames = append(resolvedFileNames, resolvedFileName)
}
return resolvedFileNames, nil
}
func containsAbsFilePath(filePaths []string) bool {
for _, filePath := range filePaths {
if filepath.IsAbs(filePath) {
return true
}
}
return false
}
func absoluteFilePaths(filePaths []string) ([]string, error) {
absFilePaths := make([]string, 0, len(filePaths))
for _, filePath := range filePaths {
absFilePath, err := canonicalize(filePath)
if err != nil {
return nil, err
}
absFilePaths = append(absFilePaths, absFilePath)
}
return absFilePaths, nil
}
func canonicalize(filePath string) (string, error) {
absPath, err := filepath.Abs(filePath)
if err != nil {
return "", err
}
// this is kind of gross, but it lets us construct a resolved path even if some
// path elements do not exist (a single call to filepath.EvalSymlinks would just
// return an error, ENOENT, in that case).
head := absPath
tail := ""
for {
noLinks, err := filepath.EvalSymlinks(head)
if err == nil {
if tail != "" {
return filepath.Join(noLinks, tail), nil
}
return noLinks, nil
}
if tail == "" {
tail = filepath.Base(head)
} else {
tail = filepath.Join(filepath.Base(head), tail)
}
head = filepath.Dir(head)
if head == "." {
// ran out of path elements to try to resolve
return absPath, nil
}
}
}
const dotPrefix = "." + string(filepath.Separator)
const dotDotPrefix = ".." + string(filepath.Separator)
func resolveFilename(absImportPaths []string, fileName string) (string, error) {
if filepath.IsAbs(fileName) {
return resolveAbsFilename(absImportPaths, fileName)
}
if !strings.HasPrefix(fileName, dotPrefix) && !strings.HasPrefix(fileName, dotDotPrefix) {
// Use of . and .. are assumed to be relative to current working
// directory. So if those aren't present, check to see if the file is
// relative to an import path.
for _, absImportPath := range absImportPaths {
absFileName := filepath.Join(absImportPath, fileName)
_, err := os.Stat(absFileName)
if err != nil {
continue
}
// found it! it was relative to this import path
return fileName, nil
}
}
// must be relative to current working dir
return resolveAbsFilename(absImportPaths, fileName)
}
func resolveAbsFilename(absImportPaths []string, fileName string) (string, error) {
absFileName, err := canonicalize(fileName)
if err != nil {
return "", err
}
for _, absImportPath := range absImportPaths {
if isDescendant(absImportPath, absFileName) {
resolvedPath, err := filepath.Rel(absImportPath, absFileName)
if err != nil {
return "", err
}
return resolvedPath, nil
}
}
return "", fmt.Errorf("%s does not reside in any import path", fileName)
}
// isDescendant returns true if file is a descendant of dir. Both dir and file must
// be cleaned, absolute paths.
func isDescendant(dir, file string) bool {
dir = filepath.Clean(dir)
cur := file
for {
d := filepath.Dir(cur)
if d == dir {
return true
}
if d == "." || d == cur {
// we've run out of path elements
return false
}
cur = d
}
}