github.com/hoveychen/protoreflect@v1.4.7-0.20221103114119-0b4b3385ec76/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 resolvedFileNames = append(resolvedFileNames, resolvedFileName) 53 } 54 return resolvedFileNames, nil 55 } 56 57 func containsAbsFilePath(filePaths []string) bool { 58 for _, filePath := range filePaths { 59 if filepath.IsAbs(filePath) { 60 return true 61 } 62 } 63 return false 64 } 65 66 func 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 78 func 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 110 const dotPrefix = "." + string(filepath.Separator) 111 const dotDotPrefix = ".." + string(filepath.Separator) 112 113 func 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 137 func 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. 156 func 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 }