| 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 |
| } |
| } |