module: allow leading dots in import path elements

These were always disallowed, but the restriction wasn't enforced in
most cases until Go 1.16. That's broken more projects than we hoped.

This change allows leading dots in import path elements. Leading dots
are still not allowed in module path elements. Leading dots were
always allowed in file path elements. Trailing dots are still
forbidden in all cases.

For golang/go#43985

Change-Id: Id9cf728a341931565ab9e81f600b2341aa178683
Reviewed-on: https://go-review.googlesource.com/c/mod/+/297089
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/module/module.go b/module/module.go
index c1c5263..272baee 100644
--- a/module/module.go
+++ b/module/module.go
@@ -270,7 +270,7 @@
 
 // CheckPath checks that a module path is valid.
 // A valid module path is a valid import path, as checked by CheckImportPath,
-// with two additional constraints.
+// with three additional constraints.
 // First, the leading path element (up to the first slash, if any),
 // by convention a domain name, must contain only lower-case ASCII letters,
 // ASCII digits, dots (U+002E), and dashes (U+002D);
@@ -280,8 +280,9 @@
 // and must not contain any dots. For paths beginning with "gopkg.in/",
 // this second requirement is replaced by a requirement that the path
 // follow the gopkg.in server's conventions.
+// Third, no path element may begin with a dot.
 func CheckPath(path string) error {
-	if err := checkPath(path, false); err != nil {
+	if err := checkPath(path, modulePath); err != nil {
 		return fmt.Errorf("malformed module path %q: %v", path, err)
 	}
 	i := strings.Index(path, "/")
@@ -315,7 +316,7 @@
 //
 // A valid path element is a non-empty string made up of
 // ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
-// It must not begin or end with a dot (U+002E), nor contain two dots in a row.
+// It must not end with a dot (U+002E), nor contain two dots in a row.
 //
 // The element prefix up to the first dot must not be a reserved file name
 // on Windows, regardless of case (CON, com1, NuL, and so on). The element
@@ -326,19 +327,29 @@
 // top-level package documentation for additional information about
 // subtleties of Unicode.
 func CheckImportPath(path string) error {
-	if err := checkPath(path, false); err != nil {
+	if err := checkPath(path, importPath); err != nil {
 		return fmt.Errorf("malformed import path %q: %v", path, err)
 	}
 	return nil
 }
 
+// pathKind indicates what kind of path we're checking. Module paths,
+// import paths, and file paths have different restrictions.
+type pathKind int
+
+const (
+	modulePath pathKind = iota
+	importPath
+	filePath
+)
+
 // 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 {
+func checkPath(path string, kind pathKind) error {
 	if !utf8.ValidString(path) {
 		return fmt.Errorf("invalid UTF-8")
 	}
@@ -357,35 +368,34 @@
 	elemStart := 0
 	for i, r := range path {
 		if r == '/' {
-			if err := checkElem(path[elemStart:i], fileName); err != nil {
+			if err := checkElem(path[elemStart:i], kind); err != nil {
 				return err
 			}
 			elemStart = i + 1
 		}
 	}
-	if err := checkElem(path[elemStart:], fileName); err != nil {
+	if err := checkElem(path[elemStart:], kind); 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 {
+func checkElem(elem string, kind pathKind) 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 {
+	if elem[0] == '.' && kind == modulePath {
 		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 {
+	if kind == filePath {
 		charOK = fileNameOK
 	}
 	for _, r := range elem {
@@ -406,7 +416,7 @@
 		}
 	}
 
-	if fileName {
+	if kind == filePath {
 		// don't check for Windows short-names in file names. They're
 		// only an issue for import paths.
 		return nil
@@ -444,7 +454,7 @@
 // top-level package documentation for additional information about
 // subtleties of Unicode.
 func CheckFilePath(path string) error {
-	if err := checkPath(path, true); err != nil {
+	if err := checkPath(path, filePath); err != nil {
 		return fmt.Errorf("malformed file path %q: %v", path, err)
 	}
 	return nil
@@ -647,7 +657,7 @@
 // Versions are allowed to be in non-semver form but must be valid file names
 // and not contain exclamation marks.
 func EscapeVersion(v string) (escaped string, err error) {
-	if err := checkElem(v, true); err != nil || strings.Contains(v, "!") {
+	if err := checkElem(v, filePath); err != nil || strings.Contains(v, "!") {
 		return "", &InvalidVersionError{
 			Version: v,
 			Err:     fmt.Errorf("disallowed version string"),
@@ -706,7 +716,7 @@
 	if !ok {
 		return "", fmt.Errorf("invalid escaped version %q", escaped)
 	}
-	if err := checkElem(v, true); err != nil {
+	if err := checkElem(v, filePath); err != nil {
 		return "", fmt.Errorf("invalid escaped version %q: %v", v, err)
 	}
 	return v, nil
diff --git a/module/module_test.go b/module/module_test.go
index 9e40367..4fc462b 100644
--- a/module/module_test.go
+++ b/module/module_test.go
@@ -78,7 +78,7 @@
 	{"x.y/\xFFz", false, false, false},
 	{"/x.y/z", false, false, false},
 	{"x./z", false, false, false},
-	{".x/z", false, false, true},
+	{".x/z", false, true, true},
 	{"-x/z", false, false, false},
 	{"x..y/z", true, true, true},
 	{"x.y/z/../../w", false, false, false},
@@ -184,8 +184,8 @@
 	{"./y", false, false, false},
 	{"x:y", false, false, false},
 	{`\temp\foo`, false, false, false},
-	{".gitignore", false, false, true},
-	{".github/ISSUE_TEMPLATE", false, false, true},
+	{".gitignore", false, true, true},
+	{".github/ISSUE_TEMPLATE", false, true, true},
 	{"x☺y", false, false, false},
 }