github.com/jhump/protoreflect@v1.16.0/desc/protoparse/resolve_files.go (about) 1 package protoparse 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 ) 10 11 var 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. 26 func 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 62 func containsAbsFilePath(filePaths []string) bool { 63 for _, filePath := range filePaths { 64 if filepath.IsAbs(filePath) { 65 return true 66 } 67 } 68 return false 69 } 70 71 func 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 83 func 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 115 const dotPrefix = "." + string(filepath.Separator) 116 const dotDotPrefix = ".." + string(filepath.Separator) 117 118 func 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 142 func 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. 161 func 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 }