Browse Source

volume/mounts: split tests per parser

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Sebastiaan van Stijn 4 years ago
parent
commit
df179a1d6a

+ 171 - 0
volume/mounts/lcow_parser_test.go

@@ -0,0 +1,171 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types/mount"
+)
+
+func TestLCOWParseMountRaw(t *testing.T) {
+	previousProvider := currentFileInfoProvider
+	defer func() { currentFileInfoProvider = previousProvider }()
+	currentFileInfoProvider = mockFiProvider{}
+
+	valid := []string{
+		`/foo`,
+		`/foo/`,
+		`/foo bar`,
+		`c:\:/foo`,
+		`c:\windows\:/foo`,
+		`c:\windows:/s p a c e`,
+		`c:\windows:/s p a c e:RW`,
+		`c:\program files:/s p a c e i n h o s t d i r`,
+		`0123456789name:/foo`,
+		`MiXeDcAsEnAmE:/foo`,
+		`name:/foo`,
+		`name:/foo:rW`,
+		`name:/foo:RW`,
+		`name:/foo:RO`,
+		`c:/:/forward/slashes/are/good/too`,
+		`c:/:/including with/spaces:ro`,
+		`/Program Files (x86)`, // With capitals and brackets
+	}
+
+	invalid := map[string]string{
+		``:                                   "invalid volume specification: ",
+		`.`:                                  "invalid volume specification: ",
+		`c:`:                                 "invalid volume specification: ",
+		`c:\`:                                "invalid volume specification: ",
+		`../`:                                "invalid volume specification: ",
+		`c:\:../`:                            "invalid volume specification: ",
+		`c:\:/foo:xyzzy`:                     "invalid volume specification: ",
+		`/`:                                  "destination can't be '/'",
+		`/..`:                                "destination can't be '/'",
+		`c:\notexist:/foo`:                   `source path does not exist: c:\notexist`,
+		`c:\windows\system32\ntdll.dll:/foo`: `source path must be a directory`,
+		`name<:/foo`:                         `invalid volume specification`,
+		`name>:/foo`:                         `invalid volume specification`,
+		`name::/foo`:                         `invalid volume specification`,
+		`name":/foo`:                         `invalid volume specification`,
+		`name\:/foo`:                         `invalid volume specification`,
+		`name*:/foo`:                         `invalid volume specification`,
+		`name|:/foo`:                         `invalid volume specification`,
+		`name?:/foo`:                         `invalid volume specification`,
+		`name/:/foo`:                         `invalid volume specification`,
+		`/foo:rw`:                            `invalid volume specification`,
+		`/foo:ro`:                            `invalid volume specification`,
+		`con:/foo`:                           `cannot be a reserved word for Windows filenames`,
+		`PRN:/foo`:                           `cannot be a reserved word for Windows filenames`,
+		`aUx:/foo`:                           `cannot be a reserved word for Windows filenames`,
+		`nul:/foo`:                           `cannot be a reserved word for Windows filenames`,
+		`com1:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`com2:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`com3:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`com4:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`com5:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`com6:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`com7:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`com8:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`com9:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt1:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt2:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt3:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt4:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt5:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt6:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt7:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt8:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt9:/foo`:                          `cannot be a reserved word for Windows filenames`,
+		`\\.\pipe\foo:/foo`:                  `Linux containers on Windows do not support named pipe mounts`,
+	}
+
+	parser := &lcowParser{}
+
+	for _, path := range valid {
+		if _, err := parser.ParseMountRaw(path, "local"); err != nil {
+			t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
+		}
+	}
+
+	for path, expectedError := range invalid {
+		if mp, err := parser.ParseMountRaw(path, "local"); err == nil {
+			t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
+		} else {
+			if !strings.Contains(err.Error(), expectedError) {
+				t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
+			}
+		}
+	}
+}
+
+func TestLCOWParseMountRawSplit(t *testing.T) {
+	previousProvider := currentFileInfoProvider
+	defer func() { currentFileInfoProvider = previousProvider }()
+	currentFileInfoProvider = mockFiProvider{}
+
+	cases := []struct {
+		bind      string
+		driver    string
+		expType   mount.Type
+		expDest   string
+		expSource string
+		expName   string
+		expDriver string
+		expRW     bool
+		fail      bool
+	}{
+		{`c:\:/foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false},
+		{`c:\:/foo:ro`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, false},
+		{`c:\:/foo:rw`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false},
+		{`c:\:/foo:foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, true},
+		{`name:/foo:rw`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false},
+		{`name:/foo`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false},
+		{`name:/foo:ro`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", false, false},
+		{`name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
+		{`driver/name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
+		{`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, true},
+		{`\\.\pipe\foo:/data`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
+		{`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
+	}
+
+	parser := &lcowParser{}
+	for i, c := range cases {
+		t.Logf("case %d", i)
+		m, err := parser.ParseMountRaw(c.bind, c.driver)
+		if c.fail {
+			if err == nil {
+				t.Errorf("Expected error, was nil, for spec %s\n", c.bind)
+			}
+			continue
+		}
+
+		if m == nil || err != nil {
+			t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err)
+			continue
+		}
+
+		if m.Destination != c.expDest {
+			t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
+		}
+
+		if m.Source != c.expSource {
+			t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind)
+		}
+
+		if m.Name != c.expName {
+			t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind)
+		}
+
+		if m.Driver != c.expDriver {
+			t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind)
+		}
+
+		if m.RW != c.expRW {
+			t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind)
+		}
+		if m.Type != c.expType {
+			t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind)
+		}
+	}
+}

+ 194 - 0
volume/mounts/linux_parser_test.go

@@ -1,12 +1,206 @@
 package mounts // import "github.com/docker/docker/volume/mounts"
 package mounts // import "github.com/docker/docker/volume/mounts"
 
 
 import (
 import (
+	"fmt"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 
 
 	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types/mount"
+	"gotest.tools/v3/assert"
 )
 )
 
 
+func TestLinuxParseMountRaw(t *testing.T) {
+	previousProvider := currentFileInfoProvider
+	defer func() { currentFileInfoProvider = previousProvider }()
+	currentFileInfoProvider = mockFiProvider{}
+
+	valid := []string{
+		"/home",
+		"/home:/home",
+		"/home:/something/else",
+		"/with space",
+		"/home:/with space",
+		"relative:/absolute-path",
+		"hostPath:/containerPath:ro",
+		"/hostPath:/containerPath:rw",
+		"/rw:/ro",
+		"/hostPath:/containerPath:shared",
+		"/hostPath:/containerPath:rshared",
+		"/hostPath:/containerPath:slave",
+		"/hostPath:/containerPath:rslave",
+		"/hostPath:/containerPath:private",
+		"/hostPath:/containerPath:rprivate",
+		"/hostPath:/containerPath:ro,shared",
+		"/hostPath:/containerPath:ro,slave",
+		"/hostPath:/containerPath:ro,private",
+		"/hostPath:/containerPath:ro,z,shared",
+		"/hostPath:/containerPath:ro,Z,slave",
+		"/hostPath:/containerPath:Z,ro,slave",
+		"/hostPath:/containerPath:slave,Z,ro",
+		"/hostPath:/containerPath:Z,slave,ro",
+		"/hostPath:/containerPath:slave,ro,Z",
+		"/hostPath:/containerPath:rslave,ro,Z",
+		"/hostPath:/containerPath:ro,rshared,Z",
+		"/hostPath:/containerPath:ro,Z,rprivate",
+	}
+
+	invalid := map[string]string{
+		"":                                "invalid volume specification",
+		"./":                              "mount path must be absolute",
+		"../":                             "mount path must be absolute",
+		"/:../":                           "mount path must be absolute",
+		"/:path":                          "mount path must be absolute",
+		":":                               "invalid volume specification",
+		"/tmp:":                           "invalid volume specification",
+		":test":                           "invalid volume specification",
+		":/test":                          "invalid volume specification",
+		"tmp:":                            "invalid volume specification",
+		":test:":                          "invalid volume specification",
+		"::":                              "invalid volume specification",
+		":::":                             "invalid volume specification",
+		"/tmp:::":                         "invalid volume specification",
+		":/tmp::":                         "invalid volume specification",
+		"/path:rw":                        "invalid volume specification",
+		"/path:ro":                        "invalid volume specification",
+		"/rw:rw":                          "invalid volume specification",
+		"path:ro":                         "invalid volume specification",
+		"/path:/path:sw":                  `invalid mode`,
+		"/path:/path:rwz":                 `invalid mode`,
+		"/path:/path:ro,rshared,rslave":   `invalid mode`,
+		"/path:/path:ro,z,rshared,rslave": `invalid mode`,
+		"/path:shared":                    "invalid volume specification",
+		"/path:slave":                     "invalid volume specification",
+		"/path:private":                   "invalid volume specification",
+		"name:/absolute-path:shared":      "invalid volume specification",
+		"name:/absolute-path:rshared":     "invalid volume specification",
+		"name:/absolute-path:slave":       "invalid volume specification",
+		"name:/absolute-path:rslave":      "invalid volume specification",
+		"name:/absolute-path:private":     "invalid volume specification",
+		"name:/absolute-path:rprivate":    "invalid volume specification",
+	}
+
+	parser := &linuxParser{}
+
+	for _, path := range valid {
+		if _, err := parser.ParseMountRaw(path, "local"); err != nil {
+			t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
+		}
+	}
+
+	for path, expectedError := range invalid {
+		if mp, err := parser.ParseMountRaw(path, "local"); err == nil {
+			t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
+		} else {
+			if !strings.Contains(err.Error(), expectedError) {
+				t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
+			}
+		}
+	}
+}
+
+func TestLinuxParseMountRawSplit(t *testing.T) {
+	previousProvider := currentFileInfoProvider
+	defer func() { currentFileInfoProvider = previousProvider }()
+	currentFileInfoProvider = mockFiProvider{}
+
+	cases := []struct {
+		bind      string
+		driver    string
+		expType   mount.Type
+		expDest   string
+		expSource string
+		expName   string
+		expDriver string
+		expRW     bool
+		fail      bool
+	}{
+		{"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false},
+		{"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false},
+		{"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false},
+		{"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true},
+		{"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false},
+		{"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false},
+		{"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false},
+		{"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false},
+		{"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true},
+	}
+
+	parser := &linuxParser{}
+	for i, c := range cases {
+		t.Logf("case %d", i)
+		m, err := parser.ParseMountRaw(c.bind, c.driver)
+		if c.fail {
+			if err == nil {
+				t.Errorf("Expected error, was nil, for spec %s\n", c.bind)
+			}
+			continue
+		}
+
+		if m == nil || err != nil {
+			t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err)
+			continue
+		}
+
+		if m.Destination != c.expDest {
+			t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
+		}
+
+		if m.Source != c.expSource {
+			t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind)
+		}
+
+		if m.Name != c.expName {
+			t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind)
+		}
+
+		if m.Driver != c.expDriver {
+			t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind)
+		}
+
+		if m.RW != c.expRW {
+			t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind)
+		}
+		if m.Type != c.expType {
+			t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind)
+		}
+	}
+}
+
+// TestLinuxParseMountSpecBindWithFileinfoError makes sure that the parser returns
+// the error produced by the fileinfo provider.
+//
+// Some extra context for the future in case of changes and possible wtf are we
+// testing this for:
+//
+// Currently this "fileInfoProvider" returns (bool, bool, error)
+// The 1st bool is "does this path exist"
+// The 2nd bool is "is this path a dir"
+// Then of course the error is an error.
+//
+// The issue is the parser was ignoring the error and only looking at the
+// "does this path exist" boolean, which is always false if there is an error.
+// Then the error returned to the caller was a (slightly, maybe) friendlier
+// error string than what comes from `os.Stat`
+// So ...the caller was always getting an error saying the path doesn't exist
+// even if it does exist but got some other error (like a permission error).
+// This is confusing to users.
+func TestLinuxParseMountSpecBindWithFileinfoError(t *testing.T) {
+	previousProvider := currentFileInfoProvider
+	defer func() { currentFileInfoProvider = previousProvider }()
+
+	testErr := fmt.Errorf("some crazy error")
+	currentFileInfoProvider = &mockFiProviderWithError{err: testErr}
+
+	parser := &linuxParser{}
+
+	_, err := parser.ParseMountSpec(mount.Mount{
+		Type:   mount.TypeBind,
+		Source: `/bananas`,
+		Target: `/bananas`,
+	})
+	assert.ErrorContains(t, err, testErr.Error())
+}
+
 func TestConvertTmpfsOptions(t *testing.T) {
 func TestConvertTmpfsOptions(t *testing.T) {
 	type testCase struct {
 	type testCase struct {
 		opt                  mount.TmpfsOptions
 		opt                  mount.TmpfsOptions

+ 9 - 401
volume/mounts/parser_test.go

@@ -1,22 +1,13 @@
 package mounts // import "github.com/docker/docker/volume/mounts"
 package mounts // import "github.com/docker/docker/volume/mounts"
 
 
 import (
 import (
-	"errors"
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
-	"runtime"
-	"strings"
 	"testing"
 	"testing"
 
 
 	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types/mount"
-	"gotest.tools/v3/assert"
 )
 )
 
 
-type parseMountRawTestSet struct {
-	valid   []string
-	invalid map[string]string
-}
-
 type mockFiProvider struct{}
 type mockFiProvider struct{}
 
 
 func (mockFiProvider) fileInfo(path string) (exists, isDir bool, err error) {
 func (mockFiProvider) fileInfo(path string) (exists, isDir bool, err error) {
@@ -41,363 +32,25 @@ func (mockFiProvider) fileInfo(path string) (exists, isDir bool, err error) {
 	return false, false, nil
 	return false, false, nil
 }
 }
 
 
-func TestParseMountRaw(t *testing.T) {
-
-	previousProvider := currentFileInfoProvider
-	defer func() { currentFileInfoProvider = previousProvider }()
-	currentFileInfoProvider = mockFiProvider{}
-	windowsSet := parseMountRawTestSet{
-		valid: []string{
-			`d:\`,
-			`d:`,
-			`d:\path`,
-			`d:\path with space`,
-			`c:\:d:\`,
-			`c:\windows\:d:`,
-			`c:\windows:d:\s p a c e`,
-			`c:\windows:d:\s p a c e:RW`,
-			`c:\program files:d:\s p a c e i n h o s t d i r`,
-			`0123456789name:d:`,
-			`MiXeDcAsEnAmE:d:`,
-			`name:D:`,
-			`name:D::rW`,
-			`name:D::RW`,
-			`name:D::RO`,
-			`c:/:d:/forward/slashes/are/good/too`,
-			`c:/:d:/including with/spaces:ro`,
-			`c:\Windows`,                // With capital
-			`c:\Program Files (x86)`,    // With capitals and brackets
-			`\\?\c:\windows\:d:`,        // Long path handling (source)
-			`c:\windows\:\\?\d:\`,       // Long path handling (target)
-			`\\.\pipe\foo:\\.\pipe\foo`, // named pipe
-			`//./pipe/foo://./pipe/foo`, // named pipe forward slashes
-		},
-		invalid: map[string]string{
-			``:                                 "invalid volume specification: ",
-			`.`:                                "invalid volume specification: ",
-			`..\`:                              "invalid volume specification: ",
-			`c:\:..\`:                          "invalid volume specification: ",
-			`c:\:d:\:xyzzy`:                    "invalid volume specification: ",
-			`c:`:                               "cannot be `c:`",
-			`c:\`:                              "cannot be `c:`",
-			`c:\notexist:d:`:                   `source path does not exist: c:\notexist`,
-			`c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`,
-			`name<:d:`:                         `invalid volume specification`,
-			`name>:d:`:                         `invalid volume specification`,
-			`name::d:`:                         `invalid volume specification`,
-			`name":d:`:                         `invalid volume specification`,
-			`name\:d:`:                         `invalid volume specification`,
-			`name*:d:`:                         `invalid volume specification`,
-			`name|:d:`:                         `invalid volume specification`,
-			`name?:d:`:                         `invalid volume specification`,
-			`name/:d:`:                         `invalid volume specification`,
-			`d:\pathandmode:rw`:                `invalid volume specification`,
-			`d:\pathandmode:ro`:                `invalid volume specification`,
-			`con:d:`:                           `cannot be a reserved word for Windows filenames`,
-			`PRN:d:`:                           `cannot be a reserved word for Windows filenames`,
-			`aUx:d:`:                           `cannot be a reserved word for Windows filenames`,
-			`nul:d:`:                           `cannot be a reserved word for Windows filenames`,
-			`com1:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`com2:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`com3:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`com4:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`com5:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`com6:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`com7:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`com8:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`com9:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt1:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt2:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt3:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt4:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt5:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt6:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt7:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt8:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt9:d:`:                          `cannot be a reserved word for Windows filenames`,
-			`c:\windows\system32\ntdll.dll`:    `Only directories can be mapped on this platform`,
-			`\\.\pipe\foo:c:\pipe`:             `'c:\pipe' is not a valid pipe path`,
-		},
-	}
-	lcowSet := parseMountRawTestSet{
-		valid: []string{
-			`/foo`,
-			`/foo/`,
-			`/foo bar`,
-			`c:\:/foo`,
-			`c:\windows\:/foo`,
-			`c:\windows:/s p a c e`,
-			`c:\windows:/s p a c e:RW`,
-			`c:\program files:/s p a c e i n h o s t d i r`,
-			`0123456789name:/foo`,
-			`MiXeDcAsEnAmE:/foo`,
-			`name:/foo`,
-			`name:/foo:rW`,
-			`name:/foo:RW`,
-			`name:/foo:RO`,
-			`c:/:/forward/slashes/are/good/too`,
-			`c:/:/including with/spaces:ro`,
-			`/Program Files (x86)`, // With capitals and brackets
-		},
-		invalid: map[string]string{
-			``:                                   "invalid volume specification: ",
-			`.`:                                  "invalid volume specification: ",
-			`c:`:                                 "invalid volume specification: ",
-			`c:\`:                                "invalid volume specification: ",
-			`../`:                                "invalid volume specification: ",
-			`c:\:../`:                            "invalid volume specification: ",
-			`c:\:/foo:xyzzy`:                     "invalid volume specification: ",
-			`/`:                                  "destination can't be '/'",
-			`/..`:                                "destination can't be '/'",
-			`c:\notexist:/foo`:                   `source path does not exist: c:\notexist`,
-			`c:\windows\system32\ntdll.dll:/foo`: `source path must be a directory`,
-			`name<:/foo`:                         `invalid volume specification`,
-			`name>:/foo`:                         `invalid volume specification`,
-			`name::/foo`:                         `invalid volume specification`,
-			`name":/foo`:                         `invalid volume specification`,
-			`name\:/foo`:                         `invalid volume specification`,
-			`name*:/foo`:                         `invalid volume specification`,
-			`name|:/foo`:                         `invalid volume specification`,
-			`name?:/foo`:                         `invalid volume specification`,
-			`name/:/foo`:                         `invalid volume specification`,
-			`/foo:rw`:                            `invalid volume specification`,
-			`/foo:ro`:                            `invalid volume specification`,
-			`con:/foo`:                           `cannot be a reserved word for Windows filenames`,
-			`PRN:/foo`:                           `cannot be a reserved word for Windows filenames`,
-			`aUx:/foo`:                           `cannot be a reserved word for Windows filenames`,
-			`nul:/foo`:                           `cannot be a reserved word for Windows filenames`,
-			`com1:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`com2:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`com3:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`com4:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`com5:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`com6:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`com7:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`com8:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`com9:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt1:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt2:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt3:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt4:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt5:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt6:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt7:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt8:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`lpt9:/foo`:                          `cannot be a reserved word for Windows filenames`,
-			`\\.\pipe\foo:/foo`:                  `Linux containers on Windows do not support named pipe mounts`,
-		},
-	}
-	linuxSet := parseMountRawTestSet{
-		valid: []string{
-			"/home",
-			"/home:/home",
-			"/home:/something/else",
-			"/with space",
-			"/home:/with space",
-			"relative:/absolute-path",
-			"hostPath:/containerPath:ro",
-			"/hostPath:/containerPath:rw",
-			"/rw:/ro",
-			"/hostPath:/containerPath:shared",
-			"/hostPath:/containerPath:rshared",
-			"/hostPath:/containerPath:slave",
-			"/hostPath:/containerPath:rslave",
-			"/hostPath:/containerPath:private",
-			"/hostPath:/containerPath:rprivate",
-			"/hostPath:/containerPath:ro,shared",
-			"/hostPath:/containerPath:ro,slave",
-			"/hostPath:/containerPath:ro,private",
-			"/hostPath:/containerPath:ro,z,shared",
-			"/hostPath:/containerPath:ro,Z,slave",
-			"/hostPath:/containerPath:Z,ro,slave",
-			"/hostPath:/containerPath:slave,Z,ro",
-			"/hostPath:/containerPath:Z,slave,ro",
-			"/hostPath:/containerPath:slave,ro,Z",
-			"/hostPath:/containerPath:rslave,ro,Z",
-			"/hostPath:/containerPath:ro,rshared,Z",
-			"/hostPath:/containerPath:ro,Z,rprivate",
-		},
-		invalid: map[string]string{
-			"":                                "invalid volume specification",
-			"./":                              "mount path must be absolute",
-			"../":                             "mount path must be absolute",
-			"/:../":                           "mount path must be absolute",
-			"/:path":                          "mount path must be absolute",
-			":":                               "invalid volume specification",
-			"/tmp:":                           "invalid volume specification",
-			":test":                           "invalid volume specification",
-			":/test":                          "invalid volume specification",
-			"tmp:":                            "invalid volume specification",
-			":test:":                          "invalid volume specification",
-			"::":                              "invalid volume specification",
-			":::":                             "invalid volume specification",
-			"/tmp:::":                         "invalid volume specification",
-			":/tmp::":                         "invalid volume specification",
-			"/path:rw":                        "invalid volume specification",
-			"/path:ro":                        "invalid volume specification",
-			"/rw:rw":                          "invalid volume specification",
-			"path:ro":                         "invalid volume specification",
-			"/path:/path:sw":                  `invalid mode`,
-			"/path:/path:rwz":                 `invalid mode`,
-			"/path:/path:ro,rshared,rslave":   `invalid mode`,
-			"/path:/path:ro,z,rshared,rslave": `invalid mode`,
-			"/path:shared":                    "invalid volume specification",
-			"/path:slave":                     "invalid volume specification",
-			"/path:private":                   "invalid volume specification",
-			"name:/absolute-path:shared":      "invalid volume specification",
-			"name:/absolute-path:rshared":     "invalid volume specification",
-			"name:/absolute-path:slave":       "invalid volume specification",
-			"name:/absolute-path:rslave":      "invalid volume specification",
-			"name:/absolute-path:private":     "invalid volume specification",
-			"name:/absolute-path:rprivate":    "invalid volume specification",
-		},
-	}
-
-	linParser := &linuxParser{}
-	winParser := &windowsParser{}
-	lcowParser := &lcowParser{}
-	tester := func(parser Parser, set parseMountRawTestSet) {
-		for _, path := range set.valid {
-			if _, err := parser.ParseMountRaw(path, "local"); err != nil {
-				t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
-			}
-		}
-
-		for path, expectedError := range set.invalid {
-			if mp, err := parser.ParseMountRaw(path, "local"); err == nil {
-				t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
-			} else {
-				if !strings.Contains(err.Error(), expectedError) {
-					t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
-				}
-			}
-		}
-	}
-
-	tester(linParser, linuxSet)
-	tester(winParser, windowsSet)
-	tester(lcowParser, lcowSet)
-}
-
-// testParseMountRaw is a structure used by TestParseMountRawSplit for
-// specifying test cases for the ParseMountRaw() function.
-type testParseMountRaw struct {
-	bind      string
-	driver    string
-	expType   mount.Type
-	expDest   string
-	expSource string
-	expName   string
-	expDriver string
-	expRW     bool
-	fail      bool
-}
-
-func TestParseMountRawSplit(t *testing.T) {
-	previousProvider := currentFileInfoProvider
-	defer func() { currentFileInfoProvider = previousProvider }()
-	currentFileInfoProvider = mockFiProvider{}
-	windowsCases := []testParseMountRaw{
-		{`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false},
-		{`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
-		{`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false},
-		{`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
-		{`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true},
-		{`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
-		{`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
-		{`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false},
-		{`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
-		{`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
-		{`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false},
-		{`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
-		{`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
-	}
-	lcowCases := []testParseMountRaw{
-		{`c:\:/foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false},
-		{`c:\:/foo:ro`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, false},
-		{`c:\:/foo:rw`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false},
-		{`c:\:/foo:foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, true},
-		{`name:/foo:rw`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false},
-		{`name:/foo`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false},
-		{`name:/foo:ro`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", false, false},
-		{`name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
-		{`driver/name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
-		{`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, true},
-		{`\\.\pipe\foo:/data`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
-		{`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
-	}
-	linuxCases := []testParseMountRaw{
-		{"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false},
-		{"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false},
-		{"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false},
-		{"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true},
-		{"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false},
-		{"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false},
-		{"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false},
-		{"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false},
-		{"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true},
-	}
-	linParser := &linuxParser{}
-	winParser := &windowsParser{}
-	lcowParser := &lcowParser{}
-	tester := func(parser Parser, cases []testParseMountRaw) {
-		for i, c := range cases {
-			t.Logf("case %d", i)
-			m, err := parser.ParseMountRaw(c.bind, c.driver)
-			if c.fail {
-				if err == nil {
-					t.Errorf("Expected error, was nil, for spec %s\n", c.bind)
-				}
-				continue
-			}
-
-			if m == nil || err != nil {
-				t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err)
-				continue
-			}
-
-			if m.Destination != c.expDest {
-				t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
-			}
-
-			if m.Source != c.expSource {
-				t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind)
-			}
-
-			if m.Name != c.expName {
-				t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind)
-			}
-
-			if m.Driver != c.expDriver {
-				t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind)
-			}
-
-			if m.RW != c.expRW {
-				t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind)
-			}
-			if m.Type != c.expType {
-				t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind)
-			}
-		}
-	}
+// always returns the configured error
+// this is used to test error handling
+type mockFiProviderWithError struct{ err error }
 
 
-	tester(linParser, linuxCases)
-	tester(winParser, windowsCases)
-	tester(lcowParser, lcowCases)
+func (m mockFiProviderWithError) fileInfo(path string) (bool, bool, error) {
+	return false, false, m.err
 }
 }
 
 
 func TestParseMountSpec(t *testing.T) {
 func TestParseMountSpec(t *testing.T) {
-	type c struct {
-		input    mount.Mount
-		expected MountPoint
-	}
 	testDir, err := ioutil.TempDir("", "test-mount-config")
 	testDir, err := ioutil.TempDir("", "test-mount-config")
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 	defer os.RemoveAll(testDir)
 	defer os.RemoveAll(testDir)
 	parser := NewParser()
 	parser := NewParser()
-	cases := []c{
+	cases := []struct {
+		input    mount.Mount
+		expected MountPoint
+	}{
 		{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
 		{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
 		{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true, Propagation: parser.DefaultPropagationMode()}},
 		{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true, Propagation: parser.DefaultPropagationMode()}},
 		{mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
 		{mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
@@ -437,48 +90,3 @@ func TestParseMountSpec(t *testing.T) {
 	}
 	}
 
 
 }
 }
-
-// always returns the configured error
-// this is used to test error handling
-type mockFiProviderWithError struct{ err error }
-
-func (m mockFiProviderWithError) fileInfo(path string) (bool, bool, error) {
-	return false, false, m.err
-}
-
-// TestParseMountSpecBindWithFileinfoError makes sure that the parser returns
-// the error produced by the fileinfo provider.
-//
-// Some extra context for the future in case of changes and possible wtf are we
-// testing this for:
-//
-// Currently this "fileInfoProvider" returns (bool, bool, error)
-// The 1st bool is "does this path exist"
-// The 2nd bool is "is this path a dir"
-// Then of course the error is an error.
-//
-// The issue is the parser was ignoring the error and only looking at the
-// "does this path exist" boolean, which is always false if there is an error.
-// Then the error returned to the caller was a (slightly, maybe) friendlier
-// error string than what comes from `os.Stat`
-// So ...the caller was always getting an error saying the path doesn't exist
-// even if it does exist but got some other error (like a permission error).
-// This is confusing to users.
-func TestParseMountSpecBindWithFileinfoError(t *testing.T) {
-	previousProvider := currentFileInfoProvider
-	defer func() { currentFileInfoProvider = previousProvider }()
-
-	testErr := errors.New("some crazy error")
-	currentFileInfoProvider = &mockFiProviderWithError{err: testErr}
-
-	p := "/bananas"
-	if runtime.GOOS == "windows" {
-		p = `c:\bananas`
-	}
-	m := mount.Mount{Type: mount.TypeBind, Source: p, Target: p}
-
-	parser := NewParser()
-
-	_, err := parser.ParseMountSpec(m)
-	assert.ErrorContains(t, err, "some crazy error")
-}

+ 214 - 0
volume/mounts/windows_parser_test.go

@@ -0,0 +1,214 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types/mount"
+	"gotest.tools/v3/assert"
+)
+
+func TestWindowsParseMountRaw(t *testing.T) {
+	previousProvider := currentFileInfoProvider
+	defer func() { currentFileInfoProvider = previousProvider }()
+	currentFileInfoProvider = mockFiProvider{}
+
+	valid := []string{
+		`d:\`,
+		`d:`,
+		`d:\path`,
+		`d:\path with space`,
+		`c:\:d:\`,
+		`c:\windows\:d:`,
+		`c:\windows:d:\s p a c e`,
+		`c:\windows:d:\s p a c e:RW`,
+		`c:\program files:d:\s p a c e i n h o s t d i r`,
+		`0123456789name:d:`,
+		`MiXeDcAsEnAmE:d:`,
+		`name:D:`,
+		`name:D::rW`,
+		`name:D::RW`,
+		`name:D::RO`,
+		`c:/:d:/forward/slashes/are/good/too`,
+		`c:/:d:/including with/spaces:ro`,
+		`c:\Windows`,                // With capital
+		`c:\Program Files (x86)`,    // With capitals and brackets
+		`\\?\c:\windows\:d:`,        // Long path handling (source)
+		`c:\windows\:\\?\d:\`,       // Long path handling (target)
+		`\\.\pipe\foo:\\.\pipe\foo`, // named pipe
+		`//./pipe/foo://./pipe/foo`, // named pipe forward slashes
+	}
+
+	invalid := map[string]string{
+		``:                                 "invalid volume specification: ",
+		`.`:                                "invalid volume specification: ",
+		`..\`:                              "invalid volume specification: ",
+		`c:\:..\`:                          "invalid volume specification: ",
+		`c:\:d:\:xyzzy`:                    "invalid volume specification: ",
+		`c:`:                               "cannot be `c:`",
+		`c:\`:                              "cannot be `c:`",
+		`c:\notexist:d:`:                   `source path does not exist: c:\notexist`,
+		`c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`,
+		`name<:d:`:                         `invalid volume specification`,
+		`name>:d:`:                         `invalid volume specification`,
+		`name::d:`:                         `invalid volume specification`,
+		`name":d:`:                         `invalid volume specification`,
+		`name\:d:`:                         `invalid volume specification`,
+		`name*:d:`:                         `invalid volume specification`,
+		`name|:d:`:                         `invalid volume specification`,
+		`name?:d:`:                         `invalid volume specification`,
+		`name/:d:`:                         `invalid volume specification`,
+		`d:\pathandmode:rw`:                `invalid volume specification`,
+		`d:\pathandmode:ro`:                `invalid volume specification`,
+		`con:d:`:                           `cannot be a reserved word for Windows filenames`,
+		`PRN:d:`:                           `cannot be a reserved word for Windows filenames`,
+		`aUx:d:`:                           `cannot be a reserved word for Windows filenames`,
+		`nul:d:`:                           `cannot be a reserved word for Windows filenames`,
+		`com1:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`com2:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`com3:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`com4:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`com5:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`com6:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`com7:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`com8:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`com9:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt1:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt2:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt3:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt4:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt5:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt6:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt7:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt8:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`lpt9:d:`:                          `cannot be a reserved word for Windows filenames`,
+		`c:\windows\system32\ntdll.dll`:    `Only directories can be mapped on this platform`,
+		`\\.\pipe\foo:c:\pipe`:             `'c:\pipe' is not a valid pipe path`,
+	}
+
+	parser := &windowsParser{}
+
+	for _, path := range valid {
+		if _, err := parser.ParseMountRaw(path, "local"); err != nil {
+			t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
+		}
+	}
+
+	for path, expectedError := range invalid {
+		if mp, err := parser.ParseMountRaw(path, "local"); err == nil {
+			t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
+		} else {
+			if !strings.Contains(err.Error(), expectedError) {
+				t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
+			}
+		}
+	}
+}
+
+func TestWindowsParseMountRawSplit(t *testing.T) {
+	previousProvider := currentFileInfoProvider
+	defer func() { currentFileInfoProvider = previousProvider }()
+	currentFileInfoProvider = mockFiProvider{}
+
+	cases := []struct {
+		bind      string
+		driver    string
+		expType   mount.Type
+		expDest   string
+		expSource string
+		expName   string
+		expDriver string
+		expRW     bool
+		fail      bool
+	}{
+		{`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false},
+		{`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
+		{`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false},
+		{`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
+		{`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true},
+		{`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
+		{`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
+		{`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false},
+		{`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
+		{`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
+		{`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false},
+		{`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
+		{`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
+	}
+
+	parser := &windowsParser{}
+	for i, c := range cases {
+		t.Logf("case %d", i)
+		m, err := parser.ParseMountRaw(c.bind, c.driver)
+		if c.fail {
+			if err == nil {
+				t.Errorf("Expected error, was nil, for spec %s\n", c.bind)
+			}
+			continue
+		}
+
+		if m == nil || err != nil {
+			t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err)
+			continue
+		}
+
+		if m.Destination != c.expDest {
+			t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
+		}
+
+		if m.Source != c.expSource {
+			t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind)
+		}
+
+		if m.Name != c.expName {
+			t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind)
+		}
+
+		if m.Driver != c.expDriver {
+			t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind)
+		}
+
+		if m.RW != c.expRW {
+			t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind)
+		}
+		if m.Type != c.expType {
+			t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind)
+		}
+	}
+}
+
+// TestWindowsParseMountSpecBindWithFileinfoError makes sure that the parser returns
+// the error produced by the fileinfo provider.
+//
+// Some extra context for the future in case of changes and possible wtf are we
+// testing this for:
+//
+// Currently this "fileInfoProvider" returns (bool, bool, error)
+// The 1st bool is "does this path exist"
+// The 2nd bool is "is this path a dir"
+// Then of course the error is an error.
+//
+// The issue is the parser was ignoring the error and only looking at the
+// "does this path exist" boolean, which is always false if there is an error.
+// Then the error returned to the caller was a (slightly, maybe) friendlier
+// error string than what comes from `os.Stat`
+// So ...the caller was always getting an error saying the path doesn't exist
+// even if it does exist but got some other error (like a permission error).
+// This is confusing to users.
+func TestWindowsParseMountSpecBindWithFileinfoError(t *testing.T) {
+	previousProvider := currentFileInfoProvider
+	defer func() { currentFileInfoProvider = previousProvider }()
+
+	testErr := fmt.Errorf("some crazy error")
+	currentFileInfoProvider = &mockFiProviderWithError{err: testErr}
+
+	parser := &windowsParser{}
+
+	_, err := parser.ParseMountSpec(mount.Mount{
+		Type:   mount.TypeBind,
+		Source: `c:\bananas`,
+		Target: `c:\bananas`,
+	})
+	assert.ErrorContains(t, err, testErr.Error())
+}