github.com/maruel/nin@v0.0.0-20220112143044-f35891e3ce7e/includes_normalize.go (about)

     1  // Copyright 2012 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package nin
    16  
    17  import (
    18  	"errors"
    19  	"strings"
    20  )
    21  
    22  // Utility functions for normalizing include paths on Windows.
    23  // TODO: this likely duplicates functionality of CanonicalizePath; refactor.
    24  type includesNormalize struct {
    25  	relativeTo      string
    26  	splitRelativeTo []string
    27  }
    28  
    29  func newIncludesNormalize(relativeTo string) (includesNormalize, error) {
    30  	relativeTo, err := absPath(relativeTo)
    31  	return includesNormalize{
    32  		relativeTo:      relativeTo,
    33  		splitRelativeTo: strings.Split(relativeTo, "/"),
    34  	}, err
    35  }
    36  
    37  // Return true if paths a and b are on the same windows drive.
    38  // Return false if this function cannot check
    39  // whether or not on the same windows drive.
    40  func sameDriveFast(a string, b string) bool {
    41  	if len(a) < 3 || len(b) < 3 {
    42  		return false
    43  	}
    44  
    45  	if !islatinalpha(a[0]) || !islatinalpha(b[0]) {
    46  		return false
    47  	}
    48  
    49  	if toLowerASCII(a[0]) != toLowerASCII(b[0]) {
    50  		return false
    51  	}
    52  
    53  	if a[1] != ':' || b[1] != ':' {
    54  		return false
    55  	}
    56  
    57  	return isPathSeparator(a[2]) && isPathSeparator(b[2])
    58  }
    59  
    60  // Return true if paths a and b are on the same Windows drive.
    61  func sameDrive(a, b string) (bool, error) {
    62  	if sameDriveFast(a, b) {
    63  		return true, nil
    64  	}
    65  
    66  	aAbsolute, err := internalGetFullPathName(a)
    67  	if err != nil {
    68  		return false, err
    69  	}
    70  	bAbsolute, err := internalGetFullPathName(b)
    71  	if err != nil {
    72  		return false, err
    73  	}
    74  	return getDrive(aAbsolute) == getDrive(bAbsolute), nil
    75  }
    76  
    77  func getDrive(s string) string {
    78  	s = strings.TrimPrefix(s, "\\\\?\\")
    79  	if len(s) >= 2 && islatinalpha(s[0]) && s[1] == ':' {
    80  		return s[:2]
    81  	}
    82  	return ""
    83  }
    84  
    85  // Check path |s| is FullPath style returned by GetFullPathName.
    86  // This ignores difference of path separator.
    87  // This is used not to call very slow GetFullPathName API.
    88  func isFullPathName(s string) bool {
    89  	if len(s) < 3 || !islatinalpha(s[0]) || s[1] != ':' || !isPathSeparator(s[2]) {
    90  		return false
    91  	}
    92  
    93  	// Check "." or ".." is contained in path.
    94  	for i := 2; i < len(s); i++ {
    95  		if !isPathSeparator(s[i]) {
    96  			continue
    97  		}
    98  
    99  		// Check ".".
   100  		if i+1 < len(s) && s[i+1] == '.' && (i+2 >= len(s) || isPathSeparator(s[i+2])) {
   101  			return false
   102  		}
   103  
   104  		// Check "..".
   105  		if i+2 < len(s) && s[i+1] == '.' && s[i+2] == '.' && (i+3 >= len(s) || isPathSeparator(s[i+3])) {
   106  			return false
   107  		}
   108  	}
   109  
   110  	return true
   111  }
   112  
   113  // Internal utilities made available for testing, maybe useful otherwise.
   114  func absPath(s string) (string, error) {
   115  	if isFullPathName(s) {
   116  		return strings.ReplaceAll(s, "\\", "/"), nil
   117  	}
   118  	result, err := internalGetFullPathName(s)
   119  	return strings.ReplaceAll(result, "\\", "/"), err
   120  }
   121  
   122  func relativize(path string, startList []string) (string, error) {
   123  	absPath, err := absPath(path)
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  	pathList := strings.Split(absPath, "/")
   128  	i := 0
   129  	end := len(startList)
   130  	if end2 := len(pathList); end2 < end {
   131  		end = end2
   132  	}
   133  	for i = 0; i < end; i++ {
   134  		if !equalsCaseInsensitiveASCII(startList[i], pathList[i]) {
   135  			break
   136  		}
   137  	}
   138  
   139  	relList := make([]string, 0, len(pathList)-i)
   140  	for j := 0; j < len(startList)-i; j++ {
   141  		relList = append(relList, "..")
   142  	}
   143  	for j := i; j < len(pathList); j++ {
   144  		relList = append(relList, pathList[j])
   145  	}
   146  	if len(relList) == 0 {
   147  		return ".", nil
   148  	}
   149  	return strings.Join(relList, "/"), nil
   150  }
   151  
   152  // Normalize by fixing slashes style, fixing redundant .. and . and makes the
   153  // path input relative to relativeTo.
   154  func (i *includesNormalize) Normalize(input string) (string, error) {
   155  	len2 := len(input)
   156  	if len2 >= maxPath {
   157  		return "", errors.New("path too long")
   158  	}
   159  	cp := CanonicalizePath(input)
   160  	absInput, err := absPath(cp)
   161  	if err != nil {
   162  		return "", err
   163  	}
   164  
   165  	same, err := sameDrive(absInput, i.relativeTo)
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	if !same {
   170  		return cp, nil
   171  	}
   172  	return relativize(absInput, i.splitRelativeTo)
   173  }