// Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package get import ( "fmt" "strings" "unicode" "unicode/utf8" ) // The following functions are copied verbatim from golang.org/x/mod/module/module.go, // with a change to additionally reject Windows short-names, // and one to accept arbitrary letters (golang.org/issue/29101). // // TODO(bcmills): After the call site for this function is backported, // consolidate this back down to a single copy. // // NOTE: DO NOT MERGE THESE UNTIL WE DECIDE ABOUT ARBITRARY LETTERS IN MODULE MODE. // CheckImportPath checks that an import path is valid. func CheckImportPath(path string) error { if err := checkPath(path, false); err != nil { return fmt.Errorf("malformed import path %q: %v", path, err) } return nil } // checkPath checks that a general path is valid. // It returns an error describing why but not mentioning path. // Because these checks apply to both module paths and import paths, // the caller is expected to add the "malformed ___ path %q: " prefix. // fileName indicates whether the final element of the path is a file name // (as opposed to a directory name). func checkPath(path string, fileName bool) error { if !utf8.ValidString(path) { return fmt.Errorf("invalid UTF-8") } if path == "" { return fmt.Errorf("empty string") } if path[0] == '-' { return fmt.Errorf("leading dash") } if strings.Contains(path, "//") { return fmt.Errorf("double slash") } if path[len(path)-1] == '/' { return fmt.Errorf("trailing slash") } elemStart := 0 for i, r := range path { if r == '/' { if err := checkElem(path[elemStart:i], fileName); err != nil { return err } elemStart = i + 1 } } if err := checkElem(path[elemStart:], fileName); err != nil { return err } return nil } // checkElem checks whether an individual path element is valid. // fileName indicates whether the element is a file name (not a directory name). func checkElem(elem string, fileName bool) error { if elem == "" { return fmt.Errorf("empty path element") } if strings.Count(elem, ".") == len(elem) { return fmt.Errorf("invalid path element %q", elem) } if elem[0] == '.' && !fileName { return fmt.Errorf("leading dot in path element") } if elem[len(elem)-1] == '.' { return fmt.Errorf("trailing dot in path element") } charOK := pathOK if fileName { charOK = fileNameOK } for _, r := range elem { if !charOK(r) { return fmt.Errorf("invalid char %q", r) } } // Windows disallows a bunch of path elements, sadly. // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file short := elem if i := strings.Index(short, "."); i >= 0 { short = short[:i] } for _, bad := range badWindowsNames { if strings.EqualFold(bad, short) { return fmt.Errorf("disallowed path element %q", elem) } } // Reject path components that look like Windows short-names. // Those usually end in a tilde followed by one or more ASCII digits. if tilde := strings.LastIndexByte(short, '~'); tilde >= 0 && tilde < len(short)-1 { suffix := short[tilde+1:] suffixIsDigits := true for _, r := range suffix { if r < '0' || r > '9' { suffixIsDigits = false break } } if suffixIsDigits { return fmt.Errorf("trailing tilde and digits in path element") } } return nil } // pathOK reports whether r can appear in an import path element. // // NOTE: This function DIVERGES from module mode pathOK by accepting Unicode letters. func pathOK(r rune) bool { if r < utf8.RuneSelf { return r == '+' || r == '-' || r == '.' || r == '_' || r == '~' || '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' } return unicode.IsLetter(r) } // fileNameOK reports whether r can appear in a file name. // For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters. // If we expand the set of allowed characters here, we have to // work harder at detecting potential case-folding and normalization collisions. // See note about "safe encoding" below. func fileNameOK(r rune) bool { if r < utf8.RuneSelf { // Entire set of ASCII punctuation, from which we remove characters: // ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ // We disallow some shell special characters: " ' * < > ? ` | // (Note that some of those are disallowed by the Windows file system as well.) // We also disallow path separators / : and \ (fileNameOK is only called on path element characters). // We allow spaces (U+0020) in file names. const allowed = "!#$%&()+,-.=@[]^_{}~ " if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' { return true } for i := 0; i < len(allowed); i++ { if rune(allowed[i]) == r { return true } } return false } // It may be OK to add more ASCII punctuation here, but only carefully. // For example Windows disallows < > \, and macOS disallows :, so we must not allow those. return unicode.IsLetter(r) } // badWindowsNames are the reserved file path elements on Windows. // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file var badWindowsNames = []string{ "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", }