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  }