github.com/bakjos/protoreflect@v1.9.2/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  }