]> Cypherpunks.ru repositories - gostls13.git/blobdiff - src/internal/safefilepath/path_windows.go
path/filepath: fix various issues in parsing Windows paths
[gostls13.git] / src / internal / safefilepath / path_windows.go
index 909c150edc87a281d94f648ae760163418e8edb1..7cfd6ce2eac2622b7a74135ea66bee460ae9c910 100644 (file)
@@ -20,15 +20,10 @@ func fromFS(path string) (string, error) {
        for p := path; p != ""; {
                // Find the next path element.
                i := 0
-               dot := -1
                for i < len(p) && p[i] != '/' {
                        switch p[i] {
                        case 0, '\\', ':':
                                return "", errInvalidPath
-                       case '.':
-                               if dot < 0 {
-                                       dot = i
-                               }
                        }
                        i++
                }
@@ -39,22 +34,8 @@ func fromFS(path string) (string, error) {
                } else {
                        p = ""
                }
-               // Trim the extension and look for a reserved name.
-               base := part
-               if dot >= 0 {
-                       base = part[:dot]
-               }
-               if isReservedName(base) {
-                       if dot < 0 {
-                               return "", errInvalidPath
-                       }
-                       // The path element is a reserved name with an extension.
-                       // Some Windows versions consider this a reserved name,
-                       // while others do not. Use FullPath to see if the name is
-                       // reserved.
-                       if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` {
-                               return "", errInvalidPath
-                       }
+               if IsReservedName(part) {
+                       return "", errInvalidPath
                }
        }
        if containsSlash {
@@ -70,23 +51,88 @@ func fromFS(path string) (string, error) {
        return path, nil
 }
 
-// isReservedName reports if name is a Windows reserved device name.
+// IsReservedName reports if name is a Windows reserved device name.
 // It does not detect names with an extension, which are also reserved on some Windows versions.
 //
 // For details, search for PRN in
 // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file.
-func isReservedName(name string) bool {
-       if 3 <= len(name) && len(name) <= 4 {
+func IsReservedName(name string) bool {
+       // Device names can have arbitrary trailing characters following a dot or colon.
+       base := name
+       for i := 0; i < len(base); i++ {
+               switch base[i] {
+               case ':', '.':
+                       base = base[:i]
+               }
+       }
+       // Trailing spaces in the last path element are ignored.
+       for len(base) > 0 && base[len(base)-1] == ' ' {
+               base = base[:len(base)-1]
+       }
+       if !isReservedBaseName(base) {
+               return false
+       }
+       if len(base) == len(name) {
+               return true
+       }
+       // The path element is a reserved name with an extension.
+       // Some Windows versions consider this a reserved name,
+       // while others do not. Use FullPath to see if the name is
+       // reserved.
+       if p, _ := syscall.FullPath(name); len(p) >= 4 && p[:4] == `\\.\` {
+               return true
+       }
+       return false
+}
+
+func isReservedBaseName(name string) bool {
+       if len(name) == 3 {
                switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
                case "CON", "PRN", "AUX", "NUL":
-                       return len(name) == 3
+                       return true
+               }
+       }
+       if len(name) >= 4 {
+               switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
                case "COM", "LPT":
-                       return len(name) == 4 && '1' <= name[3] && name[3] <= '9'
+                       if len(name) == 4 && '1' <= name[3] && name[3] <= '9' {
+                               return true
+                       }
+                       // Superscript ¹, ², and ³ are considered numbers as well.
+                       switch name[3:] {
+                       case "\u00b2", "\u00b3", "\u00b9":
+                               return true
+                       }
+                       return false
                }
        }
+
+       // Passing CONIN$ or CONOUT$ to CreateFile opens a console handle.
+       // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles
+       //
+       // While CONIN$ and CONOUT$ aren't documented as being files,
+       // they behave the same as CON. For example, ./CONIN$ also opens the console input.
+       if len(name) == 6 && name[5] == '$' && equalFold(name, "CONIN$") {
+               return true
+       }
+       if len(name) == 7 && name[6] == '$' && equalFold(name, "CONOUT$") {
+               return true
+       }
        return false
 }
 
+func equalFold(a, b string) bool {
+       if len(a) != len(b) {
+               return false
+       }
+       for i := 0; i < len(a); i++ {
+               if toUpper(a[i]) != toUpper(b[i]) {
+                       return false
+               }
+       }
+       return true
+}
+
 func toUpper(c byte) byte {
        if 'a' <= c && c <= 'z' {
                return c - ('a' - 'A')