1package 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		// On Windows, the resolved paths will use "\", but proto imports
53		// require the use of "/". So fix up here.
54		if filepath.Separator != '/' {
55			resolvedFileName = strings.Replace(resolvedFileName, string(filepath.Separator), "/", -1)
56		}
57		resolvedFileNames = append(resolvedFileNames, resolvedFileName)
58	}
59	return resolvedFileNames, nil
60}
61
62func containsAbsFilePath(filePaths []string) bool {
63	for _, filePath := range filePaths {
64		if filepath.IsAbs(filePath) {
65			return true
66		}
67	}
68	return false
69}
70
71func absoluteFilePaths(filePaths []string) ([]string, error) {
72	absFilePaths := make([]string, 0, len(filePaths))
73	for _, filePath := range filePaths {
74		absFilePath, err := canonicalize(filePath)
75		if err != nil {
76			return nil, err
77		}
78		absFilePaths = append(absFilePaths, absFilePath)
79	}
80	return absFilePaths, nil
81}
82
83func canonicalize(filePath string) (string, error) {
84	absPath, err := filepath.Abs(filePath)
85	if err != nil {
86		return "", err
87	}
88	// this is kind of gross, but it lets us construct a resolved path even if some
89	// path elements do not exist (a single call to filepath.EvalSymlinks would just
90	// return an error, ENOENT, in that case).
91	head := absPath
92	tail := ""
93	for {
94		noLinks, err := filepath.EvalSymlinks(head)
95		if err == nil {
96			if tail != "" {
97				return filepath.Join(noLinks, tail), nil
98			}
99			return noLinks, nil
100		}
101
102		if tail == "" {
103			tail = filepath.Base(head)
104		} else {
105			tail = filepath.Join(filepath.Base(head), tail)
106		}
107		head = filepath.Dir(head)
108		if head == "." {
109			// ran out of path elements to try to resolve
110			return absPath, nil
111		}
112	}
113}
114
115const dotPrefix = "." + string(filepath.Separator)
116const dotDotPrefix = ".." + string(filepath.Separator)
117
118func resolveFilename(absImportPaths []string, fileName string) (string, error) {
119	if filepath.IsAbs(fileName) {
120		return resolveAbsFilename(absImportPaths, fileName)
121	}
122
123	if !strings.HasPrefix(fileName, dotPrefix) && !strings.HasPrefix(fileName, dotDotPrefix) {
124		// Use of . and .. are assumed to be relative to current working
125		// directory. So if those aren't present, check to see if the file is
126		// relative to an import path.
127		for _, absImportPath := range absImportPaths {
128			absFileName := filepath.Join(absImportPath, fileName)
129			_, err := os.Stat(absFileName)
130			if err != nil {
131				continue
132			}
133			// found it! it was relative to this import path
134			return fileName, nil
135		}
136	}
137
138	// must be relative to current working dir
139	return resolveAbsFilename(absImportPaths, fileName)
140}
141
142func resolveAbsFilename(absImportPaths []string, fileName string) (string, error) {
143	absFileName, err := canonicalize(fileName)
144	if err != nil {
145		return "", err
146	}
147	for _, absImportPath := range absImportPaths {
148		if isDescendant(absImportPath, absFileName) {
149			resolvedPath, err := filepath.Rel(absImportPath, absFileName)
150			if err != nil {
151				return "", err
152			}
153			return resolvedPath, nil
154		}
155	}
156	return "", fmt.Errorf("%s does not reside in any import path", fileName)
157}
158
159// isDescendant returns true if file is a descendant of dir. Both dir and file must
160// be cleaned, absolute paths.
161func isDescendant(dir, file string) bool {
162	dir = filepath.Clean(dir)
163	cur := file
164	for {
165		d := filepath.Dir(cur)
166		if d == dir {
167			return true
168		}
169		if d == "." || d == cur {
170			// we've run out of path elements
171			return false
172		}
173		cur = d
174	}
175}
176