blob: 2561e56c0c6a710ada3e50206f97e5c79fe8830a [file] [log] [blame]
Scott Baker4a35a702019-11-26 08:17:33 -08001package protoparse
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9)
10
11var errNoImportPathsForAbsoluteFilePath = errors.New("must specify at least one import path if any absolute file paths are given")
12
13// ResolveFilenames tries to resolve fileNames into paths that are relative to
14// directories in the given importPaths. The returned slice has the results in
15// the same order as they are supplied in fileNames.
16//
17// The resulting names should be suitable for passing to Parser.ParseFiles.
18//
19// If no import paths are given and any file name is absolute, this returns an
20// error. If no import paths are given and all file names are relative, this
21// returns the original file names. If a file name is already relative to one
22// of the given import paths, it will be unchanged in the returned slice. If a
23// file name given is relative to the current working directory, it will be made
24// relative to one of the given import paths; but if it cannot be made relative
25// (due to no matching import path), an error will be returned.
26func ResolveFilenames(importPaths []string, fileNames ...string) ([]string, error) {
27 if len(importPaths) == 0 {
28 if containsAbsFilePath(fileNames) {
29 // We have to do this as otherwise parseProtoFiles can result in duplicate symbols.
30 // For example, assume we import "foo/bar/bar.proto" in a file "/home/alice/dev/foo/bar/baz.proto"
31 // as we call ParseFiles("/home/alice/dev/foo/bar/bar.proto","/home/alice/dev/foo/bar/baz.proto")
32 // with "/home/alice/dev" as our current directory. Due to the recursive nature of parseProtoFiles,
33 // it will discover the import "foo/bar/bar.proto" in the input file, and call parse on this,
34 // adding "foo/bar/bar.proto" to the parsed results, as well as "/home/alice/dev/foo/bar/bar.proto"
35 // from the input file list. This will result in a
36 // 'duplicate symbol SYMBOL: already defined as field in "/home/alice/dev/foo/bar/bar.proto'
37 // error being returned from ParseFiles.
38 return nil, errNoImportPathsForAbsoluteFilePath
39 }
40 return fileNames, nil
41 }
42 absImportPaths, err := absoluteFilePaths(importPaths)
43 if err != nil {
44 return nil, err
45 }
46 resolvedFileNames := make([]string, 0, len(fileNames))
47 for _, fileName := range fileNames {
48 resolvedFileName, err := resolveFilename(absImportPaths, fileName)
49 if err != nil {
50 return nil, err
51 }
52 resolvedFileNames = append(resolvedFileNames, resolvedFileName)
53 }
54 return resolvedFileNames, nil
55}
56
57func containsAbsFilePath(filePaths []string) bool {
58 for _, filePath := range filePaths {
59 if filepath.IsAbs(filePath) {
60 return true
61 }
62 }
63 return false
64}
65
66func absoluteFilePaths(filePaths []string) ([]string, error) {
67 absFilePaths := make([]string, 0, len(filePaths))
68 for _, filePath := range filePaths {
69 absFilePath, err := canonicalize(filePath)
70 if err != nil {
71 return nil, err
72 }
73 absFilePaths = append(absFilePaths, absFilePath)
74 }
75 return absFilePaths, nil
76}
77
78func canonicalize(filePath string) (string, error) {
79 absPath, err := filepath.Abs(filePath)
80 if err != nil {
81 return "", err
82 }
83 // this is kind of gross, but it lets us construct a resolved path even if some
84 // path elements do not exist (a single call to filepath.EvalSymlinks would just
85 // return an error, ENOENT, in that case).
86 head := absPath
87 tail := ""
88 for {
89 noLinks, err := filepath.EvalSymlinks(head)
90 if err == nil {
91 if tail != "" {
92 return filepath.Join(noLinks, tail), nil
93 }
94 return noLinks, nil
95 }
96
97 if tail == "" {
98 tail = filepath.Base(head)
99 } else {
100 tail = filepath.Join(filepath.Base(head), tail)
101 }
102 head = filepath.Dir(head)
103 if head == "." {
104 // ran out of path elements to try to resolve
105 return absPath, nil
106 }
107 }
108}
109
110const dotPrefix = "." + string(filepath.Separator)
111const dotDotPrefix = ".." + string(filepath.Separator)
112
113func resolveFilename(absImportPaths []string, fileName string) (string, error) {
114 if filepath.IsAbs(fileName) {
115 return resolveAbsFilename(absImportPaths, fileName)
116 }
117
118 if !strings.HasPrefix(fileName, dotPrefix) && !strings.HasPrefix(fileName, dotDotPrefix) {
119 // Use of . and .. are assumed to be relative to current working
120 // directory. So if those aren't present, check to see if the file is
121 // relative to an import path.
122 for _, absImportPath := range absImportPaths {
123 absFileName := filepath.Join(absImportPath, fileName)
124 _, err := os.Stat(absFileName)
125 if err != nil {
126 continue
127 }
128 // found it! it was relative to this import path
129 return fileName, nil
130 }
131 }
132
133 // must be relative to current working dir
134 return resolveAbsFilename(absImportPaths, fileName)
135}
136
137func resolveAbsFilename(absImportPaths []string, fileName string) (string, error) {
138 absFileName, err := canonicalize(fileName)
139 if err != nil {
140 return "", err
141 }
142 for _, absImportPath := range absImportPaths {
143 if isDescendant(absImportPath, absFileName) {
144 resolvedPath, err := filepath.Rel(absImportPath, absFileName)
145 if err != nil {
146 return "", err
147 }
148 return resolvedPath, nil
149 }
150 }
151 return "", fmt.Errorf("%s does not reside in any import path", fileName)
152}
153
154// isDescendant returns true if file is a descendant of dir. Both dir and file must
155// be cleaned, absolute paths.
156func isDescendant(dir, file string) bool {
157 dir = filepath.Clean(dir)
158 cur := file
159 for {
160 d := filepath.Dir(cur)
161 if d == dir {
162 return true
163 }
164 if d == "." || d == cur {
165 // we've run out of path elements
166 return false
167 }
168 cur = d
169 }
170}